@mariozechner/pi-coding-agent 0.37.8 → 0.39.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 (165) hide show
  1. package/CHANGELOG.md +115 -4
  2. package/README.md +11 -0
  3. package/dist/cli/args.d.ts +2 -0
  4. package/dist/cli/args.d.ts.map +1 -1
  5. package/dist/cli/args.js +8 -0
  6. package/dist/cli/args.js.map +1 -1
  7. package/dist/core/agent-session.d.ts +23 -0
  8. package/dist/core/agent-session.d.ts.map +1 -1
  9. package/dist/core/agent-session.js +75 -35
  10. package/dist/core/agent-session.js.map +1 -1
  11. package/dist/core/bash-executor.d.ts +6 -0
  12. package/dist/core/bash-executor.d.ts.map +1 -1
  13. package/dist/core/bash-executor.js +77 -0
  14. package/dist/core/bash-executor.js.map +1 -1
  15. package/dist/core/extensions/index.d.ts +3 -3
  16. package/dist/core/extensions/index.d.ts.map +1 -1
  17. package/dist/core/extensions/index.js +1 -1
  18. package/dist/core/extensions/index.js.map +1 -1
  19. package/dist/core/extensions/loader.d.ts +8 -6
  20. package/dist/core/extensions/loader.d.ts.map +1 -1
  21. package/dist/core/extensions/loader.js +94 -211
  22. package/dist/core/extensions/loader.js.map +1 -1
  23. package/dist/core/extensions/runner.d.ts +27 -30
  24. package/dist/core/extensions/runner.d.ts.map +1 -1
  25. package/dist/core/extensions/runner.js +102 -45
  26. package/dist/core/extensions/runner.js.map +1 -1
  27. package/dist/core/extensions/types.d.ts +155 -30
  28. package/dist/core/extensions/types.d.ts.map +1 -1
  29. package/dist/core/extensions/types.js.map +1 -1
  30. package/dist/core/extensions/wrapper.d.ts +5 -3
  31. package/dist/core/extensions/wrapper.d.ts.map +1 -1
  32. package/dist/core/extensions/wrapper.js +6 -4
  33. package/dist/core/extensions/wrapper.js.map +1 -1
  34. package/dist/core/index.d.ts +2 -2
  35. package/dist/core/index.d.ts.map +1 -1
  36. package/dist/core/index.js +1 -1
  37. package/dist/core/index.js.map +1 -1
  38. package/dist/core/model-resolver.d.ts +4 -2
  39. package/dist/core/model-resolver.d.ts.map +1 -1
  40. package/dist/core/model-resolver.js +8 -9
  41. package/dist/core/model-resolver.js.map +1 -1
  42. package/dist/core/sdk.d.ts +8 -5
  43. package/dist/core/sdk.d.ts.map +1 -1
  44. package/dist/core/sdk.js +39 -87
  45. package/dist/core/sdk.js.map +1 -1
  46. package/dist/core/settings-manager.d.ts +8 -0
  47. package/dist/core/settings-manager.d.ts.map +1 -1
  48. package/dist/core/settings-manager.js +9 -1
  49. package/dist/core/settings-manager.js.map +1 -1
  50. package/dist/core/system-prompt.d.ts.map +1 -1
  51. package/dist/core/system-prompt.js +1 -5
  52. package/dist/core/system-prompt.js.map +1 -1
  53. package/dist/core/tools/bash.d.ts +25 -1
  54. package/dist/core/tools/bash.d.ts.map +1 -1
  55. package/dist/core/tools/bash.js +103 -73
  56. package/dist/core/tools/bash.js.map +1 -1
  57. package/dist/core/tools/edit.d.ts +17 -1
  58. package/dist/core/tools/edit.d.ts.map +1 -1
  59. package/dist/core/tools/edit.js +12 -5
  60. package/dist/core/tools/edit.js.map +1 -1
  61. package/dist/core/tools/find.d.ts +18 -1
  62. package/dist/core/tools/find.d.ts.map +1 -1
  63. package/dist/core/tools/find.js +68 -18
  64. package/dist/core/tools/find.js.map +1 -1
  65. package/dist/core/tools/grep.d.ts +15 -1
  66. package/dist/core/tools/grep.d.ts.map +1 -1
  67. package/dist/core/tools/grep.js +22 -10
  68. package/dist/core/tools/grep.js.map +1 -1
  69. package/dist/core/tools/index.d.ts +7 -7
  70. package/dist/core/tools/index.d.ts.map +1 -1
  71. package/dist/core/tools/index.js +1 -1
  72. package/dist/core/tools/index.js.map +1 -1
  73. package/dist/core/tools/ls.d.ts +21 -1
  74. package/dist/core/tools/ls.d.ts.map +1 -1
  75. package/dist/core/tools/ls.js +80 -72
  76. package/dist/core/tools/ls.js.map +1 -1
  77. package/dist/core/tools/read.d.ts +14 -0
  78. package/dist/core/tools/read.d.ts.map +1 -1
  79. package/dist/core/tools/read.js +12 -5
  80. package/dist/core/tools/read.js.map +1 -1
  81. package/dist/core/tools/write.d.ts +15 -1
  82. package/dist/core/tools/write.d.ts.map +1 -1
  83. package/dist/core/tools/write.js +9 -4
  84. package/dist/core/tools/write.js.map +1 -1
  85. package/dist/index.d.ts +5 -4
  86. package/dist/index.d.ts.map +1 -1
  87. package/dist/index.js +4 -2
  88. package/dist/index.js.map +1 -1
  89. package/dist/main.d.ts.map +1 -1
  90. package/dist/main.js +58 -116
  91. package/dist/main.js.map +1 -1
  92. package/dist/modes/index.d.ts +2 -2
  93. package/dist/modes/index.d.ts.map +1 -1
  94. package/dist/modes/index.js.map +1 -1
  95. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  96. package/dist/modes/interactive/components/assistant-message.js +7 -3
  97. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  98. package/dist/modes/interactive/components/countdown-timer.d.ts +14 -0
  99. package/dist/modes/interactive/components/countdown-timer.d.ts.map +1 -0
  100. package/dist/modes/interactive/components/countdown-timer.js +33 -0
  101. package/dist/modes/interactive/components/countdown-timer.js.map +1 -0
  102. package/dist/modes/interactive/components/custom-editor.d.ts +1 -1
  103. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  104. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  105. package/dist/modes/interactive/components/extension-input.d.ts +10 -2
  106. package/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
  107. package/dist/modes/interactive/components/extension-input.js +18 -14
  108. package/dist/modes/interactive/components/extension-input.js.map +1 -1
  109. package/dist/modes/interactive/components/extension-selector.d.ts +10 -2
  110. package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
  111. package/dist/modes/interactive/components/extension-selector.js +18 -22
  112. package/dist/modes/interactive/components/extension-selector.js.map +1 -1
  113. package/dist/modes/interactive/components/tool-execution.d.ts +6 -0
  114. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  115. package/dist/modes/interactive/components/tool-execution.js +50 -23
  116. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  117. package/dist/modes/interactive/interactive-mode.d.ts +44 -3
  118. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  119. package/dist/modes/interactive/interactive-mode.js +440 -139
  120. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  121. package/dist/modes/interactive/theme/theme.d.ts +7 -0
  122. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  123. package/dist/modes/interactive/theme/theme.js +34 -0
  124. package/dist/modes/interactive/theme/theme.js.map +1 -1
  125. package/dist/modes/print-mode.d.ts +14 -7
  126. package/dist/modes/print-mode.d.ts.map +1 -1
  127. package/dist/modes/print-mode.js +45 -21
  128. package/dist/modes/print-mode.js.map +1 -1
  129. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  130. package/dist/modes/rpc/rpc-mode.js +111 -101
  131. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  132. package/dist/modes/rpc/rpc-types.d.ts +3 -0
  133. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  134. package/dist/modes/rpc/rpc-types.js.map +1 -1
  135. package/dist/utils/clipboard-image.d.ts.map +1 -1
  136. package/dist/utils/clipboard-image.js +1 -1
  137. package/dist/utils/clipboard-image.js.map +1 -1
  138. package/dist/utils/clipboard.d.ts.map +1 -1
  139. package/dist/utils/clipboard.js +35 -7
  140. package/dist/utils/clipboard.js.map +1 -1
  141. package/docs/extensions.md +211 -15
  142. package/docs/sdk.md +68 -9
  143. package/docs/tui.md +81 -4
  144. package/examples/extensions/README.md +3 -0
  145. package/examples/extensions/claude-rules.ts +5 -2
  146. package/examples/extensions/handoff.ts +1 -1
  147. package/examples/extensions/interactive-shell.ts +196 -0
  148. package/examples/extensions/mac-system-theme.ts +25 -0
  149. package/examples/extensions/modal-editor.ts +85 -0
  150. package/examples/extensions/overlay-test.ts +145 -0
  151. package/examples/extensions/pirate.ts +7 -4
  152. package/examples/extensions/preset.ts +3 -3
  153. package/examples/extensions/qna.ts +1 -1
  154. package/examples/extensions/rainbow-editor.ts +95 -0
  155. package/examples/extensions/shutdown-command.ts +63 -0
  156. package/examples/extensions/snake.ts +1 -1
  157. package/examples/extensions/ssh.ts +220 -0
  158. package/examples/extensions/timed-confirm.ts +32 -25
  159. package/examples/extensions/todo.ts +1 -1
  160. package/examples/extensions/tool-override.ts +143 -0
  161. package/examples/extensions/tools.ts +1 -1
  162. package/examples/extensions/with-deps/package-lock.json +2 -2
  163. package/examples/extensions/with-deps/package.json +1 -1
  164. package/examples/sdk/04-skills.ts +4 -1
  165. package/package.json +6 -6
@@ -5,6 +5,7 @@
5
5
  * - AgentSession.executeBash() for interactive and RPC modes
6
6
  * - Direct calls from modes that need bash execution
7
7
  */
