@mariozechner/pi-coding-agent 0.23.4 → 0.23.5

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 (104) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +20 -13
  3. package/dist/core/custom-tools/loader.d.ts.map +1 -1
  4. package/dist/core/custom-tools/loader.js +56 -6
  5. package/dist/core/custom-tools/loader.js.map +1 -1
  6. package/dist/core/custom-tools/types.d.ts +9 -1
  7. package/dist/core/custom-tools/types.d.ts.map +1 -1
  8. package/dist/core/custom-tools/types.js.map +1 -1
  9. package/dist/core/hooks/index.d.ts +2 -1
  10. package/dist/core/hooks/index.d.ts.map +1 -1
  11. package/dist/core/hooks/index.js +1 -0
  12. package/dist/core/hooks/index.js.map +1 -1
  13. package/dist/core/hooks/runner.d.ts.map +1 -1
  14. package/dist/core/hooks/runner.js +44 -4
  15. package/dist/core/hooks/runner.js.map +1 -1
  16. package/dist/core/hooks/tool-wrapper.d.ts.map +1 -1
  17. package/dist/core/hooks/tool-wrapper.js +5 -9
  18. package/dist/core/hooks/tool-wrapper.js.map +1 -1
  19. package/dist/core/hooks/types.d.ts +73 -11
  20. package/dist/core/hooks/types.d.ts.map +1 -1
  21. package/dist/core/hooks/types.js +22 -1
  22. package/dist/core/hooks/types.js.map +1 -1
  23. package/dist/core/skills.d.ts +18 -5
  24. package/dist/core/skills.d.ts.map +1 -1
  25. package/dist/core/skills.js +183 -72
  26. package/dist/core/skills.js.map +1 -1
  27. package/dist/core/slash-commands.d.ts.map +1 -1
  28. package/dist/core/slash-commands.js +2 -2
  29. package/dist/core/slash-commands.js.map +1 -1
  30. package/dist/core/system-prompt.d.ts.map +1 -1
  31. package/dist/core/system-prompt.js +2 -2
  32. package/dist/core/system-prompt.js.map +1 -1
  33. package/dist/core/tools/bash.d.ts +5 -0
  34. package/dist/core/tools/bash.d.ts.map +1 -1
  35. package/dist/core/tools/bash.js.map +1 -1
  36. package/dist/core/tools/find.d.ts +5 -0
  37. package/dist/core/tools/find.d.ts.map +1 -1
  38. package/dist/core/tools/find.js.map +1 -1
  39. package/dist/core/tools/grep.d.ts +6 -0
  40. package/dist/core/tools/grep.d.ts.map +1 -1
  41. package/dist/core/tools/grep.js.map +1 -1
  42. package/dist/core/tools/index.d.ts +6 -5
  43. package/dist/core/tools/index.d.ts.map +1 -1
  44. package/dist/core/tools/index.js.map +1 -1
  45. package/dist/core/tools/ls.d.ts +5 -0
  46. package/dist/core/tools/ls.d.ts.map +1 -1
  47. package/dist/core/tools/ls.js.map +1 -1
  48. package/dist/core/tools/read.d.ts +4 -0
  49. package/dist/core/tools/read.d.ts.map +1 -1
  50. package/dist/core/tools/read.js.map +1 -1
  51. package/dist/index.d.ts +5 -3
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +5 -1
  54. package/dist/index.js.map +1 -1
  55. package/dist/main.d.ts.map +1 -1
  56. package/dist/main.js +4 -0
  57. package/dist/main.js.map +1 -1
  58. package/dist/modes/interactive/components/custom-editor.d.ts +1 -0
  59. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  60. package/dist/modes/interactive/components/custom-editor.js +16 -7
  61. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  62. package/dist/modes/interactive/components/hook-input.d.ts.map +1 -1
  63. package/dist/modes/interactive/components/hook-input.js +2 -2
  64. package/dist/modes/interactive/components/hook-input.js.map +1 -1
  65. package/dist/modes/interactive/components/hook-selector.d.ts.map +1 -1
  66. package/dist/modes/interactive/components/hook-selector.js +2 -2
  67. package/dist/modes/interactive/components/hook-selector.js.map +1 -1
  68. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  69. package/dist/modes/interactive/components/model-selector.js +2 -2
  70. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  71. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  72. package/dist/modes/interactive/components/oauth-selector.js +2 -2
  73. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  74. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  75. package/dist/modes/interactive/components/session-selector.js +3 -3
  76. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  77. package/dist/modes/interactive/components/user-message-selector.d.ts.map +1 -1
  78. package/dist/modes/interactive/components/user-message-selector.js +3 -3
  79. package/dist/modes/interactive/components/user-message-selector.js.map +1 -1
  80. package/dist/modes/interactive/interactive-mode.d.ts +2 -0
  81. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  82. package/dist/modes/interactive/interactive-mode.js +63 -1
  83. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  84. package/dist/modes/print-mode.d.ts.map +1 -1
  85. package/dist/modes/print-mode.js +10 -0
  86. package/dist/modes/print-mode.js.map +1 -1
  87. package/docs/custom-tools.md +43 -4
  88. package/docs/hooks.md +104 -5
  89. package/docs/skills.md +65 -24
  90. package/examples/custom-tools/README.md +18 -7
  91. package/examples/custom-tools/subagent/README.md +172 -0
  92. package/examples/custom-tools/subagent/agents/planner.md +37 -0
  93. package/examples/custom-tools/subagent/agents/reviewer.md +35 -0
  94. package/examples/custom-tools/subagent/agents/scout.md +50 -0
  95. package/examples/custom-tools/subagent/agents/worker.md +24 -0
  96. package/examples/custom-tools/subagent/agents.ts +157 -0
  97. package/examples/custom-tools/subagent/commands/implement-and-review.md +10 -0
  98. package/examples/custom-tools/subagent/commands/implement.md +10 -0
  99. package/examples/custom-tools/subagent/commands/scout-and-plan.md +9 -0
  100. package/examples/custom-tools/subagent/index.ts +772 -0
  101. package/package.json +4 -4
  102. /package/examples/custom-tools/{hello.ts → hello/index.ts} +0 -0
  103. /package/examples/custom-tools/{question.ts → question/index.ts} +0 -0
  104. /package/examples/custom-tools/{todo.ts → todo/index.ts} +0 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,49 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ### Added
