-
Notifications
You must be signed in to change notification settings - Fork 5
feat(hermes): add Hermes Agent provider support #421
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
| } | ||
|
|
||
|
|
@@ -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<{ | ||
|
|
@@ -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[] = []; | ||
|
|
@@ -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); | ||
| } | ||
|
|
||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.tsRepository: 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();
JSRepository: 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 pathKeep the Hermes prompt out of the process command line. Pass 🤖 Prompt for AI Agents🔒 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 240Repository: 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 240Repository: 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:
💡 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 pathDo not pass the replay prompt through a Windows shell. 🤖 Prompt for AI AgentsSource: Linters/SAST tools
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🤖 web_search executed:
Length of output: 14242 🤖 web_search executed:
Length of output: 15114
The Windows injection concern remains. Please use a shell-free Windows launch path for Hermes, or a supported Hermes invocation that isolates the prompt from 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"], { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
🤖 Prompt for AI Agents
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Keeping the Hermes lightweight scan consistent with OpenCode is reasonable for this PR. The full parse path preserves Hermes token and cost data through Would you like me to create a follow-up GitHub issue for adding token and cost metadata to 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, | ||
|
|
||
| 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" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| import { hermesDataDir, hermesDbPath } from "./sqlite.js"; | ||
|
|
||
| export { hermesDataDir, hermesDbPath }; |
Uh oh!
There was an error while loading. Please reload this page.