8
+ import type { BashOperations } from "./tools/bash.js";
8
9
  export interface BashExecutorOptions {
9
10
  /** Callback for streaming output chunks (already sanitized) */
10
11
  onChunk?: (chunk: string) => void;
@@ -38,4 +39,9 @@ export interface BashResult {
38
39
  * @returns Promise resolving to execution result
39
40
  */
40
41
  export declare function executeBash(command: string, options?: BashExecutorOptions): Promise<BashResult>;
42
+ /**
43
+ * Execute a bash command using custom BashOperations.
44
+ * Used for remote execution (SSH, containers, etc.).
45
+ */
46
+ export declare function executeBashWithOperations(command: string, cwd: string, operations: BashOperations, options?: BashExecutorOptions): Promise<BashResult>;
41
47
  //# sourceMappingURL=bash-executor.d.ts.map
@@ -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,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"]}
1
+ {"version":3,"file":"bash-executor.d.ts","sourceRoot":"","sources":["../../src/core/bash-executor.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AASH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAOtD,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;AAED;;;GAGG;AACH,wBAAsB,yBAAyB,CAC9C,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,cAAc,EAC1B,OAAO,CAAC,EAAE,mBAAmB,GAC3B,OAAO,CAAC,UAAU,CAAC,CAoFrB","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 type { BashOperations } from \"./tools/bash.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\n/**\n * Execute a bash command using custom BashOperations.\n * Used for remote execution (SSH, containers, etc.).\n */\nexport async function executeBashWithOperations(\n\tcommand: string,\n\tcwd: string,\n\toperations: BashOperations,\n\toptions?: BashExecutorOptions,\n): Promise<BashResult> {\n\tconst outputChunks: string[] = [];\n\tlet outputBytes = 0;\n\tconst maxOutputBytes = DEFAULT_MAX_BYTES * 2;\n\n\tlet tempFilePath: string | undefined;\n\tlet tempFileStream: WriteStream | undefined;\n\tlet totalBytes = 0;\n\n\tconst onData = (data: Buffer) => {\n\t\ttotalBytes += data.length;\n\n\t\t// Sanitize: strip ANSI, replace binary garbage, normalize newlines\n\t\tconst text = sanitizeBinaryOutput(stripAnsi(data.toString(\"utf-8\"))).replace(/\\r/g, \"\");\n\n\t\t// Start writing to temp file if exceeds threshold\n\t\tif (totalBytes > DEFAULT_MAX_BYTES && !tempFilePath) {\n\t\t\tconst id = randomBytes(8).toString(\"hex\");\n\t\t\ttempFilePath = join(tmpdir(), `pi-bash-${id}.log`);\n\t\t\ttempFileStream = createWriteStream(tempFilePath);\n\t\t\tfor (const chunk of outputChunks) {\n\t\t\t\ttempFileStream.write(chunk);\n\t\t\t}\n\t\t}\n\n\t\tif (tempFileStream) {\n\t\t\ttempFileStream.write(text);\n\t\t}\n\n\t\t// Keep rolling buffer\n\t\toutputChunks.push(text);\n\t\toutputBytes += text.length;\n\t\twhile (outputBytes > maxOutputBytes && outputChunks.length > 1) {\n\t\t\tconst removed = outputChunks.shift()!;\n\t\t\toutputBytes -= removed.length;\n\t\t}\n\n\t\t// Stream to callback\n\t\tif (options?.onChunk) {\n\t\t\toptions.onChunk(text);\n\t\t}\n\t};\n\n\ttry {\n\t\tconst result = await operations.exec(command, cwd, {\n\t\t\tonData,\n\t\t\tsignal: options?.signal,\n\t\t});\n\n\t\tif (tempFileStream) {\n\t\t\ttempFileStream.end();\n\t\t}\n\n\t\tconst fullOutput = outputChunks.join(\"\");\n\t\tconst truncationResult = truncateTail(fullOutput);\n\t\tconst cancelled = options?.signal?.aborted ?? false;\n\n\t\treturn {\n\t\t\toutput: truncationResult.truncated ? truncationResult.content : fullOutput,\n\t\t\texitCode: cancelled ? undefined : (result.exitCode ?? undefined),\n\t\t\tcancelled,\n\t\t\ttruncated: truncationResult.truncated,\n\t\t\tfullOutputPath: tempFilePath,\n\t\t};\n\t} catch (err) {\n\t\tif (tempFileStream) {\n\t\t\ttempFileStream.end();\n\t\t}\n\n\t\t// Check if it was an abort\n\t\tif (options?.signal?.aborted) {\n\t\t\tconst fullOutput = outputChunks.join(\"\");\n\t\t\tconst truncationResult = truncateTail(fullOutput);\n\t\t\treturn {\n\t\t\t\toutput: truncationResult.truncated ? truncationResult.content : fullOutput,\n\t\t\t\texitCode: undefined,\n\t\t\t\tcancelled: true,\n\t\t\t\ttruncated: truncationResult.truncated,\n\t\t\t\tfullOutputPath: tempFilePath,\n\t\t\t};\n\t\t}\n\n\t\tthrow err;\n\t}\n}\n"]}
@@ -130,4 +130,81 @@ export function executeBash(command, options) {
130
130
  });
131
131
  });
132
132
  }
133
+ /**
134
+ * Execute a bash command using custom BashOperations.
135
+ * Used for remote execution (SSH, containers, etc.).
136
+ */
137
+ export async function executeBashWithOperations(command, cwd, operations, options) {
138
+ const outputChunks = [];
139
+ let outputBytes = 0;
140
+ const maxOutputBytes = DEFAULT_MAX_BYTES * 2;
141
+ let tempFilePath;
142
+ let tempFileStream;
143
+ let totalBytes = 0;
144
+ const onData = (data) => {
145
+ totalBytes += data.length;
146
+ // Sanitize: strip ANSI, replace binary garbage, normalize newlines
147
+ const text = sanitizeBinaryOutput(stripAnsi(data.toString("utf-8"))).replace(/\r/g, "");
148
+ // Start writing to temp file if exceeds threshold
149
+ if (totalBytes > DEFAULT_MAX_BYTES && !tempFilePath) {
150
+ const id = randomBytes(8).toString("hex");
151
+ tempFilePath = join(tmpdir(), `pi-bash-${id}.log`);
152
+ tempFileStream = createWriteStream(tempFilePath);
153
+ for (const chunk of outputChunks) {
154
+ tempFileStream.write(chunk);
155
+ }
156
+ }
157
+ if (tempFileStream) {
158
+ tempFileStream.write(text);
159
+ }
160
+ // Keep rolling buffer
161
+ outputChunks.push(text);
162
+ outputBytes += text.length;
163
+ while (outputBytes > maxOutputBytes && outputChunks.length > 1) {
164
+ const removed = outputChunks.shift();
165
+ outputBytes -= removed.length;
166
+ }
167
+ // Stream to callback
168
+ if (options?.onChunk) {
169
+ options.onChunk(text);
170
+ }
171
+ };
172
+ try {
173
+ const result = await operations.exec(command, cwd, {
174
+ onData,
175
+ signal: options?.signal,
176
+ });
177
+ if (tempFileStream) {
178
+ tempFileStream.end();
179
+ }
180
+ const fullOutput = outputChunks.join("");
181
+ const truncationResult = truncateTail(fullOutput);
182
+ const cancelled = options?.signal?.aborted ?? false;
183
+ return {
184
+ output: truncationResult.truncated ? truncationResult.content : fullOutput,
185
+ exitCode: cancelled ? undefined : (result.exitCode ?? undefined),
186
+ cancelled,
187
+ truncated: truncationResult.truncated,
188
+ fullOutputPath: tempFilePath,
189
+ };
190
+ }
191
+ catch (err) {
192
+ if (tempFileStream) {
193
+ tempFileStream.end();
194
+ }
195
+ // Check if it was an abort
196
+ if (options?.signal?.aborted) {
197
+ const fullOutput = outputChunks.join("");
198
+ const truncationResult = truncateTail(fullOutput);
199
+ return {
200
+ output: truncationResult.truncated ? truncationResult.content : fullOutput,
201
+ exitCode: undefined,
202
+ cancelled: true,
203
+ truncated: truncationResult.truncated,
204
+ fullOutputPath: tempFilePath,
205
+ };
206
+ }
207
+ throw err;
208
+ }
209
+ }
133
210
  //# sourceMappingURL=bash-executor.js.map
@@ -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 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
+ {"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 type { BashOperations } from \"./tools/bash.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\n/**\n * Execute a bash command using custom BashOperations.\n * Used for remote execution (SSH, containers, etc.).\n */\nexport async function executeBashWithOperations(\n\tcommand: string,\n\tcwd: string,\n\toperations: BashOperations,\n\toptions?: BashExecutorOptions,\n): Promise<BashResult> {\n\tconst outputChunks: string[] = [];\n\tlet outputBytes = 0;\n\tconst maxOutputBytes = DEFAULT_MAX_BYTES * 2;\n\n\tlet tempFilePath: string | undefined;\n\tlet tempFileStream: WriteStream | undefined;\n\tlet totalBytes = 0;\n\n\tconst onData = (data: Buffer) => {\n\t\ttotalBytes += data.length;\n\n\t\t// Sanitize: strip ANSI, replace binary garbage, normalize newlines\n\t\tconst text = sanitizeBinaryOutput(stripAnsi(data.toString(\"utf-8\"))).replace(/\\r/g, \"\");\n\n\t\t// Start writing to temp file if exceeds threshold\n\t\tif (totalBytes > DEFAULT_MAX_BYTES && !tempFilePath) {\n\t\t\tconst id = randomBytes(8).toString(\"hex\");\n\t\t\ttempFilePath = join(tmpdir(), `pi-bash-${id}.log`);\n\t\t\ttempFileStream = createWriteStream(tempFilePath);\n\t\t\tfor (const chunk of outputChunks) {\n\t\t\t\ttempFileStream.write(chunk);\n\t\t\t}\n\t\t}\n\n\t\tif (tempFileStream) {\n\t\t\ttempFileStream.write(text);\n\t\t}\n\n\t\t// Keep rolling buffer\n\t\toutputChunks.push(text);\n\t\toutputBytes += text.length;\n\t\twhile (outputBytes > maxOutputBytes && outputChunks.length > 1) {\n\t\t\tconst removed = outputChunks.shift()!;\n\t\t\toutputBytes -= removed.length;\n\t\t}\n\n\t\t// Stream to callback\n\t\tif (options?.onChunk) {\n\t\t\toptions.onChunk(text);\n\t\t}\n\t};\n\n\ttry {\n\t\tconst result = await operations.exec(command, cwd, {\n\t\t\tonData,\n\t\t\tsignal: options?.signal,\n\t\t});\n\n\t\tif (tempFileStream) {\n\t\t\ttempFileStream.end();\n\t\t}\n\n\t\tconst fullOutput = outputChunks.join(\"\");\n\t\tconst truncationResult = truncateTail(fullOutput);\n\t\tconst cancelled = options?.signal?.aborted ?? false;\n\n\t\treturn {\n\t\t\toutput: truncationResult.truncated ? truncationResult.content : fullOutput,\n\t\t\texitCode: cancelled ? undefined : (result.exitCode ?? undefined),\n\t\t\tcancelled,\n\t\t\ttruncated: truncationResult.truncated,\n\t\t\tfullOutputPath: tempFilePath,\n\t\t};\n\t} catch (err) {\n\t\tif (tempFileStream) {\n\t\t\ttempFileStream.end();\n\t\t}\n\n\t\t// Check if it was an abort\n\t\tif (options?.signal?.aborted) {\n\t\t\tconst fullOutput = outputChunks.join(\"\");\n\t\t\tconst truncationResult = truncateTail(fullOutput);\n\t\t\treturn {\n\t\t\t\toutput: truncationResult.truncated ? truncationResult.content : fullOutput,\n\t\t\t\texitCode: undefined,\n\t\t\t\tcancelled: true,\n\t\t\t\ttruncated: truncationResult.truncated,\n\t\t\t\tfullOutputPath: tempFilePath,\n\t\t\t};\n\t\t}\n\n\t\tthrow err;\n\t}\n}\n"]}
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Extension system for lifecycle events and custom tools.
3
3
  */
