@mariozechner/pi-coding-agent 0.34.0 → 0.34.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.34.1] - 2026-01-04
4
+
5
+ ### Added
6
+
7
+ - Hook API: `ctx.ui.setTitle(title)` allows hooks to set the terminal window/tab title ([#446](https://github.com/badlogic/pi-mono/pull/446) by [@aliou](https://github.com/aliou))
8
+
9
+ ### Changed
10
+
11
+ - Expanded keybinding documentation to list all 32 supported symbol keys with notes on ctrl+symbol behavior ([#450](https://github.com/badlogic/pi-mono/pull/450) by [@kaofelix](https://github.com/kaofelix))
12
+
3
13
  ## [0.34.0] - 2026-01-04
4
14
 
5
15
  ### Added
@@ -1335,4 +1345,4 @@ Initial public release.
1335
1345
  - Git branch display in footer
1336
1346
  - Message queueing during streaming responses
1337
1347
  - OAuth integration for Gmail and Google Calendar access
1338
- - HTML export with syntax highlighting and collapsible sections
1348
+ - HTML export with syntax highlighting and collapsible sections
package/README.md CHANGED
@@ -272,7 +272,12 @@ Both modes are configurable via `/settings`: "one-at-a-time" delivers messages o
272
272
 
273
273
  All keyboard shortcuts can be customized via `~/.pi/agent/keybindings.json`. Each action can be bound to one or more keys.
274
274
 
275
- **Key format:** `modifier+key` where modifiers are `ctrl`, `shift`, `alt` and keys are `a-z`, `0-9`, `escape`, `tab`, `enter`, `space`, `backspace`, `delete`, `home`, `end`, `up`, `down`, `left`, `right`.
275
+ **Key format:** `modifier+key` where modifiers are `ctrl`, `shift`, `alt` and keys are:
276
+
277
+ - Letters: `a-z`
278
+ - Numbers: `0-9`
279
+ - Special keys: `escape`, `tab`, `enter`, `space`, `backspace`, `delete`, `home`, `end`, `up`, `down`, `left`, `right`
280
+ - Symbol keys: `` ` ``, `-`, `=`, `[`, `]`, `\`, `;`, `'`, `,`, `.`, `/`, `!`, `@`, `#`, `$`, `%`, `^`, `&`, `*`, `(`, `)`, `_`, `+`, `|`, `~`, `{`, `}`, `:`, `<`, `>`, `?`
276
281
 
277
282
  **Configurable actions:**
278
283
 
@@ -338,6 +343,21 @@ All keyboard shortcuts can be customized via `~/.pi/agent/keybindings.json`. Eac
338
343
  }
339
344
  ```
340
345
 
346
+ **Example (symbol keys):**
347
+
348
+ ```json
349
+ {
350
+ "submit": ["enter", "ctrl+j"],
351
+ "newLine": ["shift+enter", "ctrl+;"],
352
+ "toggleThinking": "ctrl+/",
353
+ "cycleModelForward": "ctrl+.",
354
+ "cycleModelBackward": "ctrl+,",
355
+ "interrupt": ["escape", "ctrl+`"]
356
+ }
357
+ ```
358
+
359
+ > **Note:** Some `ctrl+symbol` combinations overlap with ASCII control characters due to terminal legacy behavior (e.g., `ctrl+[` is the same as Escape, `ctrl+M` is the same as Enter). These can still be used with `ctrl+shift+key` (e.g., `ctrl+shift+]`). See [Kitty keyboard protocol: legacy ctrl mapping of ASCII keys](https://sw.kovidgoyal.net/kitty/keyboard-protocol/#legacy-ctrl-mapping-of-ascii-keys) for all unsupported keys.
360
+
341
361
  ### Bash Mode
342
362
 
343
363
  Prefix commands with `!` to execute them and add output to context:
@@ -365,6 +385,8 @@ Run multiple commands before prompting; all outputs are included together.
365
385
 
366
386
  **Pasting images:** Press `Ctrl+V` to paste an image from your clipboard.
367
387
 
388
+ > **Note:** On macOS, pressing Cmd+C on an image file in Finder copies the file path, not the image contents. Use Preview or another image viewer to copy the actual image, or drag the file onto the terminal instead.
389
+
368
390
  **Dragging images:** Drag image files onto the terminal to insert their path. On macOS, you can also drag the screenshot thumbnail (after Cmd+Shift+4) directly onto the terminal.
369
391
 
370
392
  **Attaching images:** Include image paths in your message:
@@ -1 +1 @@
1
- {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../../src/core/custom-tools/loader.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAaH,OAAO,KAAK,EAAoC,qBAAqB,EAAoB,MAAM,YAAY,CAAC;AAwL5G;;;;;GAKG;AACH,wBAAsB,eAAe,CACpC,KAAK,EAAE,MAAM,EAAE,EACf,GAAG,EAAE,MAAM,EACX,gBAAgB,EAAE,MAAM,EAAE,GACxB,OAAO,CAAC,qBAAqB,CAAC,CA+ChC;AAgCD;;;;;;;;;;;GAWG;AACH,wBAAsB,0BAA0B,CAC/C,eAAe,EAAE,MAAM,EAAE,EACzB,GAAG,EAAE,MAAM,EACX,gBAAgB,EAAE,MAAM,EAAE,EAC1B,QAAQ,GAAE,MAAsB,GAC9B,OAAO,CAAC,qBAAqB,CAAC,CA2BhC","sourcesContent":["/**\n * Custom tool loader - loads TypeScript tool modules using jiti.\n *\n * For Bun compiled binaries, custom tools that import from @mariozechner/* packages\n * are not supported because Bun's plugin system doesn't intercept imports from\n * external files loaded at runtime. Users should use the npm-installed version\n * for custom tools that depend on pi packages.\n */\n\nimport * as fs from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { createJiti } from \"jiti\";\nimport { getAgentDir, isBunBinary } from \"../../config.js\";\nimport { theme } from \"../../modes/interactive/theme/theme.js\";\nimport type { ExecOptions } from \"../exec.js\";\nimport { execCommand } from \"../exec.js\";\nimport type { HookUIContext } from \"../hooks/types.js\";\nimport type { CustomToolAPI, CustomToolFactory, CustomToolsLoadResult, LoadedCustomTool } from \"./types.js\";\n\n// Create require function to resolve module paths at runtime\nconst require = createRequire(import.meta.url);\n\n// Lazily computed aliases - resolved at runtime to handle global installs\nlet _aliases: Record<string, string> | null = null;\nfunction getAliases(): Record<string, string> {\n\tif (_aliases) return _aliases;\n\n\tconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\tconst packageIndex = path.resolve(__dirname, \"../..\", \"index.js\");\n\n\t// For typebox, we need the package root directory (not the entry file)\n\t// because jiti's alias is prefix-based: imports like \"@sinclair/typebox/compiler\"\n\t// get the alias prepended. If we alias to the entry file (.../build/cjs/index.js),\n\t// then \"@sinclair/typebox/compiler\" becomes \".../build/cjs/index.js/compiler\" (invalid).\n\t// By aliasing to the package root, it becomes \".../typebox/compiler\" which resolves correctly.\n\tconst typeboxEntry = require.resolve(\"@sinclair/typebox\");\n\tconst typeboxRoot = typeboxEntry.replace(/\\/build\\/cjs\\/index\\.js$/, \"\");\n\n\t_aliases = {\n\t\t\"@mariozechner/pi-coding-agent\": packageIndex,\n\t\t\"@mariozechner/pi-tui\": require.resolve(\"@mariozechner/pi-tui\"),\n\t\t\"@mariozechner/pi-ai\": require.resolve(\"@mariozechner/pi-ai\"),\n\t\t\"@sinclair/typebox\": typeboxRoot,\n\t};\n\treturn _aliases;\n}\n\nconst UNICODE_SPACES = /[\\u00A0\\u2000-\\u200A\\u202F\\u205F\\u3000]/g;\n\nfunction normalizeUnicodeSpaces(str: string): string {\n\treturn str.replace(UNICODE_SPACES, \" \");\n}\n\nfunction expandPath(p: string): string {\n\tconst normalized = normalizeUnicodeSpaces(p);\n\tif (normalized.startsWith(\"~/\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(2));\n\t}\n\tif (normalized.startsWith(\"~\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(1));\n\t}\n\treturn normalized;\n}\n\n/**\n * Resolve tool path.\n * - Absolute paths used as-is\n * - Paths starting with ~ expanded to home directory\n * - Relative paths resolved from cwd\n */\nfunction resolveToolPath(toolPath: string, cwd: string): string {\n\tconst expanded = expandPath(toolPath);\n\n\tif (path.isAbsolute(expanded)) {\n\t\treturn expanded;\n\t}\n\n\t// Relative paths resolved from cwd\n\treturn path.resolve(cwd, expanded);\n}\n\n/**\n * Create a no-op UI context for headless modes.\n */\nfunction createNoOpUIContext(): HookUIContext {\n\treturn {\n\t\tselect: async () => undefined,\n\t\tconfirm: async () => false,\n\t\tinput: async () => undefined,\n\t\tnotify: () => {},\n\t\tsetStatus: () => {},\n\t\tsetWidget: () => {},\n\t\tcustom: async () => undefined as never,\n\t\tsetEditorText: () => {},\n\t\tgetEditorText: () => \"\",\n\t\teditor: async () => undefined,\n\t\tget theme() {\n\t\t\treturn theme;\n\t\t},\n\t};\n}\n\n/**\n * Load a tool in Bun binary mode.\n *\n * Since Bun plugins don't work for dynamically loaded external files,\n * custom tools that import from @mariozechner/* packages won't work.\n * Tools that only use standard npm packages (installed in the tool's directory)\n * may still work.\n */\nasync function loadToolWithBun(\n\tresolvedPath: string,\n\tsharedApi: CustomToolAPI,\n): Promise<{ tools: LoadedCustomTool[] | null; error: string | null }> {\n\ttry {\n\t\t// Try to import directly - will work for tools without @mariozechner/* imports\n\t\tconst module = await import(resolvedPath);\n\t\tconst factory = (module.default ?? module) as CustomToolFactory;\n\n\t\tif (typeof factory !== \"function\") {\n\t\t\treturn { tools: null, error: \"Tool must export a default function\" };\n\t\t}\n\n\t\tconst toolResult = await factory(sharedApi);\n\t\tconst toolsArray = Array.isArray(toolResult) ? toolResult : [toolResult];\n\n\t\tconst loadedTools: LoadedCustomTool[] = toolsArray.map((tool) => ({\n\t\t\tpath: resolvedPath,\n\t\t\tresolvedPath,\n\t\t\ttool,\n\t\t}));\n\n\t\treturn { tools: loadedTools, error: null };\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\n\t\t// Check if it's a module resolution error for our packages\n\t\tif (message.includes(\"Cannot find module\") && message.includes(\"@mariozechner/\")) {\n\t\t\treturn {\n\t\t\t\ttools: null,\n\t\t\t\terror:\n\t\t\t\t\t`${message}\\n` +\n\t\t\t\t\t\"Note: Custom tools importing from @mariozechner/* packages are not supported in the standalone binary.\\n\" +\n\t\t\t\t\t\"Please install pi via npm: npm install -g @mariozechner/pi-coding-agent\",\n\t\t\t};\n\t\t}\n\n\t\treturn { tools: null, error: `Failed to load tool: ${message}` };\n\t}\n}\n\n/**\n * Load a single tool module using jiti (or Bun.build for compiled binaries).\n */\nasync function loadTool(\n\ttoolPath: string,\n\tcwd: string,\n\tsharedApi: CustomToolAPI,\n): Promise<{ tools: LoadedCustomTool[] | null; error: string | null }> {\n\tconst resolvedPath = resolveToolPath(toolPath, cwd);\n\n\t// Use Bun.build for compiled binaries since jiti can't resolve bundled modules\n\tif (isBunBinary) {\n\t\treturn loadToolWithBun(resolvedPath, sharedApi);\n\t}\n\n\ttry {\n\t\t// Create jiti instance for TypeScript/ESM loading\n\t\t// Use aliases to resolve package imports since tools are loaded from user directories\n\t\t// (e.g. ~/.pi/agent/tools) but import from packages installed with pi-coding-agent\n\t\tconst jiti = createJiti(import.meta.url, {\n\t\t\talias: getAliases(),\n\t\t});\n\n\t\t// Import the module\n\t\tconst module = await jiti.import(resolvedPath, { default: true });\n\t\tconst factory = module as CustomToolFactory;\n\n\t\tif (typeof factory !== \"function\") {\n\t\t\treturn { tools: null, error: \"Tool must export a default function\" };\n\t\t}\n\n\t\t// Call factory with shared API\n\t\tconst result = await factory(sharedApi);\n\n\t\t// Handle single tool or array of tools\n\t\tconst toolsArray = Array.isArray(result) ? result : [result];\n\n\t\tconst loadedTools: LoadedCustomTool[] = toolsArray.map((tool) => ({\n\t\t\tpath: toolPath,\n\t\t\tresolvedPath,\n\t\t\ttool,\n\t\t}));\n\n\t\treturn { tools: loadedTools, error: null };\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\treturn { tools: null, error: `Failed to load tool: ${message}` };\n\t}\n}\n\n/**\n * Load all tools from configuration.\n * @param paths - Array of tool file paths\n * @param cwd - Current working directory for resolving relative paths\n * @param builtInToolNames - Names of built-in tools to check for conflicts\n */\nexport async function loadCustomTools(\n\tpaths: string[],\n\tcwd: string,\n\tbuiltInToolNames: string[],\n): Promise<CustomToolsLoadResult> {\n\tconst tools: LoadedCustomTool[] = [];\n\tconst errors: Array<{ path: string; error: string }> = [];\n\tconst seenNames = new Set<string>(builtInToolNames);\n\n\t// Shared API object - all tools get the same instance\n\tconst sharedApi: CustomToolAPI = {\n\t\tcwd,\n\t\texec: (command: string, args: string[], options?: ExecOptions) =>\n\t\t\texecCommand(command, args, options?.cwd ?? cwd, options),\n\t\tui: createNoOpUIContext(),\n\t\thasUI: false,\n\t};\n\n\tfor (const toolPath of paths) {\n\t\tconst { tools: loadedTools, error } = await loadTool(toolPath, cwd, sharedApi);\n\n\t\tif (error) {\n\t\t\terrors.push({ path: toolPath, error });\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (loadedTools) {\n\t\t\tfor (const loadedTool of loadedTools) {\n\t\t\t\t// Check for name conflicts\n\t\t\t\tif (seenNames.has(loadedTool.tool.name)) {\n\t\t\t\t\terrors.push({\n\t\t\t\t\t\tpath: toolPath,\n\t\t\t\t\t\terror: `Tool name \"${loadedTool.tool.name}\" conflicts with existing tool`,\n\t\t\t\t\t});\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tseenNames.add(loadedTool.tool.name);\n\t\t\t\ttools.push(loadedTool);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn {\n\t\ttools,\n\t\terrors,\n\t\tsetUIContext(uiContext, hasUI) {\n\t\t\tsharedApi.ui = uiContext;\n\t\t\tsharedApi.hasUI = hasUI;\n\t\t},\n\t};\n}\n\n/**\n * Discover tool files from a directory.\n * Only loads index.ts files from subdirectories (e.g., tools/mytool/index.ts).\n */\nfunction discoverToolsInDir(dir: string): string[] {\n\tif (!fs.existsSync(dir)) {\n\t\treturn [];\n\t}\n\n\tconst tools: string[] = [];\n\n\ttry {\n\t\tconst entries = fs.readdirSync(dir, { withFileTypes: true });\n\n\t\tfor (const entry of entries) {\n\t\t\tif (entry.isDirectory() || entry.isSymbolicLink()) {\n\t\t\t\t// Check for index.ts in subdirectory\n\t\t\t\tconst indexPath = path.join(dir, entry.name, \"index.ts\");\n\t\t\t\tif (fs.existsSync(indexPath)) {\n\t\t\t\t\ttools.push(indexPath);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} catch {\n\t\treturn [];\n\t}\n\n\treturn tools;\n}\n\n/**\n * Discover and load tools from standard locations:\n * 1. agentDir/tools/*.ts (global)\n * 2. cwd/.pi/tools/*.ts (project-local)\n *\n * Plus any explicitly configured paths from settings or CLI.\n *\n * @param configuredPaths - Explicit paths from settings.json and CLI --tool flags\n * @param cwd - Current working directory\n * @param builtInToolNames - Names of built-in tools to check for conflicts\n * @param agentDir - Agent config directory. Default: from getAgentDir()\n */\nexport async function discoverAndLoadCustomTools(\n\tconfiguredPaths: string[],\n\tcwd: string,\n\tbuiltInToolNames: string[],\n\tagentDir: string = getAgentDir(),\n): Promise<CustomToolsLoadResult> {\n\tconst allPaths: string[] = [];\n\tconst seen = new Set<string>();\n\n\t// Helper to add paths without duplicates\n\tconst addPaths = (paths: string[]) => {\n\t\tfor (const p of paths) {\n\t\t\tconst resolved = path.resolve(p);\n\t\t\tif (!seen.has(resolved)) {\n\t\t\t\tseen.add(resolved);\n\t\t\t\tallPaths.push(p);\n\t\t\t}\n\t\t}\n\t};\n\n\t// 1. Global tools: agentDir/tools/\n\tconst globalToolsDir = path.join(agentDir, \"tools\");\n\taddPaths(discoverToolsInDir(globalToolsDir));\n\n\t// 2. Project-local tools: cwd/.pi/tools/\n\tconst localToolsDir = path.join(cwd, \".pi\", \"tools\");\n\taddPaths(discoverToolsInDir(localToolsDir));\n\n\t// 3. Explicitly configured paths (can override/add)\n\taddPaths(configuredPaths.map((p) => resolveToolPath(p, cwd)));\n\n\treturn loadCustomTools(allPaths, cwd, builtInToolNames);\n}\n"]}
1
+ {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../../src/core/custom-tools/loader.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAaH,OAAO,KAAK,EAAoC,qBAAqB,EAAoB,MAAM,YAAY,CAAC;AAyL5G;;;;;GAKG;AACH,wBAAsB,eAAe,CACpC,KAAK,EAAE,MAAM,EAAE,EACf,GAAG,EAAE,MAAM,EACX,gBAAgB,EAAE,MAAM,EAAE,GACxB,OAAO,CAAC,qBAAqB,CAAC,CA+ChC;AAgCD;;;;;;;;;;;GAWG;AACH,wBAAsB,0BAA0B,CAC/C,eAAe,EAAE,MAAM,EAAE,EACzB,GAAG,EAAE,MAAM,EACX,gBAAgB,EAAE,MAAM,EAAE,EAC1B,QAAQ,GAAE,MAAsB,GAC9B,OAAO,CAAC,qBAAqB,CAAC,CA2BhC","sourcesContent":["/**\n * Custom tool loader - loads TypeScript tool modules using jiti.\n *\n * For Bun compiled binaries, custom tools that import from @mariozechner/* packages\n * are not supported because Bun's plugin system doesn't intercept imports from\n * external files loaded at runtime. Users should use the npm-installed version\n * for custom tools that depend on pi packages.\n */\n\nimport * as fs from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { createJiti } from \"jiti\";\nimport { getAgentDir, isBunBinary } from \"../../config.js\";\nimport { theme } from \"../../modes/interactive/theme/theme.js\";\nimport type { ExecOptions } from \"../exec.js\";\nimport { execCommand } from \"../exec.js\";\nimport type { HookUIContext } from \"../hooks/types.js\";\nimport type { CustomToolAPI, CustomToolFactory, CustomToolsLoadResult, LoadedCustomTool } from \"./types.js\";\n\n// Create require function to resolve module paths at runtime\nconst require = createRequire(import.meta.url);\n\n// Lazily computed aliases - resolved at runtime to handle global installs\nlet _aliases: Record<string, string> | null = null;\nfunction getAliases(): Record<string, string> {\n\tif (_aliases) return _aliases;\n\n\tconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\tconst packageIndex = path.resolve(__dirname, \"../..\", \"index.js\");\n\n\t// For typebox, we need the package root directory (not the entry file)\n\t// because jiti's alias is prefix-based: imports like \"@sinclair/typebox/compiler\"\n\t// get the alias prepended. If we alias to the entry file (.../build/cjs/index.js),\n\t// then \"@sinclair/typebox/compiler\" becomes \".../build/cjs/index.js/compiler\" (invalid).\n\t// By aliasing to the package root, it becomes \".../typebox/compiler\" which resolves correctly.\n\tconst typeboxEntry = require.resolve(\"@sinclair/typebox\");\n\tconst typeboxRoot = typeboxEntry.replace(/\\/build\\/cjs\\/index\\.js$/, \"\");\n\n\t_aliases = {\n\t\t\"@mariozechner/pi-coding-agent\": packageIndex,\n\t\t\"@mariozechner/pi-tui\": require.resolve(\"@mariozechner/pi-tui\"),\n\t\t\"@mariozechner/pi-ai\": require.resolve(\"@mariozechner/pi-ai\"),\n\t\t\"@sinclair/typebox\": typeboxRoot,\n\t};\n\treturn _aliases;\n}\n\nconst UNICODE_SPACES = /[\\u00A0\\u2000-\\u200A\\u202F\\u205F\\u3000]/g;\n\nfunction normalizeUnicodeSpaces(str: string): string {\n\treturn str.replace(UNICODE_SPACES, \" \");\n}\n\nfunction expandPath(p: string): string {\n\tconst normalized = normalizeUnicodeSpaces(p);\n\tif (normalized.startsWith(\"~/\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(2));\n\t}\n\tif (normalized.startsWith(\"~\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(1));\n\t}\n\treturn normalized;\n}\n\n/**\n * Resolve tool path.\n * - Absolute paths used as-is\n * - Paths starting with ~ expanded to home directory\n * - Relative paths resolved from cwd\n */\nfunction resolveToolPath(toolPath: string, cwd: string): string {\n\tconst expanded = expandPath(toolPath);\n\n\tif (path.isAbsolute(expanded)) {\n\t\treturn expanded;\n\t}\n\n\t// Relative paths resolved from cwd\n\treturn path.resolve(cwd, expanded);\n}\n\n/**\n * Create a no-op UI context for headless modes.\n */\nfunction createNoOpUIContext(): HookUIContext {\n\treturn {\n\t\tselect: async () => undefined,\n\t\tconfirm: async () => false,\n\t\tinput: async () => undefined,\n\t\tnotify: () => {},\n\t\tsetStatus: () => {},\n\t\tsetWidget: () => {},\n\t\tsetTitle: () => {},\n\t\tcustom: async () => undefined as never,\n\t\tsetEditorText: () => {},\n\t\tgetEditorText: () => \"\",\n\t\teditor: async () => undefined,\n\t\tget theme() {\n\t\t\treturn theme;\n\t\t},\n\t};\n}\n\n/**\n * Load a tool in Bun binary mode.\n *\n * Since Bun plugins don't work for dynamically loaded external files,\n * custom tools that import from @mariozechner/* packages won't work.\n * Tools that only use standard npm packages (installed in the tool's directory)\n * may still work.\n */\nasync function loadToolWithBun(\n\tresolvedPath: string,\n\tsharedApi: CustomToolAPI,\n): Promise<{ tools: LoadedCustomTool[] | null; error: string | null }> {\n\ttry {\n\t\t// Try to import directly - will work for tools without @mariozechner/* imports\n\t\tconst module = await import(resolvedPath);\n\t\tconst factory = (module.default ?? module) as CustomToolFactory;\n\n\t\tif (typeof factory !== \"function\") {\n\t\t\treturn { tools: null, error: \"Tool must export a default function\" };\n\t\t}\n\n\t\tconst toolResult = await factory(sharedApi);\n\t\tconst toolsArray = Array.isArray(toolResult) ? toolResult : [toolResult];\n\n\t\tconst loadedTools: LoadedCustomTool[] = toolsArray.map((tool) => ({\n\t\t\tpath: resolvedPath,\n\t\t\tresolvedPath,\n\t\t\ttool,\n\t\t}));\n\n\t\treturn { tools: loadedTools, error: null };\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\n\t\t// Check if it's a module resolution error for our packages\n\t\tif (message.includes(\"Cannot find module\") && message.includes(\"@mariozechner/\")) {\n\t\t\treturn {\n\t\t\t\ttools: null,\n\t\t\t\terror:\n\t\t\t\t\t`${message}\\n` +\n\t\t\t\t\t\"Note: Custom tools importing from @mariozechner/* packages are not supported in the standalone binary.\\n\" +\n\t\t\t\t\t\"Please install pi via npm: npm install -g @mariozechner/pi-coding-agent\",\n\t\t\t};\n\t\t}\n\n\t\treturn { tools: null, error: `Failed to load tool: ${message}` };\n\t}\n}\n\n/**\n * Load a single tool module using jiti (or Bun.build for compiled binaries).\n */\nasync function loadTool(\n\ttoolPath: string,\n\tcwd: string,\n\tsharedApi: CustomToolAPI,\n): Promise<{ tools: LoadedCustomTool[] | null; error: string | null }> {\n\tconst resolvedPath = resolveToolPath(toolPath, cwd);\n\n\t// Use Bun.build for compiled binaries since jiti can't resolve bundled modules\n\tif (isBunBinary) {\n\t\treturn loadToolWithBun(resolvedPath, sharedApi);\n\t}\n\n\ttry {\n\t\t// Create jiti instance for TypeScript/ESM loading\n\t\t// Use aliases to resolve package imports since tools are loaded from user directories\n\t\t// (e.g. ~/.pi/agent/tools) but import from packages installed with pi-coding-agent\n\t\tconst jiti = createJiti(import.meta.url, {\n\t\t\talias: getAliases(),\n\t\t});\n\n\t\t// Import the module\n\t\tconst module = await jiti.import(resolvedPath, { default: true });\n\t\tconst factory = module as CustomToolFactory;\n\n\t\tif (typeof factory !== \"function\") {\n\t\t\treturn { tools: null, error: \"Tool must export a default function\" };\n\t\t}\n\n\t\t// Call factory with shared API\n\t\tconst result = await factory(sharedApi);\n\n\t\t// Handle single tool or array of tools\n\t\tconst toolsArray = Array.isArray(result) ? result : [result];\n\n\t\tconst loadedTools: LoadedCustomTool[] = toolsArray.map((tool) => ({\n\t\t\tpath: toolPath,\n\t\t\tresolvedPath,\n\t\t\ttool,\n\t\t}));\n\n\t\treturn { tools: loadedTools, error: null };\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\treturn { tools: null, error: `Failed to load tool: ${message}` };\n\t}\n}\n\n/**\n * Load all tools from configuration.\n * @param paths - Array of tool file paths\n * @param cwd - Current working directory for resolving relative paths\n * @param builtInToolNames - Names of built-in tools to check for conflicts\n */\nexport async function loadCustomTools(\n\tpaths: string[],\n\tcwd: string,\n\tbuiltInToolNames: string[],\n): Promise<CustomToolsLoadResult> {\n\tconst tools: LoadedCustomTool[] = [];\n\tconst errors: Array<{ path: string; error: string }> = [];\n\tconst seenNames = new Set<string>(builtInToolNames);\n\n\t// Shared API object - all tools get the same instance\n\tconst sharedApi: CustomToolAPI = {\n\t\tcwd,\n\t\texec: (command: string, args: string[], options?: ExecOptions) =>\n\t\t\texecCommand(command, args, options?.cwd ?? cwd, options),\n\t\tui: createNoOpUIContext(),\n\t\thasUI: false,\n\t};\n\n\tfor (const toolPath of paths) {\n\t\tconst { tools: loadedTools, error } = await loadTool(toolPath, cwd, sharedApi);\n\n\t\tif (error) {\n\t\t\terrors.push({ path: toolPath, error });\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (loadedTools) {\n\t\t\tfor (const loadedTool of loadedTools) {\n\t\t\t\t// Check for name conflicts\n\t\t\t\tif (seenNames.has(loadedTool.tool.name)) {\n\t\t\t\t\terrors.push({\n\t\t\t\t\t\tpath: toolPath,\n\t\t\t\t\t\terror: `Tool name \"${loadedTool.tool.name}\" conflicts with existing tool`,\n\t\t\t\t\t});\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tseenNames.add(loadedTool.tool.name);\n\t\t\t\ttools.push(loadedTool);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn {\n\t\ttools,\n\t\terrors,\n\t\tsetUIContext(uiContext, hasUI) {\n\t\t\tsharedApi.ui = uiContext;\n\t\t\tsharedApi.hasUI = hasUI;\n\t\t},\n\t};\n}\n\n/**\n * Discover tool files from a directory.\n * Only loads index.ts files from subdirectories (e.g., tools/mytool/index.ts).\n */\nfunction discoverToolsInDir(dir: string): string[] {\n\tif (!fs.existsSync(dir)) {\n\t\treturn [];\n\t}\n\n\tconst tools: string[] = [];\n\n\ttry {\n\t\tconst entries = fs.readdirSync(dir, { withFileTypes: true });\n\n\t\tfor (const entry of entries) {\n\t\t\tif (entry.isDirectory() || entry.isSymbolicLink()) {\n\t\t\t\t// Check for index.ts in subdirectory\n\t\t\t\tconst indexPath = path.join(dir, entry.name, \"index.ts\");\n\t\t\t\tif (fs.existsSync(indexPath)) {\n\t\t\t\t\ttools.push(indexPath);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} catch {\n\t\treturn [];\n\t}\n\n\treturn tools;\n}\n\n/**\n * Discover and load tools from standard locations:\n * 1. agentDir/tools/*.ts (global)\n * 2. cwd/.pi/tools/*.ts (project-local)\n *\n * Plus any explicitly configured paths from settings or CLI.\n *\n * @param configuredPaths - Explicit paths from settings.json and CLI --tool flags\n * @param cwd - Current working directory\n * @param builtInToolNames - Names of built-in tools to check for conflicts\n * @param agentDir - Agent config directory. Default: from getAgentDir()\n */\nexport async function discoverAndLoadCustomTools(\n\tconfiguredPaths: string[],\n\tcwd: string,\n\tbuiltInToolNames: string[],\n\tagentDir: string = getAgentDir(),\n): Promise<CustomToolsLoadResult> {\n\tconst allPaths: string[] = [];\n\tconst seen = new Set<string>();\n\n\t// Helper to add paths without duplicates\n\tconst addPaths = (paths: string[]) => {\n\t\tfor (const p of paths) {\n\t\t\tconst resolved = path.resolve(p);\n\t\t\tif (!seen.has(resolved)) {\n\t\t\t\tseen.add(resolved);\n\t\t\t\tallPaths.push(p);\n\t\t\t}\n\t\t}\n\t};\n\n\t// 1. Global tools: agentDir/tools/\n\tconst globalToolsDir = path.join(agentDir, \"tools\");\n\taddPaths(discoverToolsInDir(globalToolsDir));\n\n\t// 2. Project-local tools: cwd/.pi/tools/\n\tconst localToolsDir = path.join(cwd, \".pi\", \"tools\");\n\taddPaths(discoverToolsInDir(localToolsDir));\n\n\t// 3. Explicitly configured paths (can override/add)\n\taddPaths(configuredPaths.map((p) => resolveToolPath(p, cwd)));\n\n\treturn loadCustomTools(allPaths, cwd, builtInToolNames);\n}\n"]}
@@ -78,6 +78,7 @@ function createNoOpUIContext() {
78
78
  notify: () => { },
79
79
  setStatus: () => { },
80
80
  setWidget: () => { },
81
+ setTitle: () => { },
81
82
  custom: async () => undefined,
82
83
  setEditorText: () => { },
83
84
  getEditorText: () => "",
@@ -1 +1 @@
1
- {"version":3,"file":"loader.js","sourceRoot":"","sources":["../../../src/core/custom-tools/loader.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Custom tool loader - loads TypeScript tool modules using jiti.\n *\n * For Bun compiled binaries, custom tools that import from @mariozechner/* packages\n * are not supported because Bun's plugin system doesn't intercept imports from\n * external files loaded at runtime. Users should use the npm-installed version\n * for custom tools that depend on pi packages.\n */\n\nimport * as fs from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { createJiti } from \"jiti\";\nimport { getAgentDir, isBunBinary } from \"../../config.js\";\nimport { theme } from \"../../modes/interactive/theme/theme.js\";\nimport type { ExecOptions } from \"../exec.js\";\nimport { execCommand } from \"../exec.js\";\nimport type { HookUIContext } from \"../hooks/types.js\";\nimport type { CustomToolAPI, CustomToolFactory, CustomToolsLoadResult, LoadedCustomTool } from \"./types.js\";\n\n// Create require function to resolve module paths at runtime\nconst require = createRequire(import.meta.url);\n\n// Lazily computed aliases - resolved at runtime to handle global installs\nlet _aliases: Record<string, string> | null = null;\nfunction getAliases(): Record<string, string> {\n\tif (_aliases) return _aliases;\n\n\tconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\tconst packageIndex = path.resolve(__dirname, \"../..\", \"index.js\");\n\n\t// For typebox, we need the package root directory (not the entry file)\n\t// because jiti's alias is prefix-based: imports like \"@sinclair/typebox/compiler\"\n\t// get the alias prepended. If we alias to the entry file (.../build/cjs/index.js),\n\t// then \"@sinclair/typebox/compiler\" becomes \".../build/cjs/index.js/compiler\" (invalid).\n\t// By aliasing to the package root, it becomes \".../typebox/compiler\" which resolves correctly.\n\tconst typeboxEntry = require.resolve(\"@sinclair/typebox\");\n\tconst typeboxRoot = typeboxEntry.replace(/\\/build\\/cjs\\/index\\.js$/, \"\");\n\n\t_aliases = {\n\t\t\"@mariozechner/pi-coding-agent\": packageIndex,\n\t\t\"@mariozechner/pi-tui\": require.resolve(\"@mariozechner/pi-tui\"),\n\t\t\"@mariozechner/pi-ai\": require.resolve(\"@mariozechner/pi-ai\"),\n\t\t\"@sinclair/typebox\": typeboxRoot,\n\t};\n\treturn _aliases;\n}\n\nconst UNICODE_SPACES = /[\\u00A0\\u2000-\\u200A\\u202F\\u205F\\u3000]/g;\n\nfunction normalizeUnicodeSpaces(str: string): string {\n\treturn str.replace(UNICODE_SPACES, \" \");\n}\n\nfunction expandPath(p: string): string {\n\tconst normalized = normalizeUnicodeSpaces(p);\n\tif (normalized.startsWith(\"~/\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(2));\n\t}\n\tif (normalized.startsWith(\"~\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(1));\n\t}\n\treturn normalized;\n}\n\n/**\n * Resolve tool path.\n * - Absolute paths used as-is\n * - Paths starting with ~ expanded to home directory\n * - Relative paths resolved from cwd\n */\nfunction resolveToolPath(toolPath: string, cwd: string): string {\n\tconst expanded = expandPath(toolPath);\n\n\tif (path.isAbsolute(expanded)) {\n\t\treturn expanded;\n\t}\n\n\t// Relative paths resolved from cwd\n\treturn path.resolve(cwd, expanded);\n}\n\n/**\n * Create a no-op UI context for headless modes.\n */\nfunction createNoOpUIContext(): HookUIContext {\n\treturn {\n\t\tselect: async () => undefined,\n\t\tconfirm: async () => false,\n\t\tinput: async () => undefined,\n\t\tnotify: () => {},\n\t\tsetStatus: () => {},\n\t\tsetWidget: () => {},\n\t\tcustom: async () => undefined as never,\n\t\tsetEditorText: () => {},\n\t\tgetEditorText: () => \"\",\n\t\teditor: async () => undefined,\n\t\tget theme() {\n\t\t\treturn theme;\n\t\t},\n\t};\n}\n\n/**\n * Load a tool in Bun binary mode.\n *\n * Since Bun plugins don't work for dynamically loaded external files,\n * custom tools that import from @mariozechner/* packages won't work.\n * Tools that only use standard npm packages (installed in the tool's directory)\n * may still work.\n */\nasync function loadToolWithBun(\n\tresolvedPath: string,\n\tsharedApi: CustomToolAPI,\n): Promise<{ tools: LoadedCustomTool[] | null; error: string | null }> {\n\ttry {\n\t\t// Try to import directly - will work for tools without @mariozechner/* imports\n\t\tconst module = await import(resolvedPath);\n\t\tconst factory = (module.default ?? module) as CustomToolFactory;\n\n\t\tif (typeof factory !== \"function\") {\n\t\t\treturn { tools: null, error: \"Tool must export a default function\" };\n\t\t}\n\n\t\tconst toolResult = await factory(sharedApi);\n\t\tconst toolsArray = Array.isArray(toolResult) ? toolResult : [toolResult];\n\n\t\tconst loadedTools: LoadedCustomTool[] = toolsArray.map((tool) => ({\n\t\t\tpath: resolvedPath,\n\t\t\tresolvedPath,\n\t\t\ttool,\n\t\t}));\n\n\t\treturn { tools: loadedTools, error: null };\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\n\t\t// Check if it's a module resolution error for our packages\n\t\tif (message.includes(\"Cannot find module\") && message.includes(\"@mariozechner/\")) {\n\t\t\treturn {\n\t\t\t\ttools: null,\n\t\t\t\terror:\n\t\t\t\t\t`${message}\\n` +\n\t\t\t\t\t\"Note: Custom tools importing from @mariozechner/* packages are not supported in the standalone binary.\\n\" +\n\t\t\t\t\t\"Please install pi via npm: npm install -g @mariozechner/pi-coding-agent\",\n\t\t\t};\n\t\t}\n\n\t\treturn { tools: null, error: `Failed to load tool: ${message}` };\n\t}\n}\n\n/**\n * Load a single tool module using jiti (or Bun.build for compiled binaries).\n */\nasync function loadTool(\n\ttoolPath: string,\n\tcwd: string,\n\tsharedApi: CustomToolAPI,\n): Promise<{ tools: LoadedCustomTool[] | null; error: string | null }> {\n\tconst resolvedPath = resolveToolPath(toolPath, cwd);\n\n\t// Use Bun.build for compiled binaries since jiti can't resolve bundled modules\n\tif (isBunBinary) {\n\t\treturn loadToolWithBun(resolvedPath, sharedApi);\n\t}\n\n\ttry {\n\t\t// Create jiti instance for TypeScript/ESM loading\n\t\t// Use aliases to resolve package imports since tools are loaded from user directories\n\t\t// (e.g. ~/.pi/agent/tools) but import from packages installed with pi-coding-agent\n\t\tconst jiti = createJiti(import.meta.url, {\n\t\t\talias: getAliases(),\n\t\t});\n\n\t\t// Import the module\n\t\tconst module = await jiti.import(resolvedPath, { default: true });\n\t\tconst factory = module as CustomToolFactory;\n\n\t\tif (typeof factory !== \"function\") {\n\t\t\treturn { tools: null, error: \"Tool must export a default function\" };\n\t\t}\n\n\t\t// Call factory with shared API\n\t\tconst result = await factory(sharedApi);\n\n\t\t// Handle single tool or array of tools\n\t\tconst toolsArray = Array.isArray(result) ? result : [result];\n\n\t\tconst loadedTools: LoadedCustomTool[] = toolsArray.map((tool) => ({\n\t\t\tpath: toolPath,\n\t\t\tresolvedPath,\n\t\t\ttool,\n\t\t}));\n\n\t\treturn { tools: loadedTools, error: null };\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\treturn { tools: null, error: `Failed to load tool: ${message}` };\n\t}\n}\n\n/**\n * Load all tools from configuration.\n * @param paths - Array of tool file paths\n * @param cwd - Current working directory for resolving relative paths\n * @param builtInToolNames - Names of built-in tools to check for conflicts\n */\nexport async function loadCustomTools(\n\tpaths: string[],\n\tcwd: string,\n\tbuiltInToolNames: string[],\n): Promise<CustomToolsLoadResult> {\n\tconst tools: LoadedCustomTool[] = [];\n\tconst errors: Array<{ path: string; error: string }> = [];\n\tconst seenNames = new Set<string>(builtInToolNames);\n\n\t// Shared API object - all tools get the same instance\n\tconst sharedApi: CustomToolAPI = {\n\t\tcwd,\n\t\texec: (command: string, args: string[], options?: ExecOptions) =>\n\t\t\texecCommand(command, args, options?.cwd ?? cwd, options),\n\t\tui: createNoOpUIContext(),\n\t\thasUI: false,\n\t};\n\n\tfor (const toolPath of paths) {\n\t\tconst { tools: loadedTools, error } = await loadTool(toolPath, cwd, sharedApi);\n\n\t\tif (error) {\n\t\t\terrors.push({ path: toolPath, error });\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (loadedTools) {\n\t\t\tfor (const loadedTool of loadedTools) {\n\t\t\t\t// Check for name conflicts\n\t\t\t\tif (seenNames.has(loadedTool.tool.name)) {\n\t\t\t\t\terrors.push({\n\t\t\t\t\t\tpath: toolPath,\n\t\t\t\t\t\terror: `Tool name \"${loadedTool.tool.name}\" conflicts with existing tool`,\n\t\t\t\t\t});\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tseenNames.add(loadedTool.tool.name);\n\t\t\t\ttools.push(loadedTool);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn {\n\t\ttools,\n\t\terrors,\n\t\tsetUIContext(uiContext, hasUI) {\n\t\t\tsharedApi.ui = uiContext;\n\t\t\tsharedApi.hasUI = hasUI;\n\t\t},\n\t};\n}\n\n/**\n * Discover tool files from a directory.\n * Only loads index.ts files from subdirectories (e.g., tools/mytool/index.ts).\n */\nfunction discoverToolsInDir(dir: string): string[] {\n\tif (!fs.existsSync(dir)) {\n\t\treturn [];\n\t}\n\n\tconst tools: string[] = [];\n\n\ttry {\n\t\tconst entries = fs.readdirSync(dir, { withFileTypes: true });\n\n\t\tfor (const entry of entries) {\n\t\t\tif (entry.isDirectory() || entry.isSymbolicLink()) {\n\t\t\t\t// Check for index.ts in subdirectory\n\t\t\t\tconst indexPath = path.join(dir, entry.name, \"index.ts\");\n\t\t\t\tif (fs.existsSync(indexPath)) {\n\t\t\t\t\ttools.push(indexPath);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} catch {\n\t\treturn [];\n\t}\n\n\treturn tools;\n}\n\n/**\n * Discover and load tools from standard locations:\n * 1. agentDir/tools/*.ts (global)\n * 2. cwd/.pi/tools/*.ts (project-local)\n *\n * Plus any explicitly configured paths from settings or CLI.\n *\n * @param configuredPaths - Explicit paths from settings.json and CLI --tool flags\n * @param cwd - Current working directory\n * @param builtInToolNames - Names of built-in tools to check for conflicts\n * @param agentDir - Agent config directory. Default: from getAgentDir()\n */\nexport async function discoverAndLoadCustomTools(\n\tconfiguredPaths: string[],\n\tcwd: string,\n\tbuiltInToolNames: string[],\n\tagentDir: string = getAgentDir(),\n): Promise<CustomToolsLoadResult> {\n\tconst allPaths: string[] = [];\n\tconst seen = new Set<string>();\n\n\t// Helper to add paths without duplicates\n\tconst addPaths = (paths: string[]) => {\n\t\tfor (const p of paths) {\n\t\t\tconst resolved = path.resolve(p);\n\t\t\tif (!seen.has(resolved)) {\n\t\t\t\tseen.add(resolved);\n\t\t\t\tallPaths.push(p);\n\t\t\t}\n\t\t}\n\t};\n\n\t// 1. Global tools: agentDir/tools/\n\tconst globalToolsDir = path.join(agentDir, \"tools\");\n\taddPaths(discoverToolsInDir(globalToolsDir));\n\n\t// 2. Project-local tools: cwd/.pi/tools/\n\tconst localToolsDir = path.join(cwd, \".pi\", \"tools\");\n\taddPaths(discoverToolsInDir(localToolsDir));\n\n\t// 3. Explicitly configured paths (can override/add)\n\taddPaths(configuredPaths.map((p) => resolveToolPath(p, cwd)));\n\n\treturn loadCustomTools(allPaths, cwd, builtInToolNames);\n}\n"]}
1
+ {"version":3,"file":"loader.js","sourceRoot":"","sources":["../../../src/core/custom-tools/loader.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Custom tool loader - loads TypeScript tool modules using jiti.\n *\n * For Bun compiled binaries, custom tools that import from @mariozechner/* packages\n * are not supported because Bun's plugin system doesn't intercept imports from\n * external files loaded at runtime. Users should use the npm-installed version\n * for custom tools that depend on pi packages.\n */\n\nimport * as fs from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { createJiti } from \"jiti\";\nimport { getAgentDir, isBunBinary } from \"../../config.js\";\nimport { theme } from \"../../modes/interactive/theme/theme.js\";\nimport type { ExecOptions } from \"../exec.js\";\nimport { execCommand } from \"../exec.js\";\nimport type { HookUIContext } from \"../hooks/types.js\";\nimport type { CustomToolAPI, CustomToolFactory, CustomToolsLoadResult, LoadedCustomTool } from \"./types.js\";\n\n// Create require function to resolve module paths at runtime\nconst require = createRequire(import.meta.url);\n\n// Lazily computed aliases - resolved at runtime to handle global installs\nlet _aliases: Record<string, string> | null = null;\nfunction getAliases(): Record<string, string> {\n\tif (_aliases) return _aliases;\n\n\tconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\tconst packageIndex = path.resolve(__dirname, \"../..\", \"index.js\");\n\n\t// For typebox, we need the package root directory (not the entry file)\n\t// because jiti's alias is prefix-based: imports like \"@sinclair/typebox/compiler\"\n\t// get the alias prepended. If we alias to the entry file (.../build/cjs/index.js),\n\t// then \"@sinclair/typebox/compiler\" becomes \".../build/cjs/index.js/compiler\" (invalid).\n\t// By aliasing to the package root, it becomes \".../typebox/compiler\" which resolves correctly.\n\tconst typeboxEntry = require.resolve(\"@sinclair/typebox\");\n\tconst typeboxRoot = typeboxEntry.replace(/\\/build\\/cjs\\/index\\.js$/, \"\");\n\n\t_aliases = {\n\t\t\"@mariozechner/pi-coding-agent\": packageIndex,\n\t\t\"@mariozechner/pi-tui\": require.resolve(\"@mariozechner/pi-tui\"),\n\t\t\"@mariozechner/pi-ai\": require.resolve(\"@mariozechner/pi-ai\"),\n\t\t\"@sinclair/typebox\": typeboxRoot,\n\t};\n\treturn _aliases;\n}\n\nconst UNICODE_SPACES = /[\\u00A0\\u2000-\\u200A\\u202F\\u205F\\u3000]/g;\n\nfunction normalizeUnicodeSpaces(str: string): string {\n\treturn str.replace(UNICODE_SPACES, \" \");\n}\n\nfunction expandPath(p: string): string {\n\tconst normalized = normalizeUnicodeSpaces(p);\n\tif (normalized.startsWith(\"~/\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(2));\n\t}\n\tif (normalized.startsWith(\"~\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(1));\n\t}\n\treturn normalized;\n}\n\n/**\n * Resolve tool path.\n * - Absolute paths used as-is\n * - Paths starting with ~ expanded to home directory\n * - Relative paths resolved from cwd\n */\nfunction resolveToolPath(toolPath: string, cwd: string): string {\n\tconst expanded = expandPath(toolPath);\n\n\tif (path.isAbsolute(expanded)) {\n\t\treturn expanded;\n\t}\n\n\t// Relative paths resolved from cwd\n\treturn path.resolve(cwd, expanded);\n}\n\n/**\n * Create a no-op UI context for headless modes.\n */\nfunction createNoOpUIContext(): HookUIContext {\n\treturn {\n\t\tselect: async () => undefined,\n\t\tconfirm: async () => false,\n\t\tinput: async () => undefined,\n\t\tnotify: () => {},\n\t\tsetStatus: () => {},\n\t\tsetWidget: () => {},\n\t\tsetTitle: () => {},\n\t\tcustom: async () => undefined as never,\n\t\tsetEditorText: () => {},\n\t\tgetEditorText: () => \"\",\n\t\teditor: async () => undefined,\n\t\tget theme() {\n\t\t\treturn theme;\n\t\t},\n\t};\n}\n\n/**\n * Load a tool in Bun binary mode.\n *\n * Since Bun plugins don't work for dynamically loaded external files,\n * custom tools that import from @mariozechner/* packages won't work.\n * Tools that only use standard npm packages (installed in the tool's directory)\n * may still work.\n */\nasync function loadToolWithBun(\n\tresolvedPath: string,\n\tsharedApi: CustomToolAPI,\n): Promise<{ tools: LoadedCustomTool[] | null; error: string | null }> {\n\ttry {\n\t\t// Try to import directly - will work for tools without @mariozechner/* imports\n\t\tconst module = await import(resolvedPath);\n\t\tconst factory = (module.default ?? module) as CustomToolFactory;\n\n\t\tif (typeof factory !== \"function\") {\n\t\t\treturn { tools: null, error: \"Tool must export a default function\" };\n\t\t}\n\n\t\tconst toolResult = await factory(sharedApi);\n\t\tconst toolsArray = Array.isArray(toolResult) ? toolResult : [toolResult];\n\n\t\tconst loadedTools: LoadedCustomTool[] = toolsArray.map((tool) => ({\n\t\t\tpath: resolvedPath,\n\t\t\tresolvedPath,\n\t\t\ttool,\n\t\t}));\n\n\t\treturn { tools: loadedTools, error: null };\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\n\t\t// Check if it's a module resolution error for our packages\n\t\tif (message.includes(\"Cannot find module\") && message.includes(\"@mariozechner/\")) {\n\t\t\treturn {\n\t\t\t\ttools: null,\n\t\t\t\terror:\n\t\t\t\t\t`${message}\\n` +\n\t\t\t\t\t\"Note: Custom tools importing from @mariozechner/* packages are not supported in the standalone binary.\\n\" +\n\t\t\t\t\t\"Please install pi via npm: npm install -g @mariozechner/pi-coding-agent\",\n\t\t\t};\n\t\t}\n\n\t\treturn { tools: null, error: `Failed to load tool: ${message}` };\n\t}\n}\n\n/**\n * Load a single tool module using jiti (or Bun.build for compiled binaries).\n */\nasync function loadTool(\n\ttoolPath: string,\n\tcwd: string,\n\tsharedApi: CustomToolAPI,\n): Promise<{ tools: LoadedCustomTool[] | null; error: string | null }> {\n\tconst resolvedPath = resolveToolPath(toolPath, cwd);\n\n\t// Use Bun.build for compiled binaries since jiti can't resolve bundled modules\n\tif (isBunBinary) {\n\t\treturn loadToolWithBun(resolvedPath, sharedApi);\n\t}\n\n\ttry {\n\t\t// Create jiti instance for TypeScript/ESM loading\n\t\t// Use aliases to resolve package imports since tools are loaded from user directories\n\t\t// (e.g. ~/.pi/agent/tools) but import from packages installed with pi-coding-agent\n\t\tconst jiti = createJiti(import.meta.url, {\n\t\t\talias: getAliases(),\n\t\t});\n\n\t\t// Import the module\n\t\tconst module = await jiti.import(resolvedPath, { default: true });\n\t\tconst factory = module as CustomToolFactory;\n\n\t\tif (typeof factory !== \"function\") {\n\t\t\treturn { tools: null, error: \"Tool must export a default function\" };\n\t\t}\n\n\t\t// Call factory with shared API\n\t\tconst result = await factory(sharedApi);\n\n\t\t// Handle single tool or array of tools\n\t\tconst toolsArray = Array.isArray(result) ? result : [result];\n\n\t\tconst loadedTools: LoadedCustomTool[] = toolsArray.map((tool) => ({\n\t\t\tpath: toolPath,\n\t\t\tresolvedPath,\n\t\t\ttool,\n\t\t}));\n\n\t\treturn { tools: loadedTools, error: null };\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\treturn { tools: null, error: `Failed to load tool: ${message}` };\n\t}\n}\n\n/**\n * Load all tools from configuration.\n * @param paths - Array of tool file paths\n * @param cwd - Current working directory for resolving relative paths\n * @param builtInToolNames - Names of built-in tools to check for conflicts\n */\nexport async function loadCustomTools(\n\tpaths: string[],\n\tcwd: string,\n\tbuiltInToolNames: string[],\n): Promise<CustomToolsLoadResult> {\n\tconst tools: LoadedCustomTool[] = [];\n\tconst errors: Array<{ path: string; error: string }> = [];\n\tconst seenNames = new Set<string>(builtInToolNames);\n\n\t// Shared API object - all tools get the same instance\n\tconst sharedApi: CustomToolAPI = {\n\t\tcwd,\n\t\texec: (command: string, args: string[], options?: ExecOptions) =>\n\t\t\texecCommand(command, args, options?.cwd ?? cwd, options),\n\t\tui: createNoOpUIContext(),\n\t\thasUI: false,\n\t};\n\n\tfor (const toolPath of paths) {\n\t\tconst { tools: loadedTools, error } = await loadTool(toolPath, cwd, sharedApi);\n\n\t\tif (error) {\n\t\t\terrors.push({ path: toolPath, error });\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (loadedTools) {\n\t\t\tfor (const loadedTool of loadedTools) {\n\t\t\t\t// Check for name conflicts\n\t\t\t\tif (seenNames.has(loadedTool.tool.name)) {\n\t\t\t\t\terrors.push({\n\t\t\t\t\t\tpath: toolPath,\n\t\t\t\t\t\terror: `Tool name \"${loadedTool.tool.name}\" conflicts with existing tool`,\n\t\t\t\t\t});\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tseenNames.add(loadedTool.tool.name);\n\t\t\t\ttools.push(loadedTool);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn {\n\t\ttools,\n\t\terrors,\n\t\tsetUIContext(uiContext, hasUI) {\n\t\t\tsharedApi.ui = uiContext;\n\t\t\tsharedApi.hasUI = hasUI;\n\t\t},\n\t};\n}\n\n/**\n * Discover tool files from a directory.\n * Only loads index.ts files from subdirectories (e.g., tools/mytool/index.ts).\n */\nfunction discoverToolsInDir(dir: string): string[] {\n\tif (!fs.existsSync(dir)) {\n\t\treturn [];\n\t}\n\n\tconst tools: string[] = [];\n\n\ttry {\n\t\tconst entries = fs.readdirSync(dir, { withFileTypes: true });\n\n\t\tfor (const entry of entries) {\n\t\t\tif (entry.isDirectory() || entry.isSymbolicLink()) {\n\t\t\t\t// Check for index.ts in subdirectory\n\t\t\t\tconst indexPath = path.join(dir, entry.name, \"index.ts\");\n\t\t\t\tif (fs.existsSync(indexPath)) {\n\t\t\t\t\ttools.push(indexPath);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} catch {\n\t\treturn [];\n\t}\n\n\treturn tools;\n}\n\n/**\n * Discover and load tools from standard locations:\n * 1. agentDir/tools/*.ts (global)\n * 2. cwd/.pi/tools/*.ts (project-local)\n *\n * Plus any explicitly configured paths from settings or CLI.\n *\n * @param configuredPaths - Explicit paths from settings.json and CLI --tool flags\n * @param cwd - Current working directory\n * @param builtInToolNames - Names of built-in tools to check for conflicts\n * @param agentDir - Agent config directory. Default: from getAgentDir()\n */\nexport async function discoverAndLoadCustomTools(\n\tconfiguredPaths: string[],\n\tcwd: string,\n\tbuiltInToolNames: string[],\n\tagentDir: string = getAgentDir(),\n): Promise<CustomToolsLoadResult> {\n\tconst allPaths: string[] = [];\n\tconst seen = new Set<string>();\n\n\t// Helper to add paths without duplicates\n\tconst addPaths = (paths: string[]) => {\n\t\tfor (const p of paths) {\n\t\t\tconst resolved = path.resolve(p);\n\t\t\tif (!seen.has(resolved)) {\n\t\t\t\tseen.add(resolved);\n\t\t\t\tallPaths.push(p);\n\t\t\t}\n\t\t}\n\t};\n\n\t// 1. Global tools: agentDir/tools/\n\tconst globalToolsDir = path.join(agentDir, \"tools\");\n\taddPaths(discoverToolsInDir(globalToolsDir));\n\n\t// 2. Project-local tools: cwd/.pi/tools/\n\tconst localToolsDir = path.join(cwd, \".pi\", \"tools\");\n\taddPaths(discoverToolsInDir(localToolsDir));\n\n\t// 3. Explicitly configured paths (can override/add)\n\taddPaths(configuredPaths.map((p) => resolveToolPath(p, cwd)));\n\n\treturn loadCustomTools(allPaths, cwd, builtInToolNames);\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../../src/core/hooks/runner.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Hook runner - executes hooks and manages their lifecycle.\n */\n\nimport type { AgentMessage } from \"@mariozechner/pi-agent-core\";\nimport type { ImageContent, Model } from \"@mariozechner/pi-ai\";\nimport type { KeyId } from \"@mariozechner/pi-tui\";\nimport { theme } from \"../../modes/interactive/theme/theme.js\";\nimport type { ModelRegistry } from \"../model-registry.js\";\nimport type { SessionManager } from \"../session-manager.js\";\nimport type {\n\tAppendEntryHandler,\n\tBranchHandler,\n\tHookFlag,\n\tHookShortcut,\n\tLoadedHook,\n\tNavigateTreeHandler,\n\tNewSessionHandler,\n\tSendMessageHandler,\n} from \"./loader.js\";\nimport type {\n\tBeforeAgentStartEvent,\n\tBeforeAgentStartEventResult,\n\tContextEvent,\n\tContextEventResult,\n\tHookCommandContext,\n\tHookContext,\n\tHookError,\n\tHookEvent,\n\tHookMessageRenderer,\n\tHookUIContext,\n\tRegisteredCommand,\n\tSessionBeforeCompactResult,\n\tSessionBeforeTreeResult,\n\tToolCallEvent,\n\tToolCallEventResult,\n\tToolResultEventResult,\n} from \"./types.js\";\n\n/** Combined result from all before_agent_start handlers (internal) */\ninterface BeforeAgentStartCombinedResult {\n\tmessages?: NonNullable<BeforeAgentStartEventResult[\"message\"]>[];\n\tsystemPromptAppend?: string;\n}\n\n/**\n * Listener for hook errors.\n */\nexport type HookErrorListener = (error: HookError) => void;\n\n// Re-export execCommand for backward compatibility\nexport { execCommand } from \"../exec.js\";\n\n/** No-op UI context used when no UI is available */\nconst noOpUIContext: HookUIContext = {\n\tselect: async () => undefined,\n\tconfirm: async () => false,\n\tinput: async () => undefined,\n\tnotify: () => {},\n\tsetStatus: () => {},\n\tsetWidget: () => {},\n\tcustom: async () => undefined as never,\n\tsetEditorText: () => {},\n\tgetEditorText: () => \"\",\n\teditor: async () => undefined,\n\tget theme() {\n\t\treturn theme;\n\t},\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 sessionManager: SessionManager;\n\tprivate modelRegistry: ModelRegistry;\n\tprivate errorListeners: Set<HookErrorListener> = new Set();\n\tprivate getModel: () => Model<any> | undefined = () => undefined;\n\tprivate isIdleFn: () => boolean = () => true;\n\tprivate waitForIdleFn: () => Promise<void> = async () => {};\n\tprivate abortFn: () => void = () => {};\n\tprivate hasPendingMessagesFn: () => boolean = () => false;\n\tprivate newSessionHandler: NewSessionHandler = async () => ({ cancelled: false });\n\tprivate branchHandler: BranchHandler = async () => ({ cancelled: false });\n\tprivate navigateTreeHandler: NavigateTreeHandler = async () => ({ cancelled: false });\n\n\tconstructor(hooks: LoadedHook[], cwd: string, sessionManager: SessionManager, modelRegistry: ModelRegistry) {\n\t\tthis.hooks = hooks;\n\t\tthis.uiContext = noOpUIContext;\n\t\tthis.hasUI = false;\n\t\tthis.cwd = cwd;\n\t\tthis.sessionManager = sessionManager;\n\t\tthis.modelRegistry = modelRegistry;\n\t}\n\n\t/**\n\t * Initialize HookRunner with all required context.\n\t * Modes call this once the agent session is fully set up.\n\t */\n\tinitialize(options: {\n\t\t/** Function to get the current model */\n\t\tgetModel: () => Model<any> | undefined;\n\t\t/** Handler for hooks to send messages */\n\t\tsendMessageHandler: SendMessageHandler;\n\t\t/** Handler for hooks to append entries */\n\t\tappendEntryHandler: AppendEntryHandler;\n\t\t/** Handler for getting current active tools */\n\t\tgetActiveToolsHandler: () => string[];\n\t\t/** Handler for getting all configured tools */\n\t\tgetAllToolsHandler: () => string[];\n\t\t/** Handler for setting active tools */\n\t\tsetActiveToolsHandler: (toolNames: string[]) => void;\n\t\t/** Handler for creating new sessions (for HookCommandContext) */\n\t\tnewSessionHandler?: NewSessionHandler;\n\t\t/** Handler for branching sessions (for HookCommandContext) */\n\t\tbranchHandler?: BranchHandler;\n\t\t/** Handler for navigating session tree (for HookCommandContext) */\n\t\tnavigateTreeHandler?: NavigateTreeHandler;\n\t\t/** Function to check if agent is idle */\n\t\tisIdle?: () => boolean;\n\t\t/** Function to wait for agent to be idle */\n\t\twaitForIdle?: () => Promise<void>;\n\t\t/** Function to abort current operation (fire-and-forget) */\n\t\tabort?: () => void;\n\t\t/** Function to check if there are queued messages */\n\t\thasPendingMessages?: () => boolean;\n\t\t/** UI context for interactive prompts */\n\t\tuiContext?: HookUIContext;\n\t\t/** Whether UI is available */\n\t\thasUI?: boolean;\n\t}): void {\n\t\tthis.getModel = options.getModel;\n\t\tthis.isIdleFn = options.isIdle ?? (() => true);\n\t\tthis.waitForIdleFn = options.waitForIdle ?? (async () => {});\n\t\tthis.abortFn = options.abort ?? (() => {});\n\t\tthis.hasPendingMessagesFn = options.hasPendingMessages ?? (() => false);\n\t\t// Store session handlers for HookCommandContext\n\t\tif (options.newSessionHandler) {\n\t\t\tthis.newSessionHandler = options.newSessionHandler;\n\t\t}\n\t\tif (options.branchHandler) {\n\t\t\tthis.branchHandler = options.branchHandler;\n\t\t}\n\t\tif (options.navigateTreeHandler) {\n\t\t\tthis.navigateTreeHandler = options.navigateTreeHandler;\n\t\t}\n\t\t// Set per-hook handlers for pi.sendMessage(), pi.appendEntry(), pi.getActiveTools(), pi.getAllTools(), pi.setActiveTools()\n\t\tfor (const hook of this.hooks) {\n\t\t\thook.setSendMessageHandler(options.sendMessageHandler);\n\t\t\thook.setAppendEntryHandler(options.appendEntryHandler);\n\t\t\thook.setGetActiveToolsHandler(options.getActiveToolsHandler);\n\t\t\thook.setGetAllToolsHandler(options.getAllToolsHandler);\n\t\t\thook.setSetActiveToolsHandler(options.setActiveToolsHandler);\n\t\t}\n\t\tthis.uiContext = options.uiContext ?? noOpUIContext;\n\t\tthis.hasUI = options.hasUI ?? false;\n\t}\n\n\t/**\n\t * Get the UI context (set by mode).\n\t */\n\tgetUIContext(): HookUIContext | null {\n\t\treturn this.uiContext;\n\t}\n\n\t/**\n\t * Get whether UI is available.\n\t */\n\tgetHasUI(): boolean {\n\t\treturn this.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 * Get all CLI flags registered by hooks.\n\t */\n\tgetFlags(): Map<string, HookFlag> {\n\t\tconst allFlags = new Map<string, HookFlag>();\n\t\tfor (const hook of this.hooks) {\n\t\t\tfor (const [name, flag] of hook.flags) {\n\t\t\t\tallFlags.set(name, flag);\n\t\t\t}\n\t\t}\n\t\treturn allFlags;\n\t}\n\n\t/**\n\t * Set a flag value (after CLI parsing).\n\t */\n\tsetFlagValue(name: string, value: boolean | string): void {\n\t\tfor (const hook of this.hooks) {\n\t\t\tif (hook.flags.has(name)) {\n\t\t\t\thook.setFlagValue(name, value);\n\t\t\t}\n\t\t}\n\t}\n\n\t// Built-in shortcuts that hooks should not override\n\tprivate static readonly RESERVED_SHORTCUTS = new Set([\n\t\t\"ctrl+c\",\n\t\t\"ctrl+d\",\n\t\t\"ctrl+z\",\n\t\t\"ctrl+k\",\n\t\t\"ctrl+p\",\n\t\t\"ctrl+l\",\n\t\t\"ctrl+o\",\n\t\t\"ctrl+t\",\n\t\t\"ctrl+g\",\n\t\t\"shift+tab\",\n\t\t\"shift+ctrl+p\",\n\t\t\"alt+enter\",\n\t\t\"escape\",\n\t\t\"enter\",\n\t]);\n\n\t/**\n\t * Get all keyboard shortcuts registered by hooks.\n\t * When multiple hooks register the same shortcut, the last one wins.\n\t * Conflicts with built-in shortcuts are skipped with a warning.\n\t * Conflicts between hooks are logged as warnings.\n\t */\n\tgetShortcuts(): Map<KeyId, HookShortcut> {\n\t\tconst allShortcuts = new Map<KeyId, HookShortcut>();\n\t\tfor (const hook of this.hooks) {\n\t\t\tfor (const [key, shortcut] of hook.shortcuts) {\n\t\t\t\t// Normalize to lowercase for comparison (KeyId is string at runtime)\n\t\t\t\tconst normalizedKey = key.toLowerCase() as KeyId;\n\n\t\t\t\t// Check for built-in shortcut conflicts\n\t\t\t\tif (HookRunner.RESERVED_SHORTCUTS.has(normalizedKey)) {\n\t\t\t\t\tconsole.warn(\n\t\t\t\t\t\t`Hook shortcut '${key}' from ${shortcut.hookPath} conflicts with built-in shortcut. Skipping.`,\n\t\t\t\t\t);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst existing = allShortcuts.get(normalizedKey);\n\t\t\t\tif (existing) {\n\t\t\t\t\t// Log conflict between hooks - last one wins\n\t\t\t\t\tconsole.warn(\n\t\t\t\t\t\t`Hook shortcut conflict: '${key}' registered by both ${existing.hookPath} and ${shortcut.hookPath}. Using ${shortcut.hookPath}.`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tallShortcuts.set(normalizedKey, shortcut);\n\t\t\t}\n\t\t}\n\t\treturn allShortcuts;\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\t/**\n\t * Emit an error to all error listeners.\n\t */\n\temitError(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 * Get a message renderer for the given customType.\n\t * Returns the first renderer found across all hooks, or undefined if none.\n\t */\n\tgetMessageRenderer(customType: string): HookMessageRenderer | undefined {\n\t\tfor (const hook of this.hooks) {\n\t\t\tconst renderer = hook.messageRenderers.get(customType);\n\t\t\tif (renderer) {\n\t\t\t\treturn renderer;\n\t\t\t}\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Get all registered commands from all hooks.\n\t */\n\tgetRegisteredCommands(): RegisteredCommand[] {\n\t\tconst commands: RegisteredCommand[] = [];\n\t\tfor (const hook of this.hooks) {\n\t\t\tfor (const command of hook.commands.values()) {\n\t\t\t\tcommands.push(command);\n\t\t\t}\n\t\t}\n\t\treturn commands;\n\t}\n\n\t/**\n\t * Get a registered command by name.\n\t * Returns the first command found across all hooks, or undefined if none.\n\t */\n\tgetCommand(name: string): RegisteredCommand | undefined {\n\t\tfor (const hook of this.hooks) {\n\t\t\tconst command = hook.commands.get(name);\n\t\t\tif (command) {\n\t\t\t\treturn command;\n\t\t\t}\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Create the event context for handlers.\n\t */\n\tprivate createContext(): HookContext {\n\t\treturn {\n\t\t\tui: this.uiContext,\n\t\t\thasUI: this.hasUI,\n\t\t\tcwd: this.cwd,\n\t\t\tsessionManager: this.sessionManager,\n\t\t\tmodelRegistry: this.modelRegistry,\n\t\t\tmodel: this.getModel(),\n\t\t\tisIdle: () => this.isIdleFn(),\n\t\t\tabort: () => this.abortFn(),\n\t\t\thasPendingMessages: () => this.hasPendingMessagesFn(),\n\t\t};\n\t}\n\n\t/**\n\t * Create the command context for slash command handlers.\n\t * Extends HookContext with session control methods that are only safe in commands.\n\t */\n\tcreateCommandContext(): HookCommandContext {\n\t\treturn {\n\t\t\t...this.createContext(),\n\t\t\twaitForIdle: () => this.waitForIdleFn(),\n\t\t\tnewSession: (options) => this.newSessionHandler(options),\n\t\t\tbranch: (entryId) => this.branchHandler(entryId),\n\t\t\tnavigateTree: (targetId, options) => this.navigateTreeHandler(targetId, options),\n\t\t};\n\t}\n\n\t/**\n\t * Check if event type is a session \"before_*\" event that can be cancelled.\n\t */\n\tprivate isSessionBeforeEvent(\n\t\ttype: string,\n\t): type is \"session_before_switch\" | \"session_before_branch\" | \"session_before_compact\" | \"session_before_tree\" {\n\t\treturn (\n\t\t\ttype === \"session_before_switch\" ||\n\t\t\ttype === \"session_before_branch\" ||\n\t\t\ttype === \"session_before_compact\" ||\n\t\t\ttype === \"session_before_tree\"\n\t\t);\n\t}\n\n\t/**\n\t * Emit an event to all hooks.\n\t * Returns the result from session before_* / tool_result events (if any handler returns one).\n\t */\n\tasync emit(\n\t\tevent: HookEvent,\n\t): Promise<SessionBeforeCompactResult | SessionBeforeTreeResult | ToolResultEventResult | undefined> {\n\t\tconst ctx = this.createContext();\n\t\tlet result: SessionBeforeCompactResult | SessionBeforeTreeResult | 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 handlerResult = await handler(event, ctx);\n\n\t\t\t\t\t// For session before_* events, capture the result (for cancellation)\n\t\t\t\t\tif (this.isSessionBeforeEvent(event.type) && handlerResult) {\n\t\t\t\t\t\tresult = handlerResult as SessionBeforeCompactResult | SessionBeforeTreeResult;\n\t\t\t\t\t\t// If cancelled, stop processing further hooks\n\t\t\t\t\t\tif (result.cancel) {\n\t\t\t\t\t\t\treturn result;\n\t\t\t\t\t\t}\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\tconst stack = err instanceof Error ? err.stack : undefined;\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\tstack,\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\t/**\n\t * Emit a context event to all hooks.\n\t * Handlers are chained - each gets the previous handler's output (if any).\n\t * Returns the final modified messages, or the original if no modifications.\n\t *\n\t * Messages are deep-copied before passing to hooks, so mutations are safe.\n\t */\n\tasync emitContext(messages: AgentMessage[]): Promise<AgentMessage[]> {\n\t\tconst ctx = this.createContext();\n\t\tlet currentMessages = structuredClone(messages);\n\n\t\tfor (const hook of this.hooks) {\n\t\t\tconst handlers = hook.handlers.get(\"context\");\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 event: ContextEvent = { type: \"context\", messages: currentMessages };\n\t\t\t\t\tconst handlerResult = await handler(event, ctx);\n\n\t\t\t\t\tif (handlerResult && (handlerResult as ContextEventResult).messages) {\n\t\t\t\t\t\tcurrentMessages = (handlerResult as ContextEventResult).messages!;\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\tconst stack = err instanceof Error ? err.stack : undefined;\n\t\t\t\t\tthis.emitError({\n\t\t\t\t\t\thookPath: hook.path,\n\t\t\t\t\t\tevent: \"context\",\n\t\t\t\t\t\terror: message,\n\t\t\t\t\t\tstack,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn currentMessages;\n\t}\n\n\t/**\n\t * Emit before_agent_start event to all hooks.\n\t * Returns combined result: all messages and all systemPromptAppend strings concatenated.\n\t */\n\tasync emitBeforeAgentStart(\n\t\tprompt: string,\n\t\timages?: ImageContent[],\n\t): Promise<BeforeAgentStartCombinedResult | undefined> {\n\t\tconst ctx = this.createContext();\n\t\tconst messages: NonNullable<BeforeAgentStartEventResult[\"message\"]>[] = [];\n\t\tconst systemPromptAppends: string[] = [];\n\n\t\tfor (const hook of this.hooks) {\n\t\t\tconst handlers = hook.handlers.get(\"before_agent_start\");\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 event: BeforeAgentStartEvent = { type: \"before_agent_start\", prompt, images };\n\t\t\t\t\tconst handlerResult = await handler(event, ctx);\n\n\t\t\t\t\tif (handlerResult) {\n\t\t\t\t\t\tconst result = handlerResult as BeforeAgentStartEventResult;\n\t\t\t\t\t\t// Collect all messages\n\t\t\t\t\t\tif (result.message) {\n\t\t\t\t\t\t\tmessages.push(result.message);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Collect all systemPromptAppend strings\n\t\t\t\t\t\tif (result.systemPromptAppend) {\n\t\t\t\t\t\t\tsystemPromptAppends.push(result.systemPromptAppend);\n\t\t\t\t\t\t}\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\tconst stack = err instanceof Error ? err.stack : undefined;\n\t\t\t\t\tthis.emitError({\n\t\t\t\t\t\thookPath: hook.path,\n\t\t\t\t\t\tevent: \"before_agent_start\",\n\t\t\t\t\t\terror: message,\n\t\t\t\t\t\tstack,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Return combined result\n\t\tif (messages.length > 0 || systemPromptAppends.length > 0) {\n\t\t\treturn {\n\t\t\t\tmessages: messages.length > 0 ? messages : undefined,\n\t\t\t\tsystemPromptAppend: systemPromptAppends.length > 0 ? systemPromptAppends.join(\"\\n\\n\") : undefined,\n\t\t\t};\n\t\t}\n\n\t\treturn undefined;\n\t}\n}\n"]}
1
+ {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../../src/core/hooks/runner.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Hook runner - executes hooks and manages their lifecycle.\n */\n\nimport type { AgentMessage } from \"@mariozechner/pi-agent-core\";\nimport type { ImageContent, Model } from \"@mariozechner/pi-ai\";\nimport type { KeyId } from \"@mariozechner/pi-tui\";\nimport { theme } from \"../../modes/interactive/theme/theme.js\";\nimport type { ModelRegistry } from \"../model-registry.js\";\nimport type { SessionManager } from \"../session-manager.js\";\nimport type {\n\tAppendEntryHandler,\n\tBranchHandler,\n\tHookFlag,\n\tHookShortcut,\n\tLoadedHook,\n\tNavigateTreeHandler,\n\tNewSessionHandler,\n\tSendMessageHandler,\n} from \"./loader.js\";\nimport type {\n\tBeforeAgentStartEvent,\n\tBeforeAgentStartEventResult,\n\tContextEvent,\n\tContextEventResult,\n\tHookCommandContext,\n\tHookContext,\n\tHookError,\n\tHookEvent,\n\tHookMessageRenderer,\n\tHookUIContext,\n\tRegisteredCommand,\n\tSessionBeforeCompactResult,\n\tSessionBeforeTreeResult,\n\tToolCallEvent,\n\tToolCallEventResult,\n\tToolResultEventResult,\n} from \"./types.js\";\n\n/** Combined result from all before_agent_start handlers (internal) */\ninterface BeforeAgentStartCombinedResult {\n\tmessages?: NonNullable<BeforeAgentStartEventResult[\"message\"]>[];\n\tsystemPromptAppend?: string;\n}\n\n/**\n * Listener for hook errors.\n */\nexport type HookErrorListener = (error: HookError) => void;\n\n// Re-export execCommand for backward compatibility\nexport { execCommand } from \"../exec.js\";\n\n/** No-op UI context used when no UI is available */\nconst noOpUIContext: HookUIContext = {\n\tselect: async () => undefined,\n\tconfirm: async () => false,\n\tinput: async () => undefined,\n\tnotify: () => {},\n\tsetStatus: () => {},\n\tsetWidget: () => {},\n\tsetTitle: () => {},\n\tcustom: async () => undefined as never,\n\tsetEditorText: () => {},\n\tgetEditorText: () => \"\",\n\teditor: async () => undefined,\n\tget theme() {\n\t\treturn theme;\n\t},\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 sessionManager: SessionManager;\n\tprivate modelRegistry: ModelRegistry;\n\tprivate errorListeners: Set<HookErrorListener> = new Set();\n\tprivate getModel: () => Model<any> | undefined = () => undefined;\n\tprivate isIdleFn: () => boolean = () => true;\n\tprivate waitForIdleFn: () => Promise<void> = async () => {};\n\tprivate abortFn: () => void = () => {};\n\tprivate hasPendingMessagesFn: () => boolean = () => false;\n\tprivate newSessionHandler: NewSessionHandler = async () => ({ cancelled: false });\n\tprivate branchHandler: BranchHandler = async () => ({ cancelled: false });\n\tprivate navigateTreeHandler: NavigateTreeHandler = async () => ({ cancelled: false });\n\n\tconstructor(hooks: LoadedHook[], cwd: string, sessionManager: SessionManager, modelRegistry: ModelRegistry) {\n\t\tthis.hooks = hooks;\n\t\tthis.uiContext = noOpUIContext;\n\t\tthis.hasUI = false;\n\t\tthis.cwd = cwd;\n\t\tthis.sessionManager = sessionManager;\n\t\tthis.modelRegistry = modelRegistry;\n\t}\n\n\t/**\n\t * Initialize HookRunner with all required context.\n\t * Modes call this once the agent session is fully set up.\n\t */\n\tinitialize(options: {\n\t\t/** Function to get the current model */\n\t\tgetModel: () => Model<any> | undefined;\n\t\t/** Handler for hooks to send messages */\n\t\tsendMessageHandler: SendMessageHandler;\n\t\t/** Handler for hooks to append entries */\n\t\tappendEntryHandler: AppendEntryHandler;\n\t\t/** Handler for getting current active tools */\n\t\tgetActiveToolsHandler: () => string[];\n\t\t/** Handler for getting all configured tools */\n\t\tgetAllToolsHandler: () => string[];\n\t\t/** Handler for setting active tools */\n\t\tsetActiveToolsHandler: (toolNames: string[]) => void;\n\t\t/** Handler for creating new sessions (for HookCommandContext) */\n\t\tnewSessionHandler?: NewSessionHandler;\n\t\t/** Handler for branching sessions (for HookCommandContext) */\n\t\tbranchHandler?: BranchHandler;\n\t\t/** Handler for navigating session tree (for HookCommandContext) */\n\t\tnavigateTreeHandler?: NavigateTreeHandler;\n\t\t/** Function to check if agent is idle */\n\t\tisIdle?: () => boolean;\n\t\t/** Function to wait for agent to be idle */\n\t\twaitForIdle?: () => Promise<void>;\n\t\t/** Function to abort current operation (fire-and-forget) */\n\t\tabort?: () => void;\n\t\t/** Function to check if there are queued messages */\n\t\thasPendingMessages?: () => boolean;\n\t\t/** UI context for interactive prompts */\n\t\tuiContext?: HookUIContext;\n\t\t/** Whether UI is available */\n\t\thasUI?: boolean;\n\t}): void {\n\t\tthis.getModel = options.getModel;\n\t\tthis.isIdleFn = options.isIdle ?? (() => true);\n\t\tthis.waitForIdleFn = options.waitForIdle ?? (async () => {});\n\t\tthis.abortFn = options.abort ?? (() => {});\n\t\tthis.hasPendingMessagesFn = options.hasPendingMessages ?? (() => false);\n\t\t// Store session handlers for HookCommandContext\n\t\tif (options.newSessionHandler) {\n\t\t\tthis.newSessionHandler = options.newSessionHandler;\n\t\t}\n\t\tif (options.branchHandler) {\n\t\t\tthis.branchHandler = options.branchHandler;\n\t\t}\n\t\tif (options.navigateTreeHandler) {\n\t\t\tthis.navigateTreeHandler = options.navigateTreeHandler;\n\t\t}\n\t\t// Set per-hook handlers for pi.sendMessage(), pi.appendEntry(), pi.getActiveTools(), pi.getAllTools(), pi.setActiveTools()\n\t\tfor (const hook of this.hooks) {\n\t\t\thook.setSendMessageHandler(options.sendMessageHandler);\n\t\t\thook.setAppendEntryHandler(options.appendEntryHandler);\n\t\t\thook.setGetActiveToolsHandler(options.getActiveToolsHandler);\n\t\t\thook.setGetAllToolsHandler(options.getAllToolsHandler);\n\t\t\thook.setSetActiveToolsHandler(options.setActiveToolsHandler);\n\t\t}\n\t\tthis.uiContext = options.uiContext ?? noOpUIContext;\n\t\tthis.hasUI = options.hasUI ?? false;\n\t}\n\n\t/**\n\t * Get the UI context (set by mode).\n\t */\n\tgetUIContext(): HookUIContext | null {\n\t\treturn this.uiContext;\n\t}\n\n\t/**\n\t * Get whether UI is available.\n\t */\n\tgetHasUI(): boolean {\n\t\treturn this.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 * Get all CLI flags registered by hooks.\n\t */\n\tgetFlags(): Map<string, HookFlag> {\n\t\tconst allFlags = new Map<string, HookFlag>();\n\t\tfor (const hook of this.hooks) {\n\t\t\tfor (const [name, flag] of hook.flags) {\n\t\t\t\tallFlags.set(name, flag);\n\t\t\t}\n\t\t}\n\t\treturn allFlags;\n\t}\n\n\t/**\n\t * Set a flag value (after CLI parsing).\n\t */\n\tsetFlagValue(name: string, value: boolean | string): void {\n\t\tfor (const hook of this.hooks) {\n\t\t\tif (hook.flags.has(name)) {\n\t\t\t\thook.setFlagValue(name, value);\n\t\t\t}\n\t\t}\n\t}\n\n\t// Built-in shortcuts that hooks should not override\n\tprivate static readonly RESERVED_SHORTCUTS = new Set([\n\t\t\"ctrl+c\",\n\t\t\"ctrl+d\",\n\t\t\"ctrl+z\",\n\t\t\"ctrl+k\",\n\t\t\"ctrl+p\",\n\t\t\"ctrl+l\",\n\t\t\"ctrl+o\",\n\t\t\"ctrl+t\",\n\t\t\"ctrl+g\",\n\t\t\"shift+tab\",\n\t\t\"shift+ctrl+p\",\n\t\t\"alt+enter\",\n\t\t\"escape\",\n\t\t\"enter\",\n\t]);\n\n\t/**\n\t * Get all keyboard shortcuts registered by hooks.\n\t * When multiple hooks register the same shortcut, the last one wins.\n\t * Conflicts with built-in shortcuts are skipped with a warning.\n\t * Conflicts between hooks are logged as warnings.\n\t */\n\tgetShortcuts(): Map<KeyId, HookShortcut> {\n\t\tconst allShortcuts = new Map<KeyId, HookShortcut>();\n\t\tfor (const hook of this.hooks) {\n\t\t\tfor (const [key, shortcut] of hook.shortcuts) {\n\t\t\t\t// Normalize to lowercase for comparison (KeyId is string at runtime)\n\t\t\t\tconst normalizedKey = key.toLowerCase() as KeyId;\n\n\t\t\t\t// Check for built-in shortcut conflicts\n\t\t\t\tif (HookRunner.RESERVED_SHORTCUTS.has(normalizedKey)) {\n\t\t\t\t\tconsole.warn(\n\t\t\t\t\t\t`Hook shortcut '${key}' from ${shortcut.hookPath} conflicts with built-in shortcut. Skipping.`,\n\t\t\t\t\t);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst existing = allShortcuts.get(normalizedKey);\n\t\t\t\tif (existing) {\n\t\t\t\t\t// Log conflict between hooks - last one wins\n\t\t\t\t\tconsole.warn(\n\t\t\t\t\t\t`Hook shortcut conflict: '${key}' registered by both ${existing.hookPath} and ${shortcut.hookPath}. Using ${shortcut.hookPath}.`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tallShortcuts.set(normalizedKey, shortcut);\n\t\t\t}\n\t\t}\n\t\treturn allShortcuts;\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\t/**\n\t * Emit an error to all error listeners.\n\t */\n\temitError(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 * Get a message renderer for the given customType.\n\t * Returns the first renderer found across all hooks, or undefined if none.\n\t */\n\tgetMessageRenderer(customType: string): HookMessageRenderer | undefined {\n\t\tfor (const hook of this.hooks) {\n\t\t\tconst renderer = hook.messageRenderers.get(customType);\n\t\t\tif (renderer) {\n\t\t\t\treturn renderer;\n\t\t\t}\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Get all registered commands from all hooks.\n\t */\n\tgetRegisteredCommands(): RegisteredCommand[] {\n\t\tconst commands: RegisteredCommand[] = [];\n\t\tfor (const hook of this.hooks) {\n\t\t\tfor (const command of hook.commands.values()) {\n\t\t\t\tcommands.push(command);\n\t\t\t}\n\t\t}\n\t\treturn commands;\n\t}\n\n\t/**\n\t * Get a registered command by name.\n\t * Returns the first command found across all hooks, or undefined if none.\n\t */\n\tgetCommand(name: string): RegisteredCommand | undefined {\n\t\tfor (const hook of this.hooks) {\n\t\t\tconst command = hook.commands.get(name);\n\t\t\tif (command) {\n\t\t\t\treturn command;\n\t\t\t}\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Create the event context for handlers.\n\t */\n\tprivate createContext(): HookContext {\n\t\treturn {\n\t\t\tui: this.uiContext,\n\t\t\thasUI: this.hasUI,\n\t\t\tcwd: this.cwd,\n\t\t\tsessionManager: this.sessionManager,\n\t\t\tmodelRegistry: this.modelRegistry,\n\t\t\tmodel: this.getModel(),\n\t\t\tisIdle: () => this.isIdleFn(),\n\t\t\tabort: () => this.abortFn(),\n\t\t\thasPendingMessages: () => this.hasPendingMessagesFn(),\n\t\t};\n\t}\n\n\t/**\n\t * Create the command context for slash command handlers.\n\t * Extends HookContext with session control methods that are only safe in commands.\n\t */\n\tcreateCommandContext(): HookCommandContext {\n\t\treturn {\n\t\t\t...this.createContext(),\n\t\t\twaitForIdle: () => this.waitForIdleFn(),\n\t\t\tnewSession: (options) => this.newSessionHandler(options),\n\t\t\tbranch: (entryId) => this.branchHandler(entryId),\n\t\t\tnavigateTree: (targetId, options) => this.navigateTreeHandler(targetId, options),\n\t\t};\n\t}\n\n\t/**\n\t * Check if event type is a session \"before_*\" event that can be cancelled.\n\t */\n\tprivate isSessionBeforeEvent(\n\t\ttype: string,\n\t): type is \"session_before_switch\" | \"session_before_branch\" | \"session_before_compact\" | \"session_before_tree\" {\n\t\treturn (\n\t\t\ttype === \"session_before_switch\" ||\n\t\t\ttype === \"session_before_branch\" ||\n\t\t\ttype === \"session_before_compact\" ||\n\t\t\ttype === \"session_before_tree\"\n\t\t);\n\t}\n\n\t/**\n\t * Emit an event to all hooks.\n\t * Returns the result from session before_* / tool_result events (if any handler returns one).\n\t */\n\tasync emit(\n\t\tevent: HookEvent,\n\t): Promise<SessionBeforeCompactResult | SessionBeforeTreeResult | ToolResultEventResult | undefined> {\n\t\tconst ctx = this.createContext();\n\t\tlet result: SessionBeforeCompactResult | SessionBeforeTreeResult | 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 handlerResult = await handler(event, ctx);\n\n\t\t\t\t\t// For session before_* events, capture the result (for cancellation)\n\t\t\t\t\tif (this.isSessionBeforeEvent(event.type) && handlerResult) {\n\t\t\t\t\t\tresult = handlerResult as SessionBeforeCompactResult | SessionBeforeTreeResult;\n\t\t\t\t\t\t// If cancelled, stop processing further hooks\n\t\t\t\t\t\tif (result.cancel) {\n\t\t\t\t\t\t\treturn result;\n\t\t\t\t\t\t}\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\tconst stack = err instanceof Error ? err.stack : undefined;\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\tstack,\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\t/**\n\t * Emit a context event to all hooks.\n\t * Handlers are chained - each gets the previous handler's output (if any).\n\t * Returns the final modified messages, or the original if no modifications.\n\t *\n\t * Messages are deep-copied before passing to hooks, so mutations are safe.\n\t */\n\tasync emitContext(messages: AgentMessage[]): Promise<AgentMessage[]> {\n\t\tconst ctx = this.createContext();\n\t\tlet currentMessages = structuredClone(messages);\n\n\t\tfor (const hook of this.hooks) {\n\t\t\tconst handlers = hook.handlers.get(\"context\");\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 event: ContextEvent = { type: \"context\", messages: currentMessages };\n\t\t\t\t\tconst handlerResult = await handler(event, ctx);\n\n\t\t\t\t\tif (handlerResult && (handlerResult as ContextEventResult).messages) {\n\t\t\t\t\t\tcurrentMessages = (handlerResult as ContextEventResult).messages!;\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\tconst stack = err instanceof Error ? err.stack : undefined;\n\t\t\t\t\tthis.emitError({\n\t\t\t\t\t\thookPath: hook.path,\n\t\t\t\t\t\tevent: \"context\",\n\t\t\t\t\t\terror: message,\n\t\t\t\t\t\tstack,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn currentMessages;\n\t}\n\n\t/**\n\t * Emit before_agent_start event to all hooks.\n\t * Returns combined result: all messages and all systemPromptAppend strings concatenated.\n\t */\n\tasync emitBeforeAgentStart(\n\t\tprompt: string,\n\t\timages?: ImageContent[],\n\t): Promise<BeforeAgentStartCombinedResult | undefined> {\n\t\tconst ctx = this.createContext();\n\t\tconst messages: NonNullable<BeforeAgentStartEventResult[\"message\"]>[] = [];\n\t\tconst systemPromptAppends: string[] = [];\n\n\t\tfor (const hook of this.hooks) {\n\t\t\tconst handlers = hook.handlers.get(\"before_agent_start\");\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 event: BeforeAgentStartEvent = { type: \"before_agent_start\", prompt, images };\n\t\t\t\t\tconst handlerResult = await handler(event, ctx);\n\n\t\t\t\t\tif (handlerResult) {\n\t\t\t\t\t\tconst result = handlerResult as BeforeAgentStartEventResult;\n\t\t\t\t\t\t// Collect all messages\n\t\t\t\t\t\tif (result.message) {\n\t\t\t\t\t\t\tmessages.push(result.message);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Collect all systemPromptAppend strings\n\t\t\t\t\t\tif (result.systemPromptAppend) {\n\t\t\t\t\t\t\tsystemPromptAppends.push(result.systemPromptAppend);\n\t\t\t\t\t\t}\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\tconst stack = err instanceof Error ? err.stack : undefined;\n\t\t\t\t\tthis.emitError({\n\t\t\t\t\t\thookPath: hook.path,\n\t\t\t\t\t\tevent: \"before_agent_start\",\n\t\t\t\t\t\terror: message,\n\t\t\t\t\t\tstack,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Return combined result\n\t\tif (messages.length > 0 || systemPromptAppends.length > 0) {\n\t\t\treturn {\n\t\t\t\tmessages: messages.length > 0 ? messages : undefined,\n\t\t\t\tsystemPromptAppend: systemPromptAppends.length > 0 ? systemPromptAppends.join(\"\\n\\n\") : undefined,\n\t\t\t};\n\t\t}\n\n\t\treturn undefined;\n\t}\n}\n"]}
@@ -12,6 +12,7 @@ const noOpUIContext = {
12
12
  notify: () => { },
13
13
  setStatus: () => { },
14
14
  setWidget: () => { },
15
+ setTitle: () => { },
15
16
  custom: async () => undefined,
16
17
  setEditorText: () => { },
17
18
  getEditorText: () => "",
@@ -1 +1 @@
1
- {"version":3,"file":"runner.js","sourceRoot":"","sources":["../../../src/core/hooks/runner.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Hook runner - executes hooks and manages their lifecycle.\n */\n\nimport type { AgentMessage } from \"@mariozechner/pi-agent-core\";\nimport type { ImageContent, Model } from \"@mariozechner/pi-ai\";\nimport type { KeyId } from \"@mariozechner/pi-tui\";\nimport { theme } from \"../../modes/interactive/theme/theme.js\";\nimport type { ModelRegistry } from \"../model-registry.js\";\nimport type { SessionManager } from \"../session-manager.js\";\nimport type {\n\tAppendEntryHandler,\n\tBranchHandler,\n\tHookFlag,\n\tHookShortcut,\n\tLoadedHook,\n\tNavigateTreeHandler,\n\tNewSessionHandler,\n\tSendMessageHandler,\n} from \"./loader.js\";\nimport type {\n\tBeforeAgentStartEvent,\n\tBeforeAgentStartEventResult,\n\tContextEvent,\n\tContextEventResult,\n\tHookCommandContext,\n\tHookContext,\n\tHookError,\n\tHookEvent,\n\tHookMessageRenderer,\n\tHookUIContext,\n\tRegisteredCommand,\n\tSessionBeforeCompactResult,\n\tSessionBeforeTreeResult,\n\tToolCallEvent,\n\tToolCallEventResult,\n\tToolResultEventResult,\n} from \"./types.js\";\n\n/** Combined result from all before_agent_start handlers (internal) */\ninterface BeforeAgentStartCombinedResult {\n\tmessages?: NonNullable<BeforeAgentStartEventResult[\"message\"]>[];\n\tsystemPromptAppend?: string;\n}\n\n/**\n * Listener for hook errors.\n */\nexport type HookErrorListener = (error: HookError) => void;\n\n// Re-export execCommand for backward compatibility\nexport { execCommand } from \"../exec.js\";\n\n/** No-op UI context used when no UI is available */\nconst noOpUIContext: HookUIContext = {\n\tselect: async () => undefined,\n\tconfirm: async () => false,\n\tinput: async () => undefined,\n\tnotify: () => {},\n\tsetStatus: () => {},\n\tsetWidget: () => {},\n\tcustom: async () => undefined as never,\n\tsetEditorText: () => {},\n\tgetEditorText: () => \"\",\n\teditor: async () => undefined,\n\tget theme() {\n\t\treturn theme;\n\t},\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 sessionManager: SessionManager;\n\tprivate modelRegistry: ModelRegistry;\n\tprivate errorListeners: Set<HookErrorListener> = new Set();\n\tprivate getModel: () => Model<any> | undefined = () => undefined;\n\tprivate isIdleFn: () => boolean = () => true;\n\tprivate waitForIdleFn: () => Promise<void> = async () => {};\n\tprivate abortFn: () => void = () => {};\n\tprivate hasPendingMessagesFn: () => boolean = () => false;\n\tprivate newSessionHandler: NewSessionHandler = async () => ({ cancelled: false });\n\tprivate branchHandler: BranchHandler = async () => ({ cancelled: false });\n\tprivate navigateTreeHandler: NavigateTreeHandler = async () => ({ cancelled: false });\n\n\tconstructor(hooks: LoadedHook[], cwd: string, sessionManager: SessionManager, modelRegistry: ModelRegistry) {\n\t\tthis.hooks = hooks;\n\t\tthis.uiContext = noOpUIContext;\n\t\tthis.hasUI = false;\n\t\tthis.cwd = cwd;\n\t\tthis.sessionManager = sessionManager;\n\t\tthis.modelRegistry = modelRegistry;\n\t}\n\n\t/**\n\t * Initialize HookRunner with all required context.\n\t * Modes call this once the agent session is fully set up.\n\t */\n\tinitialize(options: {\n\t\t/** Function to get the current model */\n\t\tgetModel: () => Model<any> | undefined;\n\t\t/** Handler for hooks to send messages */\n\t\tsendMessageHandler: SendMessageHandler;\n\t\t/** Handler for hooks to append entries */\n\t\tappendEntryHandler: AppendEntryHandler;\n\t\t/** Handler for getting current active tools */\n\t\tgetActiveToolsHandler: () => string[];\n\t\t/** Handler for getting all configured tools */\n\t\tgetAllToolsHandler: () => string[];\n\t\t/** Handler for setting active tools */\n\t\tsetActiveToolsHandler: (toolNames: string[]) => void;\n\t\t/** Handler for creating new sessions (for HookCommandContext) */\n\t\tnewSessionHandler?: NewSessionHandler;\n\t\t/** Handler for branching sessions (for HookCommandContext) */\n\t\tbranchHandler?: BranchHandler;\n\t\t/** Handler for navigating session tree (for HookCommandContext) */\n\t\tnavigateTreeHandler?: NavigateTreeHandler;\n\t\t/** Function to check if agent is idle */\n\t\tisIdle?: () => boolean;\n\t\t/** Function to wait for agent to be idle */\n\t\twaitForIdle?: () => Promise<void>;\n\t\t/** Function to abort current operation (fire-and-forget) */\n\t\tabort?: () => void;\n\t\t/** Function to check if there are queued messages */\n\t\thasPendingMessages?: () => boolean;\n\t\t/** UI context for interactive prompts */\n\t\tuiContext?: HookUIContext;\n\t\t/** Whether UI is available */\n\t\thasUI?: boolean;\n\t}): void {\n\t\tthis.getModel = options.getModel;\n\t\tthis.isIdleFn = options.isIdle ?? (() => true);\n\t\tthis.waitForIdleFn = options.waitForIdle ?? (async () => {});\n\t\tthis.abortFn = options.abort ?? (() => {});\n\t\tthis.hasPendingMessagesFn = options.hasPendingMessages ?? (() => false);\n\t\t// Store session handlers for HookCommandContext\n\t\tif (options.newSessionHandler) {\n\t\t\tthis.newSessionHandler = options.newSessionHandler;\n\t\t}\n\t\tif (options.branchHandler) {\n\t\t\tthis.branchHandler = options.branchHandler;\n\t\t}\n\t\tif (options.navigateTreeHandler) {\n\t\t\tthis.navigateTreeHandler = options.navigateTreeHandler;\n\t\t}\n\t\t// Set per-hook handlers for pi.sendMessage(), pi.appendEntry(), pi.getActiveTools(), pi.getAllTools(), pi.setActiveTools()\n\t\tfor (const hook of this.hooks) {\n\t\t\thook.setSendMessageHandler(options.sendMessageHandler);\n\t\t\thook.setAppendEntryHandler(options.appendEntryHandler);\n\t\t\thook.setGetActiveToolsHandler(options.getActiveToolsHandler);\n\t\t\thook.setGetAllToolsHandler(options.getAllToolsHandler);\n\t\t\thook.setSetActiveToolsHandler(options.setActiveToolsHandler);\n\t\t}\n\t\tthis.uiContext = options.uiContext ?? noOpUIContext;\n\t\tthis.hasUI = options.hasUI ?? false;\n\t}\n\n\t/**\n\t * Get the UI context (set by mode).\n\t */\n\tgetUIContext(): HookUIContext | null {\n\t\treturn this.uiContext;\n\t}\n\n\t/**\n\t * Get whether UI is available.\n\t */\n\tgetHasUI(): boolean {\n\t\treturn this.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 * Get all CLI flags registered by hooks.\n\t */\n\tgetFlags(): Map<string, HookFlag> {\n\t\tconst allFlags = new Map<string, HookFlag>();\n\t\tfor (const hook of this.hooks) {\n\t\t\tfor (const [name, flag] of hook.flags) {\n\t\t\t\tallFlags.set(name, flag);\n\t\t\t}\n\t\t}\n\t\treturn allFlags;\n\t}\n\n\t/**\n\t * Set a flag value (after CLI parsing).\n\t */\n\tsetFlagValue(name: string, value: boolean | string): void {\n\t\tfor (const hook of this.hooks) {\n\t\t\tif (hook.flags.has(name)) {\n\t\t\t\thook.setFlagValue(name, value);\n\t\t\t}\n\t\t}\n\t}\n\n\t// Built-in shortcuts that hooks should not override\n\tprivate static readonly RESERVED_SHORTCUTS = new Set([\n\t\t\"ctrl+c\",\n\t\t\"ctrl+d\",\n\t\t\"ctrl+z\",\n\t\t\"ctrl+k\",\n\t\t\"ctrl+p\",\n\t\t\"ctrl+l\",\n\t\t\"ctrl+o\",\n\t\t\"ctrl+t\",\n\t\t\"ctrl+g\",\n\t\t\"shift+tab\",\n\t\t\"shift+ctrl+p\",\n\t\t\"alt+enter\",\n\t\t\"escape\",\n\t\t\"enter\",\n\t]);\n\n\t/**\n\t * Get all keyboard shortcuts registered by hooks.\n\t * When multiple hooks register the same shortcut, the last one wins.\n\t * Conflicts with built-in shortcuts are skipped with a warning.\n\t * Conflicts between hooks are logged as warnings.\n\t */\n\tgetShortcuts(): Map<KeyId, HookShortcut> {\n\t\tconst allShortcuts = new Map<KeyId, HookShortcut>();\n\t\tfor (const hook of this.hooks) {\n\t\t\tfor (const [key, shortcut] of hook.shortcuts) {\n\t\t\t\t// Normalize to lowercase for comparison (KeyId is string at runtime)\n\t\t\t\tconst normalizedKey = key.toLowerCase() as KeyId;\n\n\t\t\t\t// Check for built-in shortcut conflicts\n\t\t\t\tif (HookRunner.RESERVED_SHORTCUTS.has(normalizedKey)) {\n\t\t\t\t\tconsole.warn(\n\t\t\t\t\t\t`Hook shortcut '${key}' from ${shortcut.hookPath} conflicts with built-in shortcut. Skipping.`,\n\t\t\t\t\t);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst existing = allShortcuts.get(normalizedKey);\n\t\t\t\tif (existing) {\n\t\t\t\t\t// Log conflict between hooks - last one wins\n\t\t\t\t\tconsole.warn(\n\t\t\t\t\t\t`Hook shortcut conflict: '${key}' registered by both ${existing.hookPath} and ${shortcut.hookPath}. Using ${shortcut.hookPath}.`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tallShortcuts.set(normalizedKey, shortcut);\n\t\t\t}\n\t\t}\n\t\treturn allShortcuts;\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\t/**\n\t * Emit an error to all error listeners.\n\t */\n\temitError(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 * Get a message renderer for the given customType.\n\t * Returns the first renderer found across all hooks, or undefined if none.\n\t */\n\tgetMessageRenderer(customType: string): HookMessageRenderer | undefined {\n\t\tfor (const hook of this.hooks) {\n\t\t\tconst renderer = hook.messageRenderers.get(customType);\n\t\t\tif (renderer) {\n\t\t\t\treturn renderer;\n\t\t\t}\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Get all registered commands from all hooks.\n\t */\n\tgetRegisteredCommands(): RegisteredCommand[] {\n\t\tconst commands: RegisteredCommand[] = [];\n\t\tfor (const hook of this.hooks) {\n\t\t\tfor (const command of hook.commands.values()) {\n\t\t\t\tcommands.push(command);\n\t\t\t}\n\t\t}\n\t\treturn commands;\n\t}\n\n\t/**\n\t * Get a registered command by name.\n\t * Returns the first command found across all hooks, or undefined if none.\n\t */\n\tgetCommand(name: string): RegisteredCommand | undefined {\n\t\tfor (const hook of this.hooks) {\n\t\t\tconst command = hook.commands.get(name);\n\t\t\tif (command) {\n\t\t\t\treturn command;\n\t\t\t}\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Create the event context for handlers.\n\t */\n\tprivate createContext(): HookContext {\n\t\treturn {\n\t\t\tui: this.uiContext,\n\t\t\thasUI: this.hasUI,\n\t\t\tcwd: this.cwd,\n\t\t\tsessionManager: this.sessionManager,\n\t\t\tmodelRegistry: this.modelRegistry,\n\t\t\tmodel: this.getModel(),\n\t\t\tisIdle: () => this.isIdleFn(),\n\t\t\tabort: () => this.abortFn(),\n\t\t\thasPendingMessages: () => this.hasPendingMessagesFn(),\n\t\t};\n\t}\n\n\t/**\n\t * Create the command context for slash command handlers.\n\t * Extends HookContext with session control methods that are only safe in commands.\n\t */\n\tcreateCommandContext(): HookCommandContext {\n\t\treturn {\n\t\t\t...this.createContext(),\n\t\t\twaitForIdle: () => this.waitForIdleFn(),\n\t\t\tnewSession: (options) => this.newSessionHandler(options),\n\t\t\tbranch: (entryId) => this.branchHandler(entryId),\n\t\t\tnavigateTree: (targetId, options) => this.navigateTreeHandler(targetId, options),\n\t\t};\n\t}\n\n\t/**\n\t * Check if event type is a session \"before_*\" event that can be cancelled.\n\t */\n\tprivate isSessionBeforeEvent(\n\t\ttype: string,\n\t): type is \"session_before_switch\" | \"session_before_branch\" | \"session_before_compact\" | \"session_before_tree\" {\n\t\treturn (\n\t\t\ttype === \"session_before_switch\" ||\n\t\t\ttype === \"session_before_branch\" ||\n\t\t\ttype === \"session_before_compact\" ||\n\t\t\ttype === \"session_before_tree\"\n\t\t);\n\t}\n\n\t/**\n\t * Emit an event to all hooks.\n\t * Returns the result from session before_* / tool_result events (if any handler returns one).\n\t */\n\tasync emit(\n\t\tevent: HookEvent,\n\t): Promise<SessionBeforeCompactResult | SessionBeforeTreeResult | ToolResultEventResult | undefined> {\n\t\tconst ctx = this.createContext();\n\t\tlet result: SessionBeforeCompactResult | SessionBeforeTreeResult | 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 handlerResult = await handler(event, ctx);\n\n\t\t\t\t\t// For session before_* events, capture the result (for cancellation)\n\t\t\t\t\tif (this.isSessionBeforeEvent(event.type) && handlerResult) {\n\t\t\t\t\t\tresult = handlerResult as SessionBeforeCompactResult | SessionBeforeTreeResult;\n\t\t\t\t\t\t// If cancelled, stop processing further hooks\n\t\t\t\t\t\tif (result.cancel) {\n\t\t\t\t\t\t\treturn result;\n\t\t\t\t\t\t}\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\tconst stack = err instanceof Error ? err.stack : undefined;\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\tstack,\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\t/**\n\t * Emit a context event to all hooks.\n\t * Handlers are chained - each gets the previous handler's output (if any).\n\t * Returns the final modified messages, or the original if no modifications.\n\t *\n\t * Messages are deep-copied before passing to hooks, so mutations are safe.\n\t */\n\tasync emitContext(messages: AgentMessage[]): Promise<AgentMessage[]> {\n\t\tconst ctx = this.createContext();\n\t\tlet currentMessages = structuredClone(messages);\n\n\t\tfor (const hook of this.hooks) {\n\t\t\tconst handlers = hook.handlers.get(\"context\");\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 event: ContextEvent = { type: \"context\", messages: currentMessages };\n\t\t\t\t\tconst handlerResult = await handler(event, ctx);\n\n\t\t\t\t\tif (handlerResult && (handlerResult as ContextEventResult).messages) {\n\t\t\t\t\t\tcurrentMessages = (handlerResult as ContextEventResult).messages!;\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\tconst stack = err instanceof Error ? err.stack : undefined;\n\t\t\t\t\tthis.emitError({\n\t\t\t\t\t\thookPath: hook.path,\n\t\t\t\t\t\tevent: \"context\",\n\t\t\t\t\t\terror: message,\n\t\t\t\t\t\tstack,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn currentMessages;\n\t}\n\n\t/**\n\t * Emit before_agent_start event to all hooks.\n\t * Returns combined result: all messages and all systemPromptAppend strings concatenated.\n\t */\n\tasync emitBeforeAgentStart(\n\t\tprompt: string,\n\t\timages?: ImageContent[],\n\t): Promise<BeforeAgentStartCombinedResult | undefined> {\n\t\tconst ctx = this.createContext();\n\t\tconst messages: NonNullable<BeforeAgentStartEventResult[\"message\"]>[] = [];\n\t\tconst systemPromptAppends: string[] = [];\n\n\t\tfor (const hook of this.hooks) {\n\t\t\tconst handlers = hook.handlers.get(\"before_agent_start\");\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 event: BeforeAgentStartEvent = { type: \"before_agent_start\", prompt, images };\n\t\t\t\t\tconst handlerResult = await handler(event, ctx);\n\n\t\t\t\t\tif (handlerResult) {\n\t\t\t\t\t\tconst result = handlerResult as BeforeAgentStartEventResult;\n\t\t\t\t\t\t// Collect all messages\n\t\t\t\t\t\tif (result.message) {\n\t\t\t\t\t\t\tmessages.push(result.message);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Collect all systemPromptAppend strings\n\t\t\t\t\t\tif (result.systemPromptAppend) {\n\t\t\t\t\t\t\tsystemPromptAppends.push(result.systemPromptAppend);\n\t\t\t\t\t\t}\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\tconst stack = err instanceof Error ? err.stack : undefined;\n\t\t\t\t\tthis.emitError({\n\t\t\t\t\t\thookPath: hook.path,\n\t\t\t\t\t\tevent: \"before_agent_start\",\n\t\t\t\t\t\terror: message,\n\t\t\t\t\t\tstack,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Return combined result\n\t\tif (messages.length > 0 || systemPromptAppends.length > 0) {\n\t\t\treturn {\n\t\t\t\tmessages: messages.length > 0 ? messages : undefined,\n\t\t\t\tsystemPromptAppend: systemPromptAppends.length > 0 ? systemPromptAppends.join(\"\\n\\n\") : undefined,\n\t\t\t};\n\t\t}\n\n\t\treturn undefined;\n\t}\n}\n"]}
1
+ {"version":3,"file":"runner.js","sourceRoot":"","sources":["../../../src/core/hooks/runner.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Hook runner - executes hooks and manages their lifecycle.\n */\n\nimport type { AgentMessage } from \"@mariozechner/pi-agent-core\";\nimport type { ImageContent, Model } from \"@mariozechner/pi-ai\";\nimport type { KeyId } from \"@mariozechner/pi-tui\";\nimport { theme } from \"../../modes/interactive/theme/theme.js\";\nimport type { ModelRegistry } from \"../model-registry.js\";\nimport type { SessionManager } from \"../session-manager.js\";\nimport type {\n\tAppendEntryHandler,\n\tBranchHandler,\n\tHookFlag,\n\tHookShortcut,\n\tLoadedHook,\n\tNavigateTreeHandler,\n\tNewSessionHandler,\n\tSendMessageHandler,\n} from \"./loader.js\";\nimport type {\n\tBeforeAgentStartEvent,\n\tBeforeAgentStartEventResult,\n\tContextEvent,\n\tContextEventResult,\n\tHookCommandContext,\n\tHookContext,\n\tHookError,\n\tHookEvent,\n\tHookMessageRenderer,\n\tHookUIContext,\n\tRegisteredCommand,\n\tSessionBeforeCompactResult,\n\tSessionBeforeTreeResult,\n\tToolCallEvent,\n\tToolCallEventResult,\n\tToolResultEventResult,\n} from \"./types.js\";\n\n/** Combined result from all before_agent_start handlers (internal) */\ninterface BeforeAgentStartCombinedResult {\n\tmessages?: NonNullable<BeforeAgentStartEventResult[\"message\"]>[];\n\tsystemPromptAppend?: string;\n}\n\n/**\n * Listener for hook errors.\n */\nexport type HookErrorListener = (error: HookError) => void;\n\n// Re-export execCommand for backward compatibility\nexport { execCommand } from \"../exec.js\";\n\n/** No-op UI context used when no UI is available */\nconst noOpUIContext: HookUIContext = {\n\tselect: async () => undefined,\n\tconfirm: async () => false,\n\tinput: async () => undefined,\n\tnotify: () => {},\n\tsetStatus: () => {},\n\tsetWidget: () => {},\n\tsetTitle: () => {},\n\tcustom: async () => undefined as never,\n\tsetEditorText: () => {},\n\tgetEditorText: () => \"\",\n\teditor: async () => undefined,\n\tget theme() {\n\t\treturn theme;\n\t},\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 sessionManager: SessionManager;\n\tprivate modelRegistry: ModelRegistry;\n\tprivate errorListeners: Set<HookErrorListener> = new Set();\n\tprivate getModel: () => Model<any> | undefined = () => undefined;\n\tprivate isIdleFn: () => boolean = () => true;\n\tprivate waitForIdleFn: () => Promise<void> = async () => {};\n\tprivate abortFn: () => void = () => {};\n\tprivate hasPendingMessagesFn: () => boolean = () => false;\n\tprivate newSessionHandler: NewSessionHandler = async () => ({ cancelled: false });\n\tprivate branchHandler: BranchHandler = async () => ({ cancelled: false });\n\tprivate navigateTreeHandler: NavigateTreeHandler = async () => ({ cancelled: false });\n\n\tconstructor(hooks: LoadedHook[], cwd: string, sessionManager: SessionManager, modelRegistry: ModelRegistry) {\n\t\tthis.hooks = hooks;\n\t\tthis.uiContext = noOpUIContext;\n\t\tthis.hasUI = false;\n\t\tthis.cwd = cwd;\n\t\tthis.sessionManager = sessionManager;\n\t\tthis.modelRegistry = modelRegistry;\n\t}\n\n\t/**\n\t * Initialize HookRunner with all required context.\n\t * Modes call this once the agent session is fully set up.\n\t */\n\tinitialize(options: {\n\t\t/** Function to get the current model */\n\t\tgetModel: () => Model<any> | undefined;\n\t\t/** Handler for hooks to send messages */\n\t\tsendMessageHandler: SendMessageHandler;\n\t\t/** Handler for hooks to append entries */\n\t\tappendEntryHandler: AppendEntryHandler;\n\t\t/** Handler for getting current active tools */\n\t\tgetActiveToolsHandler: () => string[];\n\t\t/** Handler for getting all configured tools */\n\t\tgetAllToolsHandler: () => string[];\n\t\t/** Handler for setting active tools */\n\t\tsetActiveToolsHandler: (toolNames: string[]) => void;\n\t\t/** Handler for creating new sessions (for HookCommandContext) */\n\t\tnewSessionHandler?: NewSessionHandler;\n\t\t/** Handler for branching sessions (for HookCommandContext) */\n\t\tbranchHandler?: BranchHandler;\n\t\t/** Handler for navigating session tree (for HookCommandContext) */\n\t\tnavigateTreeHandler?: NavigateTreeHandler;\n\t\t/** Function to check if agent is idle */\n\t\tisIdle?: () => boolean;\n\t\t/** Function to wait for agent to be idle */\n\t\twaitForIdle?: () => Promise<void>;\n\t\t/** Function to abort current operation (fire-and-forget) */\n\t\tabort?: () => void;\n\t\t/** Function to check if there are queued messages */\n\t\thasPendingMessages?: () => boolean;\n\t\t/** UI context for interactive prompts */\n\t\tuiContext?: HookUIContext;\n\t\t/** Whether UI is available */\n\t\thasUI?: boolean;\n\t}): void {\n\t\tthis.getModel = options.getModel;\n\t\tthis.isIdleFn = options.isIdle ?? (() => true);\n\t\tthis.waitForIdleFn = options.waitForIdle ?? (async () => {});\n\t\tthis.abortFn = options.abort ?? (() => {});\n\t\tthis.hasPendingMessagesFn = options.hasPendingMessages ?? (() => false);\n\t\t// Store session handlers for HookCommandContext\n\t\tif (options.newSessionHandler) {\n\t\t\tthis.newSessionHandler = options.newSessionHandler;\n\t\t}\n\t\tif (options.branchHandler) {\n\t\t\tthis.branchHandler = options.branchHandler;\n\t\t}\n\t\tif (options.navigateTreeHandler) {\n\t\t\tthis.navigateTreeHandler = options.navigateTreeHandler;\n\t\t}\n\t\t// Set per-hook handlers for pi.sendMessage(), pi.appendEntry(), pi.getActiveTools(), pi.getAllTools(), pi.setActiveTools()\n\t\tfor (const hook of this.hooks) {\n\t\t\thook.setSendMessageHandler(options.sendMessageHandler);\n\t\t\thook.setAppendEntryHandler(options.appendEntryHandler);\n\t\t\thook.setGetActiveToolsHandler(options.getActiveToolsHandler);\n\t\t\thook.setGetAllToolsHandler(options.getAllToolsHandler);\n\t\t\thook.setSetActiveToolsHandler(options.setActiveToolsHandler);\n\t\t}\n\t\tthis.uiContext = options.uiContext ?? noOpUIContext;\n\t\tthis.hasUI = options.hasUI ?? false;\n\t}\n\n\t/**\n\t * Get the UI context (set by mode).\n\t */\n\tgetUIContext(): HookUIContext | null {\n\t\treturn this.uiContext;\n\t}\n\n\t/**\n\t * Get whether UI is available.\n\t */\n\tgetHasUI(): boolean {\n\t\treturn this.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 * Get all CLI flags registered by hooks.\n\t */\n\tgetFlags(): Map<string, HookFlag> {\n\t\tconst allFlags = new Map<string, HookFlag>();\n\t\tfor (const hook of this.hooks) {\n\t\t\tfor (const [name, flag] of hook.flags) {\n\t\t\t\tallFlags.set(name, flag);\n\t\t\t}\n\t\t}\n\t\treturn allFlags;\n\t}\n\n\t/**\n\t * Set a flag value (after CLI parsing).\n\t */\n\tsetFlagValue(name: string, value: boolean | string): void {\n\t\tfor (const hook of this.hooks) {\n\t\t\tif (hook.flags.has(name)) {\n\t\t\t\thook.setFlagValue(name, value);\n\t\t\t}\n\t\t}\n\t}\n\n\t// Built-in shortcuts that hooks should not override\n\tprivate static readonly RESERVED_SHORTCUTS = new Set([\n\t\t\"ctrl+c\",\n\t\t\"ctrl+d\",\n\t\t\"ctrl+z\",\n\t\t\"ctrl+k\",\n\t\t\"ctrl+p\",\n\t\t\"ctrl+l\",\n\t\t\"ctrl+o\",\n\t\t\"ctrl+t\",\n\t\t\"ctrl+g\",\n\t\t\"shift+tab\",\n\t\t\"shift+ctrl+p\",\n\t\t\"alt+enter\",\n\t\t\"escape\",\n\t\t\"enter\",\n\t]);\n\n\t/**\n\t * Get all keyboard shortcuts registered by hooks.\n\t * When multiple hooks register the same shortcut, the last one wins.\n\t * Conflicts with built-in shortcuts are skipped with a warning.\n\t * Conflicts between hooks are logged as warnings.\n\t */\n\tgetShortcuts(): Map<KeyId, HookShortcut> {\n\t\tconst allShortcuts = new Map<KeyId, HookShortcut>();\n\t\tfor (const hook of this.hooks) {\n\t\t\tfor (const [key, shortcut] of hook.shortcuts) {\n\t\t\t\t// Normalize to lowercase for comparison (KeyId is string at runtime)\n\t\t\t\tconst normalizedKey = key.toLowerCase() as KeyId;\n\n\t\t\t\t// Check for built-in shortcut conflicts\n\t\t\t\tif (HookRunner.RESERVED_SHORTCUTS.has(normalizedKey)) {\n\t\t\t\t\tconsole.warn(\n\t\t\t\t\t\t`Hook shortcut '${key}' from ${shortcut.hookPath} conflicts with built-in shortcut. Skipping.`,\n\t\t\t\t\t);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst existing = allShortcuts.get(normalizedKey);\n\t\t\t\tif (existing) {\n\t\t\t\t\t// Log conflict between hooks - last one wins\n\t\t\t\t\tconsole.warn(\n\t\t\t\t\t\t`Hook shortcut conflict: '${key}' registered by both ${existing.hookPath} and ${shortcut.hookPath}. Using ${shortcut.hookPath}.`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tallShortcuts.set(normalizedKey, shortcut);\n\t\t\t}\n\t\t}\n\t\treturn allShortcuts;\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\t/**\n\t * Emit an error to all error listeners.\n\t */\n\temitError(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 * Get a message renderer for the given customType.\n\t * Returns the first renderer found across all hooks, or undefined if none.\n\t */\n\tgetMessageRenderer(customType: string): HookMessageRenderer | undefined {\n\t\tfor (const hook of this.hooks) {\n\t\t\tconst renderer = hook.messageRenderers.get(customType);\n\t\t\tif (renderer) {\n\t\t\t\treturn renderer;\n\t\t\t}\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Get all registered commands from all hooks.\n\t */\n\tgetRegisteredCommands(): RegisteredCommand[] {\n\t\tconst commands: RegisteredCommand[] = [];\n\t\tfor (const hook of this.hooks) {\n\t\t\tfor (const command of hook.commands.values()) {\n\t\t\t\tcommands.push(command);\n\t\t\t}\n\t\t}\n\t\treturn commands;\n\t}\n\n\t/**\n\t * Get a registered command by name.\n\t * Returns the first command found across all hooks, or undefined if none.\n\t */\n\tgetCommand(name: string): RegisteredCommand | undefined {\n\t\tfor (const hook of this.hooks) {\n\t\t\tconst command = hook.commands.get(name);\n\t\t\tif (command) {\n\t\t\t\treturn command;\n\t\t\t}\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Create the event context for handlers.\n\t */\n\tprivate createContext(): HookContext {\n\t\treturn {\n\t\t\tui: this.uiContext,\n\t\t\thasUI: this.hasUI,\n\t\t\tcwd: this.cwd,\n\t\t\tsessionManager: this.sessionManager,\n\t\t\tmodelRegistry: this.modelRegistry,\n\t\t\tmodel: this.getModel(),\n\t\t\tisIdle: () => this.isIdleFn(),\n\t\t\tabort: () => this.abortFn(),\n\t\t\thasPendingMessages: () => this.hasPendingMessagesFn(),\n\t\t};\n\t}\n\n\t/**\n\t * Create the command context for slash command handlers.\n\t * Extends HookContext with session control methods that are only safe in commands.\n\t */\n\tcreateCommandContext(): HookCommandContext {\n\t\treturn {\n\t\t\t...this.createContext(),\n\t\t\twaitForIdle: () => this.waitForIdleFn(),\n\t\t\tnewSession: (options) => this.newSessionHandler(options),\n\t\t\tbranch: (entryId) => this.branchHandler(entryId),\n\t\t\tnavigateTree: (targetId, options) => this.navigateTreeHandler(targetId, options),\n\t\t};\n\t}\n\n\t/**\n\t * Check if event type is a session \"before_*\" event that can be cancelled.\n\t */\n\tprivate isSessionBeforeEvent(\n\t\ttype: string,\n\t): type is \"session_before_switch\" | \"session_before_branch\" | \"session_before_compact\" | \"session_before_tree\" {\n\t\treturn (\n\t\t\ttype === \"session_before_switch\" ||\n\t\t\ttype === \"session_before_branch\" ||\n\t\t\ttype === \"session_before_compact\" ||\n\t\t\ttype === \"session_before_tree\"\n\t\t);\n\t}\n\n\t/**\n\t * Emit an event to all hooks.\n\t * Returns the result from session before_* / tool_result events (if any handler returns one).\n\t */\n\tasync emit(\n\t\tevent: HookEvent,\n\t): Promise<SessionBeforeCompactResult | SessionBeforeTreeResult | ToolResultEventResult | undefined> {\n\t\tconst ctx = this.createContext();\n\t\tlet result: SessionBeforeCompactResult | SessionBeforeTreeResult | 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 handlerResult = await handler(event, ctx);\n\n\t\t\t\t\t// For session before_* events, capture the result (for cancellation)\n\t\t\t\t\tif (this.isSessionBeforeEvent(event.type) && handlerResult) {\n\t\t\t\t\t\tresult = handlerResult as SessionBeforeCompactResult | SessionBeforeTreeResult;\n\t\t\t\t\t\t// If cancelled, stop processing further hooks\n\t\t\t\t\t\tif (result.cancel) {\n\t\t\t\t\t\t\treturn result;\n\t\t\t\t\t\t}\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\tconst stack = err instanceof Error ? err.stack : undefined;\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\tstack,\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\t/**\n\t * Emit a context event to all hooks.\n\t * Handlers are chained - each gets the previous handler's output (if any).\n\t * Returns the final modified messages, or the original if no modifications.\n\t *\n\t * Messages are deep-copied before passing to hooks, so mutations are safe.\n\t */\n\tasync emitContext(messages: AgentMessage[]): Promise<AgentMessage[]> {\n\t\tconst ctx = this.createContext();\n\t\tlet currentMessages = structuredClone(messages);\n\n\t\tfor (const hook of this.hooks) {\n\t\t\tconst handlers = hook.handlers.get(\"context\");\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 event: ContextEvent = { type: \"context\", messages: currentMessages };\n\t\t\t\t\tconst handlerResult = await handler(event, ctx);\n\n\t\t\t\t\tif (handlerResult && (handlerResult as ContextEventResult).messages) {\n\t\t\t\t\t\tcurrentMessages = (handlerResult as ContextEventResult).messages!;\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\tconst stack = err instanceof Error ? err.stack : undefined;\n\t\t\t\t\tthis.emitError({\n\t\t\t\t\t\thookPath: hook.path,\n\t\t\t\t\t\tevent: \"context\",\n\t\t\t\t\t\terror: message,\n\t\t\t\t\t\tstack,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn currentMessages;\n\t}\n\n\t/**\n\t * Emit before_agent_start event to all hooks.\n\t * Returns combined result: all messages and all systemPromptAppend strings concatenated.\n\t */\n\tasync emitBeforeAgentStart(\n\t\tprompt: string,\n\t\timages?: ImageContent[],\n\t): Promise<BeforeAgentStartCombinedResult | undefined> {\n\t\tconst ctx = this.createContext();\n\t\tconst messages: NonNullable<BeforeAgentStartEventResult[\"message\"]>[] = [];\n\t\tconst systemPromptAppends: string[] = [];\n\n\t\tfor (const hook of this.hooks) {\n\t\t\tconst handlers = hook.handlers.get(\"before_agent_start\");\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 event: BeforeAgentStartEvent = { type: \"before_agent_start\", prompt, images };\n\t\t\t\t\tconst handlerResult = await handler(event, ctx);\n\n\t\t\t\t\tif (handlerResult) {\n\t\t\t\t\t\tconst result = handlerResult as BeforeAgentStartEventResult;\n\t\t\t\t\t\t// Collect all messages\n\t\t\t\t\t\tif (result.message) {\n\t\t\t\t\t\t\tmessages.push(result.message);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Collect all systemPromptAppend strings\n\t\t\t\t\t\tif (result.systemPromptAppend) {\n\t\t\t\t\t\t\tsystemPromptAppends.push(result.systemPromptAppend);\n\t\t\t\t\t\t}\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\tconst stack = err instanceof Error ? err.stack : undefined;\n\t\t\t\t\tthis.emitError({\n\t\t\t\t\t\thookPath: hook.path,\n\t\t\t\t\t\tevent: \"before_agent_start\",\n\t\t\t\t\t\terror: message,\n\t\t\t\t\t\tstack,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Return combined result\n\t\tif (messages.length > 0 || systemPromptAppends.length > 0) {\n\t\t\treturn {\n\t\t\t\tmessages: messages.length > 0 ? messages : undefined,\n\t\t\t\tsystemPromptAppend: systemPromptAppends.length > 0 ? systemPromptAppends.join(\"\\n\\n\") : undefined,\n\t\t\t};\n\t\t}\n\n\t\treturn undefined;\n\t}\n}\n"]}
@@ -98,6 +98,12 @@ export interface HookUIContext {
98
98
  setWidget(key: string, content: ((tui: TUI, theme: Theme) => Component & {
99
99
  dispose?(): void;
100
100
  }) | undefined): void;
101
+ /**
102
+ * Set the terminal window/tab title.
103
+ * Uses OSC escape sequence (works in most modern terminals).
104
+ * @param title - Title text to display
105
+ */
106
+ setTitle(title: string): void;
101
107
  /**
102
108
  * Show a custom component with keyboard focus.
103
109
  * The factory receives TUI, theme, and a done() callback to close the component.