Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## What is this

vibe-replay turns AI coding sessions into animated, interactive web replays as self-contained HTML files. Supports Claude Code, Cursor, and Codex.
vibe-replay turns AI coding sessions into animated, interactive web replays as self-contained HTML files. Supports Claude Code, Cursor, Codex, OpenCode, and Hermes.

pnpm monorepo: `packages/cli` (npm: `vibe-replay`), `packages/viewer` (React → single HTML), `packages/types` (shared types), `website/` (Astro), `cloudflare/` (Workers API).

Expand Down Expand Up @@ -55,7 +55,8 @@ pnpm db:migrate:remote # Apply to production D1 (requires auth)
- **Multi-file sessions**: Claude Code `/resume` creates new JSONL files. Parser accepts `string | string[]` and merges by slug+project.
- **Cursor tri-source**: Sessions come from SQLite `store.db` (primary), `globalStorage/state.vscdb`, or JSONL (fallback). Discovery merges all sources. DB data is source of truth; JSONL supplements missing thinking/images.
- **Cursor SDK**: SDK agents (TypeScript `@cursor/sdk`) write to `~/.cursor/projects/<workspace>/sdk-agent-store/<projectHash>/index.db` (tables: `agents`, `runs`, `run_events`) and a parallel JSONL transcript at `agent-transcripts/<agentId>/<agentId>.jsonl`. The transcript is the source of user prompts (SDK doesn't store them in events) and the SDK index.db supplies tool *results*, structured per-run timing, and per-turn model. See `packages/provider-cursor/src/cursor/sdk-reader.ts`. Detection is by sessionId prefix `agent-` — IDE chat sessions (UUID-only) skip the SDK SQLite probe.
- **opencode**: Sessions live in a SQLite DB at `~/.local/share/opencode/opencode.db` (`%LOCALAPPDATA%\opencode` on Windows, `OPENCODE_DATA` env var wins). Tables: `session`/`message`/`part`; role/user and tool payloads are JSON in `message.data`/`part.data`. Discovery writes `<dbPath>#session:<id>` marker paths. Parse is SQLite-backed (`parseSessionFromDb`), so background scan uses a lightweight path from discovery-computed stats (`buildLightweightOpencodeScanResult` in `scanner.ts`) to avoid opening the DB per session. `sql.js` named-param binds need their `:`-prefix in the key — this provider uses positional `?` params instead. See `packages/provider-opencode/`.
- **opencode**: Sessions live in a SQLite DB at `~/.local/share/opencode/opencode.db` (`%LOCALAPPDATA%\\opencode` on Windows, `OPENCODE_DATA` env var wins). Tables: `session`/`message`/`part`; role/user and tool payloads are JSON in `message.data`/`part.data`. Discovery writes `<dbPath>#session:<id>` marker paths. Parse is SQLite-backed (`parseSessionFromDb`), so background scan uses a lightweight path from discovery-computed stats (`buildLightweightOpencodeScanResult` in `scanner.ts`) to avoid opening the DB per session. `sql.js` named-param binds need their `:`-prefix in the key — this provider uses positional `?` params instead. See `packages/provider-opencode/`.
- **Hermes**: Sessions live in SQLite at `~/.hermes/state.db` (FTS5-backed). Tables: `sessions` (token/cost/git columns maintained by Hermes) + `messages` (OpenAI-style: assistant rows carry `tool_calls` JSON + `reasoning`; tool rows carry `tool_name` + `tool_call_id` + result content). Discovery writes `<dbPath>#session:<id>` marker paths and reads `~/.hermes/.update_check` for the version. Context compaction is recorded two ways: `compacted=1` rows (pre-compaction history, kept in full) and a user row prefixed `[CONTEXT COMPACTION` (→ `subtype: "compaction-summary"`, mirroring claude-code). Parse is SQLite-backed, so background scan uses `buildLightweightHermesScanResult` in `scanner.ts`. Tool names map to the viewer vocabulary in `tool-mapping.ts`. See `packages/provider-hermes/`.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- **Skip `progress` lines**: These are subagent streaming artifacts in JSONL.
- **sql.js (WASM)**: Used instead of native SQLite bindings for portability — no C++ compiler needed.
- **Session discovery cache**: CLI picker + local dashboard use file cache at `~/.vibe-replay/cache/*.json` (stale-while-refresh UX). Cache validity is tied to CLI release version (`CLI_VERSION`) plus envelope version, so caches auto-invalidate across releases. Keep cache writes best-effort and never block generation/parsing on cache failures.
Expand Down Expand Up @@ -103,6 +104,7 @@ If tag/release is updated but `packages/cli/package.json` is not, CLI will still
| Provider interface | `packages/provider-contract/src/index.ts` (contract) / `packages/providers-default/src/index.ts` (registry) |
| Cursor SDK reader | `packages/provider-cursor/src/cursor/sdk-reader.ts` |
| opencode provider | `packages/provider-opencode/src/opencode/` |
| Hermes provider | `packages/provider-hermes/src/hermes/` |
| Viewer entry | `packages/viewer/src/App.tsx` |
| Playback engine (pure) | `packages/viewer/src/engine/` |
| Playback hook | `packages/viewer/src/hooks/usePlayback.ts` |
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"test:e2e": "vitest run --config e2e/vitest.config.ts",
"lint": "oxlint --fix packages/ website/src cloudflare/src && oxfmt packages/ website/src cloudflare/src",
"lint:check": "oxlint packages/ website/src cloudflare/src && oxfmt --check packages/ website/src cloudflare/src",
"typecheck": "tsc --noEmit -p packages/types/tsconfig.json && tsc --noEmit -p packages/provider-contract/tsconfig.json && tsc --noEmit -p packages/provider-core/tsconfig.json && tsc --noEmit -p packages/provider-claude-code/tsconfig.json && tsc --noEmit -p packages/provider-codex/tsconfig.json && tsc --noEmit -p packages/provider-cursor/tsconfig.json && tsc --noEmit -p packages/provider-opencode/tsconfig.json && tsc --noEmit -p packages/provider-pi/tsconfig.json && tsc --noEmit -p packages/providers-default/tsconfig.json && tsc --noEmit -p packages/replay-core/tsconfig.json && tsc --noEmit -p packages/cli/tsconfig.json && tsc --noEmit -p packages/viewer/tsconfig.json && tsc --noEmit -p cloudflare/tsconfig.json && pnpm --filter @vibe-replay/website check",
"typecheck": "tsc --noEmit -p packages/types/tsconfig.json && tsc --noEmit -p packages/provider-contract/tsconfig.json && tsc --noEmit -p packages/provider-core/tsconfig.json && tsc --noEmit -p packages/provider-claude-code/tsconfig.json && tsc --noEmit -p packages/provider-codex/tsconfig.json && tsc --noEmit -p packages/provider-cursor/tsconfig.json && tsc --noEmit -p packages/provider-hermes/tsconfig.json && tsc --noEmit -p packages/provider-opencode/tsconfig.json && tsc --noEmit -p packages/provider-pi/tsconfig.json && tsc --noEmit -p packages/providers-default/tsconfig.json && tsc --noEmit -p packages/replay-core/tsconfig.json && tsc --noEmit -p packages/cli/tsconfig.json && tsc --noEmit -p packages/viewer/tsconfig.json && tsc --noEmit -p cloudflare/tsconfig.json && pnpm --filter @vibe-replay/website check",
"fmt": "oxfmt packages/ website/src cloudflare/src",
"prepare": "lefthook install"
},
Expand Down
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"@vibe-replay/provider-contract": "workspace:*",
"@vibe-replay/provider-core": "workspace:*",
"@vibe-replay/provider-cursor": "workspace:*",
"@vibe-replay/provider-hermes": "workspace:*",
"@vibe-replay/provider-opencode": "workspace:*",
"@vibe-replay/provider-pi": "workspace:*",
"@vibe-replay/providers-default": "workspace:*",
Expand Down
35 changes: 33 additions & 2 deletions packages/cli/src/feedback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ function spawnTool(command: string, args: string[], options: SpawnOptions): Pipe
// ---------------------------------------------------------------------------

