feat: add Hermes Agent support (export + live SQLite) - #32
Conversation
|
Thanks; a solid PR. A few things before merge:
Happy to merge after these. oh, and the 11 test-editor-server failures aren't Node-related; they happen when the repo is checked out outside $HOME. The tests load fixtures by absolute path and /api/load rejects anything outside the home dir (assertUnderHome), so everything downstream cascades. Nothing to do in this PR; I'll make the tests location-independent separately. |
…-aware parse - discover: scan all known Hermes DBs (default + every ~/.hermes/profiles/*/state.db), merging with dedup and timestamp sort. Respects HERMES_HOME when set. Previously only the default DB was read, so sessions in the active codex profile (and any named profile) were invisible in the dashboard. - parser: openAllHermesDbs + hintedDbPath fast-path. Marker paths like .../profiles/codex/state.db#session:ID already carry the correct DB; use that before scanning all DBs. Handles both the discovered-session and --session flows. - sqlite: new hermesDbPaths() / openAllHermesDbs() helpers; single-DB openHermesDb unchanged. - tool-mapping: add browser_* (navigate/click/type/snapshot/console/ scroll/press/get_images), cronjob, memory, and codex so the replay transform renders them with a first-class name. Matches claude-replay Hermes support built in parallel: es617/claude-replay#32
…-aware parse - discover: scan all known Hermes DBs (default + every ~/.hermes/profiles/*/state.db), merging with dedup and timestamp sort. Respects HERMES_HOME when set. Previously only the default DB was read, so sessions in the active codex profile (and any named profile) were invisible in the dashboard. - parser: openAllHermesDbs + hintedDbPath fast-path. Marker paths like .../profiles/codex/state.db#session:ID already carry the correct DB; use that before scanning all DBs. Handles both the discovered-session and --session flows. - sqlite: new hermesDbPaths() / openAllHermesDbs() helpers; single-DB openHermesDb unchanged. - tool-mapping: add browser_* (navigate/click/type/snapshot/console/ scroll/press/get_images), cronjob, memory, and codex so the replay transform renders them with a first-class name. Matches claude-replay Hermes support built in parallel: es617/claude-replay#32
- Parse Hermes raw export (hermes sessions export --format jsonl) —
the JSON envelope with { id, title, messages: [...] } where messages
use the SQLite row shape (role/content/tool_calls/tool_call_id).
Assistant tool_calls are correlated with role:"tool" rows by
tool_call_id. Tool output envelopes ({output}) are unwrapped.
- Read live sessions from SQLite (node:sqlite, no native deps) at
~/.hermes/state.db and ~/.hermes/profiles/*/state.db. Virtual paths
like ~/.hermes/state.db#session:ID flow through parser,
resolve-session, and the editor.
- CLI: session IDs now resolve against Hermes SQLite (exact + prefix),
virtual paths accepted as direct inputs, assistant label "Sandi" for
hermes turns, and title derived from session title when available.
- Editor discovery: new Hermes group (default + per-profile), search
handles virtual paths via SQLite reads.
- Tool name normalization (terminal->Bash, patch->Edit, etc.) so the
renderer picks the right scenes (shell output, diffs).
- Trace export (hermes sessions export --format trace) already worked
via the claude-code parser; this adds the raw-export path.
Fixes: hermes transcripts now render as replays without requiring an
intermediate trace conversion.
Co-Authored-By: Sandi Hermes <sandi@hermes.local>
- Remove unused existsSync import in parser.mjs (broke oxlint --deny-warnings) - Watch the underlying state.db for Hermes virtual paths so --watch works - Only derive titles via the Hermes extractor when the input is Hermes format, instead of reading and line-parsing every input on the hot path - SELECT * from messages so older/newer Hermes schemas still load - Default Hermes assistant label to "Hermes" instead of "Sandi" - Reuse hermes-db.mjs in resolve-session.mjs instead of a private sqlite loader; escape LIKE wildcards (Hermes IDs contain underscores) - Drop swallowed read errors in detectFormat and dead code in hermes.mjs - Add SQLite integration tests (reduced-schema load, virtual-path parse, ID resolution incl. wildcard escaping, profile discovery) - Add hermes fixture to the Turn-contract suite; update README
9a3aa53 to
422d10f
Compare
hermes-db.mjs used top-level await for the node:sqlite probe, which the es2020 browser bundle can't compile, and pulled node:path/node:os that the website build had no shims for. Probe synchronously via createRequire instead, stub the extra modules in the browser shim, and regenerate docs/index.html — the online demo now detects Hermes exports and mentions the format alongside the others.
Claude Replay already handles Claude Code / Cursor / Codex / Gemini / OpenCode / Kimi Code.
This PR adds Hermes Agent as a first-class source so the same viewer can replay Hermes transcripts without an intermediate conversion step.
What changed
Parser:
src/formats/hermes.mjs+src/formats/index.mjshermes sessions export --format jsonl) — the JSON envelope{ id, title, source, messages: [...] }wheremessagesuse the SQLite row shape (role/content/tool_calls/tool_call_id/timestamp/reasoning_content/compacted). Thetraceexport (--format trace) already worked via the claude-code parser; this adds the native raw-export path.tool_callson assistant rows is a JSON array of{ id, call_id, function:{name,arguments} }; tool results live in separaterole:"tool"rows matched bytool_call_id. The parser correlates them (handling bothidandcall_idkeys) and unwraps the standard{output}envelope so the renderer sees shell/file text rather than a JSON wrapper.terminal→Bash,read_file→Read,patch→Edit,write_file→Write, etc.) so the renderer's scene detection (shell output, diffs) triggers.Live SQLite:
src/hermes-db.mjs+src/parser.mjs~/.hermes/state.dband~/.hermes/profiles/*/state.dbusing the Node 22.5+ built-innode:sqlite(DatabaseSync) — no native dependency, graceful fallback on older Node.~/.hermes/state.db#session:<id>flows throughparser.mjs(parseTranscript/detectFormat) so the CLI and editor can operate on a live DB without an export step.ID resolution:
src/resolve-session.mjsresolveSessionId("20260819_142844_43efe7")now also searches Hermes SQLite (exact + prefix match). Example:claude-replay 20260819_142844_43efe7 -o out.htmlfinds the Hermes session and printsFound: Hermes / codex → …state.db#session:….CLI:
bin/claude-replay.mjsclaude-replay ~/.hermes/state.db#session:ID).Sandiforhermesformat.titlefield when available (otherwise falls back to directory basename, matching existing behaviour for export files).Editor:
src/editor-server.mjs/api/sessionsnow lists Hermes sessions;/api/searchhandles virtual paths via SQLite reads.How to try it
Tests
test/fixture-hermes.json+test/test-hermes.mjs(detect, turn count, tool round-trip, thinking blocks).test-editor-server.mjshas 11 pre-existing failures on this Node version (reproduces onmainwithout this PR) — no new failures introduced.Notes
node:sqliteis only used when available; without it, Hermes paths are simply not offered — existing formats are unaffected.src/formats/<name>.mjs+ registry entry) plus the new sharedsrc/hermes-db.mjsfor the editor/CLI SQLite plumbing.