@mariozechner/pi-coding-agent 0.52.8 → 0.52.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +20 -1
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +13 -7
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/core/agent-session.d.ts +3 -6
  7. package/dist/core/agent-session.d.ts.map +1 -1
  8. package/dist/core/agent-session.js +82 -6
  9. package/dist/core/agent-session.js.map +1 -1
  10. package/dist/core/export-html/index.d.ts.map +1 -1
  11. package/dist/core/export-html/index.js +1 -1
  12. package/dist/core/export-html/index.js.map +1 -1
  13. package/dist/core/export-html/template.css +59 -0
  14. package/dist/core/export-html/template.js +21 -1
  15. package/dist/core/extensions/index.d.ts +1 -1
  16. package/dist/core/extensions/index.d.ts.map +1 -1
  17. package/dist/core/extensions/index.js.map +1 -1
  18. package/dist/core/extensions/runner.d.ts +2 -0
  19. package/dist/core/extensions/runner.d.ts.map +1 -1
  20. package/dist/core/extensions/runner.js +5 -0
  21. package/dist/core/extensions/runner.js.map +1 -1
  22. package/dist/core/extensions/types.d.ts +63 -9
  23. package/dist/core/extensions/types.d.ts.map +1 -1
  24. package/dist/core/extensions/types.js.map +1 -1
  25. package/dist/core/model-resolver.d.ts +29 -1
  26. package/dist/core/model-resolver.d.ts.map +1 -1
  27. package/dist/core/model-resolver.js +94 -4
  28. package/dist/core/model-resolver.js.map +1 -1
  29. package/dist/core/package-manager.d.ts.map +1 -1
  30. package/dist/core/package-manager.js +1 -2
  31. package/dist/core/package-manager.js.map +1 -1
  32. package/dist/index.d.ts +1 -1
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js.map +1 -1
  35. package/dist/main.d.ts.map +1 -1
  36. package/dist/main.js +35 -14
  37. package/dist/main.js.map +1 -1
  38. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  39. package/dist/modes/interactive/components/footer.js +9 -16
  40. package/dist/modes/interactive/components/footer.js.map +1 -1
  41. package/dist/modes/interactive/interactive-mode.d.ts +3 -0
  42. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  43. package/dist/modes/interactive/interactive-mode.js +25 -2
  44. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  45. package/dist/modes/print-mode.d.ts.map +1 -1
  46. package/dist/modes/print-mode.js +3 -0
  47. package/dist/modes/print-mode.js.map +1 -1
  48. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  49. package/dist/modes/rpc/rpc-mode.js +7 -0
  50. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  51. package/dist/utils/git.d.ts +5 -1
  52. package/dist/utils/git.d.ts.map +1 -1
  53. package/dist/utils/git.js +14 -3
  54. package/dist/utils/git.js.map +1 -1
  55. package/dist/utils/tools-manager.d.ts.map +1 -1
  56. package/dist/utils/tools-manager.js +12 -6
  57. package/dist/utils/tools-manager.js.map +1 -1
  58. package/docs/custom-provider.md +9 -0
  59. package/docs/extensions.md +102 -2
  60. package/docs/packages.md +5 -7
  61. package/docs/providers.md +24 -19
  62. package/docs/rpc.md +8 -2
  63. package/examples/extensions/README.md +1 -0
  64. package/examples/extensions/custom-compaction.ts +2 -2
  65. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  66. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  67. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  68. package/examples/extensions/custom-provider-qwen-cli/package.json +1 -1
  69. package/examples/extensions/reload-runtime.ts +37 -0
  70. package/examples/extensions/subagent/index.ts +2 -1
  71. package/examples/extensions/trigger-compact.ts +1 -1
  72. package/examples/extensions/with-deps/package-lock.json +2 -2
  73. package/examples/extensions/with-deps/package.json +1 -1
  74. package/package.json +4 -4
