// The hook bridge: `gortex hook --agent=pi`. Pi has no hook configuration, so // each event is shelled out with an envelope on stdin and the decision read // back is applied. import type { ExecFileOpts, ProcessDeps } from "./runtime.ts"; /** The decision shape Go writes back (hooks.BridgeDecision). */ export interface PiDecision { block?: boolean; reason?: string; additional_context?: string; orientation?: string; } /** The envelope the extension writes to the hook's stdin (hooks.BridgeEvent). */ export interface PiEnvelope { event: string; tool_name?: string; tool_input?: Record; cwd?: string; session_id?: string; is_gortex_tool?: boolean; } const HOOK_EXEC_OPTS: ExecFileOpts = { maxBuffer: 16 * 1024 * 1024, timeout: 5_000, windowsHide: true, }; /** * callHook sends a normalized event envelope to `gortex hook --agent=pi` and * parses the PiDecision it writes back. Fail-open: any error, an aborted * `signal`, or a reply that is not a JSON object returns an empty decision, so * the extension never blocks Pi's flow on a hook hiccup (daemon down, parse * error, timeout). */ export async function callHook( argv: string[], deps: ProcessDeps, envelope: PiEnvelope, signal?: AbortSignal, ): Promise { const [bin, ...args] = argv; if (!bin) return {}; try { const out = (await deps.execFile(bin, args, { ...HOOK_EXEC_OPTS, input: JSON.stringify(envelope), signal })).trim(); if (!out) return {}; const decision: unknown = JSON.parse(out); return typeof decision === "object" && decision !== null && !Array.isArray(decision) ? (decision as PiDecision) : {}; } catch { return {}; } } // --------------------------------------------------------------------------- // Tool-call normalization (Pi vocabulary -> canonical Claude-Code vocabulary) // --------------------------------------------------------------------------- // // The Go classifier switches on Claude-Code tool names and canonical input // keys ("file_path"/"pattern"/"command"), so the translation lives here. // // The built-in tool names mapped here are pinned to Pi's documented API // (packages/coding-agent/docs/extensions.md); if an upgrade renames one, adjust // this small map; the Go wire contract stays unchanged. export const toolNameMap: Record = { read: "Read", grep: "Grep", find: "Glob", ls: "Glob", bash: "Bash", powershell: "Bash", edit: "Edit", write: "Write", }; function firstString(obj: Record, keys: string[]): string | undefined { for (const k of keys) { const v = obj[k]; if (typeof v === "string" && v !== "") return v; } return undefined; } export function normalizeToolCall( piName: string, piInput: Record, ): { tool_name: string; tool_input: Record } { const canonical = toolNameMap[piName.toLowerCase()] ?? piName; const out: Record = { ...piInput }; const path = firstString(piInput, ["file_path", "path", "file", "filepath", "absolute_path"]); if (path !== undefined) out.file_path = path; let pattern = firstString(piInput, ["pattern", "glob", "query", "regex", "name"]); if (pattern === undefined && canonical === "Glob") pattern = path; if (pattern !== undefined) out.pattern = pattern; const command = firstString(piInput, ["command", "cmd", "script"]); if (command !== undefined) out.command = command; return { tool_name: canonical, tool_input: out }; }