@mariozechner/pi-coding-agent 0.17.0 → 0.18.0
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 +18 -0
- package/README.md +53 -0
- package/dist/cli/args.d.ts +1 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +5 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +4 -0
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts +25 -2
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +116 -3
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/hooks/index.d.ts +5 -0
- package/dist/core/hooks/index.d.ts.map +1 -0
- package/dist/core/hooks/index.js +4 -0
- package/dist/core/hooks/index.js.map +1 -0
- package/dist/core/hooks/loader.d.ts +56 -0
- package/dist/core/hooks/loader.d.ts.map +1 -0
- package/dist/core/hooks/loader.js +158 -0
- package/dist/core/hooks/loader.js.map +1 -0
- package/dist/core/hooks/runner.d.ts +69 -0
- package/dist/core/hooks/runner.d.ts.map +1 -0
- package/dist/core/hooks/runner.js +203 -0
- package/dist/core/hooks/runner.js.map +1 -0
- package/dist/core/hooks/tool-wrapper.d.ts +16 -0
- package/dist/core/hooks/tool-wrapper.d.ts.map +1 -0
- package/dist/core/hooks/tool-wrapper.js +71 -0
- package/dist/core/hooks/tool-wrapper.js.map +1 -0
- package/dist/core/hooks/types.d.ts +220 -0
- package/dist/core/hooks/types.d.ts.map +1 -0
- package/dist/core/hooks/types.js +8 -0
- package/dist/core/hooks/types.js.map +1 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/settings-manager.d.ts +6 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +14 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +5 -3
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +23 -12
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/hook-input.d.ts +12 -0
- package/dist/modes/interactive/components/hook-input.d.ts.map +1 -0
- package/dist/modes/interactive/components/hook-input.js +46 -0
- package/dist/modes/interactive/components/hook-input.js.map +1 -0
- package/dist/modes/interactive/components/hook-selector.d.ts +16 -0
- package/dist/modes/interactive/components/hook-selector.d.ts.map +1 -0
- package/dist/modes/interactive/components/hook-selector.js +76 -0
- package/dist/modes/interactive/components/hook-selector.js.map +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts +37 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +172 -3
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +15 -0
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts +2 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +117 -3
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +40 -0
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/docs/compaction.md +519 -0
- package/docs/hooks.md +609 -0
- package/docs/rpc.md +870 -0
- package/docs/session.md +89 -0
- package/docs/theme.md +586 -0
- package/docs/truncation.md +235 -0
- package/docs/undercompaction.md +313 -0
- package/package.json +18 -6
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook runner - executes hooks and manages their lifecycle.
|
|
3
|
+
*/
|
|
4
|
+
import type { LoadedHook, SendHandler } from "./loader.js";
|
|
5
|
+
import type { BranchEventResult, HookError, HookEvent, HookUIContext, ToolCallEvent, ToolCallEventResult, ToolResultEventResult } from "./types.js";
|
|
6
|
+
/**
|
|
7
|
+
* Listener for hook errors.
|
|
8
|
+
*/
|
|
9
|
+
export type HookErrorListener = (error: HookError) => void;
|
|
10
|
+
/**
|
|
11
|
+
* HookRunner executes hooks and manages event emission.
|
|
12
|
+
*/
|
|
13
|
+
export declare class HookRunner {
|
|
14
|
+
private hooks;
|
|
15
|
+
private uiContext;
|
|
16
|
+
private hasUI;
|
|
17
|
+
private cwd;
|
|
18
|
+
private sessionFile;
|
|
19
|
+
private timeout;
|
|
20
|
+
private errorListeners;
|
|
21
|
+
constructor(hooks: LoadedHook[], cwd: string, timeout?: number);
|
|
22
|
+
/**
|
|
23
|
+
* Set the UI context for hooks.
|
|
24
|
+
* Call this when the mode initializes and UI is available.
|
|
25
|
+
*/
|
|
26
|
+
setUIContext(uiContext: HookUIContext, hasUI: boolean): void;
|
|
27
|
+
/**
|
|
28
|
+
* Get the paths of all loaded hooks.
|
|
29
|
+
*/
|
|
30
|
+
getHookPaths(): string[];
|
|
31
|
+
/**
|
|
32
|
+
* Set the session file path.
|
|
33
|
+
*/
|
|
34
|
+
setSessionFile(sessionFile: string | null): void;
|
|
35
|
+
/**
|
|
36
|
+
* Set the send handler for all hooks' pi.send().
|
|
37
|
+
* Call this when the mode initializes.
|
|
38
|
+
*/
|
|
39
|
+
setSendHandler(handler: SendHandler): void;
|
|
40
|
+
/**
|
|
41
|
+
* Subscribe to hook errors.
|
|
42
|
+
* @returns Unsubscribe function
|
|
43
|
+
*/
|
|
44
|
+
onError(listener: HookErrorListener): () => void;
|
|
45
|
+
/**
|
|
46
|
+
* Emit an error to all listeners.
|
|
47
|
+
*/
|
|
48
|
+
private emitError;
|
|
49
|
+
/**
|
|
50
|
+
* Check if any hooks have handlers for the given event type.
|
|
51
|
+
*/
|
|
52
|
+
hasHandlers(eventType: string): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Create the event context for handlers.
|
|
55
|
+
*/
|
|
56
|
+
private createContext;
|
|
57
|
+
/**
|
|
58
|
+
* Emit an event to all hooks.
|
|
59
|
+
* Returns the result from branch/tool_result events (if any handler returns one).
|
|
60
|
+
*/
|
|
61
|
+
emit(event: HookEvent): Promise<BranchEventResult | ToolResultEventResult | undefined>;
|
|
62
|
+
/**
|
|
63
|
+
* Emit a tool_call event to all hooks.
|
|
64
|
+
* No timeout - user prompts can take as long as needed.
|
|
65
|
+
* Errors are thrown (not swallowed) so caller can block on failure.
|
|
66
|
+
*/
|
|
67
|
+
emitToolCall(event: ToolCallEvent): Promise<ToolCallEventResult | undefined>;
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=runner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../../src/core/hooks/runner.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC3D,OAAO,KAAK,EACX,iBAAiB,EAEjB,SAAS,EACT,SAAS,EAET,aAAa,EACb,aAAa,EACb,mBAAmB,EACnB,qBAAqB,EACrB,MAAM,YAAY,CAAC;AAOpB;;GAEG;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;AAoD3D;;GAEG;AACH,qBAAa,UAAU;IACtB,OAAO,CAAC,KAAK,CAAe;IAC5B,OAAO,CAAC,SAAS,CAAgB;IACjC,OAAO,CAAC,KAAK,CAAU;IACvB,OAAO,CAAC,GAAG,CAAS;IACpB,OAAO,CAAC,WAAW,CAAgB;IACnC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,cAAc,CAAqC;IAE3D,YAAY,KAAK,EAAE,UAAU,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,MAAwB,EAO9E;IAED;;;OAGG;IACH,YAAY,CAAC,SAAS,EAAE,aAAa,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAG3D;IAED;;OAEG;IACH,YAAY,IAAI,MAAM,EAAE,CAEvB;IAED;;OAEG;IACH,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAE/C;IAED;;;OAGG;IACH,cAAc,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,CAIzC;IAED;;;OAGG;IACH,OAAO,CAAC,QAAQ,EAAE,iBAAiB,GAAG,MAAM,IAAI,CAG/C;IAED;;OAEG;IACH,OAAO,CAAC,SAAS;IAMjB;;OAEG;IACH,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAQtC;IAED;;OAEG;IACH,OAAO,CAAC,aAAa;IAUrB;;;OAGG;IACG,IAAI,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,iBAAiB,GAAG,qBAAqB,GAAG,SAAS,CAAC,CAmC3F;IAED;;;;OAIG;IACG,YAAY,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC,CAuBjF;CACD","sourcesContent":["/**\n * Hook runner - executes hooks and manages their lifecycle.\n */\n\nimport { spawn } from \"node:child_process\";\nimport type { LoadedHook, SendHandler } from \"./loader.js\";\nimport type {\n\tBranchEventResult,\n\tExecResult,\n\tHookError,\n\tHookEvent,\n\tHookEventContext,\n\tHookUIContext,\n\tToolCallEvent,\n\tToolCallEventResult,\n\tToolResultEventResult,\n} from \"./types.js\";\n\n/**\n * Default timeout for hook execution (30 seconds).\n */\nconst DEFAULT_TIMEOUT = 30000;\n\n/**\n * Listener for hook errors.\n */\nexport type HookErrorListener = (error: HookError) => void;\n\n/**\n * Execute a command and return stdout/stderr/code.\n */\nasync function exec(command: string, args: string[], cwd: string): Promise<ExecResult> {\n\treturn new Promise((resolve) => {\n\t\tconst proc = spawn(command, args, { cwd, shell: false });\n\n\t\tlet stdout = \"\";\n\t\tlet stderr = \"\";\n\n\t\tproc.stdout?.on(\"data\", (data) => {\n\t\t\tstdout += data.toString();\n\t\t});\n\n\t\tproc.stderr?.on(\"data\", (data) => {\n\t\t\tstderr += data.toString();\n\t\t});\n\n\t\tproc.on(\"close\", (code) => {\n\t\t\tresolve({ stdout, stderr, code: code ?? 0 });\n\t\t});\n\n\t\tproc.on(\"error\", (_err) => {\n\t\t\tresolve({ stdout, stderr, code: 1 });\n\t\t});\n\t});\n}\n\n/**\n * Create a promise that rejects after a timeout.\n */\nfunction createTimeout(ms: number): { promise: Promise<never>; clear: () => void } {\n\tlet timeoutId: NodeJS.Timeout;\n\tconst promise = new Promise<never>((_, reject) => {\n\t\ttimeoutId = setTimeout(() => reject(new Error(`Hook timed out after ${ms}ms`)), ms);\n\t});\n\treturn {\n\t\tpromise,\n\t\tclear: () => clearTimeout(timeoutId),\n\t};\n}\n\n/** No-op UI context used when no UI is available */\nconst noOpUIContext: HookUIContext = {\n\tselect: async () => null,\n\tconfirm: async () => false,\n\tinput: async () => null,\n\tnotify: () => {},\n};\n\n/**\n * HookRunner executes hooks and manages event emission.\n */\nexport class HookRunner {\n\tprivate hooks: LoadedHook[];\n\tprivate uiContext: HookUIContext;\n\tprivate hasUI: boolean;\n\tprivate cwd: string;\n\tprivate sessionFile: string | null;\n\tprivate timeout: number;\n\tprivate errorListeners: Set<HookErrorListener> = new Set();\n\n\tconstructor(hooks: LoadedHook[], cwd: string, timeout: number = DEFAULT_TIMEOUT) {\n\t\tthis.hooks = hooks;\n\t\tthis.uiContext = noOpUIContext;\n\t\tthis.hasUI = false;\n\t\tthis.cwd = cwd;\n\t\tthis.sessionFile = null;\n\t\tthis.timeout = timeout;\n\t}\n\n\t/**\n\t * Set the UI context for hooks.\n\t * Call this when the mode initializes and UI is available.\n\t */\n\tsetUIContext(uiContext: HookUIContext, hasUI: boolean): void {\n\t\tthis.uiContext = uiContext;\n\t\tthis.hasUI = hasUI;\n\t}\n\n\t/**\n\t * Get the paths of all loaded hooks.\n\t */\n\tgetHookPaths(): string[] {\n\t\treturn this.hooks.map((h) => h.path);\n\t}\n\n\t/**\n\t * Set the session file path.\n\t */\n\tsetSessionFile(sessionFile: string | null): void {\n\t\tthis.sessionFile = sessionFile;\n\t}\n\n\t/**\n\t * Set the send handler for all hooks' pi.send().\n\t * Call this when the mode initializes.\n\t */\n\tsetSendHandler(handler: SendHandler): void {\n\t\tfor (const hook of this.hooks) {\n\t\t\thook.setSendHandler(handler);\n\t\t}\n\t}\n\n\t/**\n\t * Subscribe to hook errors.\n\t * @returns Unsubscribe function\n\t */\n\tonError(listener: HookErrorListener): () => void {\n\t\tthis.errorListeners.add(listener);\n\t\treturn () => this.errorListeners.delete(listener);\n\t}\n\n\t/**\n\t * Emit an error to all listeners.\n\t */\n\tprivate emitError(error: HookError): void {\n\t\tfor (const listener of this.errorListeners) {\n\t\t\tlistener(error);\n\t\t}\n\t}\n\n\t/**\n\t * Check if any hooks have handlers for the given event type.\n\t */\n\thasHandlers(eventType: string): boolean {\n\t\tfor (const hook of this.hooks) {\n\t\t\tconst handlers = hook.handlers.get(eventType);\n\t\t\tif (handlers && handlers.length > 0) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * Create the event context for handlers.\n\t */\n\tprivate createContext(): HookEventContext {\n\t\treturn {\n\t\t\texec: (command: string, args: string[]) => exec(command, args, this.cwd),\n\t\t\tui: this.uiContext,\n\t\t\thasUI: this.hasUI,\n\t\t\tcwd: this.cwd,\n\t\t\tsessionFile: this.sessionFile,\n\t\t};\n\t}\n\n\t/**\n\t * Emit an event to all hooks.\n\t * Returns the result from branch/tool_result events (if any handler returns one).\n\t */\n\tasync emit(event: HookEvent): Promise<BranchEventResult | ToolResultEventResult | undefined> {\n\t\tconst ctx = this.createContext();\n\t\tlet result: BranchEventResult | ToolResultEventResult | undefined;\n\n\t\tfor (const hook of this.hooks) {\n\t\t\tconst handlers = hook.handlers.get(event.type);\n\t\t\tif (!handlers || handlers.length === 0) continue;\n\n\t\t\tfor (const handler of handlers) {\n\t\t\t\ttry {\n\t\t\t\t\tconst timeout = createTimeout(this.timeout);\n\t\t\t\t\tconst handlerResult = await Promise.race([handler(event, ctx), timeout.promise]);\n\t\t\t\t\ttimeout.clear();\n\n\t\t\t\t\t// For branch events, capture the result\n\t\t\t\t\tif (event.type === \"branch\" && handlerResult) {\n\t\t\t\t\t\tresult = handlerResult as BranchEventResult;\n\t\t\t\t\t}\n\n\t\t\t\t\t// For tool_result events, capture the result\n\t\t\t\t\tif (event.type === \"tool_result\" && handlerResult) {\n\t\t\t\t\t\tresult = handlerResult as ToolResultEventResult;\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\t\t\t\tthis.emitError({\n\t\t\t\t\t\thookPath: hook.path,\n\t\t\t\t\t\tevent: event.type,\n\t\t\t\t\t\terror: message,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Emit a tool_call event to all hooks.\n\t * No timeout - user prompts can take as long as needed.\n\t * Errors are thrown (not swallowed) so caller can block on failure.\n\t */\n\tasync emitToolCall(event: ToolCallEvent): Promise<ToolCallEventResult | undefined> {\n\t\tconst ctx = this.createContext();\n\t\tlet result: ToolCallEventResult | undefined;\n\n\t\tfor (const hook of this.hooks) {\n\t\t\tconst handlers = hook.handlers.get(\"tool_call\");\n\t\t\tif (!handlers || handlers.length === 0) continue;\n\n\t\t\tfor (const handler of handlers) {\n\t\t\t\t// No timeout - let user take their time\n\t\t\t\tconst handlerResult = await handler(event, ctx);\n\n\t\t\t\tif (handlerResult) {\n\t\t\t\t\tresult = handlerResult as ToolCallEventResult;\n\t\t\t\t\t// If blocked, stop processing further hooks\n\t\t\t\t\tif (result.block) {\n\t\t\t\t\t\treturn result;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n}\n"]}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook runner - executes hooks and manages their lifecycle.
|
|
3
|
+
*/
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
/**
|
|
6
|
+
* Default timeout for hook execution (30 seconds).
|
|
7
|
+
*/
|
|
8
|
+
const DEFAULT_TIMEOUT = 30000;
|
|
9
|
+
/**
|
|
10
|
+
* Execute a command and return stdout/stderr/code.
|
|
11
|
+
*/
|
|
12
|
+
async function exec(command, args, cwd) {
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
const proc = spawn(command, args, { cwd, shell: false });
|
|
15
|
+
let stdout = "";
|
|
16
|
+
let stderr = "";
|
|
17
|
+
proc.stdout?.on("data", (data) => {
|
|
18
|
+
stdout += data.toString();
|
|
19
|
+
});
|
|
20
|
+
proc.stderr?.on("data", (data) => {
|
|
21
|
+
stderr += data.toString();
|
|
22
|
+
});
|
|
23
|
+
proc.on("close", (code) => {
|
|
24
|
+
resolve({ stdout, stderr, code: code ?? 0 });
|
|
25
|
+
});
|
|
26
|
+
proc.on("error", (_err) => {
|
|
27
|
+
resolve({ stdout, stderr, code: 1 });
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Create a promise that rejects after a timeout.
|
|
33
|
+
*/
|
|
34
|
+
function createTimeout(ms) {
|
|
35
|
+
let timeoutId;
|
|
36
|
+
const promise = new Promise((_, reject) => {
|
|
37
|
+
timeoutId = setTimeout(() => reject(new Error(`Hook timed out after ${ms}ms`)), ms);
|
|
38
|
+
});
|
|
39
|
+
return {
|
|
40
|
+
promise,
|
|
41
|
+
clear: () => clearTimeout(timeoutId),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/** No-op UI context used when no UI is available */
|
|
45
|
+
const noOpUIContext = {
|
|
46
|
+
select: async () => null,
|
|
47
|
+
confirm: async () => false,
|
|
48
|
+
input: async () => null,
|
|
49
|
+
notify: () => { },
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* HookRunner executes hooks and manages event emission.
|
|
53
|
+
*/
|
|
54
|
+
export class HookRunner {
|
|
55
|
+
hooks;
|
|
56
|
+
uiContext;
|
|
57
|
+
hasUI;
|
|
58
|
+
cwd;
|
|
59
|
+
sessionFile;
|
|
60
|
+
timeout;
|
|
61
|
+
errorListeners = new Set();
|
|
62
|
+
constructor(hooks, cwd, timeout = DEFAULT_TIMEOUT) {
|
|
63
|
+
this.hooks = hooks;
|
|
64
|
+
this.uiContext = noOpUIContext;
|
|
65
|
+
this.hasUI = false;
|
|
66
|
+
this.cwd = cwd;
|
|
67
|
+
this.sessionFile = null;
|
|
68
|
+
this.timeout = timeout;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Set the UI context for hooks.
|
|
72
|
+
* Call this when the mode initializes and UI is available.
|
|
73
|
+
*/
|
|
74
|
+
setUIContext(uiContext, hasUI) {
|
|
75
|
+
this.uiContext = uiContext;
|
|
76
|
+
this.hasUI = hasUI;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Get the paths of all loaded hooks.
|
|
80
|
+
*/
|
|
81
|
+
getHookPaths() {
|
|
82
|
+
return this.hooks.map((h) => h.path);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Set the session file path.
|
|
86
|
+
*/
|
|
87
|
+
setSessionFile(sessionFile) {
|
|
88
|
+
this.sessionFile = sessionFile;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Set the send handler for all hooks' pi.send().
|
|
92
|
+
* Call this when the mode initializes.
|
|
93
|
+
*/
|
|
94
|
+
setSendHandler(handler) {
|
|
95
|
+
for (const hook of this.hooks) {
|
|
96
|
+
hook.setSendHandler(handler);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Subscribe to hook errors.
|
|
101
|
+
* @returns Unsubscribe function
|
|
102
|
+
*/
|
|
103
|
+
onError(listener) {
|
|
104
|
+
this.errorListeners.add(listener);
|
|
105
|
+
return () => this.errorListeners.delete(listener);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Emit an error to all listeners.
|
|
109
|
+
*/
|
|
110
|
+
emitError(error) {
|
|
111
|
+
for (const listener of this.errorListeners) {
|
|
112
|
+
listener(error);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Check if any hooks have handlers for the given event type.
|
|
117
|
+
*/
|
|
118
|
+
hasHandlers(eventType) {
|
|
119
|
+
for (const hook of this.hooks) {
|
|
120
|
+
const handlers = hook.handlers.get(eventType);
|
|
121
|
+
if (handlers && handlers.length > 0) {
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Create the event context for handlers.
|
|
129
|
+
*/
|
|
130
|
+
createContext() {
|
|
131
|
+
return {
|
|
132
|
+
exec: (command, args) => exec(command, args, this.cwd),
|
|
133
|
+
ui: this.uiContext,
|
|
134
|
+
hasUI: this.hasUI,
|
|
135
|
+
cwd: this.cwd,
|
|
136
|
+
sessionFile: this.sessionFile,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Emit an event to all hooks.
|
|
141
|
+
* Returns the result from branch/tool_result events (if any handler returns one).
|
|
142
|
+
*/
|
|
143
|
+
async emit(event) {
|
|
144
|
+
const ctx = this.createContext();
|
|
145
|
+
let result;
|
|
146
|
+
for (const hook of this.hooks) {
|
|
147
|
+
const handlers = hook.handlers.get(event.type);
|
|
148
|
+
if (!handlers || handlers.length === 0)
|
|
149
|
+
continue;
|
|
150
|
+
for (const handler of handlers) {
|
|
151
|
+
try {
|
|
152
|
+
const timeout = createTimeout(this.timeout);
|
|
153
|
+
const handlerResult = await Promise.race([handler(event, ctx), timeout.promise]);
|
|
154
|
+
timeout.clear();
|
|
155
|
+
// For branch events, capture the result
|
|
156
|
+
if (event.type === "branch" && handlerResult) {
|
|
157
|
+
result = handlerResult;
|
|
158
|
+
}
|
|
159
|
+
// For tool_result events, capture the result
|
|
160
|
+
if (event.type === "tool_result" && handlerResult) {
|
|
161
|
+
result = handlerResult;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
166
|
+
this.emitError({
|
|
167
|
+
hookPath: hook.path,
|
|
168
|
+
event: event.type,
|
|
169
|
+
error: message,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return result;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Emit a tool_call event to all hooks.
|
|
178
|
+
* No timeout - user prompts can take as long as needed.
|
|
179
|
+
* Errors are thrown (not swallowed) so caller can block on failure.
|
|
180
|
+
*/
|
|
181
|
+
async emitToolCall(event) {
|
|
182
|
+
const ctx = this.createContext();
|
|
183
|
+
let result;
|
|
184
|
+
for (const hook of this.hooks) {
|
|
185
|
+
const handlers = hook.handlers.get("tool_call");
|
|
186
|
+
if (!handlers || handlers.length === 0)
|
|
187
|
+
continue;
|
|
188
|
+
for (const handler of handlers) {
|
|
189
|
+
// No timeout - let user take their time
|
|
190
|
+
const handlerResult = await handler(event, ctx);
|
|
191
|
+
if (handlerResult) {
|
|
192
|
+
result = handlerResult;
|
|
193
|
+
// If blocked, stop processing further hooks
|
|
194
|
+
if (result.block) {
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return result;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
//# sourceMappingURL=runner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runner.js","sourceRoot":"","sources":["../../../src/core/hooks/runner.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Hook runner - executes hooks and manages their lifecycle.\n */\n\nimport { spawn } from \"node:child_process\";\nimport type { LoadedHook, SendHandler } from \"./loader.js\";\nimport type {\n\tBranchEventResult,\n\tExecResult,\n\tHookError,\n\tHookEvent,\n\tHookEventContext,\n\tHookUIContext,\n\tToolCallEvent,\n\tToolCallEventResult,\n\tToolResultEventResult,\n} from \"./types.js\";\n\n/**\n * Default timeout for hook execution (30 seconds).\n */\nconst DEFAULT_TIMEOUT = 30000;\n\n/**\n * Listener for hook errors.\n */\nexport type HookErrorListener = (error: HookError) => void;\n\n/**\n * Execute a command and return stdout/stderr/code.\n */\nasync function exec(command: string, args: string[], cwd: string): Promise<ExecResult> {\n\treturn new Promise((resolve) => {\n\t\tconst proc = spawn(command, args, { cwd, shell: false });\n\n\t\tlet stdout = \"\";\n\t\tlet stderr = \"\";\n\n\t\tproc.stdout?.on(\"data\", (data) => {\n\t\t\tstdout += data.toString();\n\t\t});\n\n\t\tproc.stderr?.on(\"data\", (data) => {\n\t\t\tstderr += data.toString();\n\t\t});\n\n\t\tproc.on(\"close\", (code) => {\n\t\t\tresolve({ stdout, stderr, code: code ?? 0 });\n\t\t});\n\n\t\tproc.on(\"error\", (_err) => {\n\t\t\tresolve({ stdout, stderr, code: 1 });\n\t\t});\n\t});\n}\n\n/**\n * Create a promise that rejects after a timeout.\n */\nfunction createTimeout(ms: number): { promise: Promise<never>; clear: () => void } {\n\tlet timeoutId: NodeJS.Timeout;\n\tconst promise = new Promise<never>((_, reject) => {\n\t\ttimeoutId = setTimeout(() => reject(new Error(`Hook timed out after ${ms}ms`)), ms);\n\t});\n\treturn {\n\t\tpromise,\n\t\tclear: () => clearTimeout(timeoutId),\n\t};\n}\n\n/** No-op UI context used when no UI is available */\nconst noOpUIContext: HookUIContext = {\n\tselect: async () => null,\n\tconfirm: async () => false,\n\tinput: async () => null,\n\tnotify: () => {},\n};\n\n/**\n * HookRunner executes hooks and manages event emission.\n */\nexport class HookRunner {\n\tprivate hooks: LoadedHook[];\n\tprivate uiContext: HookUIContext;\n\tprivate hasUI: boolean;\n\tprivate cwd: string;\n\tprivate sessionFile: string | null;\n\tprivate timeout: number;\n\tprivate errorListeners: Set<HookErrorListener> = new Set();\n\n\tconstructor(hooks: LoadedHook[], cwd: string, timeout: number = DEFAULT_TIMEOUT) {\n\t\tthis.hooks = hooks;\n\t\tthis.uiContext = noOpUIContext;\n\t\tthis.hasUI = false;\n\t\tthis.cwd = cwd;\n\t\tthis.sessionFile = null;\n\t\tthis.timeout = timeout;\n\t}\n\n\t/**\n\t * Set the UI context for hooks.\n\t * Call this when the mode initializes and UI is available.\n\t */\n\tsetUIContext(uiContext: HookUIContext, hasUI: boolean): void {\n\t\tthis.uiContext = uiContext;\n\t\tthis.hasUI = hasUI;\n\t}\n\n\t/**\n\t * Get the paths of all loaded hooks.\n\t */\n\tgetHookPaths(): string[] {\n\t\treturn this.hooks.map((h) => h.path);\n\t}\n\n\t/**\n\t * Set the session file path.\n\t */\n\tsetSessionFile(sessionFile: string | null): void {\n\t\tthis.sessionFile = sessionFile;\n\t}\n\n\t/**\n\t * Set the send handler for all hooks' pi.send().\n\t * Call this when the mode initializes.\n\t */\n\tsetSendHandler(handler: SendHandler): void {\n\t\tfor (const hook of this.hooks) {\n\t\t\thook.setSendHandler(handler);\n\t\t}\n\t}\n\n\t/**\n\t * Subscribe to hook errors.\n\t * @returns Unsubscribe function\n\t */\n\tonError(listener: HookErrorListener): () => void {\n\t\tthis.errorListeners.add(listener);\n\t\treturn () => this.errorListeners.delete(listener);\n\t}\n\n\t/**\n\t * Emit an error to all listeners.\n\t */\n\tprivate emitError(error: HookError): void {\n\t\tfor (const listener of this.errorListeners) {\n\t\t\tlistener(error);\n\t\t}\n\t}\n\n\t/**\n\t * Check if any hooks have handlers for the given event type.\n\t */\n\thasHandlers(eventType: string): boolean {\n\t\tfor (const hook of this.hooks) {\n\t\t\tconst handlers = hook.handlers.get(eventType);\n\t\t\tif (handlers && handlers.length > 0) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * Create the event context for handlers.\n\t */\n\tprivate createContext(): HookEventContext {\n\t\treturn {\n\t\t\texec: (command: string, args: string[]) => exec(command, args, this.cwd),\n\t\t\tui: this.uiContext,\n\t\t\thasUI: this.hasUI,\n\t\t\tcwd: this.cwd,\n\t\t\tsessionFile: this.sessionFile,\n\t\t};\n\t}\n\n\t/**\n\t * Emit an event to all hooks.\n\t * Returns the result from branch/tool_result events (if any handler returns one).\n\t */\n\tasync emit(event: HookEvent): Promise<BranchEventResult | ToolResultEventResult | undefined> {\n\t\tconst ctx = this.createContext();\n\t\tlet result: BranchEventResult | ToolResultEventResult | undefined;\n\n\t\tfor (const hook of this.hooks) {\n\t\t\tconst handlers = hook.handlers.get(event.type);\n\t\t\tif (!handlers || handlers.length === 0) continue;\n\n\t\t\tfor (const handler of handlers) {\n\t\t\t\ttry {\n\t\t\t\t\tconst timeout = createTimeout(this.timeout);\n\t\t\t\t\tconst handlerResult = await Promise.race([handler(event, ctx), timeout.promise]);\n\t\t\t\t\ttimeout.clear();\n\n\t\t\t\t\t// For branch events, capture the result\n\t\t\t\t\tif (event.type === \"branch\" && handlerResult) {\n\t\t\t\t\t\tresult = handlerResult as BranchEventResult;\n\t\t\t\t\t}\n\n\t\t\t\t\t// For tool_result events, capture the result\n\t\t\t\t\tif (event.type === \"tool_result\" && handlerResult) {\n\t\t\t\t\t\tresult = handlerResult as ToolResultEventResult;\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\t\t\t\tthis.emitError({\n\t\t\t\t\t\thookPath: hook.path,\n\t\t\t\t\t\tevent: event.type,\n\t\t\t\t\t\terror: message,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Emit a tool_call event to all hooks.\n\t * No timeout - user prompts can take as long as needed.\n\t * Errors are thrown (not swallowed) so caller can block on failure.\n\t */\n\tasync emitToolCall(event: ToolCallEvent): Promise<ToolCallEventResult | undefined> {\n\t\tconst ctx = this.createContext();\n\t\tlet result: ToolCallEventResult | undefined;\n\n\t\tfor (const hook of this.hooks) {\n\t\t\tconst handlers = hook.handlers.get(\"tool_call\");\n\t\t\tif (!handlers || handlers.length === 0) continue;\n\n\t\t\tfor (const handler of handlers) {\n\t\t\t\t// No timeout - let user take their time\n\t\t\t\tconst handlerResult = await handler(event, ctx);\n\n\t\t\t\tif (handlerResult) {\n\t\t\t\t\tresult = handlerResult as ToolCallEventResult;\n\t\t\t\t\t// If blocked, stop processing further hooks\n\t\t\t\t\tif (result.block) {\n\t\t\t\t\t\treturn result;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n}\n"]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool wrapper - wraps tools with hook callbacks for interception.
|
|
3
|
+
*/
|
|
4
|
+
import type { AgentTool } from "@mariozechner/pi-ai";
|
|
5
|
+
import type { HookRunner } from "./runner.js";
|
|
6
|
+
/**
|
|
7
|
+
* Wrap a tool with hook callbacks.
|
|
8
|
+
* - Emits tool_call event before execution (can block)
|
|
9
|
+
* - Emits tool_result event after execution (can modify result)
|
|
10
|
+
*/
|
|
11
|
+
export declare function wrapToolWithHooks<T>(tool: AgentTool<any, T>, hookRunner: HookRunner): AgentTool<any, T>;
|
|
12
|
+
/**
|
|
13
|
+
* Wrap all tools with hook callbacks.
|
|
14
|
+
*/
|
|
15
|
+
export declare function wrapToolsWithHooks<T>(tools: AgentTool<any, T>[], hookRunner: HookRunner): AgentTool<any, T>[];
|
|
16
|
+
//# sourceMappingURL=tool-wrapper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-wrapper.d.ts","sourceRoot":"","sources":["../../../src/core/hooks/tool-wrapper.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAG9C;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,UAAU,GAAG,SAAS,CAAC,GAAG,EAAE,CAAC,CAAC,CA4DvG;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,KAAK,EAAE,SAAS,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,UAAU,EAAE,UAAU,GAAG,SAAS,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAE7G","sourcesContent":["/**\n * Tool wrapper - wraps tools with hook callbacks for interception.\n */\n\nimport type { AgentTool } from \"@mariozechner/pi-ai\";\nimport type { HookRunner } from \"./runner.js\";\nimport type { ToolCallEventResult, ToolResultEventResult } from \"./types.js\";\n\n/**\n * Wrap a tool with hook callbacks.\n * - Emits tool_call event before execution (can block)\n * - Emits tool_result event after execution (can modify result)\n */\nexport function wrapToolWithHooks<T>(tool: AgentTool<any, T>, hookRunner: HookRunner): AgentTool<any, T> {\n\treturn {\n\t\t...tool,\n\t\texecute: async (toolCallId: string, params: Record<string, unknown>, signal?: AbortSignal) => {\n\t\t\t// Emit tool_call event - hooks can block execution\n\t\t\t// If hook errors/times out, block by default (fail-safe)\n\t\t\tif (hookRunner.hasHandlers(\"tool_call\")) {\n\t\t\t\ttry {\n\t\t\t\t\tconst callResult = (await hookRunner.emitToolCall({\n\t\t\t\t\t\ttype: \"tool_call\",\n\t\t\t\t\t\ttoolName: tool.name,\n\t\t\t\t\t\ttoolCallId,\n\t\t\t\t\t\tinput: params,\n\t\t\t\t\t})) as ToolCallEventResult | undefined;\n\n\t\t\t\t\tif (callResult?.block) {\n\t\t\t\t\t\tconst reason = callResult.reason || \"Tool execution was blocked by a hook\";\n\t\t\t\t\t\tthrow new Error(reason);\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\t// Hook error or block - throw to mark as error\n\t\t\t\t\tif (err instanceof Error) {\n\t\t\t\t\t\tthrow err;\n\t\t\t\t\t}\n\t\t\t\t\tthrow new Error(`Hook failed, blocking execution: ${String(err)}`);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Execute the actual tool\n\t\t\tconst result = await tool.execute(toolCallId, params, signal);\n\n\t\t\t// Emit tool_result event - hooks can modify the result\n\t\t\tif (hookRunner.hasHandlers(\"tool_result\")) {\n\t\t\t\t// Extract text from result for hooks\n\t\t\t\tconst resultText = result.content\n\t\t\t\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t\t\t\t.map((c) => c.text)\n\t\t\t\t\t.join(\"\\n\");\n\n\t\t\t\tconst resultResult = (await hookRunner.emit({\n\t\t\t\t\ttype: \"tool_result\",\n\t\t\t\t\ttoolName: tool.name,\n\t\t\t\t\ttoolCallId,\n\t\t\t\t\tinput: params,\n\t\t\t\t\tresult: resultText,\n\t\t\t\t\tisError: false,\n\t\t\t\t})) as ToolResultEventResult | undefined;\n\n\t\t\t\t// Apply modifications if any\n\t\t\t\tif (resultResult?.result !== undefined) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\t...result,\n\t\t\t\t\t\tcontent: [{ type: \"text\", text: resultResult.result }],\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn result;\n\t\t},\n\t};\n}\n\n/**\n * Wrap all tools with hook callbacks.\n */\nexport function wrapToolsWithHooks<T>(tools: AgentTool<any, T>[], hookRunner: HookRunner): AgentTool<any, T>[] {\n\treturn tools.map((tool) => wrapToolWithHooks(tool, hookRunner));\n}\n"]}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool wrapper - wraps tools with hook callbacks for interception.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Wrap a tool with hook callbacks.
|
|
6
|
+
* - Emits tool_call event before execution (can block)
|
|
7
|
+
* - Emits tool_result event after execution (can modify result)
|
|
8
|
+
*/
|
|
9
|
+
export function wrapToolWithHooks(tool, hookRunner) {
|
|
10
|
+
return {
|
|
11
|
+
...tool,
|
|
12
|
+
execute: async (toolCallId, params, signal) => {
|
|
13
|
+
// Emit tool_call event - hooks can block execution
|
|
14
|
+
// If hook errors/times out, block by default (fail-safe)
|
|
15
|
+
if (hookRunner.hasHandlers("tool_call")) {
|
|
16
|
+
try {
|
|
17
|
+
const callResult = (await hookRunner.emitToolCall({
|
|
18
|
+
type: "tool_call",
|
|
19
|
+
toolName: tool.name,
|
|
20
|
+
toolCallId,
|
|
21
|
+
input: params,
|
|
22
|
+
}));
|
|
23
|
+
if (callResult?.block) {
|
|
24
|
+
const reason = callResult.reason || "Tool execution was blocked by a hook";
|
|
25
|
+
throw new Error(reason);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
// Hook error or block - throw to mark as error
|
|
30
|
+
if (err instanceof Error) {
|
|
31
|
+
throw err;
|
|
32
|
+
}
|
|
33
|
+
throw new Error(`Hook failed, blocking execution: ${String(err)}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// Execute the actual tool
|
|
37
|
+
const result = await tool.execute(toolCallId, params, signal);
|
|
38
|
+
// Emit tool_result event - hooks can modify the result
|
|
39
|
+
if (hookRunner.hasHandlers("tool_result")) {
|
|
40
|
+
// Extract text from result for hooks
|
|
41
|
+
const resultText = result.content
|
|
42
|
+
.filter((c) => c.type === "text")
|
|
43
|
+
.map((c) => c.text)
|
|
44
|
+
.join("\n");
|
|
45
|
+
const resultResult = (await hookRunner.emit({
|
|
46
|
+
type: "tool_result",
|
|
47
|
+
toolName: tool.name,
|
|
48
|
+
toolCallId,
|
|
49
|
+
input: params,
|
|
50
|
+
result: resultText,
|
|
51
|
+
isError: false,
|
|
52
|
+
}));
|
|
53
|
+
// Apply modifications if any
|
|
54
|
+
if (resultResult?.result !== undefined) {
|
|
55
|
+
return {
|
|
56
|
+
...result,
|
|
57
|
+
content: [{ type: "text", text: resultResult.result }],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Wrap all tools with hook callbacks.
|
|
67
|
+
*/
|
|
68
|
+
export function wrapToolsWithHooks(tools, hookRunner) {
|
|
69
|
+
return tools.map((tool) => wrapToolWithHooks(tool, hookRunner));
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=tool-wrapper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-wrapper.js","sourceRoot":"","sources":["../../../src/core/hooks/tool-wrapper.ts"],"names":[],"mappings":"AAAA;;GAEG;AAMH;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAI,IAAuB,EAAE,UAAsB,EAAqB;IACxG,OAAO;QACN,GAAG,IAAI;QACP,OAAO,EAAE,KAAK,EAAE,UAAkB,EAAE,MAA+B,EAAE,MAAoB,EAAE,EAAE,CAAC;YAC7F,mDAAmD;YACnD,yDAAyD;YACzD,IAAI,UAAU,CAAC,WAAW,CAAC,WAAW,CAAC,EAAE,CAAC;gBACzC,IAAI,CAAC;oBACJ,MAAM,UAAU,GAAG,CAAC,MAAM,UAAU,CAAC,YAAY,CAAC;wBACjD,IAAI,EAAE,WAAW;wBACjB,QAAQ,EAAE,IAAI,CAAC,IAAI;wBACnB,UAAU;wBACV,KAAK,EAAE,MAAM;qBACb,CAAC,CAAoC,CAAC;oBAEvC,IAAI,UAAU,EAAE,KAAK,EAAE,CAAC;wBACvB,MAAM,MAAM,GAAG,UAAU,CAAC,MAAM,IAAI,sCAAsC,CAAC;wBAC3E,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,CAAC;oBACzB,CAAC;gBACF,CAAC;gBAAC,OAAO,GAAG,EAAE,CAAC;oBACd,+CAA+C;oBAC/C,IAAI,GAAG,YAAY,KAAK,EAAE,CAAC;wBAC1B,MAAM,GAAG,CAAC;oBACX,CAAC;oBACD,MAAM,IAAI,KAAK,CAAC,oCAAoC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;gBACpE,CAAC;YACF,CAAC;YAED,0BAA0B;YAC1B,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;YAE9D,uDAAuD;YACvD,IAAI,UAAU,CAAC,WAAW,CAAC,aAAa,CAAC,EAAE,CAAC;gBAC3C,qCAAqC;gBACrC,MAAM,UAAU,GAAG,MAAM,CAAC,OAAO;qBAC/B,MAAM,CAAC,CAAC,CAAC,EAAuC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC;qBACrE,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;qBAClB,IAAI,CAAC,IAAI,CAAC,CAAC;gBAEb,MAAM,YAAY,GAAG,CAAC,MAAM,UAAU,CAAC,IAAI,CAAC;oBAC3C,IAAI,EAAE,aAAa;oBACnB,QAAQ,EAAE,IAAI,CAAC,IAAI;oBACnB,UAAU;oBACV,KAAK,EAAE,MAAM;oBACb,MAAM,EAAE,UAAU;oBAClB,OAAO,EAAE,KAAK;iBACd,CAAC,CAAsC,CAAC;gBAEzC,6BAA6B;gBAC7B,IAAI,YAAY,EAAE,MAAM,KAAK,SAAS,EAAE,CAAC;oBACxC,OAAO;wBACN,GAAG,MAAM;wBACT,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,CAAC,MAAM,EAAE,CAAC;qBACtD,CAAC;gBACH,CAAC;YACF,CAAC;YAED,OAAO,MAAM,CAAC;QAAA,CACd;KACD,CAAC;AAAA,CACF;AAED;;GAEG;AACH,MAAM,UAAU,kBAAkB,CAAI,KAA0B,EAAE,UAAsB,EAAuB;IAC9G,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,iBAAiB,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC;AAAA,CAChE","sourcesContent":["/**\n * Tool wrapper - wraps tools with hook callbacks for interception.\n */\n\nimport type { AgentTool } from \"@mariozechner/pi-ai\";\nimport type { HookRunner } from \"./runner.js\";\nimport type { ToolCallEventResult, ToolResultEventResult } from \"./types.js\";\n\n/**\n * Wrap a tool with hook callbacks.\n * - Emits tool_call event before execution (can block)\n * - Emits tool_result event after execution (can modify result)\n */\nexport function wrapToolWithHooks<T>(tool: AgentTool<any, T>, hookRunner: HookRunner): AgentTool<any, T> {\n\treturn {\n\t\t...tool,\n\t\texecute: async (toolCallId: string, params: Record<string, unknown>, signal?: AbortSignal) => {\n\t\t\t// Emit tool_call event - hooks can block execution\n\t\t\t// If hook errors/times out, block by default (fail-safe)\n\t\t\tif (hookRunner.hasHandlers(\"tool_call\")) {\n\t\t\t\ttry {\n\t\t\t\t\tconst callResult = (await hookRunner.emitToolCall({\n\t\t\t\t\t\ttype: \"tool_call\",\n\t\t\t\t\t\ttoolName: tool.name,\n\t\t\t\t\t\ttoolCallId,\n\t\t\t\t\t\tinput: params,\n\t\t\t\t\t})) as ToolCallEventResult | undefined;\n\n\t\t\t\t\tif (callResult?.block) {\n\t\t\t\t\t\tconst reason = callResult.reason || \"Tool execution was blocked by a hook\";\n\t\t\t\t\t\tthrow new Error(reason);\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\t// Hook error or block - throw to mark as error\n\t\t\t\t\tif (err instanceof Error) {\n\t\t\t\t\t\tthrow err;\n\t\t\t\t\t}\n\t\t\t\t\tthrow new Error(`Hook failed, blocking execution: ${String(err)}`);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Execute the actual tool\n\t\t\tconst result = await tool.execute(toolCallId, params, signal);\n\n\t\t\t// Emit tool_result event - hooks can modify the result\n\t\t\tif (hookRunner.hasHandlers(\"tool_result\")) {\n\t\t\t\t// Extract text from result for hooks\n\t\t\t\tconst resultText = result.content\n\t\t\t\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t\t\t\t.map((c) => c.text)\n\t\t\t\t\t.join(\"\\n\");\n\n\t\t\t\tconst resultResult = (await hookRunner.emit({\n\t\t\t\t\ttype: \"tool_result\",\n\t\t\t\t\ttoolName: tool.name,\n\t\t\t\t\ttoolCallId,\n\t\t\t\t\tinput: params,\n\t\t\t\t\tresult: resultText,\n\t\t\t\t\tisError: false,\n\t\t\t\t})) as ToolResultEventResult | undefined;\n\n\t\t\t\t// Apply modifications if any\n\t\t\t\tif (resultResult?.result !== undefined) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\t...result,\n\t\t\t\t\t\tcontent: [{ type: \"text\", text: resultResult.result }],\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn result;\n\t\t},\n\t};\n}\n\n/**\n * Wrap all tools with hook callbacks.\n */\nexport function wrapToolsWithHooks<T>(tools: AgentTool<any, T>[], hookRunner: HookRunner): AgentTool<any, T>[] {\n\treturn tools.map((tool) => wrapToolWithHooks(tool, hookRunner));\n}\n"]}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook system types.
|
|
3
|
+
*
|
|
4
|
+
* Hooks are TypeScript modules that can subscribe to agent lifecycle events
|
|
5
|
+
* and interact with the user via UI primitives.
|
|
6
|
+
*/
|
|
7
|
+
import type { AppMessage, Attachment } from "@mariozechner/pi-agent-core";
|
|
8
|
+
import type { SessionEntry } from "../session-manager.js";
|
|
9
|
+
/**
|
|
10
|
+
* Result of executing a command via ctx.exec()
|
|
11
|
+
*/
|
|
12
|
+
export interface ExecResult {
|
|
13
|
+
stdout: string;
|
|
14
|
+
stderr: string;
|
|
15
|
+
code: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* UI context for hooks to request interactive UI from the harness.
|
|
19
|
+
* Each mode (interactive, RPC, print) provides its own implementation.
|
|
20
|
+
*/
|
|
21
|
+
export interface HookUIContext {
|
|
22
|
+
/**
|
|
23
|
+
* Show a selector and return the user's choice.
|
|
24
|
+
* @param title - Title to display
|
|
25
|
+
* @param options - Array of string options
|
|
26
|
+
* @returns Selected option string, or null if cancelled
|
|
27
|
+
*/
|
|
28
|
+
select(title: string, options: string[]): Promise<string | null>;
|
|
29
|
+
/**
|
|
30
|
+
* Show a confirmation dialog.
|
|
31
|
+
* @returns true if confirmed, false if cancelled
|
|
32
|
+
*/
|
|
33
|
+
confirm(title: string, message: string): Promise<boolean>;
|
|
34
|
+
/**
|
|
35
|
+
* Show a text input dialog.
|
|
36
|
+
* @returns User input, or null if cancelled
|
|
37
|
+
*/
|
|
38
|
+
input(title: string, placeholder?: string): Promise<string | null>;
|
|
39
|
+
/**
|
|
40
|
+
* Show a notification to the user.
|
|
41
|
+
*/
|
|
42
|
+
notify(message: string, type?: "info" | "warning" | "error"): void;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Context passed to hook event handlers.
|
|
46
|
+
*/
|
|
47
|
+
export interface HookEventContext {
|
|
48
|
+
/** Execute a command and return stdout/stderr/code */
|
|
49
|
+
exec(command: string, args: string[]): Promise<ExecResult>;
|
|
50
|
+
/** UI methods for user interaction */
|
|
51
|
+
ui: HookUIContext;
|
|
52
|
+
/** Whether UI is available (false in print mode) */
|
|
53
|
+
hasUI: boolean;
|
|
54
|
+
/** Current working directory */
|
|
55
|
+
cwd: string;
|
|
56
|
+
/** Path to session file, or null if --no-session */
|
|
57
|
+
sessionFile: string | null;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Event data for session_start event.
|
|
61
|
+
* Fired once when the coding agent starts up.
|
|
62
|
+
*/
|
|
63
|
+
export interface SessionStartEvent {
|
|
64
|
+
type: "session_start";
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Event data for session_switch event.
|
|
68
|
+
* Fired when the session changes (branch or session switch).
|
|
69
|
+
*/
|
|
70
|
+
export interface SessionSwitchEvent {
|
|
71
|
+
type: "session_switch";
|
|
72
|
+
/** New session file path */
|
|
73
|
+
newSessionFile: string;
|
|
74
|
+
/** Previous session file path */
|
|
75
|
+
previousSessionFile: string;
|
|
76
|
+
/** Reason for the switch */
|
|
77
|
+
reason: "branch" | "switch";
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Event data for agent_start event.
|
|
81
|
+
* Fired when an agent loop starts (once per user prompt).
|
|
82
|
+
*/
|
|
83
|
+
export interface AgentStartEvent {
|
|
84
|
+
type: "agent_start";
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Event data for agent_end event.
|
|
88
|
+
*/
|
|
89
|
+
export interface AgentEndEvent {
|
|
90
|
+
type: "agent_end";
|
|
91
|
+
messages: AppMessage[];
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Event data for turn_start event.
|
|
95
|
+
*/
|
|
96
|
+
export interface TurnStartEvent {
|
|
97
|
+
type: "turn_start";
|
|
98
|
+
turnIndex: number;
|
|
99
|
+
timestamp: number;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Event data for turn_end event.
|
|
103
|
+
*/
|
|
104
|
+
export interface TurnEndEvent {
|
|
105
|
+
type: "turn_end";
|
|
106
|
+
turnIndex: number;
|
|
107
|
+
message: AppMessage;
|
|
108
|
+
toolResults: AppMessage[];
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Event data for tool_call event.
|
|
112
|
+
* Fired before a tool is executed. Hooks can block execution.
|
|
113
|
+
*/
|
|
114
|
+
export interface ToolCallEvent {
|
|
115
|
+
type: "tool_call";
|
|
116
|
+
/** Tool name (e.g., "bash", "edit", "write") */
|
|
117
|
+
toolName: string;
|
|
118
|
+
/** Tool call ID */
|
|
119
|
+
toolCallId: string;
|
|
120
|
+
/** Tool input parameters */
|
|
121
|
+
input: Record<string, unknown>;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Event data for tool_result event.
|
|
125
|
+
* Fired after a tool is executed. Hooks can modify the result.
|
|
126
|
+
*/
|
|
127
|
+
export interface ToolResultEvent {
|
|
128
|
+
type: "tool_result";
|
|
129
|
+
/** Tool name (e.g., "bash", "edit", "write") */
|
|
130
|
+
toolName: string;
|
|
131
|
+
/** Tool call ID */
|
|
132
|
+
toolCallId: string;
|
|
133
|
+
/** Tool input parameters */
|
|
134
|
+
input: Record<string, unknown>;
|
|
135
|
+
/** Tool result content (text) */
|
|
136
|
+
result: string;
|
|
137
|
+
/** Whether the tool execution was an error */
|
|
138
|
+
isError: boolean;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Event data for branch event.
|
|
142
|
+
*/
|
|
143
|
+
export interface BranchEvent {
|
|
144
|
+
type: "branch";
|
|
145
|
+
/** Index of the turn to branch from */
|
|
146
|
+
targetTurnIndex: number;
|
|
147
|
+
/** Full session history */
|
|
148
|
+
entries: SessionEntry[];
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Union of all hook event types.
|
|
152
|
+
*/
|
|
153
|
+
export type HookEvent = SessionStartEvent | SessionSwitchEvent | AgentStartEvent | AgentEndEvent | TurnStartEvent | TurnEndEvent | ToolCallEvent | ToolResultEvent | BranchEvent;
|
|
154
|
+
/**
|
|
155
|
+
* Return type for tool_call event handlers.
|
|
156
|
+
* Allows hooks to block tool execution.
|
|
157
|
+
*/
|
|
158
|
+
export interface ToolCallEventResult {
|
|
159
|
+
/** If true, block the tool from executing */
|
|
160
|
+
block?: boolean;
|
|
161
|
+
/** Reason for blocking (returned to LLM as error) */
|
|
162
|
+
reason?: string;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Return type for tool_result event handlers.
|
|
166
|
+
* Allows hooks to modify tool results.
|
|
167
|
+
*/
|
|
168
|
+
export interface ToolResultEventResult {
|
|
169
|
+
/** Modified result text (if not set, original result is used) */
|
|
170
|
+
result?: string;
|
|
171
|
+
/** Override isError flag */
|
|
172
|
+
isError?: boolean;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Return type for branch event handlers.
|
|
176
|
+
* Allows hooks to control branch behavior.
|
|
177
|
+
*/
|
|
178
|
+
export interface BranchEventResult {
|
|
179
|
+
/** If true, skip restoring the conversation (only restore code) */
|
|
180
|
+
skipConversationRestore?: boolean;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Handler function type for each event.
|
|
184
|
+
*/
|
|
185
|
+
export type HookHandler<E, R = void> = (event: E, ctx: HookEventContext) => Promise<R>;
|
|
186
|
+
/**
|
|
187
|
+
* HookAPI passed to hook factory functions.
|
|
188
|
+
* Hooks use pi.on() to subscribe to events and pi.send() to inject messages.
|
|
189
|
+
*/
|
|
190
|
+
export interface HookAPI {
|
|
191
|
+
on(event: "session_start", handler: HookHandler<SessionStartEvent>): void;
|
|
192
|
+
on(event: "session_switch", handler: HookHandler<SessionSwitchEvent>): void;
|
|
193
|
+
on(event: "agent_start", handler: HookHandler<AgentStartEvent>): void;
|
|
194
|
+
on(event: "agent_end", handler: HookHandler<AgentEndEvent>): void;
|
|
195
|
+
on(event: "turn_start", handler: HookHandler<TurnStartEvent>): void;
|
|
196
|
+
on(event: "turn_end", handler: HookHandler<TurnEndEvent>): void;
|
|
197
|
+
on(event: "tool_call", handler: HookHandler<ToolCallEvent, ToolCallEventResult | undefined>): void;
|
|
198
|
+
on(event: "tool_result", handler: HookHandler<ToolResultEvent, ToolResultEventResult | undefined>): void;
|
|
199
|
+
on(event: "branch", handler: HookHandler<BranchEvent, BranchEventResult | undefined>): void;
|
|
200
|
+
/**
|
|
201
|
+
* Send a message to the agent.
|
|
202
|
+
* If the agent is streaming, the message is queued.
|
|
203
|
+
* If the agent is idle, a new agent loop is started.
|
|
204
|
+
*/
|
|
205
|
+
send(text: string, attachments?: Attachment[]): void;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Hook factory function type.
|
|
209
|
+
* Hooks export a default function that receives the HookAPI.
|
|
210
|
+
*/
|
|
211
|
+
export type HookFactory = (pi: HookAPI) => void;
|
|
212
|
+
/**
|
|
213
|
+
* Error emitted when a hook fails.
|
|
214
|
+
*/
|
|
215
|
+
export interface HookError {
|
|
216
|
+
hookPath: string;
|
|
217
|
+
event: string;
|
|
218
|
+
error: string;
|
|
219
|
+
}
|
|
220
|
+
//# sourceMappingURL=types.d.ts.map
|