Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
6fdf6e5
feat: fix system message encryption and improve message processing
JohnEsleyer Mar 20, 2026
6ad33c8
fix: improve decryption error handling and response consistency
JohnEsleyer Mar 20, 2026
6c08db6
feat: add isRejected column and mark-seen API endpoint
JohnEsleyer Mar 20, 2026
642e34e
fix: encryption, system prompt, and parser message requirements
JohnEsleyer Mar 22, 2026
391be6f
fix: return actual API key values in settings endpoint
JohnEsleyer Mar 22, 2026
3f623fa
feat: implement agent-aware scheduler with <schedule> tag
JohnEsleyer Mar 22, 2026
c49658c
feat: CRON-like scheduling with action/deliver types, timezone fixes,…
JohnEsleyer Mar 23, 2026
7af2b22
feat: add session-based auth and takeover mode support
JohnEsleyer Mar 24, 2026
7ea38da
feat: add /api/agents/:id/history endpoint for conversation sync
JohnEsleyer Mar 24, 2026
f6ef30f
fix: filter empty/invalid responses and FILE metadata from history
JohnEsleyer Mar 24, 2026
5252c96
fix: use actual time for relative schedules instead of offset-adjuste…
JohnEsleyer Mar 24, 2026
41708f2
fix: remove time offset feature - use actual time for all operations
JohnEsleyer Mar 24, 2026
1f03952
chore: remove cookies.txt from git
JohnEsleyer Mar 24, 2026
5acd80b
fix: mask API keys in settings and remove time offset from dashboard
JohnEsleyer Mar 24, 2026
86e1362
chore: remove cookies.txt from git
JohnEsleyer Mar 24, 2026
64f890d
fix: process calendar/schedule tags even when message tag is missing
JohnEsleyer Mar 24, 2026
580558c
chore: remove cookies.txt
JohnEsleyer Mar 24, 2026
6aa28dd
fix: check for calendar tags in parsed response, add debug logging
JohnEsleyer Mar 24, 2026
30cdc4c
chore: remove cookies.txt
JohnEsleyer Mar 24, 2026
6f943f8
debug: add logging for calendar parsing
JohnEsleyer Mar 24, 2026
4a3698f
fix: include skills in context window endpoint
JohnEsleyer Mar 24, 2026
5821aef
fix: block calendar tags when responding to scheduled reminders
JohnEsleyer Mar 24, 2026
d032cc1
fix: default scheduled events to deliver type (no LLM call)
JohnEsleyer Mar 24, 2026
949269b
fix: UI shows only message content, history shows raw for debugging
JohnEsleyer Mar 24, 2026
bf6fbb3
fix: broadcast messages with roles - reminder, system formatting, cle…
JohnEsleyer Mar 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added 720p.mp4
Binary file not shown.
Binary file added assets/720p.mp4
Binary file not shown.
Binary file added assets/dog.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 4 additions & 4 deletions cmd/cli/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) {
Expand All @@ -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")
Expand Down Expand Up @@ -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")
}
Expand Down
92 changes: 25 additions & 67 deletions cmd/hermit/main.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
package main

import (
"errors"
"fmt"
"io"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
"time"

"github.com/JohnEsleyer/HermitShell/internal/api"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -298,25 +238,43 @@ 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
}
defer destFile.Close()

_, err = io.Copy(destFile, sourceFile)
if err == nil {
log.Printf("Updated %s from ./context.md", runtimeContextPath)
}
return err
}

Expand Down
92 changes: 88 additions & 4 deletions context.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,16 @@ 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.

- `<thought>...</thought>` internal short reasoning summary (never sent to user)
- `<message>...</message>` visible Telegram message bubble (**REQUIRED for all user-visible text**)
- `<terminal>...</terminal>` shell command to execute
- `<give>filename.ext</give>` deliver `/app/workspace/out/filename.ext`
- `<app name="appname">...</app>` publish `/app/workspace/apps/appname`
- `<skill>filename.md</skill>` request loading a skill file into context
- `<calendar><datetime>...</datetime><prompt>...</prompt></calendar>` schedule reminder/job (multiple allowed)
- `<schedule minutes="N" hours="H" days="D" type="action|deliver">reminder text</schedule>` schedule a reminder relative to now in your local time. Use `type="deliver"` for pre-written content.
- `<calendar type="action|deliver"><datetime>2026-03-13T15:00:00</datetime><prompt>text</prompt></calendar>` absolute datetime scheduling (interpreted as your local time)
- `<calendar action="list"/>` get all existing calendar events
- `<calendar action="delete" id="123"/>` delete a calendar event by ID
- `<calendar action="update" id="123"><prompt>new prompt</prompt></calendar>` update a calendar event
Expand Down Expand Up @@ -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
<message>Got it — I'll remind you in 3 minutes to workout and walk 3KM.</message>
<schedule minutes="3">Time to workout and walk 3KM!</schedule>
```

Alternative: user specifies absolute time:
User: "Remind me at 4:00 AM to workout and walk 3KM."

```xml
<message>Got it — I'll remind you at 4:00 AM.</message>
Expand All @@ -103,6 +112,81 @@ Expected pattern:
</calendar>
```

### Schedule tag examples

The `<schedule>` tag accepts any combination of relative time units. **All times are in your local timezone:**

```xml
<!-- In 3 minutes from now (local time) -->
<schedule minutes="3">Reminder text</schedule>

<!-- In 2 hours (local time) -->
<schedule hours="2">Reminder text</schedule>

<!-- Tomorrow at 9 AM (1 day + 9 hours from now, local time) -->
<schedule days="1" hours="9">Morning reminder</schedule>

<!-- In 30 minutes -->
<schedule minutes="30">30-minute reminder</schedule>
```

### CRON-like Scheduling: action vs deliver types

The `<schedule>` and `<calendar>` 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
<!-- ACTION: Agent will DO something at 3pm (generate report, write code, etc.) -->
<calendar type="action">
<datetime>2026-03-13T15:00:00</datetime>
<prompt>Generate and send the daily sales report</prompt>
</calendar>

<!-- DELIVER: Pre-written content will be delivered verbatim at 3pm -->
<calendar type="deliver">
<datetime>2026-03-13T15:00:00</datetime>
<prompt>Japanese Lesson: 「継続は力なり」(Keizoku wa chikara nari) — Perseverance is power.</prompt>
</calendar>

<!-- Same with schedule tag: -->
<schedule minutes="60" type="deliver">Your 1-hour reminder: Time for a break!</schedule>
```

### 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 `<calendar>` or `<schedule>` tags in your response - they won't work
- Simply respond naturally with a `<message>` 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
<message>Hey! Time to take a break. Step away from your screen for a few minutes!</message>
```
**WRONG responses that cause problems:**
```xml
<message>Got it!</message>
<schedule minutes="5">Time to take a break!</schedule> <!-- INFINITE LOOP - DON'T -->
```
```xml
<message>Sure, I'll remind you again!</message>
<calendar date="2026-03-23" time="07:21">Time to take a break!</calendar> <!-- DUPLICATE - DON'T -->
```

## Execution checkpoint model

The runtime uses `<end>` as a checkpoint to prevent re-executing old XML actions. **DO NOT include `<end>` in your responses** - the system will automatically append it when processing your output. Only actionable tags before `<end>` are considered active.
Expand Down
5 changes: 5 additions & 0 deletions cookies.txt
Original file line number Diff line number Diff line change
@@ -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
Loading