@@ -1 +1 @@
1
- {"version":3,"file":"tools-manager.d.ts","sourceRoot":"","sources":["../../src/utils/tools-manager.ts"],"names":[],"mappings":"AA0EA,wBAAgB,WAAW,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAgB5D;AAsGD,wBAAsB,UAAU,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,EAAE,MAAM,GAAE,OAAe,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAoCxG","sourcesContent":["import chalk from \"chalk\";\nimport { spawnSync } from \"child_process\";\nimport { chmodSync, createWriteStream, existsSync, mkdirSync, renameSync, rmSync } from \"fs\";\nimport { arch, platform } from \"os\";\nimport { join } from \"path\";\nimport { Readable } from \"stream\";\nimport { finished } from \"stream/promises\";\nimport { APP_NAME, getBinDir } from \"../config.js\";\n\nconst TOOLS_DIR = getBinDir();\n\ninterface ToolConfig {\n\tname: string;\n\trepo: string; // GitHub repo (e.g., \"sharkdp/fd\")\n\tbinaryName: string; // Name of the binary inside the archive\n\ttagPrefix: string; // Prefix for tags (e.g., \"v\" for v1.0.0, \"\" for 1.0.0)\n\tgetAssetName: (version: string, plat: string, architecture: string) => string | null;\n}\n\nconst TOOLS: Record<string, ToolConfig> = {\n\tfd: {\n\t\tname: \"fd\",\n\t\trepo: \"sharkdp/fd\",\n\t\tbinaryName: \"fd\",\n\t\ttagPrefix: \"v\",\n\t\tgetAssetName: (version, plat, architecture) => {\n\t\t\tif (plat === \"darwin\") {\n\t\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\t\treturn `fd-v${version}-${archStr}-apple-darwin.tar.gz`;\n\t\t\t} else if (plat === \"linux\") {\n\t\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\t\treturn `fd-v${version}-${archStr}-unknown-linux-gnu.tar.gz`;\n\t\t\t} else if (plat === \"win32\") {\n\t\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\t\treturn `fd-v${version}-${archStr}-pc-windows-msvc.zip`;\n\t\t\t}\n\t\t\treturn null;\n\t\t},\n\t},\n\trg: {\n\t\tname: \"ripgrep\",\n\t\trepo: \"BurntSushi/ripgrep\",\n\t\tbinaryName: \"rg\",\n\t\ttagPrefix: \"\",\n\t\tgetAssetName: (version, plat, architecture) => {\n\t\t\tif (plat === \"darwin\") {\n\t\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\t\treturn `ripgrep-${version}-${archStr}-apple-darwin.tar.gz`;\n\t\t\t} else if (plat === \"linux\") {\n\t\t\t\tif (architecture === \"arm64\") {\n\t\t\t\t\treturn `ripgrep-${version}-aarch64-unknown-linux-gnu.tar.gz`;\n\t\t\t\t}\n\t\t\t\treturn `ripgrep-${version}-x86_64-unknown-linux-musl.tar.gz`;\n\t\t\t} else if (plat === \"win32\") {\n\t\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\t\treturn `ripgrep-${version}-${archStr}-pc-windows-msvc.zip`;\n\t\t\t}\n\t\t\treturn null;\n\t\t},\n\t},\n};\n\n// Check if a command exists in PATH by trying to run it\nfunction commandExists(cmd: string): boolean {\n\ttry {\n\t\tconst result = spawnSync(cmd, [\"--version\"], { stdio: \"pipe\" });\n\t\t// Check for ENOENT error (command not found)\n\t\treturn result.error === undefined || result.error === null;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n// Get the path to a tool (system-wide or in our tools dir)\nexport function getToolPath(tool: \"fd\" | \"rg\"): string | null {\n\tconst config = TOOLS[tool];\n\tif (!config) return null;\n\n\t// Check our tools directory first\n\tconst localPath = join(TOOLS_DIR, config.binaryName + (platform() === \"win32\" ? \".exe\" : \"\"));\n\tif (existsSync(localPath)) {\n\t\treturn localPath;\n\t}\n\n\t// Check system PATH - if found, just return the command name (it's in PATH)\n\tif (commandExists(config.binaryName)) {\n\t\treturn config.binaryName;\n\t}\n\n\treturn null;\n}\n\n// Fetch latest release version from GitHub\nasync function getLatestVersion(repo: string): Promise<string> {\n\tconst response = await fetch(`https://api.github.com/repos/${repo}/releases/latest`, {\n\t\theaders: { \"User-Agent\": `${APP_NAME}-coding-agent` },\n\t});\n\n\tif (!response.ok) {\n\t\tthrow new Error(`GitHub API error: ${response.status}`);\n\t}\n\n\tconst data = (await response.json()) as { tag_name: string };\n\treturn data.tag_name.replace(/^v/, \"\");\n}\n\n// Download a file from URL\nasync function downloadFile(url: string, dest: string): Promise<void> {\n\tconst response = await fetch(url);\n\n\tif (!response.ok) {\n\t\tthrow new Error(`Failed to download: ${response.status}`);\n\t}\n\n\tif (!response.body) {\n\t\tthrow new Error(\"No response body\");\n\t}\n\n\tconst fileStream = createWriteStream(dest);\n\tawait finished(Readable.fromWeb(response.body as any).pipe(fileStream));\n}\n\n// Download and install a tool\nasync function downloadTool(tool: \"fd\" | \"rg\"): Promise<string> {\n\tconst config = TOOLS[tool];\n\tif (!config) throw new Error(`Unknown tool: ${tool}`);\n\n\tconst plat = platform();\n\tconst architecture = arch();\n\n\t// Get latest version\n\tconst version = await getLatestVersion(config.repo);\n\n\t// Get asset name for this platform\n\tconst assetName = config.getAssetName(version, plat, architecture);\n\tif (!assetName) {\n\t\tthrow new Error(`Unsupported platform: ${plat}/${architecture}`);\n\t}\n\n\t// Create tools directory\n\tmkdirSync(TOOLS_DIR, { recursive: true });\n\n\tconst downloadUrl = `https://github.com/${config.repo}/releases/download/${config.tagPrefix}${version}/${assetName}`;\n\tconst archivePath = join(TOOLS_DIR, assetName);\n\tconst binaryExt = plat === \"win32\" ? \".exe\" : \"\";\n\tconst binaryPath = join(TOOLS_DIR, config.binaryName + binaryExt);\n\n\t// Download\n\tawait downloadFile(downloadUrl, archivePath);\n\n\t// Extract\n\tconst extractDir = join(TOOLS_DIR, \"extract_tmp\");\n\tmkdirSync(extractDir, { recursive: true });\n\n\ttry {\n\t\tif (assetName.endsWith(\".tar.gz\")) {\n\t\t\tspawnSync(\"tar\", [\"xzf\", archivePath, \"-C\", extractDir], { stdio: \"pipe\" });\n\t\t} else if (assetName.endsWith(\".zip\")) {\n\t\t\tspawnSync(\"unzip\", [\"-o\", archivePath, \"-d\", extractDir], { stdio: \"pipe\" });\n\t\t}\n\n\t\t// Find the binary in extracted files\n\t\tconst extractedDir = join(extractDir, assetName.replace(/\\.(tar\\.gz|zip)$/, \"\"));\n\t\tconst extractedBinary = join(extractedDir, config.binaryName + binaryExt);\n\n\t\tif (existsSync(extractedBinary)) {\n\t\t\trenameSync(extractedBinary, binaryPath);\n\t\t} else {\n\t\t\tthrow new Error(`Binary not found in archive: ${extractedBinary}`);\n\t\t}\n\n\t\t// Make executable (Unix only)\n\t\tif (plat !== \"win32\") {\n\t\t\tchmodSync(binaryPath, 0o755);\n\t\t}\n\t} finally {\n\t\t// Cleanup\n\t\trmSync(archivePath, { force: true });\n\t\trmSync(extractDir, { recursive: true, force: true });\n\t}\n\n\treturn binaryPath;\n}\n\n// Termux package names for tools\nconst TERMUX_PACKAGES: Record<string, string> = {\n\tfd: \"fd-find\",\n\trg: \"ripgrep\",\n};\n\n// Ensure a tool is available, downloading if necessary\n// Returns the path to the tool, or null if unavailable\nexport async function ensureTool(tool: \"fd\" | \"rg\", silent: boolean = false): Promise<string | undefined> {\n\tconst existingPath = getToolPath(tool);\n\tif (existingPath) {\n\t\treturn existingPath;\n\t}\n\n\tconst config = TOOLS[tool];\n\tif (!config) return undefined;\n\n\t// On Android/Termux, Linux binaries don't work due to Bionic libc incompatibility.\n\t// Users must install via pkg.\n\tif (platform() === \"android\") {\n\t\tconst pkgName = TERMUX_PACKAGES[tool] ?? tool;\n\t\tif (!silent) {\n\t\t\tconsole.log(chalk.yellow(`${config.name} not found. Install with: pkg install ${pkgName}`));\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t// Tool not found - download it\n\tif (!silent) {\n\t\tconsole.log(chalk.dim(`${config.name} not found. Downloading...`));\n\t}\n\n\ttry {\n\t\tconst path = await downloadTool(tool);\n\t\tif (!silent) {\n\t\t\tconsole.log(chalk.dim(`${config.name} installed to ${path}`));\n\t\t}\n\t\treturn path;\n\t} catch (e) {\n\t\tif (!silent) {\n\t\t\tconsole.log(chalk.yellow(`Failed to download ${config.name}: ${e instanceof Error ? e.message : e}`));\n\t\t}\n\t\treturn undefined;\n\t}\n}\n"]}
1
+ {"version":3,"file":"tools-manager.d.ts","sourceRoot":"","sources":["../../src/utils/tools-manager.ts"],"names":[],"mappings":"AA0EA,wBAAgB,WAAW,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAgB5D;AA8GD,wBAAsB,UAAU,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,EAAE,MAAM,GAAE,OAAe,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAoCxG","sourcesContent":["import chalk from \"chalk\";\nimport { spawnSync } from \"child_process\";\nimport { chmodSync, createWriteStream, existsSync, mkdirSync, renameSync, rmSync } from \"fs\";\nimport { arch, platform } from \"os\";\nimport { join } from \"path\";\nimport { Readable } from \"stream\";\nimport { finished } from \"stream/promises\";\nimport { APP_NAME, getBinDir } from \"../config.js\";\n\nconst TOOLS_DIR = getBinDir();\n\ninterface ToolConfig {\n\tname: string;\n\trepo: string; // GitHub repo (e.g., \"sharkdp/fd\")\n\tbinaryName: string; // Name of the binary inside the archive\n\ttagPrefix: string; // Prefix for tags (e.g., \"v\" for v1.0.0, \"\" for 1.0.0)\n\tgetAssetName: (version: string, plat: string, architecture: string) => string | null;\n}\n\nconst TOOLS: Record<string, ToolConfig> = {\n\tfd: {\n\t\tname: \"fd\",\n\t\trepo: \"sharkdp/fd\",\n\t\tbinaryName: \"fd\",\n\t\ttagPrefix: \"v\",\n\t\tgetAssetName: (version, plat, architecture) => {\n\t\t\tif (plat === \"darwin\") {\n\t\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\t\treturn `fd-v${version}-${archStr}-apple-darwin.tar.gz`;\n\t\t\t} else if (plat === \"linux\") {\n\t\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\t\treturn `fd-v${version}-${archStr}-unknown-linux-gnu.tar.gz`;\n\t\t\t} else if (plat === \"win32\") {\n\t\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\t\treturn `fd-v${version}-${archStr}-pc-windows-msvc.zip`;\n\t\t\t}\n\t\t\treturn null;\n\t\t},\n\t},\n\trg: {\n\t\tname: \"ripgrep\",\n\t\trepo: \"BurntSushi/ripgrep\",\n\t\tbinaryName: \"rg\",\n\t\ttagPrefix: \"\",\n\t\tgetAssetName: (version, plat, architecture) => {\n\t\t\tif (plat === \"darwin\") {\n\t\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\t\treturn `ripgrep-${version}-${archStr}-apple-darwin.tar.gz`;\n\t\t\t} else if (plat === \"linux\") {\n\t\t\t\tif (architecture === \"arm64\") {\n\t\t\t\t\treturn `ripgrep-${version}-aarch64-unknown-linux-gnu.tar.gz`;\n\t\t\t\t}\n\t\t\t\treturn `ripgrep-${version}-x86_64-unknown-linux-musl.tar.gz`;\n\t\t\t} else if (plat === \"win32\") {\n\t\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\t\treturn `ripgrep-${version}-${archStr}-pc-windows-msvc.zip`;\n\t\t\t}\n\t\t\treturn null;\n\t\t},\n\t},\n};\n\n// Check if a command exists in PATH by trying to run it\nfunction commandExists(cmd: string): boolean {\n\ttry {\n\t\tconst result = spawnSync(cmd, [\"--version\"], { stdio: \"pipe\" });\n\t\t// Check for ENOENT error (command not found)\n\t\treturn result.error === undefined || result.error === null;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n// Get the path to a tool (system-wide or in our tools dir)\nexport function getToolPath(tool: \"fd\" | \"rg\"): string | null {\n\tconst config = TOOLS[tool];\n\tif (!config) return null;\n\n\t// Check our tools directory first\n\tconst localPath = join(TOOLS_DIR, config.binaryName + (platform() === \"win32\" ? \".exe\" : \"\"));\n\tif (existsSync(localPath)) {\n\t\treturn localPath;\n\t}\n\n\t// Check system PATH - if found, just return the command name (it's in PATH)\n\tif (commandExists(config.binaryName)) {\n\t\treturn config.binaryName;\n\t}\n\n\treturn null;\n}\n\n// Fetch latest release version from GitHub\nasync function getLatestVersion(repo: string): Promise<string> {\n\tconst response = await fetch(`https://api.github.com/repos/${repo}/releases/latest`, {\n\t\theaders: { \"User-Agent\": `${APP_NAME}-coding-agent` },\n\t});\n\n\tif (!response.ok) {\n\t\tthrow new Error(`GitHub API error: ${response.status}`);\n\t}\n\n\tconst data = (await response.json()) as { tag_name: string };\n\treturn data.tag_name.replace(/^v/, \"\");\n}\n\n// Download a file from URL\nasync function downloadFile(url: string, dest: string): Promise<void> {\n\tconst response = await fetch(url);\n\n\tif (!response.ok) {\n\t\tthrow new Error(`Failed to download: ${response.status}`);\n\t}\n\n\tif (!response.body) {\n\t\tthrow new Error(\"No response body\");\n\t}\n\n\tconst fileStream = createWriteStream(dest);\n\tawait finished(Readable.fromWeb(response.body as any).pipe(fileStream));\n}\n\n// Download and install a tool\nasync function downloadTool(tool: \"fd\" | \"rg\"): Promise<string> {\n\tconst config = TOOLS[tool];\n\tif (!config) throw new Error(`Unknown tool: ${tool}`);\n\n\tconst plat = platform();\n\tconst architecture = arch();\n\n\t// Get latest version\n\tconst version = await getLatestVersion(config.repo);\n\n\t// Get asset name for this platform\n\tconst assetName = config.getAssetName(version, plat, architecture);\n\tif (!assetName) {\n\t\tthrow new Error(`Unsupported platform: ${plat}/${architecture}`);\n\t}\n\n\t// Create tools directory\n\tmkdirSync(TOOLS_DIR, { recursive: true });\n\n\tconst downloadUrl = `https://github.com/${config.repo}/releases/download/${config.tagPrefix}${version}/${assetName}`;\n\tconst archivePath = join(TOOLS_DIR, assetName);\n\tconst binaryExt = plat === \"win32\" ? \".exe\" : \"\";\n\tconst binaryPath = join(TOOLS_DIR, config.binaryName + binaryExt);\n\n\t// Download\n\tawait downloadFile(downloadUrl, archivePath);\n\n\t// Extract\n\tconst extractDir = join(TOOLS_DIR, \"extract_tmp\");\n\tmkdirSync(extractDir, { recursive: true });\n\n\ttry {\n\t\t// Use tar for both .tar.gz and .zip extraction. Windows 10+ ships bsdtar\n\t\t// which handles both formats, avoiding the need for `unzip` (not available\n\t\t// on Windows by default).\n\t\tconst extractResult = assetName.endsWith(\".tar.gz\")\n\t\t\t? spawnSync(\"tar\", [\"xzf\", archivePath, \"-C\", extractDir], { stdio: \"pipe\" })\n\t\t\t: assetName.endsWith(\".zip\")\n\t\t\t\t? spawnSync(\"tar\", [\"xf\", archivePath, \"-C\", extractDir], { stdio: \"pipe\" })\n\t\t\t\t: null;\n\n\t\tif (!extractResult || extractResult.error || extractResult.status !== 0) {\n\t\t\tconst errMsg = extractResult?.error?.message ?? extractResult?.stderr?.toString().trim() ?? \"unknown error\";\n\t\t\tthrow new Error(`Failed to extract ${assetName}: ${errMsg}`);\n\t\t}\n\n\t\t// Find the binary in extracted files\n\t\tconst extractedDir = join(extractDir, assetName.replace(/\\.(tar\\.gz|zip)$/, \"\"));\n\t\tconst extractedBinary = join(extractedDir, config.binaryName + binaryExt);\n\n\t\tif (existsSync(extractedBinary)) {\n\t\t\trenameSync(extractedBinary, binaryPath);\n\t\t} else {\n\t\t\tthrow new Error(`Binary not found in archive: ${extractedBinary}`);\n\t\t}\n\n\t\t// Make executable (Unix only)\n\t\tif (plat !== \"win32\") {\n\t\t\tchmodSync(binaryPath, 0o755);\n\t\t}\n\t} finally {\n\t\t// Cleanup\n\t\trmSync(archivePath, { force: true });\n\t\trmSync(extractDir, { recursive: true, force: true });\n\t}\n\n\treturn binaryPath;\n}\n\n// Termux package names for tools\nconst TERMUX_PACKAGES: Record<string, string> = {\n\tfd: \"fd\",\n\trg: \"ripgrep\",\n};\n\n// Ensure a tool is available, downloading if necessary\n// Returns the path to the tool, or null if unavailable\nexport async function ensureTool(tool: \"fd\" | \"rg\", silent: boolean = false): Promise<string | undefined> {\n\tconst existingPath = getToolPath(tool);\n\tif (existingPath) {\n\t\treturn existingPath;\n\t}\n\n\tconst config = TOOLS[tool];\n\tif (!config) return undefined;\n\n\t// On Android/Termux, Linux binaries don't work due to Bionic libc incompatibility.\n\t// Users must install via pkg.\n\tif (platform() === \"android\") {\n\t\tconst pkgName = TERMUX_PACKAGES[tool] ?? tool;\n\t\tif (!silent) {\n\t\t\tconsole.log(chalk.yellow(`${config.name} not found. Install with: pkg install ${pkgName}`));\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t// Tool not found - download it\n\tif (!silent) {\n\t\tconsole.log(chalk.dim(`${config.name} not found. Downloading...`));\n\t}\n\n\ttry {\n\t\tconst path = await downloadTool(tool);\n\t\tif (!silent) {\n\t\t\tconsole.log(chalk.dim(`${config.name} installed to ${path}`));\n\t\t}\n\t\treturn path;\n\t} catch (e) {\n\t\tif (!silent) {\n\t\t\tconsole.log(chalk.yellow(`Failed to download ${config.name}: ${e instanceof Error ? e.message : e}`));\n\t\t}\n\t\treturn undefined;\n\t}\n}\n"]}
@@ -129,11 +129,17 @@ async function downloadTool(tool) {
129
129
  const extractDir = join(TOOLS_DIR, "extract_tmp");
130
130
  mkdirSync(extractDir, { recursive: true });
131
131
  try {
132
- if (assetName.endsWith(".tar.gz")) {
133
- spawnSync("tar", ["xzf", archivePath, "-C", extractDir], { stdio: "pipe" });
134
- }
135
- else if (assetName.endsWith(".zip")) {
136
- spawnSync("unzip", ["-o", archivePath, "-d", extractDir], { stdio: "pipe" });
132
+ // Use tar for both .tar.gz and .zip extraction. Windows 10+ ships bsdtar
133
+ // which handles both formats, avoiding the need for `unzip` (not available
134
+ // on Windows by default).
135
+ const extractResult = assetName.endsWith(".tar.gz")
136
+ ? spawnSync("tar", ["xzf", archivePath, "-C", extractDir], { stdio: "pipe" })
137
+ : assetName.endsWith(".zip")
138
+ ? spawnSync("tar", ["xf", archivePath, "-C", extractDir], { stdio: "pipe" })
139
+ : null;
140
+ if (!extractResult || extractResult.error || extractResult.status !== 0) {
141
+ const errMsg = extractResult?.error?.message ?? extractResult?.stderr?.toString().trim() ?? "unknown error";
142
+ throw new Error(`Failed to extract ${assetName}: ${errMsg}`);
137
143
  }
138
144
  // Find the binary in extracted files
139
145
  const extractedDir = join(extractDir, assetName.replace(/\.(tar\.gz|zip)$/, ""));
@@ -158,7 +164,7 @@ async function downloadTool(tool) {
158
164
  }
159
165
  // Termux package names for tools
160
166
  const TERMUX_PACKAGES = {
161
- fd: "fd-find",
167
+ fd: "fd",
162
168
  rg: "ripgrep",
163
169
  };
164
170
  // Ensure a tool is available, downloading if necessary
@@ -1 +1 @@
1
- {"version":3,"file":"tools-manager.js","sourceRoot":"","sources":["../../src/utils/tools-manager.ts"],"names":[],"mappings":"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","sourcesContent":["import chalk from \"chalk\";\nimport { spawnSync } from \"child_process\";\nimport { chmodSync, createWriteStream, existsSync, mkdirSync, renameSync, rmSync } from \"fs\";\nimport { arch, platform } from \"os\";\nimport { join } from \"path\";\nimport { Readable } from \"stream\";\nimport { finished } from \"stream/promises\";\nimport { APP_NAME, getBinDir } from \"../config.js\";\n\nconst TOOLS_DIR = getBinDir();\n\ninterface ToolConfig {\n\tname: string;\n\trepo: string; // GitHub repo (e.g., \"sharkdp/fd\")\n\tbinaryName: string; // Name of the binary inside the archive\n\ttagPrefix: string; // Prefix for tags (e.g., \"v\" for v1.0.0, \"\" for 1.0.0)\n\tgetAssetName: (version: string, plat: string, architecture: string) => string | null;\n}\n\nconst TOOLS: Record<string, ToolConfig> = {\n\tfd: {\n\t\tname: \"fd\",\n\t\trepo: \"sharkdp/fd\",\n\t\tbinaryName: \"fd\",\n\t\ttagPrefix: \"v\",\n\t\tgetAssetName: (version, plat, architecture) => {\n\t\t\tif (plat === \"darwin\") {\n\t\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\t\treturn `fd-v${version}-${archStr}-apple-darwin.tar.gz`;\n\t\t\t} else if (plat === \"linux\") {\n\t\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\t\treturn `fd-v${version}-${archStr}-unknown-linux-gnu.tar.gz`;\n\t\t\t} else if (plat === \"win32\") {\n\t\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\t\treturn `fd-v${version}-${archStr}-pc-windows-msvc.zip`;\n\t\t\t}\n\t\t\treturn null;\n\t\t},\n\t},\n\trg: {\n\t\tname: \"ripgrep\",\n\t\trepo: \"BurntSushi/ripgrep\",\n\t\tbinaryName: \"rg\",\n\t\ttagPrefix: \"\",\n\t\tgetAssetName: (version, plat, architecture) => {\n\t\t\tif (plat === \"darwin\") {\n\t\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\t\treturn `ripgrep-${version}-${archStr}-apple-darwin.tar.gz`;\n\t\t\t} else if (plat === \"linux\") {\n\t\t\t\tif (architecture === \"arm64\") {\n\t\t\t\t\treturn `ripgrep-${version}-aarch64-unknown-linux-gnu.tar.gz`;\n\t\t\t\t}\n\t\t\t\treturn `ripgrep-${version}-x86_64-unknown-linux-musl.tar.gz`;\n\t\t\t} else if (plat === \"win32\") {\n\t\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\t\treturn `ripgrep-${version}-${archStr}-pc-windows-msvc.zip`;\n\t\t\t}\n\t\t\treturn null;\n\t\t},\n\t},\n};\n\n// Check if a command exists in PATH by trying to run it\nfunction commandExists(cmd: string): boolean {\n\ttry {\n\t\tconst result = spawnSync(cmd, [\"--version\"], { stdio: \"pipe\" });\n\t\t// Check for ENOENT error (command not found)\n\t\treturn result.error === undefined || result.error === null;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n// Get the path to a tool (system-wide or in our tools dir)\nexport function getToolPath(tool: \"fd\" | \"rg\"): string | null {\n\tconst config = TOOLS[tool];\n\tif (!config) return null;\n\n\t// Check our tools directory first\n\tconst localPath = join(TOOLS_DIR, config.binaryName + (platform() === \"win32\" ? \".exe\" : \"\"));\n\tif (existsSync(localPath)) {\n\t\treturn localPath;\n\t}\n\n\t// Check system PATH - if found, just return the command name (it's in PATH)\n\tif (commandExists(config.binaryName)) {\n\t\treturn config.binaryName;\n\t}\n\n\treturn null;\n}\n\n// Fetch latest release version from GitHub\nasync function getLatestVersion(repo: string): Promise<string> {\n\tconst response = await fetch(`https://api.github.com/repos/${repo}/releases/latest`, {\n\t\theaders: { \"User-Agent\": `${APP_NAME}-coding-agent` },\n\t});\n\n\tif (!response.ok) {\n\t\tthrow new Error(`GitHub API error: ${response.status}`);\n\t}\n\n\tconst data = (await response.json()) as { tag_name: string };\n\treturn data.tag_name.replace(/^v/, \"\");\n}\n\n// Download a file from URL\nasync function downloadFile(url: string, dest: string): Promise<void> {\n\tconst response = await fetch(url);\n\n\tif (!response.ok) {\n\t\tthrow new Error(`Failed to download: ${response.status}`);\n\t}\n\n\tif (!response.body) {\n\t\tthrow new Error(\"No response body\");\n\t}\n\n\tconst fileStream = createWriteStream(dest);\n\tawait finished(Readable.fromWeb(response.body as any).pipe(fileStream));\n}\n\n// Download and install a tool\nasync function downloadTool(tool: \"fd\" | \"rg\"): Promise<string> {\n\tconst config = TOOLS[tool];\n\tif (!config) throw new Error(`Unknown tool: ${tool}`);\n\n\tconst plat = platform();\n\tconst architecture = arch();\n\n\t// Get latest version\n\tconst version = await getLatestVersion(config.repo);\n\n\t// Get asset name for this platform\n\tconst assetName = config.getAssetName(version, plat, architecture);\n\tif (!assetName) {\n\t\tthrow new Error(`Unsupported platform: ${plat}/${architecture}`);\n\t}\n\n\t// Create tools directory\n\tmkdirSync(TOOLS_DIR, { recursive: true });\n\n\tconst downloadUrl = `https://github.com/${config.repo}/releases/download/${config.tagPrefix}${version}/${assetName}`;\n\tconst archivePath = join(TOOLS_DIR, assetName);\n\tconst binaryExt = plat === \"win32\" ? \".exe\" : \"\";\n\tconst binaryPath = join(TOOLS_DIR, config.binaryName + binaryExt);\n\n\t// Download\n\tawait downloadFile(downloadUrl, archivePath);\n\n\t// Extract\n\tconst extractDir = join(TOOLS_DIR, \"extract_tmp\");\n\tmkdirSync(extractDir, { recursive: true });\n\n\ttry {\n\t\tif (assetName.endsWith(\".tar.gz\")) {\n\t\t\tspawnSync(\"tar\", [\"xzf\", archivePath, \"-C\", extractDir], { stdio: \"pipe\" });\n\t\t} else if (assetName.endsWith(\".zip\")) {\n\t\t\tspawnSync(\"unzip\", [\"-o\", archivePath, \"-d\", extractDir], { stdio: \"pipe\" });\n\t\t}\n\n\t\t// Find the binary in extracted files\n\t\tconst extractedDir = join(extractDir, assetName.replace(/\\.(tar\\.gz|zip)$/, \"\"));\n\t\tconst extractedBinary = join(extractedDir, config.binaryName + binaryExt);\n\n\t\tif (existsSync(extractedBinary)) {\n\t\t\trenameSync(extractedBinary, binaryPath);\n\t\t} else {\n\t\t\tthrow new Error(`Binary not found in archive: ${extractedBinary}`);\n\t\t}\n\n\t\t// Make executable (Unix only)\n\t\tif (plat !== \"win32\") {\n\t\t\tchmodSync(binaryPath, 0o755);\n\t\t}\n\t} finally {\n\t\t// Cleanup\n\t\trmSync(archivePath, { force: true });\n\t\trmSync(extractDir, { recursive: true, force: true });\n\t}\n\n\treturn binaryPath;\n}\n\n// Termux package names for tools\nconst TERMUX_PACKAGES: Record<string, string> = {\n\tfd: \"fd-find\",\n\trg: \"ripgrep\",\n};\n\n// Ensure a tool is available, downloading if necessary\n// Returns the path to the tool, or null if unavailable\nexport async function ensureTool(tool: \"fd\" | \"rg\", silent: boolean = false): Promise<string | undefined> {\n\tconst existingPath = getToolPath(tool);\n\tif (existingPath) {\n\t\treturn existingPath;\n\t}\n\n\tconst config = TOOLS[tool];\n\tif (!config) return undefined;\n\n\t// On Android/Termux, Linux binaries don't work due to Bionic libc incompatibility.\n\t// Users must install via pkg.\n\tif (platform() === \"android\") {\n\t\tconst pkgName = TERMUX_PACKAGES[tool] ?? tool;\n\t\tif (!silent) {\n\t\t\tconsole.log(chalk.yellow(`${config.name} not found. Install with: pkg install ${pkgName}`));\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t// Tool not found - download it\n\tif (!silent) {\n\t\tconsole.log(chalk.dim(`${config.name} not found. Downloading...`));\n\t}\n\n\ttry {\n\t\tconst path = await downloadTool(tool);\n\t\tif (!silent) {\n\t\t\tconsole.log(chalk.dim(`${config.name} installed to ${path}`));\n\t\t}\n\t\treturn path;\n\t} catch (e) {\n\t\tif (!silent) {\n\t\t\tconsole.log(chalk.yellow(`Failed to download ${config.name}: ${e instanceof Error ? e.message : e}`));\n\t\t}\n\t\treturn undefined;\n\t}\n}\n"]}
1
+ {"version":3,"file":"tools-manager.js","sourceRoot":"","sources":["../../src/utils/tools-manager.ts"],"names":[],"mappings":"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","sourcesContent":["import chalk from \"chalk\";\nimport { spawnSync } from \"child_process\";\nimport { chmodSync, createWriteStream, existsSync, mkdirSync, renameSync, rmSync } from \"fs\";\nimport { arch, platform } from \"os\";\nimport { join } from \"path\";\nimport { Readable } from \"stream\";\nimport { finished } from \"stream/promises\";\nimport { APP_NAME, getBinDir } from \"../config.js\";\n\nconst TOOLS_DIR = getBinDir();\n\ninterface ToolConfig {\n\tname: string;\n\trepo: string; // GitHub repo (e.g., \"sharkdp/fd\")\n\tbinaryName: string; // Name of the binary inside the archive\n\ttagPrefix: string; // Prefix for tags (e.g., \"v\" for v1.0.0, \"\" for 1.0.0)\n\tgetAssetName: (version: string, plat: string, architecture: string) => string | null;\n}\n\nconst TOOLS: Record<string, ToolConfig> = {\n\tfd: {\n\t\tname: \"fd\",\n\t\trepo: \"sharkdp/fd\",\n\t\tbinaryName: \"fd\",\n\t\ttagPrefix: \"v\",\n\t\tgetAssetName: (version, plat, architecture) => {\n\t\t\tif (plat === \"darwin\") {\n\t\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\t\treturn `fd-v${version}-${archStr}-apple-darwin.tar.gz`;\n\t\t\t} else if (plat === \"linux\") {\n\t\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\t\treturn `fd-v${version}-${archStr}-unknown-linux-gnu.tar.gz`;\n\t\t\t} else if (plat === \"win32\") {\n\t\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\t\treturn `fd-v${version}-${archStr}-pc-windows-msvc.zip`;\n\t\t\t}\n\t\t\treturn null;\n\t\t},\n\t},\n\trg: {\n\t\tname: \"ripgrep\",\n\t\trepo: \"BurntSushi/ripgrep\",\n\t\tbinaryName: \"rg\",\n\t\ttagPrefix: \"\",\n\t\tgetAssetName: (version, plat, architecture) => {\n\t\t\tif (plat === \"darwin\") {\n\t\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\t\treturn `ripgrep-${version}-${archStr}-apple-darwin.tar.gz`;\n\t\t\t} else if (plat === \"linux\") {\n\t\t\t\tif (architecture === \"arm64\") {\n\t\t\t\t\treturn `ripgrep-${version}-aarch64-unknown-linux-gnu.tar.gz`;\n\t\t\t\t}\n\t\t\t\treturn `ripgrep-${version}-x86_64-unknown-linux-musl.tar.gz`;\n\t\t\t} else if (plat === \"win32\") {\n\t\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\t\treturn `ripgrep-${version}-${archStr}-pc-windows-msvc.zip`;\n\t\t\t}\n\t\t\treturn null;\n\t\t},\n\t},\n};\n\n// Check if a command exists in PATH by trying to run it\nfunction commandExists(cmd: string): boolean {\n\ttry {\n\t\tconst result = spawnSync(cmd, [\"--version\"], { stdio: \"pipe\" });\n\t\t// Check for ENOENT error (command not found)\n\t\treturn result.error === undefined || result.error === null;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n// Get the path to a tool (system-wide or in our tools dir)\nexport function getToolPath(tool: \"fd\" | \"rg\"): string | null {\n\tconst config = TOOLS[tool];\n\tif (!config) return null;\n\n\t// Check our tools directory first\n\tconst localPath = join(TOOLS_DIR, config.binaryName + (platform() === \"win32\" ? \".exe\" : \"\"));\n\tif (existsSync(localPath)) {\n\t\treturn localPath;\n\t}\n\n\t// Check system PATH - if found, just return the command name (it's in PATH)\n\tif (commandExists(config.binaryName)) {\n\t\treturn config.binaryName;\n\t}\n\n\treturn null;\n}\n\n// Fetch latest release version from GitHub\nasync function getLatestVersion(repo: string): Promise<string> {\n\tconst response = await fetch(`https://api.github.com/repos/${repo}/releases/latest`, {\n\t\theaders: { \"User-Agent\": `${APP_NAME}-coding-agent` },\n\t});\n\n\tif (!response.ok) {\n\t\tthrow new Error(`GitHub API error: ${response.status}`);\n\t}\n\n\tconst data = (await response.json()) as { tag_name: string };\n\treturn data.tag_name.replace(/^v/, \"\");\n}\n\n// Download a file from URL\nasync function downloadFile(url: string, dest: string): Promise<void> {\n\tconst response = await fetch(url);\n\n\tif (!response.ok) {\n\t\tthrow new Error(`Failed to download: ${response.status}`);\n\t}\n\n\tif (!response.body) {\n\t\tthrow new Error(\"No response body\");\n\t}\n\n\tconst fileStream = createWriteStream(dest);\n\tawait finished(Readable.fromWeb(response.body as any).pipe(fileStream));\n}\n\n// Download and install a tool\nasync function downloadTool(tool: \"fd\" | \"rg\"): Promise<string> {\n\tconst config = TOOLS[tool];\n\tif (!config) throw new Error(`Unknown tool: ${tool}`);\n\n\tconst plat = platform();\n\tconst architecture = arch();\n\n\t// Get latest version\n\tconst version = await getLatestVersion(config.repo);\n\n\t// Get asset name for this platform\n\tconst assetName = config.getAssetName(version, plat, architecture);\n\tif (!assetName) {\n\t\tthrow new Error(`Unsupported platform: ${plat}/${architecture}`);\n\t}\n\n\t// Create tools directory\n\tmkdirSync(TOOLS_DIR, { recursive: true });\n\n\tconst downloadUrl = `https://github.com/${config.repo}/releases/download/${config.tagPrefix}${version}/${assetName}`;\n\tconst archivePath = join(TOOLS_DIR, assetName);\n\tconst binaryExt = plat === \"win32\" ? \".exe\" : \"\";\n\tconst binaryPath = join(TOOLS_DIR, config.binaryName + binaryExt);\n\n\t// Download\n\tawait downloadFile(downloadUrl, archivePath);\n\n\t// Extract\n\tconst extractDir = join(TOOLS_DIR, \"extract_tmp\");\n\tmkdirSync(extractDir, { recursive: true });\n\n\ttry {\n\t\t// Use tar for both .tar.gz and .zip extraction. Windows 10+ ships bsdtar\n\t\t// which handles both formats, avoiding the need for `unzip` (not available\n\t\t// on Windows by default).\n\t\tconst extractResult = assetName.endsWith(\".tar.gz\")\n\t\t\t? spawnSync(\"tar\", [\"xzf\", archivePath, \"-C\", extractDir], { stdio: \"pipe\" })\n\t\t\t: assetName.endsWith(\".zip\")\n\t\t\t\t? spawnSync(\"tar\", [\"xf\", archivePath, \"-C\", extractDir], { stdio: \"pipe\" })\n\t\t\t\t: null;\n\n\t\tif (!extractResult || extractResult.error || extractResult.status !== 0) {\n\t\t\tconst errMsg = extractResult?.error?.message ?? extractResult?.stderr?.toString().trim() ?? \"unknown error\";\n\t\t\tthrow new Error(`Failed to extract ${assetName}: ${errMsg}`);\n\t\t}\n\n\t\t// Find the binary in extracted files\n\t\tconst extractedDir = join(extractDir, assetName.replace(/\\.(tar\\.gz|zip)$/, \"\"));\n\t\tconst extractedBinary = join(extractedDir, config.binaryName + binaryExt);\n\n\t\tif (existsSync(extractedBinary)) {\n\t\t\trenameSync(extractedBinary, binaryPath);\n\t\t} else {\n\t\t\tthrow new Error(`Binary not found in archive: ${extractedBinary}`);\n\t\t}\n\n\t\t// Make executable (Unix only)\n\t\tif (plat !== \"win32\") {\n\t\t\tchmodSync(binaryPath, 0o755);\n\t\t}\n\t} finally {\n\t\t// Cleanup\n\t\trmSync(archivePath, { force: true });\n\t\trmSync(extractDir, { recursive: true, force: true });\n\t}\n\n\treturn binaryPath;\n}\n\n// Termux package names for tools\nconst TERMUX_PACKAGES: Record<string, string> = {\n\tfd: \"fd\",\n\trg: \"ripgrep\",\n};\n\n// Ensure a tool is available, downloading if necessary\n// Returns the path to the tool, or null if unavailable\nexport async function ensureTool(tool: \"fd\" | \"rg\", silent: boolean = false): Promise<string | undefined> {\n\tconst existingPath = getToolPath(tool);\n\tif (existingPath) {\n\t\treturn existingPath;\n\t}\n\n\tconst config = TOOLS[tool];\n\tif (!config) return undefined;\n\n\t// On Android/Termux, Linux binaries don't work due to Bionic libc incompatibility.\n\t// Users must install via pkg.\n\tif (platform() === \"android\") {\n\t\tconst pkgName = TERMUX_PACKAGES[tool] ?? tool;\n\t\tif (!silent) {\n\t\t\tconsole.log(chalk.yellow(`${config.name} not found. Install with: pkg install ${pkgName}`));\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t// Tool not found - download it\n\tif (!silent) {\n\t\tconsole.log(chalk.dim(`${config.name} not found. Downloading...`));\n\t}\n\n\ttry {\n\t\tconst path = await downloadTool(tool);\n\t\tif (!silent) {\n\t\t\tconsole.log(chalk.dim(`${config.name} installed to ${path}`));\n\t\t}\n\t\treturn path;\n\t} catch (e) {\n\t\tif (!silent) {\n\t\t\tconsole.log(chalk.yellow(`Failed to download ${config.name}: ${e instanceof Error ? e.message : e}`));\n\t\t}\n\t\treturn undefined;\n\t}\n}\n"]}
@@ -7,8 +7,17 @@ Extensions can register custom model providers via `pi.registerProvider()`. This
7
7
  - **OAuth/SSO** - Add authentication flows for enterprise providers
8
8
  - **Custom APIs** - Implement streaming for non-standard LLM APIs
9
9
 
10
+ ## Example Extensions
11
+
12
+ See these complete provider examples:
13
+
14
+ - [`examples/extensions/custom-provider-anthropic/`](../examples/extensions/custom-provider-anthropic/)
15
+ - [`examples/extensions/custom-provider-gitlab-duo/`](../examples/extensions/custom-provider-gitlab-duo/)
16
+ - [`examples/extensions/custom-provider-qwen-cli/`](../examples/extensions/custom-provider-qwen-cli/)
17
+
10
18
  ## Table of Contents
11
19
 
20
+ - [Example Extensions](#example-extensions)
12
21
  - [Quick Reference](#quick-reference)
13
22
  - [Override Existing Provider](#override-existing-provider)
14
23
  - [Register New Provider](#register-new-provider)
@@ -237,6 +237,7 @@ user sends prompt ────────────────────
237
237
  ├─► (skill/template expansion if not handled) │
238
238
  ├─► before_agent_start (can inject message, modify system prompt)
239
239
  ├─► agent_start │
240
+ ├─► message_start / message_update / message_end │
240
241
  │ │
241
242
  │ ┌─── turn (repeats while LLM calls tools) ───┐ │
242
243
  │ │ │ │
@@ -245,7 +246,9 @@ user sends prompt ────────────────────
245
246
  │ │ │ │
246
247
  │ │ LLM responds, may call tools: │ │
247
248
  │ │ ├─► tool_call (can block) │ │
248
- │ │ │ tool executes │ │
249
+ │ │ ├─► tool_execution_start │ │
250
+ │ │ ├─► tool_execution_update │ │
251
+ │ │ ├─► tool_execution_end │ │
249
252
  │ │ └─► tool_result (can modify) │ │
250
253
  │ │ │ │
251
254
  │ └─► turn_end │ │
@@ -434,6 +437,46 @@ pi.on("turn_end", async (event, ctx) => {
434
437
  });
435
438
  ```
436
439
 
440
+ #### message_start / message_update / message_end
441
+
442
+ Fired for message lifecycle updates.
443
+
444
+ - `message_start` and `message_end` fire for user, assistant, and toolResult messages.
445
+ - `message_update` fires for assistant streaming updates.
446
+
447
+ ```typescript
448
+ pi.on("message_start", async (event, ctx) => {
449
+ // event.message
450
+ });
451
+
452
+ pi.on("message_update", async (event, ctx) => {
453
+ // event.message
454
+ // event.assistantMessageEvent (token-by-token stream event)
455
+ });
456
+
457
+ pi.on("message_end", async (event, ctx) => {
458
+ // event.message
459
+ });
460
+ ```
461
+
462
+ #### tool_execution_start / tool_execution_update / tool_execution_end
463
+
464
+ Fired for tool execution lifecycle updates.
465
+
466
+ ```typescript
467
+ pi.on("tool_execution_start", async (event, ctx) => {
468
+ // event.toolCallId, event.toolName, event.args
469
+ });
470
+
471
+ pi.on("tool_execution_update", async (event, ctx) => {
472
+ // event.toolCallId, event.toolName, event.args, event.partialResult
473
+ });
474
+
475
+ pi.on("tool_execution_end", async (event, ctx) => {
476
+ // event.toolCallId, event.toolName, event.result, event.isError
477
+ });
478
+ ```
479
+
437
480
  #### context
438
481
 
439
482
  Fired before each LLM call. Modify messages non-destructively. See [session.md](session.md) for message types.
@@ -623,7 +666,7 @@ UI methods for user interaction. See [Custom UI](#custom-ui) for full details.
623
666
 
624
667
  ### ctx.hasUI
625
668
 
626
- `false` in print mode (`-p`), JSON mode, and RPC mode. Always check before using `ctx.ui`.
669
+ `false` in print mode (`-p`) and JSON mode. `true` in interactive and RPC mode. In RPC mode, dialog methods (`select`, `confirm`, `input`, `editor`) work via the extension UI sub-protocol, and fire-and-forget methods (`notify`, `setStatus`, `setWidget`, `setTitle`, `setEditorText`) emit requests to the client. Some TUI-specific methods are no-ops or return defaults (see [rpc.md](rpc.md#extension-ui-protocol)).
627
670
 
628
671
  ### ctx.cwd
629
672
 
@@ -771,6 +814,62 @@ Options:
771
814
  - `replaceInstructions`: If true, `customInstructions` replaces the default prompt instead of being appended
772
815
  - `label`: Label to attach to the branch summary entry (or target entry if not summarizing)
773
816
 
817
+ ### ctx.reload()
818
+
819
+ Run the same reload flow as `/reload`.
820
+
821
+ ```typescript
822
+ pi.registerCommand("reload-runtime", {
823
+ description: "Reload extensions, skills, prompts, and themes",
824
+ handler: async (_args, ctx) => {
825
+ await ctx.reload();
826
+ return;
827
+ },
828
+ });
829
+ ```
830
+
831
+ Important behavior:
832
+ - `await ctx.reload()` emits `session_shutdown` for the current extension runtime
833
+ - It then reloads resources and emits `session_start` (and `resources_discover` with reason `"reload"`) for the new runtime
834
+ - The currently running command handler still continues in the old call frame
835
+ - Code after `await ctx.reload()` still runs from the pre-reload version
836
+ - Code after `await ctx.reload()` must not assume old in-memory extension state is still valid
837
+ - After the handler returns, future commands/events/tool calls use the new extension version
838
+
839
+ For predictable behavior, treat reload as terminal for that handler (`await ctx.reload(); return;`).
840
+
841
+ Tools run with `ExtensionContext`, so they cannot call `ctx.reload()` directly. Use a command as the reload entrypoint, then expose a tool that queues that command as a follow-up user message.
842
+
843
+ Example tool the LLM can call to trigger reload:
844
+
845
+ ```typescript
846
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
847
+ import { Type } from "@sinclair/typebox";
848
+
849
+ export default function (pi: ExtensionAPI) {
850
+ pi.registerCommand("reload-runtime", {
851
+ description: "Reload extensions, skills, prompts, and themes",
852
+ handler: async (_args, ctx) => {
853
+ await ctx.reload();
854
+ return;
855
+ },
856
+ });
857
+
858
+ pi.registerTool({
859
+ name: "reload_runtime",
860
+ label: "Reload Runtime",
861
+ description: "Reload extensions, skills, prompts, and themes",
862
+ parameters: Type.Object({}),
863
+ async execute() {
864
+ pi.sendUserMessage("/reload-runtime", { deliverAs: "followUp" });
865
+ return {
866
+ content: [{ type: "text", text: "Queued /reload-runtime as a follow-up command." }],
867
+ };
868
+ },
869
+ });
870
+ }
871
+ ```
872
+
774
873
  ## ExtensionAPI Methods
775
874
 
776
875
  ### pi.on(event, handler)
@@ -1778,6 +1877,7 @@ All examples in [examples/extensions/](../examples/extensions/).
1778
1877
  | `handoff.ts` | Cross-provider model handoff | `registerCommand`, `ui.editor`, `ui.custom` |
1779
1878
  | `qna.ts` | Q&A with custom UI | `registerCommand`, `ui.custom`, `setEditorText` |
1780
1879
  | `send-user-message.ts` | Inject user messages | `registerCommand`, `sendUserMessage` |
1880
+ | `reload-runtime.ts` | Reload command and LLM tool handoff | `registerCommand`, `ctx.reload()`, `sendUserMessage` |
1781
1881
  | `shutdown-command.ts` | Graceful shutdown command | `registerCommand`, `shutdown()` |
1782
1882
  | **Events & Gates** |||
1783
1883
  | `permission-gate.ts` | Block dangerous commands | `on("tool_call")`, `ui.confirm` |
package/docs/packages.md CHANGED
@@ -59,32 +59,30 @@ npm:pkg
59
59
 
60
60
  ```
61
61
  git:github.com/user/repo@v1
62
+ git:git@github.com:user/repo@v1
62
63
  https://github.com/user/repo@v1
63
- git@github.com:user/repo@v1
64
64
  ssh://git@github.com/user/repo@v1
65
65
  ```
66
66
 
67
+ - Without `git:` prefix, only protocol URLs are accepted (`https://`, `http://`, `ssh://`, `git://`).
68
+ - With `git:` prefix, shorthand formats are accepted, including `github.com/user/repo` and `git@github.com:user/repo`.
67
69
  - HTTPS and SSH URLs are both supported.
68
70
  - SSH URLs use your configured SSH keys automatically (respects `~/.ssh/config`).
69
71
  - For non-interactive runs (for example CI), you can set `GIT_TERMINAL_PROMPT=0` to disable credential prompts and set `GIT_SSH_COMMAND` (for example `ssh -o BatchMode=yes -o ConnectTimeout=5`) to fail fast.
70
- - Raw `https://` URLs work without the `git:` prefix.
71
72
  - Refs pin the package and skip `pi update`.
72
73
  - Cloned to `~/.pi/agent/git/<host>/<path>` (global) or `.pi/git/<host>/<path>` (project).
73
74
  - Runs `npm install` after clone or pull if `package.json` exists.
74
75
 
75
76
  **SSH examples:**
76
77
  ```bash
77
- # Standard git@host:path format
78
- pi install git@github.com:user/repo
79
-
80
- # With git: prefix
78
+ # git@host:path shorthand (requires git: prefix)
81
79
  pi install git:git@github.com:user/repo
82
80
 
83
81
  # ssh:// protocol format
84
82
  pi install ssh://git@github.com/user/repo
85
83
 
86
84
  # With version ref
87
- pi install git@github.com:user/repo@v1.0.0
85
+ pi install git:git@github.com:user/repo@v1.0.0
88
86
  ```
89
87
 
90
88
  ### Local Paths
package/docs/providers.md CHANGED
@@ -42,6 +42,8 @@ Use `/logout` to clear credentials. Tokens are stored in `~/.pi/agent/auth.json`
42
42
 
43
43
  ## API Keys
44
44
 
45
+ ### Environment Variables or Auth File
46
+
45
47
  Set via environment variable:
46
48
 
47
49
  ```bash
@@ -49,25 +51,28 @@ export ANTHROPIC_API_KEY=sk-ant-...
49
51
  pi
50
52
  ```
51
53
 
52
- | Provider | Environment Variable |
53
- |----------|---------------------|
54
- | Anthropic | `ANTHROPIC_API_KEY` |
55
- | OpenAI | `OPENAI_API_KEY` |
56
- | Google Gemini | `GEMINI_API_KEY` |
57
- | Mistral | `MISTRAL_API_KEY` |
58
- | Groq | `GROQ_API_KEY` |
59
- | Cerebras | `CEREBRAS_API_KEY` |
60
- | xAI | `XAI_API_KEY` |
61
- | OpenRouter | `OPENROUTER_API_KEY` |
62
- | Vercel AI Gateway | `AI_GATEWAY_API_KEY` |
63
- | ZAI | `ZAI_API_KEY` |
64
- | OpenCode Zen | `OPENCODE_API_KEY` |
65
- | Hugging Face | `HF_TOKEN` |
66
- | Kimi For Coding | `KIMI_API_KEY` |
67
- | MiniMax | `MINIMAX_API_KEY` |
68
- | MiniMax (China) | `MINIMAX_CN_API_KEY` |
69
-
70
- ## Auth File
54
+ | Provider | Environment Variable | `auth.json` key |
55
+ |----------|----------------------|------------------|
56
+ | Anthropic | `ANTHROPIC_API_KEY` | `anthropic` |
57
+ | Azure OpenAI Responses | `AZURE_OPENAI_API_KEY` | `azure-openai-responses` |
58
+ | OpenAI | `OPENAI_API_KEY` | `openai` |
59
+ | Google Gemini | `GEMINI_API_KEY` | `google` |
60
+ | Mistral | `MISTRAL_API_KEY` | `mistral` |
61
+ | Groq | `GROQ_API_KEY` | `groq` |
62
+ | Cerebras | `CEREBRAS_API_KEY` | `cerebras` |
63
+ | xAI | `XAI_API_KEY` | `xai` |
64
+ | OpenRouter | `OPENROUTER_API_KEY` | `openrouter` |
65
+ | Vercel AI Gateway | `AI_GATEWAY_API_KEY` | `vercel-ai-gateway` |
66
+ | ZAI | `ZAI_API_KEY` | `zai` |
67
+ | OpenCode Zen | `OPENCODE_API_KEY` | `opencode` |
68
+ | Hugging Face | `HF_TOKEN` | `huggingface` |
69
+ | Kimi For Coding | `KIMI_API_KEY` | `kimi-coding` |
70
+ | MiniMax | `MINIMAX_API_KEY` | `minimax` |
71
+ | MiniMax (China) | `MINIMAX_CN_API_KEY` | `minimax-cn` |
72
+
73
+ Reference for environment variables and `auth.json` keys: [`const envMap`](https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/env-api-keys.ts) in [`packages/ai/src/env-api-keys.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/env-api-keys.ts).
74
+
75
+ #### Auth File
71
76
 
72
77
  Store credentials in `~/.pi/agent/auth.json`:
73
78
 
package/docs/rpc.md CHANGED
@@ -12,7 +12,7 @@ pi --mode rpc [options]
12
12
 
13
13
  Common options:
14
14
  - `--provider <name>`: Set the LLM provider (anthropic, openai, google, etc.)
15
- - `--model <id>`: Set the model ID
15
+ - `--model <pattern>`: Model pattern or ID (supports `provider/id` and optional `:<thinking>`)
16
16
  - `--no-session`: Disable session persistence
17
17
  - `--session-dir <path>`: Custom session storage directory
18
18
 
@@ -928,11 +928,17 @@ There are two categories of extension UI methods:
928
928
 
929
929
  If a dialog method includes a `timeout` field, the agent-side will auto-resolve with a default value when the timeout expires. The client does not need to track timeouts.
930
930
 
931
- Some `ExtensionUIContext` methods are not supported in RPC mode because they require direct TUI access:
931
+ Some `ExtensionUIContext` methods are not supported or degraded in RPC mode because they require direct TUI access:
932
932
  - `custom()` returns `undefined`
933
933
  - `setWorkingMessage()`, `setFooter()`, `setHeader()`, `setEditorComponent()`, `setToolsExpanded()` are no-ops
934
934
  - `getEditorText()` returns `""`
935
935
  - `getToolsExpanded()` returns `false`
936
+ - `pasteToEditor()` delegates to `setEditorText()` (no paste/collapse handling)
937
+ - `getAllThemes()` returns `[]`
938
+ - `getTheme()` returns `undefined`
939
+ - `setTheme()` returns `{ success: false, error: "..." }`
940
+
941
+ Note: `ctx.hasUI` is `true` in RPC mode because the dialog and fire-and-forget methods are functional via the extension UI sub-protocol.
936
942
 
937
943
  ### Extension UI Requests (stdout)
938
944
 
@@ -66,6 +66,7 @@ cp permission-gate.ts ~/.pi/agent/extensions/
66
66
  | `overlay-qa-tests.ts` | Comprehensive overlay QA tests: anchors, margins, stacking, overflow, animation |
67
67
  | `doom-overlay/` | DOOM game running as an overlay at 35 FPS (demonstrates real-time game rendering) |
68
68
  | `shutdown-command.ts` | Adds `/quit` command demonstrating `ctx.shutdown()` |
69
+ | `reload-runtime.ts` | Adds `/reload-runtime` and `reload_runtime` tool showing safe reload flow |
69
70
  | `interactive-shell.ts` | Run interactive commands (vim, htop) with full terminal via `user_bash` hook |
70
71
  | `inline-bash.ts` | Expands `!{command}` patterns in prompts via `input` event transformation |
71
72
 
@@ -13,7 +13,7 @@
13
13
  * pi --extension examples/extensions/custom-compaction.ts
14
14
  */
15
15
 
16
- import { complete, getModel } from "@mariozechner/pi-ai";
16
+ import { complete } from "@mariozechner/pi-ai";
17
17
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
18
18
  import { convertToLlm, serializeConversation } from "@mariozechner/pi-coding-agent";
19
19
 
@@ -25,7 +25,7 @@ export default function (pi: ExtensionAPI) {
25
25
  const { messagesToSummarize, turnPrefixMessages, tokensBefore, firstKeptEntryId, previousSummary } = preparation;
26
26
 
27
27
  // Use Gemini Flash for summarization (cheaper/faster than most conversation models)
28
- const model = getModel("google", "gemini-2.5-flash");
28
+ const model = ctx.modelRegistry.find("google", "gemini-2.5-flash");
29
29
  if (!model) {
30
30
  ctx.ui.notify(`Could not find Gemini Flash model, using default compaction`, "warning");
31
31
  return;
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pi-extension-custom-provider",
3
- "version": "1.3.8",
3
+ "version": "1.3.10",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pi-extension-custom-provider",
9
- "version": "1.3.8",
9
+ "version": "1.3.10",
10
10
  "dependencies": {
11
11
  "@anthropic-ai/sdk": "^0.52.0"
12
12
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-extension-custom-provider-anthropic",
3
3
  "private": true,
4
- "version": "1.3.8",
4
+ "version": "1.3.10",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "clean": "echo 'nothing to clean'",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-extension-custom-provider-gitlab-duo",
3
3
  "private": true,
4
- "version": "1.3.8",
4
+ "version": "1.3.10",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "clean": "echo 'nothing to clean'",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-extension-custom-provider-qwen-cli",
3
3
  "private": true,
4
- "version": "1.2.8",
4
+ "version": "1.2.10",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "clean": "echo 'nothing to clean'",
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Reload Runtime Extension
3
+ *
4
+ * Demonstrates ctx.reload() from ExtensionCommandContext and an LLM-callable
5
+ * tool that queues a follow-up command to trigger reload.
6
+ */
7
+
8
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
9
+ import { Type } from "@sinclair/typebox";
10
+
11
+ export default function (pi: ExtensionAPI) {
12
+ // Command entrypoint for reload.
13
+ // Treat reload as terminal for this handler.
14
+ pi.registerCommand("reload-runtime", {
15
+ description: "Reload extensions, skills, prompts, and themes",
16
+ handler: async (_args, ctx) => {
17
+ await ctx.reload();
18
+ return;
19
+ },
20
+ });
21
+
22
+ // LLM-callable tool. Tools get ExtensionContext, so they cannot call ctx.reload() directly.
23
+ // Instead, queue a follow-up user command that executes the command above.
24
+ pi.registerTool({
25
+ name: "reload_runtime",
26
+ label: "Reload Runtime",
27
+ description: "Reload extensions, skills, prompts, and themes",
28
+ parameters: Type.Object({}),
29
+ async execute() {
30
+ pi.sendUserMessage("/reload-runtime", { deliverAs: "followUp" });
31
+ return {
32
+ content: [{ type: "text", text: "Queued /reload-runtime as a follow-up command." }],
33
+ details: {},
34
+ };
35
+ },
36
+ });
37
+ }
@@ -231,13 +231,14 @@ async function runSingleAgent(
231
231
  const agent = agents.find((a) => a.name === agentName);
232
232
 
233
233
  if (!agent) {
234
+ const available = agents.map((a) => `"${a.name}"`).join(", ") || "none";
234
235
  return {
235
236
  agent: agentName,
236
237
  agentSource: "unknown",
237
238
  task,
238
239
  exitCode: 1,
239
240
  messages: [],
240
- stderr: `Unknown agent: ${agentName}`,
241
+ stderr: `Unknown agent: "${agentName}". Available agents: ${available}.`,
241
242
  usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
242
243
  step,
243
244
  };
@@ -24,7 +24,7 @@ export default function (pi: ExtensionAPI) {
24
24
 
25
25
  pi.on("turn_end", (_event, ctx) => {
26
26
  const usage = ctx.getContextUsage();
27
- if (!usage || usage.tokens <= COMPACT_THRESHOLD_TOKENS) {
27
+ if (!usage || usage.tokens === null || usage.tokens <= COMPACT_THRESHOLD_TOKENS) {
28
28
  return;
29
29
  }
30
30
  triggerCompaction(ctx);
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pi-extension-with-deps",
3
- "version": "1.16.8",
3
+ "version": "1.16.10",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pi-extension-with-deps",
9
- "version": "1.16.8",
9
+ "version": "1.16.10",
10
10
  "dependencies": {
11
11
  "ms": "^2.1.3"
12
12
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-extension-with-deps",
3
3
  "private": true,
4
- "version": "1.16.8",
4
+ "version": "1.16.10",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "clean": "echo 'nothing to clean'",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mariozechner/pi-coding-agent",
3
- "version": "0.52.8",
3
+ "version": "0.52.10",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "piConfig": {
@@ -40,9 +40,9 @@
40
40
  },
41
41
  "dependencies": {
42
42
  "@mariozechner/jiti": "^2.6.2",
43
- "@mariozechner/pi-agent-core": "^0.52.8",
44
- "@mariozechner/pi-ai": "^0.52.8",
45
- "@mariozechner/pi-tui": "^0.52.8",
43
+ "@mariozechner/pi-agent-core": "^0.52.10",
44
+ "@mariozechner/pi-ai": "^0.52.10",
45
+ "@mariozechner/pi-tui": "^0.52.10",
46
46
  "@silvia-odwyer/photon-node": "^0.3.4",
47
47
  "chalk": "^5.5.0",
48
48
  "cli-highlight": "^2.1.11",