4
- export { discoverAndLoadExtensions, loadExtensionFromFactory, loadExtensions } from "./loader.js";
5
- export type { BranchHandler, ExtensionErrorListener, NavigateTreeHandler, NewSessionHandler } from "./runner.js";
4
+ export { createExtensionRuntime, discoverAndLoadExtensions, loadExtensionFromFactory, loadExtensions, } from "./loader.js";
5
+ export type { BranchHandler, ExtensionErrorListener, NavigateTreeHandler, NewSessionHandler, ShutdownHandler, } from "./runner.js";
6
6
  export { ExtensionRunner } from "./runner.js";
7
- export type { AgentEndEvent, AgentStartEvent, AgentToolResult, AgentToolUpdateCallback, AppendEntryHandler, BashToolResultEvent, BeforeAgentStartEvent, BeforeAgentStartEventResult, ContextEvent, ContextEventResult, CustomToolResultEvent, EditToolResultEvent, ExecOptions, ExecResult, ExtensionAPI, ExtensionCommandContext, ExtensionContext, ExtensionError, ExtensionEvent, ExtensionFactory, ExtensionFlag, ExtensionHandler, ExtensionShortcut, ExtensionUIContext, FindToolResultEvent, GetActiveToolsHandler, GetAllToolsHandler, GetThinkingLevelHandler, GrepToolResultEvent, LoadExtensionsResult, LoadedExtension, LsToolResultEvent, MessageRenderer, MessageRenderOptions, ReadToolResultEvent, RegisteredCommand, RegisteredTool, SendMessageHandler, SendUserMessageHandler, SessionBeforeBranchEvent, SessionBeforeBranchResult, SessionBeforeCompactEvent, SessionBeforeCompactResult, SessionBeforeSwitchEvent, SessionBeforeSwitchResult, SessionBeforeTreeEvent, SessionBeforeTreeResult, SessionBranchEvent, SessionCompactEvent, SessionEvent, SessionShutdownEvent, SessionStartEvent, SessionSwitchEvent, SessionTreeEvent, SetActiveToolsHandler, SetModelHandler, SetThinkingLevelHandler, ToolCallEvent, ToolCallEventResult, ToolDefinition, ToolRenderResultOptions, ToolResultEvent, ToolResultEventResult, TreePreparation, TurnEndEvent, TurnStartEvent, WriteToolResultEvent, } from "./types.js";
7
+ export type { AgentEndEvent, AgentStartEvent, AgentToolResult, AgentToolUpdateCallback, AppAction, AppendEntryHandler, BashToolResultEvent, BeforeAgentStartEvent, BeforeAgentStartEventResult, ContextEvent, ContextEventResult, CustomToolResultEvent, EditToolResultEvent, ExecOptions, ExecResult, Extension, ExtensionActions, ExtensionAPI, ExtensionCommandContext, ExtensionCommandContextActions, ExtensionContext, ExtensionContextActions, ExtensionError, ExtensionEvent, ExtensionFactory, ExtensionFlag, ExtensionHandler, ExtensionRuntime, ExtensionShortcut, ExtensionUIContext, ExtensionUIDialogOptions, FindToolResultEvent, GetActiveToolsHandler, GetAllToolsHandler, GetThinkingLevelHandler, GrepToolResultEvent, KeybindingsManager, LoadExtensionsResult, LsToolResultEvent, MessageRenderer, MessageRenderOptions, ReadToolResultEvent, RegisteredCommand, RegisteredTool, SendMessageHandler, SendUserMessageHandler, SessionBeforeBranchEvent, SessionBeforeBranchResult, SessionBeforeCompactEvent, SessionBeforeCompactResult, SessionBeforeSwitchEvent, SessionBeforeSwitchResult, SessionBeforeTreeEvent, SessionBeforeTreeResult, SessionBranchEvent, SessionCompactEvent, SessionEvent, SessionShutdownEvent, SessionStartEvent, SessionSwitchEvent, SessionTreeEvent, SetActiveToolsHandler, SetModelHandler, SetThinkingLevelHandler, ToolCallEvent, ToolCallEventResult, ToolDefinition, ToolRenderResultOptions, ToolResultEvent, ToolResultEventResult, TreePreparation, TurnEndEvent, TurnStartEvent, UserBashEvent, UserBashEventResult, WriteToolResultEvent, } from "./types.js";
8
8
  export { isBashToolResult, isEditToolResult, isFindToolResult, isGrepToolResult, isLsToolResult, isReadToolResult, isWriteToolResult, } from "./types.js";
9
9
  export { wrapRegisteredTool, wrapRegisteredTools, wrapToolsWithExtensions, wrapToolWithExtensions, } from "./wrapper.js";
