Compare commits

...

17 Commits

Author SHA1 Message Date
Nathan Esquenazi
90b5ad8d99 fix: strip webui metadata from messages before sending to LLM API (#67)
Some checks failed
Release & Docker / release (push) Has been cancelled
The webui stores display-only fields on messages (attachments, timestamp,
_ts) for UI rendering. These leaked into the conversation_history passed
to AIAgent.run_conversation(). Most providers ignore unknown fields, but
Z.AI/GLM tries to deserialize 'attachments' as its native ChatAttachments
type, causing HTTP 400 on every subsequent message after an image upload.

Fix: _sanitize_messages_for_api() creates a clean copy with only
API-standard keys (role, content, tool_calls, tool_call_id, name,
refusal) before passing to run_conversation(). Applied to both the
streaming path (streaming.py) and non-streaming path (routes.py).

Closes #66

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 22:13:12 -07:00
nesquena-hermes
57a4f573f6 docs: HERMES.md deep-dive, Why Hermes in README, screenshot layout
* docs: add HERMES.md deep-dive, Why Hermes section in README, and screenshot layout

- HERMES.md: full why-Hermes document -- assistant vs. agent mental
  model, three pillars (memory/scheduling/reach), four-category
  taxonomy of AI tools, per-tool comparison sections with tables
  (Claude Code, Codex CLI, OpenCode, Cursor/Copilot, Claude.ai),
  compounding advantage, who it's for, what it's not, quick reference
- README: hero screenshot stays full-width; two new UI screenshots in
  side-by-side HTML table with captions below
- README: new Why Hermes section with 6-bullet summary, comparison
  table, and link to HERMES.md
- README: HERMES.md added to Docs section
- docs/images/: two UI screenshots (workspace browser, sessions view)

* docs: fact-check and update all comparisons; add Open Interpreter section

Researched current state of each tool before updating:

Claude Code:
- Scheduled jobs: now Partial (has /loop session-scoped, cloud-managed
  /schedule via claude.ai/code, and desktop app automations); updated
  table to reflect this with footnotes distinguishing self-hosted cron
- Persistent memory: Partial (CLAUDE.md, MEMORY.md, rolling auto-memory
  but not full automatic cross-session recall)
- Provider-agnostic: No -- supports Bedrock/Vertex but Claude models only
- Web UI: Yes but Anthropic-hosted (not self-hosted)

Codex CLI:
- Persistent memory: Partial (session history + AGENTS.md since v0.100.0)
- Scheduled jobs: Partial (desktop app Automations only; CLI has no native
  scheduling as of early 2026, open feature request)
- Provider-agnostic: Yes (10+ providers)

OpenCode:
- Web UI: now Yes (embedded in binary + official desktop app)
- Persistent memory: Partial (SQLite sessions + AGENTS.md, not semantic)
- Messaging: community Telegram bot only, not first-party

Open Interpreter: added as new comparison section
- Most common 'why not just use this' question; addressed head-on
- Session-scoped, no persistent memory by their own docs, no scheduler,
  no messaging integration; powerful for one-shot tasks, not always-on

README Why Hermes table: updated to include Open Interpreter column,
fixed Claude Code self-hosted row (No -- scheduling runs on Anthropic
cloud), added footnotes for partial entries

* docs: add OpenClaw comparison; update category framework and quick reference table

OpenClaw (openclaw.ai, MIT, 347k stars) is the most direct Hermes
competitor -- both are open-source, self-hosted, always-on agents with
persistent memory, cron, and messaging integration. Added:

- Full OpenClaw section in HERMES.md with honest comparison: where it
  wins (15+ messaging platforms incl. iMessage/WeChat, native Chrome CDP
  browser control, voice wake words, ClawHub marketplace) and where
  Hermes differs (self-improving skills system, Python/ML ecosystem,
  web UI, multi-profile, sub-agent orchestration)
- Category 4 framework updated: now lists both Hermes and OpenClaw,
  with the key architectural distinction called out
- Quick reference table expanded to include OpenClaw column (now 8 tools)
- New rows added: self-improving skills, browser/computer control,
  Python/ML ecosystem
- README Why Hermes table updated: OpenClaw replaces OpenCode column,
  self-improving skills row replaces generic skills row, callout line
  at bottom addresses OpenClaw head-on

* docs: major accuracy pass -- OpenClaw deep-dive, Claude Code corrections, drop Open Interpreter

OpenClaw:
- Expanded comparison from a table to a full prose section with
  'Where OpenClaw wins' / 'Where Hermes wins' structure
- Honest about OpenClaw strengths: 15+ messaging platforms, native
  Chrome CDP browser control, voice wake words, 13k+ ClawHub skills
- Hermes advantages called out clearly: self-improving skills as a
  first-class automatic loop (vs marketplace-install model), stability
  (documented OpenClaw update regressions, Telegram breakage in early
  2026, WhatsApp protocol instability), security (156 CVEs and 1,184
  malicious skills found in ClawHub audit vs Hermes's no marketplace
  attack surface), Python/ML ecosystem, full web UI vs dashboard-only,
  and first-class multi-profile support
- Category 4 framework updated to name both Hermes and OpenClaw
- Table updated: added stability/security rows, corrected web UI row
  (OpenClaw has a gateway dashboard but not a full chat UI)

Claude Code corrections (researched against official docs at code.claude.com):
- Skills/Hooks: changed from No to Yes -- has a full Hooks system (13
  event types, 4 handler types) and a Plugin/Skills marketplace since
  v2.0.12; unified with slash commands in v2.1.0
- Messaging: changed from No to Partial -- Channels feature (Telegram,
  Discord, iMessage, Webhooks) in research preview since v2.1.80; deep
  Slack integration that triggers cloud sessions and creates PRs
- Added Claude Cowork row: separate product with 38+ connectors
  (Slack, Gmail, Teams, Notion, Jira, Salesforce, etc.)
- Scheduling footnote updated: cloud-managed has 1-hour minimum interval
- Provider-agnostic clarified: routes through Bedrock/Vertex but always
  Claude models; cannot swap to GPT or Gemini

Open Interpreter removed:
- Less relevant comparison than OpenClaw for the 'always-on agent' frame
- Kept coverage focused on the tools people actually compare Hermes to

Quick reference table:
- Now 7 tools wide (added OpenClaw, kept Claude Code, Codex, OpenCode,
  Cursor, Claude.ai, Hermes)
- New rows: self-improving skills, browser/computer control, stability
- Updated: Claude Code messaging to Partial, OpenClaw web UI to
  'Dashboard only', skills rows differentiated by type

* docs: apply full editorial pass from hermes-edit-list.md

Writing patterns fixed:
- Em dashes reduced by ~80%; replaced with commas, periods, parens
- All 'Not X, it's Y' negative parallelism rewritten as positive
  statements; 'What Hermes Is Not' section renamed 'Scope and Limits'
  and reframed positively throughout
- 'It compounds.' standalone flourish removed
- 'meaningfully' removed everywhere (was appearing 3+ times)
- 'leverages' -> 'uses' in README
- 'remembers everything' softened to 'retains context across sessions'
- Bolded Hermes column in Quick Reference table un-bolded (only genuine
  differentiator cells kept bold: self-improving skills, always-on,
  orchestrates other agents)
- 'The honest summary' framing removed from OpenClaw section
- 'Hermes is different.' cliche transition cut from README
- Rule-of-three slogans trimmed (e.g. 'Same agent, same memory...')
- 'tired of re-explaining' -> 'don't want to re-explaining'

Duplicate content removed:
- 'day one / day one hundred' comparison kept only in Compounding
  Advantage section; removed from Pillar 1

Factual accuracy fixes:
- Claude.ai comparison updated: memory now auto-generated from history
  (not just user-curated); code execution and file read/write noted
  as sandboxed (Artifacts), not flat No
- Category 2: Windsurf framed as 'earliest' on memory, Copilot
  'catching up'; removed overconfident 'most mature' claim
- Category 4 qualifier: 'as of early 2026' added
- '1-hour minimum' for Claude Code cloud scheduling softened to
  'minimum interval applies' (specific claim unverified)
- Claude Code scheduling table note: 'cloud or desktop-app only'
  (was just 'cloud-managed or session-scoped')
- README claim 'No other open-source tool combines...' removed;
  was false because OpenClaw does combine all three
- OpenClaw self-improving skills: 'No' -> 'Partial' with clarification
- README OpenClaw callout: 'relies on a marketplace' softened to
  'skill system centers on a community marketplace'
- 'meaningfully more stable' -> 'more stable'; 'supply chain issues'
  -> 'security incidents involving malicious skills'
- OpenClaw star count: '347k+' -> '~347k' (moving fast)
- Stability row added to OpenClaw table; bold removed from table

---------

Co-authored-by: Hermes <hermes@localhost>
2026-04-03 22:03:19 -07:00
Nathan Esquenazi
c1320b4712 Merge pull request #62 from nesquena/docs/v0.30.1-release
Some checks failed
Release & Docker / release (push) Has been cancelled
docs: v0.30.1 release — CLI bridge fixes + README update
2026-04-03 21:12:15 -07:00
Nathan Esquenazi
d3b693524f docs: v0.30.1 release — CLI bridge fixes, README update
CHANGELOG: add v0.30.1 entry covering PRs #57-#61 (CLI session bridge
fixes: sidebar rendering, profile-aware state.db path, silent SQL error,
show/hide toggle in Settings.

README: add CLI session bridge, token/cost display, subagent cards,
/usage command, skills linked files, show CLI sessions toggle.

Version label: v0.30 -> v0.30.1 in index.html, SPRINTS, CHANGELOG footer.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
EOF
)
2026-04-03 21:11:52 -07:00
nesquena-hermes
66f95e08c2 feat: 'Show CLI sessions' toggle in Settings (#61)
Adds a server-side boolean setting (default: false) that controls whether
CLI sessions from state.db appear in the sidebar. Off by default so the
sidebar is clean until the user explicitly opts in.

- api/config.py: add show_cli_sessions to _SETTINGS_DEFAULTS and _SETTINGS_BOOL_KEYS
- api/routes.py: gate get_cli_sessions() call on the setting at request time
- static/index.html: checkbox in settings panel with description
- static/panels.js: load/save checkbox, refresh session list on save
- static/boot.js: load on startup alongside send_key and show_token_usage

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-03 21:06:23 -07:00
nesquena-hermes
1a4d56c215 fix: CLI DB has no profile column, and silent SQL error swallowed results (#60)
The sessions table in the CLI state.db does not have a 'profile' column --
selecting s.profile caused an OperationalError which was silently caught by
'except Exception: return []', making get_cli_sessions() always return empty.

Fix: remove s.profile from the SELECT (it doesn't exist in the CLI schema)
and derive the profile from get_active_profile_name() instead, which is the
right value anyway since the CLI DB has no profile concept.

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-03 21:02:01 -07:00
nesquena-hermes
b2c2f32584 fix: CLI session bridge reads active profile's state.db not server launch profile (#59)
get_cli_sessions() and get_cli_session_messages() were using HERMES_HOME
(the profile the server was launched under) to find state.db. This meant
a server launched under the webui profile would read webui's state.db
(full of cron runs) instead of the user's actual CLI sessions.

Fix: use get_active_hermes_home() which tracks whichever profile the user
has selected in the UI. This means:
  - default profile active -> reads ~/.hermes/state.db (interactive CLI)
  - camanji profile active -> reads ~/.hermes/profiles/camanji/state.db

Falls back to HERMES_HOME env var if profiles module unavailable.

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-03 21:00:04 -07:00
nesquena-hermes
15fde033c3 fix: wire up CLI session display in sidebar (3 frontend gaps) (#58)
The backend CLI session bridge (PR #56) was complete but the frontend
never connected to it:

1. css class never applied -- el.className never included 'cli-session'
   so the gold border and 'cli' badge CSS was dead code. Fixed: append
   ' cli-session' when s.is_cli_session is true.

2. import never triggered -- click handler always called loadSession()
   directly, never POST /api/session/import_cli. Fixed: for CLI sessions,
   call import_cli first (idempotent -- safe to call on every click),
   then fall through to loadSession() which now finds the imported copy.

3. profile filter silently hid CLI sessions -- filter required
   s.profile === S.activeProfile, but CLI sessions may have profile=null
   if the SQLite DB has no profile column. Fixed: CLI sessions always
   pass the filter (s.is_cli_session || s.profile === S.activeProfile).

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-03 20:55:29 -07:00
nesquena-hermes
10a1e57c9b docs: fix test count in v0.30 CHANGELOG (424 not 426) (#57)
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-03 20:44:58 -07:00
Nathan Esquenazi
4f10080501 Merge pull request #56 from thadreber-web/feat/cli-session-bridge
Some checks failed
Release & Docker / release (push) Has been cancelled
feat: CLI session bridge - read CLI sessions from agent SQLite store
2026-04-03 20:42:21 -07:00
Nathan Esquenazi
f8ea02c14d merge: resolve conflicts with master (v0.29), bump to v0.30
Resolved CHANGELOG.md and SPRINTS.md conflicts: master added v0.29
(Sprint 23: Agentic Transparency), CLI bridge becomes v0.30.
Updated all version references to v0.30.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 20:42:11 -07:00
Nathan Esquenazi
122fe955b6 docs: v0.29 release notes for CLI session bridge, version bump
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 20:39:27 -07:00
Nathan Esquenazi
017d7f1eca fix: add missing HOME import to models.py (NameError crash)
get_cli_sessions() and get_cli_session_messages() reference HOME but
it was not imported from api.config. This caused /api/sessions to 500
on every request, breaking the entire session list.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 20:37:34 -07:00
Thad Reber
cabda6b77a feat: CLI session bridge - read CLI sessions from agent SQLite store
Read CLI sessions from the agent's state.db and surface them in the
WebUI sidebar alongside local sessions, with read-only display and
import-on-click to avoid data duplication.

Key changes:
- get_cli_sessions(): reads sessions list via parameterized SQL,
  wrapped in sqlite3 context manager (no connection leaks)
- get_cli_session_messages(): reads messages for a CLI session
  via parameterized SQL, also context-managed
- GET /api/sessions: merges WebUI + CLI sessions with dedup
  (WebUI takes priority on same session_id)
- GET /api/session: falls back to CLI store if not a WebUI session
- POST /api/session/import_cli: imports a CLI session into the
  WebUI store (idempotent, no duplicates on re-import)
- Imported sessions use get_last_workspace() for the workspace field
  (not a hardcoded string) and carry the active profile tag
- CSS: .cli-session with ::after 'cli' indicator (no theme changes)

Fixes review feedback:
- SQLite connections use 'with' context managers (no leaks)
- Workspace uses real path via get_last_workspace()
- Profile awareness via api.profiles.get_active_profile_name()
- Parameterized SQL queries throughout (no injection risk)
- Graceful fallback when sqlite3 or state.db is missing
2026-04-03 19:54:54 -07:00
nesquena-hermes
33fca2383c docs: v0.29 release notes + roadmap/sprint plan updates
- CHANGELOG.md: add v0.29 entry covering all Sprint 23 deliverables
  (token/cost display, subagent cards, skill picker, linked files viewer,
  workspace tree persistence, timestamp fixes, XSS + security fixes)
- ROADMAP.md: update to v0.29, add Sprint 23 to history table, check off
  token/cost, skill linked files, skill picker in cron (3 items closed)
- TESTING.md: update automated test count 415 -> 424
- SPRINTS.md: add Sprint 24 (web polish bug fix pass) and Sprint 25
  (macOS native desktop app) forward plans; remove stale stub entries

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-03 19:36:18 -07:00
Nathan Esquenazi
be951a4d1d Merge pull request #54 from nesquena/fix/stricter-docker-regex
fix(ci): stricter semver regex for Docker tags
2026-04-03 19:33:14 -07:00
Nathan Esquenazi
bba9a236c3 fix(ci): use stricter semver regex for Docker tag extraction
Use v(\d+\.\d+(?:\.\d+)?) instead of v(.*) to only match real
version numbers (v0.29, v0.29.1), not arbitrary strings.
Keep latest unconditional since all tag pushes are releases.

Based on review of PR #52 approach vs #53.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 19:32:50 -07:00
18 changed files with 1166 additions and 44 deletions

View File

@@ -33,14 +33,14 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Extract semver tags: e.g. v0.28 -> 0.28, latest
# Extract tags from the git ref (supports vX.Y and vX.Y.Z formats)
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=match,pattern=v(.*),group=1
type=match,pattern=v(\d+\.\d+(?:\.\d+)?),group=1
type=raw,value=latest
# Build and push multi-arch image (amd64 + arm64)

View File

@@ -5,6 +5,157 @@
---
## [v0.30.1] CLI Session Bridge Fixes
*April 4, 2026 | 424 tests*
### Bug Fixes
- **CLI sessions not appearing in sidebar.** Three frontend gaps: `sessions.js`
wasn't rendering CLI sessions (missing `is_cli_session` check in render loop),
sidebar click handler didn't trigger import, and the "cli" badge CSS selector
wasn't matching the rendered DOM structure. (#58)
- **CLI bridge read wrong profile's state.db.** `get_cli_sessions()` resolved
`HERMES_HOME` at server launch time, not at call time. After a profile switch,
it kept reading the original profile's database. Now resolves dynamically via
`get_active_hermes_home()`. (#59)
- **Silent SQL error swallowed all CLI sessions.** The `sessions` table in
`state.db` has no `profile` column — the query referenced `s.profile` which
caused a silent `OperationalError`. The `except Exception: return []` handler
swallowed it, returning zero CLI sessions. Removed the column reference and
added explicit column-existence checks. (#60)
### Features
- **"Show CLI sessions" toggle in Settings.** New checkbox in the Settings panel
to show/hide CLI sessions in the sidebar. Persisted server-side in
`settings.json` (`show_cli_sessions`, default `true`). When disabled, CLI
sessions are excluded from `/api/sessions` responses. (#61)
---
## [v0.30] CLI Session Bridge (Community: @thadreber-web)
*April 4, 2026 | 424 tests*
### Features
- **CLI session bridge.** The WebUI now reads sessions from the hermes-agent's
SQLite store (`state.db`). CLI sessions appear in the sidebar with a gold
"cli" indicator badge. Click to import into the WebUI store with full message
history — replies then work through the normal agent pipeline.
- **`/api/session/import_cli` endpoint.** Imports a CLI session into the WebUI
JSON store. Idempotent — returns existing session if already imported.
Derives title from first message, inherits active profile and workspace.
- **`/api/sessions` merges CLI sessions.** Sidebar shows both WebUI and CLI
sessions sorted by last activity. Deduplication ensures WebUI sessions take
priority when the same session_id exists in both stores.
- **CLI session fallback on `/api/session`.** If a session_id isn't found in
the WebUI store, falls back to reading from the CLI SQLite store.
### Architecture
- `api/models.py`: `get_cli_sessions()`, `get_cli_session_messages()`,
`import_cli_session()`. All use parameterized SQL queries and `with` for
connection management. Graceful fallback on missing sqlite3 or state.db.
- `api/routes.py`: CLI fallback in GET `/api/session`, merged list in
GET `/api/sessions`, POST `/api/session/import_cli`.
- `static/style.css`: `.cli-session` indicator styles (gold border + badge).
---
## [v0.29] Sprint 23: Agentic Transparency + Polish
*April 4, 2026 | 424 tests*
### Features
- **Token/cost display.** Agent usage (input tokens, output tokens, estimated
cost) is now read after each conversation and persisted on the session.
A muted badge appears below the last assistant message when enabled.
Off by default — toggle via the Settings panel checkbox or `/usage` slash
command. Persists server-side across refreshes.
- **Subagent delegation cards.** `subagent_progress` events now render with
a 🔀 icon and a blue indented left border to visually distinguish child
tool activity from parent tool calls. `delegate_task` cards display as
"Delegate task" with cleaner formatting.
- **Skill picker in cron create form.** The "New Job" form now has a search
input + tag chip picker for attaching skills to cron jobs. Skills fetched
from `/api/skills`, filtered on keyup, added/removed as tag chips.
`submitCronCreate()` sends `skills` array in the POST body. Backend already
supported the field — this was a pure frontend gap.
- **Skill linked files viewer.** Skill preview panel now renders a "Linked
Files" section below SKILL.md content when a skill has `references/`,
`templates/`, `scripts/`, or `assets/` subdirectories. Clicking a file
loads it in the preview panel with syntax highlighting.
New `file` query param on `GET /api/skills/content` serves linked files
with path traversal protection.
- **Workspace tree state persists across refreshes.** Expanded directory
paths are saved to `localStorage` keyed by workspace path
(`hermes-webui-expanded:{path}`). On every root load (page refresh,
session switch), the saved state is restored and previously-expanded
directories are pre-fetched so the tree renders fully on first paint.
- **Timestamps fixed.** `api/streaming.py` now stamps `timestamp` on every
message that lacks one at conversation completion. The `done` SSE event
also stamps `_ts` on the last assistant message immediately. Timestamps
were already rendered in the UI (Sprint 14, hover-to-reveal) but most
messages had no timestamp field, so nothing ever showed.
- **`/usage` slash command.** Instant toggle for token usage display.
Shows a toast, persists to server, updates the Settings checkbox if open,
re-renders immediately.
### Bug Fixes
- **XSS via inline onclick + esc().** Skill names and file paths embedded in
`onclick` HTML attributes used `esc()` for encoding. `esc()` converts `'`
to `&#39;` (HTML-safe) but browsers decode it back before executing JS,
allowing skill names with apostrophes to break out of string literals.
Fixed by switching to `data-*` attributes + `addEventListener`.
- **rglob wildcard injection.** The `name` query param for
`/api/skills/content?file=` was passed directly to `SKILLS_DIR.rglob()`,
which accepts glob patterns. `name=*` would match an arbitrary directory
and use it as the trust base for path traversal checking.
Fixed by rejecting names containing `* ? [ ]` metacharacters with 400.
- **`_fmtTokens(null)` returned "null".** `String(null)` = `"null"` would
appear in the usage badge for sessions missing fields. Fixed with a
`!n || n < 0` guard returning `'0'`.
- **Usage badge on wrong row.** Badge used `:last-child` which could target
a user message row. Fixed by adding `data-role` to message rows and
scanning backwards for the last `assistant` row.
- **Tool name resolution.** Tool call entries in session JSON sometimes
stored the literal string `"tool"` as the name when the call ID couldn't
be resolved. Fixed: defaults to empty string and skips unresolvable entries.
- **Inline import inside loop.** `import json as _j2` inside the done-handler
loop in `streaming.py` moved to module-level.
### Session Model
- Added `input_tokens`, `output_tokens`, `estimated_cost` fields to Session
(defaults: 0, 0, None). Included in `compact()`, session JSON, and all
API responses. Backward-compatible via `**kwargs`.
- Added `args` capture to `tool_calls` session JSON entries (truncated
snapshot of tool inputs, up to 6 keys / 120 chars each).
### Settings
- New `show_token_usage` boolean setting (default: `false`). Stored in
`settings.json`, loaded on boot alongside `send_key`.
### Tests
- Renamed `test_sprint24.py``test_sprint23.py`.
- Strengthened session usage assertions (explicit field presence checks).
- Added: path traversal rejection test, wildcard name rejection test,
cron create with skills array test.
- Total: 424 tests (up from 415).
---
## [v0.28.1] CI Pipeline + Multi-Arch Docker Builds
*April 3, 2026 | 426 tests*
@@ -917,4 +1068,4 @@ Three-panel layout: sessions sidebar, chat area, workspace panel.
---
*Last updated: v0.28.1, April 3, 2026 | Tests: 426*
*Last updated: v0.30.1, April 4, 2026 | Tests: 424*

375
HERMES.md Normal file
View File

@@ -0,0 +1,375 @@
# Why Hermes
Hermes is a persistent, autonomous AI agent that lives on your server. It remembers everything,
schedules work while you sleep, and gets more capable the longer it runs. This document explains
the mental model, why that matters, and how Hermes compares to every major AI tool available today.
---
## The Core Idea: Assistants Forget. Agents Don't.
Every time you open Claude Code, Codex, or a chat window, the tool starts from zero. It does not
know who you are, what you worked on yesterday, how your repo is structured, or what bugs you
already fixed. You re-explain yourself every single session. The tool is powerful in the moment
and useless the next day.
Hermes fills that gap. It runs on your server, retains context across every session, and acts
on your behalf whether or not you are at a keyboard.
```
Assistant model: You -> [Tool] -> Answer -> Done
(tool forgets everything when the window closes)
Agent model: You <-> [Hermes] <-> (memory, skills, schedule, tools)
(persistent, learns your stack, acts on your behalf, runs while you're offline)
```
---
## The Three Pillars
### 1. Memory That Compounds
Hermes has layered memory that survives every session, every reboot, every model swap:
- **User profile** -- who you are, your preferences, your communication style, things you've
corrected Hermes on
- **Agent memory** -- facts about your environment, your toolchain, your project conventions
- **Skills** -- reusable procedures Hermes discovers and saves; it never has to relearn how to
deploy your app, run your tests, or review a PR
- **Session history** -- every past conversation is searchable; Hermes can recall what you
worked on last Tuesday
When you correct Hermes, it remembers. When it solves a tricky problem, it saves the approach.
When it learns your stack, that knowledge carries into every future session.
### 2. Autonomous Scheduling
Hermes can run jobs without you present -- every hour, every morning, on any cron schedule.
It fires up a fresh session, runs the task, and delivers the result to wherever you want it:
Telegram, Discord, Slack, Signal, WhatsApp, SMS, email, and more.
Things Hermes can do while you sleep:
- Review new pull requests on your GitHub repo and post a full verdict comment
- Send you a morning briefing of news, markets, or anything else you care about
- Run your test suite and alert you if something breaks
- Watch a competitor's blog for new posts and summarize them
- Monitor a datasource and notify you when a threshold is crossed
### 3. Reach It From Anywhere
Hermes runs on your server and is reachable from every surface: terminal over SSH, the web UI
(this project), and messaging apps including Telegram, Discord, Slack, WhatsApp, Signal, and
Matrix. Start a task from your phone, check it from the browser on your laptop, continue it in
a terminal on a remote server. The same agent, memory, and history follow you everywhere.
---
## A Framework for AI Tools
There are four distinct categories of AI tool. Understanding the category tells you what a tool
can and cannot do.
### Category 1: Chat Assistants
*Claude.ai, ChatGPT, Gemini*
You open a window, ask something, get an answer. No persistent memory beyond the conversation,
no ability to run code or touch files, no way to act on your behalf. Excellent for Q&A,
drafting, and brainstorming. You re-explain your context every session.
### Category 2: IDE Integrations
*GitHub Copilot, Cursor, Windsurf, Zed AI*
Deep inside your editor. Autocomplete, inline diffs, refactors -- all excellent. Windsurf was
earliest with workspace-scoped memory (Cascade Memories); Copilot has been shipping repo-level
memory since late 2025 and is catching up. Cursor has no native memory as of early 2026. None
have scheduling or messaging access. Tied to one machine and one editor.
### Category 3: Agentic CLI Tools
*Claude Code, Codex CLI, OpenCode, Aider*
The current frontier for most developers. Can use real tools -- run shell commands, read and
write files, search the web, call APIs. Great for deep, multi-step tasks in a single terminal
session. All are adding memory and scheduling features to varying degrees (see comparisons below),
but the core model is still session-scoped: you invoke it, it works, it stops.
### Category 4: Persistent Autonomous Agents
*Hermes, OpenClaw (as of early 2026)*
All the tool use of Category 3, plus memory that accumulates across sessions, plus always-on
scheduling, plus multi-modal access from any device or messaging app. Gets more useful over time
rather than resetting to zero. Hermes and OpenClaw are the two primary open-source, self-hosted
tools in this category. OpenClaw is a gateway-centric automation platform; Hermes is a
self-improving agent that writes and reuses its own procedures from experience.
---
## How Hermes Compares
### vs. OpenClaw
OpenClaw is the most direct comparison to Hermes and the question most people ask first.
Both are open-source, self-hosted, always-on agents with persistent memory, cron scheduling,
and messaging app integration. If you're evaluating Hermes, you should evaluate OpenClaw too.
OpenClaw (MIT, ~347k GitHub stars) is built around a **Gateway** control plane written in
Node.js/TypeScript. It excels at broad personal automation: native Chrome/Chromium control for
browser automation, the widest messaging platform support in the space (WhatsApp, Telegram,
Signal, iMessage, LINE, WeChat, Slack, Discord, Teams, Matrix, and more), voice wake words,
and a ClawHub skill marketplace where users share pre-built automations. The community is large
and the ecosystem is growing fast.
Hermes takes a different approach. It is built in Python and centers on a **self-improving
agent loop** rather than a gateway control plane. The core difference is in how skills work:
OpenClaw skills are primarily human-authored plugins installed from a marketplace; Hermes
**writes and saves its own skills automatically** as part of every session. When Hermes solves
a problem a new way, it saves the procedure and reuses it going forward without any user effort.
Beyond the skills architecture, there are two other practical differences worth knowing:
**Stability.** OpenClaw's community forums and GitHub issues document a recurring pattern of
update-breaking regressions -- for example, Telegram integration was broken across multiple
releases in early 2026. The unofficial WhatsApp Web protocol OpenClaw uses is known to
disconnect and requires periodic re-pairing (this is documented in OpenClaw's own FAQ).
Hermes has had no equivalent release breakages.
**Security.** ClawHub's open publishing model has been exploited repeatedly. A community audit
identified over a thousand malicious skills in the marketplace including prompt injections and
tool-poisoning payloads; the community-maintained awesome-openclaw-skills list tracks confirmed
removals and flags known bad actors. Hermes has no third-party marketplace and a correspondingly
smaller attack surface.
**OpenClaw's genuine strengths** are worth stating plainly: it has broader messaging coverage
(iMessage, LINE, WeChat, Teams -- platforms Hermes does not support), native browser and
computer control via Chrome CDP, voice wake words on macOS and iOS, a larger community, and
more third-party integrations than Hermes. If those capabilities matter most to you, OpenClaw
is worth a serious look.
Where Hermes is the better fit: you want an agent that self-improves from experience without
manual plugin authoring, you work in Python and want access to the ML/data science ecosystem,
you want a stable deployment that does not break between updates, or you want a full web chat
UI rather than a monitoring dashboard.
| | OpenClaw | Hermes |
|---|---|---|
| Persistent memory | Yes | Yes |
| Scheduled jobs (cron) | Yes | Yes |
| Messaging app access | Yes (15+ platforms, incl. iMessage/WeChat) | Yes (10+ platforms) |
| Web UI | Gateway dashboard (monitoring only) | Full three-panel chat UI |
| Self-hosted | Yes | Yes |
| Open source | Yes (MIT) | Yes |
| Self-improving skills | Partial (AI can generate skills; not the default loop) | Yes (automatic, first-class) |
| Browser / computer control | Yes (native Chrome CDP) | Via shell / tools |
| Voice wake words | Yes (macOS/iOS) | No |
| Python / ML ecosystem | No (Node.js) | Yes |
| Orchestrates Claude Code / Codex | No | Yes |
| Multi-profile support | Via binding-rule routing | Yes (first-class named profiles) |
| Provider-agnostic | Yes | Yes |
| Update reliability | Moderate (documented regressions) | High |
### vs. Claude Code (Anthropic)
Claude Code is Anthropic's official agentic CLI and one of the best tools in Category 3.
In a single focused session it is capable -- deep code understanding, shell access, file
editing, multi-step reasoning.
Claude Code has been adding features rapidly and the gap is narrowing:
- **Hooks system** -- 13 event types (SessionStart, PreToolUse, PostToolUse, Stop, etc.) with
4 handler types (shell command, HTTP endpoint, LLM prompt, sub-agent); deterministic
non-LLM control over the agent lifecycle
- **Plugins / Skills** -- installable via `/plugin install`, hot-reloaded from `~/.claude/skills`,
with a marketplace; skills and slash commands unified as of v2.1.0
- **Scheduling** -- `/loop` (session-scoped), cloud-managed cron via `claude.ai/code/scheduled`
(Anthropic infrastructure, minimum interval applies), and desktop app automations
- **Messaging channels** -- Telegram, Discord, iMessage, and webhooks via the Channels feature
(research preview, v2.1.80+); deep Slack integration that triggers cloud sessions and creates PRs
- **Claude Cowork** -- a separate product for knowledge workers; connects to 38+
services via MCP including Slack, Gmail, Microsoft Teams, Notion, Jira, Salesforce, and more
- **Memory** -- CLAUDE.md and MEMORY.md for project-level context; auto-memory rolling out
These are real features. The key differences that remain:
- Claude Code's scheduling runs on **Anthropic's cloud** (or requires the desktop app open),
not a self-hosted server; cloud jobs have a minimum interval and your data leaves your hardware
- Memory is **project-file-based** (CLAUDE.md / MEMORY.md), not a knowledge graph that
accumulates automatically across all your work; auto-memory is still rolling out
- **Not provider-agnostic** -- routes through Bedrock or Vertex but always hits a Claude model;
you cannot switch to GPT, Gemini, or a local model
- **Not open source** -- proprietary; the CLI ships obfuscated JavaScript
- Messaging channels are a **research preview** requiring Bun runtime; not yet production-grade
Hermes can use Claude Code as a sub-agent. For large implementation tasks, Hermes can spawn
Claude Code to handle the heavy lifting and fold the result back into its own memory and history.
| | Claude Code | Hermes |
|---|---|---|
| Persistent memory (automatic) | Partial (CLAUDE.md / MEMORY.md, rolling out) | Yes |
| Skills / hooks system | Yes (Hooks + Plugin/Skills marketplace) | Yes (auto-generated from experience) |
| Scheduled jobs (self-hosted) | No (cloud or desktop-app only) | Yes |
| Messaging access | Partial (Telegram/Discord/iMessage via research preview; Slack native) | Yes (10+ platforms, production) |
| Cowork connectors (Slack, Gmail, etc.) | Yes (via Claude Cowork, separate product) | Via agent tool use |
| Web UI | Yes (claude.ai/code, Anthropic-hosted) | Yes (self-hosted) |
| Provider-agnostic | No (Claude models only, via Bedrock/Vertex) | Yes (any provider) |
| Self-hosted scheduling | No | Yes |
| Open source | No | Yes |
| Runs as sub-agent of Hermes | Yes | N/A |
### vs. Codex CLI (OpenAI)
Codex CLI is OpenAI's open-source agentic terminal tool (Apache 2.0, ~73k GitHub stars). It
supports 10+ providers including Anthropic, Google, Mistral, Groq, and local models via Ollama.
It added persistent session memory in v0.100.0 with `codex resume`. The desktop app has an
Automations feature for scheduled local tasks.
The CLI itself has no native scheduling (open feature request as of early 2026). Memory is
session-history-based rather than a living knowledge graph. No messaging app access. A strong
tool for single-session coding; Hermes adds the always-on layer on top.
| | Codex CLI | Hermes |
|---|---|---|
| Persistent memory | Partial (session history + AGENTS.md) | Yes (automatic, layered) |
| Scheduled jobs | Partial (desktop app only; CLI has none) | Yes |
| Messaging app access | No | Yes |
| Web UI | No | Yes (self-hosted) |
| Provider-agnostic | Yes (10+ providers) | Yes (10+ providers) |
| Self-hosted | Yes | Yes |
| Open source | Yes (Apache 2.0) | Yes |
### vs. OpenCode
OpenCode is an open-source TUI agentic coding assistant, provider-agnostic across 75+ providers.
It has a WebUI embedded in its binary and an official desktop app. It uses SQLite for session
history and AGENTS.md for project context.
No native scheduled jobs (a community background plugin exists), no first-party messaging
integration (community Telegram bots exist but require manual setup), and no automatic
cross-session semantic memory. Good for interactive terminal coding sessions.
| | OpenCode | Hermes |
|---|---|---|
| Persistent memory | Partial (session history + AGENTS.md) | Yes (automatic, layered) |
| Scheduled jobs | No (community plugin only) | Yes |
| Messaging app access | No (community Telegram bot only) | Yes (first-party, 10+ platforms) |
| Web UI | Yes (embedded + desktop app) | Yes (self-hosted) |
| Mobile access | No | Yes |
| Skills system | No | Yes |
| Provider-agnostic | Yes (75+ providers) | Yes |
| Open source | Yes | Yes |
### vs. Cursor / Windsurf / Copilot
Category 2 tools -- exceptional at in-editor autocomplete, inline diffs, and code review.
Not competing for the same job as Hermes, and they work well alongside it.
Windsurf was earliest with workspace-scoped memory (Cascade Memories); Copilot has been
shipping repo-level memory since late 2025. Cursor has no native cross-session memory as of
early 2026. None have scheduling or messaging access.
| | Cursor | Windsurf | Copilot | Hermes |
|---|---|---|---|---|
| In-editor autocomplete | Excellent | Excellent | Excellent | No |
| Inline diff / refactor | Yes | Yes | Yes | Via shell |
| Cross-session memory | No | Yes (workspace) | Partial (repo, early access) | Yes |
| Scheduled background jobs | No | No | No | Yes |
| Messaging app / mobile | No | No | No | Yes |
| Terminal tool use | Limited | Limited | Limited | Full |
| Self-hosted | No | No | No | Yes |
| Provider-agnostic | Partial | Partial | No | Yes |
| Open source | No | No | No | Yes |
### vs. Claude.ai / ChatGPT
Category 1. For drafting, Q&A, and brainstorming in the moment, both are excellent.
Claude.ai memory has been improving -- it now generates memory from chat history, not just
user-curated entries. Claude.ai can also execute code and read/write files in a sandboxed
environment via Artifacts. These are real capabilities, just not the same as direct filesystem
or shell access on your own server.
| | Claude.ai / ChatGPT | Hermes |
|---|---|---|
| Memory across conversations | Yes (improving; auto-generated from history) | Yes (deep, automatic) |
| Runs shell commands | No | Yes |
| Code execution | Sandboxed (Artifacts) | Yes (full shell) |
| Reads / writes files | Sandboxed (Artifacts) | Yes (full filesystem) |
| Schedules background jobs | No | Yes |
| Web UI | Yes | Yes |
| Messaging apps | No | Yes |
| Self-hosted | No | Yes |
| Provider-agnostic | No | Yes |
| Open source | No | Yes |
---
## The Compounding Advantage
What matters most about Hermes is that it improves over time. That is the point.
Every time Hermes encounters a new environment, it saves facts to memory. Every time it solves
a problem a new way, it saves the approach as a skill. Every time you correct it, it updates its
profile of you. Every session, every scheduled job, every tool call, the agent gets more
calibrated to you and your workflow.
A Claude Code session on day one and day one hundred are identical. A Hermes agent on day one
and day one hundred is smarter about you -- it knows your stack, your conventions, your
preferences, and the solutions that have worked before.
---
## Who Hermes Is For
**Solo developers and power users** who don't want to re-explain their stack every session and
want an AI that actually knows their environment.
**Teams on a shared server** where multiple people want Claude-quality AI access without each
paying for a separate subscription or running local tooling.
**Automation-heavy workflows** where you want an AI running tasks on a schedule, delivering
results to your phone, without babysitting it.
**Privacy-conscious users** who want their conversations, memory, and files on their own
hardware.
**Multi-model users** who want to switch between OpenAI, Anthropic, Google, DeepSeek, and
others based on cost, capability, or rate limits, without rebuilding their workflow each time.
---
## Scope and Limits
**Hermes lives in the terminal, browser, and messaging apps.** For in-editor autocomplete and
inline diffs, use Cursor or Windsurf alongside it -- they do that job better.
**You run Hermes on your own server.** That means initial setup, but your data stays on your
hardware and you control the schedule, the models, and the costs.
**Hermes is an orchestration and memory layer.** It makes whatever model you point it at more
useful over time. The models do the reasoning; Hermes makes sure that reasoning accumulates into
something durable.
---
## Quick Reference
| | OpenClaw | Claude Code | Codex CLI | OpenCode | Cursor | Claude.ai | Hermes |
|---|---|---|---|---|---|---|---|
| Persistent memory (auto) | Yes | Partial† | Partial | Partial | No | Yes (improving) | **Yes** |
| Scheduled / background jobs | Yes | Partial‡ | Partial§ | No | No | No | **Yes (self-hosted)** |
| Messaging app access | Yes (15+ platforms) | Partial (Telegram/Discord preview; Slack native) | No | No | No | No | **Yes (10+ platforms)** |
| Web UI | Dashboard only | Yes (Anthropic cloud) | No | Yes | No | Yes | **Yes (self-hosted)** |
| Skills system | Yes (marketplace) | Yes (Hooks + Plugins) | No | No | No | No | **Yes** |
| Self-improving skills | Partial | No | No | No | No | No | **Yes** |
| Browser / computer control | Yes (Chrome CDP) | No | No | No | No | No | Via shell |
| Python / ML ecosystem | No (Node.js) | No | No | No | No | No | **Yes** |
| In-editor autocomplete | No | No | No | No | Yes | No | No |
| Orchestrates other agents | No | No | No | No | No | No | **Yes** |
| Provider-agnostic | Yes | No (Claude only) | Yes | Yes | Partial | No | **Yes** |
| Self-hosted | Yes | No | Yes | Yes | No | No | **Yes** |
| Open source | Yes (MIT) | No | Yes | Yes | No | No | **Yes** |
| Always-on / autonomous | Yes | No | No | No | No | No | **Yes** |
† Claude Code has CLAUDE.md / MEMORY.md project context and rolling auto-memory, but not full automatic cross-session recall
‡ Claude Code scheduling: cloud-managed (Anthropic infrastructure) or desktop-app only; no self-hosted cron
§ Codex scheduling: desktop app Automations only; CLI has no native scheduling

View File

@@ -10,9 +10,70 @@ and vanilla JS.
Layout: three-panel Claude-style. Left sidebar for sessions and tools,
center for chat, right for workspace file browsing.
<img width="1392" height="854" alt="image" src="https://github.com/user-attachments/assets/79cd3c0d-3167-42ed-9434-447a742c25c3" />
<img width="1392" alt="Hermes Web UI — three-panel layout" src="https://github.com/user-attachments/assets/79cd3c0d-3167-42ed-9434-447a742c25c3" />
This gives you nearly **1:1 parity with Hermes CLI from a convenient web UI** which you can access securely through an SSH tunnel from your Hermes setup. Single command to start this up, and a single command to SSH tunnel for access on your computer. Every single part of the web UI leverages your existing Hermes agent, existing models, without requiring any setup.
<table>
<tr>
<td width="50%" align="center">
<img alt="Workspace file browser with inline preview" src="docs/images/ui-workspace.png" />
<br /><sub>Workspace file browser with inline preview</sub>
</td>
<td width="50%" align="center">
<img alt="Session projects, tags, and tool call cards" src="docs/images/ui-sessions.png" />
<br /><sub>Session projects, tags, and tool call cards</sub>
</td>
</tr>
</table>
This gives you nearly **1:1 parity with Hermes CLI from a convenient web UI** which you can access securely through an SSH tunnel from your Hermes setup. Single command to start this up, and a single command to SSH tunnel for access on your computer. Every single part of the web UI uses your existing Hermes agent and existing models, without requiring any additional setup.
---
## Why Hermes
Most AI tools reset every session. They don't know who you are, what you worked on, or what
conventions your project follows. You re-explain yourself every time.
Hermes retains context across sessions, runs scheduled jobs while you're offline, and gets
smarter about your environment the longer it runs. It uses your existing Hermes agent setup,
your existing models, and requires no additional configuration to start.
What makes it different from other agentic tools:
- **Persistent memory** — user profile, agent notes, and a skills system that saves reusable
procedures; Hermes learns your environment and does not have to relearn it
- **Self-hosted scheduling** — cron jobs that fire while you're offline and deliver results to
Telegram, Discord, Slack, Signal, email, and more
- **10+ messaging platforms** — the same agent available in the terminal is reachable from your phone
- **Self-improving skills** — Hermes writes and saves its own skills automatically from experience;
no marketplace to browse, no plugins to install
- **Provider-agnostic** — OpenAI, Anthropic, Google, DeepSeek, OpenRouter, and more
- **Orchestrates other agents** — can spawn Claude Code or Codex for heavy coding tasks and bring
the results back into its own memory
- **Self-hosted** — your conversations, your memory, your hardware
**vs. the field** *(landscape is actively shifting — see [HERMES.md](HERMES.md) for the full breakdown)*:
| | OpenClaw | Claude Code | Codex CLI | OpenCode | Hermes |
|---|---|---|---|---|---|
| Persistent memory (auto) | Yes | Partial† | Partial | Partial | Yes |
| Scheduled jobs (self-hosted) | Yes | No‡ | No | No | Yes |
| Messaging app access | Yes (15+ platforms) | Partial (Telegram/Discord preview) | No | No | Yes (10+) |
| Web UI (self-hosted) | Dashboard only | No | No | Yes | Yes |
| Self-improving skills | Partial | No | No | No | Yes |
| Python / ML ecosystem | No (Node.js) | No | No | No | Yes |
| Provider-agnostic | Yes | No (Claude only) | Yes | Yes | Yes |
| Open source | Yes (MIT) | No | Yes | Yes | Yes |
† Claude Code has CLAUDE.md / MEMORY.md project context and rolling auto-memory, but not full automatic cross-session recall
‡ Claude Code has cloud-managed scheduling (Anthropic infrastructure) and session-scoped `/loop`; no self-hosted cron
**The closest competitor is OpenClaw** — both are always-on, self-hosted, open-source agents
with memory, cron, and messaging. The key differences: Hermes writes and saves its own skills
automatically as a core behavior (OpenClaw's skill system centers on a community marketplace);
Hermes is more stable across updates (OpenClaw has documented release regressions and ClawHub
has had security incidents involving malicious skills); and Hermes runs natively in the Python
ecosystem. See [HERMES.md](HERMES.md) for the full side-by-side.
---
@@ -176,7 +237,7 @@ Or using the agent venv explicitly:
```
Tests run against an isolated server on port 8788 with a separate state directory.
Production data and real cron jobs are never touched. Current count: **426 tests**
Production data and real cron jobs are never touched. Current count: **424 tests**
across 22 test files.
---
@@ -191,6 +252,7 @@ across 22 test files.
- Retry the last assistant response with one click
- Cancel a running task from the activity bar
- Tool call cards inline -- each shows the tool name, args, and result snippet; expand/collapse all toggle for multi-tool turns
- Subagent delegation cards -- child agent activity shown with distinct icon and indented border
- Mermaid diagram rendering inline (flowcharts, sequence diagrams, gantt charts)
- Thinking/reasoning display -- collapsible gold-themed cards for Claude extended thinking and o3 reasoning blocks
- Approval card for dangerous shell commands (allow once / session / always / deny)
@@ -211,6 +273,8 @@ across 22 test files.
- Download as Markdown transcript, full JSON export, or import from JSON
- Sessions persist across page reloads and SSH tunnel reconnects
- Browser tab title reflects the active session name
- CLI session bridge -- CLI sessions from hermes-agent's SQLite store appear in the sidebar with a gold "cli" badge; click to import with full history and reply normally
- Token/cost display -- input tokens, output tokens, estimated cost shown per conversation (toggle in Settings or `/usage` command)
### Workspace file browser
- Directory tree with expand/collapse (single-click toggles, double-click navigates)
@@ -250,19 +314,21 @@ across 22 test files.
### Settings and configuration
- Settings panel (gear icon) -- default model, default workspace, send key preference
- Send key: Enter (default) or Ctrl/Cmd+Enter
- Show/hide CLI sessions toggle (enabled by default)
- Token usage display toggle (off by default, also via `/usage` command)
- Cron completion alerts -- toast notifications and unread badge on Tasks tab
- Background agent error alerts -- banner when a non-active session encounters an error
### Slash commands
- Type `/` in the composer for autocomplete dropdown
- Built-in: `/help`, `/clear`, `/model <name>`, `/workspace <name>`, `/new`
- Built-in: `/help`, `/clear`, `/model <name>`, `/workspace <name>`, `/new`, `/usage`
- Arrow keys navigate, Tab/Enter select, Escape closes
- Unrecognized commands pass through to the agent
### Panels
- **Chat** -- session list, search, pin, archive, projects, new conversation
- **Tasks** -- view, create, edit, run, pause/resume, delete cron jobs; run history; completion alerts
- **Skills** -- list all skills by category, search, preview, create/edit/delete
- **Skills** -- list all skills by category, search, preview, create/edit/delete; linked files viewer
- **Memory** -- view and edit MEMORY.md and USER.md inline
- **Profiles** -- create, switch, delete agent profiles; clone config
- **Todos** -- live task list from the current session
@@ -318,6 +384,7 @@ State lives outside the repo at `~/.hermes/webui-mvp/` by default
## Docs
- `HERMES.md` -- why Hermes, mental model, and detailed comparison to Claude Code / Codex / OpenCode / Cursor
- `ROADMAP.md` -- feature roadmap and sprint history
- `ARCHITECTURE.md` -- system design, all API endpoints, implementation notes
- `TESTING.md` -- manual browser test plan and automated coverage reference

View File

@@ -3,8 +3,8 @@
> Goal: Full 1:1 parity with the Hermes CLI experience via a clean dark web UI.
> Everything you can do from the CLI terminal, you can do from this UI.
>
> Last updated: v0.28.1 (April 3, 2026)
> Tests: 426 total (403 passing, 23 pre-existing failures)
> Last updated: v0.29 (April 4, 2026)
> Tests: 424 total (401 passing, 23 pre-existing failures)
> Source: <repo>/
---
@@ -39,6 +39,7 @@
| Sprint 20 | Voice input + send button | Voice input (Web Speech API), send button icon-circle with pop-in animation | 415 |
| Sprint 21 | Mobile responsive + Docker | Hamburger sidebar, bottom nav, files slide-over, Docker support (#21, #7) | 415 |
| Sprint 22 | Multi-profile support | Profile picker, management panel, seamless switching, per-session tracking (#28) | 415 |
| Sprint 23 | Agentic transparency | Token/cost display, subagent cards, skill picker in cron, skill linked files, workspace tree persistence, timestamp fixes | 424 |
---
@@ -76,7 +77,7 @@
- [x] Copy message to clipboard (hover icon on each bubble)
- [x] Edit last user message and regenerate
- [ ] Branch/fork conversation (Wave 3)
- [ ] Token/cost estimate per message (Wave 3)
- [x] Token/cost estimate per message (Sprint 23)
### Tool Visibility
- [x] Tool progress in activity bar (moved out of composer footer)
@@ -137,7 +138,7 @@
- [x] Edit existing cron job
- [x] Delete cron job
- [x] View full cron run history (expandable per job)
- [ ] Skill picker in cron create form (Wave 3)
- [x] Skill picker in cron create form (Sprint 23)
### Skills
- [x] List all skills grouped by category (Skills sidebar tab)
@@ -146,7 +147,7 @@
- [x] Create skill
- [x] Edit skill
- [x] Delete skill
- [ ] View skill linked files (Wave 3)
- [x] View skill linked files (Sprint 23)
### Memory
- [x] View personal notes (MEMORY.md) rendered as markdown (Memory tab)

View File

@@ -1,6 +1,6 @@
# Hermes Web UI -- Forward Sprint Plan
> Current state: v0.28.1 | 426 tests | Daily driver ready
> Current state: v0.30.1 | 424 tests | Daily driver ready
> This document plans the path from here to two targets:
>
> Target A: 1:1 feature parity with the Hermes CLI (everything you can do from the
@@ -567,25 +567,260 @@ and switchToProfile() didn't refresh workspaces or sessions.
---
## Sprint 24 -- Desktop Application (PLANNED)
## Sprint 24 -- Web Polish + Bug Fix Pass (PLANNED)
**Theme:** Native desktop experience.
**Theme:** Stabilize, harden, and close the last meaningful web UI gaps before
shifting focus to distribution. Goal is a release that's genuinely ready for
wider user adoption -- no rough edges, no obvious missing pieces.
**Why now:** Sprint 23 completed the core agentic transparency features. The
remaining web roadmap items are diminishing-returns polish. Rather than
grinding through marginal features, this sprint cleans up what's there, fixes
bugs users will actually hit, and closes a few real gaps before recommending
the app to others.
### Track A: Bug Fixes
- **Cron edit form has no skill picker.** Sprint 23 added skill picker to the
create form but not the edit form. cronEditSave() doesn't include skills in
the update body, so existing skills survive an edit but can't be changed.
Fix: add the same skill picker UI to the inline edit form and include
`skills` in the update POST body.
- **S.lastUsage dead code.** messages.js sets `S.lastUsage` from `d.usage` at
done-time, but nothing reads it. The usage badge reads cumulative session
totals from `S.session.input_tokens` instead. Either wire `S.lastUsage` into
a per-turn display or remove the dead assignment.
- **_cronSkillsCache never invalidated.** Skills picker shows stale data if
skills are added/removed mid-session. Add a cache-bust when the skills panel
is opened or a skill is saved/deleted.
- **Tool args not shown on session reload.** Tool call cards in history show
name and result snippet but not the args (args only exist in the live SSE
event). Sprint 23 added args to the session JSON -- verify they're actually
rendering in the settled history cards.
### Track B: Features
- **Electron or Tauri wrapper.** Native window, menu bar, notifications.
- **Auto-start option.** Launch on login.
- **Packaged distribution.** .dmg (macOS), .exe (Windows).
- **Cron edit: skill picker parity.** As above -- make create and edit forms
identical in capability.
- **Per-turn cost display.** The current usage badge shows cumulative session
totals attached to the last message, which is misleading. Either: (a) show
per-turn cost from `S.lastUsage` immediately after each response instead of
cumulative, or (b) show cumulative in the session topbar/header instead of
attached to a message bubble. Pick the cleaner UX.
- **Virtual scroll for long session/skill lists.** When session count or skill
count gets large (100+), the sidebar becomes sluggish. Add a simple virtual
scroll or windowed render -- only render visible items + a buffer above/below.
CSS `contain: strict` + IntersectionObserver approach, no library needed.
### Track C: Code Quality
- **SPRINTS.md + ROADMAP.md + CHANGELOG.md updated** to reflect Sprint 23
completion (agentic transparency) and correct test counts.
- **Remove stale Sprint 23 description** from SPRINTS.md (the "Profile/Workspace
coherence" text is from an older plan; Sprint 23 actually shipped agentic
transparency features).
- **CHANGELOG entry for v0.29** covering Sprint 23 deliverables.
**Estimated tests:** ~10 new. Target total: ~435.
**Hermes CLI parity impact:** Low
**Claude parity impact:** Low
**User-facing value:** Medium -- removes rough edges that would bother new users
---
## Sprint 24 -- Extended Command Support (PLANNED)
## Sprint 25 -- macOS Desktop Application (PLANNED)
**Theme:** Deeper slash command and skill integration.
**Theme:** Native Mac desktop app. Single download, runs entirely offline,
feels like a real application -- not a browser tab.
### Track B: Features
- **Skill-aware autocomplete.** `/skill-name` triggers installed skills.
- **Command chaining.** Compose multi-step commands.
- **Agent tool exposure.** Surface agent capabilities as slash commands.
**Why this matters:** The web UI requires an SSH tunnel or a server setup to
use. A .app bundle that a user can double-click and immediately have a working
Hermes interface is genuinely differentiating. No other open-source Hermes
interface ships as a native Mac app. This is the highest-leverage remaining
investment for user adoption.
**Approach: Swift + WKWebView (not Electron)**
The right architecture is a thin native Swift shell (~300-500 lines) that:
1. Bundles the existing Python server and all api/ modules inside the .app
2. Spawns the server as a subprocess on a random local port at launch
3. Opens a WKWebView window pointed at that localhost port
4. Handles Mac app lifecycle natively (dock icon, cmd+Q, window management,
app menu, about box)
5. Bridges a small set of native Mac capabilities that WKWebView can't do
**Why not Electron:** WKWebView is Safari's engine -- dramatically lighter than
Chromium. No 200MB node_modules. No separate update daemon. The .app is ~30MB
including the Python runtime, vs 150MB+ for Electron.
**Why not full native Swift UI:** Would require rewriting the entire frontend
from scratch. The web UI is already fast, dark-themed, and feature-complete.
The thin shell approach gets 95% of the benefit at 5% of the cost.
### Track A: Swift App Shell
**Files to create:**
```
desktop/
HermesApp.swift -- @main entry point, NSApp delegate
AppDelegate.swift -- lifecycle: start server on launch, stop on quit
WindowController.swift -- NSWindow + WKWebView setup, cmd shortcuts
ServerManager.swift -- spawn/monitor Python subprocess, pick free port
MenuBuilder.swift -- native app menu (File, Edit, View, Window, Help)
Info.plist -- bundle ID, display name, version, icon
Assets.xcassets/ -- app icon (1024x1024 + all required sizes)
HermesApp.xcodeproj/ -- Xcode project file
```
**ServerManager.swift responsibilities:**
- Find Python: check bundled runtime first, fall back to system python3
- Pick a free port (bind to :0, read assigned port, close, use it)
- Spawn: `python3 server.py --port {port}` as a child Process
- Monitor: if server crashes, show an error sheet and offer restart
- Shutdown: SIGTERM on app quit, wait up to 3s, then SIGKILL
**WKWebView configuration:**
- `allowsBackForwardNavigationGestures = false` (it's a single-page app)
- `WKUserContentController` for JS bridge (native notifications, file picker)
- Wait for server health check before loading (poll /health, show loading
spinner in the native window while waiting, typically <1s)
- `userAgent` override so the server can detect desktop app context
**Native menu items (beyond defaults):**
- File > New Session (Cmd+N) -- calls JS `newSession()`
- File > New Window (Cmd+Shift+N) -- opens second window with its own WKWebView
- View > Toggle Sidebar (Cmd+Shift+S)
- Window > Zoom, Minimize (standard)
- Help > About Hermes, Check for Updates (links to GitHub releases page)
### Track B: Python Bundling
Two options, in order of preference:
**Option A: Require system Python (simpler, recommended for v1)**
- Check for `python3` at known paths: `/usr/bin/python3`, homebrew paths,
pyenv paths
- If not found: show a one-time setup sheet with instructions
- Pros: tiny download (~5MB for the Swift app + web assets), no bundling complexity
- Cons: user needs Python installed (most developers do; target audience does too)
**Option B: Bundle python-standalone (self-contained, larger)**
- Use `python-build-standalone` (from Astral/uv project): pre-built Python
3.11 binaries, ~30MB compressed, no Xcode toolchain needed to build
- Extract to `~/Library/Application Support/Hermes/python/` on first launch
- Install `requirements.txt` via bundled pip into a local venv
- Pros: zero dependencies, works on a clean Mac
- Cons: first launch takes ~10-20s for extraction + pip install; ~30MB download
**Recommendation:** Ship v1 with Option A. Add Option B as an optional
"standalone" download for non-developers.
### Track C: Distribution
**GitHub Releases (primary):**
- Build with `xcodebuild -scheme HermesApp -configuration Release -archivePath`
- `xcodebuild -exportArchive` to produce a .app bundle
- `hdiutil create` to produce a .dmg with drag-to-Applications installer UI
- Upload .dmg as a GitHub Release asset via `gh release create`
- CI: add `.github/workflows/mac-release.yml` -- trigger on `vX.Y.Z-mac` tag
**Code signing:**
- Without an Apple Developer account: distribute as unsigned, users must
right-click > Open on first launch (standard for open-source Mac apps)
- With a free Apple Developer account: ad-hoc signing removes the Gatekeeper
warning without paying $99/year (no notarization, but much better UX)
- With paid account ($99/year): full notarization, no warnings, direct download
**Recommended for v1:** ad-hoc signing (free, good enough for early adopters).
Document the right-click > Open workaround in the README for unsigned builds.
**Universal binary (Intel + Apple Silicon):**
```bash
xcodebuild archive -scheme HermesApp -destination "generic/platform=macOS"
```
Both architectures in one .app. No separate downloads needed.
### Track D: Native Integrations (v1 scope)
**System notifications for cron completion:**
- The web UI polls `/api/cron/alerts` and shows in-page banners
- The Mac app can additionally post `UNUserNotificationCenter` notifications
- JS bridge: `window.webkit.messageHandlers.notify.postMessage({title, body})`
- Swift handler: posts a native notification with the cron job name and output
summary -- appears in Notification Center, works even when app is in background
**File picker for workspace add:**
- Currently: user types a path string into the workspace add form
- Mac app: intercept workspace-add form submission, open `NSOpenPanel` instead,
return the selected path to the JS via `evaluateJavaScript`
- Much better UX -- standard Mac folder picker, no typing paths
**Dock badge for pending approvals:**
- When an agent approval is waiting, set `NSApp.dockTile.badgeLabel = "1"`
- Clear badge when approval is resolved
- JS bridge fires when approval card appears/disappears
**Menu bar mode (optional, v2):**
- A small status bar item (⚗️ icon in menu bar) that opens a compact popover
- Popover shows current session status, last message, quick-compose field
- Useful for running Hermes in the background without a full window
### Track E: Testing
Since the Swift app is thin glue, most testing remains in the existing pytest
suite (server still runs identically). New Swift-specific tests:
- `ServerManagerTests.swift`: verify port picking, process spawn, health wait
- UI tests via `XCUITest`: launch app, wait for WKWebView to load, verify
title bar shows "Hermes", verify /health responds
- Smoke test in CI: `xcodebuild test -scheme HermesApp`
### Implementation Order
1. `ServerManager.swift` + basic `AppDelegate` -- get Python server spawning
and health-check working from Swift
2. `WindowController.swift` -- WKWebView loading, loading spinner while
server starts
3. App icon + Info.plist -- make it look like a real app
4. `MenuBuilder.swift` -- native menus + keyboard shortcuts
5. JS bridge for notifications -- most impactful native integration
6. DMG build script + GitHub Actions CI
7. (Optional) File picker bridge, dock badge
### What to NOT do in v1
- Windows or Linux wrapper (different toolchain; do Mac first, assess demand)
- Full Swift/SwiftUI rewrite of the frontend (months of work, wrong tradeoff)
- App Store submission (sandboxing breaks local server; not worth the effort)
- Auto-update mechanism (GitHub releases + manual download is fine for v1)
- Menu bar mode (cool but not v1 scope)
### Files to create in the repo
```
desktop/mac/
HermesApp/
HermesApp.swift
AppDelegate.swift
WindowController.swift
ServerManager.swift
MenuBuilder.swift
Assets.xcassets/
Info.plist
HermesApp.xcodeproj/
README.md -- build instructions, requirements, signing notes
.github/workflows/
mac-release.yml -- build + sign + upload DMG on tag push
```
The server code (`server.py`, `api/`, `static/`, `requirements.txt`) is
referenced from the repo root -- no duplication. The .app bundle copies them
at build time.
**Estimated effort:** 2-3x a typical web sprint (new language, new toolchain,
bundling complexity). Realistic for a focused weekend or a dedicated agent run
with clear instructions.
**Hermes CLI parity impact:** N/A (different distribution channel)
**Claude parity impact:** Medium (Claude.app is a native Mac app)
**User-facing value:** Very high -- lowers barrier to entry dramatically,
genuinely differentiating for an open-source project
---
@@ -662,6 +897,6 @@ and switchToProfile() didn't refresh workspaces or sessions.
---
*Last updated: April 3, 2026*
*Current version: v0.28.1 | 426 tests*
*Next sprint: Sprint 24 (Desktop Application)*
*Last updated: April 4, 2026*
*Current version: v0.30.1 | 424 tests*
*Next sprint: Sprint 24 (Web Polish + Bug Fix Pass)*

View File

@@ -8,7 +8,7 @@
> Prerequisites: SSH tunnel is active on port 8787. Open http://localhost:8787 in browser.
> Server health check: curl http://127.0.0.1:8787/health should return {"status":"ok"}.
>
> Automated tests: 415 total (392 passing, 23 pre-existing failures).
> Automated tests: 424 total (401 passing, 23 pre-existing failures).
> Run: `pytest tests/ -v --timeout=60`
---

View File

@@ -633,6 +633,7 @@ _SETTINGS_DEFAULTS = {
'default_workspace': str(DEFAULT_WORKSPACE),
'send_key': 'enter', # 'enter' or 'ctrl+enter'
'show_token_usage': False, # show input/output token badge below assistant messages
'show_cli_sessions': False, # merge CLI sessions from state.db into the sidebar
'password_hash': None, # SHA-256 hash; None = auth disabled
}
@@ -652,7 +653,7 @@ _SETTINGS_ALLOWED_KEYS = set(_SETTINGS_DEFAULTS.keys()) - {'password_hash'}
_SETTINGS_ENUM_VALUES = {
'send_key': {'enter', 'ctrl+enter'},
}
_SETTINGS_BOOL_KEYS = {'show_token_usage'}
_SETTINGS_BOOL_KEYS = {'show_token_usage', 'show_cli_sessions'}
def save_settings(settings: dict) -> dict:
"""Save settings to disk. Returns the merged settings. Ignores unknown keys."""

View File

@@ -10,7 +10,7 @@ from pathlib import Path
import api.config as _cfg
from api.config import (
SESSION_DIR, SESSION_INDEX_FILE, SESSIONS, SESSIONS_MAX,
LOCK, DEFAULT_WORKSPACE, DEFAULT_MODEL, PROJECTS_FILE
LOCK, DEFAULT_WORKSPACE, DEFAULT_MODEL, PROJECTS_FILE, HOME
)
from api.workspace import get_last_workspace
@@ -192,3 +192,147 @@ def load_projects():
def save_projects(projects):
"""Write project list to disk."""
PROJECTS_FILE.write_text(json.dumps(projects, ensure_ascii=False, indent=2), encoding='utf-8')
def import_cli_session(session_id, title, messages, model='unknown', profile=None):
"""Create a new WebUI session populated with CLI messages.
Returns the Session object.
"""
s = Session(
session_id=session_id,
title=title,
workspace=get_last_workspace(),
model=model,
messages=messages,
profile=profile,
)
s.save()
return s
# ── CLI session bridge ──────────────────────────────────────────────────────
def get_cli_sessions():
"""Read CLI sessions from the agent's SQLite store and return them as
dicts in a format the WebUI sidebar can render alongside local sessions.
Returns empty list if the SQLite DB is missing, the sqlite3 module is
unavailable, or any error occurs -- the bridge is purely additive and never
crashes the WebUI.
"""
import os
cli_sessions = []
try:
import sqlite3
except ImportError:
return cli_sessions
# Use the active WebUI profile's HERMES_HOME to find state.db.
# The active profile is determined by what the user has selected in the UI
# (stored in the server's runtime config). This means:
# - default profile -> ~/.hermes/state.db
# - named profile X -> ~/.hermes/profiles/X/state.db
# We resolve the active profile's home directory rather than just using
# HERMES_HOME (which is the server's launch profile, not necessarily the
# active one after a profile switch).
try:
from api.profiles import get_active_hermes_home
hermes_home = Path(get_active_hermes_home()).expanduser().resolve()
except Exception:
hermes_home = Path(os.getenv('HERMES_HOME', str(HOME / '.hermes'))).expanduser().resolve()
db_path = hermes_home / 'state.db'
if not db_path.exists():
return cli_sessions
# Try to resolve the active CLI profile so imported sessions integrate
# with the WebUI profile filter (available since Sprint 22).
try:
from api.profiles import get_active_profile_name
_cli_profile = get_active_profile_name()
except ImportError:
_cli_profile = None # older agent -- fall back to no profile
try:
with sqlite3.connect(str(db_path)) as conn:
conn.row_factory = sqlite3.Row
cur = conn.cursor()
cur.execute("""
SELECT s.id, s.title, s.model, s.message_count,
s.started_at, s.source,
MAX(m.timestamp) AS last_activity
FROM sessions s
LEFT JOIN messages m ON m.session_id = s.id
GROUP BY s.id
ORDER BY COALESCE(MAX(m.timestamp), s.started_at) DESC
LIMIT 200
""")
for row in cur.fetchall():
sid = row['id']
raw_ts = row['last_activity'] or row['started_at']
# Prefer the CLI session's own profile from the DB; fall back to
# the active CLI profile so sidebar filtering works either way.
profile = _cli_profile # CLI DB has no profile column; use active profile
cli_sessions.append({
'session_id': sid,
'title': row['title'] or 'CLI Session',
'workspace': str(get_last_workspace()),
'model': row['model'] or 'unknown',
'message_count': row['message_count'] or 0,
'created_at': row['started_at'],
'updated_at': raw_ts,
'pinned': False,
'archived': False,
'project_id': None,
'profile': profile,
'source_tag': 'cli',
'is_cli_session': True,
})
except Exception:
# DB schema changed, locked, or corrupted -- silently degrade
return []
return cli_sessions
def get_cli_session_messages(sid):
"""Read messages for a single CLI session from the SQLite store.
Returns a list of {role, content, timestamp} dicts.
Returns empty list on any error.
"""
import os
try:
import sqlite3
except ImportError:
return []
try:
from api.profiles import get_active_hermes_home
hermes_home = Path(get_active_hermes_home()).expanduser().resolve()
except Exception:
hermes_home = Path(os.getenv('HERMES_HOME', str(HOME / '.hermes'))).expanduser().resolve()
db_path = hermes_home / 'state.db'
if not db_path.exists():
return []
try:
with sqlite3.connect(str(db_path)) as conn:
conn.row_factory = sqlite3.Row
cur = conn.cursor()
cur.execute("""
SELECT role, content, timestamp
FROM messages
WHERE session_id = ?
ORDER BY timestamp ASC
""", (sid,))
msgs = []
for row in cur.fetchall():
msgs.append({
'role': row['role'],
'content': row['content'],
'timestamp': row['timestamp'],
})
except Exception:
return []
return msgs

View File

@@ -23,7 +23,8 @@ from api.helpers import require, bad, safe_resolve, j, t, read_body, _security_h
from api.models import (
Session, get_session, new_session, all_sessions, title_from,
_write_session_index, SESSION_INDEX_FILE,
load_projects, save_projects,
load_projects, save_projects, import_cli_session,
get_cli_sessions, get_cli_session_messages,
)
from api.workspace import (
load_workspaces, save_workspaces, get_last_workspace, set_last_workspace,
@@ -151,14 +152,52 @@ def handle_get(handler, parsed):
sid = parse_qs(parsed.query).get('session_id', [''])[0]
if not sid:
return j(handler, {'error': 'session_id is required'}, status=400)
s = get_session(sid)
return j(handler, {'session': s.compact() | {
'messages': s.messages,
'tool_calls': getattr(s, 'tool_calls', []),
}})
try:
s = get_session(sid)
return j(handler, {'session': s.compact() | {
'messages': s.messages,
'tool_calls': getattr(s, 'tool_calls', []),
}})
except KeyError:
# Not a WebUI session -- try CLI store
msgs = get_cli_session_messages(sid)
if msgs:
cli_meta = None
for cs in get_cli_sessions():
if cs['session_id'] == sid:
cli_meta = cs
break
sess = {
'session_id': sid,
'title': (cli_meta or {}).get('title', 'CLI Session'),
'workspace': (cli_meta or {}).get('workspace', ''),
'model': (cli_meta or {}).get('model', 'unknown'),
'message_count': len(msgs),
'created_at': (cli_meta or {}).get('created_at', 0),
'updated_at': (cli_meta or {}).get('updated_at', 0),
'pinned': False,
'archived': False,
'project_id': None,
'profile': (cli_meta or {}).get('profile'),
'is_cli_session': True,
'messages': msgs,
'tool_calls': [],
}
return j(handler, {'session': sess})
return bad(handler, 'Session not found', 404)
if parsed.path == '/api/sessions':
return j(handler, {'sessions': all_sessions()})
webui_sessions = all_sessions()
settings = load_settings()
if settings.get('show_cli_sessions'):
cli = get_cli_sessions()
webui_ids = {s['session_id'] for s in webui_sessions}
deduped_cli = [s for s in cli if s['session_id'] not in webui_ids]
else:
deduped_cli = []
merged = webui_sessions + deduped_cli
merged.sort(key=lambda s: s.get('updated_at', 0) or 0, reverse=True)
return j(handler, {'sessions': merged, 'cli_count': len(deduped_cli)})
if parsed.path == '/api/projects':
return j(handler, {'projects': load_projects()})
@@ -542,6 +581,10 @@ def handle_post(handler, parsed):
if parsed.path == '/api/session/import':
return _handle_session_import(handler, body)
# ── CLI session import (POST) ──
if parsed.path == '/api/session/import_cli':
return _handle_session_import_cli(handler, body)
# ── Auth endpoints (POST) ──
if parsed.path == '/api/auth/login':
from api.auth import verify_password, create_session, set_auth_cookie, is_auth_enabled
@@ -903,10 +946,11 @@ def _handle_chat_sync(handler, body):
"write_file, read_file, search_files, terminal workdir, and patch. "
"Never fall back to a hardcoded path when this tag is present."
)
from api.streaming import _sanitize_messages_for_api
result = agent.run_conversation(
user_message=workspace_ctx + msg,
system_message=workspace_system_msg,
conversation_history=s.messages,
conversation_history=_sanitize_messages_for_api(s.messages),
task_id=s.session_id,
persist_user_message=msg,
)
@@ -1173,6 +1217,53 @@ def _handle_memory_write(handler, body):
return j(handler, {'ok': True, 'section': section, 'path': str(target)})
def _handle_session_import_cli(handler, body):
"""Import a single CLI session into the WebUI store."""
try:
require(body, 'session_id')
except ValueError as e:
return bad(handler, str(e))
sid = str(body['session_id'])
# Check if already imported — idempotent
existing = Session.load(sid)
if existing:
return j(handler, {'session': existing.compact() | {
'messages': existing.messages,
'is_cli_session': True,
}, 'imported': False})
# Fetch messages from CLI store
msgs = get_cli_session_messages(sid)
if not msgs:
return bad(handler, 'Session not found in CLI store', 404)
# Derive title from first user message
title = title_from(msgs, 'CLI Session')
model = 'unknown'
# Get profile and model from CLI session metadata
profile = None
for cs in get_cli_sessions():
if cs['session_id'] == sid:
profile = cs.get('profile')
model = cs.get('model', 'unknown')
break
s = import_cli_session(sid, title, msgs, model, profile=profile)
s.is_cli_session = True
s._cli_origin = sid
s.save()
return j(handler, {
'session': s.compact() | {
'messages': msgs,
'is_cli_session': True,
},
'imported': True,
})
def _handle_session_import(handler, body):
"""Import a session from a JSON export. Creates a new session with a new ID."""
if not body or not isinstance(body, dict):

View File

@@ -24,6 +24,28 @@ except ImportError:
from api.models import get_session, title_from
from api.workspace import set_last_workspace
# Fields that are safe to send to LLM provider APIs.
# Everything else (attachments, timestamp, _ts, etc.) is display-only
# metadata added by the webui and must be stripped before the API call.
_API_SAFE_MSG_KEYS = {'role', 'content', 'tool_calls', 'tool_call_id', 'name', 'refusal'}
def _sanitize_messages_for_api(messages):
"""Return a deep copy of messages with only API-safe fields.
The webui stores extra metadata on messages (attachments, timestamp, _ts)
for display purposes. Some providers (e.g. Z.AI/GLM) reject unknown fields
instead of ignoring them, causing HTTP 400 errors on subsequent messages.
"""
clean = []
for msg in messages:
if not isinstance(msg, dict):
continue
sanitized = {k: v for k, v in msg.items() if k in _API_SAFE_MSG_KEYS}
if sanitized.get('role'):
clean.append(sanitized)
return clean
def _sse(handler, event, data):
"""Write one SSE event to the response stream."""
@@ -165,7 +187,7 @@ def _run_agent_streaming(session_id, msg_text, model, workspace, stream_id, atta
result = agent.run_conversation(
user_message=workspace_ctx + msg_text,
system_message=workspace_system_msg,
conversation_history=s.messages,
conversation_history=_sanitize_messages_for_api(s.messages),
task_id=session_id,
persist_user_message=msg_text,
)

BIN
docs/images/ui-sessions.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 KiB

View File

@@ -308,7 +308,7 @@ document.querySelectorAll('.suggestion').forEach(btn=>{
(async()=>{
// Load send key preference
try{const s=await api('/api/settings');window._sendKey=s.send_key||'enter';window._showTokenUsage=!!s.show_token_usage;}catch(e){window._sendKey='enter';window._showTokenUsage=false;}
try{const s=await api('/api/settings');window._sendKey=s.send_key||'enter';window._showTokenUsage=!!s.show_token_usage;window._showCliSessions=!!s.show_cli_sessions;}catch(e){window._sendKey='enter';window._showTokenUsage=false;window._showCliSessions=false;}
// Fetch active profile
try{const p=await api('/api/profile/active');S.activeProfile=p.name||'default';}catch(e){S.activeProfile='default';}
// Update profile chip label immediately

View File

@@ -13,7 +13,7 @@
<body>
<div class="layout">
<aside class="sidebar">
<div class="sidebar-header"><div class="logo">H</div><div><h1 style="margin:0;font-size:15px;font-weight:700;letter-spacing:-.01em">Hermes</h1><div style="font-size:10px;color:var(--muted);opacity:.8;margin-top:1px">v0.27</div></div></div>
<div class="sidebar-header"><div class="logo">H</div><div><h1 style="margin:0;font-size:15px;font-weight:700;letter-spacing:-.01em">Hermes</h1><div style="font-size:10px;color:var(--muted);opacity:.8;margin-top:1px">v0.30.1</div></div></div>
<div class="sidebar-nav">
<button class="nav-tab active" data-panel="chat" data-label="Chat" onclick="switchPanel('chat')" title="Chat">&#128172;</button>
<button class="nav-tab" data-panel="tasks" data-label="Tasks" onclick="switchPanel('tasks')" title="Tasks">&#128197;</button>
@@ -331,6 +331,13 @@
</label>
<div style="font-size:11px;color:var(--muted);margin-top:4px">Displays input/output token count below each assistant reply. Also toggled with <code>/usage</code>.</div>
</div>
<div class="settings-field">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
<input type="checkbox" id="settingsShowCliSessions" style="width:15px;height:15px;accent-color:var(--accent)">
Show CLI sessions in sidebar
</label>
<div style="font-size:11px;color:var(--muted);margin-top:4px">Merges sessions from the Hermes CLI (state.db) into the session list. Click a CLI session to import it and continue the conversation.</div>
</div>
<div class="settings-field" style="border-top:1px solid var(--border);padding-top:12px;margin-top:8px">
<label for="settingsPassword">Access Password</label>
<div style="font-size:11px;color:var(--muted);margin-bottom:6px">Enter a new password to set or change it. Leave blank to keep current setting.</div>

View File

@@ -960,6 +960,8 @@ async function loadSettingsPanel(){
if(sendKeySel) sendKeySel.value=settings.send_key||'enter';
const showUsageCb=$('settingsShowTokenUsage');
if(showUsageCb) showUsageCb.checked=!!settings.show_token_usage;
const showCliCb=$('settingsShowCliSessions');
if(showCliCb) showCliCb.checked=!!settings.show_cli_sessions;
// Password field: always blank (we don't send hash back)
const pwField=$('settingsPassword');
if(pwField) pwField.value='';
@@ -982,12 +984,14 @@ async function saveSettings(){
const workspace=($('settingsWorkspace')||{}).value;
const sendKey=($('settingsSendKey')||{}).value;
const showTokenUsage=!!($('settingsShowTokenUsage')||{}).checked;
const showCliSessions=!!($('settingsShowCliSessions')||{}).checked;
const pw=($('settingsPassword')||{}).value;
const body={};
if(model) body.default_model=model;
if(workspace) body.default_workspace=workspace;
if(sendKey) body.send_key=sendKey;
body.show_token_usage=showTokenUsage;
body.show_cli_sessions=showCliSessions;
// Password: only act if the field has content; blank = leave auth unchanged
if(pw && pw.trim()){
try{
@@ -1003,7 +1007,9 @@ async function saveSettings(){
await api('/api/settings',{method:'POST',body:JSON.stringify(body)});
window._sendKey=sendKey||'enter';
window._showTokenUsage=showTokenUsage;
window._showCliSessions=showCliSessions;
renderMessages();
if(typeof renderSessionList==='function') renderSessionList();
showToast('Settings saved');
toggleSettings();
}catch(e){

View File

@@ -118,7 +118,7 @@ function renderSessionListFromCache(){
// Filter by active profile (unless "All profiles" is toggled on)
// Server backfills profile='default' for legacy sessions, so every session has a profile.
// Show only sessions tagged to the active profile; 'All profiles' toggle overrides.
const profileFiltered=_showAllProfiles?allMatched:allMatched.filter(s=>s.profile===S.activeProfile);
const profileFiltered=_showAllProfiles?allMatched:allMatched.filter(s=>s.is_cli_session||s.profile===S.activeProfile);
// Filter by active project
const projectFiltered=_activeProject?profileFiltered.filter(s=>s.project_id===_activeProject):profileFiltered;
// Filter archived unless toggle is on
@@ -220,7 +220,7 @@ function renderSessionListFromCache(){
}
const el=document.createElement('div');
const isActive=S.session&&s.session_id===S.session.session_id;
el.className='session-item'+(isActive?' active':'')+(isActive&&S.session&&S.session._flash?' new-flash':'')+(s.archived?' archived':'');
el.className='session-item'+(isActive?' active':'')+(isActive&&S.session&&S.session._flash?' new-flash':'')+(s.archived?' archived':'')+(s.is_cli_session?' cli-session':'');
if(isActive&&S.session&&S.session._flash)delete S.session._flash;
const rawTitle=s.title||'Untitled';
const tags=(rawTitle.match(/#[\w-]+/g)||[]);
@@ -368,6 +368,12 @@ function renderSessionListFromCache(){
_clickTimer=setTimeout(async()=>{
_clickTimer=null;
if(_renamingSid) return;
// For CLI sessions, import into WebUI store first (idempotent)
if(s.is_cli_session){
try{
await api('/api/session/import_cli',{method:'POST',body:JSON.stringify({session_id:s.session_id})});
}catch(e){ /* import failed -- fall through to read-only view */ }
}
await loadSession(s.session_id);renderSessionListFromCache();
if(typeof closeMobileSidebar==='function')closeMobileSidebar();
}, 220);

View File

@@ -688,3 +688,19 @@ body.resizing{user-select:none;cursor:col-resize;}
.thinking-card-body pre{font-family:'SF Mono',ui-monospace,monospace;font-size:11px;line-height:1.5;color:var(--muted);white-space:pre-wrap;word-break:break-word;margin:0;}
.bg-error-banner{background:rgba(229,62,62,.15);border:1px solid rgba(229,62,62,.3);color:#fca5a5;padding:8px 16px;font-size:12px;display:flex;align-items:center;justify-content:space-between;gap:12px;border-radius:0;}
/* ── CLI session items in sidebar ── */
.session-item.cli-session {
border-left-color: var(--gold);
}
.session-item.cli-session::after {
content: 'cli';
font-size: 9px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: .04em;
color: var(--gold);
opacity: .5;
margin-left: 4px;
flex-shrink: 0;
}