6
+
7
+ - **Subagent orchestration example**: Added comprehensive custom tool example for spawning and orchestrating sub-agents with isolated context windows. Includes scout/planner/reviewer/worker agents and workflow commands for multi-agent pipelines. ([#215](https://github.com/badlogic/pi-mono/pull/215) by [@nicobailon](https://github.com/nicobailon))
8
+
9
+ - **`getMarkdownTheme()` export**: Custom tools can now import `getMarkdownTheme()` from `@mariozechner/pi-coding-agent` to use the same markdown styling as the main UI.
10
+
11
+ - **`pi.exec()` signal and timeout support**: Custom tools and hooks can now pass `{ signal, timeout }` options to `pi.exec()` for cancellation and timeout handling. The result includes a `killed` flag when the process was terminated.
12
+
13
+ ### Changed
14
+
15
+ - **Subagent example improvements**: Parallel mode now streams updates from all tasks. Chain mode shows all completed steps during streaming. Expanded view uses proper markdown rendering with syntax highlighting. Usage footer shows turn count.
16
+
17
+ ### Fixed
18
+
19
+ - **JSON mode stdout flush**: Fixed race condition where `pi --mode json` could exit before all output was written to stdout, causing consumers to miss final events.
20
+
21
+ ### Breaking Changes
22
+
23
+ - **Custom tools now require `index.ts` entry point**: Auto-discovered custom tools must be in a subdirectory with an `index.ts` file. The old pattern `~/.pi/agent/tools/mytool.ts` must become `~/.pi/agent/tools/mytool/index.ts`. This allows multi-file tools to import helper modules. Explicit paths via `--tool` or `settings.json` still work with any `.ts` file.
24
+
25
+ - **Hook `tool_result` event restructured**: The `ToolResultEvent` now exposes full tool result data instead of just text. ([#233](https://github.com/badlogic/pi-mono/pull/233))
26
+ - Removed: `result: string` field
27
+ - Added: `content: (TextContent | ImageContent)[]` - full content array
28
+ - Added: `details: unknown` - tool-specific details (typed per tool via discriminated union on `toolName`)
29
+ - `ToolResultEventResult.result` renamed to `ToolResultEventResult.text` (removed), use `content` instead
30
+ - Hook handlers returning `{ result: "..." }` must change to `{ content: [{ type: "text", text: "..." }] }`
31
+ - Built-in tool details types exported: `BashToolDetails`, `ReadToolDetails`, `GrepToolDetails`, `FindToolDetails`, `LsToolDetails`, `TruncationResult`
32
+ - Type guards exported for narrowing: `isBashToolResult`, `isReadToolResult`, `isEditToolResult`, `isWriteToolResult`, `isGrepToolResult`, `isFindToolResult`, `isLsToolResult`
33
+
34
+ ### Added
35
+
36
+ - **Kitty keyboard protocol support**: Shift+Enter, Alt+Enter, Shift+Tab, Ctrl+D, and all Ctrl+key combinations now work in Ghostty, Kitty, WezTerm, and other modern terminals. ([#225](https://github.com/badlogic/pi-mono/pull/225) by [@kim0](https://github.com/kim0))
37
+
38
+ - **Dynamic API key refresh**: OAuth tokens (GitHub Copilot, Anthropic OAuth) are now refreshed before each LLM call, preventing failures in long-running agent loops where tokens expire mid-session. ([#223](https://github.com/badlogic/pi-mono/pull/223) by [@kim0](https://github.com/kim0))
39
+
40
+ - **`/hotkeys` command**: Shows all keyboard shortcuts in a formatted table.
41
+
42
+ - **Markdown table borders**: Tables now render with proper top and bottom borders.
43
+
44
+ ### Changed
45
+
46
+ - **Skills standard compliance**: Skills now adhere to the [Agent Skills standard](https://agentskills.io/specification). Validates name (must match parent directory, lowercase, max 64 chars), description (required, max 1024 chars), and frontmatter fields. Warns on violations but remains lenient. Prompt format changed to XML structure. Removed `{baseDir}` placeholder in favor of relative paths. ([#231](https://github.com/badlogic/pi-mono/issues/231))
47
+
5
48
  ## [0.23.4] - 2025-12-18
6
49
 
7
50
  ### Added
package/README.md CHANGED
@@ -167,6 +167,7 @@ The agent reads, writes, and edits files, and executes commands via bash.
167
167
  | `/queue` | Set message queue mode: one-at-a-time (default) or all-at-once |
168
168
  | `/export [file]` | Export session to self-contained HTML |
169
169
  | `/session` | Show session info: path, message counts, token usage, cost |
170
+ | `/hotkeys` | Show all keyboard shortcuts |
170
171
  | `/changelog` | Display full version history |
171
172
  | `/branch` | Create new conversation branch from a previous message |
172
173
  | `/resume` | Switch to a different session (interactive selector) |
@@ -199,8 +200,8 @@ The agent reads, writes, and edits files, and executes commands via bash.
199
200
  |-----|--------|
200
201
  | Arrow keys | Move cursor / browse history (Up when empty) |
201
202
  | Option+Left/Right | Move by word |
202
- | Ctrl+A / Home | Start of line |
203
- | Ctrl+E / End | End of line |
203
+ | Ctrl+A / Home / Cmd+Left | Start of line |
204
+ | Ctrl+E / End / Cmd+Right | End of line |
204
205
 
205
206
  **Editing:**
206
207
 
@@ -219,6 +220,7 @@ The agent reads, writes, and edits files, and executes commands via bash.
219
220
  | Tab | Path completion / accept autocomplete |
220
221
  | Escape | Cancel autocomplete / abort streaming |
221
222
  | Ctrl+C | Clear editor (first) / exit (second) |
223
+ | Ctrl+D | Exit (when editor is empty) |
222
224
  | Shift+Tab | Cycle thinking level |
223
225
  | Ctrl+P | Cycle models (scoped by `--models`) |
224
226
  | Ctrl+O | Toggle tool output expansion |
@@ -488,7 +490,9 @@ Usage: `/component Button "onClick handler" "disabled support"`
488
490
 
489
491
  ### Skills
490
492
 
491
- Skills are self-contained capability packages that the agent loads on-demand. A skill provides specialized workflows, setup instructions, helper scripts, and reference documentation for specific tasks. Skills are loaded when the agent decides a task matches the description, or when you explicitly ask to use one.
493
+ Skills are self-contained capability packages that the agent loads on-demand. Pi implements the [Agent Skills standard](https://agentskills.io/specification), warning about violations but remaining lenient.
494
+
495
+ A skill provides specialized workflows, setup instructions, helper scripts, and reference documentation for specific tasks. Skills are loaded when the agent decides a task matches the description, or when you explicitly ask to use one.
492
496
 
493
497
  **Example use cases:**
494
498
  - Web search and content extraction (Brave Search API)
@@ -508,6 +512,7 @@ Skills are self-contained capability packages that the agent loads on-demand. A
508
512
 
509
513
  ```markdown
510
514
  ---
515
+ name: brave-search
511
516
  description: Web search via Brave Search API. Use for documentation, facts, or web content.
512
517
  ---
513
518
 
@@ -515,18 +520,18 @@ description: Web search via Brave Search API. Use for documentation, facts, or w
515
520
 
516
521
  ## Setup
517
522
  \`\`\`bash
518
- cd {baseDir} && npm install
523
+ cd /path/to/brave-search && npm install
519
524
  \`\`\`
520
525
 
521
526
  ## Usage
522
527
  \`\`\`bash
523
- {baseDir}/search.js "query" # Basic search
524
- {baseDir}/search.js "query" --content # Include page content
528
+ ./search.js "query" # Basic search
529
+ ./search.js "query" --content # Include page content
525
530
  \`\`\`
526
531
  ```
527
532
 
528
- - `description`: Required. Determines when the skill is loaded.
529
- - `{baseDir}`: Placeholder for the skill's directory.
533
+ - `name`: Required. Must match parent directory name. Lowercase, hyphens, max 64 chars.
534
+ - `description`: Required. Max 1024 chars. Determines when the skill is loaded.
530
535
 
531
536
  **Disable skills:** `pi --no-skills` or set `skills.enabled: false` in settings.
532
537
 
@@ -590,10 +595,12 @@ export default function (pi: HookAPI) {
590
595
 
591
596
  Custom tools let you extend the built-in toolset (read, write, edit, bash, ...) and are called by the LLM directly. They are TypeScript modules that define tools with optional custom TUI integration for getting user input and custom tool call and result rendering.
592
597
 
593
- **Tool locations:**
594
- - Global: `~/.pi/agent/tools/*.ts`
595
- - Project: `.pi/tools/*.ts`
596
- - CLI: `--tool <path>`
598
+ **Tool locations (auto-discovered):**
599
+ - Global: `~/.pi/agent/tools/*/index.ts`
600
+ - Project: `.pi/tools/*/index.ts`
601
+
602
+ **Explicit paths:**
603
+ - CLI: `--tool <path>` (any .ts file)
597
604
  - Settings: `customTools` array in `settings.json`
598
605
 
599
606
  **Quick example:**
@@ -838,7 +845,7 @@ Pi is opinionated about what it won't do. These are intentional design decisions
838
845
 
839
846
  **No MCP.** Build CLI tools with READMEs (see [Skills](#skills)). The agent reads them on demand. [Would you like to know more?](https://mariozechner.at/posts/2025-11-02-what-if-you-dont-need-mcp/)
840
847
 
841
- **No sub-agents.** Spawn pi instances via tmux, or build a task tool with [custom tools](#custom-tools). Full observability and steerability.
848
+ **No sub-agents.** Spawn pi instances via tmux, or [build your own sub-agent tool](examples/custom-tools/subagent/) with [custom tools](#custom-tools). Full observability and steerability.
842
849
 
843
850
  **No permission popups.** Security theater. Run in a container or build your own with [Hooks](#hooks).
844
851
 
@@ -1 +1 @@
1
- {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../../src/core/custom-tools/loader.ts"],"names":[],"mappings":"AAAA;;GAEG;AAWH,OAAO,KAAK,EAAqB,qBAAqB,EAAyC,MAAM,YAAY,CAAC;AAyJlH;;;;;GAKG;AACH,wBAAsB,eAAe,CACpC,KAAK,EAAE,MAAM,EAAE,EACf,GAAG,EAAE,MAAM,EACX,gBAAgB,EAAE,MAAM,EAAE,GACxB,OAAO,CAAC,qBAAqB,CAAC,CA8ChC;AAqBD;;;;;;;;;;GAUG;AACH,wBAAsB,0BAA0B,CAC/C,eAAe,EAAE,MAAM,EAAE,EACzB,GAAG,EAAE,MAAM,EACX,gBAAgB,EAAE,MAAM,EAAE,GACxB,OAAO,CAAC,qBAAqB,CAAC,CA2BhC","sourcesContent":["/**\n * Custom tool loader - loads TypeScript tool modules using jiti.\n */\n\nimport { spawn } from \"node:child_process\";\nimport * as fs from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { createJiti } from \"jiti\";\nimport { getAgentDir } from \"../../config.js\";\nimport type { HookUIContext } from \"../hooks/types.js\";\nimport type { CustomToolFactory, CustomToolsLoadResult, ExecResult, LoadedCustomTool, ToolAPI } from \"./types.js\";\n\n// Create require function to resolve module paths at runtime\nconst require = createRequire(import.meta.url);\n\n// Lazily computed aliases - resolved at runtime to handle global installs\nlet _aliases: Record<string, string> | null = null;\nfunction getAliases(): Record<string, string> {\n\tif (_aliases) return _aliases;\n\n\tconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\tconst packageIndex = path.resolve(__dirname, \"../..\", \"index.js\");\n\n\t_aliases = {\n\t\t\"@mariozechner/pi-coding-agent\": packageIndex,\n\t\t\"@mariozechner/pi-tui\": require.resolve(\"@mariozechner/pi-tui\"),\n\t\t\"@mariozechner/pi-ai\": require.resolve(\"@mariozechner/pi-ai\"),\n\t\t\"@sinclair/typebox\": require.resolve(\"@sinclair/typebox\"),\n\t};\n\treturn _aliases;\n}\n\nconst UNICODE_SPACES = /[\\u00A0\\u2000-\\u200A\\u202F\\u205F\\u3000]/g;\n\nfunction normalizeUnicodeSpaces(str: string): string {\n\treturn str.replace(UNICODE_SPACES, \" \");\n}\n\nfunction expandPath(p: string): string {\n\tconst normalized = normalizeUnicodeSpaces(p);\n\tif (normalized.startsWith(\"~/\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(2));\n\t}\n\tif (normalized.startsWith(\"~\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(1));\n\t}\n\treturn normalized;\n}\n\n/**\n * Resolve tool path.\n * - Absolute paths used as-is\n * - Paths starting with ~ expanded to home directory\n * - Relative paths resolved from cwd\n */\nfunction resolveToolPath(toolPath: string, cwd: string): string {\n\tconst expanded = expandPath(toolPath);\n\n\tif (path.isAbsolute(expanded)) {\n\t\treturn expanded;\n\t}\n\n\t// Relative paths resolved from cwd\n\treturn path.resolve(cwd, expanded);\n}\n\n/**\n * Execute a command and return stdout/stderr/code.\n */\nasync function execCommand(command: string, args: string[], cwd: string): Promise<ExecResult> {\n\treturn new Promise((resolve) => {\n\t\tconst proc = spawn(command, args, {\n\t\t\tcwd,\n\t\t\tshell: false,\n\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t});\n\n\t\tlet stdout = \"\";\n\t\tlet stderr = \"\";\n\n\t\tproc.stdout.on(\"data\", (data) => {\n\t\t\tstdout += data.toString();\n\t\t});\n\n\t\tproc.stderr.on(\"data\", (data) => {\n\t\t\tstderr += data.toString();\n\t\t});\n\n\t\tproc.on(\"close\", (code) => {\n\t\t\tresolve({\n\t\t\t\tstdout,\n\t\t\t\tstderr,\n\t\t\t\tcode: code ?? 0,\n\t\t\t});\n\t\t});\n\n\t\tproc.on(\"error\", (err) => {\n\t\t\tresolve({\n\t\t\t\tstdout,\n\t\t\t\tstderr: stderr || err.message,\n\t\t\t\tcode: 1,\n\t\t\t});\n\t\t});\n\t});\n}\n\n/**\n * Create a no-op UI context for headless modes.\n */\nfunction createNoOpUIContext(): HookUIContext {\n\treturn {\n\t\tselect: async () => null,\n\t\tconfirm: async () => false,\n\t\tinput: async () => null,\n\t\tnotify: () => {},\n\t};\n}\n\n/**\n * Load a single tool module using jiti.\n */\nasync function loadTool(\n\ttoolPath: string,\n\tcwd: string,\n\tsharedApi: ToolAPI,\n): Promise<{ tools: LoadedCustomTool[] | null; error: string | null }> {\n\tconst resolvedPath = resolveToolPath(toolPath, cwd);\n\n\ttry {\n\t\t// Create jiti instance for TypeScript/ESM loading\n\t\t// Use aliases to resolve package imports since tools are loaded from user directories\n\t\t// (e.g. ~/.pi/agent/tools) but import from packages installed with pi-coding-agent\n\t\tconst jiti = createJiti(import.meta.url, {\n\t\t\talias: getAliases(),\n\t\t});\n\n\t\t// Import the module\n\t\tconst module = await jiti.import(resolvedPath, { default: true });\n\t\tconst factory = module as CustomToolFactory;\n\n\t\tif (typeof factory !== \"function\") {\n\t\t\treturn { tools: null, error: \"Tool must export a default function\" };\n\t\t}\n\n\t\t// Call factory with shared API\n\t\tconst result = await factory(sharedApi);\n\n\t\t// Handle single tool or array of tools\n\t\tconst toolsArray = Array.isArray(result) ? result : [result];\n\n\t\tconst loadedTools: LoadedCustomTool[] = toolsArray.map((tool) => ({\n\t\t\tpath: toolPath,\n\t\t\tresolvedPath,\n\t\t\ttool,\n\t\t}));\n\n\t\treturn { tools: loadedTools, error: null };\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\treturn { tools: null, error: `Failed to load tool: ${message}` };\n\t}\n}\n\n/**\n * Load all tools from configuration.\n * @param paths - Array of tool file paths\n * @param cwd - Current working directory for resolving relative paths\n * @param builtInToolNames - Names of built-in tools to check for conflicts\n */\nexport async function loadCustomTools(\n\tpaths: string[],\n\tcwd: string,\n\tbuiltInToolNames: string[],\n): Promise<CustomToolsLoadResult> {\n\tconst tools: LoadedCustomTool[] = [];\n\tconst errors: Array<{ path: string; error: string }> = [];\n\tconst seenNames = new Set<string>(builtInToolNames);\n\n\t// Shared API object - all tools get the same instance\n\tconst sharedApi: ToolAPI = {\n\t\tcwd,\n\t\texec: (command: string, args: string[]) => execCommand(command, args, cwd),\n\t\tui: createNoOpUIContext(),\n\t\thasUI: false,\n\t};\n\n\tfor (const toolPath of paths) {\n\t\tconst { tools: loadedTools, error } = await loadTool(toolPath, cwd, sharedApi);\n\n\t\tif (error) {\n\t\t\terrors.push({ path: toolPath, error });\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (loadedTools) {\n\t\t\tfor (const loadedTool of loadedTools) {\n\t\t\t\t// Check for name conflicts\n\t\t\t\tif (seenNames.has(loadedTool.tool.name)) {\n\t\t\t\t\terrors.push({\n\t\t\t\t\t\tpath: toolPath,\n\t\t\t\t\t\terror: `Tool name \"${loadedTool.tool.name}\" conflicts with existing tool`,\n\t\t\t\t\t});\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tseenNames.add(loadedTool.tool.name);\n\t\t\t\ttools.push(loadedTool);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn {\n\t\ttools,\n\t\terrors,\n\t\tsetUIContext(uiContext, hasUI) {\n\t\t\tsharedApi.ui = uiContext;\n\t\t\tsharedApi.hasUI = hasUI;\n\t\t},\n\t};\n}\n\n/**\n * Discover tool files from a directory.\n * Returns all .ts files (and symlinks to .ts files) in the directory (non-recursive).\n */\nfunction discoverToolsInDir(dir: string): string[] {\n\tif (!fs.existsSync(dir)) {\n\t\treturn [];\n\t}\n\n\ttry {\n\t\tconst entries = fs.readdirSync(dir, { withFileTypes: true });\n\t\treturn entries\n\t\t\t.filter((e) => (e.isFile() || e.isSymbolicLink()) && e.name.endsWith(\".ts\"))\n\t\t\t.map((e) => path.join(dir, e.name));\n\t} catch {\n\t\treturn [];\n\t}\n}\n\n/**\n * Discover and load tools from standard locations:\n * 1. ~/.pi/agent/tools/*.ts (global)\n * 2. cwd/.pi/tools/*.ts (project-local)\n *\n * Plus any explicitly configured paths from settings or CLI.\n *\n * @param configuredPaths - Explicit paths from settings.json and CLI --tool flags\n * @param cwd - Current working directory\n * @param builtInToolNames - Names of built-in tools to check for conflicts\n */\nexport async function discoverAndLoadCustomTools(\n\tconfiguredPaths: string[],\n\tcwd: string,\n\tbuiltInToolNames: string[],\n): Promise<CustomToolsLoadResult> {\n\tconst allPaths: string[] = [];\n\tconst seen = new Set<string>();\n\n\t// Helper to add paths without duplicates\n\tconst addPaths = (paths: string[]) => {\n\t\tfor (const p of paths) {\n\t\t\tconst resolved = path.resolve(p);\n\t\t\tif (!seen.has(resolved)) {\n\t\t\t\tseen.add(resolved);\n\t\t\t\tallPaths.push(p);\n\t\t\t}\n\t\t}\n\t};\n\n\t// 1. Global tools: ~/.pi/agent/tools/\n\tconst globalToolsDir = path.join(getAgentDir(), \"tools\");\n\taddPaths(discoverToolsInDir(globalToolsDir));\n\n\t// 2. Project-local tools: cwd/.pi/tools/\n\tconst localToolsDir = path.join(cwd, \".pi\", \"tools\");\n\taddPaths(discoverToolsInDir(localToolsDir));\n\n\t// 3. Explicitly configured paths (can override/add)\n\taddPaths(configuredPaths.map((p) => resolveToolPath(p, cwd)));\n\n\treturn loadCustomTools(allPaths, cwd, builtInToolNames);\n}\n"]}
1
+ {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../../src/core/custom-tools/loader.ts"],"names":[],"mappings":"AAAA;;GAEG;AAWH,OAAO,KAAK,EAEX,qBAAqB,EAKrB,MAAM,YAAY,CAAC;AAmMpB;;;;;GAKG;AACH,wBAAsB,eAAe,CACpC,KAAK,EAAE,MAAM,EAAE,EACf,GAAG,EAAE,MAAM,EACX,gBAAgB,EAAE,MAAM,EAAE,GACxB,OAAO,CAAC,qBAAqB,CAAC,CA8ChC;AAgCD;;;;;;;;;;GAUG;AACH,wBAAsB,0BAA0B,CAC/C,eAAe,EAAE,MAAM,EAAE,EACzB,GAAG,EAAE,MAAM,EACX,gBAAgB,EAAE,MAAM,EAAE,GACxB,OAAO,CAAC,qBAAqB,CAAC,CA2BhC","sourcesContent":["/**\n * Custom tool loader - loads TypeScript tool modules using jiti.\n */\n\nimport { spawn } from \"node:child_process\";\nimport * as fs from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { createJiti } from \"jiti\";\nimport { getAgentDir } from \"../../config.js\";\nimport type { HookUIContext } from \"../hooks/types.js\";\nimport type {\n\tCustomToolFactory,\n\tCustomToolsLoadResult,\n\tExecOptions,\n\tExecResult,\n\tLoadedCustomTool,\n\tToolAPI,\n} from \"./types.js\";\n\n// Create require function to resolve module paths at runtime\nconst require = createRequire(import.meta.url);\n\n// Lazily computed aliases - resolved at runtime to handle global installs\nlet _aliases: Record<string, string> | null = null;\nfunction getAliases(): Record<string, string> {\n\tif (_aliases) return _aliases;\n\n\tconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\tconst packageIndex = path.resolve(__dirname, \"../..\", \"index.js\");\n\n\t_aliases = {\n\t\t\"@mariozechner/pi-coding-agent\": packageIndex,\n\t\t\"@mariozechner/pi-tui\": require.resolve(\"@mariozechner/pi-tui\"),\n\t\t\"@mariozechner/pi-ai\": require.resolve(\"@mariozechner/pi-ai\"),\n\t\t\"@sinclair/typebox\": require.resolve(\"@sinclair/typebox\"),\n\t};\n\treturn _aliases;\n}\n\nconst UNICODE_SPACES = /[\\u00A0\\u2000-\\u200A\\u202F\\u205F\\u3000]/g;\n\nfunction normalizeUnicodeSpaces(str: string): string {\n\treturn str.replace(UNICODE_SPACES, \" \");\n}\n\nfunction expandPath(p: string): string {\n\tconst normalized = normalizeUnicodeSpaces(p);\n\tif (normalized.startsWith(\"~/\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(2));\n\t}\n\tif (normalized.startsWith(\"~\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(1));\n\t}\n\treturn normalized;\n}\n\n/**\n * Resolve tool path.\n * - Absolute paths used as-is\n * - Paths starting with ~ expanded to home directory\n * - Relative paths resolved from cwd\n */\nfunction resolveToolPath(toolPath: string, cwd: string): string {\n\tconst expanded = expandPath(toolPath);\n\n\tif (path.isAbsolute(expanded)) {\n\t\treturn expanded;\n\t}\n\n\t// Relative paths resolved from cwd\n\treturn path.resolve(cwd, expanded);\n}\n\n/**\n * Execute a command and return stdout/stderr/code.\n * Supports cancellation via AbortSignal and timeout.\n */\nasync function execCommand(command: string, args: string[], cwd: string, options?: ExecOptions): Promise<ExecResult> {\n\treturn new Promise((resolve) => {\n\t\tconst proc = spawn(command, args, {\n\t\t\tcwd,\n\t\t\tshell: false,\n\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t});\n\n\t\tlet stdout = \"\";\n\t\tlet stderr = \"\";\n\t\tlet killed = false;\n\t\tlet timeoutId: NodeJS.Timeout | undefined;\n\n\t\tconst killProcess = () => {\n\t\t\tif (!killed) {\n\t\t\t\tkilled = true;\n\t\t\t\tproc.kill(\"SIGTERM\");\n\t\t\t\t// Force kill after 5 seconds if SIGTERM doesn't work\n\t\t\t\tsetTimeout(() => {\n\t\t\t\t\tif (!proc.killed) {\n\t\t\t\t\t\tproc.kill(\"SIGKILL\");\n\t\t\t\t\t}\n\t\t\t\t}, 5000);\n\t\t\t}\n\t\t};\n\n\t\t// Handle abort signal\n\t\tif (options?.signal) {\n\t\t\tif (options.signal.aborted) {\n\t\t\t\tkillProcess();\n\t\t\t} else {\n\t\t\t\toptions.signal.addEventListener(\"abort\", killProcess, { once: true });\n\t\t\t}\n\t\t}\n\n\t\t// Handle timeout\n\t\tif (options?.timeout && options.timeout > 0) {\n\t\t\ttimeoutId = setTimeout(() => {\n\t\t\t\tkillProcess();\n\t\t\t}, options.timeout);\n\t\t}\n\n\t\tproc.stdout.on(\"data\", (data) => {\n\t\t\tstdout += data.toString();\n\t\t});\n\n\t\tproc.stderr.on(\"data\", (data) => {\n\t\t\tstderr += data.toString();\n\t\t});\n\n\t\tproc.on(\"close\", (code) => {\n\t\t\tif (timeoutId) clearTimeout(timeoutId);\n\t\t\tif (options?.signal) {\n\t\t\t\toptions.signal.removeEventListener(\"abort\", killProcess);\n\t\t\t}\n\t\t\tresolve({\n\t\t\t\tstdout,\n\t\t\t\tstderr,\n\t\t\t\tcode: code ?? 0,\n\t\t\t\tkilled,\n\t\t\t});\n\t\t});\n\n\t\tproc.on(\"error\", (err) => {\n\t\t\tif (timeoutId) clearTimeout(timeoutId);\n\t\t\tif (options?.signal) {\n\t\t\t\toptions.signal.removeEventListener(\"abort\", killProcess);\n\t\t\t}\n\t\t\tresolve({\n\t\t\t\tstdout,\n\t\t\t\tstderr: stderr || err.message,\n\t\t\t\tcode: 1,\n\t\t\t\tkilled,\n\t\t\t});\n\t\t});\n\t});\n}\n\n/**\n * Create a no-op UI context for headless modes.\n */\nfunction createNoOpUIContext(): HookUIContext {\n\treturn {\n\t\tselect: async () => null,\n\t\tconfirm: async () => false,\n\t\tinput: async () => null,\n\t\tnotify: () => {},\n\t};\n}\n\n/**\n * Load a single tool module using jiti.\n */\nasync function loadTool(\n\ttoolPath: string,\n\tcwd: string,\n\tsharedApi: ToolAPI,\n): Promise<{ tools: LoadedCustomTool[] | null; error: string | null }> {\n\tconst resolvedPath = resolveToolPath(toolPath, cwd);\n\n\ttry {\n\t\t// Create jiti instance for TypeScript/ESM loading\n\t\t// Use aliases to resolve package imports since tools are loaded from user directories\n\t\t// (e.g. ~/.pi/agent/tools) but import from packages installed with pi-coding-agent\n\t\tconst jiti = createJiti(import.meta.url, {\n\t\t\talias: getAliases(),\n\t\t});\n\n\t\t// Import the module\n\t\tconst module = await jiti.import(resolvedPath, { default: true });\n\t\tconst factory = module as CustomToolFactory;\n\n\t\tif (typeof factory !== \"function\") {\n\t\t\treturn { tools: null, error: \"Tool must export a default function\" };\n\t\t}\n\n\t\t// Call factory with shared API\n\t\tconst result = await factory(sharedApi);\n\n\t\t// Handle single tool or array of tools\n\t\tconst toolsArray = Array.isArray(result) ? result : [result];\n\n\t\tconst loadedTools: LoadedCustomTool[] = toolsArray.map((tool) => ({\n\t\t\tpath: toolPath,\n\t\t\tresolvedPath,\n\t\t\ttool,\n\t\t}));\n\n\t\treturn { tools: loadedTools, error: null };\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\treturn { tools: null, error: `Failed to load tool: ${message}` };\n\t}\n}\n\n/**\n * Load all tools from configuration.\n * @param paths - Array of tool file paths\n * @param cwd - Current working directory for resolving relative paths\n * @param builtInToolNames - Names of built-in tools to check for conflicts\n */\nexport async function loadCustomTools(\n\tpaths: string[],\n\tcwd: string,\n\tbuiltInToolNames: string[],\n): Promise<CustomToolsLoadResult> {\n\tconst tools: LoadedCustomTool[] = [];\n\tconst errors: Array<{ path: string; error: string }> = [];\n\tconst seenNames = new Set<string>(builtInToolNames);\n\n\t// Shared API object - all tools get the same instance\n\tconst sharedApi: ToolAPI = {\n\t\tcwd,\n\t\texec: (command: string, args: string[], options?: ExecOptions) => execCommand(command, args, cwd, options),\n\t\tui: createNoOpUIContext(),\n\t\thasUI: false,\n\t};\n\n\tfor (const toolPath of paths) {\n\t\tconst { tools: loadedTools, error } = await loadTool(toolPath, cwd, sharedApi);\n\n\t\tif (error) {\n\t\t\terrors.push({ path: toolPath, error });\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (loadedTools) {\n\t\t\tfor (const loadedTool of loadedTools) {\n\t\t\t\t// Check for name conflicts\n\t\t\t\tif (seenNames.has(loadedTool.tool.name)) {\n\t\t\t\t\terrors.push({\n\t\t\t\t\t\tpath: toolPath,\n\t\t\t\t\t\terror: `Tool name \"${loadedTool.tool.name}\" conflicts with existing tool`,\n\t\t\t\t\t});\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tseenNames.add(loadedTool.tool.name);\n\t\t\t\ttools.push(loadedTool);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn {\n\t\ttools,\n\t\terrors,\n\t\tsetUIContext(uiContext, hasUI) {\n\t\t\tsharedApi.ui = uiContext;\n\t\t\tsharedApi.hasUI = hasUI;\n\t\t},\n\t};\n}\n\n/**\n * Discover tool files from a directory.\n * Only loads index.ts files from subdirectories (e.g., tools/mytool/index.ts).\n */\nfunction discoverToolsInDir(dir: string): string[] {\n\tif (!fs.existsSync(dir)) {\n\t\treturn [];\n\t}\n\n\tconst tools: string[] = [];\n\n\ttry {\n\t\tconst entries = fs.readdirSync(dir, { withFileTypes: true });\n\n\t\tfor (const entry of entries) {\n\t\t\tif (entry.isDirectory() || entry.isSymbolicLink()) {\n\t\t\t\t// Check for index.ts in subdirectory\n\t\t\t\tconst indexPath = path.join(dir, entry.name, \"index.ts\");\n\t\t\t\tif (fs.existsSync(indexPath)) {\n\t\t\t\t\ttools.push(indexPath);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} catch {\n\t\treturn [];\n\t}\n\n\treturn tools;\n}\n\n/**\n * Discover and load tools from standard locations:\n * 1. ~/.pi/agent/tools/*.ts (global)\n * 2. cwd/.pi/tools/*.ts (project-local)\n *\n * Plus any explicitly configured paths from settings or CLI.\n *\n * @param configuredPaths - Explicit paths from settings.json and CLI --tool flags\n * @param cwd - Current working directory\n * @param builtInToolNames - Names of built-in tools to check for conflicts\n */\nexport async function discoverAndLoadCustomTools(\n\tconfiguredPaths: string[],\n\tcwd: string,\n\tbuiltInToolNames: string[],\n): Promise<CustomToolsLoadResult> {\n\tconst allPaths: string[] = [];\n\tconst seen = new Set<string>();\n\n\t// Helper to add paths without duplicates\n\tconst addPaths = (paths: string[]) => {\n\t\tfor (const p of paths) {\n\t\t\tconst resolved = path.resolve(p);\n\t\t\tif (!seen.has(resolved)) {\n\t\t\t\tseen.add(resolved);\n\t\t\t\tallPaths.push(p);\n\t\t\t}\n\t\t}\n\t};\n\n\t// 1. Global tools: ~/.pi/agent/tools/\n\tconst globalToolsDir = path.join(getAgentDir(), \"tools\");\n\taddPaths(discoverToolsInDir(globalToolsDir));\n\n\t// 2. Project-local tools: cwd/.pi/tools/\n\tconst localToolsDir = path.join(cwd, \".pi\", \"tools\");\n\taddPaths(discoverToolsInDir(localToolsDir));\n\n\t// 3. Explicitly configured paths (can override/add)\n\taddPaths(configuredPaths.map((p) => resolveToolPath(p, cwd)));\n\n\treturn loadCustomTools(allPaths, cwd, builtInToolNames);\n}\n"]}
@@ -56,8 +56,9 @@ function resolveToolPath(toolPath, cwd) {
56
56
  }
57
57
  /**
58
58
  * Execute a command and return stdout/stderr/code.
59
+ * Supports cancellation via AbortSignal and timeout.
59
60
  */
60
- async function execCommand(command, args, cwd) {
61
+ async function execCommand(command, args, cwd, options) {
61
62
  return new Promise((resolve) => {
62
63
  const proc = spawn(command, args, {
63
64
  cwd,
@@ -66,6 +67,35 @@ async function execCommand(command, args, cwd) {
66
67
  });
67
68
  let stdout = "";
68
69
  let stderr = "";
70
+ let killed = false;
71
+ let timeoutId;
72
+ const killProcess = () => {
73
+ if (!killed) {
74
+ killed = true;
75
+ proc.kill("SIGTERM");
76
+ // Force kill after 5 seconds if SIGTERM doesn't work
77
+ setTimeout(() => {
78
+ if (!proc.killed) {
79
+ proc.kill("SIGKILL");
80
+ }
81
+ }, 5000);
82
+ }
83
+ };
84
+ // Handle abort signal
85
+ if (options?.signal) {
86
+ if (options.signal.aborted) {
87
+ killProcess();
88
+ }
89
+ else {
90
+ options.signal.addEventListener("abort", killProcess, { once: true });
91
+ }
92
+ }
93
+ // Handle timeout
94
+ if (options?.timeout && options.timeout > 0) {
95
+ timeoutId = setTimeout(() => {
96
+ killProcess();
97
+ }, options.timeout);
98
+ }
69
99
  proc.stdout.on("data", (data) => {
70
100
  stdout += data.toString();
71
101
  });
@@ -73,17 +103,29 @@ async function execCommand(command, args, cwd) {
73
103
  stderr += data.toString();
74
104
  });
75
105
  proc.on("close", (code) => {
106
+ if (timeoutId)
107
+ clearTimeout(timeoutId);
108
+ if (options?.signal) {
109
+ options.signal.removeEventListener("abort", killProcess);
110
+ }
76
111
  resolve({
77
112
  stdout,
78
113
  stderr,
79
114
  code: code ?? 0,
115
+ killed,
80
116
  });
81
117
  });
82
118
  proc.on("error", (err) => {
119
+ if (timeoutId)
120
+ clearTimeout(timeoutId);
121
+ if (options?.signal) {
122
+ options.signal.removeEventListener("abort", killProcess);
123
+ }
83
124
  resolve({
84
125
  stdout,
85
126
  stderr: stderr || err.message,
86
127
  code: 1,
128
+ killed,
87
129
  });
88
130
  });
89
131
  });
@@ -146,7 +188,7 @@ export async function loadCustomTools(paths, cwd, builtInToolNames) {
146
188
  // Shared API object - all tools get the same instance
147
189
  const sharedApi = {
148
190
  cwd,
149
- exec: (command, args) => execCommand(command, args, cwd),
191
+ exec: (command, args, options) => execCommand(command, args, cwd, options),
150
192
  ui: createNoOpUIContext(),
151
193
  hasUI: false,
152
194
  };
@@ -182,21 +224,29 @@ export async function loadCustomTools(paths, cwd, builtInToolNames) {
182
224
  }
183
225
  /**
184
226
  * Discover tool files from a directory.
185
- * Returns all .ts files (and symlinks to .ts files) in the directory (non-recursive).
227
+ * Only loads index.ts files from subdirectories (e.g., tools/mytool/index.ts).
186
228
  */
187
229
  function discoverToolsInDir(dir) {
188
230
  if (!fs.existsSync(dir)) {
189
231
  return [];
190
232
  }
233
+ const tools = [];
191
234
  try {
192
235
  const entries = fs.readdirSync(dir, { withFileTypes: true });
193
- return entries
194
- .filter((e) => (e.isFile() || e.isSymbolicLink()) && e.name.endsWith(".ts"))
195
- .map((e) => path.join(dir, e.name));
236
+ for (const entry of entries) {
237
+ if (entry.isDirectory() || entry.isSymbolicLink()) {
238
+ // Check for index.ts in subdirectory
239
+ const indexPath = path.join(dir, entry.name, "index.ts");
240
+ if (fs.existsSync(indexPath)) {
241
+ tools.push(indexPath);
242
+ }
243
+ }
244
+ }
196
245
  }
197
246
  catch {
198
247
  return [];
199
248
  }
249
+ return tools;
200
250
  }
201
251
  /**
202
252
  * Discover and load tools from standard locations:
@@ -1 +1 @@
1
- {"version":3,"file":"loader.js","sourceRoot":"","sources":["../../../src/core/custom-tools/loader.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Custom tool loader - loads TypeScript tool modules using jiti.\n */\n\nimport { spawn } from \"node:child_process\";\nimport * as fs from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { createJiti } from \"jiti\";\nimport { getAgentDir } from \"../../config.js\";\nimport type { HookUIContext } from \"../hooks/types.js\";\nimport type { CustomToolFactory, CustomToolsLoadResult, ExecResult, LoadedCustomTool, ToolAPI } from \"./types.js\";\n\n// Create require function to resolve module paths at runtime\nconst require = createRequire(import.meta.url);\n\n// Lazily computed aliases - resolved at runtime to handle global installs\nlet _aliases: Record<string, string> | null = null;\nfunction getAliases(): Record<string, string> {\n\tif (_aliases) return _aliases;\n\n\tconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\tconst packageIndex = path.resolve(__dirname, \"../..\", \"index.js\");\n\n\t_aliases = {\n\t\t\"@mariozechner/pi-coding-agent\": packageIndex,\n\t\t\"@mariozechner/pi-tui\": require.resolve(\"@mariozechner/pi-tui\"),\n\t\t\"@mariozechner/pi-ai\": require.resolve(\"@mariozechner/pi-ai\"),\n\t\t\"@sinclair/typebox\": require.resolve(\"@sinclair/typebox\"),\n\t};\n\treturn _aliases;\n}\n\nconst UNICODE_SPACES = /[\\u00A0\\u2000-\\u200A\\u202F\\u205F\\u3000]/g;\n\nfunction normalizeUnicodeSpaces(str: string): string {\n\treturn str.replace(UNICODE_SPACES, \" \");\n}\n\nfunction expandPath(p: string): string {\n\tconst normalized = normalizeUnicodeSpaces(p);\n\tif (normalized.startsWith(\"~/\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(2));\n\t}\n\tif (normalized.startsWith(\"~\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(1));\n\t}\n\treturn normalized;\n}\n\n/**\n * Resolve tool path.\n * - Absolute paths used as-is\n * - Paths starting with ~ expanded to home directory\n * - Relative paths resolved from cwd\n */\nfunction resolveToolPath(toolPath: string, cwd: string): string {\n\tconst expanded = expandPath(toolPath);\n\n\tif (path.isAbsolute(expanded)) {\n\t\treturn expanded;\n\t}\n\n\t// Relative paths resolved from cwd\n\treturn path.resolve(cwd, expanded);\n}\n\n/**\n * Execute a command and return stdout/stderr/code.\n */\nasync function execCommand(command: string, args: string[], cwd: string): Promise<ExecResult> {\n\treturn new Promise((resolve) => {\n\t\tconst proc = spawn(command, args, {\n\t\t\tcwd,\n\t\t\tshell: false,\n\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t});\n\n\t\tlet stdout = \"\";\n\t\tlet stderr = \"\";\n\n\t\tproc.stdout.on(\"data\", (data) => {\n\t\t\tstdout += data.toString();\n\t\t});\n\n\t\tproc.stderr.on(\"data\", (data) => {\n\t\t\tstderr += data.toString();\n\t\t});\n\n\t\tproc.on(\"close\", (code) => {\n\t\t\tresolve({\n\t\t\t\tstdout,\n\t\t\t\tstderr,\n\t\t\t\tcode: code ?? 0,\n\t\t\t});\n\t\t});\n\n\t\tproc.on(\"error\", (err) => {\n\t\t\tresolve({\n\t\t\t\tstdout,\n\t\t\t\tstderr: stderr || err.message,\n\t\t\t\tcode: 1,\n\t\t\t});\n\t\t});\n\t});\n}\n\n/**\n * Create a no-op UI context for headless modes.\n */\nfunction createNoOpUIContext(): HookUIContext {\n\treturn {\n\t\tselect: async () => null,\n\t\tconfirm: async () => false,\n\t\tinput: async () => null,\n\t\tnotify: () => {},\n\t};\n}\n\n/**\n * Load a single tool module using jiti.\n */\nasync function loadTool(\n\ttoolPath: string,\n\tcwd: string,\n\tsharedApi: ToolAPI,\n): Promise<{ tools: LoadedCustomTool[] | null; error: string | null }> {\n\tconst resolvedPath = resolveToolPath(toolPath, cwd);\n\n\ttry {\n\t\t// Create jiti instance for TypeScript/ESM loading\n\t\t// Use aliases to resolve package imports since tools are loaded from user directories\n\t\t// (e.g. ~/.pi/agent/tools) but import from packages installed with pi-coding-agent\n\t\tconst jiti = createJiti(import.meta.url, {\n\t\t\talias: getAliases(),\n\t\t});\n\n\t\t// Import the module\n\t\tconst module = await jiti.import(resolvedPath, { default: true });\n\t\tconst factory = module as CustomToolFactory;\n\n\t\tif (typeof factory !== \"function\") {\n\t\t\treturn { tools: null, error: \"Tool must export a default function\" };\n\t\t}\n\n\t\t// Call factory with shared API\n\t\tconst result = await factory(sharedApi);\n\n\t\t// Handle single tool or array of tools\n\t\tconst toolsArray = Array.isArray(result) ? result : [result];\n\n\t\tconst loadedTools: LoadedCustomTool[] = toolsArray.map((tool) => ({\n\t\t\tpath: toolPath,\n\t\t\tresolvedPath,\n\t\t\ttool,\n\t\t}));\n\n\t\treturn { tools: loadedTools, error: null };\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\treturn { tools: null, error: `Failed to load tool: ${message}` };\n\t}\n}\n\n/**\n * Load all tools from configuration.\n * @param paths - Array of tool file paths\n * @param cwd - Current working directory for resolving relative paths\n * @param builtInToolNames - Names of built-in tools to check for conflicts\n */\nexport async function loadCustomTools(\n\tpaths: string[],\n\tcwd: string,\n\tbuiltInToolNames: string[],\n): Promise<CustomToolsLoadResult> {\n\tconst tools: LoadedCustomTool[] = [];\n\tconst errors: Array<{ path: string; error: string }> = [];\n\tconst seenNames = new Set<string>(builtInToolNames);\n\n\t// Shared API object - all tools get the same instance\n\tconst sharedApi: ToolAPI = {\n\t\tcwd,\n\t\texec: (command: string, args: string[]) => execCommand(command, args, cwd),\n\t\tui: createNoOpUIContext(),\n\t\thasUI: false,\n\t};\n\n\tfor (const toolPath of paths) {\n\t\tconst { tools: loadedTools, error } = await loadTool(toolPath, cwd, sharedApi);\n\n\t\tif (error) {\n\t\t\terrors.push({ path: toolPath, error });\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (loadedTools) {\n\t\t\tfor (const loadedTool of loadedTools) {\n\t\t\t\t// Check for name conflicts\n\t\t\t\tif (seenNames.has(loadedTool.tool.name)) {\n\t\t\t\t\terrors.push({\n\t\t\t\t\t\tpath: toolPath,\n\t\t\t\t\t\terror: `Tool name \"${loadedTool.tool.name}\" conflicts with existing tool`,\n\t\t\t\t\t});\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tseenNames.add(loadedTool.tool.name);\n\t\t\t\ttools.push(loadedTool);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn {\n\t\ttools,\n\t\terrors,\n\t\tsetUIContext(uiContext, hasUI) {\n\t\t\tsharedApi.ui = uiContext;\n\t\t\tsharedApi.hasUI = hasUI;\n\t\t},\n\t};\n}\n\n/**\n * Discover tool files from a directory.\n * Returns all .ts files (and symlinks to .ts files) in the directory (non-recursive).\n */\nfunction discoverToolsInDir(dir: string): string[] {\n\tif (!fs.existsSync(dir)) {\n\t\treturn [];\n\t}\n\n\ttry {\n\t\tconst entries = fs.readdirSync(dir, { withFileTypes: true });\n\t\treturn entries\n\t\t\t.filter((e) => (e.isFile() || e.isSymbolicLink()) && e.name.endsWith(\".ts\"))\n\t\t\t.map((e) => path.join(dir, e.name));\n\t} catch {\n\t\treturn [];\n\t}\n}\n\n/**\n * Discover and load tools from standard locations:\n * 1. ~/.pi/agent/tools/*.ts (global)\n * 2. cwd/.pi/tools/*.ts (project-local)\n *\n * Plus any explicitly configured paths from settings or CLI.\n *\n * @param configuredPaths - Explicit paths from settings.json and CLI --tool flags\n * @param cwd - Current working directory\n * @param builtInToolNames - Names of built-in tools to check for conflicts\n */\nexport async function discoverAndLoadCustomTools(\n\tconfiguredPaths: string[],\n\tcwd: string,\n\tbuiltInToolNames: string[],\n): Promise<CustomToolsLoadResult> {\n\tconst allPaths: string[] = [];\n\tconst seen = new Set<string>();\n\n\t// Helper to add paths without duplicates\n\tconst addPaths = (paths: string[]) => {\n\t\tfor (const p of paths) {\n\t\t\tconst resolved = path.resolve(p);\n\t\t\tif (!seen.has(resolved)) {\n\t\t\t\tseen.add(resolved);\n\t\t\t\tallPaths.push(p);\n\t\t\t}\n\t\t}\n\t};\n\n\t// 1. Global tools: ~/.pi/agent/tools/\n\tconst globalToolsDir = path.join(getAgentDir(), \"tools\");\n\taddPaths(discoverToolsInDir(globalToolsDir));\n\n\t// 2. Project-local tools: cwd/.pi/tools/\n\tconst localToolsDir = path.join(cwd, \".pi\", \"tools\");\n\taddPaths(discoverToolsInDir(localToolsDir));\n\n\t// 3. Explicitly configured paths (can override/add)\n\taddPaths(configuredPaths.map((p) => resolveToolPath(p, cwd)));\n\n\treturn loadCustomTools(allPaths, cwd, builtInToolNames);\n}\n"]}
1
+ {"version":3,"file":"loader.js","sourceRoot":"","sources":["../../../src/core/custom-tools/loader.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Custom tool loader - loads TypeScript tool modules using jiti.\n */\n\nimport { spawn } from \"node:child_process\";\nimport * as fs from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { createJiti } from \"jiti\";\nimport { getAgentDir } from \"../../config.js\";\nimport type { HookUIContext } from \"../hooks/types.js\";\nimport type {\n\tCustomToolFactory,\n\tCustomToolsLoadResult,\n\tExecOptions,\n\tExecResult,\n\tLoadedCustomTool,\n\tToolAPI,\n} from \"./types.js\";\n\n// Create require function to resolve module paths at runtime\nconst require = createRequire(import.meta.url);\n\n// Lazily computed aliases - resolved at runtime to handle global installs\nlet _aliases: Record<string, string> | null = null;\nfunction getAliases(): Record<string, string> {\n\tif (_aliases) return _aliases;\n\n\tconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\tconst packageIndex = path.resolve(__dirname, \"../..\", \"index.js\");\n\n\t_aliases = {\n\t\t\"@mariozechner/pi-coding-agent\": packageIndex,\n\t\t\"@mariozechner/pi-tui\": require.resolve(\"@mariozechner/pi-tui\"),\n\t\t\"@mariozechner/pi-ai\": require.resolve(\"@mariozechner/pi-ai\"),\n\t\t\"@sinclair/typebox\": require.resolve(\"@sinclair/typebox\"),\n\t};\n\treturn _aliases;\n}\n\nconst UNICODE_SPACES = /[\\u00A0\\u2000-\\u200A\\u202F\\u205F\\u3000]/g;\n\nfunction normalizeUnicodeSpaces(str: string): string {\n\treturn str.replace(UNICODE_SPACES, \" \");\n}\n\nfunction expandPath(p: string): string {\n\tconst normalized = normalizeUnicodeSpaces(p);\n\tif (normalized.startsWith(\"~/\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(2));\n\t}\n\tif (normalized.startsWith(\"~\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(1));\n\t}\n\treturn normalized;\n}\n\n/**\n * Resolve tool path.\n * - Absolute paths used as-is\n * - Paths starting with ~ expanded to home directory\n * - Relative paths resolved from cwd\n */\nfunction resolveToolPath(toolPath: string, cwd: string): string {\n\tconst expanded = expandPath(toolPath);\n\n\tif (path.isAbsolute(expanded)) {\n\t\treturn expanded;\n\t}\n\n\t// Relative paths resolved from cwd\n\treturn path.resolve(cwd, expanded);\n}\n\n/**\n * Execute a command and return stdout/stderr/code.\n * Supports cancellation via AbortSignal and timeout.\n */\nasync function execCommand(command: string, args: string[], cwd: string, options?: ExecOptions): Promise<ExecResult> {\n\treturn new Promise((resolve) => {\n\t\tconst proc = spawn(command, args, {\n\t\t\tcwd,\n\t\t\tshell: false,\n\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t});\n\n\t\tlet stdout = \"\";\n\t\tlet stderr = \"\";\n\t\tlet killed = false;\n\t\tlet timeoutId: NodeJS.Timeout | undefined;\n\n\t\tconst killProcess = () => {\n\t\t\tif (!killed) {\n\t\t\t\tkilled = true;\n\t\t\t\tproc.kill(\"SIGTERM\");\n\t\t\t\t// Force kill after 5 seconds if SIGTERM doesn't work\n\t\t\t\tsetTimeout(() => {\n\t\t\t\t\tif (!proc.killed) {\n\t\t\t\t\t\tproc.kill(\"SIGKILL\");\n\t\t\t\t\t}\n\t\t\t\t}, 5000);\n\t\t\t}\n\t\t};\n\n\t\t// Handle abort signal\n\t\tif (options?.signal) {\n\t\t\tif (options.signal.aborted) {\n\t\t\t\tkillProcess();\n\t\t\t} else {\n\t\t\t\toptions.signal.addEventListener(\"abort\", killProcess, { once: true });\n\t\t\t}\n\t\t}\n\n\t\t// Handle timeout\n\t\tif (options?.timeout && options.timeout > 0) {\n\t\t\ttimeoutId = setTimeout(() => {\n\t\t\t\tkillProcess();\n\t\t\t}, options.timeout);\n\t\t}\n\n\t\tproc.stdout.on(\"data\", (data) => {\n\t\t\tstdout += data.toString();\n\t\t});\n\n\t\tproc.stderr.on(\"data\", (data) => {\n\t\t\tstderr += data.toString();\n\t\t});\n\n\t\tproc.on(\"close\", (code) => {\n\t\t\tif (timeoutId) clearTimeout(timeoutId);\n\t\t\tif (options?.signal) {\n\t\t\t\toptions.signal.removeEventListener(\"abort\", killProcess);\n\t\t\t}\n\t\t\tresolve({\n\t\t\t\tstdout,\n\t\t\t\tstderr,\n\t\t\t\tcode: code ?? 0,\n\t\t\t\tkilled,\n\t\t\t});\n\t\t});\n\n\t\tproc.on(\"error\", (err) => {\n\t\t\tif (timeoutId) clearTimeout(timeoutId);\n\t\t\tif (options?.signal) {\n\t\t\t\toptions.signal.removeEventListener(\"abort\", killProcess);\n\t\t\t}\n\t\t\tresolve({\n\t\t\t\tstdout,\n\t\t\t\tstderr: stderr || err.message,\n\t\t\t\tcode: 1,\n\t\t\t\tkilled,\n\t\t\t});\n\t\t});\n\t});\n}\n\n/**\n * Create a no-op UI context for headless modes.\n */\nfunction createNoOpUIContext(): HookUIContext {\n\treturn {\n\t\tselect: async () => null,\n\t\tconfirm: async () => false,\n\t\tinput: async () => null,\n\t\tnotify: () => {},\n\t};\n}\n\n/**\n * Load a single tool module using jiti.\n */\nasync function loadTool(\n\ttoolPath: string,\n\tcwd: string,\n\tsharedApi: ToolAPI,\n): Promise<{ tools: LoadedCustomTool[] | null; error: string | null }> {\n\tconst resolvedPath = resolveToolPath(toolPath, cwd);\n\n\ttry {\n\t\t// Create jiti instance for TypeScript/ESM loading\n\t\t// Use aliases to resolve package imports since tools are loaded from user directories\n\t\t// (e.g. ~/.pi/agent/tools) but import from packages installed with pi-coding-agent\n\t\tconst jiti = createJiti(import.meta.url, {\n\t\t\talias: getAliases(),\n\t\t});\n\n\t\t// Import the module\n\t\tconst module = await jiti.import(resolvedPath, { default: true });\n\t\tconst factory = module as CustomToolFactory;\n\n\t\tif (typeof factory !== \"function\") {\n\t\t\treturn { tools: null, error: \"Tool must export a default function\" };\n\t\t}\n\n\t\t// Call factory with shared API\n\t\tconst result = await factory(sharedApi);\n\n\t\t// Handle single tool or array of tools\n\t\tconst toolsArray = Array.isArray(result) ? result : [result];\n\n\t\tconst loadedTools: LoadedCustomTool[] = toolsArray.map((tool) => ({\n\t\t\tpath: toolPath,\n\t\t\tresolvedPath,\n\t\t\ttool,\n\t\t}));\n\n\t\treturn { tools: loadedTools, error: null };\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\treturn { tools: null, error: `Failed to load tool: ${message}` };\n\t}\n}\n\n/**\n * Load all tools from configuration.\n * @param paths - Array of tool file paths\n * @param cwd - Current working directory for resolving relative paths\n * @param builtInToolNames - Names of built-in tools to check for conflicts\n */\nexport async function loadCustomTools(\n\tpaths: string[],\n\tcwd: string,\n\tbuiltInToolNames: string[],\n): Promise<CustomToolsLoadResult> {\n\tconst tools: LoadedCustomTool[] = [];\n\tconst errors: Array<{ path: string; error: string }> = [];\n\tconst seenNames = new Set<string>(builtInToolNames);\n\n\t// Shared API object - all tools get the same instance\n\tconst sharedApi: ToolAPI = {\n\t\tcwd,\n\t\texec: (command: string, args: string[], options?: ExecOptions) => execCommand(command, args, cwd, options),\n\t\tui: createNoOpUIContext(),\n\t\thasUI: false,\n\t};\n\n\tfor (const toolPath of paths) {\n\t\tconst { tools: loadedTools, error } = await loadTool(toolPath, cwd, sharedApi);\n\n\t\tif (error) {\n\t\t\terrors.push({ path: toolPath, error });\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (loadedTools) {\n\t\t\tfor (const loadedTool of loadedTools) {\n\t\t\t\t// Check for name conflicts\n\t\t\t\tif (seenNames.has(loadedTool.tool.name)) {\n\t\t\t\t\terrors.push({\n\t\t\t\t\t\tpath: toolPath,\n\t\t\t\t\t\terror: `Tool name \"${loadedTool.tool.name}\" conflicts with existing tool`,\n\t\t\t\t\t});\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tseenNames.add(loadedTool.tool.name);\n\t\t\t\ttools.push(loadedTool);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn {\n\t\ttools,\n\t\terrors,\n\t\tsetUIContext(uiContext, hasUI) {\n\t\t\tsharedApi.ui = uiContext;\n\t\t\tsharedApi.hasUI = hasUI;\n\t\t},\n\t};\n}\n\n/**\n * Discover tool files from a directory.\n * Only loads index.ts files from subdirectories (e.g., tools/mytool/index.ts).\n */\nfunction discoverToolsInDir(dir: string): string[] {\n\tif (!fs.existsSync(dir)) {\n\t\treturn [];\n\t}\n\n\tconst tools: string[] = [];\n\n\ttry {\n\t\tconst entries = fs.readdirSync(dir, { withFileTypes: true });\n\n\t\tfor (const entry of entries) {\n\t\t\tif (entry.isDirectory() || entry.isSymbolicLink()) {\n\t\t\t\t// Check for index.ts in subdirectory\n\t\t\t\tconst indexPath = path.join(dir, entry.name, \"index.ts\");\n\t\t\t\tif (fs.existsSync(indexPath)) {\n\t\t\t\t\ttools.push(indexPath);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} catch {\n\t\treturn [];\n\t}\n\n\treturn tools;\n}\n\n/**\n * Discover and load tools from standard locations:\n * 1. ~/.pi/agent/tools/*.ts (global)\n * 2. cwd/.pi/tools/*.ts (project-local)\n *\n * Plus any explicitly configured paths from settings or CLI.\n *\n * @param configuredPaths - Explicit paths from settings.json and CLI --tool flags\n * @param cwd - Current working directory\n * @param builtInToolNames - Names of built-in tools to check for conflicts\n */\nexport async function discoverAndLoadCustomTools(\n\tconfiguredPaths: string[],\n\tcwd: string,\n\tbuiltInToolNames: string[],\n): Promise<CustomToolsLoadResult> {\n\tconst allPaths: string[] = [];\n\tconst seen = new Set<string>();\n\n\t// Helper to add paths without duplicates\n\tconst addPaths = (paths: string[]) => {\n\t\tfor (const p of paths) {\n\t\t\tconst resolved = path.resolve(p);\n\t\t\tif (!seen.has(resolved)) {\n\t\t\t\tseen.add(resolved);\n\t\t\t\tallPaths.push(p);\n\t\t\t}\n\t\t}\n\t};\n\n\t// 1. Global tools: ~/.pi/agent/tools/\n\tconst globalToolsDir = path.join(getAgentDir(), \"tools\");\n\taddPaths(discoverToolsInDir(globalToolsDir));\n\n\t// 2. Project-local tools: cwd/.pi/tools/\n\tconst localToolsDir = path.join(cwd, \".pi\", \"tools\");\n\taddPaths(discoverToolsInDir(localToolsDir));\n\n\t// 3. Explicitly configured paths (can override/add)\n\taddPaths(configuredPaths.map((p) => resolveToolPath(p, cwd)));\n\n\treturn loadCustomTools(allPaths, cwd, builtInToolNames);\n}\n"]}
@@ -16,13 +16,21 @@ export interface ExecResult {
16
16
  stdout: string;
17
17
  stderr: string;
18
18
  code: number;
19
+ /** True if the process was killed due to signal or timeout */
20
+ killed?: boolean;
21
+ }
22
+ export interface ExecOptions {
23
+ /** AbortSignal to cancel the process */
24
+ signal?: AbortSignal;
25
+ /** Timeout in milliseconds */
26
+ timeout?: number;
19
27
  }
20
28
  /** API passed to custom tool factory (stable across session changes) */
21
29
  export interface ToolAPI {
22
30
  /** Current working directory */
23
31
  cwd: string;
24
32
  /** Execute a command */
25
- exec(command: string, args: string[]): Promise<ExecResult>;
33
+ exec(command: string, args: string[], options?: ExecOptions): Promise<ExecResult>;
26
34
  /** UI methods for user interaction (select, confirm, input, notify) */
27
35
  ui: ToolUIContext;
28
36
  /** Whether UI is available (false in print/RPC mode) */
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/core/custom-tools/types.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Custom tool types.\n *\n * Custom tools are TypeScript modules that define additional tools for the agent.\n * They can provide custom rendering for tool calls and results in the TUI.\n */\n\nimport type { AgentTool, AgentToolResult } from \"@mariozechner/pi-ai\";\nimport type { Component } from \"@mariozechner/pi-tui\";\nimport type { Static, TSchema } from \"@sinclair/typebox\";\nimport type { Theme } from \"../../modes/interactive/theme/theme.js\";\nimport type { HookUIContext } from \"../hooks/types.js\";\nimport type { SessionEntry } from \"../session-manager.js\";\n\n/** Alias for clarity */\nexport type ToolUIContext = HookUIContext;\n\nexport interface ExecResult {\n\tstdout: string;\n\tstderr: string;\n\tcode: number;\n}\n\n/** API passed to custom tool factory (stable across session changes) */\nexport interface ToolAPI {\n\t/** Current working directory */\n\tcwd: string;\n\t/** Execute a command */\n\texec(command: string, args: string[]): Promise<ExecResult>;\n\t/** UI methods for user interaction (select, confirm, input, notify) */\n\tui: ToolUIContext;\n\t/** Whether UI is available (false in print/RPC mode) */\n\thasUI: boolean;\n}\n\n/** Session event passed to onSession callback */\nexport interface SessionEvent {\n\t/** All session entries (including pre-compaction history) */\n\tentries: SessionEntry[];\n\t/** Current session file path, or null in --no-session mode */\n\tsessionFile: string | null;\n\t/** Previous session file path, or null for \"start\" and \"clear\" */\n\tpreviousSessionFile: string | null;\n\t/** Reason for the session event */\n\treason: \"start\" | \"switch\" | \"branch\" | \"clear\";\n}\n\n/** Rendering options passed to renderResult */\nexport interface RenderResultOptions {\n\t/** Whether the result view is expanded */\n\texpanded: boolean;\n\t/** Whether this is a partial/streaming result */\n\tisPartial: boolean;\n}\n\n/** Custom tool with optional lifecycle and rendering methods */\nexport interface CustomAgentTool<TParams extends TSchema = TSchema, TDetails = any>\n\textends AgentTool<TParams, TDetails> {\n\t/** Called on session start/switch/branch/clear - use to reconstruct state from entries */\n\tonSession?: (event: SessionEvent) => void | Promise<void>;\n\t/** Custom rendering for tool call display - return a Component */\n\trenderCall?: (args: Static<TParams>, theme: Theme) => Component;\n\t/** Custom rendering for tool result display - return a Component */\n\trenderResult?: (result: AgentToolResult<TDetails>, options: RenderResultOptions, theme: Theme) => Component;\n\t/** Called when session ends - cleanup resources */\n\tdispose?: () => Promise<void> | void;\n}\n\n/** Factory function that creates a custom tool or array of tools */\nexport type CustomToolFactory = (\n\tpi: ToolAPI,\n) => CustomAgentTool<any> | CustomAgentTool[] | Promise<CustomAgentTool | CustomAgentTool[]>;\n\n/** Loaded custom tool with metadata */\nexport interface LoadedCustomTool {\n\t/** Original path (as specified) */\n\tpath: string;\n\t/** Resolved absolute path */\n\tresolvedPath: string;\n\t/** The tool instance */\n\ttool: CustomAgentTool;\n}\n\n/** Result from loading custom tools */\nexport interface CustomToolsLoadResult {\n\ttools: LoadedCustomTool[];\n\terrors: Array<{ path: string; error: string }>;\n\t/** Update the UI context for all loaded tools. Call when mode initializes. */\n\tsetUIContext(uiContext: ToolUIContext, hasUI: boolean): void;\n}\n"]}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/core/custom-tools/types.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Custom tool types.\n *\n * Custom tools are TypeScript modules that define additional tools for the agent.\n * They can provide custom rendering for tool calls and results in the TUI.\n */\n\nimport type { AgentTool, AgentToolResult } from \"@mariozechner/pi-ai\";\nimport type { Component } from \"@mariozechner/pi-tui\";\nimport type { Static, TSchema } from \"@sinclair/typebox\";\nimport type { Theme } from \"../../modes/interactive/theme/theme.js\";\nimport type { HookUIContext } from \"../hooks/types.js\";\nimport type { SessionEntry } from \"../session-manager.js\";\n\n/** Alias for clarity */\nexport type ToolUIContext = HookUIContext;\n\nexport interface ExecResult {\n\tstdout: string;\n\tstderr: string;\n\tcode: number;\n\t/** True if the process was killed due to signal or timeout */\n\tkilled?: boolean;\n}\n\nexport interface ExecOptions {\n\t/** AbortSignal to cancel the process */\n\tsignal?: AbortSignal;\n\t/** Timeout in milliseconds */\n\ttimeout?: number;\n}\n\n/** API passed to custom tool factory (stable across session changes) */\nexport interface ToolAPI {\n\t/** Current working directory */\n\tcwd: string;\n\t/** Execute a command */\n\texec(command: string, args: string[], options?: ExecOptions): Promise<ExecResult>;\n\t/** UI methods for user interaction (select, confirm, input, notify) */\n\tui: ToolUIContext;\n\t/** Whether UI is available (false in print/RPC mode) */\n\thasUI: boolean;\n}\n\n/** Session event passed to onSession callback */\nexport interface SessionEvent {\n\t/** All session entries (including pre-compaction history) */\n\tentries: SessionEntry[];\n\t/** Current session file path, or null in --no-session mode */\n\tsessionFile: string | null;\n\t/** Previous session file path, or null for \"start\" and \"clear\" */\n\tpreviousSessionFile: string | null;\n\t/** Reason for the session event */\n\treason: \"start\" | \"switch\" | \"branch\" | \"clear\";\n}\n\n/** Rendering options passed to renderResult */\nexport interface RenderResultOptions {\n\t/** Whether the result view is expanded */\n\texpanded: boolean;\n\t/** Whether this is a partial/streaming result */\n\tisPartial: boolean;\n}\n\n/** Custom tool with optional lifecycle and rendering methods */\nexport interface CustomAgentTool<TParams extends TSchema = TSchema, TDetails = any>\n\textends AgentTool<TParams, TDetails> {\n\t/** Called on session start/switch/branch/clear - use to reconstruct state from entries */\n\tonSession?: (event: SessionEvent) => void | Promise<void>;\n\t/** Custom rendering for tool call display - return a Component */\n\trenderCall?: (args: Static<TParams>, theme: Theme) => Component;\n\t/** Custom rendering for tool result display - return a Component */\n\trenderResult?: (result: AgentToolResult<TDetails>, options: RenderResultOptions, theme: Theme) => Component;\n\t/** Called when session ends - cleanup resources */\n\tdispose?: () => Promise<void> | void;\n}\n\n/** Factory function that creates a custom tool or array of tools */\nexport type CustomToolFactory = (\n\tpi: ToolAPI,\n) => CustomAgentTool<any> | CustomAgentTool[] | Promise<CustomAgentTool | CustomAgentTool[]>;\n\n/** Loaded custom tool with metadata */\nexport interface LoadedCustomTool {\n\t/** Original path (as specified) */\n\tpath: string;\n\t/** Resolved absolute path */\n\tresolvedPath: string;\n\t/** The tool instance */\n\ttool: CustomAgentTool;\n}\n\n/** Result from loading custom tools */\nexport interface CustomToolsLoadResult {\n\ttools: LoadedCustomTool[];\n\terrors: Array<{ path: string; error: string }>;\n\t/** Update the UI context for all loaded tools. Call when mode initializes. */\n\tsetUIContext(uiContext: ToolUIContext, hasUI: boolean): void;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/core/custom-tools/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG","sourcesContent":["/**\n * Custom tool types.\n *\n * Custom tools are TypeScript modules that define additional tools for the agent.\n * They can provide custom rendering for tool calls and results in the TUI.\n */\n\nimport type { AgentTool, AgentToolResult } from \"@mariozechner/pi-ai\";\nimport type { Component } from \"@mariozechner/pi-tui\";\nimport type { Static, TSchema } from \"@sinclair/typebox\";\nimport type { Theme } from \"../../modes/interactive/theme/theme.js\";\nimport type { HookUIContext } from \"../hooks/types.js\";\nimport type { SessionEntry } from \"../session-manager.js\";\n\n/** Alias for clarity */\nexport type ToolUIContext = HookUIContext;\n\nexport interface ExecResult {\n\tstdout: string;\n\tstderr: string;\n\tcode: number;\n}\n\n/** API passed to custom tool factory (stable across session changes) */\nexport interface ToolAPI {\n\t/** Current working directory */\n\tcwd: string;\n\t/** Execute a command */\n\texec(command: string, args: string[]): Promise<ExecResult>;\n\t/** UI methods for user interaction (select, confirm, input, notify) */\n\tui: ToolUIContext;\n\t/** Whether UI is available (false in print/RPC mode) */\n\thasUI: boolean;\n}\n\n/** Session event passed to onSession callback */\nexport interface SessionEvent {\n\t/** All session entries (including pre-compaction history) */\n\tentries: SessionEntry[];\n\t/** Current session file path, or null in --no-session mode */\n\tsessionFile: string | null;\n\t/** Previous session file path, or null for \"start\" and \"clear\" */\n\tpreviousSessionFile: string | null;\n\t/** Reason for the session event */\n\treason: \"start\" | \"switch\" | \"branch\" | \"clear\";\n}\n\n/** Rendering options passed to renderResult */\nexport interface RenderResultOptions {\n\t/** Whether the result view is expanded */\n\texpanded: boolean;\n\t/** Whether this is a partial/streaming result */\n\tisPartial: boolean;\n}\n\n/** Custom tool with optional lifecycle and rendering methods */\nexport interface CustomAgentTool<TParams extends TSchema = TSchema, TDetails = any>\n\textends AgentTool<TParams, TDetails> {\n\t/** Called on session start/switch/branch/clear - use to reconstruct state from entries */\n\tonSession?: (event: SessionEvent) => void | Promise<void>;\n\t/** Custom rendering for tool call display - return a Component */\n\trenderCall?: (args: Static<TParams>, theme: Theme) => Component;\n\t/** Custom rendering for tool result display - return a Component */\n\trenderResult?: (result: AgentToolResult<TDetails>, options: RenderResultOptions, theme: Theme) => Component;\n\t/** Called when session ends - cleanup resources */\n\tdispose?: () => Promise<void> | void;\n}\n\n/** Factory function that creates a custom tool or array of tools */\nexport type CustomToolFactory = (\n\tpi: ToolAPI,\n) => CustomAgentTool<any> | CustomAgentTool[] | Promise<CustomAgentTool | CustomAgentTool[]>;\n\n/** Loaded custom tool with metadata */\nexport interface LoadedCustomTool {\n\t/** Original path (as specified) */\n\tpath: string;\n\t/** Resolved absolute path */\n\tresolvedPath: string;\n\t/** The tool instance */\n\ttool: CustomAgentTool;\n}\n\n/** Result from loading custom tools */\nexport interface CustomToolsLoadResult {\n\ttools: LoadedCustomTool[];\n\terrors: Array<{ path: string; error: string }>;\n\t/** Update the UI context for all loaded tools. Call when mode initializes. */\n\tsetUIContext(uiContext: ToolUIContext, hasUI: boolean): void;\n}\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/core/custom-tools/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG","sourcesContent":["/**\n * Custom tool types.\n *\n * Custom tools are TypeScript modules that define additional tools for the agent.\n * They can provide custom rendering for tool calls and results in the TUI.\n */\n\nimport type { AgentTool, AgentToolResult } from \"@mariozechner/pi-ai\";\nimport type { Component } from \"@mariozechner/pi-tui\";\nimport type { Static, TSchema } from \"@sinclair/typebox\";\nimport type { Theme } from \"../../modes/interactive/theme/theme.js\";\nimport type { HookUIContext } from \"../hooks/types.js\";\nimport type { SessionEntry } from \"../session-manager.js\";\n\n/** Alias for clarity */\nexport type ToolUIContext = HookUIContext;\n\nexport interface ExecResult {\n\tstdout: string;\n\tstderr: string;\n\tcode: number;\n\t/** True if the process was killed due to signal or timeout */\n\tkilled?: boolean;\n}\n\nexport interface ExecOptions {\n\t/** AbortSignal to cancel the process */\n\tsignal?: AbortSignal;\n\t/** Timeout in milliseconds */\n\ttimeout?: number;\n}\n\n/** API passed to custom tool factory (stable across session changes) */\nexport interface ToolAPI {\n\t/** Current working directory */\n\tcwd: string;\n\t/** Execute a command */\n\texec(command: string, args: string[], options?: ExecOptions): Promise<ExecResult>;\n\t/** UI methods for user interaction (select, confirm, input, notify) */\n\tui: ToolUIContext;\n\t/** Whether UI is available (false in print/RPC mode) */\n\thasUI: boolean;\n}\n\n/** Session event passed to onSession callback */\nexport interface SessionEvent {\n\t/** All session entries (including pre-compaction history) */\n\tentries: SessionEntry[];\n\t/** Current session file path, or null in --no-session mode */\n\tsessionFile: string | null;\n\t/** Previous session file path, or null for \"start\" and \"clear\" */\n\tpreviousSessionFile: string | null;\n\t/** Reason for the session event */\n\treason: \"start\" | \"switch\" | \"branch\" | \"clear\";\n}\n\n/** Rendering options passed to renderResult */\nexport interface RenderResultOptions {\n\t/** Whether the result view is expanded */\n\texpanded: boolean;\n\t/** Whether this is a partial/streaming result */\n\tisPartial: boolean;\n}\n\n/** Custom tool with optional lifecycle and rendering methods */\nexport interface CustomAgentTool<TParams extends TSchema = TSchema, TDetails = any>\n\textends AgentTool<TParams, TDetails> {\n\t/** Called on session start/switch/branch/clear - use to reconstruct state from entries */\n\tonSession?: (event: SessionEvent) => void | Promise<void>;\n\t/** Custom rendering for tool call display - return a Component */\n\trenderCall?: (args: Static<TParams>, theme: Theme) => Component;\n\t/** Custom rendering for tool result display - return a Component */\n\trenderResult?: (result: AgentToolResult<TDetails>, options: RenderResultOptions, theme: Theme) => Component;\n\t/** Called when session ends - cleanup resources */\n\tdispose?: () => Promise<void> | void;\n}\n\n/** Factory function that creates a custom tool or array of tools */\nexport type CustomToolFactory = (\n\tpi: ToolAPI,\n) => CustomAgentTool<any> | CustomAgentTool[] | Promise<CustomAgentTool | CustomAgentTool[]>;\n\n/** Loaded custom tool with metadata */\nexport interface LoadedCustomTool {\n\t/** Original path (as specified) */\n\tpath: string;\n\t/** Resolved absolute path */\n\tresolvedPath: string;\n\t/** The tool instance */\n\ttool: CustomAgentTool;\n}\n\n/** Result from loading custom tools */\nexport interface CustomToolsLoadResult {\n\ttools: LoadedCustomTool[];\n\terrors: Array<{ path: string; error: string }>;\n\t/** Update the UI context for all loaded tools. Call when mode initializes. */\n\tsetUIContext(uiContext: ToolUIContext, hasUI: boolean): void;\n}\n"]}
@@ -1,5 +1,6 @@
1
1
  export { discoverAndLoadHooks, type LoadedHook, type LoadHooksResult, loadHooks, type SendHandler } from "./loader.js";
2
2
  export { type HookErrorListener, HookRunner } from "./runner.js";
3
3
  export { wrapToolsWithHooks, wrapToolWithHooks } from "./tool-wrapper.js";
4
- export type { AgentEndEvent, AgentStartEvent, BranchEvent, BranchEventResult, ExecResult, HookAPI, HookError, HookEvent, HookEventContext, HookFactory, HookUIContext, SessionEvent, ToolCallEvent, ToolCallEventResult, ToolResultEvent, ToolResultEventResult, TurnEndEvent, TurnStartEvent, } from "./types.js";
4
+ export type { AgentEndEvent, AgentStartEvent, BashToolResultEvent, BranchEvent, BranchEventResult, CustomToolResultEvent, EditToolResultEvent, ExecResult, FindToolResultEvent, GrepToolResultEvent, HookAPI, HookError, HookEvent, HookEventContext, HookFactory, HookUIContext, LsToolResultEvent, ReadToolResultEvent, SessionEvent, ToolCallEvent, ToolCallEventResult, ToolResultEvent, ToolResultEventResult, TurnEndEvent, TurnStartEvent, WriteToolResultEvent, } from "./types.js";
5
+ export { isBashToolResult, isEditToolResult, isFindToolResult, isGrepToolResult, isLsToolResult, isReadToolResult, isWriteToolResult, } from "./types.js";
5
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/hooks/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,KAAK,UAAU,EAAE,KAAK,eAAe,EAAE,SAAS,EAAE,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AACvH,OAAO,EAAE,KAAK,iBAAiB,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAC1E,YAAY,EACX,aAAa,EACb,eAAe,EACf,WAAW,EACX,iBAAiB,EACjB,UAAU,EACV,OAAO,EACP,SAAS,EACT,SAAS,EACT,gBAAgB,EAChB,WAAW,EACX,aAAa,EACb,YAAY,EACZ,aAAa,EACb,mBAAmB,EACnB,eAAe,EACf,qBAAqB,EACrB,YAAY,EACZ,cAAc,GACd,MAAM,YAAY,CAAC","sourcesContent":["export { discoverAndLoadHooks, type LoadedHook, type LoadHooksResult, loadHooks, type SendHandler } from \"./loader.js\";\nexport { type HookErrorListener, HookRunner } from \"./runner.js\";\nexport { wrapToolsWithHooks, wrapToolWithHooks } from \"./tool-wrapper.js\";\nexport type {\n\tAgentEndEvent,\n\tAgentStartEvent,\n\tBranchEvent,\n\tBranchEventResult,\n\tExecResult,\n\tHookAPI,\n\tHookError,\n\tHookEvent,\n\tHookEventContext,\n\tHookFactory,\n\tHookUIContext,\n\tSessionEvent,\n\tToolCallEvent,\n\tToolCallEventResult,\n\tToolResultEvent,\n\tToolResultEventResult,\n\tTurnEndEvent,\n\tTurnStartEvent,\n} from \"./types.js\";\n"]}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/hooks/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,KAAK,UAAU,EAAE,KAAK,eAAe,EAAE,SAAS,EAAE,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AACvH,OAAO,EAAE,KAAK,iBAAiB,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAC1E,YAAY,EACX,aAAa,EACb,eAAe,EACf,mBAAmB,EACnB,WAAW,EACX,iBAAiB,EACjB,qBAAqB,EACrB,mBAAmB,EACnB,UAAU,EACV,mBAAmB,EACnB,mBAAmB,EACnB,OAAO,EACP,SAAS,EACT,SAAS,EACT,gBAAgB,EAChB,WAAW,EACX,aAAa,EACb,iBAAiB,EACjB,mBAAmB,EACnB,YAAY,EACZ,aAAa,EACb,mBAAmB,EACnB,eAAe,EACf,qBAAqB,EACrB,YAAY,EACZ,cAAc,EACd,oBAAoB,GACpB,MAAM,YAAY,CAAC;AACpB,OAAO,EACN,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,cAAc,EACd,gBAAgB,EAChB,iBAAiB,GACjB,MAAM,YAAY,CAAC","sourcesContent":["export { discoverAndLoadHooks, type LoadedHook, type LoadHooksResult, loadHooks, type SendHandler } from \"./loader.js\";\nexport { type HookErrorListener, HookRunner } from \"./runner.js\";\nexport { wrapToolsWithHooks, wrapToolWithHooks } from \"./tool-wrapper.js\";\nexport type {\n\tAgentEndEvent,\n\tAgentStartEvent,\n\tBashToolResultEvent,\n\tBranchEvent,\n\tBranchEventResult,\n\tCustomToolResultEvent,\n\tEditToolResultEvent,\n\tExecResult,\n\tFindToolResultEvent,\n\tGrepToolResultEvent,\n\tHookAPI,\n\tHookError,\n\tHookEvent,\n\tHookEventContext,\n\tHookFactory,\n\tHookUIContext,\n\tLsToolResultEvent,\n\tReadToolResultEvent,\n\tSessionEvent,\n\tToolCallEvent,\n\tToolCallEventResult,\n\tToolResultEvent,\n\tToolResultEventResult,\n\tTurnEndEvent,\n\tTurnStartEvent,\n\tWriteToolResultEvent,\n} from \"./types.js\";\nexport {\n\tisBashToolResult,\n\tisEditToolResult,\n\tisFindToolResult,\n\tisGrepToolResult,\n\tisLsToolResult,\n\tisReadToolResult,\n\tisWriteToolResult,\n} from \"./types.js\";\n"]}
@@ -1,4 +1,5 @@
1
1
  export { discoverAndLoadHooks, loadHooks } from "./loader.js";
2
2
  export { HookRunner } from "./runner.js";
3
3
  export { wrapToolsWithHooks, wrapToolWithHooks } from "./tool-wrapper.js";
4
+ export { isBashToolResult, isEditToolResult, isFindToolResult, isGrepToolResult, isLsToolResult, isReadToolResult, isWriteToolResult, } from "./types.js";
4
5
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/core/hooks/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAyC,SAAS,EAAoB,MAAM,aAAa,CAAC;AACvH,OAAO,EAA0B,UAAU,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC","sourcesContent":["export { discoverAndLoadHooks, type LoadedHook, type LoadHooksResult, loadHooks, type SendHandler } from \"./loader.js\";\nexport { type HookErrorListener, HookRunner } from \"./runner.js\";\nexport { wrapToolsWithHooks, wrapToolWithHooks } from \"./tool-wrapper.js\";\nexport type {\n\tAgentEndEvent,\n\tAgentStartEvent,\n\tBranchEvent,\n\tBranchEventResult,\n\tExecResult,\n\tHookAPI,\n\tHookError,\n\tHookEvent,\n\tHookEventContext,\n\tHookFactory,\n\tHookUIContext,\n\tSessionEvent,\n\tToolCallEvent,\n\tToolCallEventResult,\n\tToolResultEvent,\n\tToolResultEventResult,\n\tTurnEndEvent,\n\tTurnStartEvent,\n} from \"./types.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/core/hooks/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAyC,SAAS,EAAoB,MAAM,aAAa,CAAC;AACvH,OAAO,EAA0B,UAAU,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AA6B1E,OAAO,EACN,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,cAAc,EACd,gBAAgB,EAChB,iBAAiB,GACjB,MAAM,YAAY,CAAC","sourcesContent":["export { discoverAndLoadHooks, type LoadedHook, type LoadHooksResult, loadHooks, type SendHandler } from \"./loader.js\";\nexport { type HookErrorListener, HookRunner } from \"./runner.js\";\nexport { wrapToolsWithHooks, wrapToolWithHooks } from \"./tool-wrapper.js\";\nexport type {\n\tAgentEndEvent,\n\tAgentStartEvent,\n\tBashToolResultEvent,\n\tBranchEvent,\n\tBranchEventResult,\n\tCustomToolResultEvent,\n\tEditToolResultEvent,\n\tExecResult,\n\tFindToolResultEvent,\n\tGrepToolResultEvent,\n\tHookAPI,\n\tHookError,\n\tHookEvent,\n\tHookEventContext,\n\tHookFactory,\n\tHookUIContext,\n\tLsToolResultEvent,\n\tReadToolResultEvent,\n\tSessionEvent,\n\tToolCallEvent,\n\tToolCallEventResult,\n\tToolResultEvent,\n\tToolResultEventResult,\n\tTurnEndEvent,\n\tTurnStartEvent,\n\tWriteToolResultEvent,\n} from \"./types.js\";\nexport {\n\tisBashToolResult,\n\tisEditToolResult,\n\tisFindToolResult,\n\tisGrepToolResult,\n\tisLsToolResult,\n\tisReadToolResult,\n\tisWriteToolResult,\n} from \"./types.js\";\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../../src/core/hooks/runner.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC3D,OAAO,KAAK,EACX,iBAAiB,EAEjB,SAAS,EACT,SAAS,EAET,aAAa,EACb,aAAa,EACb,mBAAmB,EACnB,qBAAqB,EACrB,MAAM,YAAY,CAAC;AAOpB;;GAEG;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;AAoD3D;;GAEG;AACH,qBAAa,UAAU;IACtB,OAAO,CAAC,KAAK,CAAe;IAC5B,OAAO,CAAC,SAAS,CAAgB;IACjC,OAAO,CAAC,KAAK,CAAU;IACvB,OAAO,CAAC,GAAG,CAAS;IACpB,OAAO,CAAC,WAAW,CAAgB;IACnC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,cAAc,CAAqC;IAE3D,YAAY,KAAK,EAAE,UAAU,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,MAAwB,EAO9E;IAED;;;OAGG;IACH,YAAY,CAAC,SAAS,EAAE,aAAa,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAG3D;IAED;;OAEG;IACH,YAAY,IAAI,MAAM,EAAE,CAEvB;IAED;;OAEG;IACH,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAE/C;IAED;;;OAGG;IACH,cAAc,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,CAIzC;IAED;;;OAGG;IACH,OAAO,CAAC,QAAQ,EAAE,iBAAiB,GAAG,MAAM,IAAI,CAG/C;IAED;;OAEG;IACH,OAAO,CAAC,SAAS;IAMjB;;OAEG;IACH,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAQtC;IAED;;OAEG;IACH,OAAO,CAAC,aAAa;IAUrB;;;OAGG;IACG,IAAI,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,iBAAiB,GAAG,qBAAqB,GAAG,SAAS,CAAC,CAmC3F;IAED;;;;OAIG;IACG,YAAY,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC,CAuBjF;CACD","sourcesContent":["/**\n * Hook runner - executes hooks and manages their lifecycle.\n */\n\nimport { spawn } from \"node:child_process\";\nimport type { LoadedHook, SendHandler } from \"./loader.js\";\nimport type {\n\tBranchEventResult,\n\tExecResult,\n\tHookError,\n\tHookEvent,\n\tHookEventContext,\n\tHookUIContext,\n\tToolCallEvent,\n\tToolCallEventResult,\n\tToolResultEventResult,\n} from \"./types.js\";\n\n/**\n * Default timeout for hook execution (30 seconds).\n */\nconst DEFAULT_TIMEOUT = 30000;\n\n/**\n * Listener for hook errors.\n */\nexport type HookErrorListener = (error: HookError) => void;\n\n/**\n * Execute a command and return stdout/stderr/code.\n */\nasync function exec(command: string, args: string[], cwd: string): Promise<ExecResult> {\n\treturn new Promise((resolve) => {\n\t\tconst proc = spawn(command, args, { cwd, shell: false });\n\n\t\tlet stdout = \"\";\n\t\tlet stderr = \"\";\n\n\t\tproc.stdout?.on(\"data\", (data) => {\n\t\t\tstdout += data.toString();\n\t\t});\n\n\t\tproc.stderr?.on(\"data\", (data) => {\n\t\t\tstderr += data.toString();\n\t\t});\n\n\t\tproc.on(\"close\", (code) => {\n\t\t\tresolve({ stdout, stderr, code: code ?? 0 });\n\t\t});\n\n\t\tproc.on(\"error\", (_err) => {\n\t\t\tresolve({ stdout, stderr, code: 1 });\n\t\t});\n\t});\n}\n\n/**\n * Create a promise that rejects after a timeout.\n */\nfunction createTimeout(ms: number): { promise: Promise<never>; clear: () => void } {\n\tlet timeoutId: NodeJS.Timeout;\n\tconst promise = new Promise<never>((_, reject) => {\n\t\ttimeoutId = setTimeout(() => reject(new Error(`Hook timed out after ${ms}ms`)), ms);\n\t});\n\treturn {\n\t\tpromise,\n\t\tclear: () => clearTimeout(timeoutId),\n\t};\n}\n\n/** No-op UI context used when no UI is available */\nconst noOpUIContext: HookUIContext = {\n\tselect: async () => null,\n\tconfirm: async () => false,\n\tinput: async () => null,\n\tnotify: () => {},\n};\n\n/**\n * HookRunner executes hooks and manages event emission.\n */\nexport class HookRunner {\n\tprivate hooks: LoadedHook[];\n\tprivate uiContext: HookUIContext;\n\tprivate hasUI: boolean;\n\tprivate cwd: string;\n\tprivate sessionFile: string | null;\n\tprivate timeout: number;\n\tprivate errorListeners: Set<HookErrorListener> = new Set();\n\n\tconstructor(hooks: LoadedHook[], cwd: string, timeout: number = DEFAULT_TIMEOUT) {\n\t\tthis.hooks = hooks;\n\t\tthis.uiContext = noOpUIContext;\n\t\tthis.hasUI = false;\n\t\tthis.cwd = cwd;\n\t\tthis.sessionFile = null;\n\t\tthis.timeout = timeout;\n\t}\n\n\t/**\n\t * Set the UI context for hooks.\n\t * Call this when the mode initializes and UI is available.\n\t */\n\tsetUIContext(uiContext: HookUIContext, hasUI: boolean): void {\n\t\tthis.uiContext = uiContext;\n\t\tthis.hasUI = hasUI;\n\t}\n\n\t/**\n\t * Get the paths of all loaded hooks.\n\t */\n\tgetHookPaths(): string[] {\n\t\treturn this.hooks.map((h) => h.path);\n\t}\n\n\t/**\n\t * Set the session file path.\n\t */\n\tsetSessionFile(sessionFile: string | null): void {\n\t\tthis.sessionFile = sessionFile;\n\t}\n\n\t/**\n\t * Set the send handler for all hooks' pi.send().\n\t * Call this when the mode initializes.\n\t */\n\tsetSendHandler(handler: SendHandler): void {\n\t\tfor (const hook of this.hooks) {\n\t\t\thook.setSendHandler(handler);\n\t\t}\n\t}\n\n\t/**\n\t * Subscribe to hook errors.\n\t * @returns Unsubscribe function\n\t */\n\tonError(listener: HookErrorListener): () => void {\n\t\tthis.errorListeners.add(listener);\n\t\treturn () => this.errorListeners.delete(listener);\n\t}\n\n\t/**\n\t * Emit an error to all listeners.\n\t */\n\tprivate emitError(error: HookError): void {\n\t\tfor (const listener of this.errorListeners) {\n\t\t\tlistener(error);\n\t\t}\n\t}\n\n\t/**\n\t * Check if any hooks have handlers for the given event type.\n\t */\n\thasHandlers(eventType: string): boolean {\n\t\tfor (const hook of this.hooks) {\n\t\t\tconst handlers = hook.handlers.get(eventType);\n\t\t\tif (handlers && handlers.length > 0) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * Create the event context for handlers.\n\t */\n\tprivate createContext(): HookEventContext {\n\t\treturn {\n\t\t\texec: (command: string, args: string[]) => exec(command, args, this.cwd),\n\t\t\tui: this.uiContext,\n\t\t\thasUI: this.hasUI,\n\t\t\tcwd: this.cwd,\n\t\t\tsessionFile: this.sessionFile,\n\t\t};\n\t}\n\n\t/**\n\t * Emit an event to all hooks.\n\t * Returns the result from branch/tool_result events (if any handler returns one).\n\t */\n\tasync emit(event: HookEvent): Promise<BranchEventResult | ToolResultEventResult | undefined> {\n\t\tconst ctx = this.createContext();\n\t\tlet result: BranchEventResult | ToolResultEventResult | undefined;\n\n\t\tfor (const hook of this.hooks) {\n\t\t\tconst handlers = hook.handlers.get(event.type);\n\t\t\tif (!handlers || handlers.length === 0) continue;\n\n\t\t\tfor (const handler of handlers) {\n\t\t\t\ttry {\n\t\t\t\t\tconst timeout = createTimeout(this.timeout);\n\t\t\t\t\tconst handlerResult = await Promise.race([handler(event, ctx), timeout.promise]);\n\t\t\t\t\ttimeout.clear();\n\n\t\t\t\t\t// For branch events, capture the result\n\t\t\t\t\tif (event.type === \"branch\" && handlerResult) {\n\t\t\t\t\t\tresult = handlerResult as BranchEventResult;\n\t\t\t\t\t}\n\n\t\t\t\t\t// For tool_result events, capture the result\n\t\t\t\t\tif (event.type === \"tool_result\" && handlerResult) {\n\t\t\t\t\t\tresult = handlerResult as ToolResultEventResult;\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\t\t\t\tthis.emitError({\n\t\t\t\t\t\thookPath: hook.path,\n\t\t\t\t\t\tevent: event.type,\n\t\t\t\t\t\terror: message,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Emit a tool_call event to all hooks.\n\t * No timeout - user prompts can take as long as needed.\n\t * Errors are thrown (not swallowed) so caller can block on failure.\n\t */\n\tasync emitToolCall(event: ToolCallEvent): Promise<ToolCallEventResult | undefined> {\n\t\tconst ctx = this.createContext();\n\t\tlet result: ToolCallEventResult | undefined;\n\n\t\tfor (const hook of this.hooks) {\n\t\t\tconst handlers = hook.handlers.get(\"tool_call\");\n\t\t\tif (!handlers || handlers.length === 0) continue;\n\n\t\t\tfor (const handler of handlers) {\n\t\t\t\t// No timeout - let user take their time\n\t\t\t\tconst handlerResult = await handler(event, ctx);\n\n\t\t\t\tif (handlerResult) {\n\t\t\t\t\tresult = handlerResult as ToolCallEventResult;\n\t\t\t\t\t// If blocked, stop processing further hooks\n\t\t\t\t\tif (result.block) {\n\t\t\t\t\t\treturn result;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n}\n"]}
1
+ {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../../src/core/hooks/runner.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC3D,OAAO,KAAK,EACX,iBAAiB,EAGjB,SAAS,EACT,SAAS,EAET,aAAa,EACb,aAAa,EACb,mBAAmB,EACnB,qBAAqB,EACrB,MAAM,YAAY,CAAC;AAOpB;;GAEG;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;AA4F3D;;GAEG;AACH,qBAAa,UAAU;IACtB,OAAO,CAAC,KAAK,CAAe;IAC5B,OAAO,CAAC,SAAS,CAAgB;IACjC,OAAO,CAAC,KAAK,CAAU;IACvB,OAAO,CAAC,GAAG,CAAS;IACpB,OAAO,CAAC,WAAW,CAAgB;IACnC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,cAAc,CAAqC;IAE3D,YAAY,KAAK,EAAE,UAAU,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,MAAwB,EAO9E;IAED;;;OAGG;IACH,YAAY,CAAC,SAAS,EAAE,aAAa,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAG3D;IAED;;OAEG;IACH,YAAY,IAAI,MAAM,EAAE,CAEvB;IAED;;OAEG;IACH,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAE/C;IAED;;;OAGG;IACH,cAAc,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,CAIzC;IAED;;;OAGG;IACH,OAAO,CAAC,QAAQ,EAAE,iBAAiB,GAAG,MAAM,IAAI,CAG/C;IAED;;OAEG;IACH,OAAO,CAAC,SAAS;IAMjB;;OAEG;IACH,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAQtC;IAED;;OAEG;IACH,OAAO,CAAC,aAAa;IAUrB;;;OAGG;IACG,IAAI,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,iBAAiB,GAAG,qBAAqB,GAAG,SAAS,CAAC,CAmC3F;IAED;;;;OAIG;IACG,YAAY,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC,CAuBjF;CACD","sourcesContent":["/**\n * Hook runner - executes hooks and manages their lifecycle.\n */\n\nimport { spawn } from \"node:child_process\";\nimport type { LoadedHook, SendHandler } from \"./loader.js\";\nimport type {\n\tBranchEventResult,\n\tExecOptions,\n\tExecResult,\n\tHookError,\n\tHookEvent,\n\tHookEventContext,\n\tHookUIContext,\n\tToolCallEvent,\n\tToolCallEventResult,\n\tToolResultEventResult,\n} from \"./types.js\";\n\n/**\n * Default timeout for hook execution (30 seconds).\n */\nconst DEFAULT_TIMEOUT = 30000;\n\n/**\n * Listener for hook errors.\n */\nexport type HookErrorListener = (error: HookError) => void;\n\n/**\n * Execute a command and return stdout/stderr/code.\n * Supports cancellation via AbortSignal and timeout.\n */\nasync function exec(command: string, args: string[], cwd: string, options?: ExecOptions): Promise<ExecResult> {\n\treturn new Promise((resolve) => {\n\t\tconst proc = spawn(command, args, { cwd, shell: false });\n\n\t\tlet stdout = \"\";\n\t\tlet stderr = \"\";\n\t\tlet killed = false;\n\t\tlet timeoutId: NodeJS.Timeout | undefined;\n\n\t\tconst killProcess = () => {\n\t\t\tif (!killed) {\n\t\t\t\tkilled = true;\n\t\t\t\tproc.kill(\"SIGTERM\");\n\t\t\t\t// Force kill after 5 seconds if SIGTERM doesn't work\n\t\t\t\tsetTimeout(() => {\n\t\t\t\t\tif (!proc.killed) {\n\t\t\t\t\t\tproc.kill(\"SIGKILL\");\n\t\t\t\t\t}\n\t\t\t\t}, 5000);\n\t\t\t}\n\t\t};\n\n\t\t// Handle abort signal\n\t\tif (options?.signal) {\n\t\t\tif (options.signal.aborted) {\n\t\t\t\tkillProcess();\n\t\t\t} else {\n\t\t\t\toptions.signal.addEventListener(\"abort\", killProcess, { once: true });\n\t\t\t}\n\t\t}\n\n\t\t// Handle timeout\n\t\tif (options?.timeout && options.timeout > 0) {\n\t\t\ttimeoutId = setTimeout(() => {\n\t\t\t\tkillProcess();\n\t\t\t}, options.timeout);\n\t\t}\n\n\t\tproc.stdout?.on(\"data\", (data) => {\n\t\t\tstdout += data.toString();\n\t\t});\n\n\t\tproc.stderr?.on(\"data\", (data) => {\n\t\t\tstderr += data.toString();\n\t\t});\n\n\t\tproc.on(\"close\", (code) => {\n\t\t\tif (timeoutId) clearTimeout(timeoutId);\n\t\t\tif (options?.signal) {\n\t\t\t\toptions.signal.removeEventListener(\"abort\", killProcess);\n\t\t\t}\n\t\t\tresolve({ stdout, stderr, code: code ?? 0, killed });\n\t\t});\n\n\t\tproc.on(\"error\", (_err) => {\n\t\t\tif (timeoutId) clearTimeout(timeoutId);\n\t\t\tif (options?.signal) {\n\t\t\t\toptions.signal.removeEventListener(\"abort\", killProcess);\n\t\t\t}\n\t\t\tresolve({ stdout, stderr, code: 1, killed });\n\t\t});\n\t});\n}\n\n/**\n * Create a promise that rejects after a timeout.\n */\nfunction createTimeout(ms: number): { promise: Promise<never>; clear: () => void } {\n\tlet timeoutId: NodeJS.Timeout;\n\tconst promise = new Promise<never>((_, reject) => {\n\t\ttimeoutId = setTimeout(() => reject(new Error(`Hook timed out after ${ms}ms`)), ms);\n\t});\n\treturn {\n\t\tpromise,\n\t\tclear: () => clearTimeout(timeoutId),\n\t};\n}\n\n/** No-op UI context used when no UI is available */\nconst noOpUIContext: HookUIContext = {\n\tselect: async () => null,\n\tconfirm: async () => false,\n\tinput: async () => null,\n\tnotify: () => {},\n};\n\n/**\n * HookRunner executes hooks and manages event emission.\n */\nexport class HookRunner {\n\tprivate hooks: LoadedHook[];\n\tprivate uiContext: HookUIContext;\n\tprivate hasUI: boolean;\n\tprivate cwd: string;\n\tprivate sessionFile: string | null;\n\tprivate timeout: number;\n\tprivate errorListeners: Set<HookErrorListener> = new Set();\n\n\tconstructor(hooks: LoadedHook[], cwd: string, timeout: number = DEFAULT_TIMEOUT) {\n\t\tthis.hooks = hooks;\n\t\tthis.uiContext = noOpUIContext;\n\t\tthis.hasUI = false;\n\t\tthis.cwd = cwd;\n\t\tthis.sessionFile = null;\n\t\tthis.timeout = timeout;\n\t}\n\n\t/**\n\t * Set the UI context for hooks.\n\t * Call this when the mode initializes and UI is available.\n\t */\n\tsetUIContext(uiContext: HookUIContext, hasUI: boolean): void {\n\t\tthis.uiContext = uiContext;\n\t\tthis.hasUI = hasUI;\n\t}\n\n\t/**\n\t * Get the paths of all loaded hooks.\n\t */\n\tgetHookPaths(): string[] {\n\t\treturn this.hooks.map((h) => h.path);\n\t}\n\n\t/**\n\t * Set the session file path.\n\t */\n\tsetSessionFile(sessionFile: string | null): void {\n\t\tthis.sessionFile = sessionFile;\n\t}\n\n\t/**\n\t * Set the send handler for all hooks' pi.send().\n\t * Call this when the mode initializes.\n\t */\n\tsetSendHandler(handler: SendHandler): void {\n\t\tfor (const hook of this.hooks) {\n\t\t\thook.setSendHandler(handler);\n\t\t}\n\t}\n\n\t/**\n\t * Subscribe to hook errors.\n\t * @returns Unsubscribe function\n\t */\n\tonError(listener: HookErrorListener): () => void {\n\t\tthis.errorListeners.add(listener);\n\t\treturn () => this.errorListeners.delete(listener);\n\t}\n\n\t/**\n\t * Emit an error to all listeners.\n\t */\n\tprivate emitError(error: HookError): void {\n\t\tfor (const listener of this.errorListeners) {\n\t\t\tlistener(error);\n\t\t}\n\t}\n\n\t/**\n\t * Check if any hooks have handlers for the given event type.\n\t */\n\thasHandlers(eventType: string): boolean {\n\t\tfor (const hook of this.hooks) {\n\t\t\tconst handlers = hook.handlers.get(eventType);\n\t\t\tif (handlers && handlers.length > 0) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * Create the event context for handlers.\n\t */\n\tprivate createContext(): HookEventContext {\n\t\treturn {\n\t\t\texec: (command: string, args: string[], options?: ExecOptions) => exec(command, args, this.cwd, options),\n\t\t\tui: this.uiContext,\n\t\t\thasUI: this.hasUI,\n\t\t\tcwd: this.cwd,\n\t\t\tsessionFile: this.sessionFile,\n\t\t};\n\t}\n\n\t/**\n\t * Emit an event to all hooks.\n\t * Returns the result from branch/tool_result events (if any handler returns one).\n\t */\n\tasync emit(event: HookEvent): Promise<BranchEventResult | ToolResultEventResult | undefined> {\n\t\tconst ctx = this.createContext();\n\t\tlet result: BranchEventResult | ToolResultEventResult | undefined;\n\n\t\tfor (const hook of this.hooks) {\n\t\t\tconst handlers = hook.handlers.get(event.type);\n\t\t\tif (!handlers || handlers.length === 0) continue;\n\n\t\t\tfor (const handler of handlers) {\n\t\t\t\ttry {\n\t\t\t\t\tconst timeout = createTimeout(this.timeout);\n\t\t\t\t\tconst handlerResult = await Promise.race([handler(event, ctx), timeout.promise]);\n\t\t\t\t\ttimeout.clear();\n\n\t\t\t\t\t// For branch events, capture the result\n\t\t\t\t\tif (event.type === \"branch\" && handlerResult) {\n\t\t\t\t\t\tresult = handlerResult as BranchEventResult;\n\t\t\t\t\t}\n\n\t\t\t\t\t// For tool_result events, capture the result\n\t\t\t\t\tif (event.type === \"tool_result\" && handlerResult) {\n\t\t\t\t\t\tresult = handlerResult as ToolResultEventResult;\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\t\t\t\tthis.emitError({\n\t\t\t\t\t\thookPath: hook.path,\n\t\t\t\t\t\tevent: event.type,\n\t\t\t\t\t\terror: message,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Emit a tool_call event to all hooks.\n\t * No timeout - user prompts can take as long as needed.\n\t * Errors are thrown (not swallowed) so caller can block on failure.\n\t */\n\tasync emitToolCall(event: ToolCallEvent): Promise<ToolCallEventResult | undefined> {\n\t\tconst ctx = this.createContext();\n\t\tlet result: ToolCallEventResult | undefined;\n\n\t\tfor (const hook of this.hooks) {\n\t\t\tconst handlers = hook.handlers.get(\"tool_call\");\n\t\t\tif (!handlers || handlers.length === 0) continue;\n\n\t\t\tfor (const handler of handlers) {\n\t\t\t\t// No timeout - let user take their time\n\t\t\t\tconst handlerResult = await handler(event, ctx);\n\n\t\t\t\tif (handlerResult) {\n\t\t\t\t\tresult = handlerResult as ToolCallEventResult;\n\t\t\t\t\t// If blocked, stop processing further hooks\n\t\t\t\t\tif (result.block) {\n\t\t\t\t\t\treturn result;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n}\n"]}