10
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/extensions/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,yBAAyB,EAAE,wBAAwB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClG,YAAY,EAAE,aAAa,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACjH,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,YAAY,EACX,aAAa,EACb,eAAe,EAEf,eAAe,EACf,uBAAuB,EACvB,kBAAkB,EAClB,mBAAmB,EACnB,qBAAqB,EACrB,2BAA2B,EAE3B,YAAY,EAEZ,kBAAkB,EAClB,qBAAqB,EACrB,mBAAmB,EACnB,WAAW,EACX,UAAU,EAEV,YAAY,EACZ,uBAAuB,EAEvB,gBAAgB,EAEhB,cAAc,EACd,cAAc,EACd,gBAAgB,EAChB,aAAa,EACb,gBAAgB,EAChB,iBAAiB,EACjB,kBAAkB,EAClB,mBAAmB,EACnB,qBAAqB,EACrB,kBAAkB,EAClB,uBAAuB,EACvB,mBAAmB,EACnB,oBAAoB,EAEpB,eAAe,EACf,iBAAiB,EAEjB,eAAe,EACf,oBAAoB,EACpB,mBAAmB,EAEnB,iBAAiB,EACjB,cAAc,EACd,kBAAkB,EAClB,sBAAsB,EACtB,wBAAwB,EACxB,yBAAyB,EACzB,yBAAyB,EACzB,0BAA0B,EAC1B,wBAAwB,EACxB,yBAAyB,EACzB,sBAAsB,EACtB,uBAAuB,EACvB,kBAAkB,EAClB,mBAAmB,EACnB,YAAY,EACZ,oBAAoB,EAEpB,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,EAChB,qBAAqB,EACrB,eAAe,EACf,uBAAuB,EAEvB,aAAa,EACb,mBAAmB,EAEnB,cAAc,EACd,uBAAuB,EACvB,eAAe,EACf,qBAAqB,EACrB,eAAe,EACf,YAAY,EACZ,cAAc,EACd,oBAAoB,GACpB,MAAM,YAAY,CAAC;AAEpB,OAAO,EACN,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,cAAc,EACd,gBAAgB,EAChB,iBAAiB,GACjB,MAAM,YAAY,CAAC;AACpB,OAAO,EACN,kBAAkB,EAClB,mBAAmB,EACnB,uBAAuB,EACvB,sBAAsB,GACtB,MAAM,cAAc,CAAC","sourcesContent":["/**\n * Extension system for lifecycle events and custom tools.\n */\n\nexport { discoverAndLoadExtensions, loadExtensionFromFactory, loadExtensions } from \"./loader.js\";\nexport type { BranchHandler, ExtensionErrorListener, NavigateTreeHandler, NewSessionHandler } from \"./runner.js\";\nexport { ExtensionRunner } from \"./runner.js\";\nexport type {\n\tAgentEndEvent,\n\tAgentStartEvent,\n\t// Re-exports\n\tAgentToolResult,\n\tAgentToolUpdateCallback,\n\tAppendEntryHandler,\n\tBashToolResultEvent,\n\tBeforeAgentStartEvent,\n\tBeforeAgentStartEventResult,\n\t// Events - Agent\n\tContextEvent,\n\t// Event Results\n\tContextEventResult,\n\tCustomToolResultEvent,\n\tEditToolResultEvent,\n\tExecOptions,\n\tExecResult,\n\t// API\n\tExtensionAPI,\n\tExtensionCommandContext,\n\t// Context\n\tExtensionContext,\n\t// Errors\n\tExtensionError,\n\tExtensionEvent,\n\tExtensionFactory,\n\tExtensionFlag,\n\tExtensionHandler,\n\tExtensionShortcut,\n\tExtensionUIContext,\n\tFindToolResultEvent,\n\tGetActiveToolsHandler,\n\tGetAllToolsHandler,\n\tGetThinkingLevelHandler,\n\tGrepToolResultEvent,\n\tLoadExtensionsResult,\n\t// Loaded Extension\n\tLoadedExtension,\n\tLsToolResultEvent,\n\t// Message Rendering\n\tMessageRenderer,\n\tMessageRenderOptions,\n\tReadToolResultEvent,\n\t// Commands\n\tRegisteredCommand,\n\tRegisteredTool,\n\tSendMessageHandler,\n\tSendUserMessageHandler,\n\tSessionBeforeBranchEvent,\n\tSessionBeforeBranchResult,\n\tSessionBeforeCompactEvent,\n\tSessionBeforeCompactResult,\n\tSessionBeforeSwitchEvent,\n\tSessionBeforeSwitchResult,\n\tSessionBeforeTreeEvent,\n\tSessionBeforeTreeResult,\n\tSessionBranchEvent,\n\tSessionCompactEvent,\n\tSessionEvent,\n\tSessionShutdownEvent,\n\t// Events - Session\n\tSessionStartEvent,\n\tSessionSwitchEvent,\n\tSessionTreeEvent,\n\tSetActiveToolsHandler,\n\tSetModelHandler,\n\tSetThinkingLevelHandler,\n\t// Events - Tool\n\tToolCallEvent,\n\tToolCallEventResult,\n\t// Tools\n\tToolDefinition,\n\tToolRenderResultOptions,\n\tToolResultEvent,\n\tToolResultEventResult,\n\tTreePreparation,\n\tTurnEndEvent,\n\tTurnStartEvent,\n\tWriteToolResultEvent,\n} from \"./types.js\";\n// Type guards\nexport {\n\tisBashToolResult,\n\tisEditToolResult,\n\tisFindToolResult,\n\tisGrepToolResult,\n\tisLsToolResult,\n\tisReadToolResult,\n\tisWriteToolResult,\n} from \"./types.js\";\nexport {\n\twrapRegisteredTool,\n\twrapRegisteredTools,\n\twrapToolsWithExtensions,\n\twrapToolWithExtensions,\n} from \"./wrapper.js\";\n"]}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/extensions/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACN,sBAAsB,EACtB,yBAAyB,EACzB,wBAAwB,EACxB,cAAc,GACd,MAAM,aAAa,CAAC;AACrB,YAAY,EACX,aAAa,EACb,sBAAsB,EACtB,mBAAmB,EACnB,iBAAiB,EACjB,eAAe,GACf,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,YAAY,EACX,aAAa,EACb,eAAe,EAEf,eAAe,EACf,uBAAuB,EAEvB,SAAS,EACT,kBAAkB,EAClB,mBAAmB,EACnB,qBAAqB,EACrB,2BAA2B,EAE3B,YAAY,EAEZ,kBAAkB,EAClB,qBAAqB,EACrB,mBAAmB,EACnB,WAAW,EACX,UAAU,EACV,SAAS,EACT,gBAAgB,EAEhB,YAAY,EACZ,uBAAuB,EACvB,8BAA8B,EAE9B,gBAAgB,EAChB,uBAAuB,EAEvB,cAAc,EACd,cAAc,EACd,gBAAgB,EAChB,aAAa,EACb,gBAAgB,EAEhB,gBAAgB,EAChB,iBAAiB,EACjB,kBAAkB,EAClB,wBAAwB,EACxB,mBAAmB,EACnB,qBAAqB,EACrB,kBAAkB,EAClB,uBAAuB,EACvB,mBAAmB,EACnB,kBAAkB,EAClB,oBAAoB,EACpB,iBAAiB,EAEjB,eAAe,EACf,oBAAoB,EACpB,mBAAmB,EAEnB,iBAAiB,EACjB,cAAc,EACd,kBAAkB,EAClB,sBAAsB,EACtB,wBAAwB,EACxB,yBAAyB,EACzB,yBAAyB,EACzB,0BAA0B,EAC1B,wBAAwB,EACxB,yBAAyB,EACzB,sBAAsB,EACtB,uBAAuB,EACvB,kBAAkB,EAClB,mBAAmB,EACnB,YAAY,EACZ,oBAAoB,EAEpB,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,EAChB,qBAAqB,EACrB,eAAe,EACf,uBAAuB,EAEvB,aAAa,EACb,mBAAmB,EAEnB,cAAc,EACd,uBAAuB,EACvB,eAAe,EACf,qBAAqB,EACrB,eAAe,EACf,YAAY,EACZ,cAAc,EAEd,aAAa,EACb,mBAAmB,EACnB,oBAAoB,GACpB,MAAM,YAAY,CAAC;AAEpB,OAAO,EACN,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,cAAc,EACd,gBAAgB,EAChB,iBAAiB,GACjB,MAAM,YAAY,CAAC;AACpB,OAAO,EACN,kBAAkB,EAClB,mBAAmB,EACnB,uBAAuB,EACvB,sBAAsB,GACtB,MAAM,cAAc,CAAC","sourcesContent":["/**\n * Extension system for lifecycle events and custom tools.\n */\n\nexport {\n\tcreateExtensionRuntime,\n\tdiscoverAndLoadExtensions,\n\tloadExtensionFromFactory,\n\tloadExtensions,\n} from \"./loader.js\";\nexport type {\n\tBranchHandler,\n\tExtensionErrorListener,\n\tNavigateTreeHandler,\n\tNewSessionHandler,\n\tShutdownHandler,\n} from \"./runner.js\";\nexport { ExtensionRunner } from \"./runner.js\";\nexport type {\n\tAgentEndEvent,\n\tAgentStartEvent,\n\t// Re-exports\n\tAgentToolResult,\n\tAgentToolUpdateCallback,\n\t// App keybindings (for custom editors)\n\tAppAction,\n\tAppendEntryHandler,\n\tBashToolResultEvent,\n\tBeforeAgentStartEvent,\n\tBeforeAgentStartEventResult,\n\t// Events - Agent\n\tContextEvent,\n\t// Event Results\n\tContextEventResult,\n\tCustomToolResultEvent,\n\tEditToolResultEvent,\n\tExecOptions,\n\tExecResult,\n\tExtension,\n\tExtensionActions,\n\t// API\n\tExtensionAPI,\n\tExtensionCommandContext,\n\tExtensionCommandContextActions,\n\t// Context\n\tExtensionContext,\n\tExtensionContextActions,\n\t// Errors\n\tExtensionError,\n\tExtensionEvent,\n\tExtensionFactory,\n\tExtensionFlag,\n\tExtensionHandler,\n\t// Runtime\n\tExtensionRuntime,\n\tExtensionShortcut,\n\tExtensionUIContext,\n\tExtensionUIDialogOptions,\n\tFindToolResultEvent,\n\tGetActiveToolsHandler,\n\tGetAllToolsHandler,\n\tGetThinkingLevelHandler,\n\tGrepToolResultEvent,\n\tKeybindingsManager,\n\tLoadExtensionsResult,\n\tLsToolResultEvent,\n\t// Message Rendering\n\tMessageRenderer,\n\tMessageRenderOptions,\n\tReadToolResultEvent,\n\t// Commands\n\tRegisteredCommand,\n\tRegisteredTool,\n\tSendMessageHandler,\n\tSendUserMessageHandler,\n\tSessionBeforeBranchEvent,\n\tSessionBeforeBranchResult,\n\tSessionBeforeCompactEvent,\n\tSessionBeforeCompactResult,\n\tSessionBeforeSwitchEvent,\n\tSessionBeforeSwitchResult,\n\tSessionBeforeTreeEvent,\n\tSessionBeforeTreeResult,\n\tSessionBranchEvent,\n\tSessionCompactEvent,\n\tSessionEvent,\n\tSessionShutdownEvent,\n\t// Events - Session\n\tSessionStartEvent,\n\tSessionSwitchEvent,\n\tSessionTreeEvent,\n\tSetActiveToolsHandler,\n\tSetModelHandler,\n\tSetThinkingLevelHandler,\n\t// Events - Tool\n\tToolCallEvent,\n\tToolCallEventResult,\n\t// Tools\n\tToolDefinition,\n\tToolRenderResultOptions,\n\tToolResultEvent,\n\tToolResultEventResult,\n\tTreePreparation,\n\tTurnEndEvent,\n\tTurnStartEvent,\n\t// Events - User Bash\n\tUserBashEvent,\n\tUserBashEventResult,\n\tWriteToolResultEvent,\n} from \"./types.js\";\n// Type guards\nexport {\n\tisBashToolResult,\n\tisEditToolResult,\n\tisFindToolResult,\n\tisGrepToolResult,\n\tisLsToolResult,\n\tisReadToolResult,\n\tisWriteToolResult,\n} from \"./types.js\";\nexport {\n\twrapRegisteredTool,\n\twrapRegisteredTools,\n\twrapToolsWithExtensions,\n\twrapToolWithExtensions,\n} from \"./wrapper.js\";\n"]}
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Extension system for lifecycle events and custom tools.
3
3
  */
4
- export { discoverAndLoadExtensions, loadExtensionFromFactory, loadExtensions } from "./loader.js";
4
+ export { createExtensionRuntime, discoverAndLoadExtensions, loadExtensionFromFactory, loadExtensions, } from "./loader.js";
5
5
  export { ExtensionRunner } from "./runner.js";
6
6
  // Type guards
