From 739e679d1ae9112fdd6e4d046df3895eacaafa91 Mon Sep 17 00:00:00 2001 From: Michal Date: Sat, 8 Aug 2026 17:30:42 +0100 Subject: [PATCH] fix(pi): repair the /mcpctl menu, skills target, and typecheck the extension MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pi extension shipped in `src/pi-ext/` was covered by no tsconfig and no eslint config, so nothing ever checked it against pi's API. Pointing tsc at the published @earendil-works/pi-coding-agent types found the command surface to be inert. Fixes: - `/mcpctl` did nothing. `ctx.ui.select` takes `string[]` and returns the chosen string; it was called with `{value,label}` objects, so the menu rendered five `[object Object]` rows and `choice === "status"` never matched any branch. Labels are now plain strings mapped back to actions. - The headless branch returned a status string from a handler typed `Promise`; pi drops it. Reports via notify instead. - "Sync skills" omitted `--agent pi`, writing into ~/.claude/skills — in an integration whose stated purpose is to not depend on ~/.claude — and said so in its own success message. It also ran execSync with `stdio: "inherit"`, painting raw output over pi's TUI, and interpolated the project name into a shell string. Now execFile with `--agent pi` and captured output. - Tool results typed `content[].type` as `string`; pi's AgentToolResult wants the `"text"` literal. - `callTool` asserted `Promise` to `ToolCallResult`. - Sanitising MCP tool names to `[a-z0-9_]` can collide (`docs.search` vs `docs-search`). The colliding tool was silently never registered but still reported active, so its calls were forwarded to the first tool. Names are now disambiguated and tracked with the MCP tool they forward to. - `registerWithPi` rewrote settings.json even when nothing changed. Since parsing strips `//` comments, a no-op run destroyed them. Guards, so this class of bug can't return: - `src/pi-ext/tsconfig.json` checks the extension against the real published pi types (dev dependency, not a shim — a shim drifting from the published API is the exact failure being guarded). Wired into `pnpm typecheck`. - eslint now covers `src/pi-ext/*.ts` like every other source file. - A test fails if the embedded copy in `config/pi-extension.ts` is stale; editing the sources without regenerating silently shipped old code. Also: the branch added `config pi` without regenerating shell completions (the committed-completions test was failing), and the doc advertised `mcpctl pi sync-skills`, which does not exist. Both corrected, plus a note on the session-token vs `mcpctl_pat_` bearer difference that would bite against an authenticated `mcplocal serve`. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_017BMXdb2qZbPSh8Q7XpTyjB --- completions/mcpctl.bash | 5 +- completions/mcpctl.fish | 18 +- docs/pi-extension.md | 56 +- eslint.config.js | 2 +- package.json | 8 +- pnpm-lock.yaml | 1091 ++++++++++++++++- src/cli/src/config/pi-extension.ts | 4 +- src/cli/src/utils/pi-settings.ts | 7 +- .../tests/config/pi-extension-embed.test.ts | 45 + src/pi-ext/mcp-http.ts | 3 +- src/pi-ext/mcpctl-pi.ts | 98 +- src/pi-ext/tsconfig.json | 26 + 12 files changed, 1279 insertions(+), 84 deletions(-) create mode 100644 src/cli/tests/config/pi-extension-embed.test.ts create mode 100644 src/pi-ext/tsconfig.json diff --git a/completions/mcpctl.bash b/completions/mcpctl.bash index 0e2be01..8dbfce5 100644 --- a/completions/mcpctl.bash +++ b/completions/mcpctl.bash @@ -103,7 +103,7 @@ _mcpctl() { config) local config_sub=$(_mcpctl_get_subcmd $subcmd_pos) if [[ -z "$config_sub" ]]; then - COMPREPLY=($(compgen -W "view set path reset claude claude-generate prime-agent prime-agent-generate setup impersonate help" -- "$cur")) + COMPREPLY=($(compgen -W "view set path reset claude claude-generate pi prime-agent prime-agent-generate setup impersonate help" -- "$cur")) else case "$config_sub" in view) @@ -124,6 +124,9 @@ _mcpctl() { claude-generate) COMPREPLY=($(compgen -W "-p --project -o --output --inspect --stdout --skip-skills -h --help" -- "$cur")) ;; + pi) + COMPREPLY=($(compgen -W "-p --project --extension-dir --skip-skills --settings --pi-dir -h --help" -- "$cur")) + ;; prime-agent) COMPREPLY=($(compgen -W "-p --project -o --output --gateway-url --token --skip-skills --skip-extension --skip-marker --dry-run -h --help" -- "$cur")) ;; diff --git a/completions/mcpctl.fish b/completions/mcpctl.fish index 325719f..b4b8da1 100644 --- a/completions/mcpctl.fish +++ b/completions/mcpctl.fish @@ -239,7 +239,7 @@ complete -c mcpctl -n "not __mcpctl_has_project; and not __fish_seen_subcommand_ complete -c mcpctl -n "not __mcpctl_has_project; and not __fish_seen_subcommand_from $commands" -a backup -d 'Git-based backup status and management' complete -c mcpctl -n "not __mcpctl_has_project; and not __fish_seen_subcommand_from $commands" -a approve -d 'Approve a pending prompt request (atomic: delete request, create prompt)' complete -c mcpctl -n "not __mcpctl_has_project; and not __fish_seen_subcommand_from $commands" -a review -d 'Triage proposed prompts and skills' -complete -c mcpctl -n "not __mcpctl_has_project; and not __fish_seen_subcommand_from $commands" -a skills -d 'Sync skill bundles synced from mcpd (Claude Code by default; prime-agent with --agent prime-agent)' +complete -c mcpctl -n "not __mcpctl_has_project; and not __fish_seen_subcommand_from $commands" -a skills -d 'Sync skill bundles synced from mcpd (Claude Code by default; others with --agent)' complete -c mcpctl -n "not __mcpctl_has_project; and not __fish_seen_subcommand_from $commands" -a console -d 'Interactive MCP console — unified timeline with tools, provenance, and lab replay' complete -c mcpctl -n "not __mcpctl_has_project; and not __fish_seen_subcommand_from $commands" -a cache -d 'Manage ProxyModel pipeline cache' complete -c mcpctl -n "not __mcpctl_has_project; and not __fish_seen_subcommand_from $commands" -a provider -d 'Control local LLM providers (start/stop/status)' @@ -267,13 +267,14 @@ complete -c mcpctl -n "__fish_seen_subcommand_from approve; and __mcpctl_needs_r complete -c mcpctl -n "__fish_seen_subcommand_from get describe delete edit patch approve; and not __mcpctl_needs_resource_type" -a '(__mcpctl_resource_names)' -d 'Resource name' # config subcommands -set -l config_cmds view set path reset claude claude-generate prime-agent prime-agent-generate setup impersonate +set -l config_cmds view set path reset claude claude-generate pi prime-agent prime-agent-generate setup impersonate complete -c mcpctl -n "__fish_seen_subcommand_from config; and not __fish_seen_subcommand_from $config_cmds" -a view -d 'Show current configuration' complete -c mcpctl -n "__fish_seen_subcommand_from config; and not __fish_seen_subcommand_from $config_cmds" -a set -d 'Set a configuration value' complete -c mcpctl -n "__fish_seen_subcommand_from config; and not __fish_seen_subcommand_from $config_cmds" -a path -d 'Show configuration file path' complete -c mcpctl -n "__fish_seen_subcommand_from config; and not __fish_seen_subcommand_from $config_cmds" -a reset -d 'Reset configuration to defaults' complete -c mcpctl -n "__fish_seen_subcommand_from config; and not __fish_seen_subcommand_from $config_cmds" -a claude -d 'Generate .mcp.json + wire skills sync + install SessionStart hook' complete -c mcpctl -n "__fish_seen_subcommand_from config; and not __fish_seen_subcommand_from $config_cmds" -a claude-generate -d '' +complete -c mcpctl -n "__fish_seen_subcommand_from config; and not __fish_seen_subcommand_from $config_cmds" -a pi -d 'Install the pi extension + sync skills (native, no MCP client, no Claude)' complete -c mcpctl -n "__fish_seen_subcommand_from config; and not __fish_seen_subcommand_from $config_cmds" -a prime-agent -d 'Register mcpctl proxy MCP + auth + skills + /mcpctl switcher for prime-agent (~/.prime/agent)' complete -c mcpctl -n "__fish_seen_subcommand_from config; and not __fish_seen_subcommand_from $config_cmds" -a prime-agent-generate -d '' complete -c mcpctl -n "__fish_seen_subcommand_from config; and not __fish_seen_subcommand_from $config_cmds" -a setup -d 'Interactive LLM provider setup wizard' @@ -296,6 +297,13 @@ complete -c mcpctl -n "__mcpctl_subcmd_active config claude-generate" -l inspect complete -c mcpctl -n "__mcpctl_subcmd_active config claude-generate" -l stdout -d 'Print to stdout instead of writing a file' complete -c mcpctl -n "__mcpctl_subcmd_active config claude-generate" -l skip-skills -d 'Skip the skills sync + SessionStart hook install step (PR-5+)' +# config pi options +complete -c mcpctl -n "__mcpctl_subcmd_active config pi" -s p -l project -d 'Project name to make active' -xa '(__mcpctl_project_names)' +complete -c mcpctl -n "__mcpctl_subcmd_active config pi" -l extension-dir -d 'Source-tree src/pi-ext dir (dev only; default: embedded sources)' -x +complete -c mcpctl -n "__mcpctl_subcmd_active config pi" -l skip-skills -d 'Skip the initial skills sync' +complete -c mcpctl -n "__mcpctl_subcmd_active config pi" -l settings -d 'pi settings.json path (default: ~/.pi/agent/settings.json)' -x +complete -c mcpctl -n "__mcpctl_subcmd_active config pi" -l pi-dir -d 'Override the pi agent home (default: ~/.pi/agent)' -x + # config prime-agent options complete -c mcpctl -n "__mcpctl_subcmd_active config prime-agent" -s p -l project -d 'Project name' -xa '(__mcpctl_project_names)' complete -c mcpctl -n "__mcpctl_subcmd_active config prime-agent" -s o -l output -d 'prime-agent settings.json path (default: ~/.prime/agent/settings.json)' -x @@ -517,14 +525,14 @@ complete -c mcpctl -n "__mcpctl_subcmd_active review reject" -l reason -d 'Revie # skills subcommands set -l skills_cmds sync -complete -c mcpctl -n "__fish_seen_subcommand_from skills; and not __fish_seen_subcommand_from $skills_cmds" -a sync -d 'Sync skills from mcpd onto disk (~/.claude/skills/ or ~/.prime/agent/skills/)' +complete -c mcpctl -n "__fish_seen_subcommand_from skills; and not __fish_seen_subcommand_from $skills_cmds" -a sync -d 'Sync skills from mcpd onto disk (~/.claude, ~/.prime, or ~/.pi agent skill roots)' # skills sync options complete -c mcpctl -n "__mcpctl_subcmd_active skills sync" -s p -l project -d 'Project to sync (overrides .mcpctl-project marker)' -xa '(__mcpctl_project_names)' -complete -c mcpctl -n "__mcpctl_subcmd_active skills sync" -l agent -d 'Sync target: claude (default) or prime-agent' -x +complete -c mcpctl -n "__mcpctl_subcmd_active skills sync" -l agent -d 'Sync target: claude (default), prime-agent, or pi' -x complete -c mcpctl -n "__mcpctl_subcmd_active skills sync" -l dry-run -d 'Print what would change without writing anything' complete -c mcpctl -n "__mcpctl_subcmd_active skills sync" -l force -d 'Overwrite locally-modified skills' -complete -c mcpctl -n "__mcpctl_subcmd_active skills sync" -l quiet -d 'Suppress all output unless something changed (used by SessionStart hook)' +complete -c mcpctl -n "__mcpctl_subcmd_active skills sync" -l quiet -d 'Suppress all output unless something changed (used by session-start hooks)' complete -c mcpctl -n "__mcpctl_subcmd_active skills sync" -l skip-postinstall -d 'Do not run metadata.postInstall scripts (no-op in v1; reserved)' complete -c mcpctl -n "__mcpctl_subcmd_active skills sync" -l keep-orphans -d 'Do not remove skills that are no longer in the server set' diff --git a/docs/pi-extension.md b/docs/pi-extension.md index 3ca45b3..fcf98fc 100644 --- a/docs/pi-extension.md +++ b/docs/pi-extension.md @@ -81,8 +81,17 @@ tools are in the active set at any time. Skills already sync in the Agent Skills `SKILL.md` format. `mcpctl config pi` (when given a project) writes them under pi's own directory `~/.pi/agent/skills/` and registers that directory in `~/.pi/agent/settings.json` -— no Claude involvement. `mcpctl pi sync-skills` (a thin wrapper) re-runs the -existing `skills` code path against the pi target directory. +— no Claude involvement. Re-sync later with: + +```bash +mcpctl skills sync --agent pi --project +``` + +`--agent pi` is what keeps skills out of `~/.claude/skills`; the default target +is still Claude Code. The `/mcpctl` menu's **sync skills** action runs exactly +this command. Sync state is tracked separately per agent +(`~/.mcpctl/skills-state-pi.json`), so pi and Claude never fight over the same +bookkeeping. ## Layout @@ -95,3 +104,46 @@ src/pi-ext/ The extension imports only from pi-bundled packages (`@earendil-works/pi-coding-agent`, `@earendil-works/pi-ai`, `typebox`), so it loads standalone. + +## Typechecking + +The extension ships as *source*: it is embedded into the CLI +(`src/cli/src/config/pi-extension.ts`, generated by +`scripts/generate-pi-extension.ts`) and written verbatim into +`~/.pi/agent/extensions/mcpctl/`. The CLI's own build never compiles it, so +without a dedicated project nothing would check it against pi's API — which is +how a `ctx.ui.select()` call with the wrong option shape shipped in the first +place. + +`src/pi-ext/tsconfig.json` closes that gap, checking against the **real** +published `@earendil-works/pi-coding-agent` types (a dev dependency, not a +hand-written shim — a shim that drifts from the published API is the exact +failure this guards against): + +```bash +pnpm run typecheck # includes typecheck:pi-ext +``` + +After editing `src/pi-ext/*.ts`, regenerate the embedded copy or the CLI will +keep installing the old sources: + +```bash +npx tsx scripts/generate-pi-extension.ts +``` + +A test (`tests/config/pi-extension-embed.test.ts`) fails if you forget. + +## Authentication caveat + +The extension sends the mcpd session token from `~/.mcpctl/credentials` as its +bearer. A locally running `mcpctl-local` daemon does not authenticate +`/projects/*`, so this works — the header is simply ignored. + +`mcplocal serve` is different: it registers a token-auth preHandler that accepts +**only** `mcpctl_pat_` bearers (project mcptokens), and rejects a session token +with `401 Only mcpctl_pat_ bearers are accepted on this endpoint`. Pointing the +extension at an authenticated `mcplocal serve` therefore needs a project +mcptoken (`mcpctl create mcptoken --project `, printed once). +Wiring that token through to the extension is not yet implemented — the +`config prime-agent` path does the equivalent by storing it in +`~/.prime/agent/auth.json`. diff --git a/eslint.config.js b/eslint.config.js index d57ac82..53f1093 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -3,7 +3,7 @@ import tsparser from '@typescript-eslint/parser'; export default [ { - files: ['src/*/src/**/*.ts'], + files: ['src/*/src/**/*.ts', 'src/pi-ext/*.ts'], languageOptions: { parser: tsparser, parserOptions: { diff --git a/package.json b/package.json index a238b35..5658419 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "clean": "pnpm -r run clean && rimraf node_modules", "db:up": "docker compose -f deploy/docker-compose.yml up -d", "db:down": "docker compose -f deploy/docker-compose.yml down", - "typecheck": "tsc --build", + "typecheck": "tsc --build && pnpm run typecheck:pi-ext", "completions:generate": "tsx scripts/generate-completions.ts --write", "completions:check": "tsx scripts/generate-completions.ts --check", "rpm:build": "bash scripts/build-rpm.sh", @@ -32,7 +32,8 @@ "mcpd:build": "bash scripts/build-mcpd.sh", "mcpd:deploy": "bash deploy.sh", "mcpd:deploy-dry": "bash deploy.sh --dry-run", - "mcpd:logs": "bash logs.sh" + "mcpd:logs": "bash logs.sh", + "typecheck:pi-ext": "tsc -p src/pi-ext/tsconfig.json" }, "engines": { "node": ">=20.0.0", @@ -40,6 +41,8 @@ }, "packageManager": "pnpm@9.15.0", "devDependencies": { + "@earendil-works/pi-ai": "^0.84.1", + "@earendil-works/pi-coding-agent": "0.84.1", "@types/node": "^25.3.0", "@typescript-eslint/eslint-plugin": "^8.56.0", "@typescript-eslint/parser": "^8.56.0", @@ -48,6 +51,7 @@ "eslint-config-prettier": "^10.1.8", "rimraf": "^6.1.3", "tsx": "^4.21.0", + "typebox": "1.3.11", "typescript": "^5.9.3", "vitest": "^4.0.18" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 10ef364..0a6c99d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,36 +8,45 @@ importers: .: devDependencies: + '@earendil-works/pi-ai': + specifier: ^0.84.1 + version: 0.84.1(@modelcontextprotocol/sdk@1.26.0(zod@3.25.76))(ws@8.19.0)(zod@3.25.76) + '@earendil-works/pi-coding-agent': + specifier: 0.84.1 + version: 0.84.1(@modelcontextprotocol/sdk@1.26.0(zod@3.25.76))(ws@8.19.0)(zod@3.25.76) '@types/node': specifier: ^25.3.0 version: 25.3.0 '@typescript-eslint/eslint-plugin': specifier: ^8.56.0 - version: 8.56.0(@typescript-eslint/parser@8.56.0(eslint@10.0.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.1(jiti@2.6.1))(typescript@5.9.3) + version: 8.56.0(@typescript-eslint/parser@8.56.0(eslint@10.0.1(jiti@2.7.0))(typescript@5.9.3))(eslint@10.0.1(jiti@2.7.0))(typescript@5.9.3) '@typescript-eslint/parser': specifier: ^8.56.0 - version: 8.56.0(eslint@10.0.1(jiti@2.6.1))(typescript@5.9.3) + version: 8.56.0(eslint@10.0.1(jiti@2.7.0))(typescript@5.9.3) '@vitest/coverage-v8': specifier: ^4.0.18 - version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.1)(@types/node@25.3.0)(jiti@2.6.1)(jsdom@28.1.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.0)(jiti@2.7.0)(jsdom@28.1.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.9.0)) eslint: specifier: ^10.0.1 - version: 10.0.1(jiti@2.6.1) + version: 10.0.1(jiti@2.7.0) eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@10.0.1(jiti@2.6.1)) + version: 10.1.8(eslint@10.0.1(jiti@2.7.0)) rimraf: specifier: ^6.1.3 version: 6.1.3 tsx: specifier: ^4.21.0 version: 4.21.0 + typebox: + specifier: 1.3.11 + version: 1.3.11 typescript: specifier: ^5.9.3 version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.1)(@types/node@25.3.0)(jiti@2.6.1)(jsdom@28.1.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.2) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.0)(jiti@2.7.0)(jsdom@28.1.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.9.0) src/cli: dependencies: @@ -234,7 +243,7 @@ importers: devDependencies: '@tailwindcss/vite': specifier: ^4.1.16 - version: 4.2.4(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.2.4(vite@7.3.1(@types/node@25.3.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.9.0)) '@testing-library/jest-dom': specifier: ^6.7.0 version: 6.9.1 @@ -252,7 +261,7 @@ importers: version: 19.2.3(@types/react@19.2.14) '@vitejs/plugin-react': specifier: ^5.1.0 - version: 5.2.0(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.2)) + version: 5.2.0(vite@7.3.1(@types/node@25.3.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.9.0)) jsdom: specifier: ^28.0.0 version: 28.1.0 @@ -261,7 +270,7 @@ importers: version: 4.2.4 vite: specifier: ^7.2.0 - version: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.2) + version: 7.3.1(@types/node@25.3.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.9.0) packages: @@ -275,6 +284,15 @@ packages: resolution: {integrity: sha512-3NX/MpTdroi0aKz134A6RC2Gb2iXVECN4QaAXnvCIxxIm3C3AVB1mkUe8NaaiyvOpDfsrqWhYtj+Q6a62RrTsw==} engines: {node: '>=18'} + '@anthropic-ai/sdk@0.91.1': + resolution: {integrity: sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==} + hasBin: true + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + zod: + optional: true + '@asamuzakjp/css-color@5.1.11': resolution: {integrity: sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} @@ -289,6 +307,103 @@ packages: '@asamuzakjp/nwsapi@2.3.9': resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + '@aws-crypto/sha256-browser@5.2.0': + resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} + + '@aws-crypto/sha256-js@5.2.0': + resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/supports-web-crypto@5.2.0': + resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} + + '@aws-crypto/util@5.2.0': + resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} + + '@aws-sdk/client-bedrock-runtime@3.1048.0': + resolution: {integrity: sha512-u+NT61JZEkRFtpL0CAw1N1dwxnaLgwVXQl/zjJxTGgLyS/jTIdg2SdoEoCTHxgDyCnqa1HEi9QOoE9/pYRNpOQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/core@3.977.6': + resolution: {integrity: sha512-QiaJV4/zDrB4ZY2mfeSXSzSTc36W16sZXcGz+SPFk0CJ26gziO0cS+4LjJUMAbdeeBOvS0k0Aq1cZpfGdUXxSw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-env@3.972.67': + resolution: {integrity: sha512-rcIpk5kxUqDaaNa6Xk23pQ6ViY7jlqzmfFWCahQcBT97ddXaXYYwzCen9Tz1Jvo6aJft6wDl5bN44/Jw5B4oLA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-http@3.972.69': + resolution: {integrity: sha512-nggwJtZ4eeNsUw5IeWBMXsi1ryct5idi0K+/SCRF3kybLubOMaNTb3XCihXpWMiVpyzyPeIrl0zTkzhBH9porA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-ini@3.973.12': + resolution: {integrity: sha512-pNEf/OeyN5X3VmLKlgSO6TqaWmW10CvI3TfwL1XhsuhYjSLT2VDaxFnCPHnOeQXSaFisMX4jNhpETriqN8DOmg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-login@3.972.74': + resolution: {integrity: sha512-0AQfDcf99TNmqVKv0owHrw/TQs6i4ZE5t9qmz6NvO53bE/sA/tpXhXL9AAcEP1qHc6Zzjd1UMb69+/9zdhvY3g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-node@3.972.78': + resolution: {integrity: sha512-OgPAnfvbGAMWac6yvxJ1ihslrvDpPVwR68D2csospdNCCyPvHk9JLzYKwz48SNiS1T2znDwHauywRKRFfpyYng==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-process@3.972.67': + resolution: {integrity: sha512-IlUEejorGTWKb4/Dm7K5Yw4QxUmXLThLhrvBmzVBqZFTbW72cv9LTcITmo1dsnYriALE4h68mOq4LB99x6sQ7Q==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-sso@3.973.11': + resolution: {integrity: sha512-gAQBkBZxUB84d71+pPcI9L+jh2ujhuAVxc/4FgGiWFDjkPBlMKxzd5XDtkSXTFX8Ro7ansnT88+XadasxMeCRw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.972.73': + resolution: {integrity: sha512-SnlEmQa6SjOgs6iOPLUQl1Eyq4AKiAdPQlkOhFhqNfDtDCwibMGvL6QlkSmf3o6vAUSImzdPCxowT5dfQUZP1A==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/eventstream-handler-node@3.972.31': + resolution: {integrity: sha512-/BRzvkp46mF6eXBL/l9WKPQQfifLlUPaWli6n9/T/WDLUg8he7TCyuNFnk6RvHP5j5W/kMj5Gxw7W778LJaXDA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-eventstream@3.972.26': + resolution: {integrity: sha512-2eIvouTZoxPu5ClHY6ij13De1yhY8Rmllt0dlGeBNXX3wmR7fU1pvMCGb50fKm1GxcuntWK0t1T0cMjTyDoUQA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-websocket@3.972.49': + resolution: {integrity: sha512-wGYJvu1/stdWuzGcNyh44u8aY7ArU8XFrMesBlzIYn70HWVCRBNEngQexDuPIMu0NEgsKGKmTc0uvnS/bF7KWw==} + engines: {node: '>= 14.0.0'} + + '@aws-sdk/nested-clients@3.997.41': + resolution: {integrity: sha512-RDHqPGQWlF6tatA/Tp3rg6oIwtgN9IVderxE+9av2Y93Dfyu+mO1hZ5Bu2jpfZg2rwdNbsssnwM+sLafIczMlQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.996.43': + resolution: {integrity: sha512-lKekx8bLBXSv4O+cslk9Zfnw2XKSkWBs3uWL5QGhH2ZAQfNS7FE0vcSSN2vD/AhxX54ZTywWxR4STThoeOXlBA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1048.0': + resolution: {integrity: sha512-k0y/GcuesuSfWyUM0WamrGyeZmltRYaPbHO82UDA6mZ/doB+FOHKutikPAtSXMn/hDz970cF+iRuuiYO9VEbAA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1103.0': + resolution: {integrity: sha512-N4wy26MNn31ItGVHYHPrEuCIFY4MBBjC+C5v1lJKqIUSA7OZBdhleCY53zCCrXn27hsk7YNOaTuhQu807S4AfQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/types@3.974.2': + resolution: {integrity: sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-locate-window@3.965.8': + resolution: {integrity: sha512-uUbMs1cBZPafD0ohUj6EwNf0fPZ534NvBxHox4hjX+0Rxq5paSYUem7+hi833pYrzrcnBATKIYpR02MDXT5M9g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/xml-builder@3.972.37': + resolution: {integrity: sha512-zKq4HQum8JwDyEuyfuI4bbiAcU0KxP6qy+9PR/IsR92IyE/DaBAikzAS50tjxip4bqIIANpCcG+Yyj6CVhXupg==} + engines: {node: '>=20.0.0'} + + '@aws/lambda-invoke-store@0.3.0': + resolution: {integrity: sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==} + engines: {node: '>=18.0.0'} + '@babel/code-frame@7.29.0': resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} @@ -423,6 +538,36 @@ packages: resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} engines: {node: '>=20.19.0'} + '@earendil-works/pi-agent-core@0.84.1': + resolution: {integrity: sha512-evyzXYWCLQGmcaBYHlmSku02r8qoN4SGI60GZABo6iV+H+nqX+P9ud8fEZ4GmRq9mUSREvvfX+w9dA9ThF9C6w==} + engines: {node: '>=22.19.0'} + + '@earendil-works/pi-ai@0.84.1': + resolution: {integrity: sha512-wMsAdJMxuNri08vLqTyYVI201DQQezGhPSTkzYsHdw5dYX3rCNwEmSvpaAwhi7ELKI/2tE/CEgSWg/6iRxSgdQ==} + engines: {node: '>=22.19.0'} + hasBin: true + + '@earendil-works/pi-client@0.84.1': + resolution: {integrity: sha512-/V5hGHE4Zq+jG0GtwIB9PyBUOGd6gBLZ7lkQYFKchKnxYHeH3rmWC5xw4kpnZKKBuBuFTdLVbU9vEjlAGMMb2A==} + engines: {node: '>=22.19.0'} + + '@earendil-works/pi-coding-agent@0.84.1': + resolution: {integrity: sha512-ncAqFrG+iybuPGOhMiZoEHkEzTpJgz3guYD32pD+M7ucc0WeHmauP6wa7qwP8V/KWvsZDVNa5XGsdZ7fkC7w7A==} + engines: {node: '>=22.19.0'} + hasBin: true + + '@earendil-works/pi-protocol@0.84.1': + resolution: {integrity: sha512-Ox1pciyeSPGEEUcxvR0/dJcrY7C6hrEGA8y71rOsvSIUlXN1Cbp/be/eoL71OGDBk5O97TeQPfWN6Ju/2Ehjww==} + engines: {node: '>=22.19.0'} + + '@earendil-works/pi-telemetry@0.84.1': + resolution: {integrity: sha512-180/xGJtsq7IoR3p9EKWjRd0e9M4DkxInhlo9xyD7prDC7Qrhqq+nhvwrW0lFjPfXcEI2FSHmGCSyvSJE9GsaQ==} + engines: {node: '>=22.19.0'} + + '@earendil-works/pi-tui@0.84.1': + resolution: {integrity: sha512-udeXFbgEhJ6JiB0uguwNVNkDy2FENfmtQwPcY+/iJ8GWeq18wkal1tKqa5YyeH0IqtX1vG0cGh8zfSYzyzVuLA==} + engines: {node: '>=22.19.0'} + '@emnapi/runtime@1.10.0': resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} @@ -661,6 +806,15 @@ packages: resolution: {integrity: sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA==} engines: {node: ^20.17.0 || >=22.9.0} + '@google/genai@1.52.0': + resolution: {integrity: sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@modelcontextprotocol/sdk': ^1.25.2 + peerDependenciesMeta: + '@modelcontextprotocol/sdk': + optional: true + '@grpc/grpc-js@1.14.3': resolution: {integrity: sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==} engines: {node: '>=12.10.0'} @@ -1031,6 +1185,77 @@ packages: resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} hasBin: true + '@mariozechner/clipboard-darwin-arm64@0.3.9': + resolution: {integrity: sha512-BfgV7vCEWZwJwZJw03r6bP5+tf0iI/ANuQYCxi9RNn7FrWB3yzGuMKCrNLRl6V761vXRdL8+OqZ0wd4TqlsNOQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@mariozechner/clipboard-darwin-universal@0.3.9': + resolution: {integrity: sha512-BGGR4iA9Z2shAjI65eI5xtyb3LYNlDW9X3gxKxDbqtbnREohsrqznov6zpKoIrsRWpzlYVEdKphS7ksJ0/ndSQ==} + engines: {node: '>= 10'} + os: [darwin] + + '@mariozechner/clipboard-darwin-x64@0.3.9': + resolution: {integrity: sha512-4kURmCbS6nt8uYhtmWpUcJWyPHfmAr5dTpXD1nO3pIfa+TSQ9DbrGOYCKH+aEFW47XhQ4Vp8ZTszie+wfFvDKg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@mariozechner/clipboard-linux-arm64-gnu@0.3.9': + resolution: {integrity: sha512-g59OkUGP2DDfCOIKypHeYgv2M55u/cKvXa5dSxFbEJ34XvIQMdcVmpKCkGUro3ZgefXiGVdwguvTMQGpHWzIXw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@mariozechner/clipboard-linux-arm64-musl@0.3.9': + resolution: {integrity: sha512-AGuJdgKsmJdm4Pych7kv3sqe591ERRaAHW3xjLooiFzn8J+PxUyof++7YZrB5Y5tpnTO+K18Og3taj2NpluCRQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@mariozechner/clipboard-linux-riscv64-gnu@0.3.9': + resolution: {integrity: sha512-DXBEAiuMpk7dhS1a9NzNxVAFi1vaKoPu7rQNgY8LIDLGrK3lnIp3nT10DUum+PKVJoJppIP+NAA8IZe4DMNDPw==} + engines: {node: '>= 10'} + cpu: [riscv64] + os: [linux] + + '@mariozechner/clipboard-linux-x64-gnu@0.3.9': + resolution: {integrity: sha512-WORrMLd6EpElEME7JRKfSaY34nW1P5LbdgK5YNCS1ncG2LqmITsSMEJ8nh2mpvxb3TxqbOOKgY7k9eMJYlW9Mw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@mariozechner/clipboard-linux-x64-musl@0.3.9': + resolution: {integrity: sha512-/DHn+1DrfL6oRaPPWXaOKvonFFrni666fxd+zFqiQEfvBH0tsHVWjq9iqBk0oDp0qaPA72lIMy5BptxISBEhZQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@mariozechner/clipboard-win32-arm64-msvc@0.3.9': + resolution: {integrity: sha512-O5FHD3ErkMwMhNzAfu3ggy0ug4z7btZuoQgwwxlzPrwV2bxlD6WDpqBY4NCgICAgZdDKdp+loUEKVAVt8aYnhQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@mariozechner/clipboard-win32-x64-msvc@0.3.9': + resolution: {integrity: sha512-ihQC3EufqEY81vhXBgVBtK4prL+wc62zJsSvxrgz7K1hsdt6OObz6v9p3Rn1OG3GJksTTKMJF0u/guMISHPhSA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@mariozechner/clipboard@0.3.9': + resolution: {integrity: sha512-ABnA53mdfkGZwOFUdZNv2S0CWGO/EIuPj8Vv9xmBFmSYg/qFc7ihO6q5FcQjvoE67kZpWkEc4AhD6B/os04yuA==} + engines: {node: '>= 10'} + + '@mistralai/mistralai@2.2.6': + resolution: {integrity: sha512-W8pX7zHxjJvMIpw8JMxeJEleapXX0Q9NPszdNzqkM3MIEoIGPObdodujj+WHteXEvGfaP/AMwlNyRfEzSY6dQQ==} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@modelcontextprotocol/sdk@1.26.0': resolution: {integrity: sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==} engines: {node: '>=18'} @@ -1164,6 +1389,10 @@ packages: resolution: {integrity: sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w==} engines: {node: '>=8.0.0'} + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + '@opentelemetry/api@1.9.1': resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} engines: {node: '>=8.0.0'} @@ -1489,6 +1718,49 @@ packages: resolution: {integrity: sha512-qv7+G3J2cc6wwFj3yKvXOamzqhMwSk1ogPGmhpS8iXllcPrJaIIBA+4HbttlHVu1pqWTdmaCH/WE7UOC51kdoA==} engines: {node: ^20.17.0 || >=22.9.0} + '@silvia-odwyer/photon-node@0.3.4': + resolution: {integrity: sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==} + + '@smithy/core@3.31.1': + resolution: {integrity: sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.4.16': + resolution: {integrity: sha512-QfuLWAkLzptffFW980AFeHZFdqds2B64rpEd3uJ6lgs3xVn9QegGMUgUcj+4d7dRrAsya3r58ZKpku97WcFb4w==} + engines: {node: '>=18.0.0'} + + '@smithy/fetch-http-handler@5.6.13': + resolution: {integrity: sha512-4fW86pEUOMbrD5nkbyl/tTvPHHWJFbuB2odl6ps9lWfHoXf9HWh3Q/Smh59qH1g7+c/BSZghX6bbUk4gsiMs8A==} + engines: {node: '>=18.0.0'} + + '@smithy/is-array-buffer@2.2.0': + resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} + engines: {node: '>=14.0.0'} + + '@smithy/node-http-handler@4.7.3': + resolution: {integrity: sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA==} + engines: {node: '>=18.0.0'} + + '@smithy/node-http-handler@4.9.13': + resolution: {integrity: sha512-Nmd/Nl35zfYrd+a6OO2cDJb3GPh9bgTjIUhcM+JFfjpp8/osCgboDV5nCT1I01Pv6R13eSKDKLSoVa5ZB6Zsfw==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.6.12': + resolution: {integrity: sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.16.1': + resolution: {integrity: sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-buffer-from@2.2.0': + resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} + engines: {node: '>=14.0.0'} + + '@smithy/util-utf8@2.3.0': + resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} + engines: {node: '>=14.0.0'} + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -1688,6 +1960,9 @@ packages: '@types/react@19.2.14': resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + '@types/retry@0.12.0': + resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} + '@types/semver@7.7.1': resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} @@ -1989,6 +2264,9 @@ packages: bidi-js@1.0.3: resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + bin-links@6.0.2: resolution: {integrity: sha512-frE1t78WOwJ45PKV2cF2tNPjTcs9L1J9s6VkrV59wanRP4GlaomuxYPVma7BwthMg8WnfSory4w5PTE6FZZ81w==} engines: {node: ^20.17.0 || >=22.9.0} @@ -2000,6 +2278,9 @@ packages: resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} + bowser@2.14.1: + resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} + brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} @@ -2010,11 +2291,18 @@ packages: resolution: {integrity: sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==} engines: {node: 20 || >=22} + brace-expansion@5.0.9: + resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} + engines: {node: 20 || >=22} + browserslist@4.28.2: resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -2227,6 +2515,10 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + data-urls@7.0.0: resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} @@ -2287,6 +2579,10 @@ packages: resolution: {integrity: sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==} engines: {node: '>=0.3.1'} + diff@8.0.4: + resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} + engines: {node: '>=0.3.1'} + docker-modem@5.0.6: resolution: {integrity: sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ==} engines: {node: '>= 8.0'} @@ -2312,6 +2608,9 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -2489,6 +2788,9 @@ packages: exsolve@1.0.8: resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + fast-check@3.23.2: resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==} engines: {node: '>=8.0.0'} @@ -2535,6 +2837,10 @@ packages: picomatch: optional: true + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + figures@6.1.0: resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} engines: {node: '>=18'} @@ -2570,6 +2876,10 @@ packages: resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -2605,6 +2915,14 @@ packages: engines: {node: '>=10'} deprecated: This package is no longer supported. + gaxios@7.3.0: + resolution: {integrity: sha512-RB5vLV+vvQeoFPCX4QMK6/hjVkbIamPp1QSUD0CiZcnj12qbpiL+pLbYtgD+oZkWl0tl9z+o2Utp+MpM3QRhBA==} + engines: {node: '>=18'} + + gcp-metadata@8.1.2: + resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==} + engines: {node: '>=18'} + geist@1.7.0: resolution: {integrity: sha512-ZaoiZwkSf0DwwB1ncdLKp+ggAldqxl5L1+SXaNIBGkPAqcu+xjVJLxlf3/S8vLt9UHx1xu5fz3lbzKCj5iOVdQ==} peerDependencies: @@ -2622,6 +2940,10 @@ packages: resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} engines: {node: '>=18'} + get-east-asian-width@1.6.0: + resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} + engines: {node: '>=18'} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -2659,6 +2981,14 @@ packages: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + google-auth-library@10.9.1: + resolution: {integrity: sha512-i1ydyHrqcIxXkWh/uBmVkzCvIuq5yiK2ATndIe5XxKholrG/MTYP9xGYka4sQhrbIAgGjL2B6NOE7rFaiF3fXw==} + engines: {node: '>=18'} + + google-logging-utils@1.1.3: + resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==} + engines: {node: '>=14'} + google-protobuf@3.21.4: resolution: {integrity: sha512-MnG7N936zcKTco4Jd2PX2U96Kf9PxygAPKBug+74LHzmHXmceN16MmRcdgZv+DGef/S9YvQAfRsNCn4cjf9yyQ==} @@ -2669,6 +2999,10 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + grok-mermaid@0.2.2: + resolution: {integrity: sha512-XcJEP5dDC8liHBh52mlLjU18fNvu1ckFsu0QpIG3+APZ270fsj9wxpiA6cOURmbUEuoMVgjbC2+UYgTdCqqgzA==} + engines: {node: '>=18'} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -2692,6 +3026,9 @@ packages: resolution: {integrity: sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw==} engines: {node: '>=16.0.0'} + highlight.js@10.7.3: + resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} + hono@4.12.0: resolution: {integrity: sha512-NekXntS5M94pUfiVZ8oXXK/kkri+5WpX2/Ik+LVsl+uvw+soj4roXIsPqO+XsWrAw20mOzaXOZf3Q7PfB9A/IA==} engines: {node: '>=16.9.0'} @@ -2889,6 +3226,10 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + jose@6.1.3: resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} @@ -2920,6 +3261,9 @@ packages: engines: {node: '>=6'} hasBin: true + json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -2930,6 +3274,10 @@ packages: json-schema-ref-resolver@3.0.0: resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==} + json-schema-to-ts@3.1.1: + resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==} + engines: {node: '>=16'} + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -2965,6 +3313,12 @@ packages: just-diff@6.0.2: resolution: {integrity: sha512-S59eriX5u3/QhMNq3v/gm8Kd0w8OS6Tz2FS1NG4blv+z0MuQcBRJyFWjdovM0Rad4/P4aUPFtnkNjMjyMlMSYA==} + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -3097,6 +3451,11 @@ packages: engines: {node: '>= 18'} hasBin: true + marked@18.0.5: + resolution: {integrity: sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==} + engines: {node: '>= 20'} + hasBin: true + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -3148,6 +3507,10 @@ packages: resolution: {integrity: sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==} engines: {node: 18 || 20 || >=22} + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -3261,6 +3624,11 @@ packages: node-addon-api@5.1.0: resolution: {integrity: sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==} + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + node-fetch-native@1.6.7: resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} @@ -3273,6 +3641,10 @@ packages: encoding: optional: true + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + node-gyp@12.4.0: resolution: {integrity: sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw==} engines: {node: ^20.17.0 || >=22.9.0} @@ -3371,6 +3743,18 @@ packages: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} + openai@6.26.0: + resolution: {integrity: sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + openid-client@6.8.2: resolution: {integrity: sha512-uOvTCndr4udZsKihJ68H9bUICrriHdUVJ6Az+4Ns6cW55rwM5h0bjVIzDz2SxgOI84LKjFyjOFvERLzdTUROGA==} @@ -3390,6 +3774,10 @@ packages: resolution: {integrity: sha512-e8vJF4XdVkzqqSHguEMz41mQO1wKwxKm5ENrUJQUu9kLDCtn83cxbyHZcszr4QC5zEA7WffRRC4gsTecC7J9oA==} engines: {node: '>=18'} + p-retry@4.6.2: + resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} + engines: {node: '>=8'} + package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} @@ -3409,6 +3797,9 @@ packages: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} + partial-json@0.1.7: + resolution: {integrity: sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==} + patch-console@2.0.0: resolution: {integrity: sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -3512,6 +3903,9 @@ packages: promise-call-limit@3.0.2: resolution: {integrity: sha512-mRPQO2T1QQVw11E7+UdCJu7S61eJVWknzml9sC1heAdj1jxl0fWMBypIt9ZOcLFf8FkG995ZD7RnVk7HH72fZw==} + proper-lockfile@4.1.2: + resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + protobufjs@7.5.4: resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} engines: {node: '>=12.0.0'} @@ -3634,6 +4028,14 @@ packages: resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} engines: {node: '>=10'} + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + + retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -3702,6 +4104,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.8.0: + resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==} + engines: {node: '>=10'} + hasBin: true + send@1.2.1: resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} engines: {node: '>= 18'} @@ -3995,6 +4402,9 @@ packages: resolution: {integrity: sha512-gcANaAnd2QDZFmHFEOF4k7uc1J/6a6z3DJMd/QwEyxLoKGiptJRwid582r7QIsFlFMIZ3SnxfS52S4hm2DHkuQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + ts-algebra@2.0.0: + resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} + ts-api-utils@2.4.0: resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} engines: {node: '>=18.12'} @@ -4028,6 +4438,12 @@ packages: resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} engines: {node: '>= 0.6'} + typebox@1.3.11: + resolution: {integrity: sha512-tZGKIS02Opbh4EYMmAEVuXl+y3EQJ9ZlkitLZ1CmFCY4ZOqr/xWR9dDSCFmIjNDICGMWkOqMh6WxGTyRXqcSGQ==} + + typebox@1.3.7: + resolution: {integrity: sha512-meKuifc33Pccx0O6PdIzYMq3Og8zvP4TIi/a+Bw3AEMZMxOD0+RHGQvpglEe6Zdy3wZ8nqn/j95h8LUZLk/6Hg==} + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -4050,6 +4466,10 @@ packages: resolution: {integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==} engines: {node: '>=20.18.1'} + undici@8.9.0: + resolution: {integrity: sha512-aWZpUj7XoGonMClx4gdDRfgBjqeA+F473aDmROQQbM9n6PRfK/u1q/a0X4wMTgcHfT8H6fpbt98PFuDUwFg2YA==} + engines: {node: '>=22.19.0'} + unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} @@ -4167,6 +4587,10 @@ packages: resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} engines: {node: 20 || >=22} + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -4268,6 +4692,11 @@ packages: engines: {node: '>= 14.6'} hasBin: true + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} @@ -4306,6 +4735,12 @@ snapshots: ansi-styles: 6.2.3 is-fullwidth-code-point: 5.1.0 + '@anthropic-ai/sdk@0.91.1(zod@3.25.76)': + dependencies: + json-schema-to-ts: 3.1.1 + optionalDependencies: + zod: 3.25.76 + '@asamuzakjp/css-color@5.1.11': dependencies: '@asamuzakjp/generational-cache': 1.0.1 @@ -4326,6 +4761,220 @@ snapshots: '@asamuzakjp/nwsapi@2.3.9': {} + '@aws-crypto/sha256-browser@5.2.0': + dependencies: + '@aws-crypto/sha256-js': 5.2.0 + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.974.2 + '@aws-sdk/util-locate-window': 3.965.8 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-js@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.974.2 + tslib: 2.8.1 + + '@aws-crypto/supports-web-crypto@5.2.0': + dependencies: + tslib: 2.8.1 + + '@aws-crypto/util@5.2.0': + dependencies: + '@aws-sdk/types': 3.974.2 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-sdk/client-bedrock-runtime@3.1048.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.977.6 + '@aws-sdk/credential-provider-node': 3.972.78 + '@aws-sdk/eventstream-handler-node': 3.972.31 + '@aws-sdk/middleware-eventstream': 3.972.26 + '@aws-sdk/middleware-websocket': 3.972.49 + '@aws-sdk/token-providers': 3.1048.0 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.1 + '@smithy/fetch-http-handler': 5.6.13 + '@smithy/node-http-handler': 4.9.13 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/core@3.977.6': + dependencies: + '@aws-sdk/types': 3.974.2 + '@aws-sdk/xml-builder': 3.972.37 + '@aws/lambda-invoke-store': 0.3.0 + '@smithy/core': 3.31.1 + '@smithy/signature-v4': 5.6.12 + '@smithy/types': 4.16.1 + bowser: 2.14.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.972.67': + dependencies: + '@aws-sdk/core': 3.977.6 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.1 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.972.69': + dependencies: + '@aws-sdk/core': 3.977.6 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.1 + '@smithy/fetch-http-handler': 5.6.13 + '@smithy/node-http-handler': 4.9.13 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.973.12': + dependencies: + '@aws-sdk/core': 3.977.6 + '@aws-sdk/credential-provider-env': 3.972.67 + '@aws-sdk/credential-provider-http': 3.972.69 + '@aws-sdk/credential-provider-login': 3.972.74 + '@aws-sdk/credential-provider-process': 3.972.67 + '@aws-sdk/credential-provider-sso': 3.973.11 + '@aws-sdk/credential-provider-web-identity': 3.972.73 + '@aws-sdk/nested-clients': 3.997.41 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.1 + '@smithy/credential-provider-imds': 4.4.16 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-login@3.972.74': + dependencies: + '@aws-sdk/core': 3.977.6 + '@aws-sdk/nested-clients': 3.997.41 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.1 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-node@3.972.78': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.67 + '@aws-sdk/credential-provider-http': 3.972.69 + '@aws-sdk/credential-provider-ini': 3.973.12 + '@aws-sdk/credential-provider-process': 3.972.67 + '@aws-sdk/credential-provider-sso': 3.973.11 + '@aws-sdk/credential-provider-web-identity': 3.972.73 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.1 + '@smithy/credential-provider-imds': 4.4.16 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-process@3.972.67': + dependencies: + '@aws-sdk/core': 3.977.6 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.1 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.973.11': + dependencies: + '@aws-sdk/core': 3.977.6 + '@aws-sdk/nested-clients': 3.997.41 + '@aws-sdk/token-providers': 3.1103.0 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.1 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-web-identity@3.972.73': + dependencies: + '@aws-sdk/core': 3.977.6 + '@aws-sdk/nested-clients': 3.997.41 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.1 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/eventstream-handler-node@3.972.31': + dependencies: + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.1 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-eventstream@3.972.26': + dependencies: + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.1 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-websocket@3.972.49': + dependencies: + '@aws-sdk/core': 3.977.6 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.1 + '@smithy/fetch-http-handler': 5.6.13 + '@smithy/signature-v4': 5.6.12 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/nested-clients@3.997.41': + dependencies: + '@aws-sdk/core': 3.977.6 + '@aws-sdk/signature-v4-multi-region': 3.996.43 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.1 + '@smithy/fetch-http-handler': 5.6.13 + '@smithy/node-http-handler': 4.9.13 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.996.43': + dependencies: + '@aws-sdk/types': 3.974.2 + '@smithy/signature-v4': 5.6.12 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.1048.0': + dependencies: + '@aws-sdk/core': 3.977.6 + '@aws-sdk/nested-clients': 3.997.41 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.1 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.1103.0': + dependencies: + '@aws-sdk/core': 3.977.6 + '@aws-sdk/nested-clients': 3.997.41 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.1 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/types@3.974.2': + dependencies: + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/util-locate-window@3.965.8': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.972.37': + dependencies: + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws/lambda-invoke-store@0.3.0': {} + '@babel/code-frame@7.29.0': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -4472,6 +5121,92 @@ snapshots: '@csstools/css-tokenizer@4.0.0': {} + '@earendil-works/pi-agent-core@0.84.1(@modelcontextprotocol/sdk@1.26.0(zod@3.25.76))(ws@8.19.0)(zod@3.25.76)': + dependencies: + '@earendil-works/pi-ai': 0.84.1(@modelcontextprotocol/sdk@1.26.0(zod@3.25.76))(ws@8.19.0)(zod@3.25.76) + '@earendil-works/pi-telemetry': 0.84.1 + diff: 8.0.4 + ignore: 7.0.5 + typebox: 1.3.7 + yaml: 2.9.0 + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - bufferutil + - supports-color + - utf-8-validate + - ws + - zod + + '@earendil-works/pi-ai@0.84.1(@modelcontextprotocol/sdk@1.26.0(zod@3.25.76))(ws@8.19.0)(zod@3.25.76)': + dependencies: + '@anthropic-ai/sdk': 0.91.1(zod@3.25.76) + '@aws-sdk/client-bedrock-runtime': 3.1048.0 + '@earendil-works/pi-telemetry': 0.84.1 + '@google/genai': 1.52.0(@modelcontextprotocol/sdk@1.26.0(zod@3.25.76)) + '@mistralai/mistralai': 2.2.6(@opentelemetry/api@1.9.0) + '@opentelemetry/api': 1.9.0 + '@smithy/node-http-handler': 4.7.3 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + openai: 6.26.0(ws@8.19.0)(zod@3.25.76) + partial-json: 0.1.7 + typebox: 1.3.7 + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - bufferutil + - supports-color + - utf-8-validate + - ws + - zod + + '@earendil-works/pi-client@0.84.1': + dependencies: + '@earendil-works/pi-protocol': 0.84.1 + + '@earendil-works/pi-coding-agent@0.84.1(@modelcontextprotocol/sdk@1.26.0(zod@3.25.76))(ws@8.19.0)(zod@3.25.76)': + dependencies: + '@earendil-works/pi-agent-core': 0.84.1(@modelcontextprotocol/sdk@1.26.0(zod@3.25.76))(ws@8.19.0)(zod@3.25.76) + '@earendil-works/pi-ai': 0.84.1(@modelcontextprotocol/sdk@1.26.0(zod@3.25.76))(ws@8.19.0)(zod@3.25.76) + '@earendil-works/pi-client': 0.84.1 + '@earendil-works/pi-protocol': 0.84.1 + '@earendil-works/pi-tui': 0.84.1 + '@silvia-odwyer/photon-node': 0.3.4 + chalk: 5.6.2 + cross-spawn: 7.0.6 + diff: 8.0.4 + glob: 13.0.6 + grok-mermaid: 0.2.2 + highlight.js: 10.7.3 + hosted-git-info: 9.0.3 + ignore: 7.0.5 + jiti: 2.7.0 + minimatch: 10.2.5 + proper-lockfile: 4.1.2 + semver: 7.8.0 + typebox: 1.3.7 + undici: 8.9.0 + yaml: 2.9.0 + optionalDependencies: + '@mariozechner/clipboard': 0.3.9 + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - bufferutil + - supports-color + - utf-8-validate + - ws + - zod + + '@earendil-works/pi-protocol@0.84.1': + dependencies: + typebox: 1.3.7 + + '@earendil-works/pi-telemetry@0.84.1': {} + + '@earendil-works/pi-tui@0.84.1': + dependencies: + get-east-asian-width: 1.6.0 + marked: 18.0.5 + '@emnapi/runtime@1.10.0': dependencies: tslib: 2.8.1 @@ -4555,9 +5290,9 @@ snapshots: '@esbuild/win32-x64@0.27.3': optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@10.0.1(jiti@2.6.1))': + '@eslint-community/eslint-utils@4.9.1(eslint@10.0.1(jiti@2.7.0))': dependencies: - eslint: 10.0.1(jiti@2.6.1) + eslint: 10.0.1(jiti@2.7.0) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} @@ -4647,6 +5382,19 @@ snapshots: '@gar/promise-retry@1.0.3': {} + '@google/genai@1.52.0(@modelcontextprotocol/sdk@1.26.0(zod@3.25.76))': + dependencies: + google-auth-library: 10.9.1 + p-retry: 4.6.2 + protobufjs: 7.5.4 + ws: 8.19.0 + optionalDependencies: + '@modelcontextprotocol/sdk': 1.26.0(zod@3.25.76) + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + '@grpc/grpc-js@1.14.3': dependencies: '@grpc/proto-loader': 0.8.0 @@ -4994,6 +5742,62 @@ snapshots: - encoding - supports-color + '@mariozechner/clipboard-darwin-arm64@0.3.9': + optional: true + + '@mariozechner/clipboard-darwin-universal@0.3.9': + optional: true + + '@mariozechner/clipboard-darwin-x64@0.3.9': + optional: true + + '@mariozechner/clipboard-linux-arm64-gnu@0.3.9': + optional: true + + '@mariozechner/clipboard-linux-arm64-musl@0.3.9': + optional: true + + '@mariozechner/clipboard-linux-riscv64-gnu@0.3.9': + optional: true + + '@mariozechner/clipboard-linux-x64-gnu@0.3.9': + optional: true + + '@mariozechner/clipboard-linux-x64-musl@0.3.9': + optional: true + + '@mariozechner/clipboard-win32-arm64-msvc@0.3.9': + optional: true + + '@mariozechner/clipboard-win32-x64-msvc@0.3.9': + optional: true + + '@mariozechner/clipboard@0.3.9': + optionalDependencies: + '@mariozechner/clipboard-darwin-arm64': 0.3.9 + '@mariozechner/clipboard-darwin-universal': 0.3.9 + '@mariozechner/clipboard-darwin-x64': 0.3.9 + '@mariozechner/clipboard-linux-arm64-gnu': 0.3.9 + '@mariozechner/clipboard-linux-arm64-musl': 0.3.9 + '@mariozechner/clipboard-linux-riscv64-gnu': 0.3.9 + '@mariozechner/clipboard-linux-x64-gnu': 0.3.9 + '@mariozechner/clipboard-linux-x64-musl': 0.3.9 + '@mariozechner/clipboard-win32-arm64-msvc': 0.3.9 + '@mariozechner/clipboard-win32-x64-msvc': 0.3.9 + optional: true + + '@mistralai/mistralai@2.2.6(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/semantic-conventions': 1.43.0 + ws: 8.19.0 + zod: 3.25.76 + zod-to-json-schema: 3.25.1(zod@3.25.76) + optionalDependencies: + '@opentelemetry/api': 1.9.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@modelcontextprotocol/sdk@1.26.0(zod@3.25.76)': dependencies: '@hono/node-server': 1.19.9(hono@4.12.0) @@ -5175,6 +5979,8 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.1 + '@opentelemetry/api@1.9.0': {} + '@opentelemetry/api@1.9.1': {} '@opentelemetry/context-async-hooks@2.9.0(@opentelemetry/api@1.9.1)': @@ -5489,6 +6295,61 @@ snapshots: '@sigstore/core': 3.2.1 '@sigstore/protobuf-specs': 0.5.1 + '@silvia-odwyer/photon-node@0.3.4': {} + + '@smithy/core@3.31.1': + dependencies: + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@smithy/credential-provider-imds@4.4.16': + dependencies: + '@smithy/core': 3.31.1 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.6.13': + dependencies: + '@smithy/core': 3.31.1 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@smithy/is-array-buffer@2.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/node-http-handler@4.7.3': + dependencies: + '@smithy/core': 3.31.1 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@smithy/node-http-handler@4.9.13': + dependencies: + '@smithy/core': 3.31.1 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@smithy/signature-v4@5.6.12': + dependencies: + '@smithy/core': 3.31.1 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@smithy/types@4.16.1': + dependencies: + tslib: 2.8.1 + + '@smithy/util-buffer-from@2.2.0': + dependencies: + '@smithy/is-array-buffer': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-utf8@2.3.0': + dependencies: + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.8.1 + '@standard-schema/spec@1.1.0': {} '@swc/helpers@0.5.15': @@ -5556,12 +6417,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.2.4 '@tailwindcss/oxide-win32-x64-msvc': 4.2.4 - '@tailwindcss/vite@4.2.4(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.2))': + '@tailwindcss/vite@4.2.4(vite@7.3.1(@types/node@25.3.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.9.0))': dependencies: '@tailwindcss/node': 4.2.4 '@tailwindcss/oxide': 4.2.4 tailwindcss: 4.2.4 - vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.3.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.9.0) '@testing-library/dom@10.4.1': dependencies: @@ -5686,6 +6547,8 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/retry@0.12.0': {} + '@types/semver@7.7.1': {} '@types/ssh2@1.15.5': @@ -5699,15 +6562,15 @@ snapshots: '@types/trusted-types@2.0.7': optional: true - '@typescript-eslint/eslint-plugin@8.56.0(@typescript-eslint/parser@8.56.0(eslint@10.0.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.56.0(@typescript-eslint/parser@8.56.0(eslint@10.0.1(jiti@2.7.0))(typescript@5.9.3))(eslint@10.0.1(jiti@2.7.0))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.56.0(eslint@10.0.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.56.0(eslint@10.0.1(jiti@2.7.0))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.56.0 - '@typescript-eslint/type-utils': 8.56.0(eslint@10.0.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.56.0(eslint@10.0.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.56.0(eslint@10.0.1(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.0(eslint@10.0.1(jiti@2.7.0))(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.56.0 - eslint: 10.0.1(jiti@2.6.1) + eslint: 10.0.1(jiti@2.7.0) ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.4.0(typescript@5.9.3) @@ -5715,14 +6578,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.56.0(eslint@10.0.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/parser@8.56.0(eslint@10.0.1(jiti@2.7.0))(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 8.56.0 '@typescript-eslint/types': 8.56.0 '@typescript-eslint/typescript-estree': 8.56.0(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.56.0 debug: 4.4.3 - eslint: 10.0.1(jiti@2.6.1) + eslint: 10.0.1(jiti@2.7.0) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -5745,13 +6608,13 @@ snapshots: dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.56.0(eslint@10.0.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.56.0(eslint@10.0.1(jiti@2.7.0))(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.56.0 '@typescript-eslint/typescript-estree': 8.56.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.56.0(eslint@10.0.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.0(eslint@10.0.1(jiti@2.7.0))(typescript@5.9.3) debug: 4.4.3 - eslint: 10.0.1(jiti@2.6.1) + eslint: 10.0.1(jiti@2.7.0) ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: @@ -5774,13 +6637,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.56.0(eslint@10.0.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/utils@8.56.0(eslint@10.0.1(jiti@2.7.0))(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.1(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.1(jiti@2.7.0)) '@typescript-eslint/scope-manager': 8.56.0 '@typescript-eslint/types': 8.56.0 '@typescript-eslint/typescript-estree': 8.56.0(typescript@5.9.3) - eslint: 10.0.1(jiti@2.6.1) + eslint: 10.0.1(jiti@2.7.0) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -5790,7 +6653,7 @@ snapshots: '@typescript-eslint/types': 8.56.0 eslint-visitor-keys: 5.0.1 - '@vitejs/plugin-react@5.2.0(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.2))': + '@vitejs/plugin-react@5.2.0(vite@7.3.1(@types/node@25.3.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.9.0))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) @@ -5798,11 +6661,11 @@ snapshots: '@rolldown/pluginutils': 1.0.0-rc.3 '@types/babel__core': 7.20.5 react-refresh: 0.18.0 - vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.3.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.9.0) transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.1)(@types/node@25.3.0)(jiti@2.6.1)(jsdom@28.1.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.2))': + '@vitest/coverage-v8@4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.0)(jiti@2.7.0)(jsdom@28.1.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.9.0))': dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/utils': 4.0.18 @@ -5814,7 +6677,7 @@ snapshots: obug: 2.1.1 std-env: 3.10.0 tinyrainbow: 3.0.3 - vitest: 4.0.18(@opentelemetry/api@1.9.1)(@types/node@25.3.0)(jiti@2.6.1)(jsdom@28.1.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.2) + vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.0)(jiti@2.7.0)(jsdom@28.1.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.9.0) '@vitest/expect@4.0.18': dependencies: @@ -5825,13 +6688,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.2))': + '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.3.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.0.18 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.3.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.9.0) '@vitest/pretty-format@4.0.18': dependencies: @@ -6010,6 +6873,8 @@ snapshots: dependencies: require-from-string: 2.0.2 + bignumber.js@9.3.1: {} + bin-links@6.0.2: dependencies: cmd-shim: 8.0.0 @@ -6038,6 +6903,8 @@ snapshots: transitivePeerDependencies: - supports-color + bowser@2.14.1: {} + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 @@ -6051,6 +6918,10 @@ snapshots: dependencies: balanced-match: 4.0.3 + brace-expansion@5.0.9: + dependencies: + balanced-match: 4.0.3 + browserslist@4.28.2: dependencies: baseline-browser-mapping: 2.10.23 @@ -6059,6 +6930,8 @@ snapshots: node-releases: 2.0.38 update-browserslist-db: 1.2.3(browserslist@4.28.2) + buffer-equal-constant-time@1.0.1: {} + buffer-from@1.1.2: {} buffer@5.7.1: @@ -6247,6 +7120,8 @@ snapshots: csstype@3.2.3: {} + data-uri-to-buffer@4.0.1: {} + data-urls@7.0.0: dependencies: whatwg-mimetype: 5.0.0 @@ -6284,6 +7159,8 @@ snapshots: diff@8.0.3: {} + diff@8.0.4: {} + docker-modem@5.0.6: dependencies: debug: 4.4.3 @@ -6321,6 +7198,10 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + ee-first@1.1.1: {} effect@3.18.4: @@ -6411,9 +7292,9 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-prettier@10.1.8(eslint@10.0.1(jiti@2.6.1)): + eslint-config-prettier@10.1.8(eslint@10.0.1(jiti@2.7.0)): dependencies: - eslint: 10.0.1(jiti@2.6.1) + eslint: 10.0.1(jiti@2.7.0) eslint-scope@9.1.1: dependencies: @@ -6426,9 +7307,9 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@10.0.1(jiti@2.6.1): + eslint@10.0.1(jiti@2.7.0): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.1(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.1(jiti@2.7.0)) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.23.2 '@eslint/config-helpers': 0.5.2 @@ -6459,7 +7340,7 @@ snapshots: natural-compare: 1.4.0 optionator: 0.9.4 optionalDependencies: - jiti: 2.6.1 + jiti: 2.7.0 transitivePeerDependencies: - supports-color @@ -6555,6 +7436,8 @@ snapshots: exsolve@1.0.8: {} + extend@3.0.2: {} + fast-check@3.23.2: dependencies: pure-rand: 6.1.0 @@ -6612,6 +7495,11 @@ snapshots: optionalDependencies: picomatch: 4.0.3 + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + figures@6.1.0: dependencies: is-unicode-supported: 2.1.0 @@ -6662,6 +7550,10 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + forwarded@0.2.0: {} fresh@2.0.0: {} @@ -6695,6 +7587,22 @@ snapshots: strip-ansi: 6.0.1 wide-align: 1.1.5 + gaxios@7.3.0: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + node-fetch: 3.3.2 + transitivePeerDependencies: + - supports-color + + gcp-metadata@8.1.2: + dependencies: + gaxios: 7.3.0 + google-logging-utils: 1.1.3 + json-bigint: 1.0.0 + transitivePeerDependencies: + - supports-color + geist@1.7.0(next@16.2.5(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)): dependencies: next: 16.2.5(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -6705,6 +7613,8 @@ snapshots: get-east-asian-width@1.5.0: {} + get-east-asian-width@1.6.0: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -6753,7 +7663,7 @@ snapshots: glob@13.0.6: dependencies: - minimatch: 10.2.2 + minimatch: 10.2.5 minipass: 7.1.3 path-scurry: 2.0.2 @@ -6766,12 +7676,27 @@ snapshots: once: 1.4.0 path-is-absolute: 1.0.1 + google-auth-library@10.9.1: + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 7.3.0 + gcp-metadata: 8.1.2 + google-logging-utils: 1.1.3 + jws: 4.0.1 + transitivePeerDependencies: + - supports-color + + google-logging-utils@1.1.3: {} + google-protobuf@3.21.4: {} gopd@1.2.0: {} graceful-fs@4.2.11: {} + grok-mermaid@0.2.2: {} + has-flag@4.0.0: {} has-symbols@1.1.0: {} @@ -6788,6 +7713,8 @@ snapshots: helmet@7.2.0: {} + highlight.js@10.7.3: {} + hono@4.12.0: {} hosted-git-info@7.0.2: @@ -6979,6 +7906,8 @@ snapshots: jiti@2.6.1: {} + jiti@2.7.0: {} + jose@6.1.3: {} js-tokens@10.0.0: {} @@ -7020,6 +7949,10 @@ snapshots: jsesc@3.1.0: {} + json-bigint@1.0.0: + dependencies: + bignumber.js: 9.3.1 + json-buffer@3.0.1: {} json-parse-even-better-errors@5.0.0: {} @@ -7028,6 +7961,11 @@ snapshots: dependencies: dequal: 2.0.3 + json-schema-to-ts@3.1.1: + dependencies: + '@babel/runtime': 7.29.2 + ts-algebra: 2.0.0 + json-schema-traverse@0.4.1: {} json-schema-traverse@1.0.0: {} @@ -7052,6 +7990,17 @@ snapshots: just-diff@6.0.2: {} + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -7175,6 +8124,8 @@ snapshots: marked@14.0.0: {} + marked@18.0.5: {} + math-intrinsics@1.1.0: {} mdn-data@2.27.1: {} @@ -7207,6 +8158,10 @@ snapshots: dependencies: brace-expansion: 5.0.2 + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.9 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.12 @@ -7313,12 +8268,20 @@ snapshots: node-addon-api@5.1.0: {} + node-domexception@1.0.0: {} + node-fetch-native@1.6.7: {} node-fetch@2.7.0: dependencies: whatwg-url: 5.0.0 + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + node-gyp@12.4.0: dependencies: env-paths: 2.2.1 @@ -7433,6 +8396,11 @@ snapshots: dependencies: mimic-fn: 2.1.0 + openai@6.26.0(ws@8.19.0)(zod@3.25.76): + optionalDependencies: + ws: 8.19.0 + zod: 3.25.76 + openid-client@6.8.2: dependencies: jose: 6.1.3 @@ -7457,6 +8425,11 @@ snapshots: p-map@7.0.5: {} + p-retry@4.6.2: + dependencies: + '@types/retry': 0.12.0 + retry: 0.13.1 + package-json-from-dist@1.0.1: {} pacote@21.5.1: @@ -7493,6 +8466,8 @@ snapshots: parseurl@1.3.3: {} + partial-json@0.1.7: {} + patch-console@2.0.0: {} path-exists@4.0.0: {} @@ -7590,6 +8565,12 @@ snapshots: promise-call-limit@3.0.2: {} + proper-lockfile@4.1.2: + dependencies: + graceful-fs: 4.2.11 + retry: 0.12.0 + signal-exit: 3.0.7 + protobufjs@7.5.4: dependencies: '@protobufjs/aspromise': 1.1.2 @@ -7708,6 +8689,10 @@ snapshots: ret@0.5.0: {} + retry@0.12.0: {} + + retry@0.13.1: {} + reusify@1.1.0: {} rfc4648@1.5.4: {} @@ -7792,6 +8777,8 @@ snapshots: semver@7.7.4: {} + semver@7.8.0: {} + send@1.2.1: dependencies: debug: 4.4.3 @@ -7827,7 +8814,7 @@ snapshots: dependencies: '@img/colour': 1.1.0 detect-libc: 2.1.2 - semver: 7.7.4 + semver: 7.8.0 optionalDependencies: '@img/sharp-darwin-arm64': 0.34.5 '@img/sharp-darwin-x64': 0.34.5 @@ -8165,6 +9152,8 @@ snapshots: treeverse@3.0.0: {} + ts-algebra@2.0.0: {} + ts-api-utils@2.4.0(typescript@5.9.3): dependencies: typescript: 5.9.3 @@ -8202,6 +9191,10 @@ snapshots: media-typer: 1.1.0 mime-types: 3.0.2 + typebox@1.3.11: {} + + typebox@1.3.7: {} + typescript@5.9.3: {} undici-types@5.26.5: {} @@ -8214,6 +9207,8 @@ snapshots: undici@7.25.0: {} + undici@8.9.0: {} + unpipe@1.0.0: {} upath@1.2.0: {} @@ -8241,7 +9236,7 @@ snapshots: vary@1.1.2: {} - vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.2): + vite@7.3.1(@types/node@25.3.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.9.0): dependencies: esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.3) @@ -8252,15 +9247,15 @@ snapshots: optionalDependencies: '@types/node': 25.3.0 fsevents: 2.3.3 - jiti: 2.6.1 + jiti: 2.7.0 lightningcss: 1.32.0 tsx: 4.21.0 - yaml: 2.8.2 + yaml: 2.9.0 - vitest@4.0.18(@opentelemetry/api@1.9.1)(@types/node@25.3.0)(jiti@2.6.1)(jsdom@28.1.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.2): + vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.0)(jiti@2.7.0)(jsdom@28.1.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.9.0): dependencies: '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.2)) + '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.3.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.9.0)) '@vitest/pretty-format': 4.0.18 '@vitest/runner': 4.0.18 '@vitest/snapshot': 4.0.18 @@ -8277,10 +9272,10 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.3.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: - '@opentelemetry/api': 1.9.1 + '@opentelemetry/api': 1.9.0 '@types/node': 25.3.0 jsdom: 28.1.0 transitivePeerDependencies: @@ -8302,6 +9297,8 @@ snapshots: walk-up-path@4.0.0: {} + web-streams-polyfill@3.3.3: {} + webidl-conversions@3.0.1: {} webidl-conversions@8.0.1: {} @@ -8384,6 +9381,8 @@ snapshots: yaml@2.8.2: {} + yaml@2.9.0: {} + yargs-parser@21.1.1: {} yargs@17.7.2: diff --git a/src/cli/src/config/pi-extension.ts b/src/cli/src/config/pi-extension.ts index 271e2e6..2aab540 100644 --- a/src/cli/src/config/pi-extension.ts +++ b/src/cli/src/config/pi-extension.ts @@ -10,6 +10,6 @@ export const PI_EXTENSION_FILENAMES = ['mcpctl-pi.ts', 'mcp-http.ts'] as const; export const PI_EXTENSION_FILES: Record = { - 'mcpctl-pi.ts': "/**\n * mcpctl pi extension — native integration (no MCP client, no Claude).\n *\n * Talks JSON-RPC directly to mcplocal's project MCP endpoint and registers\n * each project tool as a native pi tool.\n *\n * KEY ARCHITECTURAL POINT: mcplocal's project *gate* state is bound to a\n * `mcp-session-id`. A gated project only exposes `begin_session` until it is\n * called, after which the full tool set becomes callable — but only within\n * THAT session. So we keep ONE persistent `McpHttpSession` per active project\n * and route every tools/call through it, re-listing after each call to pick up\n * the post-begin_session tool additions.\n *\n * Features:\n * - project gating auto-ungates when begin_session is called\n * - switching the active project from the pi TUI (/mcpctl)\n * - a status line\n *\n * Load standalone: pi -e ./src/pi-ext/mcpctl-pi.ts\n * or via settings: \"extensions\": [\"/abs/path/to/mcpctl-pi.ts\"]\n *\n * Only imports pi-bundled packages — no @mcpctl/*, no ~/.claude.\n */\nimport { Type, type TSchema } from \"typebox\";\nimport { StringEnum } from \"@earendil-works/pi-ai\";\nimport type { ExtensionAPI, ExtensionContext } from \"@earendil-works/pi-coding-agent\";\nimport {\n McpHttpSession,\n type ToolInfo,\n type McpHttpOptions,\n} from \"./mcp-http.js\";\nimport { readFile, writeFile, mkdir } from \"node:fs/promises\";\nimport { join, dirname } from \"node:path\";\nimport { homedir } from \"node:os\";\n\n// ── config / state ───────────────────────────────────────────────────────────\nconst McpctlDir = join(homedir(), \".mcpctl\");\nconst ConfigPath = join(McpctlDir, \"config.json\");\nconst CredPath = join(McpctlDir, \"credentials\");\nconst PiStatePath = join(McpctlDir, \"pi-state.json\");\nconst ProjectMarker = \".mcpctl-project\";\n\ninterface PiState {\n project?: string;\n}\n\nasync function readJson(path: string): Promise {\n try {\n return JSON.parse(await readFile(path, \"utf-8\")) as T;\n } catch {\n return null;\n }\n}\n\nasync function readConfig(): Promise<{ mcplocalUrl: string }> {\n const cfg = await readJson<{ mcplocalUrl?: string }>(ConfigPath);\n return { mcplocalUrl: cfg?.mcplocalUrl ?? \"http://localhost:3200\" };\n}\n\nasync function readCreds(): Promise<{ token?: string; mcpdUrl?: string } | null> {\n try {\n const raw = await readFile(CredPath, \"utf-8\");\n const parsed = JSON.parse(raw) as { token?: string; mcpdUrl?: string };\n if (!parsed.token) return null;\n return parsed;\n } catch {\n return null;\n }\n}\n\nasync function readPiState(): Promise {\n return (await readJson(PiStatePath)) ?? {};\n}\n\nasync function writePiState(state: PiState): Promise {\n await mkdir(McpctlDir, { recursive: true });\n await writeFile(PiStatePath, JSON.stringify(state, null, 2) + \"\\n\", \"utf-8\");\n}\n\n/** Walk up directories looking for a `.mcpctl-project` marker. */\nasync function findProjectMarker(start = process.cwd()): Promise {\n const home = homedir();\n let dir = start;\n for (let i = 0; i < 50; i++) {\n try {\n const raw = await readFile(join(dir, ProjectMarker), \"utf-8\");\n const project = raw.split(\"\\n\")[0]?.trim() ?? \"\";\n if (project) return project;\n } catch {\n // continue walking\n }\n if (dir === home || dir === \"/\") break;\n const parent = dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n return null;\n}\n\n/** Request the list of projects from mcpd (via mcplocal API passthrough). */\nasync function listProjects(mcplocalUrl: string, token?: string): Promise {\n const url = `${mcplocalUrl}/api/v1/projects`;\n const headers: Record = { Accept: \"application/json\" };\n if (token) headers.Authorization = `Bearer ${token}`;\n const res = await fetch(url, { headers });\n if (!res.ok) throw new Error(`list projects: HTTP ${res.status}`);\n const body = (await res.json()) as Array<{ name: string }> | { data?: Array<{ name: string }> };\n const arr = Array.isArray(body) ? body : (body.data ?? []);\n return arr.map((p) => p.name);\n}\n\n// ── JSON Schema → TypeBox ────────────────────────────────────────────────────\nfunction convertSchema(inputSchema: unknown): TSchema {\n if (!inputSchema || typeof inputSchema !== \"object\") {\n return Type.Object({});\n }\n const s = inputSchema as {\n type?: string;\n properties?: Record;\n required?: string[];\n items?: unknown;\n enum?: unknown[];\n };\n\n const required = new Set(Array.isArray(s.required) ? s.required : []);\n const props = s.properties ?? {};\n const fields: Record = {};\n\n for (const [name, raw] of Object.entries(props)) {\n const schema = convertProp(raw);\n fields[name] = required.has(name) ? schema : Type.Optional(schema);\n }\n return Type.Object(fields);\n}\n\nfunction convertProp(raw: unknown): TSchema {\n if (!raw || typeof raw !== \"object\") return Type.Unknown();\n const s = raw as {\n type?: string | string[];\n enum?: unknown[];\n description?: string;\n items?: unknown;\n properties?: Record;\n };\n\n const desc = typeof s.description === \"string\" ? s.description : undefined;\n const enumVals = Array.isArray(s.enum) && s.enum.length > 0 ? s.enum : undefined;\n\n if (enumVals && enumVals.every((v) => typeof v === \"string\")) {\n return StringEnum(enumVals as string[]);\n }\n if (enumVals && enumVals.every((v) => typeof v === \"number\")) {\n const literals = enumVals.map((v) => Type.Literal(v));\n return desc ? Type.Union(literals, { description: desc }) : Type.Union(literals);\n }\n\n let type = s.type;\n if (Array.isArray(type)) type = type.find((t) => t !== \"null\");\n type = type ?? (s.properties ? \"object\" : s.items ? \"array\" : \"string\");\n if (type === \"integer\") type = \"number\";\n\n switch (type) {\n case \"string\":\n return desc ? Type.String({ description: desc }) : Type.String();\n case \"number\":\n return desc ? Type.Number({ description: desc }) : Type.Number();\n case \"boolean\":\n return desc ? Type.Boolean({ description: desc }) : Type.Boolean();\n case \"array\": {\n const inner = s.items ? convertProp(s.items) : Type.Unknown();\n return desc ? Type.Array(inner, { description: desc }) : Type.Array(inner);\n }\n case \"object\":\n return convertSchema(s as unknown);\n default:\n return desc ? Type.Unknown({ description: desc }) : Type.Unknown();\n }\n}\n\n/** Sanitize a name for use as a pi tool name segment ([a-z0-9_]). */\nfunction safeSegment(name: string): string {\n return name.toLowerCase().replace(/[^a-z0-9_]+/g, \"_\").replace(/^_+|_+$/g, \"\") || \"x\";\n}\n\nfunction slugify(name: string): string {\n return safeSegment(name);\n}\n\n/** pi tool name for a project + MCP tool (e.g. `mc_docmost_docmost_create_page`). */\nfunction piToolName(project: string, mcpTool: string): string {\n const proj = slugify(project);\n const tool = mcpTool.replace(/^[a-z0-9_-]+\\//i, (m) => slugify(m.slice(0, -1)) + \"_\") || mcpTool;\n return `mc_${proj}_${safeSegment(tool)}`;\n}\n\n// ── the extension ────────────────────────────────────────────────────────────\nexport default function (pi: ExtensionAPI) {\n let mcplocalUrl = \"http://localhost:3200\";\n let token: string | undefined;\n let activeProject: string | null = null;\n /** Persistent session for the active project — MUST be reused to keep gate state. */\n let session: McpHttpSession | null = null;\n /** Pi tool names currently registered for the active project. */\n const projectToolNames = new Set();\n let activeCtx: ExtensionContext | null = null;\n\n function endpointUrl(): string {\n return `${mcplocalUrl.replace(/\\/$/, \"\")}/projects/${encodeURIComponent(activeProject ?? \"\")}/mcp`;\n }\n\n /** Build (or reuse) the persistent session for the active project. */\n async function ensureSession(): Promise {\n if (session) return session;\n const opts: McpHttpOptions = { bearer: token, timeoutMs: 120_000 };\n const sess = new McpHttpSession(endpointUrl(), opts);\n await sess.initialize();\n await sess.sendNotification(\"notifications/initialized\");\n session = sess;\n return sess;\n }\n\n async function closeSession(): Promise {\n const s = session;\n session = null;\n if (s) await s.close();\n }\n\n /**\n * (Re)discover the active project's current tool set and register any new pi\n * tools. Called on load and after every tool call (to pick up ungating).\n */\n async function reconcileTools(project: string): Promise<{ tools: string[] }> {\n const sess = await ensureSession();\n const { tools } = await sess.listTools();\n const names: string[] = [];\n for (const t of tools) {\n const piName = piToolName(project, t.name);\n if (!projectToolNames.has(piName)) {\n pi.registerTool(createTool(piName, t, project));\n projectToolNames.add(piName);\n }\n names.push(piName);\n }\n // Activate built-ins/other extensions + all tools discovered in this project.\n const active = pi.getActiveTools().filter((n) => !n.startsWith(\"mc_\"));\n pi.setActiveTools([...new Set([...active, ...names])]);\n return { tools: names };\n }\n\n function createTool(piName: string, t: ToolInfo, project: string) {\n const mcpName = t.name;\n return {\n name: piName,\n label: `${project} :: ${mcpName}`,\n description: t.description ?? `mcpctl tool ${mcpName} on project ${project}`,\n parameters: convertSchema(t.inputSchema),\n async execute(_toolCallId: string, params: Record) {\n const sess = await ensureSession();\n const res = await sess.callTool(mcpName, params ?? {});\n // Re-list after every call — picks up post-begin_session tool additions.\n try {\n await reconcileTools(project);\n } catch {\n // non-fatal\n }\n const text = (res.content ?? [])\n .filter((c) => c.type === \"text\")\n .map((c) => c.text ?? \"\")\n .join(\"\\n\");\n if (res.isError) {\n throw new Error(text || `${mcpName} returned an error`);\n }\n return {\n content: [{ type: \"text\", text: text || \"(no text content)\" }],\n details: { mcpTool: mcpName, project, raw: res },\n };\n },\n };\n }\n\n function setStatus(msg?: string): void {\n try {\n activeCtx?.ui?.setStatus?.(\"mcpctl\", msg ?? (activeProject ? `project: ${activeProject}` : \"no project\"));\n } catch {\n /* non-fatal */\n }\n }\n\n // ── /mcpctl command ──\n pi.registerCommand(\"mcpctl\", {\n description: \"mcpctl integration: status, switch project, refresh tools\",\n handler: async (_args, ctx) => {\n if (!ctx.hasUI) {\n return `mcpctl: active project = ${activeProject ?? \"none\"}; mcplocal = ${mcplocalUrl}`;\n }\n const choice = await ctx.ui.select(\"mcpctl\", [\n { value: \"status\", label: \"Status\" },\n { value: \"switch\", label: \"Switch project\" },\n { value: \"refresh\", label: \"Refresh tools\" },\n { value: \"sync skills\", label: \"Sync skills (mcpctl skills sync)\" },\n { value: \"close\", label: \"Close\" },\n ]);\n if (!choice) return;\n\n if (choice === \"status\") {\n const cs = await readConfig();\n const creds = await readCreds();\n const info =\n `mcplocal: ${cs.mcplocalUrl}\\n` +\n `user: ${creds?.mcpdUrl ?? \"not logged in\"}\\n` +\n `active project: ${activeProject ?? \"none\"}\\n` +\n `tools registered: ${projectToolNames.size}\\n` +\n ` ${[...projectToolNames].join(\"\\n \")}\\n`;\n await ctx.ui.editor(\"mcpctl status\", info);\n return;\n }\n\n if (choice === \"switch\") {\n await switchProject(ctx);\n return;\n }\n\n if (choice === \"refresh\") {\n if (!activeProject) {\n ctx.ui.notify(\"No active project — switch project first\", \"warning\");\n return;\n }\n try {\n const r = await reconcileTools(activeProject);\n ctx.ui.notify(`Refreshed: ${r.tools.length} tools for '${activeProject}'`, \"info\");\n setStatus();\n } catch (e) {\n ctx.ui.notify(`Refresh failed: ${(e as Error).message}`, \"error\");\n }\n return;\n }\n\n if (choice === \"sync skills\") {\n try {\n const { execSync } = await import(\"node:child_process\");\n execSync(`mcpctl skills sync${activeProject ? ` -p ${activeProject}` : \"\"} --quiet`, {\n stdio: \"inherit\",\n cwd: process.cwd(),\n });\n ctx.ui.notify(\"mcpctl skills synced (into ~/.claude/skills)\", \"info\");\n } catch (e) {\n ctx.ui.notify(`skills sync failed: ${(e as Error).message}`, \"error\");\n }\n }\n },\n });\n\n async function switchProject(ctx: ExtensionContext): Promise {\n let projects: string[] = [];\n try {\n projects = await listProjects(mcplocalUrl, token);\n } catch (e) {\n ctx.ui.notify(`Failed to list projects: ${(e as Error).message}`, \"error\");\n return;\n }\n if (projects.length === 0) {\n ctx.ui.notify(\"No projects returned by mcpd\", \"warning\");\n return;\n }\n const picked = await ctx.ui.select(\"Switch to project\", projects);\n if (!picked) return;\n\n // Tear down old project's session + active tools.\n await closeSession();\n const active = pi.getActiveTools().filter((n) => !n.startsWith(\"mc_\"));\n pi.setActiveTools([...active]);\n projectToolNames.clear();\n\n activeProject = picked;\n await writePiState({ project: picked });\n try {\n const r = await reconcileTools(picked);\n ctx.ui.notify(`Switched to '${picked}': ${r.tools.length} tool(s) ready`, \"info\");\n setStatus();\n } catch (e) {\n ctx.ui.notify(`Could not load tools for '${picked}': ${(e as Error).message}`, \"error\");\n }\n }\n\n // ── session lifecycle ──\n pi.on(\"session_start\", async (_event, ctx) => {\n activeCtx = ctx;\n try { ctx.ui?.setStatus?.(\"mcpctl\", \"connecting...\"); } catch { /* ignore */ }\n const cfg = await readConfig();\n mcplocalUrl = cfg.mcplocalUrl;\n const creds = await readCreds();\n token = creds?.token ?? undefined;\n\n const piState = await readPiState();\n const project = piState.project ?? (await findProjectMarker());\n if (!project) {\n try { ctx.ui?.setStatus?.(\"mcpctl\", \"no project (use /mcpctl)\"); } catch { /* ignore */ }\n return;\n }\n activeProject = project;\n try {\n const r = await reconcileTools(project);\n if (ctx.hasUI) ctx.ui.notify(`mcpctl: project '${project}' — ${r.tools.length} tool(s) ready`, \"info\");\n setStatus();\n } catch (e) {\n if (ctx.hasUI) ctx.ui.notify(`mcpctl: could not load tools — ${(e as Error).message}`, \"error\");\n }\n });\n\n pi.on(\"session_shutdown\", () => {\n void closeSession();\n });\n}\n", - 'mcp-http.ts': "/**\n * Self-contained Streamable-HTTP JSON-RPC client for the pi extension.\n *\n * Vendored (no `@mcpctl/*` imports) so the extension is loadable standalone\n * by pi (`pi -e ./mcpctl-pi.ts` / settings `extensions`). Talks directly to\n * mcplocal's project MCP endpoint — the same one `mcpctl mcp` bridges to.\n *\n * Only Node builtins are imported.\n */\nimport http from 'node:http';\nimport https from 'node:https';\n\nexport interface McpHttpOptions {\n bearer?: string;\n headers?: Record;\n timeoutMs?: number;\n}\n\nexport interface ToolInfo {\n name: string;\n description?: string;\n inputSchema?: unknown;\n}\n\nexport interface ToolCallResult {\n content: Array<{ type: string; text?: string }>;\n isError?: boolean;\n}\n\ninterface HttpArgs {\n url: string;\n method: string;\n headers?: Record;\n body?: string;\n timeoutMs?: number;\n}\n\ninterface HttpResult {\n status: number;\n headers: http.IncomingHttpHeaders;\n body: string;\n}\n\nfunction rawHttpRequest(opts: HttpArgs): Promise {\n return new Promise((resolve, reject) => {\n const parsed = new URL(opts.url);\n const driver = parsed.protocol === 'https:' ? https : http;\n const req = driver.request(\n {\n hostname: parsed.hostname,\n port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),\n path: parsed.pathname + parsed.search,\n method: opts.method,\n headers: opts.headers,\n timeout: opts.timeoutMs ?? 30_000,\n },\n (res) => {\n const chunks: Buffer[] = [];\n res.on('data', (chunk: Buffer) => chunks.push(chunk));\n res.on('end', () => {\n resolve({\n status: res.statusCode ?? 0,\n headers: res.headers,\n body: Buffer.concat(chunks).toString('utf-8'),\n });\n });\n },\n );\n req.on('error', reject);\n req.on('timeout', () => {\n req.destroy();\n reject(new Error('MCP request timed out'));\n });\n if (opts.body) req.write(opts.body);\n req.end();\n });\n}\n\nfunction parseSse(body: string): unknown[] {\n const messages: unknown[] = [];\n for (const line of body.split('\\n')) {\n if (line.startsWith('data: ')) {\n try {\n messages.push(JSON.parse(line.slice(6)));\n } catch {\n // skip malformed SSE line\n }\n }\n }\n return messages;\n}\n\nexport class McpProtocolError extends Error {\n constructor(public readonly code: number, message: string) {\n super(`MCP error ${code}: ${message}`);\n this.name = 'McpProtocolError';\n }\n}\n\nexport class McpTransportError extends Error {\n constructor(public readonly status: number, public readonly body: string, message?: string) {\n super(message ?? `HTTP ${status}: ${body.slice(0, 200)}`);\n this.name = 'McpTransportError';\n }\n}\n\nexport interface McpResponse {\n result?: unknown;\n error?: { code: number; message: string };\n notifications: unknown[];\n}\n\nexport class McpHttpSession {\n private sessionId: string | undefined;\n private nextId = 1;\n\n constructor(\n public readonly url: string,\n private readonly options: McpHttpOptions = {},\n ) {}\n\n private buildHeaders(extra: Record = {}): Record {\n const headers: Record = {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json, text/event-stream',\n ...(this.options.headers ?? {}),\n ...extra,\n };\n if (this.sessionId) headers['mcp-session-id'] = this.sessionId;\n if (this.options.bearer) headers['Authorization'] = `Bearer ${this.options.bearer}`;\n return headers;\n }\n\n /**\n * Send a JSON-RPC request, return the matched result AND any notifications\n * that were multiplexed into the same body (e.g. tools/list_changed).\n */\n async sendFull(method: string, params: Record = {}): Promise {\n const id = this.nextId++;\n const request = { jsonrpc: '2.0', id, method, params };\n const result = await rawHttpRequest({\n url: this.url,\n method: 'POST',\n headers: this.buildHeaders(),\n body: JSON.stringify(request),\n timeoutMs: this.options.timeoutMs,\n });\n\n if (!this.sessionId) {\n const sid = result.headers['mcp-session-id'];\n if (typeof sid === 'string') this.sessionId = sid;\n }\n\n if (result.status >= 400) {\n let message = `HTTP ${result.status}`;\n try {\n const body = JSON.parse(result.body) as { error?: string | { message?: string } };\n const errField = body.error;\n if (typeof errField === 'string') message = errField;\n else if (errField && typeof errField === 'object' && typeof errField.message === 'string') {\n message = errField.message;\n }\n } catch {\n message = `HTTP ${result.status}: ${result.body.slice(0, 200)}`;\n }\n throw new McpTransportError(result.status, result.body, message);\n }\n\n const messages = result.headers['content-type']?.includes('text/event-stream')\n ? parseSse(result.body)\n : [JSON.parse(result.body)];\n\n const notifications: unknown[] = [];\n const matched = messages.find((m) => {\n const msg = m as { id?: unknown; method?: unknown };\n if (msg.method !== undefined && (msg.id === undefined || msg.id === null)) {\n notifications.push(msg);\n return false;\n }\n return msg.id === id;\n }) as { result?: unknown; error?: { code: number; message: string } } | undefined;\n\n const parsed = matched ?? (messages[0] as { result?: unknown; error?: { code: number; message: string } } | undefined);\n if (!parsed) throw new Error(`No response for ${method}`);\n if (parsed.error) throw new McpProtocolError(parsed.error.code, parsed.error.message);\n return { result: parsed.result, error: undefined, notifications };\n }\n\n /** Convenience: return just the result, throwing on error. */\n async send(method: string, params: Record = {}): Promise {\n const res = await this.sendFull(method, params);\n return res.result;\n }\n\n async sendNotification(method: string, params: Record = {}): Promise {\n const notification = { jsonrpc: '2.0', method, params };\n await rawHttpRequest({\n url: this.url,\n method: 'POST',\n headers: this.buildHeaders(),\n body: JSON.stringify(notification),\n timeoutMs: this.options.timeoutMs,\n }).catch(() => { /* best-effort */ });\n }\n\n async initialize(): Promise {\n return this.send('initialize', {\n protocolVersion: '2024-11-05',\n capabilities: {},\n clientInfo: { name: 'mcpctl-pi', version: '1.0.0' },\n });\n }\n\n /** List tools exposed by the endpoint. Returns raw result so caller sees capabilities. */\n async listTools(): Promise<{ tools: ToolInfo[]; raw: unknown }> {\n const raw = await this.send('tools/list');\n const tools = (raw as { tools?: ToolInfo[] }).tools ?? [];\n return { tools, raw };\n }\n\n /** Call a tool. */\n async callTool(name: string, args: Record = {}): Promise {\n return this.send('tools/call', { name, arguments: args }) as ToolCallResult;\n }\n\n /** Close the session (DELETE). Safe when no session has been negotiated. */\n async close(): Promise {\n if (this.sessionId === undefined) return;\n await rawHttpRequest({\n url: this.url,\n method: 'DELETE',\n headers: this.buildHeaders(),\n timeoutMs: 5_000,\n }).catch(() => { /* best-effort */ });\n this.sessionId = undefined;\n }\n}\n", + 'mcpctl-pi.ts': "/**\n * mcpctl pi extension — native integration (no MCP client, no Claude).\n *\n * Talks JSON-RPC directly to mcplocal's project MCP endpoint and registers\n * each project tool as a native pi tool.\n *\n * KEY ARCHITECTURAL POINT: mcplocal's project *gate* state is bound to a\n * `mcp-session-id`. A gated project only exposes `begin_session` until it is\n * called, after which the full tool set becomes callable — but only within\n * THAT session. So we keep ONE persistent `McpHttpSession` per active project\n * and route every tools/call through it, re-listing after each call to pick up\n * the post-begin_session tool additions.\n *\n * Features:\n * - project gating auto-ungates when begin_session is called\n * - switching the active project from the pi TUI (/mcpctl)\n * - a status line\n *\n * Load standalone: pi -e ./src/pi-ext/mcpctl-pi.ts\n * or via settings: \"extensions\": [\"/abs/path/to/mcpctl-pi.ts\"]\n *\n * Only imports pi-bundled packages — no @mcpctl/*, no ~/.claude.\n */\nimport { Type, type TSchema } from \"typebox\";\nimport { StringEnum } from \"@earendil-works/pi-ai\";\nimport type { ExtensionAPI, ExtensionContext } from \"@earendil-works/pi-coding-agent\";\nimport {\n McpHttpSession,\n type ToolInfo,\n type McpHttpOptions,\n} from \"./mcp-http.js\";\nimport { readFile, writeFile, mkdir } from \"node:fs/promises\";\nimport { join, dirname } from \"node:path\";\nimport { homedir } from \"node:os\";\n\n// ── config / state ───────────────────────────────────────────────────────────\nconst McpctlDir = join(homedir(), \".mcpctl\");\nconst ConfigPath = join(McpctlDir, \"config.json\");\nconst CredPath = join(McpctlDir, \"credentials\");\nconst PiStatePath = join(McpctlDir, \"pi-state.json\");\nconst ProjectMarker = \".mcpctl-project\";\n\ninterface PiState {\n project?: string;\n}\n\nasync function readJson(path: string): Promise {\n try {\n return JSON.parse(await readFile(path, \"utf-8\")) as T;\n } catch {\n return null;\n }\n}\n\nasync function readConfig(): Promise<{ mcplocalUrl: string }> {\n const cfg = await readJson<{ mcplocalUrl?: string }>(ConfigPath);\n return { mcplocalUrl: cfg?.mcplocalUrl ?? \"http://localhost:3200\" };\n}\n\nasync function readCreds(): Promise<{ token?: string; mcpdUrl?: string } | null> {\n try {\n const raw = await readFile(CredPath, \"utf-8\");\n const parsed = JSON.parse(raw) as { token?: string; mcpdUrl?: string };\n if (!parsed.token) return null;\n return parsed;\n } catch {\n return null;\n }\n}\n\nasync function readPiState(): Promise {\n return (await readJson(PiStatePath)) ?? {};\n}\n\nasync function writePiState(state: PiState): Promise {\n await mkdir(McpctlDir, { recursive: true });\n await writeFile(PiStatePath, JSON.stringify(state, null, 2) + \"\\n\", \"utf-8\");\n}\n\n/** Walk up directories looking for a `.mcpctl-project` marker. */\nasync function findProjectMarker(start = process.cwd()): Promise {\n const home = homedir();\n let dir = start;\n for (let i = 0; i < 50; i++) {\n try {\n const raw = await readFile(join(dir, ProjectMarker), \"utf-8\");\n const project = raw.split(\"\\n\")[0]?.trim() ?? \"\";\n if (project) return project;\n } catch {\n // continue walking\n }\n if (dir === home || dir === \"/\") break;\n const parent = dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n return null;\n}\n\n/** Request the list of projects from mcpd (via mcplocal API passthrough). */\nasync function listProjects(mcplocalUrl: string, token?: string): Promise {\n const url = `${mcplocalUrl}/api/v1/projects`;\n const headers: Record = { Accept: \"application/json\" };\n if (token) headers.Authorization = `Bearer ${token}`;\n const res = await fetch(url, { headers });\n if (!res.ok) throw new Error(`list projects: HTTP ${res.status}`);\n const body = (await res.json()) as Array<{ name: string }> | { data?: Array<{ name: string }> };\n const arr = Array.isArray(body) ? body : (body.data ?? []);\n return arr.map((p) => p.name);\n}\n\n// ── JSON Schema → TypeBox ────────────────────────────────────────────────────\nfunction convertSchema(inputSchema: unknown): TSchema {\n if (!inputSchema || typeof inputSchema !== \"object\") {\n return Type.Object({});\n }\n const s = inputSchema as {\n type?: string;\n properties?: Record;\n required?: string[];\n items?: unknown;\n enum?: unknown[];\n };\n\n const required = new Set(Array.isArray(s.required) ? s.required : []);\n const props = s.properties ?? {};\n const fields: Record = {};\n\n for (const [name, raw] of Object.entries(props)) {\n const schema = convertProp(raw);\n fields[name] = required.has(name) ? schema : Type.Optional(schema);\n }\n return Type.Object(fields);\n}\n\nfunction convertProp(raw: unknown): TSchema {\n if (!raw || typeof raw !== \"object\") return Type.Unknown();\n const s = raw as {\n type?: string | string[];\n enum?: unknown[];\n description?: string;\n items?: unknown;\n properties?: Record;\n };\n\n const desc = typeof s.description === \"string\" ? s.description : undefined;\n const enumVals = Array.isArray(s.enum) && s.enum.length > 0 ? s.enum : undefined;\n\n if (enumVals && enumVals.every((v) => typeof v === \"string\")) {\n return StringEnum(enumVals as string[]);\n }\n if (enumVals && enumVals.every((v) => typeof v === \"number\")) {\n const literals = enumVals.map((v) => Type.Literal(v));\n return desc ? Type.Union(literals, { description: desc }) : Type.Union(literals);\n }\n\n let type = s.type;\n if (Array.isArray(type)) type = type.find((t) => t !== \"null\");\n type = type ?? (s.properties ? \"object\" : s.items ? \"array\" : \"string\");\n if (type === \"integer\") type = \"number\";\n\n switch (type) {\n case \"string\":\n return desc ? Type.String({ description: desc }) : Type.String();\n case \"number\":\n return desc ? Type.Number({ description: desc }) : Type.Number();\n case \"boolean\":\n return desc ? Type.Boolean({ description: desc }) : Type.Boolean();\n case \"array\": {\n const inner = s.items ? convertProp(s.items) : Type.Unknown();\n return desc ? Type.Array(inner, { description: desc }) : Type.Array(inner);\n }\n case \"object\":\n return convertSchema(s as unknown);\n default:\n return desc ? Type.Unknown({ description: desc }) : Type.Unknown();\n }\n}\n\n/** Sanitize a name for use as a pi tool name segment ([a-z0-9_]). */\nfunction safeSegment(name: string): string {\n return name.toLowerCase().replace(/[^a-z0-9_]+/g, \"_\").replace(/^_+|_+$/g, \"\") || \"x\";\n}\n\nfunction slugify(name: string): string {\n return safeSegment(name);\n}\n\n/** pi tool name for a project + MCP tool (e.g. `mc_docmost_docmost_create_page`). */\nfunction piToolName(project: string, mcpTool: string): string {\n const proj = slugify(project);\n const tool = mcpTool.replace(/^[a-z0-9_-]+\\//i, (m) => slugify(m.slice(0, -1)) + \"_\") || mcpTool;\n return `mc_${proj}_${safeSegment(tool)}`;\n}\n\n// ── the extension ────────────────────────────────────────────────────────────\nexport default function (pi: ExtensionAPI) {\n let mcplocalUrl = \"http://localhost:3200\";\n let token: string | undefined;\n let activeProject: string | null = null;\n /** Persistent session for the active project — MUST be reused to keep gate state. */\n let session: McpHttpSession | null = null;\n /** Registered pi tool name → the MCP tool it forwards to, for the active project. */\n const projectToolNames = new Map();\n let activeCtx: ExtensionContext | null = null;\n\n function endpointUrl(): string {\n return `${mcplocalUrl.replace(/\\/$/, \"\")}/projects/${encodeURIComponent(activeProject ?? \"\")}/mcp`;\n }\n\n /** Build (or reuse) the persistent session for the active project. */\n async function ensureSession(): Promise {\n if (session) return session;\n const opts: McpHttpOptions = { bearer: token, timeoutMs: 120_000 };\n const sess = new McpHttpSession(endpointUrl(), opts);\n await sess.initialize();\n await sess.sendNotification(\"notifications/initialized\");\n session = sess;\n return sess;\n }\n\n async function closeSession(): Promise {\n const s = session;\n session = null;\n if (s) await s.close();\n }\n\n /**\n * (Re)discover the active project's current tool set and register any new pi\n * tools. Called on load and after every tool call (to pick up ungating).\n */\n async function reconcileTools(project: string): Promise<{ tools: string[] }> {\n const sess = await ensureSession();\n const { tools } = await sess.listTools();\n const names: string[] = [];\n for (const t of tools) {\n const piName = uniqueToolName(project, t.name);\n if (piName === null) continue; // already registered for this same MCP tool\n pi.registerTool(createTool(piName, t, project));\n projectToolNames.set(piName, t.name);\n names.push(piName);\n }\n // Names already registered for tools still in the list stay active.\n for (const [piName, mcpName] of projectToolNames) {\n if (!names.includes(piName) && tools.some((t) => t.name === mcpName)) names.push(piName);\n }\n // Activate built-ins/other extensions + all tools discovered in this project.\n const active = pi.getActiveTools().filter((n) => !n.startsWith(\"mc_\"));\n pi.setActiveTools([...new Set([...active, ...names])]);\n return { tools: names };\n }\n\n /**\n * The pi tool name to register `mcpTool` under, or null if it is already\n * registered. Sanitising MCP names down to `[a-z0-9_]` can collide (e.g.\n * `docs.search` and `docs-search`); without a suffix the second tool would\n * silently never be registered while still being reported as available, and\n * its calls would be forwarded to the first tool instead.\n */\n function uniqueToolName(project: string, mcpTool: string): string | null {\n const base = piToolName(project, mcpTool);\n if (projectToolNames.get(base) === mcpTool) return null;\n if (!projectToolNames.has(base)) return base;\n for (let i = 2; i < 100; i++) {\n const candidate = `${base}_${String(i)}`;\n if (projectToolNames.get(candidate) === mcpTool) return null;\n if (!projectToolNames.has(candidate)) return candidate;\n }\n return null;\n }\n\n function createTool(piName: string, t: ToolInfo, project: string) {\n const mcpName = t.name;\n return {\n name: piName,\n label: `${project} :: ${mcpName}`,\n description: t.description ?? `mcpctl tool ${mcpName} on project ${project}`,\n parameters: convertSchema(t.inputSchema),\n async execute(_toolCallId: string, params: Record) {\n const sess = await ensureSession();\n const res = await sess.callTool(mcpName, params ?? {});\n // Re-list after every call — picks up post-begin_session tool additions.\n try {\n await reconcileTools(project);\n } catch {\n // non-fatal\n }\n const text = (res.content ?? [])\n .filter((c) => c.type === \"text\")\n .map((c) => c.text ?? \"\")\n .join(\"\\n\");\n if (res.isError) {\n throw new Error(text || `${mcpName} returned an error`);\n }\n return {\n // `type` must be the literal \"text\" — pi's AgentToolResult content is\n // a TextContent | ImageContent union, not { type: string }.\n content: [{ type: \"text\" as const, text: text || \"(no text content)\" }],\n details: { mcpTool: mcpName, project, raw: res },\n };\n },\n };\n }\n\n function setStatus(msg?: string): void {\n try {\n activeCtx?.ui?.setStatus?.(\"mcpctl\", msg ?? (activeProject ? `project: ${activeProject}` : \"no project\"));\n } catch {\n /* non-fatal */\n }\n }\n\n // ── /mcpctl command ──\n /**\n * Menu labels → actions. `ctx.ui.select` takes plain strings and returns the\n * chosen string, so the mapping has to live here rather than in the options.\n */\n const MENU: ReadonlyArray = [\n [\"Status\", \"status\"],\n [\"Switch project\", \"switch\"],\n [\"Refresh tools\", \"refresh\"],\n [\"Sync skills into pi's skills dir\", \"sync\"],\n [\"Close\", \"close\"],\n ];\n\n pi.registerCommand(\"mcpctl\", {\n description: \"mcpctl integration: status, switch project, refresh tools\",\n handler: async (_args, ctx) => {\n if (!ctx.hasUI) {\n // A command handler resolves to void — pi ignores a returned string, so\n // headless status has to go through notify.\n ctx.ui.notify(\n `mcpctl: active project = ${activeProject ?? \"none\"}; mcplocal = ${mcplocalUrl}`,\n \"info\",\n );\n return;\n }\n const picked = await ctx.ui.select(\"mcpctl\", MENU.map(([label]) => label));\n if (!picked) return;\n const choice = MENU.find(([label]) => label === picked)?.[1];\n if (!choice || choice === \"close\") return;\n\n if (choice === \"status\") {\n const cs = await readConfig();\n const creds = await readCreds();\n const info =\n `mcplocal: ${cs.mcplocalUrl}\\n` +\n `user: ${creds?.mcpdUrl ?? \"not logged in\"}\\n` +\n `active project: ${activeProject ?? \"none\"}\\n` +\n `tools registered: ${projectToolNames.size}\\n` +\n ` ${[...projectToolNames].join(\"\\n \")}\\n`;\n await ctx.ui.editor(\"mcpctl status\", info);\n return;\n }\n\n if (choice === \"switch\") {\n await switchProject(ctx);\n return;\n }\n\n if (choice === \"refresh\") {\n if (!activeProject) {\n ctx.ui.notify(\"No active project — switch project first\", \"warning\");\n return;\n }\n try {\n const r = await reconcileTools(activeProject);\n ctx.ui.notify(`Refreshed: ${r.tools.length} tools for '${activeProject}'`, \"info\");\n setStatus();\n } catch (e) {\n ctx.ui.notify(`Refresh failed: ${(e as Error).message}`, \"error\");\n }\n return;\n }\n\n if (choice === \"sync\") {\n try {\n // `--agent pi` is what keeps skills in ~/.pi/agent/skills; without it\n // this syncs into ~/.claude/skills, which the whole point of this\n // integration is to avoid depending on.\n const args = [\"skills\", \"sync\", \"--agent\", \"pi\", \"--quiet\"];\n if (activeProject) args.push(\"-p\", activeProject);\n // execFile, not execSync+stdio:\"inherit\": a shell would need the\n // project name quoted, and inherited stdio writes raw output over\n // pi's TUI. Capture instead and report through notify.\n const { execFile } = await import(\"node:child_process\");\n const stderr = await new Promise((resolve, reject) => {\n execFile(\"mcpctl\", args, { timeout: 120_000 }, (err, _stdout, errOut) => {\n if (err) reject(new Error((errOut || err.message).trim()));\n else resolve(errOut);\n });\n });\n const detail = stderr.trim().split(\"\\n\").pop();\n ctx.ui.notify(\n `mcpctl skills synced into pi's skills dir${detail ? ` — ${detail}` : \"\"}. Run /reload to pick them up.`,\n \"info\",\n );\n } catch (e) {\n ctx.ui.notify(`skills sync failed: ${(e as Error).message}`, \"error\");\n }\n }\n },\n });\n\n async function switchProject(ctx: ExtensionContext): Promise {\n let projects: string[] = [];\n try {\n projects = await listProjects(mcplocalUrl, token);\n } catch (e) {\n ctx.ui.notify(`Failed to list projects: ${(e as Error).message}`, \"error\");\n return;\n }\n if (projects.length === 0) {\n ctx.ui.notify(\"No projects returned by mcpd\", \"warning\");\n return;\n }\n const picked = await ctx.ui.select(\"Switch to project\", projects);\n if (!picked) return;\n\n // Tear down old project's session + active tools.\n await closeSession();\n const active = pi.getActiveTools().filter((n) => !n.startsWith(\"mc_\"));\n pi.setActiveTools([...active]);\n projectToolNames.clear();\n\n activeProject = picked;\n await writePiState({ project: picked });\n try {\n const r = await reconcileTools(picked);\n ctx.ui.notify(`Switched to '${picked}': ${r.tools.length} tool(s) ready`, \"info\");\n setStatus();\n } catch (e) {\n ctx.ui.notify(`Could not load tools for '${picked}': ${(e as Error).message}`, \"error\");\n }\n }\n\n // ── session lifecycle ──\n pi.on(\"session_start\", async (_event, ctx) => {\n activeCtx = ctx;\n try { ctx.ui?.setStatus?.(\"mcpctl\", \"connecting...\"); } catch { /* ignore */ }\n const cfg = await readConfig();\n mcplocalUrl = cfg.mcplocalUrl;\n const creds = await readCreds();\n token = creds?.token ?? undefined;\n\n const piState = await readPiState();\n const project = piState.project ?? (await findProjectMarker());\n if (!project) {\n try { ctx.ui?.setStatus?.(\"mcpctl\", \"no project (use /mcpctl)\"); } catch { /* ignore */ }\n return;\n }\n activeProject = project;\n try {\n const r = await reconcileTools(project);\n if (ctx.hasUI) ctx.ui.notify(`mcpctl: project '${project}' — ${r.tools.length} tool(s) ready`, \"info\");\n setStatus();\n } catch (e) {\n if (ctx.hasUI) ctx.ui.notify(`mcpctl: could not load tools — ${(e as Error).message}`, \"error\");\n }\n });\n\n pi.on(\"session_shutdown\", () => {\n void closeSession();\n });\n}\n", + 'mcp-http.ts': "/**\n * Self-contained Streamable-HTTP JSON-RPC client for the pi extension.\n *\n * Vendored (no `@mcpctl/*` imports) so the extension is loadable standalone\n * by pi (`pi -e ./mcpctl-pi.ts` / settings `extensions`). Talks directly to\n * mcplocal's project MCP endpoint — the same one `mcpctl mcp` bridges to.\n *\n * Only Node builtins are imported.\n */\nimport http from 'node:http';\nimport https from 'node:https';\n\nexport interface McpHttpOptions {\n bearer?: string;\n headers?: Record;\n timeoutMs?: number;\n}\n\nexport interface ToolInfo {\n name: string;\n description?: string;\n inputSchema?: unknown;\n}\n\nexport interface ToolCallResult {\n content: Array<{ type: string; text?: string }>;\n isError?: boolean;\n}\n\ninterface HttpArgs {\n url: string;\n method: string;\n headers?: Record;\n body?: string;\n timeoutMs?: number;\n}\n\ninterface HttpResult {\n status: number;\n headers: http.IncomingHttpHeaders;\n body: string;\n}\n\nfunction rawHttpRequest(opts: HttpArgs): Promise {\n return new Promise((resolve, reject) => {\n const parsed = new URL(opts.url);\n const driver = parsed.protocol === 'https:' ? https : http;\n const req = driver.request(\n {\n hostname: parsed.hostname,\n port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),\n path: parsed.pathname + parsed.search,\n method: opts.method,\n headers: opts.headers,\n timeout: opts.timeoutMs ?? 30_000,\n },\n (res) => {\n const chunks: Buffer[] = [];\n res.on('data', (chunk: Buffer) => chunks.push(chunk));\n res.on('end', () => {\n resolve({\n status: res.statusCode ?? 0,\n headers: res.headers,\n body: Buffer.concat(chunks).toString('utf-8'),\n });\n });\n },\n );\n req.on('error', reject);\n req.on('timeout', () => {\n req.destroy();\n reject(new Error('MCP request timed out'));\n });\n if (opts.body) req.write(opts.body);\n req.end();\n });\n}\n\nfunction parseSse(body: string): unknown[] {\n const messages: unknown[] = [];\n for (const line of body.split('\\n')) {\n if (line.startsWith('data: ')) {\n try {\n messages.push(JSON.parse(line.slice(6)));\n } catch {\n // skip malformed SSE line\n }\n }\n }\n return messages;\n}\n\nexport class McpProtocolError extends Error {\n constructor(public readonly code: number, message: string) {\n super(`MCP error ${code}: ${message}`);\n this.name = 'McpProtocolError';\n }\n}\n\nexport class McpTransportError extends Error {\n constructor(public readonly status: number, public readonly body: string, message?: string) {\n super(message ?? `HTTP ${status}: ${body.slice(0, 200)}`);\n this.name = 'McpTransportError';\n }\n}\n\nexport interface McpResponse {\n result?: unknown;\n error?: { code: number; message: string };\n notifications: unknown[];\n}\n\nexport class McpHttpSession {\n private sessionId: string | undefined;\n private nextId = 1;\n\n constructor(\n public readonly url: string,\n private readonly options: McpHttpOptions = {},\n ) {}\n\n private buildHeaders(extra: Record = {}): Record {\n const headers: Record = {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json, text/event-stream',\n ...(this.options.headers ?? {}),\n ...extra,\n };\n if (this.sessionId) headers['mcp-session-id'] = this.sessionId;\n if (this.options.bearer) headers['Authorization'] = `Bearer ${this.options.bearer}`;\n return headers;\n }\n\n /**\n * Send a JSON-RPC request, return the matched result AND any notifications\n * that were multiplexed into the same body (e.g. tools/list_changed).\n */\n async sendFull(method: string, params: Record = {}): Promise {\n const id = this.nextId++;\n const request = { jsonrpc: '2.0', id, method, params };\n const result = await rawHttpRequest({\n url: this.url,\n method: 'POST',\n headers: this.buildHeaders(),\n body: JSON.stringify(request),\n timeoutMs: this.options.timeoutMs,\n });\n\n if (!this.sessionId) {\n const sid = result.headers['mcp-session-id'];\n if (typeof sid === 'string') this.sessionId = sid;\n }\n\n if (result.status >= 400) {\n let message = `HTTP ${result.status}`;\n try {\n const body = JSON.parse(result.body) as { error?: string | { message?: string } };\n const errField = body.error;\n if (typeof errField === 'string') message = errField;\n else if (errField && typeof errField === 'object' && typeof errField.message === 'string') {\n message = errField.message;\n }\n } catch {\n message = `HTTP ${result.status}: ${result.body.slice(0, 200)}`;\n }\n throw new McpTransportError(result.status, result.body, message);\n }\n\n const messages = result.headers['content-type']?.includes('text/event-stream')\n ? parseSse(result.body)\n : [JSON.parse(result.body)];\n\n const notifications: unknown[] = [];\n const matched = messages.find((m) => {\n const msg = m as { id?: unknown; method?: unknown };\n if (msg.method !== undefined && (msg.id === undefined || msg.id === null)) {\n notifications.push(msg);\n return false;\n }\n return msg.id === id;\n }) as { result?: unknown; error?: { code: number; message: string } } | undefined;\n\n const parsed = matched ?? (messages[0] as { result?: unknown; error?: { code: number; message: string } } | undefined);\n if (!parsed) throw new Error(`No response for ${method}`);\n if (parsed.error) throw new McpProtocolError(parsed.error.code, parsed.error.message);\n return { result: parsed.result, error: undefined, notifications };\n }\n\n /** Convenience: return just the result, throwing on error. */\n async send(method: string, params: Record = {}): Promise {\n const res = await this.sendFull(method, params);\n return res.result;\n }\n\n async sendNotification(method: string, params: Record = {}): Promise {\n const notification = { jsonrpc: '2.0', method, params };\n await rawHttpRequest({\n url: this.url,\n method: 'POST',\n headers: this.buildHeaders(),\n body: JSON.stringify(notification),\n timeoutMs: this.options.timeoutMs,\n }).catch(() => { /* best-effort */ });\n }\n\n async initialize(): Promise {\n return this.send('initialize', {\n protocolVersion: '2024-11-05',\n capabilities: {},\n clientInfo: { name: 'mcpctl-pi', version: '1.0.0' },\n });\n }\n\n /** List tools exposed by the endpoint. Returns raw result so caller sees capabilities. */\n async listTools(): Promise<{ tools: ToolInfo[]; raw: unknown }> {\n const raw = await this.send('tools/list');\n const tools = (raw as { tools?: ToolInfo[] }).tools ?? [];\n return { tools, raw };\n }\n\n /** Call a tool. */\n async callTool(name: string, args: Record = {}): Promise {\n const result = await this.send('tools/call', { name, arguments: args });\n return result as ToolCallResult;\n }\n\n /** Close the session (DELETE). Safe when no session has been negotiated. */\n async close(): Promise {\n if (this.sessionId === undefined) return;\n await rawHttpRequest({\n url: this.url,\n method: 'DELETE',\n headers: this.buildHeaders(),\n timeoutMs: 5_000,\n }).catch(() => { /* best-effort */ });\n this.sessionId = undefined;\n }\n}\n", }; diff --git a/src/cli/src/utils/pi-settings.ts b/src/cli/src/utils/pi-settings.ts index db611a4..dee4d41 100644 --- a/src/cli/src/utils/pi-settings.ts +++ b/src/cli/src/utils/pi-settings.ts @@ -152,7 +152,12 @@ export async function registerWithPi( addedSkills.push(skillsDir); } - await writeSettings(settingsPath, settings); + // Only rewrite when something actually changed. Re-serialising drops any + // comments the file had (readSettings strips them to parse), so a no-op run + // must not silently reformat a hand-maintained settings.json. + if (addedExtensions.length > 0 || addedSkills.length > 0) { + await writeSettings(settingsPath, settings); + } return { addedExtensions, addedSkills }; } diff --git a/src/cli/tests/config/pi-extension-embed.test.ts b/src/cli/tests/config/pi-extension-embed.test.ts new file mode 100644 index 0000000..5453278 --- /dev/null +++ b/src/cli/tests/config/pi-extension-embed.test.ts @@ -0,0 +1,45 @@ +import { describe, it, expect } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { PI_EXTENSION_FILES, PI_EXTENSION_FILENAMES } from '../../src/config/pi-extension.js'; + +/** + * `mcpctl config pi` installs the *embedded* copy of the extension, not the + * files in src/pi-ext/. Editing the sources without re-running the generator + * therefore ships stale code to users while the repo looks correct — and the + * embedded copy is the one thing no typecheck covers. Same guarantee the + * completions check gives. + */ +const repoRoot = join(import.meta.dirname, '..', '..', '..', '..'); +const piExtDir = join(repoRoot, 'src', 'pi-ext'); + +describe('embedded pi extension', () => { + it('matches the sources in src/pi-ext (re-run scripts/generate-pi-extension.ts)', () => { + for (const name of PI_EXTENSION_FILENAMES) { + const onDisk = readFileSync(join(piExtDir, name), 'utf-8'); + expect(PI_EXTENSION_FILES[name], `${name} is embedded`).toBeDefined(); + expect(PI_EXTENSION_FILES[name], `${name} is stale — regenerate the embed`).toBe(onDisk); + } + }); + + it('embeds every file the extension needs to load', () => { + // mcpctl-pi.ts imports ./mcp-http.js — installing one without the other + // yields an extension pi cannot load. + expect(Object.keys(PI_EXTENSION_FILES).sort()).toEqual(['mcp-http.ts', 'mcpctl-pi.ts']); + expect(PI_EXTENSION_FILES['mcpctl-pi.ts']).toContain('./mcp-http.js'); + }); + + it('carries the fixes the pi API requires', () => { + const main = PI_EXTENSION_FILES['mcpctl-pi.ts'] ?? ''; + // ctx.ui.select takes string[] and returns the chosen string. + expect(main).not.toMatch(/select\([^)]*\[\s*\{\s*value:/); + // Skills must land in pi's tree, never ~/.claude: the sync passes + // --agent pi and no longer tells the user it wrote to Claude's tree. + expect(main).toContain('"--agent", "pi"'); + expect(main).not.toContain('(into ~/.claude/skills)'); + // Spawned without a shell, so the project name is never interpolated into + // a command string. Matches the call, not prose mentioning it. + expect(main).toContain('execFile("mcpctl", args'); + expect(main).not.toMatch(/execSync\s*\(/); + }); +}); diff --git a/src/pi-ext/mcp-http.ts b/src/pi-ext/mcp-http.ts index c6df53d..dda0cb8 100644 --- a/src/pi-ext/mcp-http.ts +++ b/src/pi-ext/mcp-http.ts @@ -220,7 +220,8 @@ export class McpHttpSession { /** Call a tool. */ async callTool(name: string, args: Record = {}): Promise { - return this.send('tools/call', { name, arguments: args }) as ToolCallResult; + const result = await this.send('tools/call', { name, arguments: args }); + return result as ToolCallResult; } /** Close the session (DELETE). Safe when no session has been negotiated. */ diff --git a/src/pi-ext/mcpctl-pi.ts b/src/pi-ext/mcpctl-pi.ts index 2eb726d..1d3dcd4 100644 --- a/src/pi-ext/mcpctl-pi.ts +++ b/src/pi-ext/mcpctl-pi.ts @@ -200,8 +200,8 @@ export default function (pi: ExtensionAPI) { let activeProject: string | null = null; /** Persistent session for the active project — MUST be reused to keep gate state. */ let session: McpHttpSession | null = null; - /** Pi tool names currently registered for the active project. */ - const projectToolNames = new Set(); + /** Registered pi tool name → the MCP tool it forwards to, for the active project. */ + const projectToolNames = new Map(); let activeCtx: ExtensionContext | null = null; function endpointUrl(): string { @@ -234,19 +234,41 @@ export default function (pi: ExtensionAPI) { const { tools } = await sess.listTools(); const names: string[] = []; for (const t of tools) { - const piName = piToolName(project, t.name); - if (!projectToolNames.has(piName)) { - pi.registerTool(createTool(piName, t, project)); - projectToolNames.add(piName); - } + const piName = uniqueToolName(project, t.name); + if (piName === null) continue; // already registered for this same MCP tool + pi.registerTool(createTool(piName, t, project)); + projectToolNames.set(piName, t.name); names.push(piName); } + // Names already registered for tools still in the list stay active. + for (const [piName, mcpName] of projectToolNames) { + if (!names.includes(piName) && tools.some((t) => t.name === mcpName)) names.push(piName); + } // Activate built-ins/other extensions + all tools discovered in this project. const active = pi.getActiveTools().filter((n) => !n.startsWith("mc_")); pi.setActiveTools([...new Set([...active, ...names])]); return { tools: names }; } + /** + * The pi tool name to register `mcpTool` under, or null if it is already + * registered. Sanitising MCP names down to `[a-z0-9_]` can collide (e.g. + * `docs.search` and `docs-search`); without a suffix the second tool would + * silently never be registered while still being reported as available, and + * its calls would be forwarded to the first tool instead. + */ + function uniqueToolName(project: string, mcpTool: string): string | null { + const base = piToolName(project, mcpTool); + if (projectToolNames.get(base) === mcpTool) return null; + if (!projectToolNames.has(base)) return base; + for (let i = 2; i < 100; i++) { + const candidate = `${base}_${String(i)}`; + if (projectToolNames.get(candidate) === mcpTool) return null; + if (!projectToolNames.has(candidate)) return candidate; + } + return null; + } + function createTool(piName: string, t: ToolInfo, project: string) { const mcpName = t.name; return { @@ -271,7 +293,9 @@ export default function (pi: ExtensionAPI) { throw new Error(text || `${mcpName} returned an error`); } return { - content: [{ type: "text", text: text || "(no text content)" }], + // `type` must be the literal "text" — pi's AgentToolResult content is + // a TextContent | ImageContent union, not { type: string }. + content: [{ type: "text" as const, text: text || "(no text content)" }], details: { mcpTool: mcpName, project, raw: res }, }; }, @@ -287,20 +311,34 @@ export default function (pi: ExtensionAPI) { } // ── /mcpctl command ── + /** + * Menu labels → actions. `ctx.ui.select` takes plain strings and returns the + * chosen string, so the mapping has to live here rather than in the options. + */ + const MENU: ReadonlyArray = [ + ["Status", "status"], + ["Switch project", "switch"], + ["Refresh tools", "refresh"], + ["Sync skills into pi's skills dir", "sync"], + ["Close", "close"], + ]; + pi.registerCommand("mcpctl", { description: "mcpctl integration: status, switch project, refresh tools", handler: async (_args, ctx) => { if (!ctx.hasUI) { - return `mcpctl: active project = ${activeProject ?? "none"}; mcplocal = ${mcplocalUrl}`; + // A command handler resolves to void — pi ignores a returned string, so + // headless status has to go through notify. + ctx.ui.notify( + `mcpctl: active project = ${activeProject ?? "none"}; mcplocal = ${mcplocalUrl}`, + "info", + ); + return; } - const choice = await ctx.ui.select("mcpctl", [ - { value: "status", label: "Status" }, - { value: "switch", label: "Switch project" }, - { value: "refresh", label: "Refresh tools" }, - { value: "sync skills", label: "Sync skills (mcpctl skills sync)" }, - { value: "close", label: "Close" }, - ]); - if (!choice) return; + const picked = await ctx.ui.select("mcpctl", MENU.map(([label]) => label)); + if (!picked) return; + const choice = MENU.find(([label]) => label === picked)?.[1]; + if (!choice || choice === "close") return; if (choice === "status") { const cs = await readConfig(); @@ -335,14 +373,28 @@ export default function (pi: ExtensionAPI) { return; } - if (choice === "sync skills") { + if (choice === "sync") { try { - const { execSync } = await import("node:child_process"); - execSync(`mcpctl skills sync${activeProject ? ` -p ${activeProject}` : ""} --quiet`, { - stdio: "inherit", - cwd: process.cwd(), + // `--agent pi` is what keeps skills in ~/.pi/agent/skills; without it + // this syncs into ~/.claude/skills, which the whole point of this + // integration is to avoid depending on. + const args = ["skills", "sync", "--agent", "pi", "--quiet"]; + if (activeProject) args.push("-p", activeProject); + // execFile, not execSync+stdio:"inherit": a shell would need the + // project name quoted, and inherited stdio writes raw output over + // pi's TUI. Capture instead and report through notify. + const { execFile } = await import("node:child_process"); + const stderr = await new Promise((resolve, reject) => { + execFile("mcpctl", args, { timeout: 120_000 }, (err, _stdout, errOut) => { + if (err) reject(new Error((errOut || err.message).trim())); + else resolve(errOut); + }); }); - ctx.ui.notify("mcpctl skills synced (into ~/.claude/skills)", "info"); + const detail = stderr.trim().split("\n").pop(); + ctx.ui.notify( + `mcpctl skills synced into pi's skills dir${detail ? ` — ${detail}` : ""}. Run /reload to pick them up.`, + "info", + ); } catch (e) { ctx.ui.notify(`skills sync failed: ${(e as Error).message}`, "error"); } diff --git a/src/pi-ext/tsconfig.json b/src/pi-ext/tsconfig.json new file mode 100644 index 0000000..174f31c --- /dev/null +++ b/src/pi-ext/tsconfig.json @@ -0,0 +1,26 @@ +{ + "//": [ + "The pi extension is shipped as source (embedded in the CLI, then written", + "into ~/.pi/agent/extensions/) and is therefore never compiled by the CLI's", + "own build. Without this project it was typechecked by nothing, which is how", + "a ctx.ui.select() call with the wrong option shape shipped.", + "", + "It is checked against the REAL @earendil-works/pi-coding-agent types (a dev", + "dependency) rather than a hand-written shim, because a shim drifting from", + "the published API is the exact failure mode this guards against." + ], + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2022"], + "types": ["node"], + "strict": true, + "noImplicitOverride": true, + "noUncheckedIndexedAccess": false, + "noEmit": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "files": ["mcpctl-pi.ts", "mcp-http.ts"] +}