diff --git a/720p.mp4 b/720p.mp4
new file mode 100644
index 0000000..e8df241
Binary files /dev/null and b/720p.mp4 differ
diff --git a/assets/720p.mp4 b/assets/720p.mp4
new file mode 100644
index 0000000..e8df241
Binary files /dev/null and b/assets/720p.mp4 differ
diff --git a/assets/dog.jpg b/assets/dog.jpg
new file mode 100644
index 0000000..760f2dc
Binary files /dev/null and b/assets/dog.jpg differ
diff --git a/cmd/cli/main_test.go b/cmd/cli/main_test.go
index 889b3bd..518b291 100644
--- a/cmd/cli/main_test.go
+++ b/cmd/cli/main_test.go
@@ -26,7 +26,7 @@ func TestHandleTunnelWithHealthyTunnel(t *testing.T) {
defer func() { apiBase = oldAPIBase }()
tunnelCmd := newTunnelCmd()
- handleTunnel(tunnelCmd)
+ handleTunnel(nil, tunnelCmd)
}
func TestHandleTunnelWithUnhealthyTunnel(t *testing.T) {
@@ -44,7 +44,7 @@ func TestHandleTunnelWithUnhealthyTunnel(t *testing.T) {
defer func() { apiBase = oldAPIBase }()
tunnelCmd := newTunnelCmd()
- handleTunnel(tunnelCmd)
+ handleTunnel(nil, tunnelCmd)
}
func TestHandleTunnelNoTunnel(t *testing.T) {
@@ -69,7 +69,7 @@ func TestHandleTunnelNoTunnel(t *testing.T) {
}
defer func() { exitFunc = os.Exit }()
- handleTunnel(tunnelCmd)
+ handleTunnel(nil, tunnelCmd)
if !exited {
t.Error("Expected exit for no tunnel")
@@ -98,7 +98,7 @@ func TestHandleTunnelDomainMode(t *testing.T) {
}
defer func() { exitFunc = os.Exit }()
- handleTunnel(tunnelCmd)
+ handleTunnel(nil, tunnelCmd)
if !exited {
t.Error("Expected exit for domain mode")
}
diff --git a/cmd/hermit/main.go b/cmd/hermit/main.go
index 8696f88..54d6dab 100644
--- a/cmd/hermit/main.go
+++ b/cmd/hermit/main.go
@@ -1,14 +1,12 @@
package main
import (
- "errors"
"fmt"
"io"
"log"
"os"
"os/exec"
"path/filepath"
- "strings"
"time"
"github.com/JohnEsleyer/HermitShell/internal/api"
@@ -168,12 +166,10 @@ func main() {
}
// 5. Start Background Tasks
- // Reference: See docs/time-management.md
- // Start calendar scheduler
- go calendarScheduler(database)
+ // Note: Calendar scheduler is now started inside NewServer()
// 6. Start API Server
- // Reference: See docs/api-endpoints.md
+ // Reference: See docs/api-endpoints.md for how to create new endpoints.
apiServer := api.NewServer(database, nil, bot, llmClient, dockerClient, tunnelManager)
// Start Telegram polling for all existing agents with tokens
@@ -228,62 +224,6 @@ func cleanupStaleCloudflaredProcesses() {
}
}
-func calendarScheduler(database *db.DB) {
- log.Println("Calendar scheduler started")
- ticker := time.NewTicker(10 * time.Second)
- defer ticker.Stop()
-
- for range ticker.C {
- events, err := database.GetPendingCalendarEvents()
- if err != nil {
- continue
- }
-
- if len(events) > 0 {
- log.Printf("Calendar scheduler: found %d pending events", len(events))
- }
-
- currentTime := database.GetSystemTime()
- log.Printf("Calendar scheduler: current system time %s", currentTime.Format("2006-01-02 15:04:05"))
-
- for _, event := range events {
- // Parse event datetime (stored in local time)
- eventTime, err := time.Parse("2006-01-02 15:04", event.Date+" "+event.Time)
- if err != nil {
- log.Printf("Calendar scheduler: failed to parse event time: %v", err)
- continue
- }
-
- log.Printf("Calendar scheduler: checking event %d at %s vs system time %s", event.ID, eventTime.Format("15:04"), currentTime.Format("15:04"))
-
- // Check if event time has passed (compare system time with event time)
- if currentTime.After(eventTime) || currentTime.Equal(eventTime) {
- log.Printf("Calendar scheduler: triggering event %d", event.ID)
-
- // Get agent
- agent, err := database.GetAgent(event.AgentID)
- if err != nil {
- log.Printf("Calendar scheduler: failed to get agent: %v", err)
- continue
- }
-
- // Send reminder to user
- if agent.TelegramToken != "" {
- bot := telegram.NewBot(agent.TelegramToken)
- // Get allowed users
- allowedUsers := strings.Split(agent.AllowedUsers, ",")
- if len(allowedUsers) > 0 && allowedUsers[0] != "" {
- chatID := strings.TrimSpace(allowedUsers[0])
- log.Printf("Calendar scheduler: sending reminder to chat %s", chatID)
- bot.SendMessage(chatID, "š *Reminder*\n\n"+event.Prompt)
- database.MarkCalendarEventExecuted(event.ID)
- }
- }
- }
- }
- }
-}
-
func getEnv(key, defaultValue string) string {
if value, exists := os.LookupEnv(key); exists {
return value
@@ -298,18 +238,33 @@ func ensureRuntimeData(dataDir string) error {
}
runtimeContextPath := filepath.Join(skillsDir, "context.md")
- if _, err := os.Stat(runtimeContextPath); err == nil {
- return nil
- } else if !errors.Is(err, os.ErrNotExist) {
- return err
- }
+ // Always copy from source to ensure we have the latest version
sourceFile, err := os.Open("./context.md")
if err != nil {
+ // If source doesn't exist in current dir, check if runtime version already exists
+ if _, runtimeErr := os.Stat(runtimeContextPath); runtimeErr == nil {
+ log.Printf("Note: ./context.md not found, using existing %s", runtimeContextPath)
+ return nil
+ }
return err
}
defer sourceFile.Close()
+ // Get source file info for comparison
+ sourceInfo, err := sourceFile.Stat()
+ if err != nil {
+ return err
+ }
+
+ // Check if runtime version exists and is up-to-date
+ if runtimeInfo, err := os.Stat(runtimeContextPath); err == nil {
+ // If source is older or same as runtime, skip update
+ if !sourceInfo.ModTime().After(runtimeInfo.ModTime()) {
+ return nil
+ }
+ }
+
destFile, err := os.Create(runtimeContextPath)
if err != nil {
return err
@@ -317,6 +272,9 @@ func ensureRuntimeData(dataDir string) error {
defer destFile.Close()
_, err = io.Copy(destFile, sourceFile)
+ if err == nil {
+ log.Printf("Updated %s from ./context.md", runtimeContextPath)
+ }
return err
}
diff --git a/context.md b/context.md
index f8257cb..363c7ec 100644
--- a/context.md
+++ b/context.md
@@ -22,7 +22,7 @@ Be practical, reliable, and execution-oriented:
Use XML tags for machine actions. **Plain text outside tags is IGNORED by the runtime.**
-The current date and time are automatically injected into your context on every request, so you always know the current time when scheduling calendar events.
+The current date and time are automatically injected into your context on every request. **This time is in YOUR LOCAL timezone**, so when you schedule events, they will fire at the correct local time.
- `...` internal short reasoning summary (never sent to user)
- `...` visible Telegram message bubble (**REQUIRED for all user-visible text**)
@@ -30,7 +30,8 @@ The current date and time are automatically injected into your context on every
- `filename.ext` deliver `/app/workspace/out/filename.ext`
- `...` publish `/app/workspace/apps/appname`
- `filename.md` request loading a skill file into context
-- `......` schedule reminder/job (multiple allowed)
+- `reminder text` schedule a reminder relative to now in your local time. Use `type="deliver"` for pre-written content.
+- `2026-03-13T15:00:00text` absolute datetime scheduling (interpreted as your local time)
- `` get all existing calendar events
- `` delete a calendar event by ID
- `new prompt` update a calendar event
@@ -91,9 +92,17 @@ Expected pattern:
Because users are in Telegram, `GIVE` is the primary delivery path for documents/assets.
### Scenario: reminder request
-User: "Remind me at 4:00 AM to workout and walk 3KM."
+User: "Remind me in 3 minutes to workout and walk 3KM."
-Expected pattern:
+Preferred pattern (relative time - let the server calculate):
+
+```xml
+Got it ā I'll remind you in 3 minutes to workout and walk 3KM.
+Time to workout and walk 3KM!
+```
+
+Alternative: user specifies absolute time:
+User: "Remind me at 4:00 AM to workout and walk 3KM."
```xml
Got it ā I'll remind you at 4:00 AM.
@@ -103,6 +112,81 @@ Expected pattern:
```
+### Schedule tag examples
+
+The `` tag accepts any combination of relative time units. **All times are in your local timezone:**
+
+```xml
+
+Reminder text
+
+
+Reminder text
+
+
+Morning reminder
+
+
+30-minute reminder
+```
+
+### CRON-like Scheduling: action vs deliver types
+
+The `` and `` tags support a `type` attribute:
+
+- **type="deliver"** (default): When triggered, the system sends the content directly as an agent message (no LLM call)
+ - Use for: Reminders, scheduled lessons, prepared content, "Teach me Japanese at 3pm"
+ - The agent writes the content NOW, and it gets delivered verbatim at the scheduled time
+
+- **type="action"**: When triggered, the system calls the LLM to perform a task
+ - Use for: "Write code in 1 hour", "Analyze this data at 3pm", "Generate a report tomorrow"
+ - The LLM will generate fresh content based on context at trigger time
+
+```xml
+
+
+ 2026-03-13T15:00:00
+ Generate and send the daily sales report
+
+
+
+
+ 2026-03-13T15:00:00
+ Japanese Lesson: ćē¶ē¶ćÆåćŖćć(Keizoku wa chikara nari) ā Perseverance is power.
+
+
+
+Your 1-hour reminder: Time for a break!
+```
+
+### Handling scheduled reminders
+
+When a scheduled reminder fires, you will receive a message that starts with `[SCHEDULED_REMINDER]`.
+
+**IMPORTANT:** When you receive a `[SCHEDULED_REMINDER]`:
+- This is a notification that the reminder HAS ALREADY fired
+- The system will BLOCK any `` or `` tags in your response - they won't work
+- Simply respond naturally with a `` tag acknowledging the reminder
+- If you need to create NEW future reminders, do that in a SEPARATE message (not in response to this reminder)
+
+Example:
+```
+[SCHEDULED_REMINDER] Time to take a break!
+```
+Correct response:
+```xml
+Hey! Time to take a break. Step away from your screen for a few minutes!
+```
+**WRONG responses that cause problems:**
+```xml
+Got it!
+Time to take a break!
+```
+```xml
+Sure, I'll remind you again!
+Time to take a break!
+```
+
## Execution checkpoint model
The runtime uses `` as a checkpoint to prevent re-executing old XML actions. **DO NOT include `` in your responses** - the system will automatically append it when processing your output. Only actionable tags before `` are considered active.
diff --git a/cookies.txt b/cookies.txt
new file mode 100644
index 0000000..e75b559
--- /dev/null
+++ b/cookies.txt
@@ -0,0 +1,5 @@
+# Netscape HTTP Cookie File
+# https://curl.se/docs/http-cookies.html
+# This file was generated by libcurl! Edit at your own risk.
+
+#HttpOnly_127.0.0.1 FALSE / FALSE 0 session 053239bf03f0a769ee98380c10df292f026840b66cdefde11c59be8c60656194
diff --git a/dashboard/src/components/SettingsTab.tsx b/dashboard/src/components/SettingsTab.tsx
index 06486ca..fe0514e 100644
--- a/dashboard/src/components/SettingsTab.tsx
+++ b/dashboard/src/components/SettingsTab.tsx
@@ -14,8 +14,6 @@ export function SettingsTab({ triggerToast, onLogout }: SettingsTabProps) {
tunnelEnabled: true,
tunnelURL: '',
tunnelHealthy: false,
- timezone: 'UTC',
- timeOffset: '0',
hasLLMKey: false,
currentTime: '',
currentTime12: '',
@@ -38,17 +36,7 @@ export function SettingsTab({ triggerToast, onLogout }: SettingsTabProps) {
return () => clearInterval(interval);
}, []);
- // Calculate preview time based on selected offset.
- // Docs: See docs/time-management.md for preview calculation logic.
- // How it works: Converts local browser time to UTC, then adds offset to preview the result.
- const getPreviewTime = () => {
- // We use serverUtcTime (actual UTC) as the base for all calculations to avoid local clock drift
- const base = settings.serverUtcTime ? new Date(settings.serverUtcTime) : new Date();
- const offset = parseInt(settings.timeOffset || '0');
- // serverUtcTime is already UTC, so we just add the offset hours
- const preview = new Date(base.getTime() + (offset * 3600000));
- return preview.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', second: '2-digit', hour12: true });
- };
+
const [apiKeys, setApiKeys] = useState({
openrouterKey: '',
openaiKey: '',
@@ -176,8 +164,6 @@ export function SettingsTab({ triggerToast, onLogout }: SettingsTabProps) {
setSettings({
...data,
- timezone: data.timezone || 'UTC',
- timeOffset: data.timeOffset || '0',
currentTime: timeData.time || '',
currentTime12: timeData.time12 || '',
currentDate: timeData.date || '',
@@ -211,8 +197,6 @@ export function SettingsTab({ triggerToast, onLogout }: SettingsTabProps) {
try {
const payload: any = {
tunnelEnabled: settings.tunnelEnabled,
- timezone: settings.timezone,
- timeOffset: settings.timeOffset,
...specificKeys
};
@@ -401,143 +385,23 @@ export function SettingsTab({ triggerToast, onLogout }: SettingsTabProps) {
) : (