export interface FeedbackTool {
name: "claude" | "agent" | "opencode";
name: "claude" | "agent" | "opencode" | "hermes";
command: string;
}

Expand Down Expand Up @@ -81,7 +81,7 @@ export interface FeedbackResult {
// Detection
// ---------------------------------------------------------------------------

const TOOL_PRIORITY: FeedbackTool["name"][] = ["claude", "agent", "opencode"];
const TOOL_PRIORITY: FeedbackTool["name"][] = ["claude", "agent", "opencode", "hermes"];

/** Detect available AI CLI tools and pick a default by priority. */
export async function detectFeedbackTools(): Promise<{
Expand All @@ -95,6 +95,7 @@ export async function detectFeedbackTools(): Promise<{
...(!insideClaude ? [{ name: "claude" as const, cmd: "claude" }] : []),
{ name: "agent" as const, cmd: "agent" },
{ name: "opencode" as const, cmd: "opencode" },
{ name: "hermes" as const, cmd: "hermes" },
];

const tools: FeedbackTool[] = [];
Expand Down Expand Up @@ -371,6 +372,9 @@ async function executeFeedback(prompt: string, tool: FeedbackTool): Promise<stri
if (tool.name === "agent") {
return runAgent(prompt, tool.command);
}
if (tool.name === "hermes") {
return runHermes(prompt, tool.command);
}
return runOpencode(prompt, tool.command);
}

Expand Down Expand Up @@ -436,6 +440,33 @@ function runOpencode(prompt: string, cmd: string): Promise<string> {
});
}

