@mariozechner/pi-coding-agent 0.32.3 → 0.34.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.
Files changed (102) hide show
  1. package/CHANGELOG.md +56 -1
  2. package/README.md +76 -3
  3. package/dist/cli/args.d.ts +5 -1
  4. package/dist/cli/args.d.ts.map +1 -1
  5. package/dist/cli/args.js +18 -1
  6. package/dist/cli/args.js.map +1 -1
  7. package/dist/core/agent-session.d.ts +24 -1
  8. package/dist/core/agent-session.d.ts.map +1 -1
  9. package/dist/core/agent-session.js +65 -9
  10. package/dist/core/agent-session.js.map +1 -1
  11. package/dist/core/bash-executor.d.ts.map +1 -1
  12. package/dist/core/bash-executor.js +2 -1
  13. package/dist/core/bash-executor.js.map +1 -1
  14. package/dist/core/custom-tools/loader.d.ts.map +1 -1
  15. package/dist/core/custom-tools/loader.js +1 -0
  16. package/dist/core/custom-tools/loader.js.map +1 -1
  17. package/dist/core/export-html/template.css +34 -4
  18. package/dist/core/export-html/template.js +17 -4
  19. package/dist/core/hooks/index.d.ts +1 -1
  20. package/dist/core/hooks/index.d.ts.map +1 -1
  21. package/dist/core/hooks/index.js.map +1 -1
  22. package/dist/core/hooks/loader.d.ts +56 -1
  23. package/dist/core/hooks/loader.d.ts.map +1 -1
  24. package/dist/core/hooks/loader.js +54 -2
  25. package/dist/core/hooks/loader.js.map +1 -1
  26. package/dist/core/hooks/runner.d.ts +33 -5
  27. package/dist/core/hooks/runner.d.ts.map +1 -1
  28. package/dist/core/hooks/runner.js +100 -9
  29. package/dist/core/hooks/runner.js.map +1 -1
  30. package/dist/core/hooks/types.d.ts +135 -3
  31. package/dist/core/hooks/types.d.ts.map +1 -1
  32. package/dist/core/hooks/types.js.map +1 -1
  33. package/dist/core/keybindings.d.ts +59 -0
  34. package/dist/core/keybindings.d.ts.map +1 -0
  35. package/dist/core/keybindings.js +149 -0
  36. package/dist/core/keybindings.js.map +1 -0
  37. package/dist/core/sdk.d.ts +3 -0
  38. package/dist/core/sdk.d.ts.map +1 -1
  39. package/dist/core/sdk.js +102 -27
  40. package/dist/core/sdk.js.map +1 -1
  41. package/dist/index.d.ts +1 -1
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +1 -1
  44. package/dist/index.js.map +1 -1
  45. package/dist/main.d.ts.map +1 -1
  46. package/dist/main.js +32 -7
  47. package/dist/main.js.map +1 -1
  48. package/dist/modes/interactive/components/custom-editor.d.ts +13 -12
  49. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  50. package/dist/modes/interactive/components/custom-editor.js +50 -68
  51. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  52. package/dist/modes/interactive/components/hook-editor.d.ts.map +1 -1
  53. package/dist/modes/interactive/components/hook-editor.js +5 -4
  54. package/dist/modes/interactive/components/hook-editor.js.map +1 -1
  55. package/dist/modes/interactive/components/hook-input.d.ts.map +1 -1
  56. package/dist/modes/interactive/components/hook-input.js +4 -3
  57. package/dist/modes/interactive/components/hook-input.js.map +1 -1
  58. package/dist/modes/interactive/components/hook-selector.d.ts.map +1 -1
  59. package/dist/modes/interactive/components/hook-selector.js +6 -5
  60. package/dist/modes/interactive/components/hook-selector.js.map +1 -1
  61. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  62. package/dist/modes/interactive/components/model-selector.js +6 -5
  63. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  64. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  65. package/dist/modes/interactive/components/oauth-selector.js +6 -5
  66. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  67. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  68. package/dist/modes/interactive/components/session-selector.js +6 -9
  69. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  70. package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
  71. package/dist/modes/interactive/components/tree-selector.js +14 -15
  72. package/dist/modes/interactive/components/tree-selector.js.map +1 -1
  73. package/dist/modes/interactive/components/user-message-selector.d.ts.map +1 -1
  74. package/dist/modes/interactive/components/user-message-selector.js +6 -11
  75. package/dist/modes/interactive/components/user-message-selector.js.map +1 -1
  76. package/dist/modes/interactive/interactive-mode.d.ts +34 -1
  77. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  78. package/dist/modes/interactive/interactive-mode.js +300 -64
  79. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  80. package/dist/modes/interactive/theme/theme.d.ts +1 -0
  81. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  82. package/dist/modes/interactive/theme/theme.js +3 -0
  83. package/dist/modes/interactive/theme/theme.js.map +1 -1
  84. package/dist/modes/print-mode.d.ts.map +1 -1
  85. package/dist/modes/print-mode.js +3 -0
  86. package/dist/modes/print-mode.js.map +1 -1
  87. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  88. package/dist/modes/rpc/rpc-mode.js +16 -0
  89. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  90. package/dist/modes/rpc/rpc-types.d.ts +6 -0
  91. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  92. package/dist/modes/rpc/rpc-types.js.map +1 -1
  93. package/docs/hooks.md +114 -4
  94. package/docs/tui.md +18 -15
  95. package/examples/custom-tools/subagent/README.md +2 -2
  96. package/examples/hooks/README.md +3 -0
  97. package/examples/hooks/pirate.ts +44 -0
  98. package/examples/hooks/plan-mode.ts +548 -0
  99. package/examples/hooks/snake.ts +7 -7
  100. package/examples/hooks/todo/index.ts +2 -2
  101. package/examples/hooks/tools.ts +145 -0
  102. package/package.json +5 -4
