@mariozechner/pi-coding-agent 0.29.0 → 0.29.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 +13 -0
- package/README.md +27 -12
- package/dist/core/custom-tools/loader.d.ts +5 -0
- package/dist/core/custom-tools/loader.d.ts.map +1 -1
- package/dist/core/custom-tools/loader.js +58 -3
- package/dist/core/custom-tools/loader.js.map +1 -1
- package/dist/core/hooks/loader.d.ts.map +1 -1
- package/dist/core/hooks/loader.js +8 -1
- package/dist/core/hooks/loader.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +19 -2
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +33 -0
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -0
- package/dist/modes/interactive/components/settings-selector.js +164 -0
- package/dist/modes/interactive/components/settings-selector.js.map +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts +1 -5
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +70 -116
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/theme.d.ts +1 -0
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +9 -0
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
## [0.29.1] - 2025-12-25
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **Automatic custom system prompt loading**: Pi now auto-loads `SYSTEM.md` files to replace the default system prompt. Project-local `.pi/SYSTEM.md` takes precedence over global `~/.pi/agent/SYSTEM.md`. CLI `--system-prompt` flag overrides both. ([#309](https://github.com/badlogic/pi-mono/issues/309))
|
|
10
|
+
- **Unified `/settings` command**: New settings menu consolidating thinking level, theme, queue mode, auto-compact, show images, hide thinking, and collapse changelog. Replaces individual `/thinking`, `/queue`, `/theme`, `/autocompact`, and `/show-images` commands. ([#310](https://github.com/badlogic/pi-mono/issues/310))
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **Custom tools/hooks with typebox subpath imports**: Fixed jiti alias for `@sinclair/typebox` to point to package root instead of entry file, allowing imports like `@sinclair/typebox/compiler` to resolve correctly. ([#311](https://github.com/badlogic/pi-mono/issues/311) by [@kim0](https://github.com/kim0))
|
|
15
|
+
|
|
3
16
|
## [0.29.0] - 2025-12-25
|
|
4
17
|
|
|
5
18
|
### Breaking Changes
|
package/README.md
CHANGED
|
@@ -23,6 +23,7 @@ Works on Linux, macOS, and Windows (requires bash; see [Windows Setup](#windows-
|
|
|
23
23
|
- [Branching](#branching)
|
|
24
24
|
- [Configuration](#configuration)
|
|
25
25
|
- [Project Context Files](#project-context-files)
|
|
26
|
+
- [Custom System Prompt](#custom-system-prompt)
|
|
26
27
|
- [Custom Models and Providers](#custom-models-and-providers)
|
|
27
28
|
- [Themes](#themes)
|
|
28
29
|
- [Custom Slash Commands](#custom-slash-commands)
|
|
@@ -186,9 +187,8 @@ The agent reads, writes, and edits files, and executes commands via bash.
|
|
|
186
187
|
|
|
187
188
|
| Command | Description |
|
|
188
189
|
|---------|-------------|
|
|
190
|
+
| `/settings` | Open settings menu (thinking, theme, queue mode, toggles) |
|
|
189
191
|
| `/model` | Switch models mid-session (fuzzy search, arrow keys, Enter to select) |
|
|
190
|
-
| `/thinking` | Adjust thinking level for reasoning models (off/minimal/low/medium/high) |
|
|
191
|
-
| `/queue` | Set message queue mode: one-at-a-time (default) or all-at-once |
|
|
192
192
|
| `/export [file]` | Export session to self-contained HTML |
|
|
193
193
|
| `/session` | Show session info: path, message counts, token usage, cost |
|
|
194
194
|
| `/hotkeys` | Show all keyboard shortcuts |
|
|
@@ -200,9 +200,6 @@ The agent reads, writes, and edits files, and executes commands via bash.
|
|
|
200
200
|
| `/new` | Start a new session |
|
|
201
201
|
| `/copy` | Copy last agent message to clipboard |
|
|
202
202
|
| `/compact [instructions]` | Manually compact conversation context |
|
|
203
|
-
| `/autocompact` | Toggle automatic context compaction |
|
|
204
|
-
| `/theme` | Select color theme |
|
|
205
|
-
| `/show-images` | Toggle inline image display (supported terminals only) |
|
|
206
203
|
|
|
207
204
|
### Editor Features
|
|
208
205
|
|
|
@@ -214,7 +211,7 @@ The agent reads, writes, and edits files, and executes commands via bash.
|
|
|
214
211
|
|
|
215
212
|
**Multi-line paste:** Pasted content is collapsed to `[paste #N <lines> lines]` but sent in full.
|
|
216
213
|
|
|
217
|
-
**Message queuing:** Submit messages while the agent is working. They queue and process based on
|
|
214
|
+
**Message queuing:** Submit messages while the agent is working. They queue and process based on queue mode (configurable via `/settings`). Press Escape to abort and restore queued messages to editor.
|
|
218
215
|
|
|
219
216
|
### Keyboard Shortcuts
|
|
220
217
|
|
|
@@ -287,7 +284,7 @@ Supported formats: `.jpg`, `.jpeg`, `.png`, `.gif`, `.webp`
|
|
|
287
284
|
|
|
288
285
|
**Inline rendering:** On terminals that support the Kitty graphics protocol (Kitty, Ghostty, WezTerm) or iTerm2 inline images, images in tool output are rendered inline. On unsupported terminals, a text placeholder is shown instead.
|
|
289
286
|
|
|
290
|
-
Toggle inline images
|
|
287
|
+
Toggle inline images via `/settings` or set `terminal.showImages: false` in settings.
|
|
291
288
|
|
|
292
289
|
---
|
|
293
290
|
|
|
@@ -315,7 +312,7 @@ Long sessions can exhaust context windows. Compaction summarizes older messages
|
|
|
315
312
|
|
|
316
313
|
**Manual:** `/compact` or `/compact Focus on the API changes`
|
|
317
314
|
|
|
318
|
-
**Automatic:** Enable
|
|
315
|
+
**Automatic:** Enable via `/settings`. When enabled, triggers in two cases:
|
|
319
316
|
- **Overflow recovery**: LLM returns context overflow error. Compacts and auto-retries.
|
|
320
317
|
- **Threshold maintenance**: Context exceeds `contextWindow - reserveTokens` after a successful turn. Compacts without retry.
|
|
321
318
|
|
|
@@ -381,6 +378,26 @@ Use these for:
|
|
|
381
378
|
- Prefer async/await over promises
|
|
382
379
|
```
|
|
383
380
|
|
|
381
|
+
### Custom System Prompt
|
|
382
|
+
|
|
383
|
+
Replace the default system prompt entirely by creating a `SYSTEM.md` file:
|
|
384
|
+
|
|
385
|
+
1. **Project-local:** `.pi/SYSTEM.md` (takes precedence)
|
|
386
|
+
2. **Global:** `~/.pi/agent/SYSTEM.md` (fallback)
|
|
387
|
+
|
|
388
|
+
This is useful when using pi as different types of agents across repos (coding assistant, personal assistant, domain-specific agent, etc.).
|
|
389
|
+
|
|
390
|
+
```markdown
|
|
391
|
+
You are a technical writing assistant. Help users write clear documentation.
|
|
392
|
+
|
|
393
|
+
Focus on:
|
|
394
|
+
- Concise explanations
|
|
395
|
+
- Code examples
|
|
396
|
+
- Proper formatting
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
The `--system-prompt` CLI flag overrides both files. Use `--append-system-prompt` to add to (rather than replace) the prompt.
|
|
400
|
+
|
|
384
401
|
### Custom Models and Providers
|
|
385
402
|
|
|
386
403
|
Add custom models (Ollama, vLLM, LM Studio, etc.) via `~/.pi/agent/models.json`:
|
|
@@ -459,9 +476,7 @@ Add custom models (Ollama, vLLM, LM Studio, etc.) via `~/.pi/agent/models.json`:
|
|
|
459
476
|
|
|
460
477
|
Built-in themes: `dark` (default), `light`. Auto-detected on first run.
|
|
461
478
|
|
|
462
|
-
|
|
463
|
-
/theme # Interactive selector
|
|
464
|
-
```
|
|
479
|
+
Select theme via `/settings` or set in `~/.pi/agent/settings.json`.
|
|
465
480
|
|
|
466
481
|
**Custom themes:** Create `~/.pi/agent/themes/*.json`. Custom themes support live reload.
|
|
467
482
|
|
|
@@ -470,7 +485,7 @@ mkdir -p ~/.pi/agent/themes
|
|
|
470
485
|
cp $(npm root -g)/@mariozechner/pi-coding-agent/dist/theme/dark.json ~/.pi/agent/themes/my-theme.json
|
|
471
486
|
```
|
|
472
487
|
|
|
473
|
-
Select with `/
|
|
488
|
+
Select with `/settings`, then edit the file. Changes apply on save.
|
|
474
489
|
|
|
475
490
|
> See [Theme Documentation](docs/theme.md) on how to create custom themes in detail. Pi can help you create a new one.
|
|
476
491
|
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Custom tool loader - loads TypeScript tool modules using jiti.
|
|
3
|
+
*
|
|
4
|
+
* For Bun compiled binaries, custom tools that import from @mariozechner/* packages
|
|
5
|
+
* are not supported because Bun's plugin system doesn't intercept imports from
|
|
6
|
+
* external files loaded at runtime. Users should use the npm-installed version
|
|
7
|
+
* for custom tools that depend on pi packages.
|
|
3
8
|
*/
|
|
4
9
|
import type { CustomToolsLoadResult } from "./types.js";
|
|
5
10
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../../src/core/custom-tools/loader.ts"],"names":[],"mappings":"AAAA;;GAEG;AAWH,OAAO,KAAK,EAEX,qBAAqB,EAKrB,MAAM,YAAY,CAAC;AAmMpB;;;;;GAKG;AACH,wBAAsB,eAAe,CACpC,KAAK,EAAE,MAAM,EAAE,EACf,GAAG,EAAE,MAAM,EACX,gBAAgB,EAAE,MAAM,EAAE,GACxB,OAAO,CAAC,qBAAqB,CAAC,CA8ChC;AAgCD;;;;;;;;;;;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\nimport { spawn } from \"node:child_process\";\nimport * as fs from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { createJiti } from \"jiti\";\nimport { getAgentDir } from \"../../config.js\";\nimport type { HookUIContext } from \"../hooks/types.js\";\nimport type {\n\tCustomToolFactory,\n\tCustomToolsLoadResult,\n\tExecOptions,\n\tExecResult,\n\tLoadedCustomTool,\n\tToolAPI,\n} from \"./types.js\";\n\n// Create require function to resolve module paths at runtime\nconst require = createRequire(import.meta.url);\n\n// Lazily computed aliases - resolved at runtime to handle global installs\nlet _aliases: Record<string, string> | null = null;\nfunction getAliases(): Record<string, string> {\n\tif (_aliases) return _aliases;\n\n\tconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\tconst packageIndex = path.resolve(__dirname, \"../..\", \"index.js\");\n\n\t_aliases = {\n\t\t\"@mariozechner/pi-coding-agent\": packageIndex,\n\t\t\"@mariozechner/pi-tui\": require.resolve(\"@mariozechner/pi-tui\"),\n\t\t\"@mariozechner/pi-ai\": require.resolve(\"@mariozechner/pi-ai\"),\n\t\t\"@sinclair/typebox\": require.resolve(\"@sinclair/typebox\"),\n\t};\n\treturn _aliases;\n}\n\nconst UNICODE_SPACES = /[\\u00A0\\u2000-\\u200A\\u202F\\u205F\\u3000]/g;\n\nfunction normalizeUnicodeSpaces(str: string): string {\n\treturn str.replace(UNICODE_SPACES, \" \");\n}\n\nfunction expandPath(p: string): string {\n\tconst normalized = normalizeUnicodeSpaces(p);\n\tif (normalized.startsWith(\"~/\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(2));\n\t}\n\tif (normalized.startsWith(\"~\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(1));\n\t}\n\treturn normalized;\n}\n\n/**\n * Resolve tool path.\n * - Absolute paths used as-is\n * - Paths starting with ~ expanded to home directory\n * - Relative paths resolved from cwd\n */\nfunction resolveToolPath(toolPath: string, cwd: string): string {\n\tconst expanded = expandPath(toolPath);\n\n\tif (path.isAbsolute(expanded)) {\n\t\treturn expanded;\n\t}\n\n\t// Relative paths resolved from cwd\n\treturn path.resolve(cwd, expanded);\n}\n\n/**\n * Execute a command and return stdout/stderr/code.\n * Supports cancellation via AbortSignal and timeout.\n */\nasync function execCommand(command: string, args: string[], cwd: string, options?: ExecOptions): Promise<ExecResult> {\n\treturn new Promise((resolve) => {\n\t\tconst proc = spawn(command, args, {\n\t\t\tcwd,\n\t\t\tshell: false,\n\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t});\n\n\t\tlet stdout = \"\";\n\t\tlet stderr = \"\";\n\t\tlet killed = false;\n\t\tlet timeoutId: NodeJS.Timeout | undefined;\n\n\t\tconst killProcess = () => {\n\t\t\tif (!killed) {\n\t\t\t\tkilled = true;\n\t\t\t\tproc.kill(\"SIGTERM\");\n\t\t\t\t// Force kill after 5 seconds if SIGTERM doesn't work\n\t\t\t\tsetTimeout(() => {\n\t\t\t\t\tif (!proc.killed) {\n\t\t\t\t\t\tproc.kill(\"SIGKILL\");\n\t\t\t\t\t}\n\t\t\t\t}, 5000);\n\t\t\t}\n\t\t};\n\n\t\t// Handle abort signal\n\t\tif (options?.signal) {\n\t\t\tif (options.signal.aborted) {\n\t\t\t\tkillProcess();\n\t\t\t} else {\n\t\t\t\toptions.signal.addEventListener(\"abort\", killProcess, { once: true });\n\t\t\t}\n\t\t}\n\n\t\t// Handle timeout\n\t\tif (options?.timeout && options.timeout > 0) {\n\t\t\ttimeoutId = setTimeout(() => {\n\t\t\t\tkillProcess();\n\t\t\t}, options.timeout);\n\t\t}\n\n\t\tproc.stdout.on(\"data\", (data) => {\n\t\t\tstdout += data.toString();\n\t\t});\n\n\t\tproc.stderr.on(\"data\", (data) => {\n\t\t\tstderr += data.toString();\n\t\t});\n\n\t\tproc.on(\"close\", (code) => {\n\t\t\tif (timeoutId) clearTimeout(timeoutId);\n\t\t\tif (options?.signal) {\n\t\t\t\toptions.signal.removeEventListener(\"abort\", killProcess);\n\t\t\t}\n\t\t\tresolve({\n\t\t\t\tstdout,\n\t\t\t\tstderr,\n\t\t\t\tcode: code ?? 0,\n\t\t\t\tkilled,\n\t\t\t});\n\t\t});\n\n\t\tproc.on(\"error\", (err) => {\n\t\t\tif (timeoutId) clearTimeout(timeoutId);\n\t\t\tif (options?.signal) {\n\t\t\t\toptions.signal.removeEventListener(\"abort\", killProcess);\n\t\t\t}\n\t\t\tresolve({\n\t\t\t\tstdout,\n\t\t\t\tstderr: stderr || err.message,\n\t\t\t\tcode: 1,\n\t\t\t\tkilled,\n\t\t\t});\n\t\t});\n\t});\n}\n\n/**\n * Create a no-op UI context for headless modes.\n */\nfunction createNoOpUIContext(): HookUIContext {\n\treturn {\n\t\tselect: async () => null,\n\t\tconfirm: async () => false,\n\t\tinput: async () => null,\n\t\tnotify: () => {},\n\t};\n}\n\n/**\n * Load a single tool module using jiti.\n */\nasync function loadTool(\n\ttoolPath: string,\n\tcwd: string,\n\tsharedApi: ToolAPI,\n): Promise<{ tools: LoadedCustomTool[] | null; error: string | null }> {\n\tconst resolvedPath = resolveToolPath(toolPath, cwd);\n\n\ttry {\n\t\t// Create jiti instance for TypeScript/ESM loading\n\t\t// Use aliases to resolve package imports since tools are loaded from user directories\n\t\t// (e.g. ~/.pi/agent/tools) but import from packages installed with pi-coding-agent\n\t\tconst jiti = createJiti(import.meta.url, {\n\t\t\talias: getAliases(),\n\t\t});\n\n\t\t// Import the module\n\t\tconst module = await jiti.import(resolvedPath, { default: true });\n\t\tconst factory = module as CustomToolFactory;\n\n\t\tif (typeof factory !== \"function\") {\n\t\t\treturn { tools: null, error: \"Tool must export a default function\" };\n\t\t}\n\n\t\t// Call factory with shared API\n\t\tconst result = await factory(sharedApi);\n\n\t\t// Handle single tool or array of tools\n\t\tconst toolsArray = Array.isArray(result) ? result : [result];\n\n\t\tconst loadedTools: LoadedCustomTool[] = toolsArray.map((tool) => ({\n\t\t\tpath: toolPath,\n\t\t\tresolvedPath,\n\t\t\ttool,\n\t\t}));\n\n\t\treturn { tools: loadedTools, error: null };\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\treturn { tools: null, error: `Failed to load tool: ${message}` };\n\t}\n}\n\n/**\n * Load all tools from configuration.\n * @param paths - Array of tool file paths\n * @param cwd - Current working directory for resolving relative paths\n * @param builtInToolNames - Names of built-in tools to check for conflicts\n */\nexport async function loadCustomTools(\n\tpaths: string[],\n\tcwd: string,\n\tbuiltInToolNames: string[],\n): Promise<CustomToolsLoadResult> {\n\tconst tools: LoadedCustomTool[] = [];\n\tconst errors: Array<{ path: string; error: string }> = [];\n\tconst seenNames = new Set<string>(builtInToolNames);\n\n\t// Shared API object - all tools get the same instance\n\tconst sharedApi: ToolAPI = {\n\t\tcwd,\n\t\texec: (command: string, args: string[], options?: ExecOptions) => execCommand(command, args, cwd, options),\n\t\tui: createNoOpUIContext(),\n\t\thasUI: false,\n\t};\n\n\tfor (const toolPath of paths) {\n\t\tconst { tools: loadedTools, error } = await loadTool(toolPath, cwd, sharedApi);\n\n\t\tif (error) {\n\t\t\terrors.push({ path: toolPath, error });\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (loadedTools) {\n\t\t\tfor (const loadedTool of loadedTools) {\n\t\t\t\t// Check for name conflicts\n\t\t\t\tif (seenNames.has(loadedTool.tool.name)) {\n\t\t\t\t\terrors.push({\n\t\t\t\t\t\tpath: toolPath,\n\t\t\t\t\t\terror: `Tool name \"${loadedTool.tool.name}\" conflicts with existing tool`,\n\t\t\t\t\t});\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tseenNames.add(loadedTool.tool.name);\n\t\t\t\ttools.push(loadedTool);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn {\n\t\ttools,\n\t\terrors,\n\t\tsetUIContext(uiContext, hasUI) {\n\t\t\tsharedApi.ui = uiContext;\n\t\t\tsharedApi.hasUI = hasUI;\n\t\t},\n\t};\n}\n\n/**\n * Discover tool files from a directory.\n * Only loads index.ts files from subdirectories (e.g., tools/mytool/index.ts).\n */\nfunction discoverToolsInDir(dir: string): string[] {\n\tif (!fs.existsSync(dir)) {\n\t\treturn [];\n\t}\n\n\tconst tools: string[] = [];\n\n\ttry {\n\t\tconst entries = fs.readdirSync(dir, { withFileTypes: true });\n\n\t\tfor (const entry of entries) {\n\t\t\tif (entry.isDirectory() || entry.isSymbolicLink()) {\n\t\t\t\t// Check for index.ts in subdirectory\n\t\t\t\tconst indexPath = path.join(dir, entry.name, \"index.ts\");\n\t\t\t\tif (fs.existsSync(indexPath)) {\n\t\t\t\t\ttools.push(indexPath);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} catch {\n\t\treturn [];\n\t}\n\n\treturn tools;\n}\n\n/**\n * Discover and load tools from standard locations:\n * 1. 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;AAWH,OAAO,KAAK,EAEX,qBAAqB,EAKrB,MAAM,YAAY,CAAC;AAiQpB;;;;;GAKG;AACH,wBAAsB,eAAe,CACpC,KAAK,EAAE,MAAM,EAAE,EACf,GAAG,EAAE,MAAM,EACX,gBAAgB,EAAE,MAAM,EAAE,GACxB,OAAO,CAAC,qBAAqB,CAAC,CA8ChC;AAgCD;;;;;;;;;;;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 { spawn } from \"node:child_process\";\nimport * as fs from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { createJiti } from \"jiti\";\nimport { getAgentDir, isBunBinary } from \"../../config.js\";\nimport type { HookUIContext } from \"../hooks/types.js\";\nimport type {\n\tCustomToolFactory,\n\tCustomToolsLoadResult,\n\tExecOptions,\n\tExecResult,\n\tLoadedCustomTool,\n\tToolAPI,\n} from \"./types.js\";\n\n// Create require function to resolve module paths at runtime\nconst require = createRequire(import.meta.url);\n\n// Lazily computed aliases - resolved at runtime to handle global installs\nlet _aliases: Record<string, string> | null = null;\nfunction getAliases(): Record<string, string> {\n\tif (_aliases) return _aliases;\n\n\tconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\tconst packageIndex = path.resolve(__dirname, \"../..\", \"index.js\");\n\n\t// 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 * Execute a command and return stdout/stderr/code.\n * Supports cancellation via AbortSignal and timeout.\n */\nasync function execCommand(command: string, args: string[], cwd: string, options?: ExecOptions): Promise<ExecResult> {\n\treturn new Promise((resolve) => {\n\t\tconst proc = spawn(command, args, {\n\t\t\tcwd,\n\t\t\tshell: false,\n\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t});\n\n\t\tlet stdout = \"\";\n\t\tlet stderr = \"\";\n\t\tlet killed = false;\n\t\tlet timeoutId: NodeJS.Timeout | undefined;\n\n\t\tconst killProcess = () => {\n\t\t\tif (!killed) {\n\t\t\t\tkilled = true;\n\t\t\t\tproc.kill(\"SIGTERM\");\n\t\t\t\t// Force kill after 5 seconds if SIGTERM doesn't work\n\t\t\t\tsetTimeout(() => {\n\t\t\t\t\tif (!proc.killed) {\n\t\t\t\t\t\tproc.kill(\"SIGKILL\");\n\t\t\t\t\t}\n\t\t\t\t}, 5000);\n\t\t\t}\n\t\t};\n\n\t\t// Handle abort signal\n\t\tif (options?.signal) {\n\t\t\tif (options.signal.aborted) {\n\t\t\t\tkillProcess();\n\t\t\t} else {\n\t\t\t\toptions.signal.addEventListener(\"abort\", killProcess, { once: true });\n\t\t\t}\n\t\t}\n\n\t\t// Handle timeout\n\t\tif (options?.timeout && options.timeout > 0) {\n\t\t\ttimeoutId = setTimeout(() => {\n\t\t\t\tkillProcess();\n\t\t\t}, options.timeout);\n\t\t}\n\n\t\tproc.stdout.on(\"data\", (data) => {\n\t\t\tstdout += data.toString();\n\t\t});\n\n\t\tproc.stderr.on(\"data\", (data) => {\n\t\t\tstderr += data.toString();\n\t\t});\n\n\t\tproc.on(\"close\", (code) => {\n\t\t\tif (timeoutId) clearTimeout(timeoutId);\n\t\t\tif (options?.signal) {\n\t\t\t\toptions.signal.removeEventListener(\"abort\", killProcess);\n\t\t\t}\n\t\t\tresolve({\n\t\t\t\tstdout,\n\t\t\t\tstderr,\n\t\t\t\tcode: code ?? 0,\n\t\t\t\tkilled,\n\t\t\t});\n\t\t});\n\n\t\tproc.on(\"error\", (err) => {\n\t\t\tif (timeoutId) clearTimeout(timeoutId);\n\t\t\tif (options?.signal) {\n\t\t\t\toptions.signal.removeEventListener(\"abort\", killProcess);\n\t\t\t}\n\t\t\tresolve({\n\t\t\t\tstdout,\n\t\t\t\tstderr: stderr || err.message,\n\t\t\t\tcode: 1,\n\t\t\t\tkilled,\n\t\t\t});\n\t\t});\n\t});\n}\n\n/**\n * Create a no-op UI context for headless modes.\n */\nfunction createNoOpUIContext(): HookUIContext {\n\treturn {\n\t\tselect: async () => null,\n\t\tconfirm: async () => false,\n\t\tinput: async () => null,\n\t\tnotify: () => {},\n\t};\n}\n\n/**\n * Load a 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: ToolAPI,\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: ToolAPI,\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: ToolAPI = {\n\t\tcwd,\n\t\texec: (command: string, args: string[], options?: ExecOptions) => execCommand(command, args, cwd, options),\n\t\tui: createNoOpUIContext(),\n\t\thasUI: false,\n\t};\n\n\tfor (const toolPath of paths) {\n\t\tconst { tools: loadedTools, error } = await loadTool(toolPath, cwd, sharedApi);\n\n\t\tif (error) {\n\t\t\terrors.push({ path: toolPath, error });\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (loadedTools) {\n\t\t\tfor (const loadedTool of loadedTools) {\n\t\t\t\t// Check for name conflicts\n\t\t\t\tif (seenNames.has(loadedTool.tool.name)) {\n\t\t\t\t\terrors.push({\n\t\t\t\t\t\tpath: toolPath,\n\t\t\t\t\t\terror: `Tool name \"${loadedTool.tool.name}\" conflicts with existing tool`,\n\t\t\t\t\t});\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tseenNames.add(loadedTool.tool.name);\n\t\t\t\ttools.push(loadedTool);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn {\n\t\ttools,\n\t\terrors,\n\t\tsetUIContext(uiContext, hasUI) {\n\t\t\tsharedApi.ui = uiContext;\n\t\t\tsharedApi.hasUI = hasUI;\n\t\t},\n\t};\n}\n\n/**\n * Discover tool files from a directory.\n * Only loads index.ts files from subdirectories (e.g., tools/mytool/index.ts).\n */\nfunction discoverToolsInDir(dir: string): string[] {\n\tif (!fs.existsSync(dir)) {\n\t\treturn [];\n\t}\n\n\tconst tools: string[] = [];\n\n\ttry {\n\t\tconst entries = fs.readdirSync(dir, { withFileTypes: true });\n\n\t\tfor (const entry of entries) {\n\t\t\tif (entry.isDirectory() || entry.isSymbolicLink()) {\n\t\t\t\t// Check for index.ts in subdirectory\n\t\t\t\tconst indexPath = path.join(dir, entry.name, \"index.ts\");\n\t\t\t\tif (fs.existsSync(indexPath)) {\n\t\t\t\t\ttools.push(indexPath);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} catch {\n\t\treturn [];\n\t}\n\n\treturn tools;\n}\n\n/**\n * Discover and load tools from standard locations:\n * 1. 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,5 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Custom tool loader - loads TypeScript tool modules using jiti.
|
|
3
|
+
*
|
|
4
|
+
* For Bun compiled binaries, custom tools that import from @mariozechner/* packages
|
|
5
|
+
* are not supported because Bun's plugin system doesn't intercept imports from
|
|
6
|
+
* external files loaded at runtime. Users should use the npm-installed version
|
|
7
|
+
* for custom tools that depend on pi packages.
|
|
3
8
|
*/
|
|
4
9
|
import { spawn } from "node:child_process";
|
|
5
10
|
import * as fs from "node:fs";
|
|
@@ -8,7 +13,7 @@ import * as os from "node:os";
|
|
|
8
13
|
import * as path from "node:path";
|
|
9
14
|
import { fileURLToPath } from "node:url";
|
|
10
15
|
import { createJiti } from "jiti";
|
|
11
|
-
import { getAgentDir } from "../../config.js";
|
|
16
|
+
import { getAgentDir, isBunBinary } from "../../config.js";
|
|
12
17
|
// Create require function to resolve module paths at runtime
|
|
13
18
|
const require = createRequire(import.meta.url);
|
|
14
19
|
// Lazily computed aliases - resolved at runtime to handle global installs
|
|
@@ -18,11 +23,18 @@ function getAliases() {
|
|
|
18
23
|
return _aliases;
|
|
19
24
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
20
25
|
const packageIndex = path.resolve(__dirname, "../..", "index.js");
|
|
26
|
+
// For typebox, we need the package root directory (not the entry file)
|
|
27
|
+
// because jiti's alias is prefix-based: imports like "@sinclair/typebox/compiler"
|
|
28
|
+
// get the alias prepended. If we alias to the entry file (.../build/cjs/index.js),
|
|
29
|
+
// then "@sinclair/typebox/compiler" becomes ".../build/cjs/index.js/compiler" (invalid).
|
|
30
|
+
// By aliasing to the package root, it becomes ".../typebox/compiler" which resolves correctly.
|
|
31
|
+
const typeboxEntry = require.resolve("@sinclair/typebox");
|
|
32
|
+
const typeboxRoot = typeboxEntry.replace(/\/build\/cjs\/index\.js$/, "");
|
|
21
33
|
_aliases = {
|
|
22
34
|
"@mariozechner/pi-coding-agent": packageIndex,
|
|
23
35
|
"@mariozechner/pi-tui": require.resolve("@mariozechner/pi-tui"),
|
|
24
36
|
"@mariozechner/pi-ai": require.resolve("@mariozechner/pi-ai"),
|
|
25
|
-
"@sinclair/typebox":
|
|
37
|
+
"@sinclair/typebox": typeboxRoot,
|
|
26
38
|
};
|
|
27
39
|
return _aliases;
|
|
28
40
|
}
|
|
@@ -142,10 +154,53 @@ function createNoOpUIContext() {
|
|
|
142
154
|
};
|
|
143
155
|
}
|
|
144
156
|
/**
|
|
145
|
-
* Load a
|
|
157
|
+
* Load a tool in Bun binary mode.
|
|
158
|
+
*
|
|
159
|
+
* Since Bun plugins don't work for dynamically loaded external files,
|
|
160
|
+
* custom tools that import from @mariozechner/* packages won't work.
|
|
161
|
+
* Tools that only use standard npm packages (installed in the tool's directory)
|
|
162
|
+
* may still work.
|
|
163
|
+
*/
|
|
164
|
+
async function loadToolWithBun(resolvedPath, sharedApi) {
|
|
165
|
+
try {
|
|
166
|
+
// Try to import directly - will work for tools without @mariozechner/* imports
|
|
167
|
+
const module = await import(resolvedPath);
|
|
168
|
+
const factory = (module.default ?? module);
|
|
169
|
+
if (typeof factory !== "function") {
|
|
170
|
+
return { tools: null, error: "Tool must export a default function" };
|
|
171
|
+
}
|
|
172
|
+
const toolResult = await factory(sharedApi);
|
|
173
|
+
const toolsArray = Array.isArray(toolResult) ? toolResult : [toolResult];
|
|
174
|
+
const loadedTools = toolsArray.map((tool) => ({
|
|
175
|
+
path: resolvedPath,
|
|
176
|
+
resolvedPath,
|
|
177
|
+
tool,
|
|
178
|
+
}));
|
|
179
|
+
return { tools: loadedTools, error: null };
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
183
|
+
// Check if it's a module resolution error for our packages
|
|
184
|
+
if (message.includes("Cannot find module") && message.includes("@mariozechner/")) {
|
|
185
|
+
return {
|
|
186
|
+
tools: null,
|
|
187
|
+
error: `${message}\n` +
|
|
188
|
+
"Note: Custom tools importing from @mariozechner/* packages are not supported in the standalone binary.\n" +
|
|
189
|
+
"Please install pi via npm: npm install -g @mariozechner/pi-coding-agent",
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
return { tools: null, error: `Failed to load tool: ${message}` };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Load a single tool module using jiti (or Bun.build for compiled binaries).
|
|
146
197
|
*/
|
|
147
198
|
async function loadTool(toolPath, cwd, sharedApi) {
|
|
148
199
|
const resolvedPath = resolveToolPath(toolPath, cwd);
|
|
200
|
+
// Use Bun.build for compiled binaries since jiti can't resolve bundled modules
|
|
201
|
+
if (isBunBinary) {
|
|
202
|
+
return loadToolWithBun(resolvedPath, sharedApi);
|
|
203
|
+
}
|
|
149
204
|
try {
|
|
150
205
|
// Create jiti instance for TypeScript/ESM loading
|
|
151
206
|
// Use aliases to resolve package imports since tools are loaded from user directories
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loader.js","sourceRoot":"","sources":["../../../src/core/custom-tools/loader.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Custom tool loader - loads TypeScript tool modules using jiti.\n */\n\nimport { spawn } from \"node:child_process\";\nimport * as fs from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { createJiti } from \"jiti\";\nimport { getAgentDir } from \"../../config.js\";\nimport type { HookUIContext } from \"../hooks/types.js\";\nimport type {\n\tCustomToolFactory,\n\tCustomToolsLoadResult,\n\tExecOptions,\n\tExecResult,\n\tLoadedCustomTool,\n\tToolAPI,\n} from \"./types.js\";\n\n// Create require function to resolve module paths at runtime\nconst require = createRequire(import.meta.url);\n\n// Lazily computed aliases - resolved at runtime to handle global installs\nlet _aliases: Record<string, string> | null = null;\nfunction getAliases(): Record<string, string> {\n\tif (_aliases) return _aliases;\n\n\tconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\tconst packageIndex = path.resolve(__dirname, \"../..\", \"index.js\");\n\n\t_aliases = {\n\t\t\"@mariozechner/pi-coding-agent\": packageIndex,\n\t\t\"@mariozechner/pi-tui\": require.resolve(\"@mariozechner/pi-tui\"),\n\t\t\"@mariozechner/pi-ai\": require.resolve(\"@mariozechner/pi-ai\"),\n\t\t\"@sinclair/typebox\": require.resolve(\"@sinclair/typebox\"),\n\t};\n\treturn _aliases;\n}\n\nconst UNICODE_SPACES = /[\\u00A0\\u2000-\\u200A\\u202F\\u205F\\u3000]/g;\n\nfunction normalizeUnicodeSpaces(str: string): string {\n\treturn str.replace(UNICODE_SPACES, \" \");\n}\n\nfunction expandPath(p: string): string {\n\tconst normalized = normalizeUnicodeSpaces(p);\n\tif (normalized.startsWith(\"~/\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(2));\n\t}\n\tif (normalized.startsWith(\"~\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(1));\n\t}\n\treturn normalized;\n}\n\n/**\n * Resolve tool path.\n * - Absolute paths used as-is\n * - Paths starting with ~ expanded to home directory\n * - Relative paths resolved from cwd\n */\nfunction resolveToolPath(toolPath: string, cwd: string): string {\n\tconst expanded = expandPath(toolPath);\n\n\tif (path.isAbsolute(expanded)) {\n\t\treturn expanded;\n\t}\n\n\t// Relative paths resolved from cwd\n\treturn path.resolve(cwd, expanded);\n}\n\n/**\n * Execute a command and return stdout/stderr/code.\n * Supports cancellation via AbortSignal and timeout.\n */\nasync function execCommand(command: string, args: string[], cwd: string, options?: ExecOptions): Promise<ExecResult> {\n\treturn new Promise((resolve) => {\n\t\tconst proc = spawn(command, args, {\n\t\t\tcwd,\n\t\t\tshell: false,\n\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t});\n\n\t\tlet stdout = \"\";\n\t\tlet stderr = \"\";\n\t\tlet killed = false;\n\t\tlet timeoutId: NodeJS.Timeout | undefined;\n\n\t\tconst killProcess = () => {\n\t\t\tif (!killed) {\n\t\t\t\tkilled = true;\n\t\t\t\tproc.kill(\"SIGTERM\");\n\t\t\t\t// Force kill after 5 seconds if SIGTERM doesn't work\n\t\t\t\tsetTimeout(() => {\n\t\t\t\t\tif (!proc.killed) {\n\t\t\t\t\t\tproc.kill(\"SIGKILL\");\n\t\t\t\t\t}\n\t\t\t\t}, 5000);\n\t\t\t}\n\t\t};\n\n\t\t// Handle abort signal\n\t\tif (options?.signal) {\n\t\t\tif (options.signal.aborted) {\n\t\t\t\tkillProcess();\n\t\t\t} else {\n\t\t\t\toptions.signal.addEventListener(\"abort\", killProcess, { once: true });\n\t\t\t}\n\t\t}\n\n\t\t// Handle timeout\n\t\tif (options?.timeout && options.timeout > 0) {\n\t\t\ttimeoutId = setTimeout(() => {\n\t\t\t\tkillProcess();\n\t\t\t}, options.timeout);\n\t\t}\n\n\t\tproc.stdout.on(\"data\", (data) => {\n\t\t\tstdout += data.toString();\n\t\t});\n\n\t\tproc.stderr.on(\"data\", (data) => {\n\t\t\tstderr += data.toString();\n\t\t});\n\n\t\tproc.on(\"close\", (code) => {\n\t\t\tif (timeoutId) clearTimeout(timeoutId);\n\t\t\tif (options?.signal) {\n\t\t\t\toptions.signal.removeEventListener(\"abort\", killProcess);\n\t\t\t}\n\t\t\tresolve({\n\t\t\t\tstdout,\n\t\t\t\tstderr,\n\t\t\t\tcode: code ?? 0,\n\t\t\t\tkilled,\n\t\t\t});\n\t\t});\n\n\t\tproc.on(\"error\", (err) => {\n\t\t\tif (timeoutId) clearTimeout(timeoutId);\n\t\t\tif (options?.signal) {\n\t\t\t\toptions.signal.removeEventListener(\"abort\", killProcess);\n\t\t\t}\n\t\t\tresolve({\n\t\t\t\tstdout,\n\t\t\t\tstderr: stderr || err.message,\n\t\t\t\tcode: 1,\n\t\t\t\tkilled,\n\t\t\t});\n\t\t});\n\t});\n}\n\n/**\n * Create a no-op UI context for headless modes.\n */\nfunction createNoOpUIContext(): HookUIContext {\n\treturn {\n\t\tselect: async () => null,\n\t\tconfirm: async () => false,\n\t\tinput: async () => null,\n\t\tnotify: () => {},\n\t};\n}\n\n/**\n * Load a single tool module using jiti.\n */\nasync function loadTool(\n\ttoolPath: string,\n\tcwd: string,\n\tsharedApi: ToolAPI,\n): Promise<{ tools: LoadedCustomTool[] | null; error: string | null }> {\n\tconst resolvedPath = resolveToolPath(toolPath, cwd);\n\n\ttry {\n\t\t// Create jiti instance for TypeScript/ESM loading\n\t\t// Use aliases to resolve package imports since tools are loaded from user directories\n\t\t// (e.g. ~/.pi/agent/tools) but import from packages installed with pi-coding-agent\n\t\tconst jiti = createJiti(import.meta.url, {\n\t\t\talias: getAliases(),\n\t\t});\n\n\t\t// Import the module\n\t\tconst module = await jiti.import(resolvedPath, { default: true });\n\t\tconst factory = module as CustomToolFactory;\n\n\t\tif (typeof factory !== \"function\") {\n\t\t\treturn { tools: null, error: \"Tool must export a default function\" };\n\t\t}\n\n\t\t// Call factory with shared API\n\t\tconst result = await factory(sharedApi);\n\n\t\t// Handle single tool or array of tools\n\t\tconst toolsArray = Array.isArray(result) ? result : [result];\n\n\t\tconst loadedTools: LoadedCustomTool[] = toolsArray.map((tool) => ({\n\t\t\tpath: toolPath,\n\t\t\tresolvedPath,\n\t\t\ttool,\n\t\t}));\n\n\t\treturn { tools: loadedTools, error: null };\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\treturn { tools: null, error: `Failed to load tool: ${message}` };\n\t}\n}\n\n/**\n * Load all tools from configuration.\n * @param paths - Array of tool file paths\n * @param cwd - Current working directory for resolving relative paths\n * @param builtInToolNames - Names of built-in tools to check for conflicts\n */\nexport async function loadCustomTools(\n\tpaths: string[],\n\tcwd: string,\n\tbuiltInToolNames: string[],\n): Promise<CustomToolsLoadResult> {\n\tconst tools: LoadedCustomTool[] = [];\n\tconst errors: Array<{ path: string; error: string }> = [];\n\tconst seenNames = new Set<string>(builtInToolNames);\n\n\t// Shared API object - all tools get the same instance\n\tconst sharedApi: ToolAPI = {\n\t\tcwd,\n\t\texec: (command: string, args: string[], options?: ExecOptions) => execCommand(command, args, cwd, options),\n\t\tui: createNoOpUIContext(),\n\t\thasUI: false,\n\t};\n\n\tfor (const toolPath of paths) {\n\t\tconst { tools: loadedTools, error } = await loadTool(toolPath, cwd, sharedApi);\n\n\t\tif (error) {\n\t\t\terrors.push({ path: toolPath, error });\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (loadedTools) {\n\t\t\tfor (const loadedTool of loadedTools) {\n\t\t\t\t// Check for name conflicts\n\t\t\t\tif (seenNames.has(loadedTool.tool.name)) {\n\t\t\t\t\terrors.push({\n\t\t\t\t\t\tpath: toolPath,\n\t\t\t\t\t\terror: `Tool name \"${loadedTool.tool.name}\" conflicts with existing tool`,\n\t\t\t\t\t});\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tseenNames.add(loadedTool.tool.name);\n\t\t\t\ttools.push(loadedTool);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn {\n\t\ttools,\n\t\terrors,\n\t\tsetUIContext(uiContext, hasUI) {\n\t\t\tsharedApi.ui = uiContext;\n\t\t\tsharedApi.hasUI = hasUI;\n\t\t},\n\t};\n}\n\n/**\n * Discover tool files from a directory.\n * Only loads index.ts files from subdirectories (e.g., tools/mytool/index.ts).\n */\nfunction discoverToolsInDir(dir: string): string[] {\n\tif (!fs.existsSync(dir)) {\n\t\treturn [];\n\t}\n\n\tconst tools: string[] = [];\n\n\ttry {\n\t\tconst entries = fs.readdirSync(dir, { withFileTypes: true });\n\n\t\tfor (const entry of entries) {\n\t\t\tif (entry.isDirectory() || entry.isSymbolicLink()) {\n\t\t\t\t// Check for index.ts in subdirectory\n\t\t\t\tconst indexPath = path.join(dir, entry.name, \"index.ts\");\n\t\t\t\tif (fs.existsSync(indexPath)) {\n\t\t\t\t\ttools.push(indexPath);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} catch {\n\t\treturn [];\n\t}\n\n\treturn tools;\n}\n\n/**\n * Discover and load tools from standard locations:\n * 1. 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 { spawn } from \"node:child_process\";\nimport * as fs from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { createJiti } from \"jiti\";\nimport { getAgentDir, isBunBinary } from \"../../config.js\";\nimport type { HookUIContext } from \"../hooks/types.js\";\nimport type {\n\tCustomToolFactory,\n\tCustomToolsLoadResult,\n\tExecOptions,\n\tExecResult,\n\tLoadedCustomTool,\n\tToolAPI,\n} from \"./types.js\";\n\n// Create require function to resolve module paths at runtime\nconst require = createRequire(import.meta.url);\n\n// Lazily computed aliases - resolved at runtime to handle global installs\nlet _aliases: Record<string, string> | null = null;\nfunction getAliases(): Record<string, string> {\n\tif (_aliases) return _aliases;\n\n\tconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\tconst packageIndex = path.resolve(__dirname, \"../..\", \"index.js\");\n\n\t// 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 * Execute a command and return stdout/stderr/code.\n * Supports cancellation via AbortSignal and timeout.\n */\nasync function execCommand(command: string, args: string[], cwd: string, options?: ExecOptions): Promise<ExecResult> {\n\treturn new Promise((resolve) => {\n\t\tconst proc = spawn(command, args, {\n\t\t\tcwd,\n\t\t\tshell: false,\n\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t});\n\n\t\tlet stdout = \"\";\n\t\tlet stderr = \"\";\n\t\tlet killed = false;\n\t\tlet timeoutId: NodeJS.Timeout | undefined;\n\n\t\tconst killProcess = () => {\n\t\t\tif (!killed) {\n\t\t\t\tkilled = true;\n\t\t\t\tproc.kill(\"SIGTERM\");\n\t\t\t\t// Force kill after 5 seconds if SIGTERM doesn't work\n\t\t\t\tsetTimeout(() => {\n\t\t\t\t\tif (!proc.killed) {\n\t\t\t\t\t\tproc.kill(\"SIGKILL\");\n\t\t\t\t\t}\n\t\t\t\t}, 5000);\n\t\t\t}\n\t\t};\n\n\t\t// Handle abort signal\n\t\tif (options?.signal) {\n\t\t\tif (options.signal.aborted) {\n\t\t\t\tkillProcess();\n\t\t\t} else {\n\t\t\t\toptions.signal.addEventListener(\"abort\", killProcess, { once: true });\n\t\t\t}\n\t\t}\n\n\t\t// Handle timeout\n\t\tif (options?.timeout && options.timeout > 0) {\n\t\t\ttimeoutId = setTimeout(() => {\n\t\t\t\tkillProcess();\n\t\t\t}, options.timeout);\n\t\t}\n\n\t\tproc.stdout.on(\"data\", (data) => {\n\t\t\tstdout += data.toString();\n\t\t});\n\n\t\tproc.stderr.on(\"data\", (data) => {\n\t\t\tstderr += data.toString();\n\t\t});\n\n\t\tproc.on(\"close\", (code) => {\n\t\t\tif (timeoutId) clearTimeout(timeoutId);\n\t\t\tif (options?.signal) {\n\t\t\t\toptions.signal.removeEventListener(\"abort\", killProcess);\n\t\t\t}\n\t\t\tresolve({\n\t\t\t\tstdout,\n\t\t\t\tstderr,\n\t\t\t\tcode: code ?? 0,\n\t\t\t\tkilled,\n\t\t\t});\n\t\t});\n\n\t\tproc.on(\"error\", (err) => {\n\t\t\tif (timeoutId) clearTimeout(timeoutId);\n\t\t\tif (options?.signal) {\n\t\t\t\toptions.signal.removeEventListener(\"abort\", killProcess);\n\t\t\t}\n\t\t\tresolve({\n\t\t\t\tstdout,\n\t\t\t\tstderr: stderr || err.message,\n\t\t\t\tcode: 1,\n\t\t\t\tkilled,\n\t\t\t});\n\t\t});\n\t});\n}\n\n/**\n * Create a no-op UI context for headless modes.\n */\nfunction createNoOpUIContext(): HookUIContext {\n\treturn {\n\t\tselect: async () => null,\n\t\tconfirm: async () => false,\n\t\tinput: async () => null,\n\t\tnotify: () => {},\n\t};\n}\n\n/**\n * Load a 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: ToolAPI,\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: ToolAPI,\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: ToolAPI = {\n\t\tcwd,\n\t\texec: (command: string, args: string[], options?: ExecOptions) => execCommand(command, args, cwd, options),\n\t\tui: createNoOpUIContext(),\n\t\thasUI: false,\n\t};\n\n\tfor (const toolPath of paths) {\n\t\tconst { tools: loadedTools, error } = await loadTool(toolPath, cwd, sharedApi);\n\n\t\tif (error) {\n\t\t\terrors.push({ path: toolPath, error });\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (loadedTools) {\n\t\t\tfor (const loadedTool of loadedTools) {\n\t\t\t\t// Check for name conflicts\n\t\t\t\tif (seenNames.has(loadedTool.tool.name)) {\n\t\t\t\t\terrors.push({\n\t\t\t\t\t\tpath: toolPath,\n\t\t\t\t\t\terror: `Tool name \"${loadedTool.tool.name}\" conflicts with existing tool`,\n\t\t\t\t\t});\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tseenNames.add(loadedTool.tool.name);\n\t\t\t\ttools.push(loadedTool);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn {\n\t\ttools,\n\t\terrors,\n\t\tsetUIContext(uiContext, hasUI) {\n\t\t\tsharedApi.ui = uiContext;\n\t\t\tsharedApi.hasUI = hasUI;\n\t\t},\n\t};\n}\n\n/**\n * Discover tool files from a directory.\n * Only loads index.ts files from subdirectories (e.g., tools/mytool/index.ts).\n */\nfunction discoverToolsInDir(dir: string): string[] {\n\tif (!fs.existsSync(dir)) {\n\t\treturn [];\n\t}\n\n\tconst tools: string[] = [];\n\n\ttry {\n\t\tconst entries = fs.readdirSync(dir, { withFileTypes: true });\n\n\t\tfor (const entry of entries) {\n\t\t\tif (entry.isDirectory() || entry.isSymbolicLink()) {\n\t\t\t\t// Check for index.ts in subdirectory\n\t\t\t\tconst indexPath = path.join(dir, entry.name, \"index.ts\");\n\t\t\t\tif (fs.existsSync(indexPath)) {\n\t\t\t\t\ttools.push(indexPath);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} catch {\n\t\treturn [];\n\t}\n\n\treturn tools;\n}\n\n/**\n * Discover and load tools from standard locations:\n * 1. 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":"loader.d.ts","sourceRoot":"","sources":["../../../src/core/hooks/loader.ts"],"names":[],"mappings":"AAAA;;GAEG;AAOH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../../src/core/hooks/loader.ts"],"names":[],"mappings":"AAAA;;GAEG;AAOH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAkC9D;;GAEG;AACH,KAAK,SAAS,GAAG,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;AAE1D;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,UAAU,EAAE,KAAK,IAAI,CAAC;AAE7E;;GAEG;AACH,MAAM,WAAW,UAAU;IAC1B,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,6BAA6B;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,6CAA6C;IAC7C,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;IACnC,qDAAqD;IACrD,cAAc,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,CAAC;CAC/C;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC/B,gCAAgC;IAChC,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,wCAAwC;IACxC,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC/C;AA0GD;;;;GAIG;AACH,wBAAsB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAkBtF;AAqBD;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CACzC,eAAe,EAAE,MAAM,EAAE,EACzB,GAAG,EAAE,MAAM,EACX,QAAQ,GAAE,MAAsB,GAC9B,OAAO,CAAC,eAAe,CAAC,CA2B1B","sourcesContent":["/**\n * Hook loader - loads TypeScript hook modules using jiti.\n */\n\nimport * as fs from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type { Attachment } from \"@mariozechner/pi-agent-core\";\nimport { createJiti } from \"jiti\";\nimport { getAgentDir } from \"../../config.js\";\nimport type { HookAPI, HookFactory } from \"./types.js\";\n\n// Create require function to resolve module paths at runtime\nconst require = createRequire(import.meta.url);\n\n// Lazily computed aliases - resolved at runtime to handle global installs\nlet _aliases: Record<string, string> | null = null;\nfunction getAliases(): Record<string, string> {\n\tif (_aliases) return _aliases;\n\n\tconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\tconst packageIndex = path.resolve(__dirname, \"../..\", \"index.js\");\n\n\t// For typebox, we need the package root directory (not the entry file)\n\t// because jiti's alias is prefix-based: imports like \"@sinclair/typebox/compiler\"\n\t// get the alias prepended. If we alias to the entry file (.../build/cjs/index.js),\n\t// then \"@sinclair/typebox/compiler\" becomes \".../build/cjs/index.js/compiler\" (invalid).\n\t// By aliasing to the package root, it becomes \".../typebox/compiler\" which resolves correctly.\n\tconst typeboxEntry = require.resolve(\"@sinclair/typebox\");\n\tconst typeboxRoot = typeboxEntry.replace(/\\/build\\/cjs\\/index\\.js$/, \"\");\n\n\t_aliases = {\n\t\t\"@mariozechner/pi-coding-agent\": packageIndex,\n\t\t\"@mariozechner/pi-coding-agent/hooks\": path.resolve(__dirname, \"index.js\"),\n\t\t\"@mariozechner/pi-tui\": require.resolve(\"@mariozechner/pi-tui\"),\n\t\t\"@mariozechner/pi-ai\": require.resolve(\"@mariozechner/pi-ai\"),\n\t\t\"@sinclair/typebox\": typeboxRoot,\n\t};\n\treturn _aliases;\n}\n\n/**\n * Generic handler function type.\n */\ntype HandlerFn = (...args: unknown[]) => Promise<unknown>;\n\n/**\n * Send handler type for pi.send().\n */\nexport type SendHandler = (text: string, attachments?: Attachment[]) => void;\n\n/**\n * Registered handlers for a loaded hook.\n */\nexport interface LoadedHook {\n\t/** Original path from config */\n\tpath: string;\n\t/** Resolved absolute path */\n\tresolvedPath: string;\n\t/** Map of event type to handler functions */\n\thandlers: Map<string, HandlerFn[]>;\n\t/** Set the send handler for this hook's pi.send() */\n\tsetSendHandler: (handler: SendHandler) => void;\n}\n\n/**\n * Result of loading hooks.\n */\nexport interface LoadHooksResult {\n\t/** Successfully loaded hooks */\n\thooks: LoadedHook[];\n\t/** Errors encountered during loading */\n\terrors: Array<{ path: string; error: string }>;\n}\n\nconst UNICODE_SPACES = /[\\u00A0\\u2000-\\u200A\\u202F\\u205F\\u3000]/g;\n\nfunction normalizeUnicodeSpaces(str: string): string {\n\treturn str.replace(UNICODE_SPACES, \" \");\n}\n\nfunction expandPath(p: string): string {\n\tconst normalized = normalizeUnicodeSpaces(p);\n\tif (normalized.startsWith(\"~/\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(2));\n\t}\n\tif (normalized.startsWith(\"~\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(1));\n\t}\n\treturn normalized;\n}\n\n/**\n * Resolve hook path.\n * - Absolute paths used as-is\n * - Paths starting with ~ expanded to home directory\n * - Relative paths resolved from cwd\n */\nfunction resolveHookPath(hookPath: string, cwd: string): string {\n\tconst expanded = expandPath(hookPath);\n\n\tif (path.isAbsolute(expanded)) {\n\t\treturn expanded;\n\t}\n\n\t// Relative paths resolved from cwd\n\treturn path.resolve(cwd, expanded);\n}\n\n/**\n * Create a HookAPI instance that collects handlers.\n * Returns the API and a function to set the send handler later.\n */\nfunction createHookAPI(handlers: Map<string, HandlerFn[]>): {\n\tapi: HookAPI;\n\tsetSendHandler: (handler: SendHandler) => void;\n} {\n\tlet sendHandler: SendHandler = () => {\n\t\t// Default no-op until mode sets the handler\n\t};\n\n\tconst api: HookAPI = {\n\t\ton(event: string, handler: HandlerFn): void {\n\t\t\tconst list = handlers.get(event) ?? [];\n\t\t\tlist.push(handler);\n\t\t\thandlers.set(event, list);\n\t\t},\n\t\tsend(text: string, attachments?: Attachment[]): void {\n\t\t\tsendHandler(text, attachments);\n\t\t},\n\t} as HookAPI;\n\n\treturn {\n\t\tapi,\n\t\tsetSendHandler: (handler: SendHandler) => {\n\t\t\tsendHandler = handler;\n\t\t},\n\t};\n}\n\n/**\n * Load a single hook module using jiti.\n */\nasync function loadHook(hookPath: string, cwd: string): Promise<{ hook: LoadedHook | null; error: string | null }> {\n\tconst resolvedPath = resolveHookPath(hookPath, cwd);\n\n\ttry {\n\t\t// Create jiti instance for TypeScript/ESM loading\n\t\t// Use aliases to resolve package imports since hooks are loaded from user directories\n\t\t// (e.g. ~/.pi/agent/hooks) but import from packages installed with pi-coding-agent\n\t\tconst jiti = createJiti(import.meta.url, {\n\t\t\talias: getAliases(),\n\t\t});\n\n\t\t// Import the module\n\t\tconst module = await jiti.import(resolvedPath, { default: true });\n\t\tconst factory = module as HookFactory;\n\n\t\tif (typeof factory !== \"function\") {\n\t\t\treturn { hook: null, error: \"Hook must export a default function\" };\n\t\t}\n\n\t\t// Create handlers map and API\n\t\tconst handlers = new Map<string, HandlerFn[]>();\n\t\tconst { api, setSendHandler } = createHookAPI(handlers);\n\n\t\t// Call factory to register handlers\n\t\tfactory(api);\n\n\t\treturn {\n\t\t\thook: { path: hookPath, resolvedPath, handlers, setSendHandler },\n\t\t\terror: null,\n\t\t};\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\treturn { hook: null, error: `Failed to load hook: ${message}` };\n\t}\n}\n\n/**\n * Load all hooks from configuration.\n * @param paths - Array of hook file paths\n * @param cwd - Current working directory for resolving relative paths\n */\nexport async function loadHooks(paths: string[], cwd: string): Promise<LoadHooksResult> {\n\tconst hooks: LoadedHook[] = [];\n\tconst errors: Array<{ path: string; error: string }> = [];\n\n\tfor (const hookPath of paths) {\n\t\tconst { hook, error } = await loadHook(hookPath, cwd);\n\n\t\tif (error) {\n\t\t\terrors.push({ path: hookPath, error });\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (hook) {\n\t\t\thooks.push(hook);\n\t\t}\n\t}\n\n\treturn { hooks, errors };\n}\n\n/**\n * Discover hook files from a directory.\n * Returns all .ts files (and symlinks to .ts files) in the directory (non-recursive).\n */\nfunction discoverHooksInDir(dir: string): string[] {\n\tif (!fs.existsSync(dir)) {\n\t\treturn [];\n\t}\n\n\ttry {\n\t\tconst entries = fs.readdirSync(dir, { withFileTypes: true });\n\t\treturn entries\n\t\t\t.filter((e) => (e.isFile() || e.isSymbolicLink()) && e.name.endsWith(\".ts\"))\n\t\t\t.map((e) => path.join(dir, e.name));\n\t} catch {\n\t\treturn [];\n\t}\n}\n\n/**\n * Discover and load hooks from standard locations:\n * 1. agentDir/hooks/*.ts (global)\n * 2. cwd/.pi/hooks/*.ts (project-local)\n *\n * Plus any explicitly configured paths from settings.\n */\nexport async function discoverAndLoadHooks(\n\tconfiguredPaths: string[],\n\tcwd: string,\n\tagentDir: string = getAgentDir(),\n): Promise<LoadHooksResult> {\n\tconst allPaths: string[] = [];\n\tconst seen = new Set<string>();\n\n\t// Helper to add paths without duplicates\n\tconst addPaths = (paths: string[]) => {\n\t\tfor (const p of paths) {\n\t\t\tconst resolved = path.resolve(p);\n\t\t\tif (!seen.has(resolved)) {\n\t\t\t\tseen.add(resolved);\n\t\t\t\tallPaths.push(p);\n\t\t\t}\n\t\t}\n\t};\n\n\t// 1. Global hooks: agentDir/hooks/\n\tconst globalHooksDir = path.join(agentDir, \"hooks\");\n\taddPaths(discoverHooksInDir(globalHooksDir));\n\n\t// 2. Project-local hooks: cwd/.pi/hooks/\n\tconst localHooksDir = path.join(cwd, \".pi\", \"hooks\");\n\taddPaths(discoverHooksInDir(localHooksDir));\n\n\t// 3. Explicitly configured paths (can override/add)\n\taddPaths(configuredPaths.map((p) => resolveHookPath(p, cwd)));\n\n\treturn loadHooks(allPaths, cwd);\n}\n"]}
|
|
@@ -17,12 +17,19 @@ function getAliases() {
|
|
|
17
17
|
return _aliases;
|
|
18
18
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
19
19
|
const packageIndex = path.resolve(__dirname, "../..", "index.js");
|
|
20
|
+
// For typebox, we need the package root directory (not the entry file)
|
|
21
|
+
// because jiti's alias is prefix-based: imports like "@sinclair/typebox/compiler"
|
|
22
|
+
// get the alias prepended. If we alias to the entry file (.../build/cjs/index.js),
|
|
23
|
+
// then "@sinclair/typebox/compiler" becomes ".../build/cjs/index.js/compiler" (invalid).
|
|
24
|
+
// By aliasing to the package root, it becomes ".../typebox/compiler" which resolves correctly.
|
|
25
|
+
const typeboxEntry = require.resolve("@sinclair/typebox");
|
|
26
|
+
const typeboxRoot = typeboxEntry.replace(/\/build\/cjs\/index\.js$/, "");
|
|
20
27
|
_aliases = {
|
|
21
28
|
"@mariozechner/pi-coding-agent": packageIndex,
|
|
22
29
|
"@mariozechner/pi-coding-agent/hooks": path.resolve(__dirname, "index.js"),
|
|
23
30
|
"@mariozechner/pi-tui": require.resolve("@mariozechner/pi-tui"),
|
|
24
31
|
"@mariozechner/pi-ai": require.resolve("@mariozechner/pi-ai"),
|
|
25
|
-
"@sinclair/typebox":
|
|
32
|
+
"@sinclair/typebox": typeboxRoot,
|
|
26
33
|
};
|
|
27
34
|
return _aliases;
|
|
28
35
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loader.js","sourceRoot":"","sources":["../../../src/core/hooks/loader.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Hook loader - loads TypeScript hook modules using jiti.\n */\n\nimport * as fs from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type { Attachment } from \"@mariozechner/pi-agent-core\";\nimport { createJiti } from \"jiti\";\nimport { getAgentDir } from \"../../config.js\";\nimport type { HookAPI, HookFactory } from \"./types.js\";\n\n// Create require function to resolve module paths at runtime\nconst require = createRequire(import.meta.url);\n\n// Lazily computed aliases - resolved at runtime to handle global installs\nlet _aliases: Record<string, string> | null = null;\nfunction getAliases(): Record<string, string> {\n\tif (_aliases) return _aliases;\n\n\tconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\tconst packageIndex = path.resolve(__dirname, \"../..\", \"index.js\");\n\n\t_aliases = {\n\t\t\"@mariozechner/pi-coding-agent\": packageIndex,\n\t\t\"@mariozechner/pi-coding-agent/hooks\": path.resolve(__dirname, \"index.js\"),\n\t\t\"@mariozechner/pi-tui\": require.resolve(\"@mariozechner/pi-tui\"),\n\t\t\"@mariozechner/pi-ai\": require.resolve(\"@mariozechner/pi-ai\"),\n\t\t\"@sinclair/typebox\": require.resolve(\"@sinclair/typebox\"),\n\t};\n\treturn _aliases;\n}\n\n/**\n * Generic handler function type.\n */\ntype HandlerFn = (...args: unknown[]) => Promise<unknown>;\n\n/**\n * Send handler type for pi.send().\n */\nexport type SendHandler = (text: string, attachments?: Attachment[]) => void;\n\n/**\n * Registered handlers for a loaded hook.\n */\nexport interface LoadedHook {\n\t/** Original path from config */\n\tpath: string;\n\t/** Resolved absolute path */\n\tresolvedPath: string;\n\t/** Map of event type to handler functions */\n\thandlers: Map<string, HandlerFn[]>;\n\t/** Set the send handler for this hook's pi.send() */\n\tsetSendHandler: (handler: SendHandler) => void;\n}\n\n/**\n * Result of loading hooks.\n */\nexport interface LoadHooksResult {\n\t/** Successfully loaded hooks */\n\thooks: LoadedHook[];\n\t/** Errors encountered during loading */\n\terrors: Array<{ path: string; error: string }>;\n}\n\nconst UNICODE_SPACES = /[\\u00A0\\u2000-\\u200A\\u202F\\u205F\\u3000]/g;\n\nfunction normalizeUnicodeSpaces(str: string): string {\n\treturn str.replace(UNICODE_SPACES, \" \");\n}\n\nfunction expandPath(p: string): string {\n\tconst normalized = normalizeUnicodeSpaces(p);\n\tif (normalized.startsWith(\"~/\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(2));\n\t}\n\tif (normalized.startsWith(\"~\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(1));\n\t}\n\treturn normalized;\n}\n\n/**\n * Resolve hook path.\n * - Absolute paths used as-is\n * - Paths starting with ~ expanded to home directory\n * - Relative paths resolved from cwd\n */\nfunction resolveHookPath(hookPath: string, cwd: string): string {\n\tconst expanded = expandPath(hookPath);\n\n\tif (path.isAbsolute(expanded)) {\n\t\treturn expanded;\n\t}\n\n\t// Relative paths resolved from cwd\n\treturn path.resolve(cwd, expanded);\n}\n\n/**\n * Create a HookAPI instance that collects handlers.\n * Returns the API and a function to set the send handler later.\n */\nfunction createHookAPI(handlers: Map<string, HandlerFn[]>): {\n\tapi: HookAPI;\n\tsetSendHandler: (handler: SendHandler) => void;\n} {\n\tlet sendHandler: SendHandler = () => {\n\t\t// Default no-op until mode sets the handler\n\t};\n\n\tconst api: HookAPI = {\n\t\ton(event: string, handler: HandlerFn): void {\n\t\t\tconst list = handlers.get(event) ?? [];\n\t\t\tlist.push(handler);\n\t\t\thandlers.set(event, list);\n\t\t},\n\t\tsend(text: string, attachments?: Attachment[]): void {\n\t\t\tsendHandler(text, attachments);\n\t\t},\n\t} as HookAPI;\n\n\treturn {\n\t\tapi,\n\t\tsetSendHandler: (handler: SendHandler) => {\n\t\t\tsendHandler = handler;\n\t\t},\n\t};\n}\n\n/**\n * Load a single hook module using jiti.\n */\nasync function loadHook(hookPath: string, cwd: string): Promise<{ hook: LoadedHook | null; error: string | null }> {\n\tconst resolvedPath = resolveHookPath(hookPath, cwd);\n\n\ttry {\n\t\t// Create jiti instance for TypeScript/ESM loading\n\t\t// Use aliases to resolve package imports since hooks are loaded from user directories\n\t\t// (e.g. ~/.pi/agent/hooks) but import from packages installed with pi-coding-agent\n\t\tconst jiti = createJiti(import.meta.url, {\n\t\t\talias: getAliases(),\n\t\t});\n\n\t\t// Import the module\n\t\tconst module = await jiti.import(resolvedPath, { default: true });\n\t\tconst factory = module as HookFactory;\n\n\t\tif (typeof factory !== \"function\") {\n\t\t\treturn { hook: null, error: \"Hook must export a default function\" };\n\t\t}\n\n\t\t// Create handlers map and API\n\t\tconst handlers = new Map<string, HandlerFn[]>();\n\t\tconst { api, setSendHandler } = createHookAPI(handlers);\n\n\t\t// Call factory to register handlers\n\t\tfactory(api);\n\n\t\treturn {\n\t\t\thook: { path: hookPath, resolvedPath, handlers, setSendHandler },\n\t\t\terror: null,\n\t\t};\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\treturn { hook: null, error: `Failed to load hook: ${message}` };\n\t}\n}\n\n/**\n * Load all hooks from configuration.\n * @param paths - Array of hook file paths\n * @param cwd - Current working directory for resolving relative paths\n */\nexport async function loadHooks(paths: string[], cwd: string): Promise<LoadHooksResult> {\n\tconst hooks: LoadedHook[] = [];\n\tconst errors: Array<{ path: string; error: string }> = [];\n\n\tfor (const hookPath of paths) {\n\t\tconst { hook, error } = await loadHook(hookPath, cwd);\n\n\t\tif (error) {\n\t\t\terrors.push({ path: hookPath, error });\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (hook) {\n\t\t\thooks.push(hook);\n\t\t}\n\t}\n\n\treturn { hooks, errors };\n}\n\n/**\n * Discover hook files from a directory.\n * Returns all .ts files (and symlinks to .ts files) in the directory (non-recursive).\n */\nfunction discoverHooksInDir(dir: string): string[] {\n\tif (!fs.existsSync(dir)) {\n\t\treturn [];\n\t}\n\n\ttry {\n\t\tconst entries = fs.readdirSync(dir, { withFileTypes: true });\n\t\treturn entries\n\t\t\t.filter((e) => (e.isFile() || e.isSymbolicLink()) && e.name.endsWith(\".ts\"))\n\t\t\t.map((e) => path.join(dir, e.name));\n\t} catch {\n\t\treturn [];\n\t}\n}\n\n/**\n * Discover and load hooks from standard locations:\n * 1. agentDir/hooks/*.ts (global)\n * 2. cwd/.pi/hooks/*.ts (project-local)\n *\n * Plus any explicitly configured paths from settings.\n */\nexport async function discoverAndLoadHooks(\n\tconfiguredPaths: string[],\n\tcwd: string,\n\tagentDir: string = getAgentDir(),\n): Promise<LoadHooksResult> {\n\tconst allPaths: string[] = [];\n\tconst seen = new Set<string>();\n\n\t// Helper to add paths without duplicates\n\tconst addPaths = (paths: string[]) => {\n\t\tfor (const p of paths) {\n\t\t\tconst resolved = path.resolve(p);\n\t\t\tif (!seen.has(resolved)) {\n\t\t\t\tseen.add(resolved);\n\t\t\t\tallPaths.push(p);\n\t\t\t}\n\t\t}\n\t};\n\n\t// 1. Global hooks: agentDir/hooks/\n\tconst globalHooksDir = path.join(agentDir, \"hooks\");\n\taddPaths(discoverHooksInDir(globalHooksDir));\n\n\t// 2. Project-local hooks: cwd/.pi/hooks/\n\tconst localHooksDir = path.join(cwd, \".pi\", \"hooks\");\n\taddPaths(discoverHooksInDir(localHooksDir));\n\n\t// 3. Explicitly configured paths (can override/add)\n\taddPaths(configuredPaths.map((p) => resolveHookPath(p, cwd)));\n\n\treturn loadHooks(allPaths, cwd);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"loader.js","sourceRoot":"","sources":["../../../src/core/hooks/loader.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Hook loader - loads TypeScript hook modules using jiti.\n */\n\nimport * as fs from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type { Attachment } from \"@mariozechner/pi-agent-core\";\nimport { createJiti } from \"jiti\";\nimport { getAgentDir } from \"../../config.js\";\nimport type { HookAPI, HookFactory } from \"./types.js\";\n\n// Create require function to resolve module paths at runtime\nconst require = createRequire(import.meta.url);\n\n// Lazily computed aliases - resolved at runtime to handle global installs\nlet _aliases: Record<string, string> | null = null;\nfunction getAliases(): Record<string, string> {\n\tif (_aliases) return _aliases;\n\n\tconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\tconst packageIndex = path.resolve(__dirname, \"../..\", \"index.js\");\n\n\t// For typebox, we need the package root directory (not the entry file)\n\t// because jiti's alias is prefix-based: imports like \"@sinclair/typebox/compiler\"\n\t// get the alias prepended. If we alias to the entry file (.../build/cjs/index.js),\n\t// then \"@sinclair/typebox/compiler\" becomes \".../build/cjs/index.js/compiler\" (invalid).\n\t// By aliasing to the package root, it becomes \".../typebox/compiler\" which resolves correctly.\n\tconst typeboxEntry = require.resolve(\"@sinclair/typebox\");\n\tconst typeboxRoot = typeboxEntry.replace(/\\/build\\/cjs\\/index\\.js$/, \"\");\n\n\t_aliases = {\n\t\t\"@mariozechner/pi-coding-agent\": packageIndex,\n\t\t\"@mariozechner/pi-coding-agent/hooks\": path.resolve(__dirname, \"index.js\"),\n\t\t\"@mariozechner/pi-tui\": require.resolve(\"@mariozechner/pi-tui\"),\n\t\t\"@mariozechner/pi-ai\": require.resolve(\"@mariozechner/pi-ai\"),\n\t\t\"@sinclair/typebox\": typeboxRoot,\n\t};\n\treturn _aliases;\n}\n\n/**\n * Generic handler function type.\n */\ntype HandlerFn = (...args: unknown[]) => Promise<unknown>;\n\n/**\n * Send handler type for pi.send().\n */\nexport type SendHandler = (text: string, attachments?: Attachment[]) => void;\n\n/**\n * Registered handlers for a loaded hook.\n */\nexport interface LoadedHook {\n\t/** Original path from config */\n\tpath: string;\n\t/** Resolved absolute path */\n\tresolvedPath: string;\n\t/** Map of event type to handler functions */\n\thandlers: Map<string, HandlerFn[]>;\n\t/** Set the send handler for this hook's pi.send() */\n\tsetSendHandler: (handler: SendHandler) => void;\n}\n\n/**\n * Result of loading hooks.\n */\nexport interface LoadHooksResult {\n\t/** Successfully loaded hooks */\n\thooks: LoadedHook[];\n\t/** Errors encountered during loading */\n\terrors: Array<{ path: string; error: string }>;\n}\n\nconst UNICODE_SPACES = /[\\u00A0\\u2000-\\u200A\\u202F\\u205F\\u3000]/g;\n\nfunction normalizeUnicodeSpaces(str: string): string {\n\treturn str.replace(UNICODE_SPACES, \" \");\n}\n\nfunction expandPath(p: string): string {\n\tconst normalized = normalizeUnicodeSpaces(p);\n\tif (normalized.startsWith(\"~/\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(2));\n\t}\n\tif (normalized.startsWith(\"~\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(1));\n\t}\n\treturn normalized;\n}\n\n/**\n * Resolve hook path.\n * - Absolute paths used as-is\n * - Paths starting with ~ expanded to home directory\n * - Relative paths resolved from cwd\n */\nfunction resolveHookPath(hookPath: string, cwd: string): string {\n\tconst expanded = expandPath(hookPath);\n\n\tif (path.isAbsolute(expanded)) {\n\t\treturn expanded;\n\t}\n\n\t// Relative paths resolved from cwd\n\treturn path.resolve(cwd, expanded);\n}\n\n/**\n * Create a HookAPI instance that collects handlers.\n * Returns the API and a function to set the send handler later.\n */\nfunction createHookAPI(handlers: Map<string, HandlerFn[]>): {\n\tapi: HookAPI;\n\tsetSendHandler: (handler: SendHandler) => void;\n} {\n\tlet sendHandler: SendHandler = () => {\n\t\t// Default no-op until mode sets the handler\n\t};\n\n\tconst api: HookAPI = {\n\t\ton(event: string, handler: HandlerFn): void {\n\t\t\tconst list = handlers.get(event) ?? [];\n\t\t\tlist.push(handler);\n\t\t\thandlers.set(event, list);\n\t\t},\n\t\tsend(text: string, attachments?: Attachment[]): void {\n\t\t\tsendHandler(text, attachments);\n\t\t},\n\t} as HookAPI;\n\n\treturn {\n\t\tapi,\n\t\tsetSendHandler: (handler: SendHandler) => {\n\t\t\tsendHandler = handler;\n\t\t},\n\t};\n}\n\n/**\n * Load a single hook module using jiti.\n */\nasync function loadHook(hookPath: string, cwd: string): Promise<{ hook: LoadedHook | null; error: string | null }> {\n\tconst resolvedPath = resolveHookPath(hookPath, cwd);\n\n\ttry {\n\t\t// Create jiti instance for TypeScript/ESM loading\n\t\t// Use aliases to resolve package imports since hooks are loaded from user directories\n\t\t// (e.g. ~/.pi/agent/hooks) but import from packages installed with pi-coding-agent\n\t\tconst jiti = createJiti(import.meta.url, {\n\t\t\talias: getAliases(),\n\t\t});\n\n\t\t// Import the module\n\t\tconst module = await jiti.import(resolvedPath, { default: true });\n\t\tconst factory = module as HookFactory;\n\n\t\tif (typeof factory !== \"function\") {\n\t\t\treturn { hook: null, error: \"Hook must export a default function\" };\n\t\t}\n\n\t\t// Create handlers map and API\n\t\tconst handlers = new Map<string, HandlerFn[]>();\n\t\tconst { api, setSendHandler } = createHookAPI(handlers);\n\n\t\t// Call factory to register handlers\n\t\tfactory(api);\n\n\t\treturn {\n\t\t\thook: { path: hookPath, resolvedPath, handlers, setSendHandler },\n\t\t\terror: null,\n\t\t};\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\treturn { hook: null, error: `Failed to load hook: ${message}` };\n\t}\n}\n\n/**\n * Load all hooks from configuration.\n * @param paths - Array of hook file paths\n * @param cwd - Current working directory for resolving relative paths\n */\nexport async function loadHooks(paths: string[], cwd: string): Promise<LoadHooksResult> {\n\tconst hooks: LoadedHook[] = [];\n\tconst errors: Array<{ path: string; error: string }> = [];\n\n\tfor (const hookPath of paths) {\n\t\tconst { hook, error } = await loadHook(hookPath, cwd);\n\n\t\tif (error) {\n\t\t\terrors.push({ path: hookPath, error });\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (hook) {\n\t\t\thooks.push(hook);\n\t\t}\n\t}\n\n\treturn { hooks, errors };\n}\n\n/**\n * Discover hook files from a directory.\n * Returns all .ts files (and symlinks to .ts files) in the directory (non-recursive).\n */\nfunction discoverHooksInDir(dir: string): string[] {\n\tif (!fs.existsSync(dir)) {\n\t\treturn [];\n\t}\n\n\ttry {\n\t\tconst entries = fs.readdirSync(dir, { withFileTypes: true });\n\t\treturn entries\n\t\t\t.filter((e) => (e.isFile() || e.isSymbolicLink()) && e.name.endsWith(\".ts\"))\n\t\t\t.map((e) => path.join(dir, e.name));\n\t} catch {\n\t\treturn [];\n\t}\n}\n\n/**\n * Discover and load hooks from standard locations:\n * 1. agentDir/hooks/*.ts (global)\n * 2. cwd/.pi/hooks/*.ts (project-local)\n *\n * Plus any explicitly configured paths from settings.\n */\nexport async function discoverAndLoadHooks(\n\tconfiguredPaths: string[],\n\tcwd: string,\n\tagentDir: string = getAgentDir(),\n): Promise<LoadHooksResult> {\n\tconst allPaths: string[] = [];\n\tconst seen = new Set<string>();\n\n\t// Helper to add paths without duplicates\n\tconst addPaths = (paths: string[]) => {\n\t\tfor (const p of paths) {\n\t\t\tconst resolved = path.resolve(p);\n\t\t\tif (!seen.has(resolved)) {\n\t\t\t\tseen.add(resolved);\n\t\t\t\tallPaths.push(p);\n\t\t\t}\n\t\t}\n\t};\n\n\t// 1. Global hooks: agentDir/hooks/\n\tconst globalHooksDir = path.join(agentDir, \"hooks\");\n\taddPaths(discoverHooksInDir(globalHooksDir));\n\n\t// 2. Project-local hooks: cwd/.pi/hooks/\n\tconst localHooksDir = path.join(cwd, \".pi\", \"hooks\");\n\taddPaths(discoverHooksInDir(localHooksDir));\n\n\t// 3. Explicitly configured paths (can override/add)\n\taddPaths(configuredPaths.map((p) => resolveHookPath(p, cwd)));\n\n\treturn loadHooks(allPaths, cwd);\n}\n"]}
|
package/dist/main.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AA6PH,wBAAsB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,iBAwKxC","sourcesContent":["/**\n * Main entry point for the coding agent CLI.\n *\n * This file handles CLI argument parsing and translates them into\n * createAgentSession() options. The SDK does the heavy lifting.\n */\n\nimport type { Attachment } from \"@mariozechner/pi-agent-core\";\nimport { supportsXhigh } from \"@mariozechner/pi-ai\";\nimport chalk from \"chalk\";\nimport { join } from \"path\";\nimport { type Args, parseArgs, printHelp } from \"./cli/args.js\";\nimport { processFileArguments } from \"./cli/file-processor.js\";\nimport { listModels } from \"./cli/list-models.js\";\nimport { selectSession } from \"./cli/session-picker.js\";\nimport { getAgentDir, getModelsPath, VERSION } from \"./config.js\";\nimport type { AgentSession } from \"./core/agent-session.js\";\nimport { AuthStorage } from \"./core/auth-storage.js\";\nimport type { LoadedCustomTool } from \"./core/custom-tools/index.js\";\nimport { exportFromFile } from \"./core/export-html.js\";\nimport type { HookUIContext } from \"./core/index.js\";\nimport type { ModelRegistry } from \"./core/model-registry.js\";\nimport { resolveModelScope, type ScopedModel } from \"./core/model-resolver.js\";\nimport { type CreateAgentSessionOptions, createAgentSession, discoverAuthStorage, discoverModels } from \"./core/sdk.js\";\nimport { SessionManager } from \"./core/session-manager.js\";\nimport { SettingsManager } from \"./core/settings-manager.js\";\nimport { resolvePromptInput } from \"./core/system-prompt.js\";\nimport { printTimings, time } from \"./core/timings.js\";\nimport { allTools } from \"./core/tools/index.js\";\nimport { InteractiveMode, runPrintMode, runRpcMode } from \"./modes/index.js\";\nimport { initTheme, stopThemeWatcher } from \"./modes/interactive/theme/theme.js\";\nimport { getChangelogPath, getNewEntries, parseChangelog } from \"./utils/changelog.js\";\nimport { ensureTool } from \"./utils/tools-manager.js\";\n\nasync function checkForNewVersion(currentVersion: string): Promise<string | null> {\n\ttry {\n\t\tconst response = await fetch(\"https://registry.npmjs.org/@mariozechner/pi -coding-agent/latest\");\n\t\tif (!response.ok) return null;\n\n\t\tconst data = (await response.json()) as { version?: string };\n\t\tconst latestVersion = data.version;\n\n\t\tif (latestVersion && latestVersion !== currentVersion) {\n\t\t\treturn latestVersion;\n\t\t}\n\n\t\treturn null;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nasync function runInteractiveMode(\n\tsession: AgentSession,\n\tversion: string,\n\tchangelogMarkdown: string | null,\n\tmodelFallbackMessage: string | undefined,\n\tmodelsJsonError: string | null,\n\tmigratedProviders: string[],\n\tversionCheckPromise: Promise<string | null>,\n\tinitialMessages: string[],\n\tcustomTools: LoadedCustomTool[],\n\tsetToolUIContext: (uiContext: HookUIContext, hasUI: boolean) => void,\n\tinitialMessage?: string,\n\tinitialAttachments?: Attachment[],\n\tfdPath: string | null = null,\n): Promise<void> {\n\tconst mode = new InteractiveMode(session, version, changelogMarkdown, customTools, setToolUIContext, fdPath);\n\n\tawait mode.init();\n\n\tversionCheckPromise.then((newVersion) => {\n\t\tif (newVersion) {\n\t\t\tmode.showNewVersionNotification(newVersion);\n\t\t}\n\t});\n\n\tmode.renderInitialMessages(session.state);\n\n\tif (migratedProviders.length > 0) {\n\t\tmode.showWarning(`Migrated credentials to auth.json: ${migratedProviders.join(\", \")}`);\n\t}\n\n\tif (modelsJsonError) {\n\t\tmode.showError(`models.json error: ${modelsJsonError}`);\n\t}\n\n\tif (modelFallbackMessage) {\n\t\tmode.showWarning(modelFallbackMessage);\n\t}\n\n\tif (initialMessage) {\n\t\ttry {\n\t\t\tawait session.prompt(initialMessage, { attachments: initialAttachments });\n\t\t} catch (error: unknown) {\n\t\t\tconst errorMessage = error instanceof Error ? error.message : \"Unknown error occurred\";\n\t\t\tmode.showError(errorMessage);\n\t\t}\n\t}\n\n\tfor (const message of initialMessages) {\n\t\ttry {\n\t\t\tawait session.prompt(message);\n\t\t} catch (error: unknown) {\n\t\t\tconst errorMessage = error instanceof Error ? error.message : \"Unknown error occurred\";\n\t\t\tmode.showError(errorMessage);\n\t\t}\n\t}\n\n\twhile (true) {\n\t\tconst userInput = await mode.getUserInput();\n\t\ttry {\n\t\t\tawait session.prompt(userInput);\n\t\t} catch (error: unknown) {\n\t\t\tconst errorMessage = error instanceof Error ? error.message : \"Unknown error occurred\";\n\t\t\tmode.showError(errorMessage);\n\t\t}\n\t}\n}\n\nasync function prepareInitialMessage(parsed: Args): Promise<{\n\tinitialMessage?: string;\n\tinitialAttachments?: Attachment[];\n}> {\n\tif (parsed.fileArgs.length === 0) {\n\t\treturn {};\n\t}\n\n\tconst { textContent, imageAttachments } = await processFileArguments(parsed.fileArgs);\n\n\tlet initialMessage: string;\n\tif (parsed.messages.length > 0) {\n\t\tinitialMessage = textContent + parsed.messages[0];\n\t\tparsed.messages.shift();\n\t} else {\n\t\tinitialMessage = textContent;\n\t}\n\n\treturn {\n\t\tinitialMessage,\n\t\tinitialAttachments: imageAttachments.length > 0 ? imageAttachments : undefined,\n\t};\n}\n\nfunction getChangelogForDisplay(parsed: Args, settingsManager: SettingsManager): string | null {\n\tif (parsed.continue || parsed.resume) {\n\t\treturn null;\n\t}\n\n\tconst lastVersion = settingsManager.getLastChangelogVersion();\n\tconst changelogPath = getChangelogPath();\n\tconst entries = parseChangelog(changelogPath);\n\n\tif (!lastVersion) {\n\t\tif (entries.length > 0) {\n\t\t\tsettingsManager.setLastChangelogVersion(VERSION);\n\t\t\treturn entries.map((e) => e.content).join(\"\\n\\n\");\n\t\t}\n\t} else {\n\t\tconst newEntries = getNewEntries(entries, lastVersion);\n\t\tif (newEntries.length > 0) {\n\t\t\tsettingsManager.setLastChangelogVersion(VERSION);\n\t\t\treturn newEntries.map((e) => e.content).join(\"\\n\\n\");\n\t\t}\n\t}\n\n\treturn null;\n}\n\nfunction createSessionManager(parsed: Args, cwd: string): SessionManager | null {\n\tif (parsed.noSession) {\n\t\treturn SessionManager.inMemory();\n\t}\n\tif (parsed.session) {\n\t\treturn SessionManager.open(parsed.session);\n\t}\n\tif (parsed.continue) {\n\t\treturn SessionManager.continueRecent(cwd);\n\t}\n\t// --resume is handled separately (needs picker UI)\n\t// Default case (new session) returns null, SDK will create one\n\treturn null;\n}\n\nfunction buildSessionOptions(\n\tparsed: Args,\n\tscopedModels: ScopedModel[],\n\tsessionManager: SessionManager | null,\n\tmodelRegistry: ModelRegistry,\n): CreateAgentSessionOptions {\n\tconst options: CreateAgentSessionOptions = {};\n\n\tconst resolvedSystemPrompt = resolvePromptInput(parsed.systemPrompt, \"system prompt\");\n\tconst resolvedAppendPrompt = resolvePromptInput(parsed.appendSystemPrompt, \"append system prompt\");\n\n\tif (sessionManager) {\n\t\toptions.sessionManager = sessionManager;\n\t}\n\n\t// Model from CLI\n\tif (parsed.provider && parsed.model) {\n\t\tconst model = modelRegistry.find(parsed.provider, parsed.model);\n\t\tif (!model) {\n\t\t\tconsole.error(chalk.red(`Model ${parsed.provider}/${parsed.model} not found`));\n\t\t\tprocess.exit(1);\n\t\t}\n\t\toptions.model = model;\n\t} else if (scopedModels.length > 0 && !parsed.continue && !parsed.resume) {\n\t\toptions.model = scopedModels[0].model;\n\t}\n\n\t// Thinking level\n\tif (parsed.thinking) {\n\t\toptions.thinkingLevel = parsed.thinking;\n\t} else if (scopedModels.length > 0 && !parsed.continue && !parsed.resume) {\n\t\toptions.thinkingLevel = scopedModels[0].thinkingLevel;\n\t}\n\n\t// Scoped models for Ctrl+P cycling\n\tif (scopedModels.length > 0) {\n\t\toptions.scopedModels = scopedModels;\n\t}\n\n\t// API key from CLI - set in authStorage\n\t// (handled by caller before createAgentSession)\n\n\t// System prompt\n\tif (resolvedSystemPrompt && resolvedAppendPrompt) {\n\t\toptions.systemPrompt = `${resolvedSystemPrompt}\\n\\n${resolvedAppendPrompt}`;\n\t} else if (resolvedSystemPrompt) {\n\t\toptions.systemPrompt = resolvedSystemPrompt;\n\t} else if (resolvedAppendPrompt) {\n\t\toptions.systemPrompt = (defaultPrompt) => `${defaultPrompt}\\n\\n${resolvedAppendPrompt}`;\n\t}\n\n\t// Tools\n\tif (parsed.tools) {\n\t\toptions.tools = parsed.tools.map((name) => allTools[name]);\n\t}\n\n\t// Skills\n\tif (parsed.noSkills) {\n\t\toptions.skills = [];\n\t}\n\n\t// Additional hook paths from CLI\n\tif (parsed.hooks && parsed.hooks.length > 0) {\n\t\toptions.additionalHookPaths = parsed.hooks;\n\t}\n\n\t// Additional custom tool paths from CLI\n\tif (parsed.customTools && parsed.customTools.length > 0) {\n\t\toptions.additionalCustomToolPaths = parsed.customTools;\n\t}\n\n\treturn options;\n}\n\nexport async function main(args: string[]) {\n\ttime(\"start\");\n\n\t// Migrate legacy oauth.json and settings.json apiKeys to auth.json\n\tconst agentDir = getAgentDir();\n\tconst migratedProviders = AuthStorage.migrateLegacy(join(agentDir, \"auth.json\"), agentDir);\n\n\t// Create AuthStorage and ModelRegistry upfront\n\tconst authStorage = discoverAuthStorage();\n\tconst modelRegistry = discoverModels(authStorage);\n\ttime(\"discoverModels\");\n\n\tconst parsed = parseArgs(args);\n\ttime(\"parseArgs\");\n\n\tif (parsed.version) {\n\t\tconsole.log(VERSION);\n\t\treturn;\n\t}\n\n\tif (parsed.help) {\n\t\tprintHelp();\n\t\treturn;\n\t}\n\n\tif (parsed.listModels !== undefined) {\n\t\tconst searchPattern = typeof parsed.listModels === \"string\" ? parsed.listModels : undefined;\n\t\tawait listModels(modelRegistry, searchPattern);\n\t\treturn;\n\t}\n\n\tif (parsed.export) {\n\t\ttry {\n\t\t\tconst outputPath = parsed.messages.length > 0 ? parsed.messages[0] : undefined;\n\t\t\tconst result = exportFromFile(parsed.export, outputPath);\n\t\t\tconsole.log(`Exported to: ${result}`);\n\t\t\treturn;\n\t\t} catch (error: unknown) {\n\t\t\tconst message = error instanceof Error ? error.message : \"Failed to export session\";\n\t\t\tconsole.error(chalk.red(`Error: ${message}`));\n\t\t\tprocess.exit(1);\n\t\t}\n\t}\n\n\tif (parsed.mode === \"rpc\" && parsed.fileArgs.length > 0) {\n\t\tconsole.error(chalk.red(\"Error: @file arguments are not supported in RPC mode\"));\n\t\tprocess.exit(1);\n\t}\n\n\tconst cwd = process.cwd();\n\tconst { initialMessage, initialAttachments } = await prepareInitialMessage(parsed);\n\ttime(\"prepareInitialMessage\");\n\tconst isInteractive = !parsed.print && parsed.mode === undefined;\n\tconst mode = parsed.mode || \"text\";\n\n\tconst settingsManager = SettingsManager.create(cwd);\n\ttime(\"SettingsManager.create\");\n\tinitTheme(settingsManager.getTheme(), isInteractive);\n\ttime(\"initTheme\");\n\n\tlet scopedModels: ScopedModel[] = [];\n\tif (parsed.models && parsed.models.length > 0) {\n\t\tscopedModels = await resolveModelScope(parsed.models, modelRegistry);\n\t\ttime(\"resolveModelScope\");\n\t}\n\n\t// Create session manager based on CLI flags\n\tlet sessionManager = createSessionManager(parsed, cwd);\n\ttime(\"createSessionManager\");\n\n\t// Handle --resume: show session picker\n\tif (parsed.resume) {\n\t\tconst sessions = SessionManager.list(cwd);\n\t\ttime(\"SessionManager.list\");\n\t\tif (sessions.length === 0) {\n\t\t\tconsole.log(chalk.dim(\"No sessions found\"));\n\t\t\treturn;\n\t\t}\n\t\tconst selectedPath = await selectSession(sessions);\n\t\ttime(\"selectSession\");\n\t\tif (!selectedPath) {\n\t\t\tconsole.log(chalk.dim(\"No session selected\"));\n\t\t\treturn;\n\t\t}\n\t\tsessionManager = SessionManager.open(selectedPath);\n\t}\n\n\tconst sessionOptions = buildSessionOptions(parsed, scopedModels, sessionManager, modelRegistry);\n\tsessionOptions.authStorage = authStorage;\n\tsessionOptions.modelRegistry = modelRegistry;\n\n\t// Handle CLI --api-key as runtime override (not persisted)\n\tif (parsed.apiKey) {\n\t\tif (!sessionOptions.model) {\n\t\t\tconsole.error(chalk.red(\"--api-key requires a model to be specified via --provider/--model or -m/--models\"));\n\t\t\tprocess.exit(1);\n\t\t}\n\t\tauthStorage.setRuntimeApiKey(sessionOptions.model.provider, parsed.apiKey);\n\t}\n\n\ttime(\"buildSessionOptions\");\n\tconst { session, customToolsResult, modelFallbackMessage } = await createAgentSession(sessionOptions);\n\ttime(\"createAgentSession\");\n\n\tif (!isInteractive && !session.model) {\n\t\tconsole.error(chalk.red(\"No models available.\"));\n\t\tconsole.error(chalk.yellow(\"\\nSet an API key environment variable:\"));\n\t\tconsole.error(\" ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, etc.\");\n\t\tconsole.error(chalk.yellow(`\\nOr create ${getModelsPath()}`));\n\t\tprocess.exit(1);\n\t}\n\n\t// Clamp thinking level to model capabilities (for CLI override case)\n\tif (session.model && parsed.thinking) {\n\t\tlet effectiveThinking = parsed.thinking;\n\t\tif (!session.model.reasoning) {\n\t\t\teffectiveThinking = \"off\";\n\t\t} else if (effectiveThinking === \"xhigh\" && !supportsXhigh(session.model)) {\n\t\t\teffectiveThinking = \"high\";\n\t\t}\n\t\tif (effectiveThinking !== session.thinkingLevel) {\n\t\t\tsession.setThinkingLevel(effectiveThinking);\n\t\t}\n\t}\n\n\tif (mode === \"rpc\") {\n\t\tawait runRpcMode(session);\n\t} else if (isInteractive) {\n\t\tconst versionCheckPromise = checkForNewVersion(VERSION).catch(() => null);\n\t\tconst changelogMarkdown = getChangelogForDisplay(parsed, settingsManager);\n\n\t\tif (scopedModels.length > 0) {\n\t\t\tconst modelList = scopedModels\n\t\t\t\t.map((sm) => {\n\t\t\t\t\tconst thinkingStr = sm.thinkingLevel !== \"off\" ? `:${sm.thinkingLevel}` : \"\";\n\t\t\t\t\treturn `${sm.model.id}${thinkingStr}`;\n\t\t\t\t})\n\t\t\t\t.join(\", \");\n\t\t\tconsole.log(chalk.dim(`Model scope: ${modelList} ${chalk.gray(\"(Ctrl+P to cycle)\")}`));\n\t\t}\n\n\t\tconst fdPath = await ensureTool(\"fd\");\n\t\ttime(\"ensureTool(fd)\");\n\n\t\tprintTimings();\n\t\tawait runInteractiveMode(\n\t\t\tsession,\n\t\t\tVERSION,\n\t\t\tchangelogMarkdown,\n\t\t\tmodelFallbackMessage,\n\t\t\tmodelRegistry.getError(),\n\t\t\tmigratedProviders,\n\t\t\tversionCheckPromise,\n\t\t\tparsed.messages,\n\t\t\tcustomToolsResult.tools,\n\t\t\tcustomToolsResult.setUIContext,\n\t\t\tinitialMessage,\n\t\t\tinitialAttachments,\n\t\t\tfdPath,\n\t\t);\n\t} else {\n\t\tawait runPrintMode(session, mode, parsed.messages, initialMessage, initialAttachments);\n\t\tstopThemeWatcher();\n\t\tif (process.stdout.writableLength > 0) {\n\t\t\tawait new Promise<void>((resolve) => process.stdout.once(\"drain\", resolve));\n\t\t}\n\t\tprocess.exit(0);\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAiRH,wBAAsB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,iBAwKxC","sourcesContent":["/**\n * Main entry point for the coding agent CLI.\n *\n * This file handles CLI argument parsing and translates them into\n * createAgentSession() options. The SDK does the heavy lifting.\n */\n\nimport type { Attachment } from \"@mariozechner/pi-agent-core\";\nimport { supportsXhigh } from \"@mariozechner/pi-ai\";\nimport chalk from \"chalk\";\nimport { existsSync } from \"fs\";\nimport { join } from \"path\";\nimport { type Args, parseArgs, printHelp } from \"./cli/args.js\";\nimport { processFileArguments } from \"./cli/file-processor.js\";\nimport { listModels } from \"./cli/list-models.js\";\nimport { selectSession } from \"./cli/session-picker.js\";\nimport { CONFIG_DIR_NAME, getAgentDir, getModelsPath, VERSION } from \"./config.js\";\nimport type { AgentSession } from \"./core/agent-session.js\";\nimport { AuthStorage } from \"./core/auth-storage.js\";\nimport type { LoadedCustomTool } from \"./core/custom-tools/index.js\";\nimport { exportFromFile } from \"./core/export-html.js\";\nimport type { HookUIContext } from \"./core/index.js\";\nimport type { ModelRegistry } from \"./core/model-registry.js\";\nimport { resolveModelScope, type ScopedModel } from \"./core/model-resolver.js\";\nimport { type CreateAgentSessionOptions, createAgentSession, discoverAuthStorage, discoverModels } from \"./core/sdk.js\";\nimport { SessionManager } from \"./core/session-manager.js\";\nimport { SettingsManager } from \"./core/settings-manager.js\";\nimport { resolvePromptInput } from \"./core/system-prompt.js\";\nimport { printTimings, time } from \"./core/timings.js\";\nimport { allTools } from \"./core/tools/index.js\";\nimport { InteractiveMode, runPrintMode, runRpcMode } from \"./modes/index.js\";\nimport { initTheme, stopThemeWatcher } from \"./modes/interactive/theme/theme.js\";\nimport { getChangelogPath, getNewEntries, parseChangelog } from \"./utils/changelog.js\";\nimport { ensureTool } from \"./utils/tools-manager.js\";\n\nasync function checkForNewVersion(currentVersion: string): Promise<string | null> {\n\ttry {\n\t\tconst response = await fetch(\"https://registry.npmjs.org/@mariozechner/pi -coding-agent/latest\");\n\t\tif (!response.ok) return null;\n\n\t\tconst data = (await response.json()) as { version?: string };\n\t\tconst latestVersion = data.version;\n\n\t\tif (latestVersion && latestVersion !== currentVersion) {\n\t\t\treturn latestVersion;\n\t\t}\n\n\t\treturn null;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nasync function runInteractiveMode(\n\tsession: AgentSession,\n\tversion: string,\n\tchangelogMarkdown: string | null,\n\tmodelFallbackMessage: string | undefined,\n\tmodelsJsonError: string | null,\n\tmigratedProviders: string[],\n\tversionCheckPromise: Promise<string | null>,\n\tinitialMessages: string[],\n\tcustomTools: LoadedCustomTool[],\n\tsetToolUIContext: (uiContext: HookUIContext, hasUI: boolean) => void,\n\tinitialMessage?: string,\n\tinitialAttachments?: Attachment[],\n\tfdPath: string | null = null,\n): Promise<void> {\n\tconst mode = new InteractiveMode(session, version, changelogMarkdown, customTools, setToolUIContext, fdPath);\n\n\tawait mode.init();\n\n\tversionCheckPromise.then((newVersion) => {\n\t\tif (newVersion) {\n\t\t\tmode.showNewVersionNotification(newVersion);\n\t\t}\n\t});\n\n\tmode.renderInitialMessages(session.state);\n\n\tif (migratedProviders.length > 0) {\n\t\tmode.showWarning(`Migrated credentials to auth.json: ${migratedProviders.join(\", \")}`);\n\t}\n\n\tif (modelsJsonError) {\n\t\tmode.showError(`models.json error: ${modelsJsonError}`);\n\t}\n\n\tif (modelFallbackMessage) {\n\t\tmode.showWarning(modelFallbackMessage);\n\t}\n\n\tif (initialMessage) {\n\t\ttry {\n\t\t\tawait session.prompt(initialMessage, { attachments: initialAttachments });\n\t\t} catch (error: unknown) {\n\t\t\tconst errorMessage = error instanceof Error ? error.message : \"Unknown error occurred\";\n\t\t\tmode.showError(errorMessage);\n\t\t}\n\t}\n\n\tfor (const message of initialMessages) {\n\t\ttry {\n\t\t\tawait session.prompt(message);\n\t\t} catch (error: unknown) {\n\t\t\tconst errorMessage = error instanceof Error ? error.message : \"Unknown error occurred\";\n\t\t\tmode.showError(errorMessage);\n\t\t}\n\t}\n\n\twhile (true) {\n\t\tconst userInput = await mode.getUserInput();\n\t\ttry {\n\t\t\tawait session.prompt(userInput);\n\t\t} catch (error: unknown) {\n\t\t\tconst errorMessage = error instanceof Error ? error.message : \"Unknown error occurred\";\n\t\t\tmode.showError(errorMessage);\n\t\t}\n\t}\n}\n\nasync function prepareInitialMessage(parsed: Args): Promise<{\n\tinitialMessage?: string;\n\tinitialAttachments?: Attachment[];\n}> {\n\tif (parsed.fileArgs.length === 0) {\n\t\treturn {};\n\t}\n\n\tconst { textContent, imageAttachments } = await processFileArguments(parsed.fileArgs);\n\n\tlet initialMessage: string;\n\tif (parsed.messages.length > 0) {\n\t\tinitialMessage = textContent + parsed.messages[0];\n\t\tparsed.messages.shift();\n\t} else {\n\t\tinitialMessage = textContent;\n\t}\n\n\treturn {\n\t\tinitialMessage,\n\t\tinitialAttachments: imageAttachments.length > 0 ? imageAttachments : undefined,\n\t};\n}\n\nfunction getChangelogForDisplay(parsed: Args, settingsManager: SettingsManager): string | null {\n\tif (parsed.continue || parsed.resume) {\n\t\treturn null;\n\t}\n\n\tconst lastVersion = settingsManager.getLastChangelogVersion();\n\tconst changelogPath = getChangelogPath();\n\tconst entries = parseChangelog(changelogPath);\n\n\tif (!lastVersion) {\n\t\tif (entries.length > 0) {\n\t\t\tsettingsManager.setLastChangelogVersion(VERSION);\n\t\t\treturn entries.map((e) => e.content).join(\"\\n\\n\");\n\t\t}\n\t} else {\n\t\tconst newEntries = getNewEntries(entries, lastVersion);\n\t\tif (newEntries.length > 0) {\n\t\t\tsettingsManager.setLastChangelogVersion(VERSION);\n\t\t\treturn newEntries.map((e) => e.content).join(\"\\n\\n\");\n\t\t}\n\t}\n\n\treturn null;\n}\n\nfunction createSessionManager(parsed: Args, cwd: string): SessionManager | null {\n\tif (parsed.noSession) {\n\t\treturn SessionManager.inMemory();\n\t}\n\tif (parsed.session) {\n\t\treturn SessionManager.open(parsed.session);\n\t}\n\tif (parsed.continue) {\n\t\treturn SessionManager.continueRecent(cwd);\n\t}\n\t// --resume is handled separately (needs picker UI)\n\t// Default case (new session) returns null, SDK will create one\n\treturn null;\n}\n\n/** Discover SYSTEM.md file if no CLI system prompt was provided */\nfunction discoverSystemPromptFile(): string | undefined {\n\t// Check project-local first: .pi/SYSTEM.md\n\tconst projectPath = join(process.cwd(), CONFIG_DIR_NAME, \"SYSTEM.md\");\n\tif (existsSync(projectPath)) {\n\t\treturn projectPath;\n\t}\n\n\t// Fall back to global: ~/.pi/agent/SYSTEM.md\n\tconst globalPath = join(getAgentDir(), \"SYSTEM.md\");\n\tif (existsSync(globalPath)) {\n\t\treturn globalPath;\n\t}\n\n\treturn undefined;\n}\n\nfunction buildSessionOptions(\n\tparsed: Args,\n\tscopedModels: ScopedModel[],\n\tsessionManager: SessionManager | null,\n\tmodelRegistry: ModelRegistry,\n): CreateAgentSessionOptions {\n\tconst options: CreateAgentSessionOptions = {};\n\n\t// Auto-discover SYSTEM.md if no CLI system prompt provided\n\tconst systemPromptSource = parsed.systemPrompt ?? discoverSystemPromptFile();\n\tconst resolvedSystemPrompt = resolvePromptInput(systemPromptSource, \"system prompt\");\n\tconst resolvedAppendPrompt = resolvePromptInput(parsed.appendSystemPrompt, \"append system prompt\");\n\n\tif (sessionManager) {\n\t\toptions.sessionManager = sessionManager;\n\t}\n\n\t// Model from CLI\n\tif (parsed.provider && parsed.model) {\n\t\tconst model = modelRegistry.find(parsed.provider, parsed.model);\n\t\tif (!model) {\n\t\t\tconsole.error(chalk.red(`Model ${parsed.provider}/${parsed.model} not found`));\n\t\t\tprocess.exit(1);\n\t\t}\n\t\toptions.model = model;\n\t} else if (scopedModels.length > 0 && !parsed.continue && !parsed.resume) {\n\t\toptions.model = scopedModels[0].model;\n\t}\n\n\t// Thinking level\n\tif (parsed.thinking) {\n\t\toptions.thinkingLevel = parsed.thinking;\n\t} else if (scopedModels.length > 0 && !parsed.continue && !parsed.resume) {\n\t\toptions.thinkingLevel = scopedModels[0].thinkingLevel;\n\t}\n\n\t// Scoped models for Ctrl+P cycling\n\tif (scopedModels.length > 0) {\n\t\toptions.scopedModels = scopedModels;\n\t}\n\n\t// API key from CLI - set in authStorage\n\t// (handled by caller before createAgentSession)\n\n\t// System prompt\n\tif (resolvedSystemPrompt && resolvedAppendPrompt) {\n\t\toptions.systemPrompt = `${resolvedSystemPrompt}\\n\\n${resolvedAppendPrompt}`;\n\t} else if (resolvedSystemPrompt) {\n\t\toptions.systemPrompt = resolvedSystemPrompt;\n\t} else if (resolvedAppendPrompt) {\n\t\toptions.systemPrompt = (defaultPrompt) => `${defaultPrompt}\\n\\n${resolvedAppendPrompt}`;\n\t}\n\n\t// Tools\n\tif (parsed.tools) {\n\t\toptions.tools = parsed.tools.map((name) => allTools[name]);\n\t}\n\n\t// Skills\n\tif (parsed.noSkills) {\n\t\toptions.skills = [];\n\t}\n\n\t// Additional hook paths from CLI\n\tif (parsed.hooks && parsed.hooks.length > 0) {\n\t\toptions.additionalHookPaths = parsed.hooks;\n\t}\n\n\t// Additional custom tool paths from CLI\n\tif (parsed.customTools && parsed.customTools.length > 0) {\n\t\toptions.additionalCustomToolPaths = parsed.customTools;\n\t}\n\n\treturn options;\n}\n\nexport async function main(args: string[]) {\n\ttime(\"start\");\n\n\t// Migrate legacy oauth.json and settings.json apiKeys to auth.json\n\tconst agentDir = getAgentDir();\n\tconst migratedProviders = AuthStorage.migrateLegacy(join(agentDir, \"auth.json\"), agentDir);\n\n\t// Create AuthStorage and ModelRegistry upfront\n\tconst authStorage = discoverAuthStorage();\n\tconst modelRegistry = discoverModels(authStorage);\n\ttime(\"discoverModels\");\n\n\tconst parsed = parseArgs(args);\n\ttime(\"parseArgs\");\n\n\tif (parsed.version) {\n\t\tconsole.log(VERSION);\n\t\treturn;\n\t}\n\n\tif (parsed.help) {\n\t\tprintHelp();\n\t\treturn;\n\t}\n\n\tif (parsed.listModels !== undefined) {\n\t\tconst searchPattern = typeof parsed.listModels === \"string\" ? parsed.listModels : undefined;\n\t\tawait listModels(modelRegistry, searchPattern);\n\t\treturn;\n\t}\n\n\tif (parsed.export) {\n\t\ttry {\n\t\t\tconst outputPath = parsed.messages.length > 0 ? parsed.messages[0] : undefined;\n\t\t\tconst result = exportFromFile(parsed.export, outputPath);\n\t\t\tconsole.log(`Exported to: ${result}`);\n\t\t\treturn;\n\t\t} catch (error: unknown) {\n\t\t\tconst message = error instanceof Error ? error.message : \"Failed to export session\";\n\t\t\tconsole.error(chalk.red(`Error: ${message}`));\n\t\t\tprocess.exit(1);\n\t\t}\n\t}\n\n\tif (parsed.mode === \"rpc\" && parsed.fileArgs.length > 0) {\n\t\tconsole.error(chalk.red(\"Error: @file arguments are not supported in RPC mode\"));\n\t\tprocess.exit(1);\n\t}\n\n\tconst cwd = process.cwd();\n\tconst { initialMessage, initialAttachments } = await prepareInitialMessage(parsed);\n\ttime(\"prepareInitialMessage\");\n\tconst isInteractive = !parsed.print && parsed.mode === undefined;\n\tconst mode = parsed.mode || \"text\";\n\n\tconst settingsManager = SettingsManager.create(cwd);\n\ttime(\"SettingsManager.create\");\n\tinitTheme(settingsManager.getTheme(), isInteractive);\n\ttime(\"initTheme\");\n\n\tlet scopedModels: ScopedModel[] = [];\n\tif (parsed.models && parsed.models.length > 0) {\n\t\tscopedModels = await resolveModelScope(parsed.models, modelRegistry);\n\t\ttime(\"resolveModelScope\");\n\t}\n\n\t// Create session manager based on CLI flags\n\tlet sessionManager = createSessionManager(parsed, cwd);\n\ttime(\"createSessionManager\");\n\n\t// Handle --resume: show session picker\n\tif (parsed.resume) {\n\t\tconst sessions = SessionManager.list(cwd);\n\t\ttime(\"SessionManager.list\");\n\t\tif (sessions.length === 0) {\n\t\t\tconsole.log(chalk.dim(\"No sessions found\"));\n\t\t\treturn;\n\t\t}\n\t\tconst selectedPath = await selectSession(sessions);\n\t\ttime(\"selectSession\");\n\t\tif (!selectedPath) {\n\t\t\tconsole.log(chalk.dim(\"No session selected\"));\n\t\t\treturn;\n\t\t}\n\t\tsessionManager = SessionManager.open(selectedPath);\n\t}\n\n\tconst sessionOptions = buildSessionOptions(parsed, scopedModels, sessionManager, modelRegistry);\n\tsessionOptions.authStorage = authStorage;\n\tsessionOptions.modelRegistry = modelRegistry;\n\n\t// Handle CLI --api-key as runtime override (not persisted)\n\tif (parsed.apiKey) {\n\t\tif (!sessionOptions.model) {\n\t\t\tconsole.error(chalk.red(\"--api-key requires a model to be specified via --provider/--model or -m/--models\"));\n\t\t\tprocess.exit(1);\n\t\t}\n\t\tauthStorage.setRuntimeApiKey(sessionOptions.model.provider, parsed.apiKey);\n\t}\n\n\ttime(\"buildSessionOptions\");\n\tconst { session, customToolsResult, modelFallbackMessage } = await createAgentSession(sessionOptions);\n\ttime(\"createAgentSession\");\n\n\tif (!isInteractive && !session.model) {\n\t\tconsole.error(chalk.red(\"No models available.\"));\n\t\tconsole.error(chalk.yellow(\"\\nSet an API key environment variable:\"));\n\t\tconsole.error(\" ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, etc.\");\n\t\tconsole.error(chalk.yellow(`\\nOr create ${getModelsPath()}`));\n\t\tprocess.exit(1);\n\t}\n\n\t// Clamp thinking level to model capabilities (for CLI override case)\n\tif (session.model && parsed.thinking) {\n\t\tlet effectiveThinking = parsed.thinking;\n\t\tif (!session.model.reasoning) {\n\t\t\teffectiveThinking = \"off\";\n\t\t} else if (effectiveThinking === \"xhigh\" && !supportsXhigh(session.model)) {\n\t\t\teffectiveThinking = \"high\";\n\t\t}\n\t\tif (effectiveThinking !== session.thinkingLevel) {\n\t\t\tsession.setThinkingLevel(effectiveThinking);\n\t\t}\n\t}\n\n\tif (mode === \"rpc\") {\n\t\tawait runRpcMode(session);\n\t} else if (isInteractive) {\n\t\tconst versionCheckPromise = checkForNewVersion(VERSION).catch(() => null);\n\t\tconst changelogMarkdown = getChangelogForDisplay(parsed, settingsManager);\n\n\t\tif (scopedModels.length > 0) {\n\t\t\tconst modelList = scopedModels\n\t\t\t\t.map((sm) => {\n\t\t\t\t\tconst thinkingStr = sm.thinkingLevel !== \"off\" ? `:${sm.thinkingLevel}` : \"\";\n\t\t\t\t\treturn `${sm.model.id}${thinkingStr}`;\n\t\t\t\t})\n\t\t\t\t.join(\", \");\n\t\t\tconsole.log(chalk.dim(`Model scope: ${modelList} ${chalk.gray(\"(Ctrl+P to cycle)\")}`));\n\t\t}\n\n\t\tconst fdPath = await ensureTool(\"fd\");\n\t\ttime(\"ensureTool(fd)\");\n\n\t\tprintTimings();\n\t\tawait runInteractiveMode(\n\t\t\tsession,\n\t\t\tVERSION,\n\t\t\tchangelogMarkdown,\n\t\t\tmodelFallbackMessage,\n\t\t\tmodelRegistry.getError(),\n\t\t\tmigratedProviders,\n\t\t\tversionCheckPromise,\n\t\t\tparsed.messages,\n\t\t\tcustomToolsResult.tools,\n\t\t\tcustomToolsResult.setUIContext,\n\t\t\tinitialMessage,\n\t\t\tinitialAttachments,\n\t\t\tfdPath,\n\t\t);\n\t} else {\n\t\tawait runPrintMode(session, mode, parsed.messages, initialMessage, initialAttachments);\n\t\tstopThemeWatcher();\n\t\tif (process.stdout.writableLength > 0) {\n\t\t\tawait new Promise<void>((resolve) => process.stdout.once(\"drain\", resolve));\n\t\t}\n\t\tprocess.exit(0);\n\t}\n}\n"]}
|