7
7
  export { isBashToolResult, isEditToolResult, isFindToolResult, isGrepToolResult, isLsToolResult, isReadToolResult, isWriteToolResult, } from "./types.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/core/extensions/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,yBAAyB,EAAE,wBAAwB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElG,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAkF9C,cAAc;AACd,OAAO,EACN,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,cAAc,EACd,gBAAgB,EAChB,iBAAiB,GACjB,MAAM,YAAY,CAAC;AACpB,OAAO,EACN,kBAAkB,EAClB,mBAAmB,EACnB,uBAAuB,EACvB,sBAAsB,GACtB,MAAM,cAAc,CAAC","sourcesContent":["/**\n * Extension system for lifecycle events and custom tools.\n */\n\nexport { discoverAndLoadExtensions, loadExtensionFromFactory, loadExtensions } from \"./loader.js\";\nexport type { BranchHandler, ExtensionErrorListener, NavigateTreeHandler, NewSessionHandler } from \"./runner.js\";\nexport { ExtensionRunner } from \"./runner.js\";\nexport type {\n\tAgentEndEvent,\n\tAgentStartEvent,\n\t// Re-exports\n\tAgentToolResult,\n\tAgentToolUpdateCallback,\n\tAppendEntryHandler,\n\tBashToolResultEvent,\n\tBeforeAgentStartEvent,\n\tBeforeAgentStartEventResult,\n\t// Events - Agent\n\tContextEvent,\n\t// Event Results\n\tContextEventResult,\n\tCustomToolResultEvent,\n\tEditToolResultEvent,\n\tExecOptions,\n\tExecResult,\n\t// API\n\tExtensionAPI,\n\tExtensionCommandContext,\n\t// Context\n\tExtensionContext,\n\t// Errors\n\tExtensionError,\n\tExtensionEvent,\n\tExtensionFactory,\n\tExtensionFlag,\n\tExtensionHandler,\n\tExtensionShortcut,\n\tExtensionUIContext,\n\tFindToolResultEvent,\n\tGetActiveToolsHandler,\n\tGetAllToolsHandler,\n\tGetThinkingLevelHandler,\n\tGrepToolResultEvent,\n\tLoadExtensionsResult,\n\t// Loaded Extension\n\tLoadedExtension,\n\tLsToolResultEvent,\n\t// Message Rendering\n\tMessageRenderer,\n\tMessageRenderOptions,\n\tReadToolResultEvent,\n\t// Commands\n\tRegisteredCommand,\n\tRegisteredTool,\n\tSendMessageHandler,\n\tSendUserMessageHandler,\n\tSessionBeforeBranchEvent,\n\tSessionBeforeBranchResult,\n\tSessionBeforeCompactEvent,\n\tSessionBeforeCompactResult,\n\tSessionBeforeSwitchEvent,\n\tSessionBeforeSwitchResult,\n\tSessionBeforeTreeEvent,\n\tSessionBeforeTreeResult,\n\tSessionBranchEvent,\n\tSessionCompactEvent,\n\tSessionEvent,\n\tSessionShutdownEvent,\n\t// Events - Session\n\tSessionStartEvent,\n\tSessionSwitchEvent,\n\tSessionTreeEvent,\n\tSetActiveToolsHandler,\n\tSetModelHandler,\n\tSetThinkingLevelHandler,\n\t// Events - Tool\n\tToolCallEvent,\n\tToolCallEventResult,\n\t// Tools\n\tToolDefinition,\n\tToolRenderResultOptions,\n\tToolResultEvent,\n\tToolResultEventResult,\n\tTreePreparation,\n\tTurnEndEvent,\n\tTurnStartEvent,\n\tWriteToolResultEvent,\n} from \"./types.js\";\n// Type guards\nexport {\n\tisBashToolResult,\n\tisEditToolResult,\n\tisFindToolResult,\n\tisGrepToolResult,\n\tisLsToolResult,\n\tisReadToolResult,\n\tisWriteToolResult,\n} from \"./types.js\";\nexport {\n\twrapRegisteredTool,\n\twrapRegisteredTools,\n\twrapToolsWithExtensions,\n\twrapToolWithExtensions,\n} from \"./wrapper.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/core/extensions/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACN,sBAAsB,EACtB,yBAAyB,EACzB,wBAAwB,EACxB,cAAc,GACd,MAAM,aAAa,CAAC;AAQrB,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AA6F9C,cAAc;AACd,OAAO,EACN,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,cAAc,EACd,gBAAgB,EAChB,iBAAiB,GACjB,MAAM,YAAY,CAAC;AACpB,OAAO,EACN,kBAAkB,EAClB,mBAAmB,EACnB,uBAAuB,EACvB,sBAAsB,GACtB,MAAM,cAAc,CAAC","sourcesContent":["/**\n * Extension system for lifecycle events and custom tools.\n */\n\nexport {\n\tcreateExtensionRuntime,\n\tdiscoverAndLoadExtensions,\n\tloadExtensionFromFactory,\n\tloadExtensions,\n} from \"./loader.js\";\nexport type {\n\tBranchHandler,\n\tExtensionErrorListener,\n\tNavigateTreeHandler,\n\tNewSessionHandler,\n\tShutdownHandler,\n} from \"./runner.js\";\nexport { ExtensionRunner } from \"./runner.js\";\nexport type {\n\tAgentEndEvent,\n\tAgentStartEvent,\n\t// Re-exports\n\tAgentToolResult,\n\tAgentToolUpdateCallback,\n\t// App keybindings (for custom editors)\n\tAppAction,\n\tAppendEntryHandler,\n\tBashToolResultEvent,\n\tBeforeAgentStartEvent,\n\tBeforeAgentStartEventResult,\n\t// Events - Agent\n\tContextEvent,\n\t// Event Results\n\tContextEventResult,\n\tCustomToolResultEvent,\n\tEditToolResultEvent,\n\tExecOptions,\n\tExecResult,\n\tExtension,\n\tExtensionActions,\n\t// API\n\tExtensionAPI,\n\tExtensionCommandContext,\n\tExtensionCommandContextActions,\n\t// Context\n\tExtensionContext,\n\tExtensionContextActions,\n\t// Errors\n\tExtensionError,\n\tExtensionEvent,\n\tExtensionFactory,\n\tExtensionFlag,\n\tExtensionHandler,\n\t// Runtime\n\tExtensionRuntime,\n\tExtensionShortcut,\n\tExtensionUIContext,\n\tExtensionUIDialogOptions,\n\tFindToolResultEvent,\n\tGetActiveToolsHandler,\n\tGetAllToolsHandler,\n\tGetThinkingLevelHandler,\n\tGrepToolResultEvent,\n\tKeybindingsManager,\n\tLoadExtensionsResult,\n\tLsToolResultEvent,\n\t// Message Rendering\n\tMessageRenderer,\n\tMessageRenderOptions,\n\tReadToolResultEvent,\n\t// Commands\n\tRegisteredCommand,\n\tRegisteredTool,\n\tSendMessageHandler,\n\tSendUserMessageHandler,\n\tSessionBeforeBranchEvent,\n\tSessionBeforeBranchResult,\n\tSessionBeforeCompactEvent,\n\tSessionBeforeCompactResult,\n\tSessionBeforeSwitchEvent,\n\tSessionBeforeSwitchResult,\n\tSessionBeforeTreeEvent,\n\tSessionBeforeTreeResult,\n\tSessionBranchEvent,\n\tSessionCompactEvent,\n\tSessionEvent,\n\tSessionShutdownEvent,\n\t// Events - Session\n\tSessionStartEvent,\n\tSessionSwitchEvent,\n\tSessionTreeEvent,\n\tSetActiveToolsHandler,\n\tSetModelHandler,\n\tSetThinkingLevelHandler,\n\t// Events - Tool\n\tToolCallEvent,\n\tToolCallEventResult,\n\t// Tools\n\tToolDefinition,\n\tToolRenderResultOptions,\n\tToolResultEvent,\n\tToolResultEventResult,\n\tTreePreparation,\n\tTurnEndEvent,\n\tTurnStartEvent,\n\t// Events - User Bash\n\tUserBashEvent,\n\tUserBashEventResult,\n\tWriteToolResultEvent,\n} from \"./types.js\";\n// Type guards\nexport {\n\tisBashToolResult,\n\tisEditToolResult,\n\tisFindToolResult,\n\tisGrepToolResult,\n\tisLsToolResult,\n\tisReadToolResult,\n\tisWriteToolResult,\n} from \"./types.js\";\nexport {\n\twrapRegisteredTool,\n\twrapRegisteredTools,\n\twrapToolsWithExtensions,\n\twrapToolWithExtensions,\n} from \"./wrapper.js\";\n"]}
@@ -2,14 +2,16 @@
2
2
  * Extension loader - loads TypeScript extension modules using jiti.
3
3
  */
4
4
  import { type EventBus } from "../event-bus.js";
5
- import type { ExtensionFactory, ExtensionUIContext, LoadExtensionsResult, LoadedExtension } from "./types.js";
5
+ import type { Extension, ExtensionFactory, ExtensionRuntime, LoadExtensionsResult } from "./types.js";
6
6
  /**
7
- * Create a LoadedExtension from an inline factory function.
7
+ * Create a runtime with throwing stubs for action methods.
8
+ * Runner.initialize() replaces these with real implementations.
8
9
  */
9
- export declare function loadExtensionFromFactory(factory: ExtensionFactory, cwd: string, eventBus: EventBus, sharedUI: {
10
- ui: ExtensionUIContext;
11
- hasUI: boolean;
12
- }, name?: string): LoadedExtension;
10
+ export declare function createExtensionRuntime(): ExtensionRuntime;
11
+ /**
12
+ * Create an Extension from an inline factory function.
13
+ */
14
+ export declare function loadExtensionFromFactory(factory: ExtensionFactory, cwd: string, eventBus: EventBus, runtime: ExtensionRuntime, extensionPath?: string): Promise<Extension>;
13
15
  /**
14
16
  * Load extensions from paths.
15
17
  */
