@kolisachint/hoocode-agent 0.5.20 → 0.5.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.21] - 2026-08-16
4
+
5
+ ### Fixed
6
+
7
+ - Plugin hooks no longer crash the process when a hook exits without reading its
8
+ stdin. `runHookCommand` wrote the JSON payload inside a `try`, which catches
9
+ only synchronous throws: a hook that exits first (`exit 2`, any script
10
+ ignoring its input) made the write fail asynchronously, and `child.stdin`
11
+ emitted an unhandled `EPIPE`. Intermittently fatal in real sessions, and the
12
+ cause of the `bun-test (coding-agent)` failure on `main` where every test file
13
+ passed but the run still exited 1.
14
+
3
15
  ## [0.5.20] - 2026-08-16
4
16
 
5
17
  ## [0.5.19] - 2026-08-16
@@ -1 +1 @@
1
- {"version":3,"file":"hooks-bridge.d.ts","sourceRoot":"","sources":["../../../../src/core/extensions/plugins/hooks-bridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAkC,MAAM,aAAa,CAAC;AAChF,OAAO,KAAK,EAA6C,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAwFlG;;;GAGG;AACH,wBAAgB,kBAAkB,CACjC,EAAE,EAAE,YAAY,EAChB,KAAK,EAAE,iBAAiB,EACxB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAChC,IAAI,CAoGN","sourcesContent":["/**\n * Hooks bridge — runs Claude Code / native-plugin shell hooks against hoocode events.\n *\n * Claude Code hooks are shell commands wired to named events and matched by tool\n * name. hoocode hooks are TypeScript handlers on the {@link ExtensionEvent} union.\n * This bridge registers handlers that shell out per the hook protocol and translate\n * stdin JSON + exit codes + stdout JSON back into hoocode result objects.\n *\n * Protocol (faithful to Claude Code):\n * - Input: a JSON object on stdin describing the event.\n * - Exit 0: success. stdout may carry a JSON decision; for prompt/session events\n * plain stdout is treated as additional context.\n * - Exit 2: blocking error. stderr (or JSON `reason`) is the block reason.\n * - Other non-zero: non-blocking error (logged, not surfaced to the model).\n * - Optional stdout JSON: `{ decision: \"block\"|\"approve\", reason, permissionDecision }`.\n */\n\nimport { spawn } from \"node:child_process\";\nimport type { ExtensionAPI, ToolCallEvent, ToolResultEvent } from \"../types.js\";\nimport type { PluginHookCommand, PluginHookMatcherGroup, PluginHooksConfig } from \"./manifest.js\";\n\nconst DEFAULT_TIMEOUT_MS = 60_000;\n\ninterface HookRunResult {\n\texitCode: number;\n\tstdout: string;\n\tstderr: string;\n\tjson: { decision?: string; reason?: string; permissionDecision?: string; continue?: boolean } | undefined;\n}\n\n/** Run one shell hook command, piping `input` as JSON on stdin. */\nfunction runHookCommand(\n\tcmd: PluginHookCommand,\n\tinput: unknown,\n\troot: string,\n\tvars: Record<string, string>,\n): Promise<HookRunResult> {\n\treturn new Promise((resolve) => {\n\t\tconst child = spawn(cmd.command, {\n\t\t\tshell: true,\n\t\t\t// Every vendor spelling, so a hook written for either agent resolves:\n\t\t\t// the shell expands these, which is the hook-side equivalent of the\n\t\t\t// string substitution MCP configs get.\n\t\t\tenv: { ...process.env, ...vars },\n\t\t\tcwd: root,\n\t\t});\n\n\t\tlet stdout = \"\";\n\t\tlet stderr = \"\";\n\t\tconst timer = setTimeout(() => child.kill(\"SIGTERM\"), (cmd.timeout ?? DEFAULT_TIMEOUT_MS / 1000) * 1000);\n\t\ttimer.unref?.();\n\n\t\tchild.stdout.on(\"data\", (d) => {\n\t\t\tstdout += d.toString();\n\t\t});\n\t\tchild.stderr.on(\"data\", (d) => {\n\t\t\tstderr += d.toString();\n\t\t});\n\t\tchild.on(\"error\", () => {\n\t\t\tclearTimeout(timer);\n\t\t\tresolve({ exitCode: 1, stdout, stderr, json: undefined });\n\t\t});\n\t\tchild.on(\"close\", (code) => {\n\t\t\tclearTimeout(timer);\n\t\t\tlet json: HookRunResult[\"json\"];\n\t\t\tconst trimmed = stdout.trim();\n\t\t\tif (trimmed.startsWith(\"{\")) {\n\t\t\t\ttry {\n\t\t\t\t\tjson = JSON.parse(trimmed);\n\t\t\t\t} catch {\n\t\t\t\t\tjson = undefined;\n\t\t\t\t}\n\t\t\t}\n\t\t\tresolve({ exitCode: code ?? 0, stdout, stderr, json });\n\t\t});\n\n\t\ttry {\n\t\t\tchild.stdin.write(JSON.stringify(input));\n\t\t\tchild.stdin.end();\n\t\t} catch {\n\t\t\t/* child may have already exited */\n\t\t}\n\t});\n}\n\n/** Empty / \"*\" matcher matches everything; otherwise treat as an anchored regex on the tool name. */\nfunction matcherMatches(matcher: string | undefined, toolName: string): boolean {\n\tif (!matcher || matcher === \"*\") return true;\n\ttry {\n\t\treturn new RegExp(`^(?:${matcher})$`).test(toolName);\n\t} catch {\n\t\treturn matcher === toolName;\n\t}\n}\n\nfunction groupsForTool(groups: PluginHookMatcherGroup[], toolName: string): PluginHookCommand[] {\n\tconst cmds: PluginHookCommand[] = [];\n\tfor (const g of groups) {\n\t\tif (matcherMatches(g.matcher, toolName)) cmds.push(...g.hooks);\n\t}\n\treturn cmds;\n}\n\nfunction allCommands(groups: PluginHookMatcherGroup[]): PluginHookCommand[] {\n\treturn groups.flatMap((g) => g.hooks);\n}\n\n/**\n * Register all hook events for one plugin against the ExtensionAPI.\n * `onError` reports non-blocking failures (kept off the model's path).\n */\nexport function installPluginHooks(\n\tpi: ExtensionAPI,\n\thooks: PluginHooksConfig,\n\troot: string,\n\tvars: Record<string, string>,\n\tonError: (message: string) => void,\n): void {\n\t// ── PreToolUse → tool_call (blocking) ────────────────────────────────────\n\tconst preGroups = hooks.PreToolUse;\n\tif (preGroups?.length) {\n\t\tpi.on(\"tool_call\", async (event: ToolCallEvent) => {\n\t\t\tconst cmds = groupsForTool(preGroups, event.toolName);\n\t\t\tfor (const cmd of cmds) {\n\t\t\t\tconst res = await runHookCommand(\n\t\t\t\t\tcmd,\n\t\t\t\t\t{ hook_event_name: \"PreToolUse\", tool_name: event.toolName, tool_input: event.input },\n\t\t\t\t\troot,\n\t\t\t\t\tvars,\n\t\t\t\t);\n\t\t\t\tconst decision = res.json?.decision ?? res.json?.permissionDecision;\n\t\t\t\tif (res.exitCode === 2 || decision === \"block\" || decision === \"deny\") {\n\t\t\t\t\treturn { block: true, reason: res.json?.reason || res.stderr.trim() || \"Blocked by plugin hook\" };\n\t\t\t\t}\n\t\t\t\tif (res.exitCode !== 0) onError(`PreToolUse hook failed (${res.exitCode}): ${res.stderr.trim()}`);\n\t\t\t}\n\t\t});\n\t}\n\n\t// ── PostToolUse → tool_result (best-effort) ──────────────────────────────\n\tconst postGroups = hooks.PostToolUse;\n\tif (postGroups?.length) {\n\t\tpi.on(\"tool_result\", async (event: ToolResultEvent) => {\n\t\t\tconst cmds = groupsForTool(postGroups, event.toolName);\n\t\t\tfor (const cmd of cmds) {\n\t\t\t\tconst res = await runHookCommand(\n\t\t\t\t\tcmd,\n\t\t\t\t\t{\n\t\t\t\t\t\thook_event_name: \"PostToolUse\",\n\t\t\t\t\t\ttool_name: event.toolName,\n\t\t\t\t\t\ttool_input: event.input,\n\t\t\t\t\t\ttool_response: event.content,\n\t\t\t\t\t},\n\t\t\t\t\troot,\n\t\t\t\t\tvars,\n\t\t\t\t);\n\t\t\t\tif (res.exitCode === 2 || res.json?.decision === \"block\") {\n\t\t\t\t\tconst reason = res.json?.reason || res.stderr.trim() || \"Flagged by plugin hook\";\n\t\t\t\t\treturn {\n\t\t\t\t\t\tcontent: [...event.content, { type: \"text\" as const, text: `\\n[plugin hook] ${reason}` }],\n\t\t\t\t\t\tisError: true,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\tif (res.exitCode !== 0) onError(`PostToolUse hook failed (${res.exitCode}): ${res.stderr.trim()}`);\n\t\t\t}\n\t\t});\n\t}\n\n\t// ── UserPromptSubmit → before_agent_start (adds context) ─────────────────\n\tconst promptGroups = hooks.UserPromptSubmit;\n\tif (promptGroups?.length) {\n\t\tpi.on(\"before_agent_start\", async (event) => {\n\t\t\tlet systemPrompt = event.systemPrompt;\n\t\t\tfor (const cmd of allCommands(promptGroups)) {\n\t\t\t\tconst res = await runHookCommand(\n\t\t\t\t\tcmd,\n\t\t\t\t\t{ hook_event_name: \"UserPromptSubmit\", prompt: event.prompt },\n\t\t\t\t\troot,\n\t\t\t\t\tvars,\n\t\t\t\t);\n\t\t\t\tif (res.exitCode !== 0 && res.exitCode !== 2) {\n\t\t\t\t\tonError(`UserPromptSubmit hook failed (${res.exitCode}): ${res.stderr.trim()}`);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst extra = res.exitCode === 2 ? res.stderr.trim() : res.json?.reason || res.stdout.trim();\n\t\t\t\tif (extra) systemPrompt = `${systemPrompt}\\n\\n<!-- plugin hook -->\\n${extra}`;\n\t\t\t}\n\t\t\treturn systemPrompt === event.systemPrompt ? undefined : { systemPrompt };\n\t\t});\n\t}\n\n\t// ── SessionStart → session_start (side effects) ──────────────────────────\n\tconst sessionGroups = hooks.SessionStart;\n\tif (sessionGroups?.length) {\n\t\tpi.on(\"session_start\", async (event) => {\n\t\t\tfor (const cmd of allCommands(sessionGroups)) {\n\t\t\t\tconst res = await runHookCommand(\n\t\t\t\t\tcmd,\n\t\t\t\t\t{ hook_event_name: \"SessionStart\", source: event.reason },\n\t\t\t\t\troot,\n\t\t\t\t\tvars,\n\t\t\t\t);\n\t\t\t\tif (res.exitCode !== 0) onError(`SessionStart hook failed (${res.exitCode}): ${res.stderr.trim()}`);\n\t\t\t}\n\t\t});\n\t}\n\n\t// ── Stop → agent_end (side effects) ──────────────────────────────────────\n\tconst stopGroups = hooks.Stop;\n\tif (stopGroups?.length) {\n\t\tpi.on(\"agent_end\", async () => {\n\t\t\tfor (const cmd of allCommands(stopGroups)) {\n\t\t\t\tconst res = await runHookCommand(cmd, { hook_event_name: \"Stop\" }, root, vars);\n\t\t\t\tif (res.exitCode !== 0) onError(`Stop hook failed (${res.exitCode}): ${res.stderr.trim()}`);\n\t\t\t}\n\t\t});\n\t}\n}\n"]}
1
+ {"version":3,"file":"hooks-bridge.d.ts","sourceRoot":"","sources":["../../../../src/core/extensions/plugins/hooks-bridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAkC,MAAM,aAAa,CAAC;AAChF,OAAO,KAAK,EAA6C,iBAAiB,EAAE,MAAM,eAAe,CAAC;AA4FlG;;;GAGG;AACH,wBAAgB,kBAAkB,CACjC,EAAE,EAAE,YAAY,EAChB,KAAK,EAAE,iBAAiB,EACxB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAChC,IAAI,CAoGN","sourcesContent":["/**\n * Hooks bridge — runs Claude Code / native-plugin shell hooks against hoocode events.\n *\n * Claude Code hooks are shell commands wired to named events and matched by tool\n * name. hoocode hooks are TypeScript handlers on the {@link ExtensionEvent} union.\n * This bridge registers handlers that shell out per the hook protocol and translate\n * stdin JSON + exit codes + stdout JSON back into hoocode result objects.\n *\n * Protocol (faithful to Claude Code):\n * - Input: a JSON object on stdin describing the event.\n * - Exit 0: success. stdout may carry a JSON decision; for prompt/session events\n * plain stdout is treated as additional context.\n * - Exit 2: blocking error. stderr (or JSON `reason`) is the block reason.\n * - Other non-zero: non-blocking error (logged, not surfaced to the model).\n * - Optional stdout JSON: `{ decision: \"block\"|\"approve\", reason, permissionDecision }`.\n */\n\nimport { spawn } from \"node:child_process\";\nimport type { ExtensionAPI, ToolCallEvent, ToolResultEvent } from \"../types.js\";\nimport type { PluginHookCommand, PluginHookMatcherGroup, PluginHooksConfig } from \"./manifest.js\";\n\nconst DEFAULT_TIMEOUT_MS = 60_000;\n\ninterface HookRunResult {\n\texitCode: number;\n\tstdout: string;\n\tstderr: string;\n\tjson: { decision?: string; reason?: string; permissionDecision?: string; continue?: boolean } | undefined;\n}\n\n/** Run one shell hook command, piping `input` as JSON on stdin. */\nfunction runHookCommand(\n\tcmd: PluginHookCommand,\n\tinput: unknown,\n\troot: string,\n\tvars: Record<string, string>,\n): Promise<HookRunResult> {\n\treturn new Promise((resolve) => {\n\t\tconst child = spawn(cmd.command, {\n\t\t\tshell: true,\n\t\t\t// Every vendor spelling, so a hook written for either agent resolves:\n\t\t\t// the shell expands these, which is the hook-side equivalent of the\n\t\t\t// string substitution MCP configs get.\n\t\t\tenv: { ...process.env, ...vars },\n\t\t\tcwd: root,\n\t\t});\n\n\t\tlet stdout = \"\";\n\t\tlet stderr = \"\";\n\t\tconst timer = setTimeout(() => child.kill(\"SIGTERM\"), (cmd.timeout ?? DEFAULT_TIMEOUT_MS / 1000) * 1000);\n\t\ttimer.unref?.();\n\n\t\tchild.stdout.on(\"data\", (d) => {\n\t\t\tstdout += d.toString();\n\t\t});\n\t\tchild.stderr.on(\"data\", (d) => {\n\t\t\tstderr += d.toString();\n\t\t});\n\t\t// A hook that exits without draining stdin (block.sh, `exit 2`, any script\n\t\t// that ignores its input) makes the payload write below fail asynchronously.\n\t\t// stdin then emits EPIPE, which is fatal to the process if unhandled.\n\t\tchild.stdin.on(\"error\", () => {});\n\t\tchild.on(\"error\", () => {\n\t\t\tclearTimeout(timer);\n\t\t\tresolve({ exitCode: 1, stdout, stderr, json: undefined });\n\t\t});\n\t\tchild.on(\"close\", (code) => {\n\t\t\tclearTimeout(timer);\n\t\t\tlet json: HookRunResult[\"json\"];\n\t\t\tconst trimmed = stdout.trim();\n\t\t\tif (trimmed.startsWith(\"{\")) {\n\t\t\t\ttry {\n\t\t\t\t\tjson = JSON.parse(trimmed);\n\t\t\t\t} catch {\n\t\t\t\t\tjson = undefined;\n\t\t\t\t}\n\t\t\t}\n\t\t\tresolve({ exitCode: code ?? 0, stdout, stderr, json });\n\t\t});\n\n\t\ttry {\n\t\t\tchild.stdin.write(JSON.stringify(input));\n\t\t\tchild.stdin.end();\n\t\t} catch {\n\t\t\t/* child already exited: the payload is best-effort, the exit code is not */\n\t\t}\n\t});\n}\n\n/** Empty / \"*\" matcher matches everything; otherwise treat as an anchored regex on the tool name. */\nfunction matcherMatches(matcher: string | undefined, toolName: string): boolean {\n\tif (!matcher || matcher === \"*\") return true;\n\ttry {\n\t\treturn new RegExp(`^(?:${matcher})$`).test(toolName);\n\t} catch {\n\t\treturn matcher === toolName;\n\t}\n}\n\nfunction groupsForTool(groups: PluginHookMatcherGroup[], toolName: string): PluginHookCommand[] {\n\tconst cmds: PluginHookCommand[] = [];\n\tfor (const g of groups) {\n\t\tif (matcherMatches(g.matcher, toolName)) cmds.push(...g.hooks);\n\t}\n\treturn cmds;\n}\n\nfunction allCommands(groups: PluginHookMatcherGroup[]): PluginHookCommand[] {\n\treturn groups.flatMap((g) => g.hooks);\n}\n\n/**\n * Register all hook events for one plugin against the ExtensionAPI.\n * `onError` reports non-blocking failures (kept off the model's path).\n */\nexport function installPluginHooks(\n\tpi: ExtensionAPI,\n\thooks: PluginHooksConfig,\n\troot: string,\n\tvars: Record<string, string>,\n\tonError: (message: string) => void,\n): void {\n\t// ── PreToolUse → tool_call (blocking) ────────────────────────────────────\n\tconst preGroups = hooks.PreToolUse;\n\tif (preGroups?.length) {\n\t\tpi.on(\"tool_call\", async (event: ToolCallEvent) => {\n\t\t\tconst cmds = groupsForTool(preGroups, event.toolName);\n\t\t\tfor (const cmd of cmds) {\n\t\t\t\tconst res = await runHookCommand(\n\t\t\t\t\tcmd,\n\t\t\t\t\t{ hook_event_name: \"PreToolUse\", tool_name: event.toolName, tool_input: event.input },\n\t\t\t\t\troot,\n\t\t\t\t\tvars,\n\t\t\t\t);\n\t\t\t\tconst decision = res.json?.decision ?? res.json?.permissionDecision;\n\t\t\t\tif (res.exitCode === 2 || decision === \"block\" || decision === \"deny\") {\n\t\t\t\t\treturn { block: true, reason: res.json?.reason || res.stderr.trim() || \"Blocked by plugin hook\" };\n\t\t\t\t}\n\t\t\t\tif (res.exitCode !== 0) onError(`PreToolUse hook failed (${res.exitCode}): ${res.stderr.trim()}`);\n\t\t\t}\n\t\t});\n\t}\n\n\t// ── PostToolUse → tool_result (best-effort) ──────────────────────────────\n\tconst postGroups = hooks.PostToolUse;\n\tif (postGroups?.length) {\n\t\tpi.on(\"tool_result\", async (event: ToolResultEvent) => {\n\t\t\tconst cmds = groupsForTool(postGroups, event.toolName);\n\t\t\tfor (const cmd of cmds) {\n\t\t\t\tconst res = await runHookCommand(\n\t\t\t\t\tcmd,\n\t\t\t\t\t{\n\t\t\t\t\t\thook_event_name: \"PostToolUse\",\n\t\t\t\t\t\ttool_name: event.toolName,\n\t\t\t\t\t\ttool_input: event.input,\n\t\t\t\t\t\ttool_response: event.content,\n\t\t\t\t\t},\n\t\t\t\t\troot,\n\t\t\t\t\tvars,\n\t\t\t\t);\n\t\t\t\tif (res.exitCode === 2 || res.json?.decision === \"block\") {\n\t\t\t\t\tconst reason = res.json?.reason || res.stderr.trim() || \"Flagged by plugin hook\";\n\t\t\t\t\treturn {\n\t\t\t\t\t\tcontent: [...event.content, { type: \"text\" as const, text: `\\n[plugin hook] ${reason}` }],\n\t\t\t\t\t\tisError: true,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\tif (res.exitCode !== 0) onError(`PostToolUse hook failed (${res.exitCode}): ${res.stderr.trim()}`);\n\t\t\t}\n\t\t});\n\t}\n\n\t// ── UserPromptSubmit → before_agent_start (adds context) ─────────────────\n\tconst promptGroups = hooks.UserPromptSubmit;\n\tif (promptGroups?.length) {\n\t\tpi.on(\"before_agent_start\", async (event) => {\n\t\t\tlet systemPrompt = event.systemPrompt;\n\t\t\tfor (const cmd of allCommands(promptGroups)) {\n\t\t\t\tconst res = await runHookCommand(\n\t\t\t\t\tcmd,\n\t\t\t\t\t{ hook_event_name: \"UserPromptSubmit\", prompt: event.prompt },\n\t\t\t\t\troot,\n\t\t\t\t\tvars,\n\t\t\t\t);\n\t\t\t\tif (res.exitCode !== 0 && res.exitCode !== 2) {\n\t\t\t\t\tonError(`UserPromptSubmit hook failed (${res.exitCode}): ${res.stderr.trim()}`);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst extra = res.exitCode === 2 ? res.stderr.trim() : res.json?.reason || res.stdout.trim();\n\t\t\t\tif (extra) systemPrompt = `${systemPrompt}\\n\\n<!-- plugin hook -->\\n${extra}`;\n\t\t\t}\n\t\t\treturn systemPrompt === event.systemPrompt ? undefined : { systemPrompt };\n\t\t});\n\t}\n\n\t// ── SessionStart → session_start (side effects) ──────────────────────────\n\tconst sessionGroups = hooks.SessionStart;\n\tif (sessionGroups?.length) {\n\t\tpi.on(\"session_start\", async (event) => {\n\t\t\tfor (const cmd of allCommands(sessionGroups)) {\n\t\t\t\tconst res = await runHookCommand(\n\t\t\t\t\tcmd,\n\t\t\t\t\t{ hook_event_name: \"SessionStart\", source: event.reason },\n\t\t\t\t\troot,\n\t\t\t\t\tvars,\n\t\t\t\t);\n\t\t\t\tif (res.exitCode !== 0) onError(`SessionStart hook failed (${res.exitCode}): ${res.stderr.trim()}`);\n\t\t\t}\n\t\t});\n\t}\n\n\t// ── Stop → agent_end (side effects) ──────────────────────────────────────\n\tconst stopGroups = hooks.Stop;\n\tif (stopGroups?.length) {\n\t\tpi.on(\"agent_end\", async () => {\n\t\t\tfor (const cmd of allCommands(stopGroups)) {\n\t\t\t\tconst res = await runHookCommand(cmd, { hook_event_name: \"Stop\" }, root, vars);\n\t\t\t\tif (res.exitCode !== 0) onError(`Stop hook failed (${res.exitCode}): ${res.stderr.trim()}`);\n\t\t\t}\n\t\t});\n\t}\n}\n"]}
@@ -37,6 +37,10 @@ function runHookCommand(cmd, input, root, vars) {
37
37
  child.stderr.on("data", (d) => {
38
38
  stderr += d.toString();
39
39
  });
40
+ // A hook that exits without draining stdin (block.sh, `exit 2`, any script
41
+ // that ignores its input) makes the payload write below fail asynchronously.
42
+ // stdin then emits EPIPE, which is fatal to the process if unhandled.
43
+ child.stdin.on("error", () => { });
40
44
  child.on("error", () => {
41
45
  clearTimeout(timer);
42
46
  resolve({ exitCode: 1, stdout, stderr, json: undefined });
@@ -60,7 +64,7 @@ function runHookCommand(cmd, input, root, vars) {
60
64
  child.stdin.end();
61
65
  }
62
66
  catch {
63
- /* child may have already exited */
67
+ /* child already exited: the payload is best-effort, the exit code is not */
64
68
  }
65
69
  });
66
70
  }
@@ -1 +1 @@
1
- {"version":3,"file":"hooks-bridge.js","sourceRoot":"","sources":["../../../../src/core/extensions/plugins/hooks-bridge.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Hooks bridge — runs Claude Code / native-plugin shell hooks against hoocode events.\n *\n * Claude Code hooks are shell commands wired to named events and matched by tool\n * name. hoocode hooks are TypeScript handlers on the {@link ExtensionEvent} union.\n * This bridge registers handlers that shell out per the hook protocol and translate\n * stdin JSON + exit codes + stdout JSON back into hoocode result objects.\n *\n * Protocol (faithful to Claude Code):\n * - Input: a JSON object on stdin describing the event.\n * - Exit 0: success. stdout may carry a JSON decision; for prompt/session events\n * plain stdout is treated as additional context.\n * - Exit 2: blocking error. stderr (or JSON `reason`) is the block reason.\n * - Other non-zero: non-blocking error (logged, not surfaced to the model).\n * - Optional stdout JSON: `{ decision: \"block\"|\"approve\", reason, permissionDecision }`.\n */\n\nimport { spawn } from \"node:child_process\";\nimport type { ExtensionAPI, ToolCallEvent, ToolResultEvent } from \"../types.js\";\nimport type { PluginHookCommand, PluginHookMatcherGroup, PluginHooksConfig } from \"./manifest.js\";\n\nconst DEFAULT_TIMEOUT_MS = 60_000;\n\ninterface HookRunResult {\n\texitCode: number;\n\tstdout: string;\n\tstderr: string;\n\tjson: { decision?: string; reason?: string; permissionDecision?: string; continue?: boolean } | undefined;\n}\n\n/** Run one shell hook command, piping `input` as JSON on stdin. */\nfunction runHookCommand(\n\tcmd: PluginHookCommand,\n\tinput: unknown,\n\troot: string,\n\tvars: Record<string, string>,\n): Promise<HookRunResult> {\n\treturn new Promise((resolve) => {\n\t\tconst child = spawn(cmd.command, {\n\t\t\tshell: true,\n\t\t\t// Every vendor spelling, so a hook written for either agent resolves:\n\t\t\t// the shell expands these, which is the hook-side equivalent of the\n\t\t\t// string substitution MCP configs get.\n\t\t\tenv: { ...process.env, ...vars },\n\t\t\tcwd: root,\n\t\t});\n\n\t\tlet stdout = \"\";\n\t\tlet stderr = \"\";\n\t\tconst timer = setTimeout(() => child.kill(\"SIGTERM\"), (cmd.timeout ?? DEFAULT_TIMEOUT_MS / 1000) * 1000);\n\t\ttimer.unref?.();\n\n\t\tchild.stdout.on(\"data\", (d) => {\n\t\t\tstdout += d.toString();\n\t\t});\n\t\tchild.stderr.on(\"data\", (d) => {\n\t\t\tstderr += d.toString();\n\t\t});\n\t\tchild.on(\"error\", () => {\n\t\t\tclearTimeout(timer);\n\t\t\tresolve({ exitCode: 1, stdout, stderr, json: undefined });\n\t\t});\n\t\tchild.on(\"close\", (code) => {\n\t\t\tclearTimeout(timer);\n\t\t\tlet json: HookRunResult[\"json\"];\n\t\t\tconst trimmed = stdout.trim();\n\t\t\tif (trimmed.startsWith(\"{\")) {\n\t\t\t\ttry {\n\t\t\t\t\tjson = JSON.parse(trimmed);\n\t\t\t\t} catch {\n\t\t\t\t\tjson = undefined;\n\t\t\t\t}\n\t\t\t}\n\t\t\tresolve({ exitCode: code ?? 0, stdout, stderr, json });\n\t\t});\n\n\t\ttry {\n\t\t\tchild.stdin.write(JSON.stringify(input));\n\t\t\tchild.stdin.end();\n\t\t} catch {\n\t\t\t/* child may have already exited */\n\t\t}\n\t});\n}\n\n/** Empty / \"*\" matcher matches everything; otherwise treat as an anchored regex on the tool name. */\nfunction matcherMatches(matcher: string | undefined, toolName: string): boolean {\n\tif (!matcher || matcher === \"*\") return true;\n\ttry {\n\t\treturn new RegExp(`^(?:${matcher})$`).test(toolName);\n\t} catch {\n\t\treturn matcher === toolName;\n\t}\n}\n\nfunction groupsForTool(groups: PluginHookMatcherGroup[], toolName: string): PluginHookCommand[] {\n\tconst cmds: PluginHookCommand[] = [];\n\tfor (const g of groups) {\n\t\tif (matcherMatches(g.matcher, toolName)) cmds.push(...g.hooks);\n\t}\n\treturn cmds;\n}\n\nfunction allCommands(groups: PluginHookMatcherGroup[]): PluginHookCommand[] {\n\treturn groups.flatMap((g) => g.hooks);\n}\n\n/**\n * Register all hook events for one plugin against the ExtensionAPI.\n * `onError` reports non-blocking failures (kept off the model's path).\n */\nexport function installPluginHooks(\n\tpi: ExtensionAPI,\n\thooks: PluginHooksConfig,\n\troot: string,\n\tvars: Record<string, string>,\n\tonError: (message: string) => void,\n): void {\n\t// ── PreToolUse → tool_call (blocking) ────────────────────────────────────\n\tconst preGroups = hooks.PreToolUse;\n\tif (preGroups?.length) {\n\t\tpi.on(\"tool_call\", async (event: ToolCallEvent) => {\n\t\t\tconst cmds = groupsForTool(preGroups, event.toolName);\n\t\t\tfor (const cmd of cmds) {\n\t\t\t\tconst res = await runHookCommand(\n\t\t\t\t\tcmd,\n\t\t\t\t\t{ hook_event_name: \"PreToolUse\", tool_name: event.toolName, tool_input: event.input },\n\t\t\t\t\troot,\n\t\t\t\t\tvars,\n\t\t\t\t);\n\t\t\t\tconst decision = res.json?.decision ?? res.json?.permissionDecision;\n\t\t\t\tif (res.exitCode === 2 || decision === \"block\" || decision === \"deny\") {\n\t\t\t\t\treturn { block: true, reason: res.json?.reason || res.stderr.trim() || \"Blocked by plugin hook\" };\n\t\t\t\t}\n\t\t\t\tif (res.exitCode !== 0) onError(`PreToolUse hook failed (${res.exitCode}): ${res.stderr.trim()}`);\n\t\t\t}\n\t\t});\n\t}\n\n\t// ── PostToolUse → tool_result (best-effort) ──────────────────────────────\n\tconst postGroups = hooks.PostToolUse;\n\tif (postGroups?.length) {\n\t\tpi.on(\"tool_result\", async (event: ToolResultEvent) => {\n\t\t\tconst cmds = groupsForTool(postGroups, event.toolName);\n\t\t\tfor (const cmd of cmds) {\n\t\t\t\tconst res = await runHookCommand(\n\t\t\t\t\tcmd,\n\t\t\t\t\t{\n\t\t\t\t\t\thook_event_name: \"PostToolUse\",\n\t\t\t\t\t\ttool_name: event.toolName,\n\t\t\t\t\t\ttool_input: event.input,\n\t\t\t\t\t\ttool_response: event.content,\n\t\t\t\t\t},\n\t\t\t\t\troot,\n\t\t\t\t\tvars,\n\t\t\t\t);\n\t\t\t\tif (res.exitCode === 2 || res.json?.decision === \"block\") {\n\t\t\t\t\tconst reason = res.json?.reason || res.stderr.trim() || \"Flagged by plugin hook\";\n\t\t\t\t\treturn {\n\t\t\t\t\t\tcontent: [...event.content, { type: \"text\" as const, text: `\\n[plugin hook] ${reason}` }],\n\t\t\t\t\t\tisError: true,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\tif (res.exitCode !== 0) onError(`PostToolUse hook failed (${res.exitCode}): ${res.stderr.trim()}`);\n\t\t\t}\n\t\t});\n\t}\n\n\t// ── UserPromptSubmit → before_agent_start (adds context) ─────────────────\n\tconst promptGroups = hooks.UserPromptSubmit;\n\tif (promptGroups?.length) {\n\t\tpi.on(\"before_agent_start\", async (event) => {\n\t\t\tlet systemPrompt = event.systemPrompt;\n\t\t\tfor (const cmd of allCommands(promptGroups)) {\n\t\t\t\tconst res = await runHookCommand(\n\t\t\t\t\tcmd,\n\t\t\t\t\t{ hook_event_name: \"UserPromptSubmit\", prompt: event.prompt },\n\t\t\t\t\troot,\n\t\t\t\t\tvars,\n\t\t\t\t);\n\t\t\t\tif (res.exitCode !== 0 && res.exitCode !== 2) {\n\t\t\t\t\tonError(`UserPromptSubmit hook failed (${res.exitCode}): ${res.stderr.trim()}`);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst extra = res.exitCode === 2 ? res.stderr.trim() : res.json?.reason || res.stdout.trim();\n\t\t\t\tif (extra) systemPrompt = `${systemPrompt}\\n\\n<!-- plugin hook -->\\n${extra}`;\n\t\t\t}\n\t\t\treturn systemPrompt === event.systemPrompt ? undefined : { systemPrompt };\n\t\t});\n\t}\n\n\t// ── SessionStart → session_start (side effects) ──────────────────────────\n\tconst sessionGroups = hooks.SessionStart;\n\tif (sessionGroups?.length) {\n\t\tpi.on(\"session_start\", async (event) => {\n\t\t\tfor (const cmd of allCommands(sessionGroups)) {\n\t\t\t\tconst res = await runHookCommand(\n\t\t\t\t\tcmd,\n\t\t\t\t\t{ hook_event_name: \"SessionStart\", source: event.reason },\n\t\t\t\t\troot,\n\t\t\t\t\tvars,\n\t\t\t\t);\n\t\t\t\tif (res.exitCode !== 0) onError(`SessionStart hook failed (${res.exitCode}): ${res.stderr.trim()}`);\n\t\t\t}\n\t\t});\n\t}\n\n\t// ── Stop → agent_end (side effects) ──────────────────────────────────────\n\tconst stopGroups = hooks.Stop;\n\tif (stopGroups?.length) {\n\t\tpi.on(\"agent_end\", async () => {\n\t\t\tfor (const cmd of allCommands(stopGroups)) {\n\t\t\t\tconst res = await runHookCommand(cmd, { hook_event_name: \"Stop\" }, root, vars);\n\t\t\t\tif (res.exitCode !== 0) onError(`Stop hook failed (${res.exitCode}): ${res.stderr.trim()}`);\n\t\t\t}\n\t\t});\n\t}\n}\n"]}
1
+ {"version":3,"file":"hooks-bridge.js","sourceRoot":"","sources":["../../../../src/core/extensions/plugins/hooks-bridge.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Hooks bridge — runs Claude Code / native-plugin shell hooks against hoocode events.\n *\n * Claude Code hooks are shell commands wired to named events and matched by tool\n * name. hoocode hooks are TypeScript handlers on the {@link ExtensionEvent} union.\n * This bridge registers handlers that shell out per the hook protocol and translate\n * stdin JSON + exit codes + stdout JSON back into hoocode result objects.\n *\n * Protocol (faithful to Claude Code):\n * - Input: a JSON object on stdin describing the event.\n * - Exit 0: success. stdout may carry a JSON decision; for prompt/session events\n * plain stdout is treated as additional context.\n * - Exit 2: blocking error. stderr (or JSON `reason`) is the block reason.\n * - Other non-zero: non-blocking error (logged, not surfaced to the model).\n * - Optional stdout JSON: `{ decision: \"block\"|\"approve\", reason, permissionDecision }`.\n */\n\nimport { spawn } from \"node:child_process\";\nimport type { ExtensionAPI, ToolCallEvent, ToolResultEvent } from \"../types.js\";\nimport type { PluginHookCommand, PluginHookMatcherGroup, PluginHooksConfig } from \"./manifest.js\";\n\nconst DEFAULT_TIMEOUT_MS = 60_000;\n\ninterface HookRunResult {\n\texitCode: number;\n\tstdout: string;\n\tstderr: string;\n\tjson: { decision?: string; reason?: string; permissionDecision?: string; continue?: boolean } | undefined;\n}\n\n/** Run one shell hook command, piping `input` as JSON on stdin. */\nfunction runHookCommand(\n\tcmd: PluginHookCommand,\n\tinput: unknown,\n\troot: string,\n\tvars: Record<string, string>,\n): Promise<HookRunResult> {\n\treturn new Promise((resolve) => {\n\t\tconst child = spawn(cmd.command, {\n\t\t\tshell: true,\n\t\t\t// Every vendor spelling, so a hook written for either agent resolves:\n\t\t\t// the shell expands these, which is the hook-side equivalent of the\n\t\t\t// string substitution MCP configs get.\n\t\t\tenv: { ...process.env, ...vars },\n\t\t\tcwd: root,\n\t\t});\n\n\t\tlet stdout = \"\";\n\t\tlet stderr = \"\";\n\t\tconst timer = setTimeout(() => child.kill(\"SIGTERM\"), (cmd.timeout ?? DEFAULT_TIMEOUT_MS / 1000) * 1000);\n\t\ttimer.unref?.();\n\n\t\tchild.stdout.on(\"data\", (d) => {\n\t\t\tstdout += d.toString();\n\t\t});\n\t\tchild.stderr.on(\"data\", (d) => {\n\t\t\tstderr += d.toString();\n\t\t});\n\t\t// A hook that exits without draining stdin (block.sh, `exit 2`, any script\n\t\t// that ignores its input) makes the payload write below fail asynchronously.\n\t\t// stdin then emits EPIPE, which is fatal to the process if unhandled.\n\t\tchild.stdin.on(\"error\", () => {});\n\t\tchild.on(\"error\", () => {\n\t\t\tclearTimeout(timer);\n\t\t\tresolve({ exitCode: 1, stdout, stderr, json: undefined });\n\t\t});\n\t\tchild.on(\"close\", (code) => {\n\t\t\tclearTimeout(timer);\n\t\t\tlet json: HookRunResult[\"json\"];\n\t\t\tconst trimmed = stdout.trim();\n\t\t\tif (trimmed.startsWith(\"{\")) {\n\t\t\t\ttry {\n\t\t\t\t\tjson = JSON.parse(trimmed);\n\t\t\t\t} catch {\n\t\t\t\t\tjson = undefined;\n\t\t\t\t}\n\t\t\t}\n\t\t\tresolve({ exitCode: code ?? 0, stdout, stderr, json });\n\t\t});\n\n\t\ttry {\n\t\t\tchild.stdin.write(JSON.stringify(input));\n\t\t\tchild.stdin.end();\n\t\t} catch {\n\t\t\t/* child already exited: the payload is best-effort, the exit code is not */\n\t\t}\n\t});\n}\n\n/** Empty / \"*\" matcher matches everything; otherwise treat as an anchored regex on the tool name. */\nfunction matcherMatches(matcher: string | undefined, toolName: string): boolean {\n\tif (!matcher || matcher === \"*\") return true;\n\ttry {\n\t\treturn new RegExp(`^(?:${matcher})$`).test(toolName);\n\t} catch {\n\t\treturn matcher === toolName;\n\t}\n}\n\nfunction groupsForTool(groups: PluginHookMatcherGroup[], toolName: string): PluginHookCommand[] {\n\tconst cmds: PluginHookCommand[] = [];\n\tfor (const g of groups) {\n\t\tif (matcherMatches(g.matcher, toolName)) cmds.push(...g.hooks);\n\t}\n\treturn cmds;\n}\n\nfunction allCommands(groups: PluginHookMatcherGroup[]): PluginHookCommand[] {\n\treturn groups.flatMap((g) => g.hooks);\n}\n\n/**\n * Register all hook events for one plugin against the ExtensionAPI.\n * `onError` reports non-blocking failures (kept off the model's path).\n */\nexport function installPluginHooks(\n\tpi: ExtensionAPI,\n\thooks: PluginHooksConfig,\n\troot: string,\n\tvars: Record<string, string>,\n\tonError: (message: string) => void,\n): void {\n\t// ── PreToolUse → tool_call (blocking) ────────────────────────────────────\n\tconst preGroups = hooks.PreToolUse;\n\tif (preGroups?.length) {\n\t\tpi.on(\"tool_call\", async (event: ToolCallEvent) => {\n\t\t\tconst cmds = groupsForTool(preGroups, event.toolName);\n\t\t\tfor (const cmd of cmds) {\n\t\t\t\tconst res = await runHookCommand(\n\t\t\t\t\tcmd,\n\t\t\t\t\t{ hook_event_name: \"PreToolUse\", tool_name: event.toolName, tool_input: event.input },\n\t\t\t\t\troot,\n\t\t\t\t\tvars,\n\t\t\t\t);\n\t\t\t\tconst decision = res.json?.decision ?? res.json?.permissionDecision;\n\t\t\t\tif (res.exitCode === 2 || decision === \"block\" || decision === \"deny\") {\n\t\t\t\t\treturn { block: true, reason: res.json?.reason || res.stderr.trim() || \"Blocked by plugin hook\" };\n\t\t\t\t}\n\t\t\t\tif (res.exitCode !== 0) onError(`PreToolUse hook failed (${res.exitCode}): ${res.stderr.trim()}`);\n\t\t\t}\n\t\t});\n\t}\n\n\t// ── PostToolUse → tool_result (best-effort) ──────────────────────────────\n\tconst postGroups = hooks.PostToolUse;\n\tif (postGroups?.length) {\n\t\tpi.on(\"tool_result\", async (event: ToolResultEvent) => {\n\t\t\tconst cmds = groupsForTool(postGroups, event.toolName);\n\t\t\tfor (const cmd of cmds) {\n\t\t\t\tconst res = await runHookCommand(\n\t\t\t\t\tcmd,\n\t\t\t\t\t{\n\t\t\t\t\t\thook_event_name: \"PostToolUse\",\n\t\t\t\t\t\ttool_name: event.toolName,\n\t\t\t\t\t\ttool_input: event.input,\n\t\t\t\t\t\ttool_response: event.content,\n\t\t\t\t\t},\n\t\t\t\t\troot,\n\t\t\t\t\tvars,\n\t\t\t\t);\n\t\t\t\tif (res.exitCode === 2 || res.json?.decision === \"block\") {\n\t\t\t\t\tconst reason = res.json?.reason || res.stderr.trim() || \"Flagged by plugin hook\";\n\t\t\t\t\treturn {\n\t\t\t\t\t\tcontent: [...event.content, { type: \"text\" as const, text: `\\n[plugin hook] ${reason}` }],\n\t\t\t\t\t\tisError: true,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\tif (res.exitCode !== 0) onError(`PostToolUse hook failed (${res.exitCode}): ${res.stderr.trim()}`);\n\t\t\t}\n\t\t});\n\t}\n\n\t// ── UserPromptSubmit → before_agent_start (adds context) ─────────────────\n\tconst promptGroups = hooks.UserPromptSubmit;\n\tif (promptGroups?.length) {\n\t\tpi.on(\"before_agent_start\", async (event) => {\n\t\t\tlet systemPrompt = event.systemPrompt;\n\t\t\tfor (const cmd of allCommands(promptGroups)) {\n\t\t\t\tconst res = await runHookCommand(\n\t\t\t\t\tcmd,\n\t\t\t\t\t{ hook_event_name: \"UserPromptSubmit\", prompt: event.prompt },\n\t\t\t\t\troot,\n\t\t\t\t\tvars,\n\t\t\t\t);\n\t\t\t\tif (res.exitCode !== 0 && res.exitCode !== 2) {\n\t\t\t\t\tonError(`UserPromptSubmit hook failed (${res.exitCode}): ${res.stderr.trim()}`);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst extra = res.exitCode === 2 ? res.stderr.trim() : res.json?.reason || res.stdout.trim();\n\t\t\t\tif (extra) systemPrompt = `${systemPrompt}\\n\\n<!-- plugin hook -->\\n${extra}`;\n\t\t\t}\n\t\t\treturn systemPrompt === event.systemPrompt ? undefined : { systemPrompt };\n\t\t});\n\t}\n\n\t// ── SessionStart → session_start (side effects) ──────────────────────────\n\tconst sessionGroups = hooks.SessionStart;\n\tif (sessionGroups?.length) {\n\t\tpi.on(\"session_start\", async (event) => {\n\t\t\tfor (const cmd of allCommands(sessionGroups)) {\n\t\t\t\tconst res = await runHookCommand(\n\t\t\t\t\tcmd,\n\t\t\t\t\t{ hook_event_name: \"SessionStart\", source: event.reason },\n\t\t\t\t\troot,\n\t\t\t\t\tvars,\n\t\t\t\t);\n\t\t\t\tif (res.exitCode !== 0) onError(`SessionStart hook failed (${res.exitCode}): ${res.stderr.trim()}`);\n\t\t\t}\n\t\t});\n\t}\n\n\t// ── Stop → agent_end (side effects) ──────────────────────────────────────\n\tconst stopGroups = hooks.Stop;\n\tif (stopGroups?.length) {\n\t\tpi.on(\"agent_end\", async () => {\n\t\t\tfor (const cmd of allCommands(stopGroups)) {\n\t\t\t\tconst res = await runHookCommand(cmd, { hook_event_name: \"Stop\" }, root, vars);\n\t\t\t\tif (res.exitCode !== 0) onError(`Stop hook failed (${res.exitCode}): ${res.stderr.trim()}`);\n\t\t\t}\n\t\t});\n\t}\n}\n"]}
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kolisachint/hoocode-extension-custom-provider-anthropic",
3
3
  "private": true,
4
- "version": "0.3.20",
4
+ "version": "0.3.21",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "bun": ">=1.0.0"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kolisachint/hoocode-extension-custom-provider-gitlab-duo",
3
3
  "private": true,
4
- "version": "0.3.20",
4
+ "version": "0.3.21",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "bun": ">=1.0.0"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kolisachint/hoocode-extension-sandbox",
3
3
  "private": true,
4
- "version": "0.3.20",
4
+ "version": "0.3.21",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "bun": ">=1.0.0"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kolisachint/hoocode-extension-with-deps",
3
3
  "private": true,
4
- "version": "0.3.20",
4
+ "version": "0.3.21",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "bun": ">=1.0.0"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolisachint/hoocode-agent",
3
- "version": "0.5.20",
3
+ "version": "0.5.21",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "hoocodeConfig": {
@@ -49,9 +49,9 @@
49
49
  "prepublishOnly": "npm run clean && npm run build"
50
50
  },
51
51
  "dependencies": {
52
- "@kolisachint/hoocode-agent-core": "^0.5.20",
53
- "@kolisachint/hoocode-ai": "^0.5.20",
54
- "@kolisachint/hoocode-tui": "^0.5.20",
52
+ "@kolisachint/hoocode-agent-core": "^0.5.21",
53
+ "@kolisachint/hoocode-ai": "^0.5.21",
54
+ "@kolisachint/hoocode-tui": "^0.5.21",
55
55
  "@silvia-odwyer/photon-node": "^0.3.4",
56
56
  "chalk": "^5.5.0",
57
57
  "cli-highlight": "^2.1.11",