@@ -1 +1 @@
1
- {"version":3,"file":"bash-executor.d.ts","sourceRoot":"","sources":["../../src/core/bash-executor.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAeH,MAAM,WAAW,mBAAmB;IACnC,+DAA+D;IAC/D,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,mCAAmC;IACnC,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IAC1B,sEAAsE;IACtE,MAAM,EAAE,MAAM,CAAC;IACf,wDAAwD;IACxD,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,mDAAmD;IACnD,SAAS,EAAE,OAAO,CAAC;IACnB,uCAAuC;IACvC,SAAS,EAAE,OAAO,CAAC;IACnB,yFAAyF;IACzF,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAMD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,UAAU,CAAC,CAqH/F","sourcesContent":["/**\n * Bash command execution with streaming support and cancellation.\n *\n * This module provides a unified bash execution implementation used by:\n * - AgentSession.executeBash() for interactive and RPC modes\n * - Direct calls from modes that need bash execution\n */\n\nimport { randomBytes } from \"node:crypto\";\nimport { createWriteStream, type WriteStream } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { type ChildProcess, spawn } from \"child_process\";\nimport stripAnsi from \"strip-ansi\";\nimport { getShellConfig, killProcessTree, sanitizeBinaryOutput } from \"../utils/shell.js\";\nimport { DEFAULT_MAX_BYTES, truncateTail } from \"./tools/truncate.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface BashExecutorOptions {\n\t/** Callback for streaming output chunks (already sanitized) */\n\tonChunk?: (chunk: string) => void;\n\t/** AbortSignal for cancellation */\n\tsignal?: AbortSignal;\n}\n\nexport interface BashResult {\n\t/** Combined stdout + stderr output (sanitized, possibly truncated) */\n\toutput: string;\n\t/** Process exit code (undefined if killed/cancelled) */\n\texitCode: number | undefined;\n\t/** Whether the command was cancelled via signal */\n\tcancelled: boolean;\n\t/** Whether the output was truncated */\n\ttruncated: boolean;\n\t/** Path to temp file containing full output (if output exceeded truncation threshold) */\n\tfullOutputPath?: string;\n}\n\n// ============================================================================\n// Implementation\n// ============================================================================\n\n/**\n * Execute a bash command with optional streaming and cancellation support.\n *\n * Features:\n * - Streams sanitized output via onChunk callback\n * - Writes large output to temp file for later retrieval\n * - Supports cancellation via AbortSignal\n * - Sanitizes output (strips ANSI, removes binary garbage, normalizes newlines)\n * - Truncates output if it exceeds the default max bytes\n *\n * @param command - The bash command to execute\n * @param options - Optional streaming callback and abort signal\n * @returns Promise resolving to execution result\n */\nexport function executeBash(command: string, options?: BashExecutorOptions): Promise<BashResult> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst { shell, args } = getShellConfig();\n\t\tconst child: ChildProcess = spawn(shell, [...args, command], {\n\t\t\tdetached: true,\n\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t});\n\n\t\t// Track sanitized output for truncation\n\t\tconst outputChunks: string[] = [];\n\t\tlet outputBytes = 0;\n\t\tconst maxOutputBytes = DEFAULT_MAX_BYTES * 2;\n\n\t\t// Temp file for large output\n\t\tlet tempFilePath: string | undefined;\n\t\tlet tempFileStream: WriteStream | undefined;\n\t\tlet totalBytes = 0;\n\n\t\t// Handle abort signal\n\t\tconst abortHandler = () => {\n\t\t\tif (child.pid) {\n\t\t\t\tkillProcessTree(child.pid);\n\t\t\t}\n\t\t};\n\n\t\tif (options?.signal) {\n\t\t\tif (options.signal.aborted) {\n\t\t\t\t// Already aborted, don't even start\n\t\t\t\tchild.kill();\n\t\t\t\tresolve({\n\t\t\t\t\toutput: \"\",\n\t\t\t\t\texitCode: undefined,\n\t\t\t\t\tcancelled: true,\n\t\t\t\t\ttruncated: false,\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t}\n\t\t\toptions.signal.addEventListener(\"abort\", abortHandler, { once: true });\n\t\t}\n\n\t\tconst handleData = (data: Buffer) => {\n\t\t\ttotalBytes += data.length;\n\n\t\t\t// Sanitize once at the source: strip ANSI, replace binary garbage, normalize newlines\n\t\t\tconst text = sanitizeBinaryOutput(stripAnsi(data.toString())).replace(/\\r/g, \"\");\n\n\t\t\t// Start writing to temp file if exceeds threshold\n\t\t\tif (totalBytes > DEFAULT_MAX_BYTES && !tempFilePath) {\n\t\t\t\tconst id = randomBytes(8).toString(\"hex\");\n\t\t\t\ttempFilePath = join(tmpdir(), `pi-bash-${id}.log`);\n\t\t\t\ttempFileStream = createWriteStream(tempFilePath);\n\t\t\t\t// Write already-buffered chunks to temp file\n\t\t\t\tfor (const chunk of outputChunks) {\n\t\t\t\t\ttempFileStream.write(chunk);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (tempFileStream) {\n\t\t\t\ttempFileStream.write(text);\n\t\t\t}\n\n\t\t\t// Keep rolling buffer of sanitized text\n\t\t\toutputChunks.push(text);\n\t\t\toutputBytes += text.length;\n\t\t\twhile (outputBytes > maxOutputBytes && outputChunks.length > 1) {\n\t\t\t\tconst removed = outputChunks.shift()!;\n\t\t\t\toutputBytes -= removed.length;\n\t\t\t}\n\n\t\t\t// Stream to callback if provided\n\t\t\tif (options?.onChunk) {\n\t\t\t\toptions.onChunk(text);\n\t\t\t}\n\t\t};\n\n\t\tchild.stdout?.on(\"data\", handleData);\n\t\tchild.stderr?.on(\"data\", handleData);\n\n\t\tchild.on(\"close\", (code) => {\n\t\t\t// Clean up abort listener\n\t\t\tif (options?.signal) {\n\t\t\t\toptions.signal.removeEventListener(\"abort\", abortHandler);\n\t\t\t}\n\n\t\t\tif (tempFileStream) {\n\t\t\t\ttempFileStream.end();\n\t\t\t}\n\n\t\t\t// Combine buffered chunks for truncation (already sanitized)\n\t\t\tconst fullOutput = outputChunks.join(\"\");\n\t\t\tconst truncationResult = truncateTail(fullOutput);\n\n\t\t\t// code === null means killed (cancelled)\n\t\t\tconst cancelled = code === null;\n\n\t\t\tresolve({\n\t\t\t\toutput: truncationResult.truncated ? truncationResult.content : fullOutput,\n\t\t\t\texitCode: cancelled ? undefined : code,\n\t\t\t\tcancelled,\n\t\t\t\ttruncated: truncationResult.truncated,\n\t\t\t\tfullOutputPath: tempFilePath,\n\t\t\t});\n\t\t});\n\n\t\tchild.on(\"error\", (err) => {\n\t\t\t// Clean up abort listener\n\t\t\tif (options?.signal) {\n\t\t\t\toptions.signal.removeEventListener(\"abort\", abortHandler);\n\t\t\t}\n\n\t\t\tif (tempFileStream) {\n\t\t\t\ttempFileStream.end();\n\t\t\t}\n\n\t\t\treject(err);\n\t\t});\n\t});\n}\n"]}
1
+ {"version":3,"file":"bash-executor.d.ts","sourceRoot":"","sources":["../../src/core/bash-executor.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAeH,MAAM,WAAW,mBAAmB;IACnC,+DAA+D;IAC/D,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,mCAAmC;IACnC,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IAC1B,sEAAsE;IACtE,MAAM,EAAE,MAAM,CAAC;IACf,wDAAwD;IACxD,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,mDAAmD;IACnD,SAAS,EAAE,OAAO,CAAC;IACnB,uCAAuC;IACvC,SAAS,EAAE,OAAO,CAAC;IACnB,yFAAyF;IACzF,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAMD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,UAAU,CAAC,CAuH/F","sourcesContent":["/**\n * Bash command execution with streaming support and cancellation.\n *\n * This module provides a unified bash execution implementation used by:\n * - AgentSession.executeBash() for interactive and RPC modes\n * - Direct calls from modes that need bash execution\n */\n\nimport { randomBytes } from \"node:crypto\";\nimport { createWriteStream, type WriteStream } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { type ChildProcess, spawn } from \"child_process\";\nimport stripAnsi from \"strip-ansi\";\nimport { getShellConfig, killProcessTree, sanitizeBinaryOutput } from \"../utils/shell.js\";\nimport { DEFAULT_MAX_BYTES, truncateTail } from \"./tools/truncate.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface BashExecutorOptions {\n\t/** Callback for streaming output chunks (already sanitized) */\n\tonChunk?: (chunk: string) => void;\n\t/** AbortSignal for cancellation */\n\tsignal?: AbortSignal;\n}\n\nexport interface BashResult {\n\t/** Combined stdout + stderr output (sanitized, possibly truncated) */\n\toutput: string;\n\t/** Process exit code (undefined if killed/cancelled) */\n\texitCode: number | undefined;\n\t/** Whether the command was cancelled via signal */\n\tcancelled: boolean;\n\t/** Whether the output was truncated */\n\ttruncated: boolean;\n\t/** Path to temp file containing full output (if output exceeded truncation threshold) */\n\tfullOutputPath?: string;\n}\n\n// ============================================================================\n// Implementation\n// ============================================================================\n\n/**\n * Execute a bash command with optional streaming and cancellation support.\n *\n * Features:\n * - Streams sanitized output via onChunk callback\n * - Writes large output to temp file for later retrieval\n * - Supports cancellation via AbortSignal\n * - Sanitizes output (strips ANSI, removes binary garbage, normalizes newlines)\n * - Truncates output if it exceeds the default max bytes\n *\n * @param command - The bash command to execute\n * @param options - Optional streaming callback and abort signal\n * @returns Promise resolving to execution result\n */\nexport function executeBash(command: string, options?: BashExecutorOptions): Promise<BashResult> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst { shell, args } = getShellConfig();\n\t\tconst child: ChildProcess = spawn(shell, [...args, command], {\n\t\t\tdetached: true,\n\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t});\n\n\t\t// Track sanitized output for truncation\n\t\tconst outputChunks: string[] = [];\n\t\tlet outputBytes = 0;\n\t\tconst maxOutputBytes = DEFAULT_MAX_BYTES * 2;\n\n\t\t// Temp file for large output\n\t\tlet tempFilePath: string | undefined;\n\t\tlet tempFileStream: WriteStream | undefined;\n\t\tlet totalBytes = 0;\n\n\t\t// Handle abort signal\n\t\tconst abortHandler = () => {\n\t\t\tif (child.pid) {\n\t\t\t\tkillProcessTree(child.pid);\n\t\t\t}\n\t\t};\n\n\t\tif (options?.signal) {\n\t\t\tif (options.signal.aborted) {\n\t\t\t\t// Already aborted, don't even start\n\t\t\t\tchild.kill();\n\t\t\t\tresolve({\n\t\t\t\t\toutput: \"\",\n\t\t\t\t\texitCode: undefined,\n\t\t\t\t\tcancelled: true,\n\t\t\t\t\ttruncated: false,\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t}\n\t\t\toptions.signal.addEventListener(\"abort\", abortHandler, { once: true });\n\t\t}\n\n\t\tconst decoder = new TextDecoder();\n\n\t\tconst handleData = (data: Buffer) => {\n\t\t\ttotalBytes += data.length;\n\n\t\t\t// Sanitize once at the source: strip ANSI, replace binary garbage, normalize newlines\n\t\t\tconst text = sanitizeBinaryOutput(stripAnsi(decoder.decode(data, { stream: true }))).replace(/\\r/g, \"\");\n\n\t\t\t// Start writing to temp file if exceeds threshold\n\t\t\tif (totalBytes > DEFAULT_MAX_BYTES && !tempFilePath) {\n\t\t\t\tconst id = randomBytes(8).toString(\"hex\");\n\t\t\t\ttempFilePath = join(tmpdir(), `pi-bash-${id}.log`);\n\t\t\t\ttempFileStream = createWriteStream(tempFilePath);\n\t\t\t\t// Write already-buffered chunks to temp file\n\t\t\t\tfor (const chunk of outputChunks) {\n\t\t\t\t\ttempFileStream.write(chunk);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (tempFileStream) {\n\t\t\t\ttempFileStream.write(text);\n\t\t\t}\n\n\t\t\t// Keep rolling buffer of sanitized text\n\t\t\toutputChunks.push(text);\n\t\t\toutputBytes += text.length;\n\t\t\twhile (outputBytes > maxOutputBytes && outputChunks.length > 1) {\n\t\t\t\tconst removed = outputChunks.shift()!;\n\t\t\t\toutputBytes -= removed.length;\n\t\t\t}\n\n\t\t\t// Stream to callback if provided\n\t\t\tif (options?.onChunk) {\n\t\t\t\toptions.onChunk(text);\n\t\t\t}\n\t\t};\n\n\t\tchild.stdout?.on(\"data\", handleData);\n\t\tchild.stderr?.on(\"data\", handleData);\n\n\t\tchild.on(\"close\", (code) => {\n\t\t\t// Clean up abort listener\n\t\t\tif (options?.signal) {\n\t\t\t\toptions.signal.removeEventListener(\"abort\", abortHandler);\n\t\t\t}\n\n\t\t\tif (tempFileStream) {\n\t\t\t\ttempFileStream.end();\n\t\t\t}\n\n\t\t\t// Combine buffered chunks for truncation (already sanitized)\n\t\t\tconst fullOutput = outputChunks.join(\"\");\n\t\t\tconst truncationResult = truncateTail(fullOutput);\n\n\t\t\t// code === null means killed (cancelled)\n\t\t\tconst cancelled = code === null;\n\n\t\t\tresolve({\n\t\t\t\toutput: truncationResult.truncated ? truncationResult.content : fullOutput,\n\t\t\t\texitCode: cancelled ? undefined : code,\n\t\t\t\tcancelled,\n\t\t\t\ttruncated: truncationResult.truncated,\n\t\t\t\tfullOutputPath: tempFilePath,\n\t\t\t});\n\t\t});\n\n\t\tchild.on(\"error\", (err) => {\n\t\t\t// Clean up abort listener\n\t\t\tif (options?.signal) {\n\t\t\t\toptions.signal.removeEventListener(\"abort\", abortHandler);\n\t\t\t}\n\n\t\t\tif (tempFileStream) {\n\t\t\t\ttempFileStream.end();\n\t\t\t}\n\n\t\t\treject(err);\n\t\t});\n\t});\n}\n"]}
@@ -65,10 +65,11 @@ export function executeBash(command, options) {
65
65
  }
66
66
  options.signal.addEventListener("abort", abortHandler, { once: true });
67
67
  }
68
+ const decoder = new TextDecoder();
68
69
  const handleData = (data) => {
69
70
  totalBytes += data.length;
70
71
  // Sanitize once at the source: strip ANSI, replace binary garbage, normalize newlines
71
- const text = sanitizeBinaryOutput(stripAnsi(data.toString())).replace(/\r/g, "");
72
+ const text = sanitizeBinaryOutput(stripAnsi(decoder.decode(data, { stream: true }))).replace(/\r/g, "");
72
73
  // Start writing to temp file if exceeds threshold
73
74
  if (totalBytes > DEFAULT_MAX_BYTES && !tempFilePath) {
74
75
  const id = randomBytes(8).toString("hex");
@@ -1 +1 @@
1
- {"version":3,"file":"bash-executor.js","sourceRoot":"","sources":["../../src/core/bash-executor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Bash command execution with streaming support and cancellation.\n *\n * This module provides a unified bash execution implementation used by:\n * - AgentSession.executeBash() for interactive and RPC modes\n * - Direct calls from modes that need bash execution\n */\n\nimport { randomBytes } from \"node:crypto\";\nimport { createWriteStream, type WriteStream } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { type ChildProcess, spawn } from \"child_process\";\nimport stripAnsi from \"strip-ansi\";\nimport { getShellConfig, killProcessTree, sanitizeBinaryOutput } from \"../utils/shell.js\";\nimport { DEFAULT_MAX_BYTES, truncateTail } from \"./tools/truncate.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface BashExecutorOptions {\n\t/** Callback for streaming output chunks (already sanitized) */\n\tonChunk?: (chunk: string) => void;\n\t/** AbortSignal for cancellation */\n\tsignal?: AbortSignal;\n}\n\nexport interface BashResult {\n\t/** Combined stdout + stderr output (sanitized, possibly truncated) */\n\toutput: string;\n\t/** Process exit code (undefined if killed/cancelled) */\n\texitCode: number | undefined;\n\t/** Whether the command was cancelled via signal */\n\tcancelled: boolean;\n\t/** Whether the output was truncated */\n\ttruncated: boolean;\n\t/** Path to temp file containing full output (if output exceeded truncation threshold) */\n\tfullOutputPath?: string;\n}\n\n// ============================================================================\n// Implementation\n// ============================================================================\n\n/**\n * Execute a bash command with optional streaming and cancellation support.\n *\n * Features:\n * - Streams sanitized output via onChunk callback\n * - Writes large output to temp file for later retrieval\n * - Supports cancellation via AbortSignal\n * - Sanitizes output (strips ANSI, removes binary garbage, normalizes newlines)\n * - Truncates output if it exceeds the default max bytes\n *\n * @param command - The bash command to execute\n * @param options - Optional streaming callback and abort signal\n * @returns Promise resolving to execution result\n */\nexport function executeBash(command: string, options?: BashExecutorOptions): Promise<BashResult> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst { shell, args } = getShellConfig();\n\t\tconst child: ChildProcess = spawn(shell, [...args, command], {\n\t\t\tdetached: true,\n\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t});\n\n\t\t// Track sanitized output for truncation\n\t\tconst outputChunks: string[] = [];\n\t\tlet outputBytes = 0;\n\t\tconst maxOutputBytes = DEFAULT_MAX_BYTES * 2;\n\n\t\t// Temp file for large output\n\t\tlet tempFilePath: string | undefined;\n\t\tlet tempFileStream: WriteStream | undefined;\n\t\tlet totalBytes = 0;\n\n\t\t// Handle abort signal\n\t\tconst abortHandler = () => {\n\t\t\tif (child.pid) {\n\t\t\t\tkillProcessTree(child.pid);\n\t\t\t}\n\t\t};\n\n\t\tif (options?.signal) {\n\t\t\tif (options.signal.aborted) {\n\t\t\t\t// Already aborted, don't even start\n\t\t\t\tchild.kill();\n\t\t\t\tresolve({\n\t\t\t\t\toutput: \"\",\n\t\t\t\t\texitCode: undefined,\n\t\t\t\t\tcancelled: true,\n\t\t\t\t\ttruncated: false,\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t}\n\t\t\toptions.signal.addEventListener(\"abort\", abortHandler, { once: true });\n\t\t}\n\n\t\tconst handleData = (data: Buffer) => {\n\t\t\ttotalBytes += data.length;\n\n\t\t\t// Sanitize once at the source: strip ANSI, replace binary garbage, normalize newlines\n\t\t\tconst text = sanitizeBinaryOutput(stripAnsi(data.toString())).replace(/\\r/g, \"\");\n\n\t\t\t// Start writing to temp file if exceeds threshold\n\t\t\tif (totalBytes > DEFAULT_MAX_BYTES && !tempFilePath) {\n\t\t\t\tconst id = randomBytes(8).toString(\"hex\");\n\t\t\t\ttempFilePath = join(tmpdir(), `pi-bash-${id}.log`);\n\t\t\t\ttempFileStream = createWriteStream(tempFilePath);\n\t\t\t\t// Write already-buffered chunks to temp file\n\t\t\t\tfor (const chunk of outputChunks) {\n\t\t\t\t\ttempFileStream.write(chunk);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (tempFileStream) {\n\t\t\t\ttempFileStream.write(text);\n\t\t\t}\n\n\t\t\t// Keep rolling buffer of sanitized text\n\t\t\toutputChunks.push(text);\n\t\t\toutputBytes += text.length;\n\t\t\twhile (outputBytes > maxOutputBytes && outputChunks.length > 1) {\n\t\t\t\tconst removed = outputChunks.shift()!;\n\t\t\t\toutputBytes -= removed.length;\n\t\t\t}\n\n\t\t\t// Stream to callback if provided\n\t\t\tif (options?.onChunk) {\n\t\t\t\toptions.onChunk(text);\n\t\t\t}\n\t\t};\n\n\t\tchild.stdout?.on(\"data\", handleData);\n\t\tchild.stderr?.on(\"data\", handleData);\n\n\t\tchild.on(\"close\", (code) => {\n\t\t\t// Clean up abort listener\n\t\t\tif (options?.signal) {\n\t\t\t\toptions.signal.removeEventListener(\"abort\", abortHandler);\n\t\t\t}\n\n\t\t\tif (tempFileStream) {\n\t\t\t\ttempFileStream.end();\n\t\t\t}\n\n\t\t\t// Combine buffered chunks for truncation (already sanitized)\n\t\t\tconst fullOutput = outputChunks.join(\"\");\n\t\t\tconst truncationResult = truncateTail(fullOutput);\n\n\t\t\t// code === null means killed (cancelled)\n\t\t\tconst cancelled = code === null;\n\n\t\t\tresolve({\n\t\t\t\toutput: truncationResult.truncated ? truncationResult.content : fullOutput,\n\t\t\t\texitCode: cancelled ? undefined : code,\n\t\t\t\tcancelled,\n\t\t\t\ttruncated: truncationResult.truncated,\n\t\t\t\tfullOutputPath: tempFilePath,\n\t\t\t});\n\t\t});\n\n\t\tchild.on(\"error\", (err) => {\n\t\t\t// Clean up abort listener\n\t\t\tif (options?.signal) {\n\t\t\t\toptions.signal.removeEventListener(\"abort\", abortHandler);\n\t\t\t}\n\n\t\t\tif (tempFileStream) {\n\t\t\t\ttempFileStream.end();\n\t\t\t}\n\n\t\t\treject(err);\n\t\t});\n\t});\n}\n"]}
1
+ {"version":3,"file":"bash-executor.js","sourceRoot":"","sources":["../../src/core/bash-executor.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,iBAAiB,EAAoB,MAAM,SAAS,CAAC;AAC9D,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAqB,KAAK,EAAE,MAAM,eAAe,CAAC;AACzD,OAAO,SAAS,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAC1F,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AA0BtE,+EAA+E;AAC/E,iBAAiB;AACjB,+EAA+E;AAE/E;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,WAAW,CAAC,OAAe,EAAE,OAA6B,EAAuB;IAChG,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC;QACvC,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,cAAc,EAAE,CAAC;QACzC,MAAM,KAAK,GAAiB,KAAK,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,CAAC,EAAE;YAC5D,QAAQ,EAAE,IAAI;YACd,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC;SACjC,CAAC,CAAC;QAEH,wCAAwC;QACxC,MAAM,YAAY,GAAa,EAAE,CAAC;QAClC,IAAI,WAAW,GAAG,CAAC,CAAC;QACpB,MAAM,cAAc,GAAG,iBAAiB,GAAG,CAAC,CAAC;QAE7C,6BAA6B;QAC7B,IAAI,YAAgC,CAAC;QACrC,IAAI,cAAuC,CAAC;QAC5C,IAAI,UAAU,GAAG,CAAC,CAAC;QAEnB,sBAAsB;QACtB,MAAM,YAAY,GAAG,GAAG,EAAE,CAAC;YAC1B,IAAI,KAAK,CAAC,GAAG,EAAE,CAAC;gBACf,eAAe,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YAC5B,CAAC;QAAA,CACD,CAAC;QAEF,IAAI,OAAO,EAAE,MAAM,EAAE,CAAC;YACrB,IAAI,OAAO,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;gBAC5B,oCAAoC;gBACpC,KAAK,CAAC,IAAI,EAAE,CAAC;gBACb,OAAO,CAAC;oBACP,MAAM,EAAE,EAAE;oBACV,QAAQ,EAAE,SAAS;oBACnB,SAAS,EAAE,IAAI;oBACf,SAAS,EAAE,KAAK;iBAChB,CAAC,CAAC;gBACH,OAAO;YACR,CAAC;YACD,OAAO,CAAC,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,YAAY,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QACxE,CAAC;QAED,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAC;QAElC,MAAM,UAAU,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC;YACpC,UAAU,IAAI,IAAI,CAAC,MAAM,CAAC;YAE1B,sFAAsF;YACtF,MAAM,IAAI,GAAG,oBAAoB,CAAC,SAAS,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;YAExG,kDAAkD;YAClD,IAAI,UAAU,GAAG,iBAAiB,IAAI,CAAC,YAAY,EAAE,CAAC;gBACrD,MAAM,EAAE,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;gBAC1C,YAAY,GAAG,IAAI,CAAC,MAAM,EAAE,EAAE,WAAW,EAAE,MAAM,CAAC,CAAC;gBACnD,cAAc,GAAG,iBAAiB,CAAC,YAAY,CAAC,CAAC;gBACjD,6CAA6C;gBAC7C,KAAK,MAAM,KAAK,IAAI,YAAY,EAAE,CAAC;oBAClC,cAAc,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;gBAC7B,CAAC;YACF,CAAC;YAED,IAAI,cAAc,EAAE,CAAC;gBACpB,cAAc,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YAC5B,CAAC;YAED,wCAAwC;YACxC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACxB,WAAW,IAAI,IAAI,CAAC,MAAM,CAAC;YAC3B,OAAO,WAAW,GAAG,cAAc,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAChE,MAAM,OAAO,GAAG,YAAY,CAAC,KAAK,EAAG,CAAC;gBACtC,WAAW,IAAI,OAAO,CAAC,MAAM,CAAC;YAC/B,CAAC;YAED,iCAAiC;YACjC,IAAI,OAAO,EAAE,OAAO,EAAE,CAAC;gBACtB,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;YACvB,CAAC;QAAA,CACD,CAAC;QAEF,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;QACrC,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;QAErC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC;YAC3B,0BAA0B;YAC1B,IAAI,OAAO,EAAE,MAAM,EAAE,CAAC;gBACrB,OAAO,CAAC,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;YAC3D,CAAC;YAED,IAAI,cAAc,EAAE,CAAC;gBACpB,cAAc,CAAC,GAAG,EAAE,CAAC;YACtB,CAAC;YAED,6DAA6D;YAC7D,MAAM,UAAU,GAAG,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACzC,MAAM,gBAAgB,GAAG,YAAY,CAAC,UAAU,CAAC,CAAC;YAElD,yCAAyC;YACzC,MAAM,SAAS,GAAG,IAAI,KAAK,IAAI,CAAC;YAEhC,OAAO,CAAC;gBACP,MAAM,EAAE,gBAAgB,CAAC,SAAS,CAAC,CAAC,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC,CAAC,UAAU;gBAC1E,QAAQ,EAAE,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI;gBACtC,SAAS;gBACT,SAAS,EAAE,gBAAgB,CAAC,SAAS;gBACrC,cAAc,EAAE,YAAY;aAC5B,CAAC,CAAC;QAAA,CACH,CAAC,CAAC;QAEH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC;YAC1B,0BAA0B;YAC1B,IAAI,OAAO,EAAE,MAAM,EAAE,CAAC;gBACrB,OAAO,CAAC,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;YAC3D,CAAC;YAED,IAAI,cAAc,EAAE,CAAC;gBACpB,cAAc,CAAC,GAAG,EAAE,CAAC;YACtB,CAAC;YAED,MAAM,CAAC,GAAG,CAAC,CAAC;QAAA,CACZ,CAAC,CAAC;IAAA,CACH,CAAC,CAAC;AAAA,CACH","sourcesContent":["/**\n * Bash command execution with streaming support and cancellation.\n *\n * This module provides a unified bash execution implementation used by:\n * - AgentSession.executeBash() for interactive and RPC modes\n * - Direct calls from modes that need bash execution\n */\n\nimport { randomBytes } from \"node:crypto\";\nimport { createWriteStream, type WriteStream } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { type ChildProcess, spawn } from \"child_process\";\nimport stripAnsi from \"strip-ansi\";\nimport { getShellConfig, killProcessTree, sanitizeBinaryOutput } from \"../utils/shell.js\";\nimport { DEFAULT_MAX_BYTES, truncateTail } from \"./tools/truncate.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface BashExecutorOptions {\n\t/** Callback for streaming output chunks (already sanitized) */\n\tonChunk?: (chunk: string) => void;\n\t/** AbortSignal for cancellation */\n\tsignal?: AbortSignal;\n}\n\nexport interface BashResult {\n\t/** Combined stdout + stderr output (sanitized, possibly truncated) */\n\toutput: string;\n\t/** Process exit code (undefined if killed/cancelled) */\n\texitCode: number | undefined;\n\t/** Whether the command was cancelled via signal */\n\tcancelled: boolean;\n\t/** Whether the output was truncated */\n\ttruncated: boolean;\n\t/** Path to temp file containing full output (if output exceeded truncation threshold) */\n\tfullOutputPath?: string;\n}\n\n// ============================================================================\n// Implementation\n// ============================================================================\n\n/**\n * Execute a bash command with optional streaming and cancellation support.\n *\n * Features:\n * - Streams sanitized output via onChunk callback\n * - Writes large output to temp file for later retrieval\n * - Supports cancellation via AbortSignal\n * - Sanitizes output (strips ANSI, removes binary garbage, normalizes newlines)\n * - Truncates output if it exceeds the default max bytes\n *\n * @param command - The bash command to execute\n * @param options - Optional streaming callback and abort signal\n * @returns Promise resolving to execution result\n */\nexport function executeBash(command: string, options?: BashExecutorOptions): Promise<BashResult> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst { shell, args } = getShellConfig();\n\t\tconst child: ChildProcess = spawn(shell, [...args, command], {\n\t\t\tdetached: true,\n\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t});\n\n\t\t// Track sanitized output for truncation\n\t\tconst outputChunks: string[] = [];\n\t\tlet outputBytes = 0;\n\t\tconst maxOutputBytes = DEFAULT_MAX_BYTES * 2;\n\n\t\t// Temp file for large output\n\t\tlet tempFilePath: string | undefined;\n\t\tlet tempFileStream: WriteStream | undefined;\n\t\tlet totalBytes = 0;\n\n\t\t// Handle abort signal\n\t\tconst abortHandler = () => {\n\t\t\tif (child.pid) {\n\t\t\t\tkillProcessTree(child.pid);\n\t\t\t}\n\t\t};\n\n\t\tif (options?.signal) {\n\t\t\tif (options.signal.aborted) {\n\t\t\t\t// Already aborted, don't even start\n\t\t\t\tchild.kill();\n\t\t\t\tresolve({\n\t\t\t\t\toutput: \"\",\n\t\t\t\t\texitCode: undefined,\n\t\t\t\t\tcancelled: true,\n\t\t\t\t\ttruncated: false,\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t}\n\t\t\toptions.signal.addEventListener(\"abort\", abortHandler, { once: true });\n\t\t}\n\n\t\tconst decoder = new TextDecoder();\n\n\t\tconst handleData = (data: Buffer) => {\n\t\t\ttotalBytes += data.length;\n\n\t\t\t// Sanitize once at the source: strip ANSI, replace binary garbage, normalize newlines\n\t\t\tconst text = sanitizeBinaryOutput(stripAnsi(decoder.decode(data, { stream: true }))).replace(/\\r/g, \"\");\n\n\t\t\t// Start writing to temp file if exceeds threshold\n\t\t\tif (totalBytes > DEFAULT_MAX_BYTES && !tempFilePath) {\n\t\t\t\tconst id = randomBytes(8).toString(\"hex\");\n\t\t\t\ttempFilePath = join(tmpdir(), `pi-bash-${id}.log`);\n\t\t\t\ttempFileStream = createWriteStream(tempFilePath);\n\t\t\t\t// Write already-buffered chunks to temp file\n\t\t\t\tfor (const chunk of outputChunks) {\n\t\t\t\t\ttempFileStream.write(chunk);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (tempFileStream) {\n\t\t\t\ttempFileStream.write(text);\n\t\t\t}\n\n\t\t\t// Keep rolling buffer of sanitized text\n\t\t\toutputChunks.push(text);\n\t\t\toutputBytes += text.length;\n\t\t\twhile (outputBytes > maxOutputBytes && outputChunks.length > 1) {\n\t\t\t\tconst removed = outputChunks.shift()!;\n\t\t\t\toutputBytes -= removed.length;\n\t\t\t}\n\n\t\t\t// Stream to callback if provided\n\t\t\tif (options?.onChunk) {\n\t\t\t\toptions.onChunk(text);\n\t\t\t}\n\t\t};\n\n\t\tchild.stdout?.on(\"data\", handleData);\n\t\tchild.stderr?.on(\"data\", handleData);\n\n\t\tchild.on(\"close\", (code) => {\n\t\t\t// Clean up abort listener\n\t\t\tif (options?.signal) {\n\t\t\t\toptions.signal.removeEventListener(\"abort\", abortHandler);\n\t\t\t}\n\n\t\t\tif (tempFileStream) {\n\t\t\t\ttempFileStream.end();\n\t\t\t}\n\n\t\t\t// Combine buffered chunks for truncation (already sanitized)\n\t\t\tconst fullOutput = outputChunks.join(\"\");\n\t\t\tconst truncationResult = truncateTail(fullOutput);\n\n\t\t\t// code === null means killed (cancelled)\n\t\t\tconst cancelled = code === null;\n\n\t\t\tresolve({\n\t\t\t\toutput: truncationResult.truncated ? truncationResult.content : fullOutput,\n\t\t\t\texitCode: cancelled ? undefined : code,\n\t\t\t\tcancelled,\n\t\t\t\ttruncated: truncationResult.truncated,\n\t\t\t\tfullOutputPath: tempFilePath,\n\t\t\t});\n\t\t});\n\n\t\tchild.on(\"error\", (err) => {\n\t\t\t// Clean up abort listener\n\t\t\tif (options?.signal) {\n\t\t\t\toptions.signal.removeEventListener(\"abort\", abortHandler);\n\t\t\t}\n\n\t\t\tif (tempFileStream) {\n\t\t\t\ttempFileStream.end();\n\t\t\t}\n\n\t\t\treject(err);\n\t\t});\n\t});\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../../src/core/custom-tools/loader.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAaH,OAAO,KAAK,EAAoC,qBAAqB,EAAoB,MAAM,YAAY,CAAC;AAuL5G;;;;;GAKG;AACH,wBAAsB,eAAe,CACpC,KAAK,EAAE,MAAM,EAAE,EACf,GAAG,EAAE,MAAM,EACX,gBAAgB,EAAE,MAAM,EAAE,GACxB,OAAO,CAAC,qBAAqB,CAAC,CA+ChC;AAgCD;;;;;;;;;;;GAWG;AACH,wBAAsB,0BAA0B,CAC/C,eAAe,EAAE,MAAM,EAAE,EACzB,GAAG,EAAE,MAAM,EACX,gBAAgB,EAAE,MAAM,EAAE,EAC1B,QAAQ,GAAE,MAAsB,GAC9B,OAAO,CAAC,qBAAqB,CAAC,CA2BhC","sourcesContent":["/**\n * Custom tool loader - loads TypeScript tool modules using jiti.\n *\n * For Bun compiled binaries, custom tools that import from @mariozechner/* packages\n * are not supported because Bun's plugin system doesn't intercept imports from\n * external files loaded at runtime. Users should use the npm-installed version\n * for custom tools that depend on pi packages.\n */\n\nimport * as fs from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { createJiti } from \"jiti\";\nimport { getAgentDir, isBunBinary } from \"../../config.js\";\nimport { theme } from \"../../modes/interactive/theme/theme.js\";\nimport type { ExecOptions } from \"../exec.js\";\nimport { execCommand } from \"../exec.js\";\nimport type { HookUIContext } from \"../hooks/types.js\";\nimport type { CustomToolAPI, CustomToolFactory, CustomToolsLoadResult, LoadedCustomTool } from \"./types.js\";\n\n// Create require function to resolve module paths at runtime\nconst require = createRequire(import.meta.url);\n\n// Lazily computed aliases - resolved at runtime to handle global installs\nlet _aliases: Record<string, string> | null = null;\nfunction getAliases(): Record<string, string> {\n\tif (_aliases) return _aliases;\n\n\tconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\tconst packageIndex = path.resolve(__dirname, \"../..\", \"index.js\");\n\n\t// For typebox, we need the package root directory (not the entry file)\n\t// because jiti's alias is prefix-based: imports like \"@sinclair/typebox/compiler\"\n\t// get the alias prepended. If we alias to the entry file (.../build/cjs/index.js),\n\t// then \"@sinclair/typebox/compiler\" becomes \".../build/cjs/index.js/compiler\" (invalid).\n\t// By aliasing to the package root, it becomes \".../typebox/compiler\" which resolves correctly.\n\tconst typeboxEntry = require.resolve(\"@sinclair/typebox\");\n\tconst typeboxRoot = typeboxEntry.replace(/\\/build\\/cjs\\/index\\.js$/, \"\");\n\n\t_aliases = {\n\t\t\"@mariozechner/pi-coding-agent\": packageIndex,\n\t\t\"@mariozechner/pi-tui\": require.resolve(\"@mariozechner/pi-tui\"),\n\t\t\"@mariozechner/pi-ai\": require.resolve(\"@mariozechner/pi-ai\"),\n\t\t\"@sinclair/typebox\": typeboxRoot,\n\t};\n\treturn _aliases;\n}\n\nconst UNICODE_SPACES = /[\\u00A0\\u2000-\\u200A\\u202F\\u205F\\u3000]/g;\n\nfunction normalizeUnicodeSpaces(str: string): string {\n\treturn str.replace(UNICODE_SPACES, \" \");\n}\n\nfunction expandPath(p: string): string {\n\tconst normalized = normalizeUnicodeSpaces(p);\n\tif (normalized.startsWith(\"~/\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(2));\n\t}\n\tif (normalized.startsWith(\"~\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(1));\n\t}\n\treturn normalized;\n}\n\n/**\n * Resolve tool path.\n * - Absolute paths used as-is\n * - Paths starting with ~ expanded to home directory\n * - Relative paths resolved from cwd\n */\nfunction resolveToolPath(toolPath: string, cwd: string): string {\n\tconst expanded = expandPath(toolPath);\n\n\tif (path.isAbsolute(expanded)) {\n\t\treturn expanded;\n\t}\n\n\t// Relative paths resolved from cwd\n\treturn path.resolve(cwd, expanded);\n}\n\n/**\n * Create a no-op UI context for headless modes.\n */\nfunction createNoOpUIContext(): HookUIContext {\n\treturn {\n\t\tselect: async () => undefined,\n\t\tconfirm: async () => false,\n\t\tinput: async () => undefined,\n\t\tnotify: () => {},\n\t\tsetStatus: () => {},\n\t\tcustom: async () => undefined as never,\n\t\tsetEditorText: () => {},\n\t\tgetEditorText: () => \"\",\n\t\teditor: async () => undefined,\n\t\tget theme() {\n\t\t\treturn theme;\n\t\t},\n\t};\n}\n\n/**\n * Load a tool in Bun binary mode.\n *\n * Since Bun plugins don't work for dynamically loaded external files,\n * custom tools that import from @mariozechner/* packages won't work.\n * Tools that only use standard npm packages (installed in the tool's directory)\n * may still work.\n */\nasync function loadToolWithBun(\n\tresolvedPath: string,\n\tsharedApi: CustomToolAPI,\n): Promise<{ tools: LoadedCustomTool[] | null; error: string | null }> {\n\ttry {\n\t\t// Try to import directly - will work for tools without @mariozechner/* imports\n\t\tconst module = await import(resolvedPath);\n\t\tconst factory = (module.default ?? module) as CustomToolFactory;\n\n\t\tif (typeof factory !== \"function\") {\n\t\t\treturn { tools: null, error: \"Tool must export a default function\" };\n\t\t}\n\n\t\tconst toolResult = await factory(sharedApi);\n\t\tconst toolsArray = Array.isArray(toolResult) ? toolResult : [toolResult];\n\n\t\tconst loadedTools: LoadedCustomTool[] = toolsArray.map((tool) => ({\n\t\t\tpath: resolvedPath,\n\t\t\tresolvedPath,\n\t\t\ttool,\n\t\t}));\n\n\t\treturn { tools: loadedTools, error: null };\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\n\t\t// Check if it's a module resolution error for our packages\n\t\tif (message.includes(\"Cannot find module\") && message.includes(\"@mariozechner/\")) {\n\t\t\treturn {\n\t\t\t\ttools: null,\n\t\t\t\terror:\n\t\t\t\t\t`${message}\\n` +\n\t\t\t\t\t\"Note: Custom tools importing from @mariozechner/* packages are not supported in the standalone binary.\\n\" +\n\t\t\t\t\t\"Please install pi via npm: npm install -g @mariozechner/pi-coding-agent\",\n\t\t\t};\n\t\t}\n\n\t\treturn { tools: null, error: `Failed to load tool: ${message}` };\n\t}\n}\n\n/**\n * Load a single tool module using jiti (or Bun.build for compiled binaries).\n */\nasync function loadTool(\n\ttoolPath: string,\n\tcwd: string,\n\tsharedApi: CustomToolAPI,\n): Promise<{ tools: LoadedCustomTool[] | null; error: string | null }> {\n\tconst resolvedPath = resolveToolPath(toolPath, cwd);\n\n\t// Use Bun.build for compiled binaries since jiti can't resolve bundled modules\n\tif (isBunBinary) {\n\t\treturn loadToolWithBun(resolvedPath, sharedApi);\n\t}\n\n\ttry {\n\t\t// Create jiti instance for TypeScript/ESM loading\n\t\t// Use aliases to resolve package imports since tools are loaded from user directories\n\t\t// (e.g. ~/.pi/agent/tools) but import from packages installed with pi-coding-agent\n\t\tconst jiti = createJiti(import.meta.url, {\n\t\t\talias: getAliases(),\n\t\t});\n\n\t\t// Import the module\n\t\tconst module = await jiti.import(resolvedPath, { default: true });\n\t\tconst factory = module as CustomToolFactory;\n\n\t\tif (typeof factory !== \"function\") {\n\t\t\treturn { tools: null, error: \"Tool must export a default function\" };\n\t\t}\n\n\t\t// Call factory with shared API\n\t\tconst result = await factory(sharedApi);\n\n\t\t// Handle single tool or array of tools\n\t\tconst toolsArray = Array.isArray(result) ? result : [result];\n\n\t\tconst loadedTools: LoadedCustomTool[] = toolsArray.map((tool) => ({\n\t\t\tpath: toolPath,\n\t\t\tresolvedPath,\n\t\t\ttool,\n\t\t}));\n\n\t\treturn { tools: loadedTools, error: null };\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\treturn { tools: null, error: `Failed to load tool: ${message}` };\n\t}\n}\n\n/**\n * Load all tools from configuration.\n * @param paths - Array of tool file paths\n * @param cwd - Current working directory for resolving relative paths\n * @param builtInToolNames - Names of built-in tools to check for conflicts\n */\nexport async function loadCustomTools(\n\tpaths: string[],\n\tcwd: string,\n\tbuiltInToolNames: string[],\n): Promise<CustomToolsLoadResult> {\n\tconst tools: LoadedCustomTool[] = [];\n\tconst errors: Array<{ path: string; error: string }> = [];\n\tconst seenNames = new Set<string>(builtInToolNames);\n\n\t// Shared API object - all tools get the same instance\n\tconst sharedApi: CustomToolAPI = {\n\t\tcwd,\n\t\texec: (command: string, args: string[], options?: ExecOptions) =>\n\t\t\texecCommand(command, args, options?.cwd ?? cwd, options),\n\t\tui: createNoOpUIContext(),\n\t\thasUI: false,\n\t};\n\n\tfor (const toolPath of paths) {\n\t\tconst { tools: loadedTools, error } = await loadTool(toolPath, cwd, sharedApi);\n\n\t\tif (error) {\n\t\t\terrors.push({ path: toolPath, error });\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (loadedTools) {\n\t\t\tfor (const loadedTool of loadedTools) {\n\t\t\t\t// Check for name conflicts\n\t\t\t\tif (seenNames.has(loadedTool.tool.name)) {\n\t\t\t\t\terrors.push({\n\t\t\t\t\t\tpath: toolPath,\n\t\t\t\t\t\terror: `Tool name \"${loadedTool.tool.name}\" conflicts with existing tool`,\n\t\t\t\t\t});\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tseenNames.add(loadedTool.tool.name);\n\t\t\t\ttools.push(loadedTool);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn {\n\t\ttools,\n\t\terrors,\n\t\tsetUIContext(uiContext, hasUI) {\n\t\t\tsharedApi.ui = uiContext;\n\t\t\tsharedApi.hasUI = hasUI;\n\t\t},\n\t};\n}\n\n/**\n * Discover tool files from a directory.\n * Only loads index.ts files from subdirectories (e.g., tools/mytool/index.ts).\n */\nfunction discoverToolsInDir(dir: string): string[] {\n\tif (!fs.existsSync(dir)) {\n\t\treturn [];\n\t}\n\n\tconst tools: string[] = [];\n\n\ttry {\n\t\tconst entries = fs.readdirSync(dir, { withFileTypes: true });\n\n\t\tfor (const entry of entries) {\n\t\t\tif (entry.isDirectory() || entry.isSymbolicLink()) {\n\t\t\t\t// Check for index.ts in subdirectory\n\t\t\t\tconst indexPath = path.join(dir, entry.name, \"index.ts\");\n\t\t\t\tif (fs.existsSync(indexPath)) {\n\t\t\t\t\ttools.push(indexPath);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} catch {\n\t\treturn [];\n\t}\n\n\treturn tools;\n}\n\n/**\n * Discover and load tools from standard locations:\n * 1. agentDir/tools/*.ts (global)\n * 2. cwd/.pi/tools/*.ts (project-local)\n *\n * Plus any explicitly configured paths from settings or CLI.\n *\n * @param configuredPaths - Explicit paths from settings.json and CLI --tool flags\n * @param cwd - Current working directory\n * @param builtInToolNames - Names of built-in tools to check for conflicts\n * @param agentDir - Agent config directory. Default: from getAgentDir()\n */\nexport async function discoverAndLoadCustomTools(\n\tconfiguredPaths: string[],\n\tcwd: string,\n\tbuiltInToolNames: string[],\n\tagentDir: string = getAgentDir(),\n): Promise<CustomToolsLoadResult> {\n\tconst allPaths: string[] = [];\n\tconst seen = new Set<string>();\n\n\t// Helper to add paths without duplicates\n\tconst addPaths = (paths: string[]) => {\n\t\tfor (const p of paths) {\n\t\t\tconst resolved = path.resolve(p);\n\t\t\tif (!seen.has(resolved)) {\n\t\t\t\tseen.add(resolved);\n\t\t\t\tallPaths.push(p);\n\t\t\t}\n\t\t}\n\t};\n\n\t// 1. Global tools: agentDir/tools/\n\tconst globalToolsDir = path.join(agentDir, \"tools\");\n\taddPaths(discoverToolsInDir(globalToolsDir));\n\n\t// 2. Project-local tools: cwd/.pi/tools/\n\tconst localToolsDir = path.join(cwd, \".pi\", \"tools\");\n\taddPaths(discoverToolsInDir(localToolsDir));\n\n\t// 3. Explicitly configured paths (can override/add)\n\taddPaths(configuredPaths.map((p) => resolveToolPath(p, cwd)));\n\n\treturn loadCustomTools(allPaths, cwd, builtInToolNames);\n}\n"]}
1
+ {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../../src/core/custom-tools/loader.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAaH,OAAO,KAAK,EAAoC,qBAAqB,EAAoB,MAAM,YAAY,CAAC;AAwL5G;;;;;GAKG;AACH,wBAAsB,eAAe,CACpC,KAAK,EAAE,MAAM,EAAE,EACf,GAAG,EAAE,MAAM,EACX,gBAAgB,EAAE,MAAM,EAAE,GACxB,OAAO,CAAC,qBAAqB,CAAC,CA+ChC;AAgCD;;;;;;;;;;;GAWG;AACH,wBAAsB,0BAA0B,CAC/C,eAAe,EAAE,MAAM,EAAE,EACzB,GAAG,EAAE,MAAM,EACX,gBAAgB,EAAE,MAAM,EAAE,EAC1B,QAAQ,GAAE,MAAsB,GAC9B,OAAO,CAAC,qBAAqB,CAAC,CA2BhC","sourcesContent":["/**\n * Custom tool loader - loads TypeScript tool modules using jiti.\n *\n * For Bun compiled binaries, custom tools that import from @mariozechner/* packages\n * are not supported because Bun's plugin system doesn't intercept imports from\n * external files loaded at runtime. Users should use the npm-installed version\n * for custom tools that depend on pi packages.\n */\n\nimport * as fs from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { createJiti } from \"jiti\";\nimport { getAgentDir, isBunBinary } from \"../../config.js\";\nimport { theme } from \"../../modes/interactive/theme/theme.js\";\nimport type { ExecOptions } from \"../exec.js\";\nimport { execCommand } from \"../exec.js\";\nimport type { HookUIContext } from \"../hooks/types.js\";\nimport type { CustomToolAPI, CustomToolFactory, CustomToolsLoadResult, LoadedCustomTool } from \"./types.js\";\n\n// Create require function to resolve module paths at runtime\nconst require = createRequire(import.meta.url);\n\n// Lazily computed aliases - resolved at runtime to handle global installs\nlet _aliases: Record<string, string> | null = null;\nfunction getAliases(): Record<string, string> {\n\tif (_aliases) return _aliases;\n\n\tconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\tconst packageIndex = path.resolve(__dirname, \"../..\", \"index.js\");\n\n\t// For typebox, we need the package root directory (not the entry file)\n\t// because jiti's alias is prefix-based: imports like \"@sinclair/typebox/compiler\"\n\t// get the alias prepended. If we alias to the entry file (.../build/cjs/index.js),\n\t// then \"@sinclair/typebox/compiler\" becomes \".../build/cjs/index.js/compiler\" (invalid).\n\t// By aliasing to the package root, it becomes \".../typebox/compiler\" which resolves correctly.\n\tconst typeboxEntry = require.resolve(\"@sinclair/typebox\");\n\tconst typeboxRoot = typeboxEntry.replace(/\\/build\\/cjs\\/index\\.js$/, \"\");\n\n\t_aliases = {\n\t\t\"@mariozechner/pi-coding-agent\": packageIndex,\n\t\t\"@mariozechner/pi-tui\": require.resolve(\"@mariozechner/pi-tui\"),\n\t\t\"@mariozechner/pi-ai\": require.resolve(\"@mariozechner/pi-ai\"),\n\t\t\"@sinclair/typebox\": typeboxRoot,\n\t};\n\treturn _aliases;\n}\n\nconst UNICODE_SPACES = /[\\u00A0\\u2000-\\u200A\\u202F\\u205F\\u3000]/g;\n\nfunction normalizeUnicodeSpaces(str: string): string {\n\treturn str.replace(UNICODE_SPACES, \" \");\n}\n\nfunction expandPath(p: string): string {\n\tconst normalized = normalizeUnicodeSpaces(p);\n\tif (normalized.startsWith(\"~/\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(2));\n\t}\n\tif (normalized.startsWith(\"~\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(1));\n\t}\n\treturn normalized;\n}\n\n/**\n * Resolve tool path.\n * - Absolute paths used as-is\n * - Paths starting with ~ expanded to home directory\n * - Relative paths resolved from cwd\n */\nfunction resolveToolPath(toolPath: string, cwd: string): string {\n\tconst expanded = expandPath(toolPath);\n\n\tif (path.isAbsolute(expanded)) {\n\t\treturn expanded;\n\t}\n\n\t// Relative paths resolved from cwd\n\treturn path.resolve(cwd, expanded);\n}\n\n/**\n * Create a no-op UI context for headless modes.\n */\nfunction createNoOpUIContext(): HookUIContext {\n\treturn {\n\t\tselect: async () => undefined,\n\t\tconfirm: async () => false,\n\t\tinput: async () => undefined,\n\t\tnotify: () => {},\n\t\tsetStatus: () => {},\n\t\tsetWidget: () => {},\n\t\tcustom: async () => undefined as never,\n\t\tsetEditorText: () => {},\n\t\tgetEditorText: () => \"\",\n\t\teditor: async () => undefined,\n\t\tget theme() {\n\t\t\treturn theme;\n\t\t},\n\t};\n}\n\n/**\n * Load a tool in Bun binary mode.\n *\n * Since Bun plugins don't work for dynamically loaded external files,\n * custom tools that import from @mariozechner/* packages won't work.\n * Tools that only use standard npm packages (installed in the tool's directory)\n * may still work.\n */\nasync function loadToolWithBun(\n\tresolvedPath: string,\n\tsharedApi: CustomToolAPI,\n): Promise<{ tools: LoadedCustomTool[] | null; error: string | null }> {\n\ttry {\n\t\t// Try to import directly - will work for tools without @mariozechner/* imports\n\t\tconst module = await import(resolvedPath);\n\t\tconst factory = (module.default ?? module) as CustomToolFactory;\n\n\t\tif (typeof factory !== \"function\") {\n\t\t\treturn { tools: null, error: \"Tool must export a default function\" };\n\t\t}\n\n\t\tconst toolResult = await factory(sharedApi);\n\t\tconst toolsArray = Array.isArray(toolResult) ? toolResult : [toolResult];\n\n\t\tconst loadedTools: LoadedCustomTool[] = toolsArray.map((tool) => ({\n\t\t\tpath: resolvedPath,\n\t\t\tresolvedPath,\n\t\t\ttool,\n\t\t}));\n\n\t\treturn { tools: loadedTools, error: null };\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\n\t\t// Check if it's a module resolution error for our packages\n\t\tif (message.includes(\"Cannot find module\") && message.includes(\"@mariozechner/\")) {\n\t\t\treturn {\n\t\t\t\ttools: null,\n\t\t\t\terror:\n\t\t\t\t\t`${message}\\n` +\n\t\t\t\t\t\"Note: Custom tools importing from @mariozechner/* packages are not supported in the standalone binary.\\n\" +\n\t\t\t\t\t\"Please install pi via npm: npm install -g @mariozechner/pi-coding-agent\",\n\t\t\t};\n\t\t}\n\n\t\treturn { tools: null, error: `Failed to load tool: ${message}` };\n\t}\n}\n\n/**\n * Load a single tool module using jiti (or Bun.build for compiled binaries).\n */\nasync function loadTool(\n\ttoolPath: string,\n\tcwd: string,\n\tsharedApi: CustomToolAPI,\n): Promise<{ tools: LoadedCustomTool[] | null; error: string | null }> {\n\tconst resolvedPath = resolveToolPath(toolPath, cwd);\n\n\t// Use Bun.build for compiled binaries since jiti can't resolve bundled modules\n\tif (isBunBinary) {\n\t\treturn loadToolWithBun(resolvedPath, sharedApi);\n\t}\n\n\ttry {\n\t\t// Create jiti instance for TypeScript/ESM loading\n\t\t// Use aliases to resolve package imports since tools are loaded from user directories\n\t\t// (e.g. ~/.pi/agent/tools) but import from packages installed with pi-coding-agent\n\t\tconst jiti = createJiti(import.meta.url, {\n\t\t\talias: getAliases(),\n\t\t});\n\n\t\t// Import the module\n\t\tconst module = await jiti.import(resolvedPath, { default: true });\n\t\tconst factory = module as CustomToolFactory;\n\n\t\tif (typeof factory !== \"function\") {\n\t\t\treturn { tools: null, error: \"Tool must export a default function\" };\n\t\t}\n\n\t\t// Call factory with shared API\n\t\tconst result = await factory(sharedApi);\n\n\t\t// Handle single tool or array of tools\n\t\tconst toolsArray = Array.isArray(result) ? result : [result];\n\n\t\tconst loadedTools: LoadedCustomTool[] = toolsArray.map((tool) => ({\n\t\t\tpath: toolPath,\n\t\t\tresolvedPath,\n\t\t\ttool,\n\t\t}));\n\n\t\treturn { tools: loadedTools, error: null };\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\treturn { tools: null, error: `Failed to load tool: ${message}` };\n\t}\n}\n\n/**\n * Load all tools from configuration.\n * @param paths - Array of tool file paths\n * @param cwd - Current working directory for resolving relative paths\n * @param builtInToolNames - Names of built-in tools to check for conflicts\n */\nexport async function loadCustomTools(\n\tpaths: string[],\n\tcwd: string,\n\tbuiltInToolNames: string[],\n): Promise<CustomToolsLoadResult> {\n\tconst tools: LoadedCustomTool[] = [];\n\tconst errors: Array<{ path: string; error: string }> = [];\n\tconst seenNames = new Set<string>(builtInToolNames);\n\n\t// Shared API object - all tools get the same instance\n\tconst sharedApi: CustomToolAPI = {\n\t\tcwd,\n\t\texec: (command: string, args: string[], options?: ExecOptions) =>\n\t\t\texecCommand(command, args, options?.cwd ?? cwd, options),\n\t\tui: createNoOpUIContext(),\n\t\thasUI: false,\n\t};\n\n\tfor (const toolPath of paths) {\n\t\tconst { tools: loadedTools, error } = await loadTool(toolPath, cwd, sharedApi);\n\n\t\tif (error) {\n\t\t\terrors.push({ path: toolPath, error });\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (loadedTools) {\n\t\t\tfor (const loadedTool of loadedTools) {\n\t\t\t\t// Check for name conflicts\n\t\t\t\tif (seenNames.has(loadedTool.tool.name)) {\n\t\t\t\t\terrors.push({\n\t\t\t\t\t\tpath: toolPath,\n\t\t\t\t\t\terror: `Tool name \"${loadedTool.tool.name}\" conflicts with existing tool`,\n\t\t\t\t\t});\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tseenNames.add(loadedTool.tool.name);\n\t\t\t\ttools.push(loadedTool);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn {\n\t\ttools,\n\t\terrors,\n\t\tsetUIContext(uiContext, hasUI) {\n\t\t\tsharedApi.ui = uiContext;\n\t\t\tsharedApi.hasUI = hasUI;\n\t\t},\n\t};\n}\n\n/**\n * Discover tool files from a directory.\n * Only loads index.ts files from subdirectories (e.g., tools/mytool/index.ts).\n */\nfunction discoverToolsInDir(dir: string): string[] {\n\tif (!fs.existsSync(dir)) {\n\t\treturn [];\n\t}\n\n\tconst tools: string[] = [];\n\n\ttry {\n\t\tconst entries = fs.readdirSync(dir, { withFileTypes: true });\n\n\t\tfor (const entry of entries) {\n\t\t\tif (entry.isDirectory() || entry.isSymbolicLink()) {\n\t\t\t\t// Check for index.ts in subdirectory\n\t\t\t\tconst indexPath = path.join(dir, entry.name, \"index.ts\");\n\t\t\t\tif (fs.existsSync(indexPath)) {\n\t\t\t\t\ttools.push(indexPath);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} catch {\n\t\treturn [];\n\t}\n\n\treturn tools;\n}\n\n/**\n * Discover and load tools from standard locations:\n * 1. agentDir/tools/*.ts (global)\n * 2. cwd/.pi/tools/*.ts (project-local)\n *\n * Plus any explicitly configured paths from settings or CLI.\n *\n * @param configuredPaths - Explicit paths from settings.json and CLI --tool flags\n * @param cwd - Current working directory\n * @param builtInToolNames - Names of built-in tools to check for conflicts\n * @param agentDir - Agent config directory. Default: from getAgentDir()\n */\nexport async function discoverAndLoadCustomTools(\n\tconfiguredPaths: string[],\n\tcwd: string,\n\tbuiltInToolNames: string[],\n\tagentDir: string = getAgentDir(),\n): Promise<CustomToolsLoadResult> {\n\tconst allPaths: string[] = [];\n\tconst seen = new Set<string>();\n\n\t// Helper to add paths without duplicates\n\tconst addPaths = (paths: string[]) => {\n\t\tfor (const p of paths) {\n\t\t\tconst resolved = path.resolve(p);\n\t\t\tif (!seen.has(resolved)) {\n\t\t\t\tseen.add(resolved);\n\t\t\t\tallPaths.push(p);\n\t\t\t}\n\t\t}\n\t};\n\n\t// 1. Global tools: agentDir/tools/\n\tconst globalToolsDir = path.join(agentDir, \"tools\");\n\taddPaths(discoverToolsInDir(globalToolsDir));\n\n\t// 2. Project-local tools: cwd/.pi/tools/\n\tconst localToolsDir = path.join(cwd, \".pi\", \"tools\");\n\taddPaths(discoverToolsInDir(localToolsDir));\n\n\t// 3. Explicitly configured paths (can override/add)\n\taddPaths(configuredPaths.map((p) => resolveToolPath(p, cwd)));\n\n\treturn loadCustomTools(allPaths, cwd, builtInToolNames);\n}\n"]}
@@ -77,6 +77,7 @@ function createNoOpUIContext() {
77
77
  input: async () => undefined,
78
78
  notify: () => { },
79
79
  setStatus: () => { },
80
+ setWidget: () => { },
80
81
  custom: async () => undefined,
81
82
  setEditorText: () => { },
82
83
  getEditorText: () => "",
@@ -1 +1 @@
1
- {"version":3,"file":"loader.js","sourceRoot":"","sources":["../../../src/core/custom-tools/loader.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Custom tool loader - loads TypeScript tool modules using jiti.\n *\n * For Bun compiled binaries, custom tools that import from @mariozechner/* packages\n * are not supported because Bun's plugin system doesn't intercept imports from\n * external files loaded at runtime. Users should use the npm-installed version\n * for custom tools that depend on pi packages.\n */\n\nimport * as fs from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { createJiti } from \"jiti\";\nimport { getAgentDir, isBunBinary } from \"../../config.js\";\nimport { theme } from \"../../modes/interactive/theme/theme.js\";\nimport type { ExecOptions } from \"../exec.js\";\nimport { execCommand } from \"../exec.js\";\nimport type { HookUIContext } from \"../hooks/types.js\";\nimport type { CustomToolAPI, CustomToolFactory, CustomToolsLoadResult, LoadedCustomTool } from \"./types.js\";\n\n// Create require function to resolve module paths at runtime\nconst require = createRequire(import.meta.url);\n\n// Lazily computed aliases - resolved at runtime to handle global installs\nlet _aliases: Record<string, string> | null = null;\nfunction getAliases(): Record<string, string> {\n\tif (_aliases) return _aliases;\n\n\tconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\tconst packageIndex = path.resolve(__dirname, \"../..\", \"index.js\");\n\n\t// For typebox, we need the package root directory (not the entry file)\n\t// because jiti's alias is prefix-based: imports like \"@sinclair/typebox/compiler\"\n\t// get the alias prepended. If we alias to the entry file (.../build/cjs/index.js),\n\t// then \"@sinclair/typebox/compiler\" becomes \".../build/cjs/index.js/compiler\" (invalid).\n\t// By aliasing to the package root, it becomes \".../typebox/compiler\" which resolves correctly.\n\tconst typeboxEntry = require.resolve(\"@sinclair/typebox\");\n\tconst typeboxRoot = typeboxEntry.replace(/\\/build\\/cjs\\/index\\.js$/, \"\");\n\n\t_aliases = {\n\t\t\"@mariozechner/pi-coding-agent\": packageIndex,\n\t\t\"@mariozechner/pi-tui\": require.resolve(\"@mariozechner/pi-tui\"),\n\t\t\"@mariozechner/pi-ai\": require.resolve(\"@mariozechner/pi-ai\"),\n\t\t\"@sinclair/typebox\": typeboxRoot,\n\t};\n\treturn _aliases;\n}\n\nconst UNICODE_SPACES = /[\\u00A0\\u2000-\\u200A\\u202F\\u205F\\u3000]/g;\n\nfunction normalizeUnicodeSpaces(str: string): string {\n\treturn str.replace(UNICODE_SPACES, \" \");\n}\n\nfunction expandPath(p: string): string {\n\tconst normalized = normalizeUnicodeSpaces(p);\n\tif (normalized.startsWith(\"~/\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(2));\n\t}\n\tif (normalized.startsWith(\"~\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(1));\n\t}\n\treturn normalized;\n}\n\n/**\n * Resolve tool path.\n * - Absolute paths used as-is\n * - Paths starting with ~ expanded to home directory\n * - Relative paths resolved from cwd\n */\nfunction resolveToolPath(toolPath: string, cwd: string): string {\n\tconst expanded = expandPath(toolPath);\n\n\tif (path.isAbsolute(expanded)) {\n\t\treturn expanded;\n\t}\n\n\t// Relative paths resolved from cwd\n\treturn path.resolve(cwd, expanded);\n}\n\n/**\n * Create a no-op UI context for headless modes.\n */\nfunction createNoOpUIContext(): HookUIContext {\n\treturn {\n\t\tselect: async () => undefined,\n\t\tconfirm: async () => false,\n\t\tinput: async () => undefined,\n\t\tnotify: () => {},\n\t\tsetStatus: () => {},\n\t\tcustom: async () => undefined as never,\n\t\tsetEditorText: () => {},\n\t\tgetEditorText: () => \"\",\n\t\teditor: async () => undefined,\n\t\tget theme() {\n\t\t\treturn theme;\n\t\t},\n\t};\n}\n\n/**\n * Load a tool in Bun binary mode.\n *\n * Since Bun plugins don't work for dynamically loaded external files,\n * custom tools that import from @mariozechner/* packages won't work.\n * Tools that only use standard npm packages (installed in the tool's directory)\n * may still work.\n */\nasync function loadToolWithBun(\n\tresolvedPath: string,\n\tsharedApi: CustomToolAPI,\n): Promise<{ tools: LoadedCustomTool[] | null; error: string | null }> {\n\ttry {\n\t\t// Try to import directly - will work for tools without @mariozechner/* imports\n\t\tconst module = await import(resolvedPath);\n\t\tconst factory = (module.default ?? module) as CustomToolFactory;\n\n\t\tif (typeof factory !== \"function\") {\n\t\t\treturn { tools: null, error: \"Tool must export a default function\" };\n\t\t}\n\n\t\tconst toolResult = await factory(sharedApi);\n\t\tconst toolsArray = Array.isArray(toolResult) ? toolResult : [toolResult];\n\n\t\tconst loadedTools: LoadedCustomTool[] = toolsArray.map((tool) => ({\n\t\t\tpath: resolvedPath,\n\t\t\tresolvedPath,\n\t\t\ttool,\n\t\t}));\n\n\t\treturn { tools: loadedTools, error: null };\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\n\t\t// Check if it's a module resolution error for our packages\n\t\tif (message.includes(\"Cannot find module\") && message.includes(\"@mariozechner/\")) {\n\t\t\treturn {\n\t\t\t\ttools: null,\n\t\t\t\terror:\n\t\t\t\t\t`${message}\\n` +\n\t\t\t\t\t\"Note: Custom tools importing from @mariozechner/* packages are not supported in the standalone binary.\\n\" +\n\t\t\t\t\t\"Please install pi via npm: npm install -g @mariozechner/pi-coding-agent\",\n\t\t\t};\n\t\t}\n\n\t\treturn { tools: null, error: `Failed to load tool: ${message}` };\n\t}\n}\n\n/**\n * Load a single tool module using jiti (or Bun.build for compiled binaries).\n */\nasync function loadTool(\n\ttoolPath: string,\n\tcwd: string,\n\tsharedApi: CustomToolAPI,\n): Promise<{ tools: LoadedCustomTool[] | null; error: string | null }> {\n\tconst resolvedPath = resolveToolPath(toolPath, cwd);\n\n\t// Use Bun.build for compiled binaries since jiti can't resolve bundled modules\n\tif (isBunBinary) {\n\t\treturn loadToolWithBun(resolvedPath, sharedApi);\n\t}\n\n\ttry {\n\t\t// Create jiti instance for TypeScript/ESM loading\n\t\t// Use aliases to resolve package imports since tools are loaded from user directories\n\t\t// (e.g. ~/.pi/agent/tools) but import from packages installed with pi-coding-agent\n\t\tconst jiti = createJiti(import.meta.url, {\n\t\t\talias: getAliases(),\n\t\t});\n\n\t\t// Import the module\n\t\tconst module = await jiti.import(resolvedPath, { default: true });\n\t\tconst factory = module as CustomToolFactory;\n\n\t\tif (typeof factory !== \"function\") {\n\t\t\treturn { tools: null, error: \"Tool must export a default function\" };\n\t\t}\n\n\t\t// Call factory with shared API\n\t\tconst result = await factory(sharedApi);\n\n\t\t// Handle single tool or array of tools\n\t\tconst toolsArray = Array.isArray(result) ? result : [result];\n\n\t\tconst loadedTools: LoadedCustomTool[] = toolsArray.map((tool) => ({\n\t\t\tpath: toolPath,\n\t\t\tresolvedPath,\n\t\t\ttool,\n\t\t}));\n\n\t\treturn { tools: loadedTools, error: null };\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\treturn { tools: null, error: `Failed to load tool: ${message}` };\n\t}\n}\n\n/**\n * Load all tools from configuration.\n * @param paths - Array of tool file paths\n * @param cwd - Current working directory for resolving relative paths\n * @param builtInToolNames - Names of built-in tools to check for conflicts\n */\nexport async function loadCustomTools(\n\tpaths: string[],\n\tcwd: string,\n\tbuiltInToolNames: string[],\n): Promise<CustomToolsLoadResult> {\n\tconst tools: LoadedCustomTool[] = [];\n\tconst errors: Array<{ path: string; error: string }> = [];\n\tconst seenNames = new Set<string>(builtInToolNames);\n\n\t// Shared API object - all tools get the same instance\n\tconst sharedApi: CustomToolAPI = {\n\t\tcwd,\n\t\texec: (command: string, args: string[], options?: ExecOptions) =>\n\t\t\texecCommand(command, args, options?.cwd ?? cwd, options),\n\t\tui: createNoOpUIContext(),\n\t\thasUI: false,\n\t};\n\n\tfor (const toolPath of paths) {\n\t\tconst { tools: loadedTools, error } = await loadTool(toolPath, cwd, sharedApi);\n\n\t\tif (error) {\n\t\t\terrors.push({ path: toolPath, error });\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (loadedTools) {\n\t\t\tfor (const loadedTool of loadedTools) {\n\t\t\t\t// Check for name conflicts\n\t\t\t\tif (seenNames.has(loadedTool.tool.name)) {\n\t\t\t\t\terrors.push({\n\t\t\t\t\t\tpath: toolPath,\n\t\t\t\t\t\terror: `Tool name \"${loadedTool.tool.name}\" conflicts with existing tool`,\n\t\t\t\t\t});\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tseenNames.add(loadedTool.tool.name);\n\t\t\t\ttools.push(loadedTool);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn {\n\t\ttools,\n\t\terrors,\n\t\tsetUIContext(uiContext, hasUI) {\n\t\t\tsharedApi.ui = uiContext;\n\t\t\tsharedApi.hasUI = hasUI;\n\t\t},\n\t};\n}\n\n/**\n * Discover tool files from a directory.\n * Only loads index.ts files from subdirectories (e.g., tools/mytool/index.ts).\n */\nfunction discoverToolsInDir(dir: string): string[] {\n\tif (!fs.existsSync(dir)) {\n\t\treturn [];\n\t}\n\n\tconst tools: string[] = [];\n\n\ttry {\n\t\tconst entries = fs.readdirSync(dir, { withFileTypes: true });\n\n\t\tfor (const entry of entries) {\n\t\t\tif (entry.isDirectory() || entry.isSymbolicLink()) {\n\t\t\t\t// Check for index.ts in subdirectory\n\t\t\t\tconst indexPath = path.join(dir, entry.name, \"index.ts\");\n\t\t\t\tif (fs.existsSync(indexPath)) {\n\t\t\t\t\ttools.push(indexPath);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} catch {\n\t\treturn [];\n\t}\n\n\treturn tools;\n}\n\n/**\n * Discover and load tools from standard locations:\n * 1. agentDir/tools/*.ts (global)\n * 2. cwd/.pi/tools/*.ts (project-local)\n *\n * Plus any explicitly configured paths from settings or CLI.\n *\n * @param configuredPaths - Explicit paths from settings.json and CLI --tool flags\n * @param cwd - Current working directory\n * @param builtInToolNames - Names of built-in tools to check for conflicts\n * @param agentDir - Agent config directory. Default: from getAgentDir()\n */\nexport async function discoverAndLoadCustomTools(\n\tconfiguredPaths: string[],\n\tcwd: string,\n\tbuiltInToolNames: string[],\n\tagentDir: string = getAgentDir(),\n): Promise<CustomToolsLoadResult> {\n\tconst allPaths: string[] = [];\n\tconst seen = new Set<string>();\n\n\t// Helper to add paths without duplicates\n\tconst addPaths = (paths: string[]) => {\n\t\tfor (const p of paths) {\n\t\t\tconst resolved = path.resolve(p);\n\t\t\tif (!seen.has(resolved)) {\n\t\t\t\tseen.add(resolved);\n\t\t\t\tallPaths.push(p);\n\t\t\t}\n\t\t}\n\t};\n\n\t// 1. Global tools: agentDir/tools/\n\tconst globalToolsDir = path.join(agentDir, \"tools\");\n\taddPaths(discoverToolsInDir(globalToolsDir));\n\n\t// 2. Project-local tools: cwd/.pi/tools/\n\tconst localToolsDir = path.join(cwd, \".pi\", \"tools\");\n\taddPaths(discoverToolsInDir(localToolsDir));\n\n\t// 3. Explicitly configured paths (can override/add)\n\taddPaths(configuredPaths.map((p) => resolveToolPath(p, cwd)));\n\n\treturn loadCustomTools(allPaths, cwd, builtInToolNames);\n}\n"]}
1
+ {"version":3,"file":"loader.js","sourceRoot":"","sources":["../../../src/core/custom-tools/loader.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Custom tool loader - loads TypeScript tool modules using jiti.\n *\n * For Bun compiled binaries, custom tools that import from @mariozechner/* packages\n * are not supported because Bun's plugin system doesn't intercept imports from\n * external files loaded at runtime. Users should use the npm-installed version\n * for custom tools that depend on pi packages.\n */\n\nimport * as fs from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { createJiti } from \"jiti\";\nimport { getAgentDir, isBunBinary } from \"../../config.js\";\nimport { theme } from \"../../modes/interactive/theme/theme.js\";\nimport type { ExecOptions } from \"../exec.js\";\nimport { execCommand } from \"../exec.js\";\nimport type { HookUIContext } from \"../hooks/types.js\";\nimport type { CustomToolAPI, CustomToolFactory, CustomToolsLoadResult, LoadedCustomTool } from \"./types.js\";\n\n// Create require function to resolve module paths at runtime\nconst require = createRequire(import.meta.url);\n\n// Lazily computed aliases - resolved at runtime to handle global installs\nlet _aliases: Record<string, string> | null = null;\nfunction getAliases(): Record<string, string> {\n\tif (_aliases) return _aliases;\n\n\tconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\tconst packageIndex = path.resolve(__dirname, \"../..\", \"index.js\");\n\n\t// For typebox, we need the package root directory (not the entry file)\n\t// because jiti's alias is prefix-based: imports like \"@sinclair/typebox/compiler\"\n\t// get the alias prepended. If we alias to the entry file (.../build/cjs/index.js),\n\t// then \"@sinclair/typebox/compiler\" becomes \".../build/cjs/index.js/compiler\" (invalid).\n\t// By aliasing to the package root, it becomes \".../typebox/compiler\" which resolves correctly.\n\tconst typeboxEntry = require.resolve(\"@sinclair/typebox\");\n\tconst typeboxRoot = typeboxEntry.replace(/\\/build\\/cjs\\/index\\.js$/, \"\");\n\n\t_aliases = {\n\t\t\"@mariozechner/pi-coding-agent\": packageIndex,\n\t\t\"@mariozechner/pi-tui\": require.resolve(\"@mariozechner/pi-tui\"),\n\t\t\"@mariozechner/pi-ai\": require.resolve(\"@mariozechner/pi-ai\"),\n\t\t\"@sinclair/typebox\": typeboxRoot,\n\t};\n\treturn _aliases;\n}\n\nconst UNICODE_SPACES = /[\\u00A0\\u2000-\\u200A\\u202F\\u205F\\u3000]/g;\n\nfunction normalizeUnicodeSpaces(str: string): string {\n\treturn str.replace(UNICODE_SPACES, \" \");\n}\n\nfunction expandPath(p: string): string {\n\tconst normalized = normalizeUnicodeSpaces(p);\n\tif (normalized.startsWith(\"~/\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(2));\n\t}\n\tif (normalized.startsWith(\"~\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(1));\n\t}\n\treturn normalized;\n}\n\n/**\n * Resolve tool path.\n * - Absolute paths used as-is\n * - Paths starting with ~ expanded to home directory\n * - Relative paths resolved from cwd\n */\nfunction resolveToolPath(toolPath: string, cwd: string): string {\n\tconst expanded = expandPath(toolPath);\n\n\tif (path.isAbsolute(expanded)) {\n\t\treturn expanded;\n\t}\n\n\t// Relative paths resolved from cwd\n\treturn path.resolve(cwd, expanded);\n}\n\n/**\n * Create a no-op UI context for headless modes.\n */\nfunction createNoOpUIContext(): HookUIContext {\n\treturn {\n\t\tselect: async () => undefined,\n\t\tconfirm: async () => false,\n\t\tinput: async () => undefined,\n\t\tnotify: () => {},\n\t\tsetStatus: () => {},\n\t\tsetWidget: () => {},\n\t\tcustom: async () => undefined as never,\n\t\tsetEditorText: () => {},\n\t\tgetEditorText: () => \"\",\n\t\teditor: async () => undefined,\n\t\tget theme() {\n\t\t\treturn theme;\n\t\t},\n\t};\n}\n\n/**\n * Load a tool in Bun binary mode.\n *\n * Since Bun plugins don't work for dynamically loaded external files,\n * custom tools that import from @mariozechner/* packages won't work.\n * Tools that only use standard npm packages (installed in the tool's directory)\n * may still work.\n */\nasync function loadToolWithBun(\n\tresolvedPath: string,\n\tsharedApi: CustomToolAPI,\n): Promise<{ tools: LoadedCustomTool[] | null; error: string | null }> {\n\ttry {\n\t\t// Try to import directly - will work for tools without @mariozechner/* imports\n\t\tconst module = await import(resolvedPath);\n\t\tconst factory = (module.default ?? module) as CustomToolFactory;\n\n\t\tif (typeof factory !== \"function\") {\n\t\t\treturn { tools: null, error: \"Tool must export a default function\" };\n\t\t}\n\n\t\tconst toolResult = await factory(sharedApi);\n\t\tconst toolsArray = Array.isArray(toolResult) ? toolResult : [toolResult];\n\n\t\tconst loadedTools: LoadedCustomTool[] = toolsArray.map((tool) => ({\n\t\t\tpath: resolvedPath,\n\t\t\tresolvedPath,\n\t\t\ttool,\n\t\t}));\n\n\t\treturn { tools: loadedTools, error: null };\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\n\t\t// Check if it's a module resolution error for our packages\n\t\tif (message.includes(\"Cannot find module\") && message.includes(\"@mariozechner/\")) {\n\t\t\treturn {\n\t\t\t\ttools: null,\n\t\t\t\terror:\n\t\t\t\t\t`${message}\\n` +\n\t\t\t\t\t\"Note: Custom tools importing from @mariozechner/* packages are not supported in the standalone binary.\\n\" +\n\t\t\t\t\t\"Please install pi via npm: npm install -g @mariozechner/pi-coding-agent\",\n\t\t\t};\n\t\t}\n\n\t\treturn { tools: null, error: `Failed to load tool: ${message}` };\n\t}\n}\n\n/**\n * Load a single tool module using jiti (or Bun.build for compiled binaries).\n */\nasync function loadTool(\n\ttoolPath: string,\n\tcwd: string,\n\tsharedApi: CustomToolAPI,\n): Promise<{ tools: LoadedCustomTool[] | null; error: string | null }> {\n\tconst resolvedPath = resolveToolPath(toolPath, cwd);\n\n\t// Use Bun.build for compiled binaries since jiti can't resolve bundled modules\n\tif (isBunBinary) {\n\t\treturn loadToolWithBun(resolvedPath, sharedApi);\n\t}\n\n\ttry {\n\t\t// Create jiti instance for TypeScript/ESM loading\n\t\t// Use aliases to resolve package imports since tools are loaded from user directories\n\t\t// (e.g. ~/.pi/agent/tools) but import from packages installed with pi-coding-agent\n\t\tconst jiti = createJiti(import.meta.url, {\n\t\t\talias: getAliases(),\n\t\t});\n\n\t\t// Import the module\n\t\tconst module = await jiti.import(resolvedPath, { default: true });\n\t\tconst factory = module as CustomToolFactory;\n\n\t\tif (typeof factory !== \"function\") {\n\t\t\treturn { tools: null, error: \"Tool must export a default function\" };\n\t\t}\n\n\t\t// Call factory with shared API\n\t\tconst result = await factory(sharedApi);\n\n\t\t// Handle single tool or array of tools\n\t\tconst toolsArray = Array.isArray(result) ? result : [result];\n\n\t\tconst loadedTools: LoadedCustomTool[] = toolsArray.map((tool) => ({\n\t\t\tpath: toolPath,\n\t\t\tresolvedPath,\n\t\t\ttool,\n\t\t}));\n\n\t\treturn { tools: loadedTools, error: null };\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\treturn { tools: null, error: `Failed to load tool: ${message}` };\n\t}\n}\n\n/**\n * Load all tools from configuration.\n * @param paths - Array of tool file paths\n * @param cwd - Current working directory for resolving relative paths\n * @param builtInToolNames - Names of built-in tools to check for conflicts\n */\nexport async function loadCustomTools(\n\tpaths: string[],\n\tcwd: string,\n\tbuiltInToolNames: string[],\n): Promise<CustomToolsLoadResult> {\n\tconst tools: LoadedCustomTool[] = [];\n\tconst errors: Array<{ path: string; error: string }> = [];\n\tconst seenNames = new Set<string>(builtInToolNames);\n\n\t// Shared API object - all tools get the same instance\n\tconst sharedApi: CustomToolAPI = {\n\t\tcwd,\n\t\texec: (command: string, args: string[], options?: ExecOptions) =>\n\t\t\texecCommand(command, args, options?.cwd ?? cwd, options),\n\t\tui: createNoOpUIContext(),\n\t\thasUI: false,\n\t};\n\n\tfor (const toolPath of paths) {\n\t\tconst { tools: loadedTools, error } = await loadTool(toolPath, cwd, sharedApi);\n\n\t\tif (error) {\n\t\t\terrors.push({ path: toolPath, error });\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (loadedTools) {\n\t\t\tfor (const loadedTool of loadedTools) {\n\t\t\t\t// Check for name conflicts\n\t\t\t\tif (seenNames.has(loadedTool.tool.name)) {\n\t\t\t\t\terrors.push({\n\t\t\t\t\t\tpath: toolPath,\n\t\t\t\t\t\terror: `Tool name \"${loadedTool.tool.name}\" conflicts with existing tool`,\n\t\t\t\t\t});\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tseenNames.add(loadedTool.tool.name);\n\t\t\t\ttools.push(loadedTool);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn {\n\t\ttools,\n\t\terrors,\n\t\tsetUIContext(uiContext, hasUI) {\n\t\t\tsharedApi.ui = uiContext;\n\t\t\tsharedApi.hasUI = hasUI;\n\t\t},\n\t};\n}\n\n/**\n * Discover tool files from a directory.\n * Only loads index.ts files from subdirectories (e.g., tools/mytool/index.ts).\n */\nfunction discoverToolsInDir(dir: string): string[] {\n\tif (!fs.existsSync(dir)) {\n\t\treturn [];\n\t}\n\n\tconst tools: string[] = [];\n\n\ttry {\n\t\tconst entries = fs.readdirSync(dir, { withFileTypes: true });\n\n\t\tfor (const entry of entries) {\n\t\t\tif (entry.isDirectory() || entry.isSymbolicLink()) {\n\t\t\t\t// Check for index.ts in subdirectory\n\t\t\t\tconst indexPath = path.join(dir, entry.name, \"index.ts\");\n\t\t\t\tif (fs.existsSync(indexPath)) {\n\t\t\t\t\ttools.push(indexPath);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} catch {\n\t\treturn [];\n\t}\n\n\treturn tools;\n}\n\n/**\n * Discover and load tools from standard locations:\n * 1. agentDir/tools/*.ts (global)\n * 2. cwd/.pi/tools/*.ts (project-local)\n *\n * Plus any explicitly configured paths from settings or CLI.\n *\n * @param configuredPaths - Explicit paths from settings.json and CLI --tool flags\n * @param cwd - Current working directory\n * @param builtInToolNames - Names of built-in tools to check for conflicts\n * @param agentDir - Agent config directory. Default: from getAgentDir()\n */\nexport async function discoverAndLoadCustomTools(\n\tconfiguredPaths: string[],\n\tcwd: string,\n\tbuiltInToolNames: string[],\n\tagentDir: string = getAgentDir(),\n): Promise<CustomToolsLoadResult> {\n\tconst allPaths: string[] = [];\n\tconst seen = new Set<string>();\n\n\t// Helper to add paths without duplicates\n\tconst addPaths = (paths: string[]) => {\n\t\tfor (const p of paths) {\n\t\t\tconst resolved = path.resolve(p);\n\t\t\tif (!seen.has(resolved)) {\n\t\t\t\tseen.add(resolved);\n\t\t\t\tallPaths.push(p);\n\t\t\t}\n\t\t}\n\t};\n\n\t// 1. Global tools: agentDir/tools/\n\tconst globalToolsDir = path.join(agentDir, \"tools\");\n\taddPaths(discoverToolsInDir(globalToolsDir));\n\n\t// 2. Project-local tools: cwd/.pi/tools/\n\tconst localToolsDir = path.join(cwd, \".pi\", \"tools\");\n\taddPaths(discoverToolsInDir(localToolsDir));\n\n\t// 3. Explicitly configured paths (can override/add)\n\taddPaths(configuredPaths.map((p) => resolveToolPath(p, cwd)));\n\n\treturn loadCustomTools(allPaths, cwd, builtInToolNames);\n}\n"]}
@@ -333,6 +333,10 @@
333
333
  margin-top: var(--line-height);
334
334
  }
335
335
 
336
+ .assistant-text + .tool-execution {
337
+ margin-top: var(--line-height);
338
+ }
339
+
336
340
  .tool-execution.pending { background: var(--toolPendingBg); }
337
341
  .tool-execution.success { background: var(--toolSuccessBg); }
338
342
  .tool-execution.error { background: var(--toolErrorBg); }
@@ -493,21 +497,47 @@
493
497
  margin-bottom: var(--line-height);
494
498
  }
495
499
 
500
+ .system-prompt.expandable {
501
+ cursor: pointer;
502
+ }
503
+
496
504
  .system-prompt-header {
497
505
  font-weight: bold;
498
506
  color: var(--customMessageLabel);
499
507
  }
500
508
 
501
- .system-prompt-content {
509
+ .system-prompt-preview {
502
510
  color: var(--customMessageText);
503
511
  white-space: pre-wrap;
504
512
  word-wrap: break-word;
505
513
  font-size: 11px;
506
- max-height: 200px;
507
- overflow-y: auto;
508
514
  margin-top: var(--line-height);
509
515
  }
510
516
 
517
+ .system-prompt-expand-hint {
518
+ color: var(--muted);
519
+ font-style: italic;
520
+ margin-top: 4px;
521
+ }
522
+
523
+ .system-prompt-full {
524
+ display: none;
525
+ color: var(--customMessageText);
526
+ white-space: pre-wrap;
527
+ word-wrap: break-word;
528
+ font-size: 11px;
529
+ margin-top: var(--line-height);
530
+ }
531
+
532
+ .system-prompt.expanded .system-prompt-preview,
533
+ .system-prompt.expanded .system-prompt-expand-hint {
534
+ display: none;
535
+ }
536
+
537
+ .system-prompt.expanded .system-prompt-full {
538
+ display: block;
539
+ }
540
+
511
541
  /* Tools list */
512
542
  .tools-list {
513
543
  background: var(--customMessageBg);
@@ -518,7 +548,7 @@
518
548
 
519
549
  .tools-header {
520
550
  font-weight: bold;
521
- color: var(--warning);
551
+ color: var(--customMessageLabel);
522
552
  margin-bottom: var(--line-height);
523
553
  }
524
554
 
@@ -964,10 +964,23 @@
964
964
  </div>`;
965
965
 
966
966
  if (systemPrompt) {
967
- html += `<div class="system-prompt">
968
- <div class="system-prompt-header">System Prompt</div>
969
- <div class="system-prompt-content">${escapeHtml(systemPrompt)}</div>
970
- </div>`;
967
+ const lines = systemPrompt.split('\n');
968
+ const previewLines = 10;
969
+ if (lines.length > previewLines) {
970
+ const preview = lines.slice(0, previewLines).join('\n');
971
+ const remaining = lines.length - previewLines;
972
+ html += `<div class="system-prompt expandable" onclick="this.classList.toggle('expanded')">
973
+ <div class="system-prompt-header">System Prompt</div>
974
+ <div class="system-prompt-preview">${escapeHtml(preview)}</div>
975
+ <div class="system-prompt-expand-hint">... (${remaining} more lines, click to expand)</div>
976
+ <div class="system-prompt-full">${escapeHtml(systemPrompt)}</div>
977
+ </div>`;
978
+ } else {
979
+ html += `<div class="system-prompt">
980
+ <div class="system-prompt-header">System Prompt</div>
981
+ <div class="system-prompt-full" style="display: block">${escapeHtml(systemPrompt)}</div>
982
+ </div>`;
983
+ }
971
984
  }
972
985
 
973
986
  if (tools && tools.length > 0) {
@@ -1,4 +1,4 @@
1
- export { discoverAndLoadHooks, loadHooks, type AppendEntryHandler, type BranchHandler, type LoadedHook, type LoadHooksResult, type NavigateTreeHandler, type NewSessionHandler, type SendMessageHandler, } from "./loader.js";
1
+ export { discoverAndLoadHooks, loadHooks, type AppendEntryHandler, type BranchHandler, type GetActiveToolsHandler, type GetAllToolsHandler, type HookFlag, type HookShortcut, type LoadedHook, type LoadHooksResult, type NavigateTreeHandler, type NewSessionHandler, type SendMessageHandler, type SetActiveToolsHandler, } from "./loader.js";
2
2
  export { execCommand, HookRunner, type HookErrorListener } from "./runner.js";
3
3
  export { wrapToolsWithHooks, wrapToolWithHooks } from "./tool-wrapper.js";
4
4
  export * from "./types.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/hooks/index.ts"],"names":[],"mappings":"AACA,OAAO,EACN,oBAAoB,EACpB,SAAS,EACT,KAAK,kBAAkB,EACvB,KAAK,aAAa,EAClB,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,GACvB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,KAAK,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAC9E,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAC1E,cAAc,YAAY,CAAC;AAC3B,YAAY,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC","sourcesContent":["// biome-ignore assist/source/organizeImports: biome is not smart\nexport {\n\tdiscoverAndLoadHooks,\n\tloadHooks,\n\ttype AppendEntryHandler,\n\ttype BranchHandler,\n\ttype LoadedHook,\n\ttype LoadHooksResult,\n\ttype NavigateTreeHandler,\n\ttype NewSessionHandler,\n\ttype SendMessageHandler,\n} from \"./loader.js\";\nexport { execCommand, HookRunner, type HookErrorListener } from \"./runner.js\";\nexport { wrapToolsWithHooks, wrapToolWithHooks } from \"./tool-wrapper.js\";\nexport * from \"./types.js\";\nexport type { ReadonlySessionManager } from \"../session-manager.js\";\n"]}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/hooks/index.ts"],"names":[],"mappings":"AACA,OAAO,EACN,oBAAoB,EACpB,SAAS,EACT,KAAK,kBAAkB,EACvB,KAAK,aAAa,EAClB,KAAK,qBAAqB,EAC1B,KAAK,kBAAkB,EACvB,KAAK,QAAQ,EACb,KAAK,YAAY,EACjB,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACvB,KAAK,qBAAqB,GAC1B,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,KAAK,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAC9E,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAC1E,cAAc,YAAY,CAAC;AAC3B,YAAY,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC","sourcesContent":["// biome-ignore assist/source/organizeImports: biome is not smart\nexport {\n\tdiscoverAndLoadHooks,\n\tloadHooks,\n\ttype AppendEntryHandler,\n\ttype BranchHandler,\n\ttype GetActiveToolsHandler,\n\ttype GetAllToolsHandler,\n\ttype HookFlag,\n\ttype HookShortcut,\n\ttype LoadedHook,\n\ttype LoadHooksResult,\n\ttype NavigateTreeHandler,\n\ttype NewSessionHandler,\n\ttype SendMessageHandler,\n\ttype SetActiveToolsHandler,\n} from \"./loader.js\";\nexport { execCommand, HookRunner, type HookErrorListener } from \"./runner.js\";\nexport { wrapToolsWithHooks, wrapToolWithHooks } from \"./tool-wrapper.js\";\nexport * from \"./types.js\";\nexport type { ReadonlySessionManager } from \"../session-manager.js\";\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/core/hooks/index.ts"],"names":[],"mappings":"AAAA,iEAAiE;AACjE,OAAO,EACN,oBAAoB,EACpB,SAAS,GAQT,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,WAAW,EAAE,UAAU,EAA0B,MAAM,aAAa,CAAC;AAC9E,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAC1E,cAAc,YAAY,CAAC","sourcesContent":["// biome-ignore assist/source/organizeImports: biome is not smart\nexport {\n\tdiscoverAndLoadHooks,\n\tloadHooks,\n\ttype AppendEntryHandler,\n\ttype BranchHandler,\n\ttype LoadedHook,\n\ttype LoadHooksResult,\n\ttype NavigateTreeHandler,\n\ttype NewSessionHandler,\n\ttype SendMessageHandler,\n} from \"./loader.js\";\nexport { execCommand, HookRunner, type HookErrorListener } from \"./runner.js\";\nexport { wrapToolsWithHooks, wrapToolWithHooks } from \"./tool-wrapper.js\";\nexport * from \"./types.js\";\nexport type { ReadonlySessionManager } from \"../session-manager.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/core/hooks/index.ts"],"names":[],"mappings":"AAAA,iEAAiE;AACjE,OAAO,EACN,oBAAoB,EACpB,SAAS,GAaT,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,WAAW,EAAE,UAAU,EAA0B,MAAM,aAAa,CAAC;AAC9E,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAC1E,cAAc,YAAY,CAAC","sourcesContent":["// biome-ignore assist/source/organizeImports: biome is not smart\nexport {\n\tdiscoverAndLoadHooks,\n\tloadHooks,\n\ttype AppendEntryHandler,\n\ttype BranchHandler,\n\ttype GetActiveToolsHandler,\n\ttype GetAllToolsHandler,\n\ttype HookFlag,\n\ttype HookShortcut,\n\ttype LoadedHook,\n\ttype LoadHooksResult,\n\ttype NavigateTreeHandler,\n\ttype NewSessionHandler,\n\ttype SendMessageHandler,\n\ttype SetActiveToolsHandler,\n} from \"./loader.js\";\nexport { execCommand, HookRunner, type HookErrorListener } from \"./runner.js\";\nexport { wrapToolsWithHooks, wrapToolWithHooks } from \"./tool-wrapper.js\";\nexport * from \"./types.js\";\nexport type { ReadonlySessionManager } from \"../session-manager.js\";\n"]}
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * Hook loader - loads TypeScript hook modules using jiti.
3
3
  */
4
+ import type { KeyId } from "@mariozechner/pi-tui";
4
5
  import type { HookMessage } from "../messages.js";
5
6
  import type { SessionManager } from "../session-manager.js";
6
- import type { HookMessageRenderer, RegisteredCommand } from "./types.js";
7
+ import type { HookContext, HookMessageRenderer, RegisteredCommand } from "./types.js";
7
8
  /**
8
9
  * Generic handler function type.
9
10
  */
@@ -19,6 +20,46 @@ export type SendMessageHandler = <T = unknown>(message: Pick<HookMessage<T>, "cu
19
20
  * Append entry handler type for pi.appendEntry().
20
21
  */
21
22
  export type AppendEntryHandler = <T = unknown>(customType: string, data?: T) => void;
23
+ /**
24
+ * Get active tools handler type for pi.getActiveTools().
25
+ */
26
+ export type GetActiveToolsHandler = () => string[];
27
+ /**
28
+ * Get all tools handler type for pi.getAllTools().
29
+ */
30
+ export type GetAllToolsHandler = () => string[];
31
+ /**
32
+ * Set active tools handler type for pi.setActiveTools().
33
+ */
34
+ export type SetActiveToolsHandler = (toolNames: string[]) => void;
35
+ /**
36
+ * CLI flag definition registered by a hook.
37
+ */
38
+ export interface HookFlag {
39
+ /** Flag name (without --) */
40
+ name: string;
41
+ /** Description for --help */
42
+ description?: string;
43
+ /** Type: boolean or string */
44
+ type: "boolean" | "string";
45
+ /** Default value */
46
+ default?: boolean | string;
47
+ /** Hook path that registered this flag */
48
+ hookPath: string;
49
+ }
50
+ /**
51
+ * Keyboard shortcut registered by a hook.
52
+ */
53
+ export interface HookShortcut {
54
+ /** Key identifier (e.g., Key.shift("p"), "ctrl+x") */
55
+ shortcut: KeyId;
56
+ /** Description for help */
57
+ description?: string;
58
+ /** Handler function */
59
+ handler: (ctx: HookContext) => Promise<void> | void;
60
+ /** Hook path that registered this shortcut */
61
+ hookPath: string;
62
+ }
22
63
  /**
23
64
  * New session handler type for ctx.newSession() in HookCommandContext.
24
65
  */
@@ -56,10 +97,24 @@ export interface LoadedHook {
56
97
  messageRenderers: Map<string, HookMessageRenderer>;
57
98
  /** Map of command name to registered command */
58
99
  commands: Map<string, RegisteredCommand>;
100
+ /** CLI flags registered by this hook */
101
+ flags: Map<string, HookFlag>;
102
+ /** Flag values (set after CLI parsing) */
103
+ flagValues: Map<string, boolean | string>;
104
+ /** Keyboard shortcuts registered by this hook */
105
+ shortcuts: Map<KeyId, HookShortcut>;
59
106
  /** Set the send message handler for this hook's pi.sendMessage() */
60
107
  setSendMessageHandler: (handler: SendMessageHandler) => void;
61
108
  /** Set the append entry handler for this hook's pi.appendEntry() */
62
109
  setAppendEntryHandler: (handler: AppendEntryHandler) => void;
110
+ /** Set the get active tools handler for this hook's pi.getActiveTools() */
111
+ setGetActiveToolsHandler: (handler: GetActiveToolsHandler) => void;
112
+ /** Set the get all tools handler for this hook's pi.getAllTools() */
113
+ setGetAllToolsHandler: (handler: GetAllToolsHandler) => void;
114
+ /** Set the set active tools handler for this hook's pi.setActiveTools() */
115
+ setSetActiveToolsHandler: (handler: SetActiveToolsHandler) => void;
116
+ /** Set a flag value (called after CLI parsing) */
117
+ setFlagValue: (name: string, value: boolean | string) => void;
63
118
  }
64
119
  /**
65
120
  * Result of loading hooks.
@@ -1 +1 @@
1
- {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../../src/core/hooks/loader.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Hook loader - loads TypeScript hook modules using jiti.\n */\n\nimport * as fs from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { createJiti } from \"jiti\";\nimport { getAgentDir } from \"../../config.js\";\nimport type { HookMessage } from \"../messages.js\";\nimport type { SessionManager } from \"../session-manager.js\";\nimport { execCommand } from \"./runner.js\";\nimport type { ExecOptions, HookAPI, HookFactory, HookMessageRenderer, RegisteredCommand } from \"./types.js\";\n\n// Create require function to resolve module paths at runtime\nconst require = createRequire(import.meta.url);\n\n// Lazily computed aliases - resolved at runtime to handle global installs\nlet _aliases: Record<string, string> | null = null;\nfunction getAliases(): Record<string, string> {\n\tif (_aliases) return _aliases;\n\n\tconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\tconst packageIndex = path.resolve(__dirname, \"../..\", \"index.js\");\n\n\t// For typebox, we need the package root directory (not the entry file)\n\t// because jiti's alias is prefix-based: imports like \"@sinclair/typebox/compiler\"\n\t// get the alias prepended. If we alias to the entry file (.../build/cjs/index.js),\n\t// then \"@sinclair/typebox/compiler\" becomes \".../build/cjs/index.js/compiler\" (invalid).\n\t// By aliasing to the package root, it becomes \".../typebox/compiler\" which resolves correctly.\n\tconst typeboxEntry = require.resolve(\"@sinclair/typebox\");\n\tconst typeboxRoot = typeboxEntry.replace(/\\/build\\/cjs\\/index\\.js$/, \"\");\n\n\t_aliases = {\n\t\t\"@mariozechner/pi-coding-agent\": packageIndex,\n\t\t\"@mariozechner/pi-coding-agent/hooks\": path.resolve(__dirname, \"index.js\"),\n\t\t\"@mariozechner/pi-tui\": require.resolve(\"@mariozechner/pi-tui\"),\n\t\t\"@mariozechner/pi-ai\": require.resolve(\"@mariozechner/pi-ai\"),\n\t\t\"@sinclair/typebox\": typeboxRoot,\n\t};\n\treturn _aliases;\n}\n\n/**\n * Generic handler function type.\n */\ntype HandlerFn = (...args: unknown[]) => Promise<unknown>;\n\n/**\n * Send message handler type for pi.sendMessage().\n */\nexport type SendMessageHandler = <T = unknown>(\n\tmessage: Pick<HookMessage<T>, \"customType\" | \"content\" | \"display\" | \"details\">,\n\toptions?: { triggerTurn?: boolean; deliverAs?: \"steer\" | \"followUp\" },\n) => void;\n\n/**\n * Append entry handler type for pi.appendEntry().\n */\nexport type AppendEntryHandler = <T = unknown>(customType: string, data?: T) => void;\n\n/**\n * New session handler type for ctx.newSession() in HookCommandContext.\n */\nexport type NewSessionHandler = (options?: {\n\tparentSession?: string;\n\tsetup?: (sessionManager: SessionManager) => Promise<void>;\n}) => Promise<{ cancelled: boolean }>;\n\n/**\n * Branch handler type for ctx.branch() in HookCommandContext.\n */\nexport type BranchHandler = (entryId: string) => Promise<{ cancelled: boolean }>;\n\n/**\n * Navigate tree handler type for ctx.navigateTree() in HookCommandContext.\n */\nexport type NavigateTreeHandler = (\n\ttargetId: string,\n\toptions?: { summarize?: boolean },\n) => Promise<{ cancelled: boolean }>;\n\n/**\n * Registered handlers for a loaded hook.\n */\nexport interface LoadedHook {\n\t/** Original path from config */\n\tpath: string;\n\t/** Resolved absolute path */\n\tresolvedPath: string;\n\t/** Map of event type to handler functions */\n\thandlers: Map<string, HandlerFn[]>;\n\t/** Map of customType to hook message renderer */\n\tmessageRenderers: Map<string, HookMessageRenderer>;\n\t/** Map of command name to registered command */\n\tcommands: Map<string, RegisteredCommand>;\n\t/** Set the send message handler for this hook's pi.sendMessage() */\n\tsetSendMessageHandler: (handler: SendMessageHandler) => void;\n\t/** Set the append entry handler for this hook's pi.appendEntry() */\n\tsetAppendEntryHandler: (handler: AppendEntryHandler) => void;\n}\n\n/**\n * Result of loading hooks.\n */\nexport interface LoadHooksResult {\n\t/** Successfully loaded hooks */\n\thooks: LoadedHook[];\n\t/** Errors encountered during loading */\n\terrors: Array<{ path: string; error: string }>;\n}\n\nconst UNICODE_SPACES = /[\\u00A0\\u2000-\\u200A\\u202F\\u205F\\u3000]/g;\n\nfunction normalizeUnicodeSpaces(str: string): string {\n\treturn str.replace(UNICODE_SPACES, \" \");\n}\n\nfunction expandPath(p: string): string {\n\tconst normalized = normalizeUnicodeSpaces(p);\n\tif (normalized.startsWith(\"~/\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(2));\n\t}\n\tif (normalized.startsWith(\"~\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(1));\n\t}\n\treturn normalized;\n}\n\n/**\n * Resolve hook path.\n * - Absolute paths used as-is\n * - Paths starting with ~ expanded to home directory\n * - Relative paths resolved from cwd\n */\nfunction resolveHookPath(hookPath: string, cwd: string): string {\n\tconst expanded = expandPath(hookPath);\n\n\tif (path.isAbsolute(expanded)) {\n\t\treturn expanded;\n\t}\n\n\t// Relative paths resolved from cwd\n\treturn path.resolve(cwd, expanded);\n}\n\n/**\n * Create a HookAPI instance that collects handlers, renderers, and commands.\n * Returns the API, maps, and functions to set handlers later.\n */\nfunction createHookAPI(\n\thandlers: Map<string, HandlerFn[]>,\n\tcwd: string,\n): {\n\tapi: HookAPI;\n\tmessageRenderers: Map<string, HookMessageRenderer>;\n\tcommands: Map<string, RegisteredCommand>;\n\tsetSendMessageHandler: (handler: SendMessageHandler) => void;\n\tsetAppendEntryHandler: (handler: AppendEntryHandler) => void;\n} {\n\tlet sendMessageHandler: SendMessageHandler = () => {\n\t\t// Default no-op until mode sets the handler\n\t};\n\tlet appendEntryHandler: AppendEntryHandler = () => {\n\t\t// Default no-op until mode sets the handler\n\t};\n\tconst messageRenderers = new Map<string, HookMessageRenderer>();\n\tconst commands = new Map<string, RegisteredCommand>();\n\n\t// Cast to HookAPI - the implementation is more general (string event names)\n\t// but the interface has specific overloads for type safety in hooks\n\tconst api = {\n\t\ton(event: string, handler: HandlerFn): void {\n\t\t\tconst list = handlers.get(event) ?? [];\n\t\t\tlist.push(handler);\n\t\t\thandlers.set(event, list);\n\t\t},\n\t\tsendMessage<T = unknown>(\n\t\t\tmessage: HookMessage<T>,\n\t\t\toptions?: { triggerTurn?: boolean; deliverAs?: \"steer\" | \"followUp\" },\n\t\t): void {\n\t\t\tsendMessageHandler(message, options);\n\t\t},\n\t\tappendEntry<T = unknown>(customType: string, data?: T): void {\n\t\t\tappendEntryHandler(customType, data);\n\t\t},\n\t\tregisterMessageRenderer<T = unknown>(customType: string, renderer: HookMessageRenderer<T>): void {\n\t\t\tmessageRenderers.set(customType, renderer as HookMessageRenderer);\n\t\t},\n\t\tregisterCommand(name: string, options: { description?: string; handler: RegisteredCommand[\"handler\"] }): void {\n\t\t\tcommands.set(name, { name, ...options });\n\t\t},\n\t\texec(command: string, args: string[], options?: ExecOptions) {\n\t\t\treturn execCommand(command, args, options?.cwd ?? cwd, options);\n\t\t},\n\t} as HookAPI;\n\n\treturn {\n\t\tapi,\n\t\tmessageRenderers,\n\t\tcommands,\n\t\tsetSendMessageHandler: (handler: SendMessageHandler) => {\n\t\t\tsendMessageHandler = handler;\n\t\t},\n\t\tsetAppendEntryHandler: (handler: AppendEntryHandler) => {\n\t\t\tappendEntryHandler = handler;\n\t\t},\n\t};\n}\n\n/**\n * Load a single hook module using jiti.\n */\nasync function loadHook(hookPath: string, cwd: string): Promise<{ hook: LoadedHook | null; error: string | null }> {\n\tconst resolvedPath = resolveHookPath(hookPath, cwd);\n\n\ttry {\n\t\t// Create jiti instance for TypeScript/ESM loading\n\t\t// Use aliases to resolve package imports since hooks are loaded from user directories\n\t\t// (e.g. ~/.pi/agent/hooks) but import from packages installed with pi-coding-agent\n\t\tconst jiti = createJiti(import.meta.url, {\n\t\t\talias: getAliases(),\n\t\t});\n\n\t\t// Import the module\n\t\tconst module = await jiti.import(resolvedPath, { default: true });\n\t\tconst factory = module as HookFactory;\n\n\t\tif (typeof factory !== \"function\") {\n\t\t\treturn { hook: null, error: \"Hook must export a default function\" };\n\t\t}\n\n\t\t// Create handlers map and API\n\t\tconst handlers = new Map<string, HandlerFn[]>();\n\t\tconst { api, messageRenderers, commands, setSendMessageHandler, setAppendEntryHandler } = createHookAPI(\n\t\t\thandlers,\n\t\t\tcwd,\n\t\t);\n\n\t\t// Call factory to register handlers\n\t\tfactory(api);\n\n\t\treturn {\n\t\t\thook: {\n\t\t\t\tpath: hookPath,\n\t\t\t\tresolvedPath,\n\t\t\t\thandlers,\n\t\t\t\tmessageRenderers,\n\t\t\t\tcommands,\n\t\t\t\tsetSendMessageHandler,\n\t\t\t\tsetAppendEntryHandler,\n\t\t\t},\n\t\t\terror: null,\n\t\t};\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\treturn { hook: null, error: `Failed to load hook: ${message}` };\n\t}\n}\n\n/**\n * Load all hooks from configuration.\n * @param paths - Array of hook file paths\n * @param cwd - Current working directory for resolving relative paths\n */\nexport async function loadHooks(paths: string[], cwd: string): Promise<LoadHooksResult> {\n\tconst hooks: LoadedHook[] = [];\n\tconst errors: Array<{ path: string; error: string }> = [];\n\n\tfor (const hookPath of paths) {\n\t\tconst { hook, error } = await loadHook(hookPath, cwd);\n\n\t\tif (error) {\n\t\t\terrors.push({ path: hookPath, error });\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (hook) {\n\t\t\thooks.push(hook);\n\t\t}\n\t}\n\n\treturn { hooks, errors };\n}\n\n/**\n * Discover hook files from a directory.\n * Returns all .ts files (and symlinks to .ts files) in the directory (non-recursive).\n */\nfunction discoverHooksInDir(dir: string): string[] {\n\tif (!fs.existsSync(dir)) {\n\t\treturn [];\n\t}\n\n\ttry {\n\t\tconst entries = fs.readdirSync(dir, { withFileTypes: true });\n\t\treturn entries\n\t\t\t.filter((e) => (e.isFile() || e.isSymbolicLink()) && e.name.endsWith(\".ts\"))\n\t\t\t.map((e) => path.join(dir, e.name));\n\t} catch {\n\t\treturn [];\n\t}\n}\n\n/**\n * Discover and load hooks from standard locations:\n * 1. agentDir/hooks/*.ts (global)\n * 2. cwd/.pi/hooks/*.ts (project-local)\n *\n * Plus any explicitly configured paths from settings.\n */\nexport async function discoverAndLoadHooks(\n\tconfiguredPaths: string[],\n\tcwd: string,\n\tagentDir: string = getAgentDir(),\n): Promise<LoadHooksResult> {\n\tconst allPaths: string[] = [];\n\tconst seen = new Set<string>();\n\n\t// Helper to add paths without duplicates\n\tconst addPaths = (paths: string[]) => {\n\t\tfor (const p of paths) {\n\t\t\tconst resolved = path.resolve(p);\n\t\t\tif (!seen.has(resolved)) {\n\t\t\t\tseen.add(resolved);\n\t\t\t\tallPaths.push(p);\n\t\t\t}\n\t\t}\n\t};\n\n\t// 1. Global hooks: agentDir/hooks/\n\tconst globalHooksDir = path.join(agentDir, \"hooks\");\n\taddPaths(discoverHooksInDir(globalHooksDir));\n\n\t// 2. Project-local hooks: cwd/.pi/hooks/\n\tconst localHooksDir = path.join(cwd, \".pi\", \"hooks\");\n\taddPaths(discoverHooksInDir(localHooksDir));\n\n\t// 3. Explicitly configured paths (can override/add)\n\taddPaths(configuredPaths.map((p) => resolveHookPath(p, cwd)));\n\n\treturn loadHooks(allPaths, cwd);\n}\n"]}
1
+ {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../../src/core/hooks/loader.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Hook loader - loads TypeScript hook modules using jiti.\n */\n\nimport * as fs from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type { KeyId } from \"@mariozechner/pi-tui\";\nimport { createJiti } from \"jiti\";\nimport { getAgentDir } from \"../../config.js\";\nimport type { HookMessage } from \"../messages.js\";\nimport type { SessionManager } from \"../session-manager.js\";\nimport { execCommand } from \"./runner.js\";\nimport type {\n\tExecOptions,\n\tHookAPI,\n\tHookContext,\n\tHookFactory,\n\tHookMessageRenderer,\n\tRegisteredCommand,\n} from \"./types.js\";\n\n// Create require function to resolve module paths at runtime\nconst require = createRequire(import.meta.url);\n\n// Lazily computed aliases - resolved at runtime to handle global installs\nlet _aliases: Record<string, string> | null = null;\nfunction getAliases(): Record<string, string> {\n\tif (_aliases) return _aliases;\n\n\tconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\tconst packageIndex = path.resolve(__dirname, \"../..\", \"index.js\");\n\n\t// For typebox, we need the package root directory (not the entry file)\n\t// because jiti's alias is prefix-based: imports like \"@sinclair/typebox/compiler\"\n\t// get the alias prepended. If we alias to the entry file (.../build/cjs/index.js),\n\t// then \"@sinclair/typebox/compiler\" becomes \".../build/cjs/index.js/compiler\" (invalid).\n\t// By aliasing to the package root, it becomes \".../typebox/compiler\" which resolves correctly.\n\tconst typeboxEntry = require.resolve(\"@sinclair/typebox\");\n\tconst typeboxRoot = typeboxEntry.replace(/\\/build\\/cjs\\/index\\.js$/, \"\");\n\n\t_aliases = {\n\t\t\"@mariozechner/pi-coding-agent\": packageIndex,\n\t\t\"@mariozechner/pi-coding-agent/hooks\": path.resolve(__dirname, \"index.js\"),\n\t\t\"@mariozechner/pi-tui\": require.resolve(\"@mariozechner/pi-tui\"),\n\t\t\"@mariozechner/pi-ai\": require.resolve(\"@mariozechner/pi-ai\"),\n\t\t\"@sinclair/typebox\": typeboxRoot,\n\t};\n\treturn _aliases;\n}\n\n/**\n * Generic handler function type.\n */\ntype HandlerFn = (...args: unknown[]) => Promise<unknown>;\n\n/**\n * Send message handler type for pi.sendMessage().\n */\nexport type SendMessageHandler = <T = unknown>(\n\tmessage: Pick<HookMessage<T>, \"customType\" | \"content\" | \"display\" | \"details\">,\n\toptions?: { triggerTurn?: boolean; deliverAs?: \"steer\" | \"followUp\" },\n) => void;\n\n/**\n * Append entry handler type for pi.appendEntry().\n */\nexport type AppendEntryHandler = <T = unknown>(customType: string, data?: T) => void;\n\n/**\n * Get active tools handler type for pi.getActiveTools().\n */\nexport type GetActiveToolsHandler = () => string[];\n\n/**\n * Get all tools handler type for pi.getAllTools().\n */\nexport type GetAllToolsHandler = () => string[];\n\n/**\n * Set active tools handler type for pi.setActiveTools().\n */\nexport type SetActiveToolsHandler = (toolNames: string[]) => void;\n\n/**\n * CLI flag definition registered by a hook.\n */\nexport interface HookFlag {\n\t/** Flag name (without --) */\n\tname: string;\n\t/** Description for --help */\n\tdescription?: string;\n\t/** Type: boolean or string */\n\ttype: \"boolean\" | \"string\";\n\t/** Default value */\n\tdefault?: boolean | string;\n\t/** Hook path that registered this flag */\n\thookPath: string;\n}\n\n/**\n * Keyboard shortcut registered by a hook.\n */\nexport interface HookShortcut {\n\t/** Key identifier (e.g., Key.shift(\"p\"), \"ctrl+x\") */\n\tshortcut: KeyId;\n\t/** Description for help */\n\tdescription?: string;\n\t/** Handler function */\n\thandler: (ctx: HookContext) => Promise<void> | void;\n\t/** Hook path that registered this shortcut */\n\thookPath: string;\n}\n\n/**\n * New session handler type for ctx.newSession() in HookCommandContext.\n */\nexport type NewSessionHandler = (options?: {\n\tparentSession?: string;\n\tsetup?: (sessionManager: SessionManager) => Promise<void>;\n}) => Promise<{ cancelled: boolean }>;\n\n/**\n * Branch handler type for ctx.branch() in HookCommandContext.\n */\nexport type BranchHandler = (entryId: string) => Promise<{ cancelled: boolean }>;\n\n/**\n * Navigate tree handler type for ctx.navigateTree() in HookCommandContext.\n */\nexport type NavigateTreeHandler = (\n\ttargetId: string,\n\toptions?: { summarize?: boolean },\n) => Promise<{ cancelled: boolean }>;\n\n/**\n * Registered handlers for a loaded hook.\n */\nexport interface LoadedHook {\n\t/** Original path from config */\n\tpath: string;\n\t/** Resolved absolute path */\n\tresolvedPath: string;\n\t/** Map of event type to handler functions */\n\thandlers: Map<string, HandlerFn[]>;\n\t/** Map of customType to hook message renderer */\n\tmessageRenderers: Map<string, HookMessageRenderer>;\n\t/** Map of command name to registered command */\n\tcommands: Map<string, RegisteredCommand>;\n\t/** CLI flags registered by this hook */\n\tflags: Map<string, HookFlag>;\n\t/** Flag values (set after CLI parsing) */\n\tflagValues: Map<string, boolean | string>;\n\t/** Keyboard shortcuts registered by this hook */\n\tshortcuts: Map<KeyId, HookShortcut>;\n\t/** Set the send message handler for this hook's pi.sendMessage() */\n\tsetSendMessageHandler: (handler: SendMessageHandler) => void;\n\t/** Set the append entry handler for this hook's pi.appendEntry() */\n\tsetAppendEntryHandler: (handler: AppendEntryHandler) => void;\n\t/** Set the get active tools handler for this hook's pi.getActiveTools() */\n\tsetGetActiveToolsHandler: (handler: GetActiveToolsHandler) => void;\n\t/** Set the get all tools handler for this hook's pi.getAllTools() */\n\tsetGetAllToolsHandler: (handler: GetAllToolsHandler) => void;\n\t/** Set the set active tools handler for this hook's pi.setActiveTools() */\n\tsetSetActiveToolsHandler: (handler: SetActiveToolsHandler) => void;\n\t/** Set a flag value (called after CLI parsing) */\n\tsetFlagValue: (name: string, value: boolean | string) => void;\n}\n\n/**\n * Result of loading hooks.\n */\nexport interface LoadHooksResult {\n\t/** Successfully loaded hooks */\n\thooks: LoadedHook[];\n\t/** Errors encountered during loading */\n\terrors: Array<{ path: string; error: string }>;\n}\n\nconst UNICODE_SPACES = /[\\u00A0\\u2000-\\u200A\\u202F\\u205F\\u3000]/g;\n\nfunction normalizeUnicodeSpaces(str: string): string {\n\treturn str.replace(UNICODE_SPACES, \" \");\n}\n\nfunction expandPath(p: string): string {\n\tconst normalized = normalizeUnicodeSpaces(p);\n\tif (normalized.startsWith(\"~/\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(2));\n\t}\n\tif (normalized.startsWith(\"~\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(1));\n\t}\n\treturn normalized;\n}\n\n/**\n * Resolve hook path.\n * - Absolute paths used as-is\n * - Paths starting with ~ expanded to home directory\n * - Relative paths resolved from cwd\n */\nfunction resolveHookPath(hookPath: string, cwd: string): string {\n\tconst expanded = expandPath(hookPath);\n\n\tif (path.isAbsolute(expanded)) {\n\t\treturn expanded;\n\t}\n\n\t// Relative paths resolved from cwd\n\treturn path.resolve(cwd, expanded);\n}\n\n/**\n * Create a HookAPI instance that collects handlers, renderers, and commands.\n * Returns the API, maps, and functions to set handlers later.\n */\nfunction createHookAPI(\n\thandlers: Map<string, HandlerFn[]>,\n\tcwd: string,\n\thookPath: string,\n): {\n\tapi: HookAPI;\n\tmessageRenderers: Map<string, HookMessageRenderer>;\n\tcommands: Map<string, RegisteredCommand>;\n\tflags: Map<string, HookFlag>;\n\tflagValues: Map<string, boolean | string>;\n\tshortcuts: Map<KeyId, HookShortcut>;\n\tsetSendMessageHandler: (handler: SendMessageHandler) => void;\n\tsetAppendEntryHandler: (handler: AppendEntryHandler) => void;\n\tsetGetActiveToolsHandler: (handler: GetActiveToolsHandler) => void;\n\tsetGetAllToolsHandler: (handler: GetAllToolsHandler) => void;\n\tsetSetActiveToolsHandler: (handler: SetActiveToolsHandler) => void;\n\tsetFlagValue: (name: string, value: boolean | string) => void;\n} {\n\tlet sendMessageHandler: SendMessageHandler = () => {\n\t\t// Default no-op until mode sets the handler\n\t};\n\tlet appendEntryHandler: AppendEntryHandler = () => {\n\t\t// Default no-op until mode sets the handler\n\t};\n\tlet getActiveToolsHandler: GetActiveToolsHandler = () => [];\n\tlet getAllToolsHandler: GetAllToolsHandler = () => [];\n\tlet setActiveToolsHandler: SetActiveToolsHandler = () => {\n\t\t// Default no-op until mode sets the handler\n\t};\n\tconst messageRenderers = new Map<string, HookMessageRenderer>();\n\tconst commands = new Map<string, RegisteredCommand>();\n\tconst flags = new Map<string, HookFlag>();\n\tconst flagValues = new Map<string, boolean | string>();\n\tconst shortcuts = new Map<KeyId, HookShortcut>();\n\n\t// Cast to HookAPI - the implementation is more general (string event names)\n\t// but the interface has specific overloads for type safety in hooks\n\tconst api = {\n\t\ton(event: string, handler: HandlerFn): void {\n\t\t\tconst list = handlers.get(event) ?? [];\n\t\t\tlist.push(handler);\n\t\t\thandlers.set(event, list);\n\t\t},\n\t\tsendMessage<T = unknown>(\n\t\t\tmessage: HookMessage<T>,\n\t\t\toptions?: { triggerTurn?: boolean; deliverAs?: \"steer\" | \"followUp\" },\n\t\t): void {\n\t\t\tsendMessageHandler(message, options);\n\t\t},\n\t\tappendEntry<T = unknown>(customType: string, data?: T): void {\n\t\t\tappendEntryHandler(customType, data);\n\t\t},\n\t\tregisterMessageRenderer<T = unknown>(customType: string, renderer: HookMessageRenderer<T>): void {\n\t\t\tmessageRenderers.set(customType, renderer as HookMessageRenderer);\n\t\t},\n\t\tregisterCommand(name: string, options: { description?: string; handler: RegisteredCommand[\"handler\"] }): void {\n\t\t\tcommands.set(name, { name, ...options });\n\t\t},\n\t\texec(command: string, args: string[], options?: ExecOptions) {\n\t\t\treturn execCommand(command, args, options?.cwd ?? cwd, options);\n\t\t},\n\t\tgetActiveTools(): string[] {\n\t\t\treturn getActiveToolsHandler();\n\t\t},\n\t\tgetAllTools(): string[] {\n\t\t\treturn getAllToolsHandler();\n\t\t},\n\t\tsetActiveTools(toolNames: string[]): void {\n\t\t\tsetActiveToolsHandler(toolNames);\n\t\t},\n\t\tregisterFlag(\n\t\t\tname: string,\n\t\t\toptions: { description?: string; type: \"boolean\" | \"string\"; default?: boolean | string },\n\t\t): void {\n\t\t\tflags.set(name, { name, hookPath, ...options });\n\t\t\t// Set default value if provided\n\t\t\tif (options.default !== undefined) {\n\t\t\t\tflagValues.set(name, options.default);\n\t\t\t}\n\t\t},\n\t\tgetFlag(name: string): boolean | string | undefined {\n\t\t\treturn flagValues.get(name);\n\t\t},\n\t\tregisterShortcut(\n\t\t\tshortcut: KeyId,\n\t\t\toptions: {\n\t\t\t\tdescription?: string;\n\t\t\t\thandler: (ctx: HookContext) => Promise<void> | void;\n\t\t\t},\n\t\t): void {\n\t\t\tshortcuts.set(shortcut, { shortcut, hookPath, ...options });\n\t\t},\n\t} as HookAPI;\n\n\treturn {\n\t\tapi,\n\t\tmessageRenderers,\n\t\tcommands,\n\t\tflags,\n\t\tflagValues,\n\t\tshortcuts,\n\t\tsetSendMessageHandler: (handler: SendMessageHandler) => {\n\t\t\tsendMessageHandler = handler;\n\t\t},\n\t\tsetAppendEntryHandler: (handler: AppendEntryHandler) => {\n\t\t\tappendEntryHandler = handler;\n\t\t},\n\t\tsetGetActiveToolsHandler: (handler: GetActiveToolsHandler) => {\n\t\t\tgetActiveToolsHandler = handler;\n\t\t},\n\t\tsetGetAllToolsHandler: (handler: GetAllToolsHandler) => {\n\t\t\tgetAllToolsHandler = handler;\n\t\t},\n\t\tsetSetActiveToolsHandler: (handler: SetActiveToolsHandler) => {\n\t\t\tsetActiveToolsHandler = handler;\n\t\t},\n\t\tsetFlagValue: (name: string, value: boolean | string) => {\n\t\t\tflagValues.set(name, value);\n\t\t},\n\t};\n}\n\n/**\n * Load a single hook module using jiti.\n */\nasync function loadHook(hookPath: string, cwd: string): Promise<{ hook: LoadedHook | null; error: string | null }> {\n\tconst resolvedPath = resolveHookPath(hookPath, cwd);\n\n\ttry {\n\t\t// Create jiti instance for TypeScript/ESM loading\n\t\t// Use aliases to resolve package imports since hooks are loaded from user directories\n\t\t// (e.g. ~/.pi/agent/hooks) but import from packages installed with pi-coding-agent\n\t\tconst jiti = createJiti(import.meta.url, {\n\t\t\talias: getAliases(),\n\t\t});\n\n\t\t// Import the module\n\t\tconst module = await jiti.import(resolvedPath, { default: true });\n\t\tconst factory = module as HookFactory;\n\n\t\tif (typeof factory !== \"function\") {\n\t\t\treturn { hook: null, error: \"Hook must export a default function\" };\n\t\t}\n\n\t\t// Create handlers map and API\n\t\tconst handlers = new Map<string, HandlerFn[]>();\n\t\tconst {\n\t\t\tapi,\n\t\t\tmessageRenderers,\n\t\t\tcommands,\n\t\t\tflags,\n\t\t\tflagValues,\n\t\t\tshortcuts,\n\t\t\tsetSendMessageHandler,\n\t\t\tsetAppendEntryHandler,\n\t\t\tsetGetActiveToolsHandler,\n\t\t\tsetGetAllToolsHandler,\n\t\t\tsetSetActiveToolsHandler,\n\t\t\tsetFlagValue,\n\t\t} = createHookAPI(handlers, cwd, hookPath);\n\n\t\t// Call factory to register handlers\n\t\tfactory(api);\n\n\t\treturn {\n\t\t\thook: {\n\t\t\t\tpath: hookPath,\n\t\t\t\tresolvedPath,\n\t\t\t\thandlers,\n\t\t\t\tmessageRenderers,\n\t\t\t\tcommands,\n\t\t\t\tflags,\n\t\t\t\tflagValues,\n\t\t\t\tshortcuts,\n\t\t\t\tsetSendMessageHandler,\n\t\t\t\tsetAppendEntryHandler,\n\t\t\t\tsetGetActiveToolsHandler,\n\t\t\t\tsetGetAllToolsHandler,\n\t\t\t\tsetSetActiveToolsHandler,\n\t\t\t\tsetFlagValue,\n\t\t\t},\n\t\t\terror: null,\n\t\t};\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\treturn { hook: null, error: `Failed to load hook: ${message}` };\n\t}\n}\n\n/**\n * Load all hooks from configuration.\n * @param paths - Array of hook file paths\n * @param cwd - Current working directory for resolving relative paths\n */\nexport async function loadHooks(paths: string[], cwd: string): Promise<LoadHooksResult> {\n\tconst hooks: LoadedHook[] = [];\n\tconst errors: Array<{ path: string; error: string }> = [];\n\n\tfor (const hookPath of paths) {\n\t\tconst { hook, error } = await loadHook(hookPath, cwd);\n\n\t\tif (error) {\n\t\t\terrors.push({ path: hookPath, error });\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (hook) {\n\t\t\thooks.push(hook);\n\t\t}\n\t}\n\n\treturn { hooks, errors };\n}\n\n/**\n * Discover hook files from a directory.\n * Returns all .ts files (and symlinks to .ts files) in the directory (non-recursive).\n */\nfunction discoverHooksInDir(dir: string): string[] {\n\tif (!fs.existsSync(dir)) {\n\t\treturn [];\n\t}\n\n\ttry {\n\t\tconst entries = fs.readdirSync(dir, { withFileTypes: true });\n\t\treturn entries\n\t\t\t.filter((e) => (e.isFile() || e.isSymbolicLink()) && e.name.endsWith(\".ts\"))\n\t\t\t.map((e) => path.join(dir, e.name));\n\t} catch {\n\t\treturn [];\n\t}\n}\n\n/**\n * Discover and load hooks from standard locations:\n * 1. agentDir/hooks/*.ts (global)\n * 2. cwd/.pi/hooks/*.ts (project-local)\n *\n * Plus any explicitly configured paths from settings.\n */\nexport async function discoverAndLoadHooks(\n\tconfiguredPaths: string[],\n\tcwd: string,\n\tagentDir: string = getAgentDir(),\n): Promise<LoadHooksResult> {\n\tconst allPaths: string[] = [];\n\tconst seen = new Set<string>();\n\n\t// Helper to add paths without duplicates\n\tconst addPaths = (paths: string[]) => {\n\t\tfor (const p of paths) {\n\t\t\tconst resolved = path.resolve(p);\n\t\t\tif (!seen.has(resolved)) {\n\t\t\t\tseen.add(resolved);\n\t\t\t\tallPaths.push(p);\n\t\t\t}\n\t\t}\n\t};\n\n\t// 1. Global hooks: agentDir/hooks/\n\tconst globalHooksDir = path.join(agentDir, \"hooks\");\n\taddPaths(discoverHooksInDir(globalHooksDir));\n\n\t// 2. Project-local hooks: cwd/.pi/hooks/\n\tconst localHooksDir = path.join(cwd, \".pi\", \"hooks\");\n\taddPaths(discoverHooksInDir(localHooksDir));\n\n\t// 3. Explicitly configured paths (can override/add)\n\taddPaths(configuredPaths.map((p) => resolveHookPath(p, cwd)));\n\n\treturn loadHooks(allPaths, cwd);\n}\n"]}