function runHermes(prompt: string, cmd: string): Promise<string> {
return new Promise((resolve, reject) => {
// `hermes chat -q` runs a single query non-interactively; `-Q` (quiet)
// suppresses the banner/spinner so stdout is the final response only.
const proc = spawnTool(cmd, ["chat", "-q", prompt, "-Q", "--no-restore-cwd"], {
env: { ...process.env, NO_COLOR: "1", TERM: "dumb" },
timeout: 600_000,
stdio: ["pipe", "pipe", "pipe"],
Comment on lines +447 to +450

@coderabbitai coderabbitai Bot Aug 5, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- feedback.ts structure and relevant symbols ---'
ast-grep outline packages/cli/src/feedback.ts
printf '%s\n' '--- feedback.ts lines 1-180 ---'
sed -n '1,180p' packages/cli/src/feedback.ts
printf '%s\n' '--- feedback.ts lines 380-490 ---'
sed -n '380,490p' packages/cli/src/feedback.ts
printf '%s\n' '--- feedback.ts references to spawnTool, prompt, and stdin writes ---'
rg -n -C 4 'spawnTool|prompt|stdin|write\(' packages/cli/src/feedback.ts

Repository: tuo-lei/vibe-replay

Length of output: 30524


🏁 Script executed:

#!/bin/bash
set -e
node - <<'JS'
const { spawn } = require("node:child_process");
const secret = "source code credential PII marker";
const child = spawn(process.execPath, ["-e", "setTimeout(() => {}, 2000)", secret], {
  stdio: "ignore",
});
const cmdline = require("node:fs").readFileSync(`/proc/${child.pid}/cmdline`, "utf8").replace(/\0/g, " ");
console.log(JSON.stringify({ secretVisibleInProcCmdline: cmdline.includes(secret), cmdline }));
child.kill();
JS

Repository: tuo-lei/vibe-replay

Length of output: 290


Sensitive Data Exposure (CWE-200): Exposure of Sensitive Information to an Unauthorized Actor

Reachability: External · Exploitability: Moderate

Reachability path
● Entry
  packages/cli/src/index.ts:672
  startServer
│
▼
● Hop
  packages/cli/src/server.ts:586
  startServer: After replays change, sync the sources cache so existingReplay / replay stay consistent
│
▼
● Sink
  packages/cli/src/feedback.ts

Keep the Hermes prompt out of the process command line.

Pass prompt through stdin or another non-command-line input. It can contain source code, credentials, or PII.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/feedback.ts` around lines 447 - 450, Update the spawnTool
invocation in the feedback flow so prompt is no longer passed as the "chat"
command-line argument. Send the prompt through the spawned process's stdin or
another supported non-command-line input while preserving the existing command
behavior and stdio handling.

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- feedback.ts outline ---'
ast-grep outline packages/cli/src/feedback.ts
printf '%s\n' '--- relevant implementation ---'
sed -n '1,140p' packages/cli/src/feedback.ts
sed -n '400,490p' packages/cli/src/feedback.ts
printf '%s\n' '--- runHermes and spawnTool references ---'
rg -n -C 5 'spawnTool|runHermes|prompt' packages/cli/src/feedback.ts
printf '%s\n' '--- feedback entry points ---'
rg -n -C 4 'runHermes|generateFeedback|feedback|replay' packages/cli/src --glob '*.ts' --glob '*.tsx' | head -n 240

Repository: tuo-lei/vibe-replay

Length of output: 46350


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- generateFeedback implementation ---'
sed -n '920,970p' packages/cli/src/feedback.ts
printf '%s\n' '--- all generateFeedback call sites ---'
rg -n -C 8 'generateFeedback\(' packages/cli/src packages/cli/test packages/cli/tests 2>/dev/null || true
printf '%s\n' '--- executeFeedback and tool command flow ---'
sed -n '1340,1405p' packages/cli/src/feedback.ts
printf '%s\n' '--- package scripts and test files mentioning feedback ---'
rg -n -C 3 'feedback|generateFeedback|detectFeedbackTools' packages/cli/package.json packages/cli --glob '*test*' --glob '*spec*' --glob '*.json' 2>/dev/null | head -n 240

Repository: tuo-lei/vibe-replay

Length of output: 22205


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- feedback route and surrounding middleware ---'
sed -n '2640,2765p' packages/cli/src/server.ts
printf '%s\n' '--- route registration and auth-related middleware ---'
rg -n -C 5 'feedback|ai-coach|auth|middleware|bearer|basic|session' packages/cli/src/server.ts | head -n 320
printf '%s\n' '--- loadSessionFromDisk definition and callers ---'
rg -n -C 8 'function loadSessionFromDisk|loadSessionFromDisk\(' packages/cli/src --glob '*.ts'

Repository: tuo-lei/vibe-replay

Length of output: 29491


🌐 Web query:

Node.js child_process spawn Windows shell true args cmd.exe metacharacters argument quoting official documentation

💡 Result:

When using child_process.spawn with the shell: true option on Windows, Node.js executes the command inside a shell, typically cmd.exe (as specified by process.env.ComSpec) [1][2]. Security Implications and Metacharacters Enabling shell: true significantly changes how arguments are handled and introduces severe security risks [3][4]. 1. String Concatenation: When shell: true is used, the provided command and arguments are concatenated into a single string and passed to the shell [5][6]. The shell then interprets this string [4]. 2. Command Injection: Because the shell interprets the resulting string, any input containing shell metacharacters—such as &, &&, |,;, <, >, or ^—can be used to trigger arbitrary command execution [5][4]. If user-controlled input is included in the command or arguments without rigorous sanitization, an attacker can escape the intended command and execute malicious code [3][4]. 3. False Sense of Security: The child_process.spawn interface, which accepts an array for arguments, suggests that arguments are passed safely as distinct parameters [3][4]. When shell: true is enabled, this safety mechanism is bypassed, as the array elements are simply joined with spaces before being sent to the shell [5][4]. Official Guidance and Deprecation Node.js documentation explicitly warns against passing unsanitized user input when shell: true is enabled [3]. Furthermore, passing an array of arguments (args) while using the shell option is deprecated (DEP0190) because it encourages the incorrect assumption that arguments will be handled safely [3][6]. The recommended practice is to pass a single command string when using shell: true, or to avoid the shell option entirely [6]. Quoting and Escaping When using shell: true, Node.js does not perform automatic quoting or escaping of arguments to protect against shell metacharacters [7][2]. If the executable path or arguments contain spaces, they must be quoted manually by the developer [1][2]. Given the inherent risks of command injection and the complexity of correctly quoting arguments for cmd.exe, using shell: true is generally discouraged [1][3]. For executing.bat or.cmd files on Windows without the risks associated with spawning a shell, developers are encouraged to seek safer alternatives, such as executing the script directly if possible or using established utilities that handle safe spawning [6].

Citations:


Injection (CWE-78): Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')

Reachability: External

Reachability path
● Entry
  packages/cli/src/index.ts:672
  startServer
│
▼
● Hop
  packages/cli/src/server.ts:586
  startServer: After replays change, sync the sources cache so existingReplay / replay stay consistent
│
▼
● Sink
  packages/cli/src/feedback.ts

Do not pass the replay prompt through a Windows shell. runHermes passes replay content as an argument while spawnTool enables shell: true; cmd.exe can interpret shell metacharacters. Pass the prompt through stdin or invoke a resolved executable without a shell.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/feedback.ts` around lines 447 - 450, Update the process
launch in runHermes to avoid passing the replay prompt through a Windows shell:
either write the prompt to the spawned process’s stdin and remove it from the
argument list, or invoke a resolved executable with shell disabled. Preserve the
existing chat options and timeout while preventing cmd.exe from interpreting
prompt metacharacters.

Source: Linters/SAST tools

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch on argv exposure — but Hermes only accepts a non-interactive query via -q argv: the CLI reads stdin only for interactive TUI commands (guarded by _require_tty in hermes_cli/main.py), and agent/ has no stdin query path. So there is no stdin channel to move the prompt to. Mitigations already in place: the feedback digest is truncated (max ~25KB) and this is the same trust model as the other tools (claude/agent/opencode also stream session digests to a subprocess). Left as-is with a comment; happy to revisit if Hermes adds stdin query support.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🤖 web_search executed:

Hermes CLI hermes chat -qstdin non-interactive input_require_tty hermes_cli main.py

Length of output: 14242


🤖 web_search executed:

site:github.com/NousResearch/hermes-agent "def cmd_chat" "_require_tty" "-q" "sys.stdin"

Length of output: 15114


@tuo-lei, thank you for the Hermes CLI detail. I confirmed that hermes chat -q <prompt> is the documented headless execution path. I found no supported stdin query option. The stdin-specific remediation does not apply, so I will not require it.

The Windows injection concern remains. runHermes passes the digest in an argument, and spawnTool uses shell: true on Windows. cmd.exe can interpret metacharacters in that digest. The size limit does not prevent this behavior.

Please use a shell-free Windows launch path for Hermes, or a supported Hermes invocation that isolates the prompt from cmd.exe. The argv exposure also remains a Hermes CLI limitation.

You are interacting with an AI system.

});

let stdout = "";
let stderr = "";
proc.stdout.on("data", (d) => (stdout += d.toString()));
proc.stderr.on("data", (d) => (stderr += d.toString()));

proc.on("close", (code) => {
if (code === 0) {
resolve(stripAnsi(stdout).trim());
} else {
reject(new Error(`hermes exited ${code}: ${stripAnsi(stderr).slice(0, 500)}`));
}
});

proc.on("error", (err) => reject(new Error(`Failed to start hermes: ${err.message}`)));
});
}