@@ -1 +1 @@
1
- {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../../src/core/extensions/loader.ts"],"names":[],"mappings":"AAAA;;GAEG;AAWH,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAGhE,OAAO,KAAK,EAGX,gBAAgB,EAGhB,kBAAkB,EAIlB,oBAAoB,EACpB,eAAe,EAUf,MAAM,YAAY,CAAC;AA8YpB;;GAEG;AACH,wBAAgB,wBAAwB,CACvC,OAAO,EAAE,gBAAgB,EACzB,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,QAAQ,EAClB,QAAQ,EAAE;IAAE,EAAE,EAAE,kBAAkB,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,EACpD,IAAI,SAAa,GACf,eAAe,CA6CjB;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,oBAAoB,CAAC,CA2BrH;AA8GD;;GAEG;AACH,wBAAsB,yBAAyB,CAC9C,eAAe,EAAE,MAAM,EAAE,EACzB,GAAG,EAAE,MAAM,EACX,QAAQ,GAAE,MAAsB,EAChC,QAAQ,CAAC,EAAE,QAAQ,GACjB,OAAO,CAAC,oBAAoB,CAAC,CAqC/B","sourcesContent":["/**\n * Extension loader - loads TypeScript extension 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, isBunBinary } from \"../../config.js\";\nimport { theme } from \"../../modes/interactive/theme/theme.js\";\nimport { createEventBus, type EventBus } from \"../event-bus.js\";\nimport type { ExecOptions } from \"../exec.js\";\nimport { execCommand } from \"../exec.js\";\nimport type {\n\tAppendEntryHandler,\n\tExtensionAPI,\n\tExtensionFactory,\n\tExtensionFlag,\n\tExtensionShortcut,\n\tExtensionUIContext,\n\tGetActiveToolsHandler,\n\tGetAllToolsHandler,\n\tGetThinkingLevelHandler,\n\tLoadExtensionsResult,\n\tLoadedExtension,\n\tMessageRenderer,\n\tRegisteredCommand,\n\tRegisteredTool,\n\tSendMessageHandler,\n\tSendUserMessageHandler,\n\tSetActiveToolsHandler,\n\tSetModelHandler,\n\tSetThinkingLevelHandler,\n\tToolDefinition,\n} from \"./types.js\";\n\nconst require = createRequire(import.meta.url);\n\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\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/extensions\": 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\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\nfunction resolvePath(extPath: string, cwd: string): string {\n\tconst expanded = expandPath(extPath);\n\tif (path.isAbsolute(expanded)) {\n\t\treturn expanded;\n\t}\n\treturn path.resolve(cwd, expanded);\n}\n\nfunction createNoOpUIContext(): ExtensionUIContext {\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\tsetFooter: () => {},\n\t\tsetHeader: () => {},\n\t\tsetTitle: () => {},\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\ntype HandlerFn = (...args: unknown[]) => Promise<unknown>;\n\nfunction createExtensionAPI(\n\thandlers: Map<string, HandlerFn[]>,\n\ttools: Map<string, RegisteredTool>,\n\tcwd: string,\n\textensionPath: string,\n\teventBus: EventBus,\n\t_sharedUI: { ui: ExtensionUIContext; hasUI: boolean },\n): {\n\tapi: ExtensionAPI;\n\tmessageRenderers: Map<string, MessageRenderer>;\n\tcommands: Map<string, RegisteredCommand>;\n\tflags: Map<string, ExtensionFlag>;\n\tflagValues: Map<string, boolean | string>;\n\tshortcuts: Map<KeyId, ExtensionShortcut>;\n\tsetSendMessageHandler: (handler: SendMessageHandler) => void;\n\tsetSendUserMessageHandler: (handler: SendUserMessageHandler) => void;\n\tsetAppendEntryHandler: (handler: AppendEntryHandler) => void;\n\tsetGetActiveToolsHandler: (handler: GetActiveToolsHandler) => void;\n\tsetGetAllToolsHandler: (handler: GetAllToolsHandler) => void;\n\tsetSetActiveToolsHandler: (handler: SetActiveToolsHandler) => void;\n\tsetSetModelHandler: (handler: SetModelHandler) => void;\n\tsetGetThinkingLevelHandler: (handler: GetThinkingLevelHandler) => void;\n\tsetSetThinkingLevelHandler: (handler: SetThinkingLevelHandler) => void;\n\tsetFlagValue: (name: string, value: boolean | string) => void;\n} {\n\tlet sendMessageHandler: SendMessageHandler = () => {};\n\tlet sendUserMessageHandler: SendUserMessageHandler = () => {};\n\tlet appendEntryHandler: AppendEntryHandler = () => {};\n\tlet getActiveToolsHandler: GetActiveToolsHandler = () => [];\n\tlet getAllToolsHandler: GetAllToolsHandler = () => [];\n\tlet setActiveToolsHandler: SetActiveToolsHandler = () => {};\n\tlet setModelHandler: SetModelHandler = async () => false;\n\tlet getThinkingLevelHandler: GetThinkingLevelHandler = () => \"off\";\n\tlet setThinkingLevelHandler: SetThinkingLevelHandler = () => {};\n\n\tconst messageRenderers = new Map<string, MessageRenderer>();\n\tconst commands = new Map<string, RegisteredCommand>();\n\tconst flags = new Map<string, ExtensionFlag>();\n\tconst flagValues = new Map<string, boolean | string>();\n\tconst shortcuts = new Map<KeyId, ExtensionShortcut>();\n\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\n\t\tregisterTool(tool: ToolDefinition): void {\n\t\t\ttools.set(tool.name, {\n\t\t\t\tdefinition: tool,\n\t\t\t\textensionPath,\n\t\t\t});\n\t\t},\n\n\t\tregisterCommand(name: string, options: { description?: string; handler: RegisteredCommand[\"handler\"] }): void {\n\t\t\tcommands.set(name, { name, ...options });\n\t\t},\n\n\t\tregisterShortcut(\n\t\t\tshortcut: KeyId,\n\t\t\toptions: {\n\t\t\t\tdescription?: string;\n\t\t\t\thandler: (ctx: import(\"./types.js\").ExtensionContext) => Promise<void> | void;\n\t\t\t},\n\t\t): void {\n\t\t\tshortcuts.set(shortcut, { shortcut, extensionPath, ...options });\n\t\t},\n\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, extensionPath, ...options });\n\t\t\tif (options.default !== undefined) {\n\t\t\t\tflagValues.set(name, options.default);\n\t\t\t}\n\t\t},\n\n\t\tgetFlag(name: string): boolean | string | undefined {\n\t\t\treturn flagValues.get(name);\n\t\t},\n\n\t\tregisterMessageRenderer<T>(customType: string, renderer: MessageRenderer<T>): void {\n\t\t\tmessageRenderers.set(customType, renderer as MessageRenderer);\n\t\t},\n\n\t\tsendMessage(message, options): void {\n\t\t\tsendMessageHandler(message, options);\n\t\t},\n\n\t\tsendUserMessage(content, options): void {\n\t\t\tsendUserMessageHandler(content, options);\n\t\t},\n\n\t\tappendEntry(customType: string, data?: unknown): void {\n\t\t\tappendEntryHandler(customType, data);\n\t\t},\n\n\t\texec(command: string, args: string[], options?: ExecOptions) {\n\t\t\treturn execCommand(command, args, options?.cwd ?? cwd, options);\n\t\t},\n\n\t\tgetActiveTools(): string[] {\n\t\t\treturn getActiveToolsHandler();\n\t\t},\n\n\t\tgetAllTools(): string[] {\n\t\t\treturn getAllToolsHandler();\n\t\t},\n\n\t\tsetActiveTools(toolNames: string[]): void {\n\t\t\tsetActiveToolsHandler(toolNames);\n\t\t},\n\n\t\tsetModel(model) {\n\t\t\treturn setModelHandler(model);\n\t\t},\n\n\t\tgetThinkingLevel() {\n\t\t\treturn getThinkingLevelHandler();\n\t\t},\n\n\t\tsetThinkingLevel(level) {\n\t\t\tsetThinkingLevelHandler(level);\n\t\t},\n\n\t\tevents: eventBus,\n\t} as ExtensionAPI;\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\tsetSendUserMessageHandler: (handler: SendUserMessageHandler) => {\n\t\t\tsendUserMessageHandler = 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\tsetSetModelHandler: (handler: SetModelHandler) => {\n\t\t\tsetModelHandler = handler;\n\t\t},\n\t\tsetGetThinkingLevelHandler: (handler: GetThinkingLevelHandler) => {\n\t\t\tgetThinkingLevelHandler = handler;\n\t\t},\n\t\tsetSetThinkingLevelHandler: (handler: SetThinkingLevelHandler) => {\n\t\t\tsetThinkingLevelHandler = 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\nasync function loadExtensionWithBun(\n\tresolvedPath: string,\n\tcwd: string,\n\textensionPath: string,\n\teventBus: EventBus,\n\tsharedUI: { ui: ExtensionUIContext; hasUI: boolean },\n): Promise<{ extension: LoadedExtension | null; error: string | null }> {\n\ttry {\n\t\tconst module = await import(resolvedPath);\n\t\tconst factory = (module.default ?? module) as ExtensionFactory;\n\n\t\tif (typeof factory !== \"function\") {\n\t\t\treturn { extension: null, error: \"Extension must export a default function\" };\n\t\t}\n\n\t\tconst handlers = new Map<string, HandlerFn[]>();\n\t\tconst tools = new Map<string, RegisteredTool>();\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\tsetSendUserMessageHandler,\n\t\t\tsetAppendEntryHandler,\n\t\t\tsetGetActiveToolsHandler,\n\t\t\tsetGetAllToolsHandler,\n\t\t\tsetSetActiveToolsHandler,\n\t\t\tsetSetModelHandler,\n\t\t\tsetGetThinkingLevelHandler,\n\t\t\tsetSetThinkingLevelHandler,\n\t\t\tsetFlagValue,\n\t\t} = createExtensionAPI(handlers, tools, cwd, extensionPath, eventBus, sharedUI);\n\n\t\tfactory(api);\n\n\t\treturn {\n\t\t\textension: {\n\t\t\t\tpath: extensionPath,\n\t\t\t\tresolvedPath,\n\t\t\t\thandlers,\n\t\t\t\ttools,\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\tsetSendUserMessageHandler,\n\t\t\t\tsetAppendEntryHandler,\n\t\t\t\tsetGetActiveToolsHandler,\n\t\t\t\tsetGetAllToolsHandler,\n\t\t\t\tsetSetActiveToolsHandler,\n\t\t\t\tsetSetModelHandler,\n\t\t\t\tsetGetThinkingLevelHandler,\n\t\t\t\tsetSetThinkingLevelHandler,\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\n\t\tif (message.includes(\"Cannot find module\") && message.includes(\"@mariozechner/\")) {\n\t\t\treturn {\n\t\t\t\textension: null,\n\t\t\t\terror:\n\t\t\t\t\t`${message}\\n` +\n\t\t\t\t\t\"Note: Extensions 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 { extension: null, error: `Failed to load extension: ${message}` };\n\t}\n}\n\nasync function loadExtension(\n\textensionPath: string,\n\tcwd: string,\n\teventBus: EventBus,\n\tsharedUI: { ui: ExtensionUIContext; hasUI: boolean },\n): Promise<{ extension: LoadedExtension | null; error: string | null }> {\n\tconst resolvedPath = resolvePath(extensionPath, cwd);\n\n\tif (isBunBinary) {\n\t\treturn loadExtensionWithBun(resolvedPath, cwd, extensionPath, eventBus, sharedUI);\n\t}\n\n\ttry {\n\t\tconst jiti = createJiti(import.meta.url, {\n\t\t\talias: getAliases(),\n\t\t});\n\n\t\tconst module = await jiti.import(resolvedPath, { default: true });\n\t\tconst factory = module as ExtensionFactory;\n\n\t\tif (typeof factory !== \"function\") {\n\t\t\treturn { extension: null, error: \"Extension must export a default function\" };\n\t\t}\n\n\t\tconst handlers = new Map<string, HandlerFn[]>();\n\t\tconst tools = new Map<string, RegisteredTool>();\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\tsetSendUserMessageHandler,\n\t\t\tsetAppendEntryHandler,\n\t\t\tsetGetActiveToolsHandler,\n\t\t\tsetGetAllToolsHandler,\n\t\t\tsetSetActiveToolsHandler,\n\t\t\tsetSetModelHandler,\n\t\t\tsetGetThinkingLevelHandler,\n\t\t\tsetSetThinkingLevelHandler,\n\t\t\tsetFlagValue,\n\t\t} = createExtensionAPI(handlers, tools, cwd, extensionPath, eventBus, sharedUI);\n\n\t\tfactory(api);\n\n\t\treturn {\n\t\t\textension: {\n\t\t\t\tpath: extensionPath,\n\t\t\t\tresolvedPath,\n\t\t\t\thandlers,\n\t\t\t\ttools,\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\tsetSendUserMessageHandler,\n\t\t\t\tsetAppendEntryHandler,\n\t\t\t\tsetGetActiveToolsHandler,\n\t\t\t\tsetGetAllToolsHandler,\n\t\t\t\tsetSetActiveToolsHandler,\n\t\t\t\tsetSetModelHandler,\n\t\t\t\tsetGetThinkingLevelHandler,\n\t\t\t\tsetSetThinkingLevelHandler,\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 { extension: null, error: `Failed to load extension: ${message}` };\n\t}\n}\n\n/**\n * Create a LoadedExtension from an inline factory function.\n */\nexport function loadExtensionFromFactory(\n\tfactory: ExtensionFactory,\n\tcwd: string,\n\teventBus: EventBus,\n\tsharedUI: { ui: ExtensionUIContext; hasUI: boolean },\n\tname = \"<inline>\",\n): LoadedExtension {\n\tconst handlers = new Map<string, HandlerFn[]>();\n\tconst tools = new Map<string, RegisteredTool>();\n\tconst {\n\t\tapi,\n\t\tmessageRenderers,\n\t\tcommands,\n\t\tflags,\n\t\tflagValues,\n\t\tshortcuts,\n\t\tsetSendMessageHandler,\n\t\tsetSendUserMessageHandler,\n\t\tsetAppendEntryHandler,\n\t\tsetGetActiveToolsHandler,\n\t\tsetGetAllToolsHandler,\n\t\tsetSetActiveToolsHandler,\n\t\tsetSetModelHandler,\n\t\tsetGetThinkingLevelHandler,\n\t\tsetSetThinkingLevelHandler,\n\t\tsetFlagValue,\n\t} = createExtensionAPI(handlers, tools, cwd, name, eventBus, sharedUI);\n\n\tfactory(api);\n\n\treturn {\n\t\tpath: name,\n\t\tresolvedPath: name,\n\t\thandlers,\n\t\ttools,\n\t\tmessageRenderers,\n\t\tcommands,\n\t\tflags,\n\t\tflagValues,\n\t\tshortcuts,\n\t\tsetSendMessageHandler,\n\t\tsetSendUserMessageHandler,\n\t\tsetAppendEntryHandler,\n\t\tsetGetActiveToolsHandler,\n\t\tsetGetAllToolsHandler,\n\t\tsetSetActiveToolsHandler,\n\t\tsetSetModelHandler,\n\t\tsetGetThinkingLevelHandler,\n\t\tsetSetThinkingLevelHandler,\n\t\tsetFlagValue,\n\t};\n}\n\n/**\n * Load extensions from paths.\n */\nexport async function loadExtensions(paths: string[], cwd: string, eventBus?: EventBus): Promise<LoadExtensionsResult> {\n\tconst extensions: LoadedExtension[] = [];\n\tconst errors: Array<{ path: string; error: string }> = [];\n\tconst resolvedEventBus = eventBus ?? createEventBus();\n\tconst sharedUI = { ui: createNoOpUIContext(), hasUI: false };\n\n\tfor (const extPath of paths) {\n\t\tconst { extension, error } = await loadExtension(extPath, cwd, resolvedEventBus, sharedUI);\n\n\t\tif (error) {\n\t\t\terrors.push({ path: extPath, error });\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (extension) {\n\t\t\textensions.push(extension);\n\t\t}\n\t}\n\n\treturn {\n\t\textensions,\n\t\terrors,\n\t\tsetUIContext(uiContext, hasUI) {\n\t\t\tsharedUI.ui = uiContext;\n\t\t\tsharedUI.hasUI = hasUI;\n\t\t},\n\t};\n}\n\ninterface PiManifest {\n\textensions?: string[];\n\tthemes?: string[];\n\tskills?: string[];\n}\n\nfunction readPiManifest(packageJsonPath: string): PiManifest | null {\n\ttry {\n\t\tconst content = fs.readFileSync(packageJsonPath, \"utf-8\");\n\t\tconst pkg = JSON.parse(content);\n\t\tif (pkg.pi && typeof pkg.pi === \"object\") {\n\t\t\treturn pkg.pi as PiManifest;\n\t\t}\n\t\treturn null;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nfunction isExtensionFile(name: string): boolean {\n\treturn name.endsWith(\".ts\") || name.endsWith(\".js\");\n}\n\n/**\n * Resolve extension entry points from a directory.\n *\n * Checks for:\n * 1. package.json with \"pi.extensions\" field -> returns declared paths\n * 2. index.ts or index.js -> returns the index file\n *\n * Returns resolved paths or null if no entry points found.\n */\nfunction resolveExtensionEntries(dir: string): string[] | null {\n\t// Check for package.json with \"pi\" field first\n\tconst packageJsonPath = path.join(dir, \"package.json\");\n\tif (fs.existsSync(packageJsonPath)) {\n\t\tconst manifest = readPiManifest(packageJsonPath);\n\t\tif (manifest?.extensions?.length) {\n\t\t\tconst entries: string[] = [];\n\t\t\tfor (const extPath of manifest.extensions) {\n\t\t\t\tconst resolvedExtPath = path.resolve(dir, extPath);\n\t\t\t\tif (fs.existsSync(resolvedExtPath)) {\n\t\t\t\t\tentries.push(resolvedExtPath);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (entries.length > 0) {\n\t\t\t\treturn entries;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check for index.ts or index.js\n\tconst indexTs = path.join(dir, \"index.ts\");\n\tconst indexJs = path.join(dir, \"index.js\");\n\tif (fs.existsSync(indexTs)) {\n\t\treturn [indexTs];\n\t}\n\tif (fs.existsSync(indexJs)) {\n\t\treturn [indexJs];\n\t}\n\n\treturn null;\n}\n\n/**\n * Discover extensions in a directory.\n *\n * Discovery rules:\n * 1. Direct files: `extensions/*.ts` or `*.js` → load\n * 2. Subdirectory with index: `extensions/* /index.ts` or `index.js` → load\n * 3. Subdirectory with package.json: `extensions/* /package.json` with \"pi\" field → load what it declares\n *\n * No recursion beyond one level. Complex packages must use package.json manifest.\n */\nfunction discoverExtensionsInDir(dir: string): string[] {\n\tif (!fs.existsSync(dir)) {\n\t\treturn [];\n\t}\n\n\tconst discovered: string[] = [];\n\n\ttry {\n\t\tconst entries = fs.readdirSync(dir, { withFileTypes: true });\n\n\t\tfor (const entry of entries) {\n\t\t\tconst entryPath = path.join(dir, entry.name);\n\n\t\t\t// 1. Direct files: *.ts or *.js\n\t\t\tif ((entry.isFile() || entry.isSymbolicLink()) && isExtensionFile(entry.name)) {\n\t\t\t\tdiscovered.push(entryPath);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// 2 & 3. Subdirectories\n\t\t\tif (entry.isDirectory() || entry.isSymbolicLink()) {\n\t\t\t\tconst entries = resolveExtensionEntries(entryPath);\n\t\t\t\tif (entries) {\n\t\t\t\t\tdiscovered.push(...entries);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} catch {\n\t\treturn [];\n\t}\n\n\treturn discovered;\n}\n\n/**\n * Discover and load extensions from standard locations.\n */\nexport async function discoverAndLoadExtensions(\n\tconfiguredPaths: string[],\n\tcwd: string,\n\tagentDir: string = getAgentDir(),\n\teventBus?: EventBus,\n): Promise<LoadExtensionsResult> {\n\tconst allPaths: string[] = [];\n\tconst seen = new Set<string>();\n\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 extensions: agentDir/extensions/\n\tconst globalExtDir = path.join(agentDir, \"extensions\");\n\taddPaths(discoverExtensionsInDir(globalExtDir));\n\n\t// 2. Project-local extensions: cwd/.pi/extensions/\n\tconst localExtDir = path.join(cwd, \".pi\", \"extensions\");\n\taddPaths(discoverExtensionsInDir(localExtDir));\n\n\t// 3. Explicitly configured paths\n\tfor (const p of configuredPaths) {\n\t\tconst resolved = resolvePath(p, cwd);\n\t\tif (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {\n\t\t\tconst entries = resolveExtensionEntries(resolved);\n\t\t\tif (entries) {\n\t\t\t\taddPaths(entries);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\n\t\taddPaths([resolved]);\n\t}\n\n\treturn loadExtensions(allPaths, cwd, eventBus);\n}\n"]}
1
+ {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../../src/core/extensions/loader.ts"],"names":[],"mappings":"AAAA;;GAEG;AAUH,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAGhE,OAAO,KAAK,EACX,SAAS,EAET,gBAAgB,EAChB,gBAAgB,EAChB,oBAAoB,EAIpB,MAAM,YAAY,CAAC;AAmDpB;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,gBAAgB,CAiBzD;AAsKD;;GAEG;AACH,wBAAsB,wBAAwB,CAC7C,OAAO,EAAE,gBAAgB,EACzB,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,gBAAgB,EACzB,aAAa,SAAa,GACxB,OAAO,CAAC,SAAS,CAAC,CAKpB;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAwBrH;AA8GD;;GAEG;AACH,wBAAsB,yBAAyB,CAC9C,eAAe,EAAE,MAAM,EAAE,EACzB,GAAG,EAAE,MAAM,EACX,QAAQ,GAAE,MAAsB,EAChC,QAAQ,CAAC,EAAE,QAAQ,GACjB,OAAO,CAAC,oBAAoB,CAAC,CAqC/B","sourcesContent":["/**\n * Extension loader - loads TypeScript extension 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, isBunBinary } from \"../../config.js\";\nimport { createEventBus, type EventBus } from \"../event-bus.js\";\nimport type { ExecOptions } from \"../exec.js\";\nimport { execCommand } from \"../exec.js\";\nimport type {\n\tExtension,\n\tExtensionAPI,\n\tExtensionFactory,\n\tExtensionRuntime,\n\tLoadExtensionsResult,\n\tMessageRenderer,\n\tRegisteredCommand,\n\tToolDefinition,\n} from \"./types.js\";\n\nconst require = createRequire(import.meta.url);\n\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\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/extensions\": 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\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\nfunction resolvePath(extPath: string, cwd: string): string {\n\tconst expanded = expandPath(extPath);\n\tif (path.isAbsolute(expanded)) {\n\t\treturn expanded;\n\t}\n\treturn path.resolve(cwd, expanded);\n}\n\ntype HandlerFn = (...args: unknown[]) => Promise<unknown>;\n\n/**\n * Create a runtime with throwing stubs for action methods.\n * Runner.initialize() replaces these with real implementations.\n */\nexport function createExtensionRuntime(): ExtensionRuntime {\n\tconst notInitialized = () => {\n\t\tthrow new Error(\"Extension runtime not initialized. Action methods cannot be called during extension loading.\");\n\t};\n\n\treturn {\n\t\tsendMessage: notInitialized,\n\t\tsendUserMessage: notInitialized,\n\t\tappendEntry: notInitialized,\n\t\tgetActiveTools: notInitialized,\n\t\tgetAllTools: notInitialized,\n\t\tsetActiveTools: notInitialized,\n\t\tsetModel: () => Promise.reject(new Error(\"Extension runtime not initialized\")),\n\t\tgetThinkingLevel: notInitialized,\n\t\tsetThinkingLevel: notInitialized,\n\t\tflagValues: new Map(),\n\t};\n}\n\n/**\n * Create the ExtensionAPI for an extension.\n * Registration methods write to the extension object.\n * Action methods delegate to the shared runtime.\n */\nfunction createExtensionAPI(\n\textension: Extension,\n\truntime: ExtensionRuntime,\n\tcwd: string,\n\teventBus: EventBus,\n): ExtensionAPI {\n\tconst api = {\n\t\t// Registration methods - write to extension\n\t\ton(event: string, handler: HandlerFn): void {\n\t\t\tconst list = extension.handlers.get(event) ?? [];\n\t\t\tlist.push(handler);\n\t\t\textension.handlers.set(event, list);\n\t\t},\n\n\t\tregisterTool(tool: ToolDefinition): void {\n\t\t\textension.tools.set(tool.name, {\n\t\t\t\tdefinition: tool,\n\t\t\t\textensionPath: extension.path,\n\t\t\t});\n\t\t},\n\n\t\tregisterCommand(name: string, options: { description?: string; handler: RegisteredCommand[\"handler\"] }): void {\n\t\t\textension.commands.set(name, { name, ...options });\n\t\t},\n\n\t\tregisterShortcut(\n\t\t\tshortcut: KeyId,\n\t\t\toptions: {\n\t\t\t\tdescription?: string;\n\t\t\t\thandler: (ctx: import(\"./types.js\").ExtensionContext) => Promise<void> | void;\n\t\t\t},\n\t\t): void {\n\t\t\textension.shortcuts.set(shortcut, { shortcut, extensionPath: extension.path, ...options });\n\t\t},\n\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\textension.flags.set(name, { name, extensionPath: extension.path, ...options });\n\t\t\tif (options.default !== undefined) {\n\t\t\t\truntime.flagValues.set(name, options.default);\n\t\t\t}\n\t\t},\n\n\t\tregisterMessageRenderer<T>(customType: string, renderer: MessageRenderer<T>): void {\n\t\t\textension.messageRenderers.set(customType, renderer as MessageRenderer);\n\t\t},\n\n\t\t// Flag access - checks extension registered it, reads from runtime\n\t\tgetFlag(name: string): boolean | string | undefined {\n\t\t\tif (!extension.flags.has(name)) return undefined;\n\t\t\treturn runtime.flagValues.get(name);\n\t\t},\n\n\t\t// Action methods - delegate to shared runtime\n\t\tsendMessage(message, options): void {\n\t\t\truntime.sendMessage(message, options);\n\t\t},\n\n\t\tsendUserMessage(content, options): void {\n\t\t\truntime.sendUserMessage(content, options);\n\t\t},\n\n\t\tappendEntry(customType: string, data?: unknown): void {\n\t\t\truntime.appendEntry(customType, data);\n\t\t},\n\n\t\texec(command: string, args: string[], options?: ExecOptions) {\n\t\t\treturn execCommand(command, args, options?.cwd ?? cwd, options);\n\t\t},\n\n\t\tgetActiveTools(): string[] {\n\t\t\treturn runtime.getActiveTools();\n\t\t},\n\n\t\tgetAllTools(): string[] {\n\t\t\treturn runtime.getAllTools();\n\t\t},\n\n\t\tsetActiveTools(toolNames: string[]): void {\n\t\t\truntime.setActiveTools(toolNames);\n\t\t},\n\n\t\tsetModel(model) {\n\t\t\treturn runtime.setModel(model);\n\t\t},\n\n\t\tgetThinkingLevel() {\n\t\t\treturn runtime.getThinkingLevel();\n\t\t},\n\n\t\tsetThinkingLevel(level) {\n\t\t\truntime.setThinkingLevel(level);\n\t\t},\n\n\t\tevents: eventBus,\n\t} as ExtensionAPI;\n\n\treturn api;\n}\n\nasync function loadBun(path: string) {\n\tconst module = await import(path);\n\tconst factory = (module.default ?? module) as ExtensionFactory;\n\treturn typeof factory !== \"function\" ? undefined : factory;\n}\n\nasync function loadJiti(path: string) {\n\tconst jiti = createJiti(import.meta.url, {\n\t\talias: getAliases(),\n\t});\n\n\tconst module = await jiti.import(path, { default: true });\n\tconst factory = module as ExtensionFactory;\n\treturn typeof factory !== \"function\" ? undefined : factory;\n}\n\n/**\n * Create an Extension object with empty collections.\n */\nfunction createExtension(extensionPath: string, resolvedPath: string): Extension {\n\treturn {\n\t\tpath: extensionPath,\n\t\tresolvedPath,\n\t\thandlers: new Map(),\n\t\ttools: new Map(),\n\t\tmessageRenderers: new Map(),\n\t\tcommands: new Map(),\n\t\tflags: new Map(),\n\t\tshortcuts: new Map(),\n\t};\n}\n\nasync function loadExtension(\n\textensionPath: string,\n\tcwd: string,\n\teventBus: EventBus,\n\truntime: ExtensionRuntime,\n): Promise<{ extension: Extension | null; error: string | null }> {\n\tconst resolvedPath = resolvePath(extensionPath, cwd);\n\n\ttry {\n\t\tconst factory = isBunBinary ? await loadBun(resolvedPath) : await loadJiti(resolvedPath);\n\t\tif (!factory) {\n\t\t\treturn { extension: null, error: `Extension does not export a valid factory function: ${extensionPath}` };\n\t\t}\n\n\t\tconst extension = createExtension(extensionPath, resolvedPath);\n\t\tconst api = createExtensionAPI(extension, runtime, cwd, eventBus);\n\t\tawait factory(api);\n\n\t\treturn { extension, error: null };\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\treturn { extension: null, error: `Failed to load extension: ${message}` };\n\t}\n}\n\n/**\n * Create an Extension from an inline factory function.\n */\nexport async function loadExtensionFromFactory(\n\tfactory: ExtensionFactory,\n\tcwd: string,\n\teventBus: EventBus,\n\truntime: ExtensionRuntime,\n\textensionPath = \"<inline>\",\n): Promise<Extension> {\n\tconst extension = createExtension(extensionPath, extensionPath);\n\tconst api = createExtensionAPI(extension, runtime, cwd, eventBus);\n\tawait factory(api);\n\treturn extension;\n}\n\n/**\n * Load extensions from paths.\n */\nexport async function loadExtensions(paths: string[], cwd: string, eventBus?: EventBus): Promise<LoadExtensionsResult> {\n\tconst extensions: Extension[] = [];\n\tconst errors: Array<{ path: string; error: string }> = [];\n\tconst resolvedEventBus = eventBus ?? createEventBus();\n\tconst runtime = createExtensionRuntime();\n\n\tfor (const extPath of paths) {\n\t\tconst { extension, error } = await loadExtension(extPath, cwd, resolvedEventBus, runtime);\n\n\t\tif (error) {\n\t\t\terrors.push({ path: extPath, error });\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (extension) {\n\t\t\textensions.push(extension);\n\t\t}\n\t}\n\n\treturn {\n\t\textensions,\n\t\terrors,\n\t\truntime,\n\t};\n}\n\ninterface PiManifest {\n\textensions?: string[];\n\tthemes?: string[];\n\tskills?: string[];\n}\n\nfunction readPiManifest(packageJsonPath: string): PiManifest | null {\n\ttry {\n\t\tconst content = fs.readFileSync(packageJsonPath, \"utf-8\");\n\t\tconst pkg = JSON.parse(content);\n\t\tif (pkg.pi && typeof pkg.pi === \"object\") {\n\t\t\treturn pkg.pi as PiManifest;\n\t\t}\n\t\treturn null;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nfunction isExtensionFile(name: string): boolean {\n\treturn name.endsWith(\".ts\") || name.endsWith(\".js\");\n}\n\n/**\n * Resolve extension entry points from a directory.\n *\n * Checks for:\n * 1. package.json with \"pi.extensions\" field -> returns declared paths\n * 2. index.ts or index.js -> returns the index file\n *\n * Returns resolved paths or null if no entry points found.\n */\nfunction resolveExtensionEntries(dir: string): string[] | null {\n\t// Check for package.json with \"pi\" field first\n\tconst packageJsonPath = path.join(dir, \"package.json\");\n\tif (fs.existsSync(packageJsonPath)) {\n\t\tconst manifest = readPiManifest(packageJsonPath);\n\t\tif (manifest?.extensions?.length) {\n\t\t\tconst entries: string[] = [];\n\t\t\tfor (const extPath of manifest.extensions) {\n\t\t\t\tconst resolvedExtPath = path.resolve(dir, extPath);\n\t\t\t\tif (fs.existsSync(resolvedExtPath)) {\n\t\t\t\t\tentries.push(resolvedExtPath);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (entries.length > 0) {\n\t\t\t\treturn entries;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check for index.ts or index.js\n\tconst indexTs = path.join(dir, \"index.ts\");\n\tconst indexJs = path.join(dir, \"index.js\");\n\tif (fs.existsSync(indexTs)) {\n\t\treturn [indexTs];\n\t}\n\tif (fs.existsSync(indexJs)) {\n\t\treturn [indexJs];\n\t}\n\n\treturn null;\n}\n\n/**\n * Discover extensions in a directory.\n *\n * Discovery rules:\n * 1. Direct files: `extensions/*.ts` or `*.js` → load\n * 2. Subdirectory with index: `extensions/* /index.ts` or `index.js` → load\n * 3. Subdirectory with package.json: `extensions/* /package.json` with \"pi\" field → load what it declares\n *\n * No recursion beyond one level. Complex packages must use package.json manifest.\n */\nfunction discoverExtensionsInDir(dir: string): string[] {\n\tif (!fs.existsSync(dir)) {\n\t\treturn [];\n\t}\n\n\tconst discovered: string[] = [];\n\n\ttry {\n\t\tconst entries = fs.readdirSync(dir, { withFileTypes: true });\n\n\t\tfor (const entry of entries) {\n\t\t\tconst entryPath = path.join(dir, entry.name);\n\n\t\t\t// 1. Direct files: *.ts or *.js\n\t\t\tif ((entry.isFile() || entry.isSymbolicLink()) && isExtensionFile(entry.name)) {\n\t\t\t\tdiscovered.push(entryPath);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// 2 & 3. Subdirectories\n\t\t\tif (entry.isDirectory() || entry.isSymbolicLink()) {\n\t\t\t\tconst entries = resolveExtensionEntries(entryPath);\n\t\t\t\tif (entries) {\n\t\t\t\t\tdiscovered.push(...entries);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} catch {\n\t\treturn [];\n\t}\n\n\treturn discovered;\n}\n\n/**\n * Discover and load extensions from standard locations.\n */\nexport async function discoverAndLoadExtensions(\n\tconfiguredPaths: string[],\n\tcwd: string,\n\tagentDir: string = getAgentDir(),\n\teventBus?: EventBus,\n): Promise<LoadExtensionsResult> {\n\tconst allPaths: string[] = [];\n\tconst seen = new Set<string>();\n\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 extensions: agentDir/extensions/\n\tconst globalExtDir = path.join(agentDir, \"extensions\");\n\taddPaths(discoverExtensionsInDir(globalExtDir));\n\n\t// 2. Project-local extensions: cwd/.pi/extensions/\n\tconst localExtDir = path.join(cwd, \".pi\", \"extensions\");\n\taddPaths(discoverExtensionsInDir(localExtDir));\n\n\t// 3. Explicitly configured paths\n\tfor (const p of configuredPaths) {\n\t\tconst resolved = resolvePath(p, cwd);\n\t\tif (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {\n\t\t\tconst entries = resolveExtensionEntries(resolved);\n\t\t\tif (entries) {\n\t\t\t\taddPaths(entries);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\n\t\taddPaths([resolved]);\n\t}\n\n\treturn loadExtensions(allPaths, cwd, eventBus);\n}\n"]}