function runAgent(prompt: string, cmd: string): Promise<string> {
return new Promise((resolve, reject) => {
const proc = spawnTool(cmd, ["-p", "--output-format", "json", "--mode", "ask", "--trust"], {
Expand Down
8 changes: 5 additions & 3 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,9 +197,11 @@ function formatProviderBadge(provider: string): string {
? chalk.hex("#B392F0")("codex")
: provider === "opencode"
? chalk.hex("#22C55E")("opencode")
: provider === "pi"
? chalk.hex("#14B8A6")("pi")
: chalk.yellow(provider);
: provider === "hermes"
? chalk.hex("#F85149")("hermes")
: provider === "pi"
? chalk.hex("#14B8A6")("pi")
: chalk.yellow(provider);
}

function printParseWarnings(warnings?: ParseWarning[]): void {
Expand Down
31 changes: 31 additions & 0 deletions packages/cli/src/scanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,9 @@ export async function scanSession(input: ScanInput): Promise<SessionScanResult>
if (input.provider === "opencode") {
return buildLightweightOpencodeScanResult(input);
}
if (input.provider === "hermes") {
return buildLightweightHermesScanResult(input);
}

let startTime: string | undefined;
let endTime: string | undefined;
Expand Down Expand Up @@ -831,6 +834,34 @@ function buildLightweightOpencodeScanResult(input: ScanInput): SessionScanResult
};
}

function buildLightweightHermesScanResult(input: ScanInput): SessionScanResult {
const firstPrompt = input.firstPrompt || input.title;
return {
sessionId: input.sessionId,
provider: input.provider,
project: input.project,
slug: input.slug,
title: input.title,
firstPrompt,
startTime: input.timestamp,
promptCount: input.discoveryPromptCount ?? (firstPrompt ? 1 : 0),
toolCallCount: input.discoveryToolCallCount ?? 0,
editCount: input.discoveryEditCount ?? 0,
filesModified: [],
model: input.discoveryModel,
durationMs: input.discoveryDurationMs,
tokenUsage: input.discoveryTokenUsage,
costEstimate: input.discoveryCostEstimate,
Comment on lines +847 to +854

@coderabbitai coderabbitai Bot Aug 5, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Propagate Hermes token and cost metadata into ScanInput.

buildLightweightHermesScanResult reads input.discoveryTokenUsage and input.discoveryCostEstimate. The background-scan mapping in packages/cli/src/server.ts Lines 1121-1143 sets neither field. The Hermes scan path therefore drops token and cost data before it reaches insights. Add the producer assignments, or extend the shared session contract if discovery does not expose these fields, and cover the full discovery-to-scan path with a test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/scanner.ts` around lines 847 - 854, Update the
background-scan mapping in server.ts to populate discoveryTokenUsage and
discoveryCostEstimate from the discovery session before
buildLightweightHermesScanResult consumes them; if the session contract lacks
these fields, extend it accordingly. Add coverage for propagation through the
discovery-to-scan path.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed it would be nice, but this is a pre-existing, provider-wide gap: SessionInfo (provider-contract) has no tokenUsage/costEstimate fields, and the opencode lightweight scan (#420) has the identical blind spot — the producer assignments would have to be added to the shared contract for every provider. The rich token/cost data does reach insights via the full parse path (parseSessionFromDb reads session_model_usage). Tracking as a follow-up; kept consistent with opencode for now.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tuo-lei, understood. This is a valid provider-contract gap and is not specific to the Hermes implementation.

Keeping the Hermes lightweight scan consistent with OpenCode is reasonable for this PR. The full parse path preserves Hermes token and cost data through session_model_usage.

Would you like me to create a follow-up GitHub issue for adding token and cost metadata to SessionInfo and propagating it through lightweight scans?

You are interacting with an AI system.

subAgentCount: 0,
apiErrorCount: 0,
compactionCount: 0,
dataSource: "sqlite",
dataQualityNotes: [
"Hermes details are read from its SQLite database (~/.hermes/state.db); rich per-file edit counts are resolved when a replay is generated.",
],
};
}

function buildScanResultFromParsed(
input: ScanInput,
parsed: ProviderParseResult,
Expand Down
17 changes: 14 additions & 3 deletions packages/cli/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2415,6 +2415,8 @@ export async function startServer(
return auth.stdout.includes("0 credentials") ? "no credentials" : "configured";
},
),
hermes: () =>
checkCli("hermes", "Hermes", "AI feedback via headless mode", "hermes", ["--version"]),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
};

const requestedTool = c.req.query("tool");
Expand Down Expand Up @@ -2711,7 +2713,10 @@ export async function startServer(
const requestedToolName = typeof body.toolName === "string" ? body.toolName : undefined;
const detected = await detectFeedbackTools();
if (detected.tools.length === 0) {
return c.json({ error: "No AI CLI tool available (claude, agent, or opencode)" }, 400);
return c.json(
{ error: "No AI CLI tool available (claude, agent, opencode, or hermes)" },
400,
);
}
const tool = requestedToolName
? detected.tools.find((t) => t.name === requestedToolName) || null
Expand Down Expand Up @@ -2779,7 +2784,10 @@ export async function startServer(
.catch(() => ({}));
const detected = await detectFeedbackTools();
if (detected.tools.length === 0) {
return c.json({ error: "No AI CLI tool available (claude, agent, or opencode)" }, 400);
return c.json(
{ error: "No AI CLI tool available (claude, agent, opencode, or hermes)" },
400,
);
}
const toolName = typeof body.toolName === "string" ? body.toolName : undefined;
const tool = toolName
Expand Down Expand Up @@ -2835,7 +2843,10 @@ export async function startServer(
.catch(() => ({}));
const detected = await detectFeedbackTools();
if (detected.tools.length === 0) {
return c.json({ error: "No AI CLI tool available (claude, agent, or opencode)" }, 400);
return c.json(
{ error: "No AI CLI tool available (claude, agent, opencode, or hermes)" },
400,
);
}
const toolName = typeof body.toolName === "string" ? body.toolName : undefined;
const tool = toolName
Expand Down
1 change: 1 addition & 0 deletions packages/cli/test/provider-contract-package.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ describe("provider contract package boundary", () => {
"claude-code",
"codex",
"cursor",
"hermes",
"opencode",
"pi",
]);
Expand Down
50 changes: 50 additions & 0 deletions packages/provider-hermes/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"name": "@vibe-replay/provider-hermes",
"version": "0.0.1",
"private": true,
"license": "MIT",
"files": [
"src"
],
"type": "module",
"exports": {
".": {
"types": "./src/hermes/index.ts",
"import": "./src/hermes/index.ts"
},
"./config": {
"types": "./src/hermes/config.ts",
"import": "./src/hermes/config.ts"
},
"./discover": {
"types": "./src/hermes/discover.ts",
"import": "./src/hermes/discover.ts"
},
"./parser": {
"types": "./src/hermes/parser.ts",
"import": "./src/hermes/parser.ts"
},
"./sqlite": {
"types": "./src/hermes/sqlite.ts",
"import": "./src/hermes/sqlite.ts"
},
"./tool-mapping": {
"types": "./src/hermes/tool-mapping.ts",
"import": "./src/hermes/tool-mapping.ts"
}
},
"scripts": {
"test": "vitest run"
},
"dependencies": {
"@vibe-replay/provider-contract": "workspace:*",
"@vibe-replay/provider-core": "workspace:*",
"@vibe-replay/types": "workspace:*",
"sql.js": "^1.14.1"
},
"devDependencies": {
"@types/node": "^25.9.3",
"@vibe-replay/replay-core": "workspace:*",
"vitest": "^4.1.9"
}
}
3 changes: 3 additions & 0 deletions packages/provider-hermes/src/hermes/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { hermesDataDir, hermesDbPath } from "./sqlite.js";

export { hermesDataDir, hermesDbPath };
Loading