diff --git a/.changeset/olive-hounds-repeat.md b/.changeset/olive-hounds-repeat.md new file mode 100644 index 0000000000..43d361ec54 --- /dev/null +++ b/.changeset/olive-hounds-repeat.md @@ -0,0 +1,9 @@ +--- +"@executor-js/pi": minor +--- + +Add `@executor-js/pi`, a first-party extension that connects the Pi coding agent to Executor. + +Pi ships no MCP client, so Pi users previously needed a third-party bridge to reach Executor. This package registers Executor's core tools — `executor_execute`, `executor_skills`, and `executor_resume` — as native Pi tools and forwards each call over MCP. + +Install it with `pi install npm:@executor-js/pi`, then point it at Executor with `EXECUTOR_MCP_URL` and a bearer: `EXECUTOR_API_KEY` for a hosted Executor, or `EXECUTOR_AUTH_TOKEN` for a local or desktop one. `/executor` reports the resolved endpoint and checks the connection. diff --git a/.oxlintrc.jsonc b/.oxlintrc.jsonc index c86ee9dc58..0cc3bc0c9d 100644 --- a/.oxlintrc.jsonc +++ b/.oxlintrc.jsonc @@ -145,6 +145,25 @@ "executor/no-unknown-error-message": "off", }, }, + { + // boundary: the Pi extension runs inside Pi's runtime, not ours. There + // is no Effect runtime in that process, and throwing is Pi's error + // PROTOCOL, not a shortcut: `AgentToolResult` has no error field, and the + // harness marks a tool call failed exactly when `execute` throws + // (pi-agent-core, harness/execution/tools.js). The MCP client it drives + // is likewise promise-native. Scoped to this package because every file + // in it sits on that boundary; the rules that keep our own failures + // typed (no-double-cast, no-json-parse, no-explicit-any, …) stay on. + "files": ["packages/hosts/pi/**/*.ts"], + "rules": { + "executor/no-error-constructor": "off", + "executor/no-instanceof-error": "off", + "executor/no-promise-catch": "off", + "executor/no-promise-reject": "off", + "executor/no-try-catch-or-throw": "off", + "executor/no-unknown-error-message": "off", + }, + }, { "files": ["packages/kernel/core/src/code-recovery.ts"], "rules": { diff --git a/AGENTS.md b/AGENTS.md index 244b88678a..7e7e1fc71c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -51,6 +51,8 @@ executor. React, API, and testing helpers. - `packages/react`: shared React UI and client/atom integration. - `packages/hosts/mcp`: MCP host surface. +- `packages/hosts/pi`: the Pi extension (`@executor-js/pi`). Pi ships no MCP + client, so this bridges Executor's core tools into it. - `packages/kernel/*`: execution runtimes and code-execution substrate. - `apps/{local,cloud,cli,desktop}`: product composition roots. diff --git a/README.md b/README.md index cbf860f1bd..0bf0f5bfe0 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,41 @@ differs) already filled in. Most MCP clients only load servers at startup, so you may need to restart the client or open a new chat before the Executor tools appear. +### Use with Pi + +[Pi](https://pi.dev) ships no MCP client, so `add-mcp` does not apply to it. +Install the first-party extension instead — it needs an Executor to talk to, so +set one up first (any of the forms above): + +```bash +pi install npm:@executor-js/pi +``` + +Point it at your Executor with two environment variables — the endpoint, and a +bearer for it. Which bearer depends on where that Executor runs: + +```bash +# Hosted (executor.sh, or a deployment of your own) +export EXECUTOR_MCP_URL=https://executor.example/acme/mcp # the URL from the Connect card +export EXECUTOR_API_KEY=… # from Executor's API Keys page + +# Local CLI service or desktop app — loopback is not a free pass, auth is on +export EXECUTOR_MCP_URL=http://127.0.0.1:4788/mcp # the URL from the Connect card +export EXECUTOR_AUTH_TOKEN=… # the server's own bearer token +``` + +A local server mints that token on first run and keeps it, so it stays valid +across restarts. The Connect card's `add-mcp` command carries it in the +`Authorization: Bearer …` header it prints; on disk it is the `token` in +`~/.executor/server-control/auth.json` (or `$EXECUTOR_DATA_DIR/server-control/auth.json`). +`EXECUTOR_API_KEY` wins when both are set. + +Then run `/executor` in Pi: it prints the endpoint it resolved and the tools it +can see. Executor arrives as `executor_execute`, `executor_skills`, and +`executor_resume` — the same small surface every other agent gets, so Pi's +context stays clear of your individual tool schemas. Start with +`executor_skills` for the guide to writing `executor_execute` code. + ## Add an integration From the web UI, click **Add Integration**, paste an OpenAPI, GraphQL, or MCP URL, diff --git a/bun.lock b/bun.lock index 473028d7be..c32de7eb9c 100644 --- a/bun.lock +++ b/bun.lock @@ -355,9 +355,11 @@ "name": "@executor-js/e2e", "version": "0.0.48", "dependencies": { + "@earendil-works/pi-coding-agent": "^0.85.1", "@executor-js/api": "workspace:*", "@executor-js/emulate": "^0.14.1", "@executor-js/mcporter": "^0.11.4", + "@executor-js/pi": "workspace:*", "@executor-js/plugin-graphql": "workspace:*", "@executor-js/plugin-mcp": "workspace:*", "@executor-js/plugin-openapi": "workspace:*", @@ -755,6 +757,34 @@ "zod": "4.3.6", }, }, + "packages/hosts/pi": { + "name": "@executor-js/pi", + "version": "0.0.0", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.29.0", + }, + "devDependencies": { + "@earendil-works/pi-ai": "^0.85.1", + "@earendil-works/pi-coding-agent": "^0.85.1", + "@effect/vitest": "catalog:", + "@executor-js/execution": "workspace:*", + "@executor-js/host-mcp": "workspace:*", + "@executor-js/plugin-mcp": "workspace:*", + "@types/node": "catalog:", + "bun-types": "catalog:", + "effect": "catalog:", + "tsup": "catalog:", + "typebox": "^1.3.7", + "typescript": "catalog:", + "vitest": "catalog:", + "zod": "4.3.6", + }, + "peerDependencies": { + "@earendil-works/pi-ai": "*", + "@earendil-works/pi-coding-agent": "*", + "typebox": "*", + }, + }, "packages/kernel/core": { "name": "@executor-js/codemode-core", "version": "1.6.8", @@ -1339,6 +1369,8 @@ "@antfu/ni": ["@antfu/ni@0.23.2", "", { "bin": { "na": "bin/na.mjs", "ni": "bin/ni.mjs", "nr": "bin/nr.mjs", "nu": "bin/nu.mjs", "nci": "bin/nci.mjs", "nlx": "bin/nlx.mjs", "nun": "bin/nun.mjs" } }, "sha512-FSEVWXvwroExDXUu8qV6Wqp2X3D1nJ0Li4LFymCyvCVrm7I3lNfG0zZWSWvGU1RE7891eTnFTyh31L3igOwNKQ=="], + "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.123.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1", "standardwebhooks": "^1.0.0" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-Y9oX9mPNGZClHQOFqrWRk43Srcu/UHuPq3rfxxOq7JgW0gi+lJA2MAOK4Ul3k/+AUrwRWFJvd0tK3oC0Pw25dw=="], + "@astrojs/cloudflare": ["@astrojs/cloudflare@13.1.10", "", { "dependencies": { "@astrojs/internal-helpers": "0.8.0", "@astrojs/underscore-redirects": "1.0.3", "@cloudflare/vite-plugin": "^1.25.6", "piccolore": "^0.1.3", "tinyglobby": "^0.2.15", "vite": "^7.3.1" }, "peerDependencies": { "astro": "^6.0.0", "wrangler": "^4.61.1" } }, "sha512-Ogl8p4MifPMHbpHKJL78eqEL+I3wbHrYmo83P/FkKZQ9VzzKi2A1RjnbaiiPAwrA8xQOqIUo4zlN7+Mse0aJAQ=="], "@astrojs/compiler": ["@astrojs/compiler@3.0.1", "", {}, "sha512-z97oYbdebO5aoWzuJ/8q5hLK232+17KcLZ7cJ8BCWk6+qNzVxn/gftC0KzMBUTD8WAaBkPpNSQK6PXLnNrZ0CA=="], @@ -1371,6 +1403,8 @@ "@aws-sdk/checksums": ["@aws-sdk/checksums@3.1000.8", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", "@aws-sdk/core": "^3.974.23", "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-v0U9S7gBIme3OTgt1LdbAF4RpvavCc+4GK1+1xqAcqtbrHsEhjQo6R45LKcjhs/+WrRJij1Y0Gztw7QPAIeUfA=="], + "@aws-sdk/client-bedrock-runtime": ["@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.974.11", "@aws-sdk/credential-provider-node": "^3.972.42", "@aws-sdk/eventstream-handler-node": "^3.972.16", "@aws-sdk/middleware-eventstream": "^3.972.12", "@aws-sdk/middleware-websocket": "^3.972.19", "@aws-sdk/token-providers": "3.1048.0", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/fetch-http-handler": "^5.4.2", "@smithy/node-http-handler": "^4.7.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-u+NT61JZEkRFtpL0CAw1N1dwxnaLgwVXQl/zjJxTGgLyS/jTIdg2SdoEoCTHxgDyCnqa1HEi9QOoE9/pYRNpOQ=="], + "@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.1075.0", "", { "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.23", "@aws-sdk/credential-provider-node": "^3.972.58", "@aws-sdk/middleware-flexible-checksums": "^3.974.33", "@aws-sdk/middleware-sdk-s3": "^3.972.54", "@aws-sdk/signature-v4-multi-region": "^3.996.35", "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/fetch-http-handler": "^5.4.6", "@smithy/node-http-handler": "^4.7.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-h1A6nIl1YX6Y45enGsTK7ef3ZrOnBiQJ1qF5R2K/nMWfsu6A9mc2Y5T66nxerABzyjjyyvign3MrzafnFoQKmA=="], "@aws-sdk/client-sqs": ["@aws-sdk/client-sqs@3.1075.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.23", "@aws-sdk/credential-provider-node": "^3.972.58", "@aws-sdk/middleware-sdk-sqs": "^3.972.31", "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/fetch-http-handler": "^5.4.6", "@smithy/node-http-handler": "^4.7.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-ZHowzLHFTIz482Z98Y8GtszLN7Q1YvC74xxExwr2rbKJeNWTXHVoMVwlq/jv5Kyf0elVAGmoID12Gf6aSdWy0Q=="], @@ -1393,17 +1427,23 @@ "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.55", "", { "dependencies": { "@aws-sdk/core": "^3.974.23", "@aws-sdk/nested-clients": "^3.997.23", "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-g2BoECD1q01kTPByi56+VLVvdWDzMkKIcr77qixpqH0okw2t0U5CoPv+6S8v/D1Y2Wa6QKKtn6XAtDzP+Kfpvg=="], + "@aws-sdk/eventstream-handler-node": ["@aws-sdk/eventstream-handler-node@3.972.34", "", { "dependencies": { "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-cTeVzpu1xEAkryTZBYhGwnQ6gOGyp8ZYZvmn0Sg/nI/ABmy/CRHHxPDJDUi9PxwxUtGGaatvfRUB3FCgT/rSWw=="], + + "@aws-sdk/middleware-eventstream": ["@aws-sdk/middleware-eventstream@3.972.29", "", { "dependencies": { "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-dlRzHCgyB8W6hLuDC5pcT5q+ziPt00n4QGgGBE17ucLVU4zMa6lsbuUdQ2Pm75Z5VA8GF+R/+SgrRcaTdIzSIQ=="], + "@aws-sdk/middleware-flexible-checksums": ["@aws-sdk/middleware-flexible-checksums@3.974.33", "", { "dependencies": { "@aws-sdk/checksums": "^3.1000.8", "tslib": "^2.6.2" } }, "sha512-qMgQSPemQq2/eW/e/0+SpY4kYR5L7dUgBiVdEc5bd+ztHNv07ZMYiI+sTiir3TgKndFfglSw/VFi7oZJ6bZ63g=="], "@aws-sdk/middleware-sdk-s3": ["@aws-sdk/middleware-sdk-s3@3.972.54", "", { "dependencies": { "@aws-sdk/core": "^3.974.23", "@aws-sdk/signature-v4-multi-region": "^3.996.35", "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-GDfDQ0gwLFRKN9gWIKcmVrHJ3e7XagnY7N1LLzMVNgnOnuY7f/ALgmy3CuBjosWD95T/Z6e+gs1IeWmLPkyLKQ=="], "@aws-sdk/middleware-sdk-sqs": ["@aws-sdk/middleware-sdk-sqs@3.972.31", "", { "dependencies": { "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-56ifsBmK9bLn5EE/t6c0nmjOB1BO8cJDLkA1VOlsN1GR85ROqnaCwVDspqcwsLaBDgPlwyYNedoDIoT3t6Ho1A=="], + "@aws-sdk/middleware-websocket": ["@aws-sdk/middleware-websocket@3.972.52", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-vsPPM+nMbKJlUCFU+eoGZbdxdxDIAX9LbpjSXaR5Ufpmqgp8TdYQnoExhLu4T3umW/JIIPny1ydbhWidZZYokQ=="], + "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.23", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.23", "@aws-sdk/signature-v4-multi-region": "^3.996.35", "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/fetch-http-handler": "^5.4.6", "@smithy/node-http-handler": "^4.7.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-gO93ZPsI2bxeFZD42f1/qjDw6FAZkNZcKRO94LIiT03fzOmcJ9e/tunxjVjA1Rl69ClmVJzz8H3G9CdKef10PA=="], "@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.35", "", { "dependencies": { "@aws-sdk/types": "^3.973.13", "@smithy/signature-v4": "^5.4.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-6L/VWs+Wch2stHemCGTmUNqKLMzURxQDK5boNG3Jn3kAOp71meDUuS5sbObpEvFxHDq0uWeSLFDNSYsjNt+Dlg=="], - "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1074.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.23", "@aws-sdk/nested-clients": "^3.997.23", "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-pv80IzgGW4RnXWtft692chZOM9i6PhebVsLCcnaM4dBEPZva2fE6FXAHs76G7Rc7s3yGyX/68G0nZMrUy+Vmpg=="], + "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1048.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.11", "@aws-sdk/nested-clients": "^3.997.9", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-k0y/GcuesuSfWyUM0WamrGyeZmltRYaPbHO82UDA6mZ/doB+FOHKutikPAtSXMn/hDz970cF+iRuuiYO9VEbAA=="], "@aws-sdk/types": ["@aws-sdk/types@3.973.13", "", { "dependencies": { "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-pEHZqRkAlHfnfAU9tK+WpKv/gBNjGJrHMgA3A0iYRGyswBS2t0pfez+lWlwktb3Bqa0ovh7w/QJTFwp3fDxLNg=="], @@ -1611,6 +1651,18 @@ "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], + "@earendil-works/chord": ["@earendil-works/chord@0.85.1", "", { "dependencies": { "esbuild": "0.28.1" } }, "sha512-VDlkEC3dhCzQ5fcyH1OhG19dq+6jCn+rqc/iXFivwDYGR5anwo2RCiXij9PpHhqNR5GuhhE+Er69Zi1Sn4eY6w=="], + + "@earendil-works/pi-agent-core": ["@earendil-works/pi-agent-core@0.85.1", "", { "dependencies": { "@earendil-works/chord": "^0.85.1", "@earendil-works/pi-ai": "^0.85.1", "@earendil-works/pi-telemetry": "^0.85.1", "diff": "8.0.4", "ignore": "7.0.5", "typebox": "1.3.7", "yaml": "2.9.0" } }, "sha512-hIXIP3eAWueAYiAl8aMvWCvvZ8Q5gT3Dip5bE5uJyIGh4+YlWRjtMLI4BaeoXoSs93zndjue61u1B/vhefLnuA=="], + + "@earendil-works/pi-ai": ["@earendil-works/pi-ai@0.85.1", "", { "dependencies": { "@anthropic-ai/sdk": "0.123.0", "@aws-sdk/client-bedrock-runtime": "3.1048.0", "@earendil-works/pi-telemetry": "^0.85.1", "@google/genai": "1.52.0", "@smithy/node-http-handler": "4.7.3", "http-proxy-agent": "7.0.2", "https-proxy-agent": "7.0.6", "openai": "6.40.0", "partial-json": "0.1.7", "typebox": "1.3.7" }, "bin": { "pi-ai": "dist/cli.js" } }, "sha512-+VgVIJDkDO2efYJKEEqvPTH4zmnIaXdAppGbO+vKFA9qy5PdhFiAenuFAkU+oiCSfOC4dMHDyrjdQeL4ZoC5CQ=="], + + "@earendil-works/pi-coding-agent": ["@earendil-works/pi-coding-agent@0.85.1", "", { "dependencies": { "@earendil-works/chord": "^0.85.1", "@earendil-works/pi-agent-core": "^0.85.1", "@earendil-works/pi-ai": "^0.85.1", "@earendil-works/pi-tui": "^0.85.1", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", "diff": "8.0.4", "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" }, "bin": { "pi": "dist/bundle/cli.js" } }, "sha512-FGRN+OHbWaefBPGaTggAdLjrIHW+s2PzLyglz/5dfLzb9of7uuXMXYC0fJIeZTw+shS32o2cuQ9jF7YSDuL/oQ=="], + + "@earendil-works/pi-telemetry": ["@earendil-works/pi-telemetry@0.85.1", "", {}, "sha512-Bg/YN6kA7Swja/NQxka8xFdecb4E/auIEGF2G5A25EaQXhRnPj300/7/KpgsDDMYUzHTDAv4RyUxaQPJKW81Rw=="], + + "@earendil-works/pi-tui": ["@earendil-works/pi-tui@0.85.1", "", { "dependencies": { "get-east-asian-width": "1.6.0", "marked": "18.0.5" } }, "sha512-OIzw9efInmO4WOBnD4TxcTdBjmzvYJpzslkgoUro946nEGoYWg5rwv1p4fDt3/JvMx9QybryUCUwlm7j8Dreig=="], + "@effect/atom-react": ["@effect/atom-react@4.0.0-beta.59", "", { "peerDependencies": { "effect": "^4.0.0-beta.59", "react": "^19.2.4", "scheduler": "*" } }, "sha512-VkznQz5c+Z/BLxX+hQNPzPOyUnLQjnbppFSNP7tbPru7HKR4ihzCDC6Xjbx87156MOrZ+JOa6shTMbmvGT5W0w=="], "@effect/language-service": ["@effect/language-service@0.85.1", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-EXnJjIy6zQ3nUO/MZ+ynWUb8B895KZPotd1++oTs9JjDkplwM7cb6zo8Zq2zU6piwq+KflO7amXbEfj1UMpHkw=="], @@ -1813,6 +1865,8 @@ "@executor-js/onboarding-demo": ["@executor-js/onboarding-demo@workspace:packages/onboarding-demo"], + "@executor-js/pi": ["@executor-js/pi@workspace:packages/hosts/pi"], + "@executor-js/plugin-desktop-settings": ["@executor-js/plugin-desktop-settings@workspace:packages/plugins/desktop-settings"], "@executor-js/plugin-encrypted-secrets": ["@executor-js/plugin-encrypted-secrets@workspace:packages/plugins/encrypted-secrets"], @@ -1869,6 +1923,8 @@ "@giscus/react": ["@giscus/react@3.1.0", "", { "dependencies": { "giscus": "^1.6.0" }, "peerDependencies": { "react": "^16 || ^17 || ^18 || ^19", "react-dom": "^16 || ^17 || ^18 || ^19" } }, "sha512-0TCO2TvL43+oOdyVVGHDItwxD1UMKP2ZYpT6gXmhFOqfAJtZxTzJ9hkn34iAF/b6YzyJ4Um89QIt9z/ajmAEeg=="], + "@google/genai": ["@google/genai@1.52.0", "", { "dependencies": { "google-auth-library": "^10.3.0", "p-retry": "^4.6.2", "protobufjs": "^7.5.4", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.2" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q=="], + "@graphql-tools/executor": ["@graphql-tools/executor@1.5.3", "", { "dependencies": { "@graphql-tools/utils": "^11.1.0", "@graphql-typed-document-node/core": "^3.2.0", "@repeaterjs/repeater": "^3.0.4", "@whatwg-node/disposablestack": "^0.0.6", "@whatwg-node/promise-helpers": "^1.0.0", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mgBFC0bsrZPZLu9EnydpMnAuQ8Iiq0CEbUcsmvXsm2/iYektGHDN/+bmb7hicA6dWZtdPfklYJmr21WD0GnOfA=="], "@graphql-tools/merge": ["@graphql-tools/merge@9.1.9", "", { "dependencies": { "@graphql-tools/utils": "^11.1.0", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-iHUWNjRHeQRYdgIMIuChThOwoKzA9vrzYeslgfBo5eUYEyHGZCoDPjAavssoYXLwstYt1dZj2J22jSzc2DrN0Q=="], @@ -2125,6 +2181,28 @@ "@manypkg/get-packages": ["@manypkg/get-packages@1.1.3", "", { "dependencies": { "@babel/runtime": "^7.5.5", "@changesets/types": "^4.0.1", "@manypkg/find-root": "^1.1.0", "fs-extra": "^8.1.0", "globby": "^11.0.0", "read-yaml-file": "^1.1.0" } }, "sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A=="], + "@mariozechner/clipboard": ["@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" } }, "sha512-ABnA53mdfkGZwOFUdZNv2S0CWGO/EIuPj8Vv9xmBFmSYg/qFc7ihO6q5FcQjvoE67kZpWkEc4AhD6B/os04yuA=="], + + "@mariozechner/clipboard-darwin-arm64": ["@mariozechner/clipboard-darwin-arm64@0.3.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BfgV7vCEWZwJwZJw03r6bP5+tf0iI/ANuQYCxi9RNn7FrWB3yzGuMKCrNLRl6V761vXRdL8+OqZ0wd4TqlsNOQ=="], + + "@mariozechner/clipboard-darwin-universal": ["@mariozechner/clipboard-darwin-universal@0.3.9", "", { "os": "darwin" }, "sha512-BGGR4iA9Z2shAjI65eI5xtyb3LYNlDW9X3gxKxDbqtbnREohsrqznov6zpKoIrsRWpzlYVEdKphS7ksJ0/ndSQ=="], + + "@mariozechner/clipboard-darwin-x64": ["@mariozechner/clipboard-darwin-x64@0.3.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-4kURmCbS6nt8uYhtmWpUcJWyPHfmAr5dTpXD1nO3pIfa+TSQ9DbrGOYCKH+aEFW47XhQ4Vp8ZTszie+wfFvDKg=="], + + "@mariozechner/clipboard-linux-arm64-gnu": ["@mariozechner/clipboard-linux-arm64-gnu@0.3.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-g59OkUGP2DDfCOIKypHeYgv2M55u/cKvXa5dSxFbEJ34XvIQMdcVmpKCkGUro3ZgefXiGVdwguvTMQGpHWzIXw=="], + + "@mariozechner/clipboard-linux-arm64-musl": ["@mariozechner/clipboard-linux-arm64-musl@0.3.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-AGuJdgKsmJdm4Pych7kv3sqe591ERRaAHW3xjLooiFzn8J+PxUyof++7YZrB5Y5tpnTO+K18Og3taj2NpluCRQ=="], + + "@mariozechner/clipboard-linux-riscv64-gnu": ["@mariozechner/clipboard-linux-riscv64-gnu@0.3.9", "", { "os": "linux", "cpu": "none" }, "sha512-DXBEAiuMpk7dhS1a9NzNxVAFi1vaKoPu7rQNgY8LIDLGrK3lnIp3nT10DUum+PKVJoJppIP+NAA8IZe4DMNDPw=="], + + "@mariozechner/clipboard-linux-x64-gnu": ["@mariozechner/clipboard-linux-x64-gnu@0.3.9", "", { "os": "linux", "cpu": "x64" }, "sha512-WORrMLd6EpElEME7JRKfSaY34nW1P5LbdgK5YNCS1ncG2LqmITsSMEJ8nh2mpvxb3TxqbOOKgY7k9eMJYlW9Mw=="], + + "@mariozechner/clipboard-linux-x64-musl": ["@mariozechner/clipboard-linux-x64-musl@0.3.9", "", { "os": "linux", "cpu": "x64" }, "sha512-/DHn+1DrfL6oRaPPWXaOKvonFFrni666fxd+zFqiQEfvBH0tsHVWjq9iqBk0oDp0qaPA72lIMy5BptxISBEhZQ=="], + + "@mariozechner/clipboard-win32-arm64-msvc": ["@mariozechner/clipboard-win32-arm64-msvc@0.3.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-O5FHD3ErkMwMhNzAfu3ggy0ug4z7btZuoQgwwxlzPrwV2bxlD6WDpqBY4NCgICAgZdDKdp+loUEKVAVt8aYnhQ=="], + + "@mariozechner/clipboard-win32-x64-msvc": ["@mariozechner/clipboard-win32-x64-msvc@0.3.9", "", { "os": "win32", "cpu": "x64" }, "sha512-ihQC3EufqEY81vhXBgVBtK4prL+wc62zJsSvxrgz7K1hsdt6OObz6v9p3Rn1OG3GJksTTKMJF0u/guMISHPhSA=="], + "@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="], "@mdx-js/react": ["@mdx-js/react@3.1.1", "", { "dependencies": { "@types/mdx": "^2.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw=="], @@ -2907,6 +2985,8 @@ "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], + "@silvia-odwyer/photon-node": ["@silvia-odwyer/photon-node@0.3.4", "", {}, "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA=="], + "@sindresorhus/is": ["@sindresorhus/is@4.6.0", "", {}, "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="], "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="], @@ -2925,7 +3005,7 @@ "@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], - "@smithy/node-http-handler": ["@smithy/node-http-handler@4.8.2", "", { "dependencies": { "@smithy/core": "^3.26.0", "@smithy/types": "^4.15.0", "tslib": "^2.6.2" } }, "sha512-wfl1uwrAqMH9/pi4kqBo5LBcFwrJLxuDLqL7p7qNcJIFcyZDUc6pzhYk4CYv+DP7fIUpQCZumwNnkhPKS52osQ=="], + "@smithy/node-http-handler": ["@smithy/node-http-handler@4.7.3", "", { "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA=="], "@smithy/signature-v4": ["@smithy/signature-v4@5.5.2", "", { "dependencies": { "@smithy/core": "^3.26.0", "@smithy/types": "^4.15.0", "tslib": "^2.6.2" } }, "sha512-7xHpmPY4rt0IOmeAA8EfjgEH8isT+587TCdy9H6a7d4OMi5CQ0oEHhWllunvPu4j4Cq0vTFwdxXN/kABWPjdyA=="], @@ -4121,7 +4201,7 @@ "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], - "get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="], + "get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="], "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], @@ -4177,6 +4257,8 @@ "graphql-yoga": ["graphql-yoga@5.21.0", "", { "dependencies": { "@envelop/core": "^5.5.1", "@envelop/instrumentation": "^1.0.0", "@graphql-tools/executor": "^1.5.0", "@graphql-tools/schema": "^10.0.11", "@graphql-tools/utils": "^10.11.0", "@graphql-yoga/logger": "^2.0.1", "@graphql-yoga/subscription": "^5.0.5", "@whatwg-node/fetch": "^0.10.6", "@whatwg-node/promise-helpers": "^1.3.2", "@whatwg-node/server": "^0.10.14", "lru-cache": "^10.0.0", "tslib": "^2.8.1" }, "peerDependencies": { "graphql": "^15.2.0 || ^16.0.0" } }, "sha512-PS37UoDihx8209RRl1ogttzWevNYDnGvP7beHkwHzUpUdfZTHsVRTVe1ysGXre1EjwUAePbpez302YSrq70Ngw=="], + "grok-mermaid": ["grok-mermaid@0.2.2", "", {}, "sha512-XcJEP5dDC8liHBh52mlLjU18fNvu1ckFsu0QpIG3+APZ270fsj9wxpiA6cOURmbUEuoMVgjbC2+UYgTdCqqgzA=="], + "gtoken": ["gtoken@8.0.0", "", { "dependencies": { "gaxios": "^7.0.0", "jws": "^4.0.0" } }, "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw=="], "h3": ["h3@1.15.11", "", { "dependencies": { "cookie-es": "^1.2.3", "crossws": "^0.3.5", "defu": "^6.1.6", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg=="], @@ -4229,11 +4311,13 @@ "hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="], + "highlight.js": ["highlight.js@10.7.3", "", {}, "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A=="], + "hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="], "hono": ["hono@4.12.14", "", {}, "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w=="], - "hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="], + "hosted-git-info": ["hosted-git-info@9.0.3", "", { "dependencies": { "lru-cache": "^11.1.0" } }, "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg=="], "html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="], @@ -4439,6 +4523,8 @@ "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], + "json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="], + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], @@ -4871,6 +4957,8 @@ "open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], + "openai": ["openai@6.40.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"] }, "sha512-MWtTjd/gQt4jpbji61NTgFWJLoY/PdRJ6wG9/ZDRMYNMlBKrCrSlkLI+KgHP1vR1qT6LKSAyAqIxno6lcK9JiA=="], + "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], "openid-client": ["openid-client@6.8.4", "", { "dependencies": { "jose": "^6.2.2", "oauth4webapi": "^3.8.5" } }, "sha512-QSw0BA08piujetEwfZsHoTrDpMEha7GDZDicQqVwX4u0ChCjefvjDB++TZ8BTg76UpwhzIQgdvvfgfl3HpCSAw=="], @@ -4939,6 +5027,8 @@ "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + "partial-json": ["partial-json@0.1.7", "", {}, "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA=="], + "partyserver": ["partyserver@0.5.10", "", { "dependencies": { "nanoid": "^5.1.9" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20260424.1 || ^5.20260703.1" } }, "sha512-t2B3mhTL1IOxCsj8rZgn4TxTuub7oLhypjc1/fGPNsbgnn9OsHms6uR3QEd5bFJ/7xrFo771j3DdUlll8cxgBg=="], "partysocket": ["partysocket@1.3.0", "", { "dependencies": { "event-target-polyfill": "^0.0.4" }, "peerDependencies": { "react": ">=17" }, "optionalPeers": ["react"] }, "sha512-1zToNyolZFK/7nuAw/K2bZrNzFqaZyRoCEkS+9vG6WSC5ikrN6qWRe96q6ImU51uptz2r+dAwSkwhJVdQi4LiA=="], @@ -5291,7 +5381,7 @@ "retext-stringify": ["retext-stringify@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "nlcst-to-string": "^4.0.0", "unified": "^11.0.0" } }, "sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA=="], - "retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="], + "retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], @@ -5609,6 +5699,8 @@ "truncate-utf8-bytes": ["truncate-utf8-bytes@1.0.2", "", { "dependencies": { "utf8-byte-length": "^1.0.1" } }, "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ=="], + "ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="], + "ts-dedent": ["ts-dedent@2.2.0", "", {}, "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ=="], "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], @@ -5635,6 +5727,8 @@ "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], + "typebox": ["typebox@1.3.28", "", {}, "sha512-5hTNlwzKj7BPPqhT91TPa4fK9bh/ZOei7mzweMpNidwLTxtduXHgr/OAbwx4mRW8aKFYu1I8fT1+0+PQtst1/g=="], + "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], "typeorm": ["typeorm@0.3.29", "", { "dependencies": { "@sqltools/formatter": "^1.2.5", "ansis": "^4.2.0", "app-root-path": "^3.1.0", "buffer": "^6.0.3", "dayjs": "^1.11.20", "debug": "^4.4.3", "dedent": "^1.7.2", "dotenv": "^16.6.1", "glob": "^10.5.0", "reflect-metadata": "^0.2.2", "sha.js": "^2.4.12", "sql-highlight": "^6.1.0", "tslib": "^2.8.1", "uuid": "^11.1.1", "yargs": "^17.7.2" }, "peerDependencies": { "@google-cloud/spanner": "^5.18.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@sap/hana-client": "^2.14.22", "better-sqlite3": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0", "ioredis": "^5.0.4", "mongodb": "^5.8.0 || ^6.0.0", "mssql": "^9.1.1 || ^10.0.0 || ^11.0.0 || ^12.0.0", "mysql2": "^2.2.5 || ^3.0.1", "oracledb": "^6.3.0", "pg": "^8.5.1", "pg-native": "^3.0.0", "pg-query-stream": "^4.0.0", "redis": "^3.1.1 || ^4.0.0 || ^5.0.14", "sql.js": "^1.4.0", "sqlite3": "^5.0.3", "ts-node": "^10.7.0", "typeorm-aurora-data-api-driver": "^2.0.0 || ^3.0.0" }, "optionalPeers": ["@google-cloud/spanner", "@sap/hana-client", "better-sqlite3", "ioredis", "mongodb", "mssql", "mysql2", "oracledb", "pg", "pg-native", "pg-query-stream", "redis", "sql.js", "sqlite3", "ts-node", "typeorm-aurora-data-api-driver"], "bin": { "typeorm": "cli.js", "typeorm-ts-node-esm": "cli-ts-node-esm.js", "typeorm-ts-node-commonjs": "cli-ts-node-commonjs.js" } }, "sha512-wwPEX/df4l72gCmOsrs0otJZYLGA9lLQkUZCkukbsymEycV4zXv2KM7wU7v2r8L01TaCgY9ApSSqHQWBOUhEoQ=="], @@ -5869,6 +5963,42 @@ "@astrojs/react/vite": ["vite@7.3.2", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg=="], + "@aws-sdk/client-bedrock-runtime/@smithy/node-http-handler": ["@smithy/node-http-handler@4.8.2", "", { "dependencies": { "@smithy/core": "^3.26.0", "@smithy/types": "^4.15.0", "tslib": "^2.6.2" } }, "sha512-wfl1uwrAqMH9/pi4kqBo5LBcFwrJLxuDLqL7p7qNcJIFcyZDUc6pzhYk4CYv+DP7fIUpQCZumwNnkhPKS52osQ=="], + + "@aws-sdk/client-s3/@smithy/node-http-handler": ["@smithy/node-http-handler@4.8.2", "", { "dependencies": { "@smithy/core": "^3.26.0", "@smithy/types": "^4.15.0", "tslib": "^2.6.2" } }, "sha512-wfl1uwrAqMH9/pi4kqBo5LBcFwrJLxuDLqL7p7qNcJIFcyZDUc6pzhYk4CYv+DP7fIUpQCZumwNnkhPKS52osQ=="], + + "@aws-sdk/client-sqs/@smithy/node-http-handler": ["@smithy/node-http-handler@4.8.2", "", { "dependencies": { "@smithy/core": "^3.26.0", "@smithy/types": "^4.15.0", "tslib": "^2.6.2" } }, "sha512-wfl1uwrAqMH9/pi4kqBo5LBcFwrJLxuDLqL7p7qNcJIFcyZDUc6pzhYk4CYv+DP7fIUpQCZumwNnkhPKS52osQ=="], + + "@aws-sdk/credential-provider-http/@smithy/node-http-handler": ["@smithy/node-http-handler@4.8.2", "", { "dependencies": { "@smithy/core": "^3.26.0", "@smithy/types": "^4.15.0", "tslib": "^2.6.2" } }, "sha512-wfl1uwrAqMH9/pi4kqBo5LBcFwrJLxuDLqL7p7qNcJIFcyZDUc6pzhYk4CYv+DP7fIUpQCZumwNnkhPKS52osQ=="], + + "@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1074.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.23", "@aws-sdk/nested-clients": "^3.997.23", "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-pv80IzgGW4RnXWtft692chZOM9i6PhebVsLCcnaM4dBEPZva2fE6FXAHs76G7Rc7s3yGyX/68G0nZMrUy+Vmpg=="], + + "@aws-sdk/eventstream-handler-node/@aws-sdk/types": ["@aws-sdk/types@3.974.5", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ=="], + + "@aws-sdk/eventstream-handler-node/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], + + "@aws-sdk/eventstream-handler-node/@smithy/types": ["@smithy/types@4.18.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CgB6HHWer/vrKps24ulRIbpcpb7K4xAU7SkZ7YHzBPlwHsvsrCJFEXK421s+cJzX+ZrqtA/TuU5w1HzI7k9N8A=="], + + "@aws-sdk/middleware-eventstream/@aws-sdk/types": ["@aws-sdk/types@3.974.5", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ=="], + + "@aws-sdk/middleware-eventstream/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], + + "@aws-sdk/middleware-eventstream/@smithy/types": ["@smithy/types@4.18.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CgB6HHWer/vrKps24ulRIbpcpb7K4xAU7SkZ7YHzBPlwHsvsrCJFEXK421s+cJzX+ZrqtA/TuU5w1HzI7k9N8A=="], + + "@aws-sdk/middleware-websocket/@aws-sdk/core": ["@aws-sdk/core@3.977.9", "", { "dependencies": { "@aws-sdk/types": "^3.974.5", "@aws-sdk/xml-builder": "^3.972.40", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.33.3", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.17.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA=="], + + "@aws-sdk/middleware-websocket/@aws-sdk/types": ["@aws-sdk/types@3.974.5", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ=="], + + "@aws-sdk/middleware-websocket/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], + + "@aws-sdk/middleware-websocket/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.8.0", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.18.0", "tslib": "^2.6.2" } }, "sha512-ycSJu3tFAQ4v04CBB0agqFMVsSQ1iG3yw+SpgxRqKfaURpQD4CZ8Wn0zPMmSnOuTpTh65Vz+EA0rMrw089wvkA=="], + + "@aws-sdk/middleware-websocket/@smithy/signature-v4": ["@smithy/signature-v4@5.7.3", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-7ImGm+FkHRLcBaRttIAMZ6bzJZWb2cJGoYjq46F2UjycujWzrL9GEN9h4w7eQyXJYnltrUhxbbieBAIRrdqpow=="], + + "@aws-sdk/middleware-websocket/@smithy/types": ["@smithy/types@4.18.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CgB6HHWer/vrKps24ulRIbpcpb7K4xAU7SkZ7YHzBPlwHsvsrCJFEXK421s+cJzX+ZrqtA/TuU5w1HzI7k9N8A=="], + + "@aws-sdk/nested-clients/@smithy/node-http-handler": ["@smithy/node-http-handler@4.8.2", "", { "dependencies": { "@smithy/core": "^3.26.0", "@smithy/types": "^4.15.0", "tslib": "^2.6.2" } }, "sha512-wfl1uwrAqMH9/pi4kqBo5LBcFwrJLxuDLqL7p7qNcJIFcyZDUc6pzhYk4CYv+DP7fIUpQCZumwNnkhPKS52osQ=="], + "@babel/core/@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="], "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], @@ -5945,6 +6075,24 @@ "@develar/schema-utils/ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], + "@earendil-works/chord/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="], + + "@earendil-works/pi-agent-core/typebox": ["typebox@1.3.7", "", {}, "sha512-meKuifc33Pccx0O6PdIzYMq3Og8zvP4TIi/a+Bw3AEMZMxOD0+RHGQvpglEe6Zdy3wZ8nqn/j95h8LUZLk/6Hg=="], + + "@earendil-works/pi-agent-core/yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], + + "@earendil-works/pi-ai/typebox": ["typebox@1.3.7", "", {}, "sha512-meKuifc33Pccx0O6PdIzYMq3Og8zvP4TIi/a+Bw3AEMZMxOD0+RHGQvpglEe6Zdy3wZ8nqn/j95h8LUZLk/6Hg=="], + + "@earendil-works/pi-coding-agent/semver": ["semver@7.8.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="], + + "@earendil-works/pi-coding-agent/typebox": ["typebox@1.3.7", "", {}, "sha512-meKuifc33Pccx0O6PdIzYMq3Og8zvP4TIi/a+Bw3AEMZMxOD0+RHGQvpglEe6Zdy3wZ8nqn/j95h8LUZLk/6Hg=="], + + "@earendil-works/pi-coding-agent/undici": ["undici@8.9.0", "", {}, "sha512-aWZpUj7XoGonMClx4gdDRfgBjqeA+F473aDmROQQbM9n6PRfK/u1q/a0X4wMTgcHfT8H6fpbt98PFuDUwFg2YA=="], + + "@earendil-works/pi-coding-agent/yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], + + "@earendil-works/pi-tui/marked": ["marked@18.0.5", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w=="], + "@effect/platform-node-shared/ws": ["ws@8.20.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="], "@electron/asar/commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="], @@ -6019,6 +6167,8 @@ "@fastify/otel/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.212.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.212.0", "import-in-the-middle": "^2.0.6", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IyXmpNnifNouMOe0I/gX7ENfv2ZCNdYTF0FpCsoBcpbIHzk81Ww9rQTYTnvghszCg7qGrIhNvWC8dhEifgX9Jg=="], + "@google/genai/ws": ["ws@8.20.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w=="], + "@graphql-tools/executor/@graphql-tools/utils": ["@graphql-tools/utils@11.1.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-PtFVG4r8Z2LEBSaPYQMusBiB3o6kjLVJyjCLbnWem/SpSuM21v6LTmgpkXfYU1qpBV2UGsFyuEnSJInl8fR1Ag=="], "@graphql-tools/merge/@graphql-tools/utils": ["@graphql-tools/utils@11.1.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-PtFVG4r8Z2LEBSaPYQMusBiB3o6kjLVJyjCLbnWem/SpSuM21v6LTmgpkXfYU1qpBV2UGsFyuEnSJInl8fR1Ag=="], @@ -6329,6 +6479,8 @@ "@slack/web-api/p-queue": ["p-queue@6.6.2", "", { "dependencies": { "eventemitter3": "^4.0.4", "p-timeout": "^3.2.0" } }, "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ=="], + "@slack/web-api/retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="], + "@tailwindcss/node/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="], @@ -6417,6 +6569,8 @@ "app-builder-lib/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], + "app-builder-lib/hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="], + "app-builder-lib/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], "app-builder-lib/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], @@ -6559,7 +6713,7 @@ "hoist-non-react-statics/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], - "hosted-git-info/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], + "hosted-git-info/lru-cache": ["lru-cache@11.3.5", "", {}, "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw=="], "htmlparser2/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], @@ -6583,6 +6737,8 @@ "ink-confirm-input/ink-text-input": ["ink-text-input@3.3.0", "", { "dependencies": { "chalk": "^3.0.0", "prop-types": "^15.5.10" }, "peerDependencies": { "ink": "^2.0.0", "react": "^16.5.2" } }, "sha512-gO4wrOf2ie3YuEARTIwGlw37lMjFn3Gk6CKIDrMlHb46WFMagZU7DplohjM24zynlqfnXA5UDEIfC2NBcvD8kg=="], + "is-fullwidth-code-point/get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="], + "jake/async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], "jsonwebtoken/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], @@ -6611,6 +6767,8 @@ "mermaid/uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], + "micromark-extension-cjk-friendly-util/get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="], + "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], "mimetext/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], @@ -6645,6 +6803,8 @@ "p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + "p-retry/retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="], + "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], @@ -6665,12 +6825,8 @@ "postject/commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="], - "promise-retry/retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], - "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], - "proper-lockfile/retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], - "proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], "protobufjs/@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="], @@ -6729,6 +6885,8 @@ "streamdown/marked": ["marked@17.0.6", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-gB0gkNafnonOw0obSTEGZTT86IuhILt2Wfx0mWH/1Au83kybTayroZ/V6nS25mN7u8ASy+5fMhgB3XPNrOZdmA=="], + "string-width/get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="], + "string-width/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], "string-width-cjs/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], @@ -6789,6 +6947,10 @@ "@astrojs/react/@vitejs/plugin-react/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="], + "@aws-sdk/middleware-websocket/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.40", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA=="], + + "@aws-sdk/middleware-websocket/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], + "@babel/helper-annotate-as-pure/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@8.0.0", "", {}, "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg=="], "@babel/helper-annotate-as-pure/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@8.0.4", "", {}, "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg=="], @@ -6923,6 +7085,58 @@ "@develar/schema-utils/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + "@earendil-works/chord/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="], + + "@earendil-works/chord/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="], + + "@earendil-works/chord/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="], + + "@earendil-works/chord/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="], + + "@earendil-works/chord/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="], + + "@earendil-works/chord/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="], + + "@earendil-works/chord/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="], + + "@earendil-works/chord/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="], + + "@earendil-works/chord/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="], + + "@earendil-works/chord/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="], + + "@earendil-works/chord/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="], + + "@earendil-works/chord/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="], + + "@earendil-works/chord/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="], + + "@earendil-works/chord/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="], + + "@earendil-works/chord/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="], + + "@earendil-works/chord/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="], + + "@earendil-works/chord/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="], + + "@earendil-works/chord/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="], + + "@earendil-works/chord/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="], + + "@earendil-works/chord/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="], + + "@earendil-works/chord/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="], + + "@earendil-works/chord/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="], + + "@earendil-works/chord/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="], + + "@earendil-works/chord/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="], + + "@earendil-works/chord/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="], + + "@earendil-works/chord/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="], + "@electron/asar/minimatch/brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="], "@electron/fuses/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], @@ -7075,6 +7289,8 @@ "@opentui/core/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + "@opentui/core/string-width/get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="], + "@opentui/core/string-width/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], "@opentui/core/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], @@ -7249,6 +7465,8 @@ "app-builder-lib/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + "app-builder-lib/hosted-git-info/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], + "app-builder-lib/which/isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="], "astro/@clack/prompts/@clack/core": ["@clack/core@1.2.0", "", { "dependencies": { "fast-wrap-ansi": "^0.1.3", "sisteransi": "^1.0.5" } }, "sha512-qfxof/3T3t9DPU/Rj3OmcFyZInceqj/NVtO9rwIuJqCUgh32gwPjpFQQp/ben07qKlhpwq7GzfWpST4qdJ5Drg=="], @@ -7437,8 +7655,6 @@ "har-validator/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], - "hosted-git-info/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], - "iconv-corefoundation/cli-truncate/slice-ansi": ["slice-ansi@3.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ=="], "iconv-corefoundation/cli-truncate/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -7579,6 +7795,8 @@ "wrap-ansi/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + "wrap-ansi/string-width/get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="], + "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], "yargs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], @@ -7739,6 +7957,8 @@ "@react-grab/cli/ora/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + "@react-grab/cli/ora/string-width/get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="], + "@react-grab/cli/ora/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], "@slack/web-api/axios/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], @@ -7749,8 +7969,12 @@ "agents/yargs/cliui/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + "agents/yargs/string-width/get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="], + "agents/yargs/string-width/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + "app-builder-lib/hosted-git-info/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + "astro/@clack/prompts/fast-string-width/fast-string-truncated-width": ["fast-string-truncated-width@1.2.1", "", {}, "sha512-Q9acT/+Uu3GwGj+5w/zsGuQjh9O1TyywhIwAxHudtWrgF09nHOPrvTLhQevPbttcxjr/SNN7mJmfOw/B1bXgow=="], "dir-compare/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], @@ -7793,6 +8017,8 @@ "agents/yargs/cliui/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + "agents/yargs/cliui/string-width/get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="], + "agents/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], "agents/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], diff --git a/e2e/local/pi-extension.test.ts b/e2e/local/pi-extension.test.ts new file mode 100644 index 0000000000..29f084ad29 --- /dev/null +++ b/e2e/local/pi-extension.test.ts @@ -0,0 +1,51 @@ +// The local half of the Pi setup the README documents. The hosted half is +// covered cross-target (scenarios/pi-extension.test.ts) with an API key in +// `EXECUTOR_API_KEY`; a local or desktop Executor has no API keys page, and +// hands out its own bearer token instead — `EXECUTOR_AUTH_TOKEN`. +// +// Nothing about that token is special to the extension, which is the point: the +// only way to know a local user can follow the README is to boot a real +// `executor web`, take the token it prints, and drive the installed extension +// against it exactly as the docs say. +import { expect } from "@effect/vitest"; +import { Effect } from "effect"; + +import { installPi, piToolText } from "../src/clients/pi"; +import { scenario } from "../src/scenario"; +import { Cli, RunDir } from "../src/services"; +import { withLocalServer } from "./local-server"; + +scenario( + "Local · the Pi extension reaches a local Executor with EXECUTOR_AUTH_TOKEN", + // Strictly greater than withLocalServer's 240s boot wait, so a stuck boot + // surfaces its terminal tail instead of vitest's generic timeout. + { timeout: 300_000 }, + Effect.gen(function* () { + const cli = yield* Cli; + const runDir = yield* RunDir; + + yield* withLocalServer(cli, runDir, (server) => + Effect.gen(function* () { + const pi = yield* Effect.promise(() => + installPi({ + EXECUTOR_MCP_URL: new URL("/mcp", server.origin).toString(), + EXECUTOR_AUTH_TOKEN: server.token, + }), + ); + yield* Effect.addFinalizer(() => Effect.promise(() => pi.close())); + + // `/executor` is the check a user runs after following the README. + const [status] = yield* Effect.promise(() => pi.runCommand("/executor")); + expect(status?.type, `the local server accepted the token: ${status?.message}`).toBe( + "info", + ); + expect(status?.message, "it reports the local endpoint").toContain(server.origin); + + const executed = yield* Effect.promise(() => + pi.callTool("executor_execute", { code: "return 21 * 2;" }), + ); + expect(piToolText(executed), "the local sandbox ran the code").toContain("42"); + }).pipe(Effect.scoped), + ); + }), +); diff --git a/e2e/package.json b/e2e/package.json index 6ddd66d3f3..cd6394d6e2 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -22,9 +22,11 @@ "motel": "MOTEL_OTEL_BASE_URL=http://127.0.0.1:4796 MOTEL_OTEL_DB_PATH=runs/.motel/telemetry.sqlite motel server" }, "dependencies": { + "@earendil-works/pi-coding-agent": "^0.85.1", "@executor-js/api": "workspace:*", "@executor-js/emulate": "^0.14.1", "@executor-js/mcporter": "^0.11.4", + "@executor-js/pi": "workspace:*", "@executor-js/plugin-graphql": "workspace:*", "@executor-js/plugin-mcp": "workspace:*", "@executor-js/plugin-openapi": "workspace:*", diff --git a/e2e/scenarios/pi-extension.test.ts b/e2e/scenarios/pi-extension.test.ts new file mode 100644 index 0000000000..6949871c9e --- /dev/null +++ b/e2e/scenarios/pi-extension.test.ts @@ -0,0 +1,83 @@ +// Cross-target: the published Pi extension against a real Executor. Nothing +// here is target-specific — the extension only needs an MCP endpoint and a +// bearer, which is exactly the claim worth testing on every target that serves +// them (cloud's org-scoped /{org}/mcp path included). +// +// The package is packed and installed through Pi's own package manager, then +// discovered, loaded, and wrapped by Pi's own loader (src/clients/pi.ts). So +// what runs is the whole user path — `pi install npm:@executor-js/pi`, the +// `pi` manifest, the default export, `/executor`, Pi's tool wrapper — with only +// the LLM left out: a model picks these tools, it does not make them work. +import { expect } from "@effect/vitest"; +import { Effect } from "effect"; + +import { installPi, piToolText } from "../src/clients/pi"; +import { scenario } from "../src/scenario"; +import { Mcp, Target } from "../src/services"; +import type { Identity } from "../src/target"; + +const emailOf = (identity: Identity): string => identity.credentials?.email ?? identity.label; + +scenario( + "Pi · the installed Executor extension drives execute, skills, and resume over MCP", + { timeout: 300_000 }, + Effect.gen(function* () { + const target = yield* Target; + const mcp = yield* Mcp; + const identity = yield* target.newIdentity(); + const bearer = yield* mcp.mintBearer(emailOf(identity)); + + // The environment a user copies out of Executor's Connect card and API + // Keys page — the extension's only configuration. + const pi = yield* Effect.promise(() => + installPi({ EXECUTOR_MCP_URL: mcp.url, EXECUTOR_API_KEY: bearer }), + ); + yield* Effect.addFinalizer(() => Effect.promise(() => pi.close())); + + expect( + pi.loadedExtensions.some((path) => path.includes("@executor-js/pi")), + "Pi installed the package and loaded its extension from node_modules", + ).toBe(true); + expect(pi.toolNames, "Executor's three tools joined Pi's registry").toEqual( + expect.arrayContaining(["executor_execute", "executor_skills", "executor_resume"]), + ); + + // `/executor` is the one thing a Pi user runs before trusting the install. + const [status] = yield* Effect.promise(() => pi.runCommand("/executor")); + expect(status?.type, "the connection check succeeded").toBe("info"); + expect(status?.message, "it reports the endpoint it resolved").toContain(mcp.url); + expect(status?.message, "pinned to the elicitation mode the resume schema assumes").toContain( + "elicitation_mode=model", + ); + expect(status?.message, "and the tools Executor actually serves").toContain("execute"); + + const executed = yield* Effect.promise(() => + pi.callTool("executor_execute", { code: "return 21 * 2;" }), + ); + expect(piToolText(executed), "the sandbox ran the code and returned it").toContain("42"); + + const documented = yield* Effect.promise(() => + pi.callTool("executor_skills", { name: "execute" }), + ); + expect( + piToolText(documented).length, + "the execute guide comes back for the model to read", + ).toBeGreaterThan(0); + + // Resume needs a paused execution to actually resume, which needs a policy + // that gates a tool — far more setup than this scenario is for. What is + // worth proving here is the part that silently breaks: that the server + // ACCEPTS the arguments this package advertises. A schema mismatch fails + // as an MCP argument-validation error; an accepted call fails on the + // unknown id instead, which is what we assert. + const refused = yield* Effect.promise(() => + pi.callTool("executor_resume", { executionId: "exec_does_not_exist", action: "accept" }).then( + () => "resolved", + (error: unknown) => (error instanceof Error ? error.message : String(error)), + ), + ); + expect(refused, "the server took the arguments and answered about the id").toContain( + "exec_does_not_exist", + ); + }).pipe(Effect.scoped), +); diff --git a/e2e/src/clients/pi.ts b/e2e/src/clients/pi.ts new file mode 100644 index 0000000000..8e25b76bb7 --- /dev/null +++ b/e2e/src/clients/pi.ts @@ -0,0 +1,244 @@ +// Drive the REAL Pi installation path for @executor-js/pi: pack the package the +// way the release does, hand the tarball to Pi's own package manager (what +// `pi install npm:…` runs), and let Pi discover, load, and wrap the extension +// itself. Everything past the `installAndPersist` call is Pi's code — npm +// install, the `pi.extensions` manifest, the jiti loader, the default-export +// call, the tool wrapper, the slash-command dispatcher. +// +// Pi needs an LLM provider only to run a turn. Installing, loading, registering +// tools, and dispatching `/executor` need none, so this stays headless: no +// model is configured, and the tools are invoked the way Pi's agent loop +// invokes them (AgentTool.execute) instead of through a model. +import { execFileSync } from "node:child_process"; +import { mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { + createAgentSession, + DefaultPackageManager, + DefaultResourceLoader, + SessionManager, + SettingsManager, + type AgentSession, + type ExtensionUIContext, +} from "@earendil-works/pi-coding-agent"; + +/** Pi's wrapped tool and its result, named off the session so this file needs + * no direct dependency on pi-agent-core. */ +type PiTool = NonNullable[number]; +type PiToolResult = Awaited>; + +const EXTENSION_PACKAGE_DIR = fileURLToPath( + new URL("../../../packages/hosts/pi/", import.meta.url), +); + +/** A message the extension pushed to Pi's UI (what `/executor` prints). */ +export interface PiNotice { + readonly message: string; + readonly type: "info" | "warning" | "error"; +} + +export interface PiHome { + /** Absolute paths of the extension files Pi loaded, from Pi's own loader. */ + readonly loadedExtensions: readonly string[]; + /** Every tool in Pi's registry after the extension registered its own. */ + readonly toolNames: readonly string[]; + /** Invoke a tool through Pi's wrapper, as Pi's agent loop does. */ + readonly callTool: ( + name: string, + params: Record, + signal?: AbortSignal, + ) => Promise; + /** Type a slash command into Pi and collect what the extension notified. */ + readonly runCommand: (text: string) => Promise; + /** Quit the session the way Pi does, so `session_shutdown` handlers run. */ + readonly close: () => Promise; +} + +/** The text a model would read off a tool result, images and the rest dropped. */ +export const piToolText = (result: PiToolResult): string => + result.content + .filter((part): part is Extract => part.type === "text") + .map((part) => part.text) + .join("\n"); + +/** + * The package's manifest, minus `devDependencies`. + * + * Packing outside the workspace means no lockfile, and the dev block's + * `catalog:` / `workspace:*` specifiers can only be resolved from one. Dropping + * it costs nothing that this test could observe — npm never installs a + * dependency's dev deps — while `dependencies` and `peerDependencies`, the two + * blocks npm acts on, are plain ranges and pass through untouched. Should a + * `workspace:*` ever land in one of those, `bun pm pack` fails here loudly + * rather than shipping something the tarball can't install. + */ +const stagedManifest = (): string => { + const manifest = JSON.parse( + readFileSync(join(EXTENSION_PACKAGE_DIR, "package.json"), "utf8"), + ) as Record; + delete manifest.devDependencies; + return `${JSON.stringify(manifest, null, 2)}\n`; +}; + +/** + * Build and pack the extension into a tarball, without touching the workspace. + * + * `bun run build` is deliberately NOT used: tsup runs with `clean: true`, so a + * second target packaging the same package concurrently (vitest runs projects + * in parallel) would have its `dist/` deleted mid-pack. Both compilers run with + * the package as cwd — that is where their configs and dependencies are — but + * emit into this run's own staging dir, which nothing else can reach. + * + * What lands in the archive is `files` applied to that build, i.e. the same + * artifact npm would serve. The release's `publishConfig.exports` rewrite is + * absent and irrelevant: Pi resolves the extension by path (`pi.extensions`), + * and the published `exports` map is what `scripts/smoke-test-packed.ts` covers. + */ +const packExtension = (destination: string): string => { + const staging = join(destination, "package"); + const dist = join(staging, "dist"); + mkdirSync(staging, { recursive: true }); + writeFileSync(join(staging, "package.json"), stagedManifest()); + + const run = (command: string, args: readonly string[]): void => { + execFileSync(join(EXTENSION_PACKAGE_DIR, "node_modules", ".bin", command), args, { + cwd: EXTENSION_PACKAGE_DIR, + stdio: "pipe", + }); + }; + run("tsup", ["--out-dir", dist]); + run("tsc", ["-p", "tsconfig.build.json", "--outDir", join(dist, "types")]); + + execFileSync("bun", ["pm", "pack", "--destination", destination], { + cwd: staging, + stdio: "pipe", + }); + const tarball = readdirSync(destination).find((entry) => entry.endsWith(".tgz")); + if (!tarball) throw new Error(`bun pm pack produced no tarball in ${destination}`); + return join(destination, tarball); +}; + +/** + * The extension reads its endpoint and token from the environment, and it runs + * in this process — so the environment IS this process's. Set before load, + * restored on close. + */ +const withEnv = (env: Record): (() => void) => { + const previous = new Map(Object.keys(env).map((key) => [key, process.env[key]])); + for (const [key, value] of Object.entries(env)) process.env[key] = value; + return () => { + for (const [key, value] of previous) { + if (value === undefined) delete process.env[key]; + else process.env[key] = value; + } + }; +}; + +/** + * Install the packed extension into a throwaway Pi and start a session with it. + * + * `installAndPersist` writes `npm:@executor-js/pi@…` into the agent dir's + * settings.json and npm-installs the tarball under it — the same two steps + * `pi install npm:@executor-js/pi` performs, against the same code. + */ +export const installPi = async (env: Record): Promise => { + const root = mkdtempSync(join(tmpdir(), "e2e-pi-")); + // Until the handle exists there is nothing for the scenario to hang a + // finalizer on, so everything past the temp dir's creation — the build, the + // pack, the install — undoes its own state on the way out. `restoreEnv` is + // only defined once the environment has actually been touched. + let restoreEnv: (() => void) | undefined; + try { + const tarball = packExtension(root); + restoreEnv = withEnv(env); + return await startPi(root, tarball, restoreEnv); + } catch (error) { + restoreEnv?.(); + rmSync(root, { recursive: true, force: true }); + throw error; + } +}; + +const startPi = async (root: string, tarball: string, restoreEnv: () => void): Promise => { + const agentDir = join(root, "agent"); + const projectDir = join(root, "project"); + for (const dir of [agentDir, projectDir]) mkdirSync(dir, { recursive: true }); + + const settingsManager = SettingsManager.create(projectDir, agentDir); + const packageManager = new DefaultPackageManager({ cwd: projectDir, agentDir, settingsManager }); + await packageManager.installAndPersist(`npm:@executor-js/pi@file:${tarball}`); + + // Pi's own resource discovery: read the installed packages out of settings, + // follow each package's `pi` manifest, load what it names. + const resourceLoader = new DefaultResourceLoader({ cwd: projectDir, agentDir, settingsManager }); + await resourceLoader.reload(); + const extensions = resourceLoader.getExtensions(); + if (extensions.errors.length > 0) { + throw new Error( + `Pi failed to load the extension: ${extensions.errors + .map((failure) => `${failure.path}: ${failure.error}`) + .join("; ")}`, + ); + } + + const { session } = await createAgentSession({ + cwd: projectDir, + agentDir, + settingsManager, + resourceLoader, + sessionManager: SessionManager.inMemory(projectDir), + }); + + const notices: PiNotice[] = []; + // Headless: there is no terminal to draw dialogs into, and the extension only + // ever notifies. Anything interactive would be a bug in the extension, so it + // is left off this context rather than stubbed into silence. + const ui: Pick = { + notify: (message, type) => notices.push({ message, type: type ?? "info" }), + }; + await session.bindExtensions({ uiContext: ui as ExtensionUIContext, mode: "print" }); + + const toolByName = (name: string): PiTool => { + const tool = session.state.tools?.find((candidate) => candidate.name === name); + if (!tool) throw new Error(`Pi has no tool named ${name}`); + return tool; + }; + + let calls = 0; + let closed = false; + + return { + loadedExtensions: extensions.extensions.map((extension) => extension.path), + toolNames: session.getAllTools().map((tool) => tool.name), + callTool: (name, params, signal) => { + calls += 1; + return toolByName(name).execute(`e2e-call-${calls}`, params, signal); + }, + runCommand: async (text) => { + const before = notices.length; + await session.prompt(text); + return notices.slice(before); + }, + // Idempotent, and each step cleans up after the one before it fails: a + // shutdown handler that throws must not leave this process holding the + // scenario's credentials or a stray Pi installation on disk. + close: async () => { + if (closed) return; + closed = true; + try { + try { + // What Pi emits on quit — the extension closes its MCP session here. + await session.extensionRunner.emit({ type: "session_shutdown", reason: "quit" }); + } finally { + session.dispose(); + } + } finally { + restoreEnv(); + rmSync(root, { recursive: true, force: true }); + } + }, + }; +}; diff --git a/package.json b/package.json index c284f45623..8c71a82fe7 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "test": "turbo run test --filter=!@executor-js/e2e ${TURBO_TEST_CONCURRENCY:+--concurrency=$TURBO_TEST_CONCURRENCY}", "test:e2e": "bun run --cwd e2e test", "test:release:bootstrap": "vitest run tests/release-bootstrap-smoke.test.ts", - "build:packages": "bun run --filter='@executor-js/fumadb' build && bun run --filter='@executor-js/codemode-core' build && bun run --filter='@executor-js/runtime-quickjs' build && bun run --filter='@executor-js/sdk' build && bun run --filter='@executor-js/config' build && bun run --filter='@executor-js/execution' build && bun run --filter='@executor-js/cli' build && bun run --filter='@executor-js/plugin-*' build", + "build:packages": "bun run --filter='@executor-js/fumadb' build && bun run --filter='@executor-js/codemode-core' build && bun run --filter='@executor-js/runtime-quickjs' build && bun run --filter='@executor-js/sdk' build && bun run --filter='@executor-js/config' build && bun run --filter='@executor-js/execution' build && bun run --filter='@executor-js/cli' build && bun run --filter='@executor-js/plugin-*' build && bun run --filter='@executor-js/pi' build", "typecheck": "turbo run typecheck", "typecheck:slow": "turbo run typecheck:slow", "ci": "bun run lint && bun run typecheck && bun run test", diff --git a/packages/hosts/pi/CHANGELOG.md b/packages/hosts/pi/CHANGELOG.md new file mode 100644 index 0000000000..bcc814438a --- /dev/null +++ b/packages/hosts/pi/CHANGELOG.md @@ -0,0 +1 @@ +# @executor-js/pi diff --git a/packages/hosts/pi/package.json b/packages/hosts/pi/package.json new file mode 100644 index 0000000000..59c8e449ea --- /dev/null +++ b/packages/hosts/pi/package.json @@ -0,0 +1,81 @@ +{ + "name": "@executor-js/pi", + "version": "0.0.0", + "description": "Executor extension for the Pi coding agent: Pi's tools, backed by your Executor catalog.", + "keywords": [ + "agent", + "executor", + "extension", + "mcp", + "pi", + "pi-coding-agent", + "pi-package" + ], + "homepage": "https://github.com/UsefulSoftwareCo/executor/tree/main/packages/hosts/pi", + "bugs": { + "url": "https://github.com/UsefulSoftwareCo/executor/issues" + }, + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/UsefulSoftwareCo/executor.git", + "directory": "packages/hosts/pi" + }, + "files": [ + "dist" + ], + "type": "module", + "exports": { + ".": "./src/extension.ts" + }, + "publishConfig": { + "access": "public", + "exports": { + ".": { + "import": { + "types": "./dist/types/extension.d.ts", + "default": "./dist/extensions/executor.js" + } + } + } + }, + "scripts": { + "build": "tsup && tsc -p tsconfig.build.json", + "typecheck": "tsgo --noEmit", + "test": "vitest run", + "test:watch": "vitest", + "typecheck:slow": "tsc --noEmit" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.29.0" + }, + "devDependencies": { + "@earendil-works/pi-ai": "^0.85.1", + "@earendil-works/pi-coding-agent": "^0.85.1", + "@effect/vitest": "catalog:", + "@executor-js/execution": "workspace:*", + "@executor-js/host-mcp": "workspace:*", + "@executor-js/plugin-mcp": "workspace:*", + "@types/node": "catalog:", + "bun-types": "catalog:", + "effect": "catalog:", + "tsup": "catalog:", + "typebox": "^1.3.7", + "typescript": "catalog:", + "vitest": "catalog:", + "zod": "4.3.6" + }, + "peerDependencies": { + "@earendil-works/pi-ai": "*", + "@earendil-works/pi-coding-agent": "*", + "typebox": "*" + }, + "engines": { + "node": ">=22.19.0" + }, + "pi": { + "extensions": [ + "./dist/extensions" + ] + } +} diff --git a/packages/hosts/pi/src/config.test.ts b/packages/hosts/pi/src/config.test.ts new file mode 100644 index 0000000000..58631f82c5 --- /dev/null +++ b/packages/hosts/pi/src/config.test.ts @@ -0,0 +1,81 @@ +import { describe, expect, it } from "@effect/vitest"; + +import { ENDPOINT_ENV_VAR, resolvePiExecutorConfig } from "./config"; + +const resolve = (env: Record) => resolvePiExecutorConfig(env); + +describe("resolvePiExecutorConfig", () => { + it("reports a missing endpoint instead of throwing, so startup survives it", () => { + const resolved = resolve({}); + expect(resolved.ok).toBe(false); + if (resolved.ok) return; + expect(resolved.reason, "the reason names the variable to set").toContain(ENDPOINT_ENV_VAR); + }); + + it("treats a blank endpoint as unset", () => { + expect(resolve({ [ENDPOINT_ENV_VAR]: " " }).ok).toBe(false); + }); + + it("rejects an endpoint that is not a URL", () => { + const resolved = resolve({ [ENDPOINT_ENV_VAR]: "127.0.0.1:4788/mcp" }); + expect(resolved.ok).toBe(false); + if (resolved.ok) return; + expect(resolved.reason).toContain("not a valid URL"); + }); + + it("pins elicitation_mode=model, because the resume schema depends on it", () => { + const resolved = resolve({ [ENDPOINT_ENV_VAR]: "https://executor.example/acme/mcp" }); + expect(resolved.ok).toBe(true); + if (!resolved.ok) return; + expect(resolved.config.endpoint).toBe( + "https://executor.example/acme/mcp?elicitation_mode=model", + ); + }); + + it("replaces a browser elicitation mode rather than trusting the pasted URL", () => { + const resolved = resolve({ + [ENDPOINT_ENV_VAR]: "https://executor.example/mcp?elicitation_mode=browser&artifacts=false", + }); + expect(resolved.ok).toBe(true); + if (!resolved.ok) return; + const url = new URL(resolved.config.endpoint); + expect(url.searchParams.get("elicitation_mode"), "browser mode is overridden").toBe("model"); + expect(url.searchParams.get("artifacts"), "unrelated options survive").toBe("false"); + }); + + it("prefers a hosted API key over a local server token, like the CLI does", () => { + const resolved = resolve({ + [ENDPOINT_ENV_VAR]: "https://executor.example/mcp", + EXECUTOR_API_KEY: "key_123", + EXECUTOR_AUTH_TOKEN: "token_456", + }); + expect(resolved.ok).toBe(true); + if (!resolved.ok) return; + expect(resolved.config.authorization).toBe("Bearer key_123"); + expect(resolved.config.tokenSource, "and the source is kept, for the rejection hint").toBe( + "EXECUTOR_API_KEY", + ); + }); + + it("falls back to the local server token", () => { + const resolved = resolve({ + [ENDPOINT_ENV_VAR]: "https://executor.example/mcp", + EXECUTOR_AUTH_TOKEN: "token_456", + }); + expect(resolved.ok).toBe(true); + if (!resolved.ok) return; + expect(resolved.config.authorization).toBe("Bearer token_456"); + expect(resolved.config.tokenSource).toBe("EXECUTOR_AUTH_TOKEN"); + }); + + it("carries no header when no token is configured", () => { + const resolved = resolve({ + [ENDPOINT_ENV_VAR]: "http://127.0.0.1:4788/mcp", + EXECUTOR_API_KEY: "", + }); + expect(resolved.ok).toBe(true); + if (!resolved.ok) return; + expect(resolved.config.authorization).toBeNull(); + expect(resolved.config.tokenSource).toBeNull(); + }); +}); diff --git a/packages/hosts/pi/src/config.ts b/packages/hosts/pi/src/config.ts new file mode 100644 index 0000000000..63271aadc1 --- /dev/null +++ b/packages/hosts/pi/src/config.ts @@ -0,0 +1,93 @@ +// --------------------------------------------------------------------------- +// Where this extension points, and how it authenticates. +// +// The environment is the only source. Pi installs this package from npm, so a +// credential in package config would be a credential in every user's +// node_modules; the issue asking for this integration called that out by name. +// --------------------------------------------------------------------------- + +/** Names the Executor MCP endpoint. Copy it from the Connect an agent card. */ +export const ENDPOINT_ENV_VAR = "EXECUTOR_MCP_URL"; + +/** + * Bearer sources, in the CLI's own precedence order (see + * `readCliServerAuth` in apps/cli/src/server-connection.ts): a hosted API key + * beats a local/desktop server token. + */ +export const TOKEN_ENV_VARS = ["EXECUTOR_API_KEY", "EXECUTOR_AUTH_TOKEN"] as const; + +/** Which variable a bearer came from. Hosted keys and local server tokens are + * minted in different places, so a rejected one has a different remedy. */ +export type PiTokenSource = (typeof TOKEN_ENV_VARS)[number]; + +export interface PiExecutorConfig { + /** Endpoint to POST MCP at, already pinned to model elicitation. */ + readonly endpoint: string; + /** Full `Authorization` header value, or null when no token is configured. */ + readonly authorization: string | null; + /** The variable that supplied `authorization`, or null when none did. */ + readonly tokenSource: PiTokenSource | null; +} + +export type ResolvedPiExecutorConfig = + | { readonly ok: true; readonly config: PiExecutorConfig } + | { readonly ok: false; readonly reason: string }; + +/** + * Pin `elicitation_mode=model`, replacing whatever the URL carried. + * + * The `resume` tool this extension registers has the shape Executor serves in + * MODEL mode (`executionId` + `action` + `content`). In browser mode the server + * registers a `resume` taking `executionId` alone, so a pasted + * `?elicitation_mode=browser` URL would leave us advertising two parameters the + * server rejects. Pinning the mode is what makes the fixed schema honest. + * Unrelated query parameters (`artifacts`, `search_tools`, …) are preserved. + */ +const pinModelElicitation = (url: URL): string => { + url.searchParams.set("elicitation_mode", "model"); + return url.toString(); +}; + +const firstToken = ( + env: Record, +): { readonly name: PiTokenSource; readonly value: string } | undefined => { + for (const name of TOKEN_ENV_VARS) { + const value = env[name]?.trim(); + if (value !== undefined && value.length > 0) return { name, value }; + } + return undefined; +}; + +/** + * Resolve configuration without throwing: extension startup must never fail on + * a missing or malformed endpoint. The reason is surfaced later, when a tool + * call or `/executor` actually needs a connection. + */ +export const resolvePiExecutorConfig = ( + env: Record, +): ResolvedPiExecutorConfig => { + const rawEndpoint = env[ENDPOINT_ENV_VAR]?.trim(); + if (rawEndpoint === undefined || rawEndpoint.length === 0) { + return { + ok: false, + reason: `${ENDPOINT_ENV_VAR} is not set. Copy the MCP URL from Executor's "Connect an agent" card, and set ${TOKEN_ENV_VARS[0]} to an API key from Executor's API Keys page — or, for a local or desktop Executor, ${TOKEN_ENV_VARS[1]} to that server's own bearer token.`, + }; + } + + // `URL.parse` returns null instead of throwing (Node >= 22.1; Pi requires + // >= 22.19), which keeps this module free of exception control flow. + const url = URL.parse(rawEndpoint); + if (url === null) { + return { ok: false, reason: `${ENDPOINT_ENV_VAR} is not a valid URL: ${rawEndpoint}` }; + } + + const token = firstToken(env); + return { + ok: true, + config: { + endpoint: pinModelElicitation(url), + authorization: token === undefined ? null : `Bearer ${token.value}`, + tokenSource: token?.name ?? null, + }, + }; +}; diff --git a/packages/hosts/pi/src/connection.ts b/packages/hosts/pi/src/connection.ts new file mode 100644 index 0000000000..e1faa1917d --- /dev/null +++ b/packages/hosts/pi/src/connection.ts @@ -0,0 +1,132 @@ +// --------------------------------------------------------------------------- +// The MCP connection to Executor. +// +// Opened on the first tool call, never at startup: Pi must start cleanly even +// when Executor is unreachable, misconfigured, or simply not running yet. +// --------------------------------------------------------------------------- + +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; +import { CallToolResultSchema, type CallToolResult } from "@modelcontextprotocol/sdk/types.js"; + +import { + TOKEN_ENV_VARS, + type PiExecutorConfig, + type PiTokenSource, + type ResolvedPiExecutorConfig, +} from "./config"; + +const CLIENT_INFO = { name: "executor-pi", version: "0.1.0" } as const; + +export interface ExecutorConnection { + readonly resolved: ResolvedPiExecutorConfig; + readonly callTool: ( + name: string, + args: Record, + signal: AbortSignal | undefined, + ) => Promise; + /** Tool names Executor currently serves. Used by `/executor` to prove reachability. */ + readonly listToolNames: () => Promise; + readonly close: () => Promise; +} + +/** + * What to do about a credential the server refused. The two variables are + * refilled from different places — a hosted Executor has an API Keys page, a + * local or desktop one hands out its own server token and has no such page — so + * the remedy follows the variable the token actually came from. + */ +const rejectedTokenHint = (source: PiTokenSource): string => + source === TOKEN_ENV_VARS[0] + ? `The API key in ${TOKEN_ENV_VARS[0]} was rejected — mint a fresh one on Executor's API Keys page.` + : `The token in ${TOKEN_ENV_VARS[1]} was rejected — copy your local or desktop server's current token again (the "Connect an agent" card's command carries it).`; + +const describeFailure = (error: unknown, config: PiExecutorConfig): Error => { + const detail = error instanceof Error ? error.message : String(error); + // Executor answers an unauthenticated MCP POST with an OAuth challenge, and + // the SDK surfaces it as `invalid_token` — often with no status code in the + // message at all. Match the vocabulary, not just the number. + const unauthorized = /\b(401|403)\b|unauthorized|forbidden|invalid_token/i.test(detail); + const hint = + config.tokenSource === null + ? `No token is set — put a hosted API key in ${TOKEN_ENV_VARS[0]}, or a local or desktop server's bearer token in ${TOKEN_ENV_VARS[1]}.` + : unauthorized + ? rejectedTokenHint(config.tokenSource) + : "A token was sent."; + return new Error(`Could not reach Executor at ${config.endpoint}. ${hint} (${detail})`); +}; + +export const createExecutorConnection = ( + resolved: ResolvedPiExecutorConfig, +): ExecutorConnection => { + // A single in-flight connect, so parallel tool calls share one MCP session + // rather than racing two. + let pending: Promise | null = null; + + const connect = async (config: PiExecutorConfig): Promise => { + const client = new Client(CLIENT_INFO); + const transport = new StreamableHTTPClientTransport(new URL(config.endpoint), { + requestInit: + config.authorization === null + ? undefined + : { headers: { Authorization: config.authorization } }, + }); + await client.connect(transport); + return client; + }; + + const connected = (): Promise => { + if (!resolved.ok) return Promise.reject(new Error(resolved.reason)); + const config = resolved.config; + if (pending === null) { + pending = connect(config).catch((error: unknown) => { + // A failed connect must not poison every later call: clear the memo so + // the next one retries (Executor may just have been starting up). + pending = null; + throw describeFailure(error, config); + }); + } + return pending; + }; + + /** + * Rejections here are transport or protocol failures, not tool failures — + * a failing Executor tool comes back as a result with `isError`. So a + * rejection means this session is suspect: drop it and let the next call + * reconnect, which is what makes an Executor restart survivable without + * restarting Pi. + */ + const withSession = async (use: (client: Client) => Promise): Promise => { + const client = await connected(); + try { + return await use(client); + } catch (error) { + pending = null; + void client.close().catch(() => undefined); + throw error; + } + }; + + return { + resolved, + callTool: (name, args, signal) => + // `callTool` types its result as the union of the modern shape and the + // 2024-10-07 `{ toolResult }` one, because the overload accepts either + // schema. Passing `CallToolResultSchema` is what actually decides it, so + // the modern branch is the only one that can arrive here. + withSession((client) => + client.callTool({ name, arguments: args }, CallToolResultSchema, { signal }), + ) as Promise, + listToolNames: () => + withSession(async (client) => { + const listed = await client.listTools(); + return listed.tools.map((tool) => tool.name); + }), + close: async () => { + const current = pending; + pending = null; + if (current === null) return; + await current.then((client) => client.close()).catch(() => undefined); + }, + }; +}; diff --git a/packages/hosts/pi/src/extension.test.ts b/packages/hosts/pi/src/extension.test.ts new file mode 100644 index 0000000000..ce9cdc546b --- /dev/null +++ b/packages/hosts/pi/src/extension.test.ts @@ -0,0 +1,316 @@ +// End-to-end for the bridge: a real MCP server over HTTP, the real MCP client, +// and the real tool definitions — only Pi itself is faked. + +import { describe, expect, it } from "@effect/vitest"; +import { Effect } from "effect"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; +import { serveMcpServer, TEST_IMAGE_PNG_BASE64 } from "@executor-js/plugin-mcp/testing"; +import z from "zod"; + +import { resolvePiExecutorConfig } from "./config"; +import { createExecutorConnection, type ExecutorConnection } from "./connection"; +import { describeConnection } from "./extension"; +import { registerExecutorTools } from "./tools"; + +const API_KEY = "key_123"; + +/** A stand-in for Executor: the same three tool names, scripted results. */ +const executorLikeServer = (): McpServer => { + const server = new McpServer({ name: "executor-fake", version: "1.0.0" }); + server.registerTool( + "execute", + { description: "run code", inputSchema: { code: z.string() } }, + ({ code }) => { + if (code === "boom") { + return { + content: [{ type: "text" as const, text: "Error: policy blocked" }], + isError: true, + }; + } + if (code === "screenshot") { + return { + content: [{ type: "image" as const, data: TEST_IMAGE_PNG_BASE64, mimeType: "image/png" }], + }; + } + return { content: [{ type: "text" as const, text: `ran: ${code}` }] }; + }, + ); + server.registerTool( + "skills", + { description: "docs", inputSchema: { name: z.string().optional() } }, + ({ name }) => ({ content: [{ type: "text" as const, text: `doc: ${name ?? "index"}` }] }), + ); + server.registerTool( + "resume", + { + description: "resume", + inputSchema: { + executionId: z.string(), + action: z.enum(["accept", "decline", "cancel"]), + content: z.string().default("{}"), + }, + }, + ({ executionId, action }) => ({ + content: [{ type: "text" as const, text: `${action}ed ${executionId}` }], + }), + ); + return server; +}; + +interface CapturedTool { + readonly name: string; + readonly call: ( + params: Record, + signal?: AbortSignal, + ) => Promise<{ readonly content: unknown }>; +} + +interface FakePi { + readonly api: ExtensionAPI; + readonly tools: Map; + readonly commands: string[]; + readonly events: string[]; +} + +const makeFakePi = (): FakePi => { + const tools = new Map(); + const commands: string[] = []; + const events: string[] = []; + const api: Pick = { + registerTool: (tool) => { + tools.set(tool.name, { + name: tool.name, + call: (params, signal) => + tool.execute("call-1", params as never, signal, undefined, {} as ExtensionContext), + }); + }, + registerCommand: (name) => { + commands.push(name); + }, + on: (event: string) => { + events.push(event); + }, + }; + return { api: api as ExtensionAPI, tools, commands, events }; +}; + +const callTool = ( + pi: FakePi, + name: string, + params: Record, + signal?: AbortSignal, +): Promise<{ readonly content: unknown }> => { + const tool = pi.tools.get(name); + if (tool === undefined) return Promise.reject(new Error(`${name} was never registered`)); + return tool.call(params, signal); +}; + +/** Resolve to the thrown message instead of the value, for failure assertions. */ +const failureOf = (work: Promise): Promise => + work.then( + () => "resolved", + (error: unknown) => (error instanceof Error ? error.message : String(error)), + ); + +/** Everything wired the way `executorExtension` wires it, minus Pi. `source` is + * the variable the bearer arrives in — hosted API key by default, the local + * server's own token when a scenario asks for it. */ +const connect = ( + endpoint: string | undefined, + token = API_KEY, + source: "EXECUTOR_API_KEY" | "EXECUTOR_AUTH_TOKEN" = "EXECUTOR_API_KEY", +): { pi: FakePi; connection: ExecutorConnection } => { + const pi = makeFakePi(); + const connection = createExecutorConnection( + resolvePiExecutorConfig( + endpoint === undefined ? {} : { EXECUTOR_MCP_URL: endpoint, [source]: token }, + ), + ); + registerExecutorTools(pi.api, connection); + return { pi, connection }; +}; + +const authorizedServer = () => + serveMcpServer(executorLikeServer, { + path: "/mcp", + auth: { + validateAuthorization: (authorization) => + Effect.succeed(authorization === `Bearer ${API_KEY}`), + }, + }); + +describe("the Pi extension", () => { + it.effect("registers its tools with no Executor reachable at all", () => + Effect.sync(() => { + const { pi } = connect(undefined); + // The whole point of registering from a literal: Pi still starts. + expect([...pi.tools.keys()]).toEqual([ + "executor_execute", + "executor_skills", + "executor_resume", + ]); + }), + ); + + it.effect("explains what to set when a call happens without configuration", () => + Effect.gen(function* () { + const { pi } = connect(undefined); + const failure = yield* Effect.promise(() => + failureOf(callTool(pi, "executor_execute", { code: "1 + 1" })), + ); + expect(failure).toContain("EXECUTOR_MCP_URL"); + }), + ); + + it.effect("round-trips a tool call over HTTP with the bearer attached", () => + Effect.scoped( + Effect.gen(function* () { + const server = yield* authorizedServer(); + const { pi } = connect(server.endpoint); + + const result = yield* Effect.promise(() => + callTool(pi, "executor_execute", { code: "1 + 1" }), + ); + expect(result.content).toEqual([{ type: "text", text: "ran: 1 + 1" }]); + + const requests = yield* server.requests; + expect( + requests.some((request) => request.authorization === `Bearer ${API_KEY}`), + "the API key reached the server", + ).toBe(true); + expect( + requests.some((request) => request.url.includes("elicitation_mode=model")), + "the endpoint is pinned to model elicitation", + ).toBe(true); + }), + ), + ); + + it.effect("sends a rejected hosted key to the API Keys page", () => + Effect.scoped( + Effect.gen(function* () { + const server = yield* authorizedServer(); + const { pi } = connect(server.endpoint, "wrong-key", "EXECUTOR_API_KEY"); + + const failure = yield* Effect.promise(() => + failureOf(callTool(pi, "executor_execute", { code: "1 + 1" })), + ); + expect(failure, "the failure is the rejection, not a missing token").toContain( + "was rejected", + ); + expect(failure, "names the variable that holds the bad key").toContain("EXECUTOR_API_KEY"); + expect(failure, "and where hosted keys come from").toContain("API Keys page"); + }), + ), + ); + + // A local or desktop Executor has no API Keys page: it hands out its own + // server token, so telling that user to mint an API key is a dead end. + it.effect("sends a rejected local server token back to the server's own token", () => + Effect.scoped( + Effect.gen(function* () { + const server = yield* authorizedServer(); + const { pi } = connect(server.endpoint, "wrong-token", "EXECUTOR_AUTH_TOKEN"); + + const failure = yield* Effect.promise(() => + failureOf(callTool(pi, "executor_execute", { code: "1 + 1" })), + ); + expect(failure, "the failure is the rejection, not a missing token").toContain( + "was rejected", + ); + expect(failure, "names the variable that holds the bad token").toContain( + "EXECUTOR_AUTH_TOKEN", + ); + expect(failure, "and does not send a local user to a page they do not have").not.toContain( + "API Keys page", + ); + }), + ), + ); + + it.effect("turns an isError result into a thrown error carrying the server's text", () => + Effect.scoped( + Effect.gen(function* () { + const server = yield* authorizedServer(); + const { pi } = connect(server.endpoint); + + // Pi has no error flag on results: only a throw marks the call failed. + const failure = yield* Effect.promise(() => + failureOf(callTool(pi, "executor_execute", { code: "boom" })), + ); + expect(failure).toBe("Error: policy blocked"); + }), + ), + ); + + it.effect("hands images to Pi rather than a placeholder", () => + Effect.scoped( + Effect.gen(function* () { + const server = yield* authorizedServer(); + const { pi } = connect(server.endpoint); + + const result = yield* Effect.promise(() => + callTool(pi, "executor_execute", { code: "screenshot" }), + ); + expect(result.content).toEqual([ + { type: "image", data: TEST_IMAGE_PNG_BASE64, mimeType: "image/png" }, + ]); + }), + ), + ); + + it.effect("passes the resume arguments the model-mode schema promises", () => + Effect.scoped( + Effect.gen(function* () { + const server = yield* authorizedServer(); + const { pi } = connect(server.endpoint); + + const result = yield* Effect.promise(() => + callTool(pi, "executor_resume", { executionId: "exec_1", action: "accept" }), + ); + expect(result.content).toEqual([{ type: "text", text: "accepted exec_1" }]); + }), + ), + ); + + it.effect("cancels an in-flight call when Pi aborts the turn", () => + Effect.scoped( + Effect.gen(function* () { + const server = yield* authorizedServer(); + const { pi } = connect(server.endpoint); + + const outcome = yield* Effect.promise(() => + callTool(pi, "executor_execute", { code: "1 + 1" }, AbortSignal.abort()).then( + () => "resolved", + () => "rejected", + ), + ); + expect(outcome).toBe("rejected"); + }), + ), + ); + + it.effect("`/executor` reports the endpoint and the tools it can see", () => + Effect.scoped( + Effect.gen(function* () { + const server = yield* authorizedServer(); + const { connection } = connect(server.endpoint); + + const report = yield* Effect.promise(() => describeConnection(connection)); + expect(report.type).toBe("info"); + expect(report.message).toContain("execute"); + yield* Effect.promise(() => connection.close()); + }), + ), + ); + + it.effect("`/executor` says what is missing when nothing is configured", () => + Effect.gen(function* () { + const { connection } = connect(undefined); + const report = yield* Effect.promise(() => describeConnection(connection)); + expect(report.type).toBe("error"); + expect(report.message).toContain("EXECUTOR_MCP_URL"); + }), + ); +}); diff --git a/packages/hosts/pi/src/extension.ts b/packages/hosts/pi/src/extension.ts new file mode 100644 index 0000000000..da8d069d66 --- /dev/null +++ b/packages/hosts/pi/src/extension.ts @@ -0,0 +1,69 @@ +// --------------------------------------------------------------------------- +// @executor-js/pi — Executor's tools, inside Pi. +// +// Pi ships no MCP client, so this extension is the bridge: it registers +// Executor's small `execute` / `skills` / `resume` surface as Pi tools and +// forwards each call over MCP. Configuration is environment-only; see config.ts. +// --------------------------------------------------------------------------- + +import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; + +import { ENDPOINT_ENV_VAR, resolvePiExecutorConfig } from "./config"; +import { createExecutorConnection, type ExecutorConnection } from "./connection"; +import { registerExecutorTools } from "./tools"; + +// Pi only needs the default export. The pieces are re-exported so each can be +// tested on its own — resolution, the connection, and the registered schemas — +// without standing up a Pi session for it. +export { + ENDPOINT_ENV_VAR, + TOKEN_ENV_VARS, + resolvePiExecutorConfig, + type PiExecutorConfig, + type PiTokenSource, + type ResolvedPiExecutorConfig, +} from "./config"; +export { createExecutorConnection, type ExecutorConnection } from "./connection"; +export { registerExecutorTools, TOOL_NAME_PREFIX } from "./tools"; + +export interface StatusReport { + readonly message: string; + readonly type: "info" | "error"; +} + +/** What `/executor` prints: where we point, and whether it actually answers. */ +export const describeConnection = async (connection: ExecutorConnection): Promise => { + const resolved = connection.resolved; + if (!resolved.ok) + return { message: `Executor is not configured. ${resolved.reason}`, type: "error" }; + + const auth = resolved.config.authorization === null ? "no token" : "token set"; + const target = `${resolved.config.endpoint} (${auth})`; + try { + const names = await connection.listToolNames(); + return { message: `Executor connected: ${target}\nTools: ${names.join(", ")}`, type: "info" }; + } catch (error) { + const detail = error instanceof Error ? error.message : String(error); + return { message: `Executor is unreachable: ${target}\n${detail}`, type: "error" }; + } +}; + +export default function executorExtension(pi: ExtensionAPI): void { + const connection = createExecutorConnection(resolvePiExecutorConfig(process.env)); + + registerExecutorTools(pi, connection); + + pi.registerCommand("executor", { + description: `Check this session's Executor connection (${ENDPOINT_ENV_VAR}).`, + handler: async (_args, ctx) => { + const report = await describeConnection(connection); + ctx.ui.notify(report.message, report.type); + }, + }); + + // Pi owns the process lifetime; close the session deterministically rather + // than leaving Executor to time it out. + pi.on("session_shutdown", async () => { + await connection.close(); + }); +} diff --git a/packages/hosts/pi/src/result.test.ts b/packages/hosts/pi/src/result.test.ts new file mode 100644 index 0000000000..c90f194287 --- /dev/null +++ b/packages/hosts/pi/src/result.test.ts @@ -0,0 +1,78 @@ +import { describe, expect, it } from "@effect/vitest"; +import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; + +import { resultText, toPiContent } from "./result"; + +const result = (content: CallToolResult["content"]): CallToolResult => ({ content }); + +describe("toPiContent", () => { + it("passes text through", () => { + expect(toPiContent(result([{ type: "text", text: "ran: 1 + 1" }]))).toEqual([ + { type: "text", text: "ran: 1 + 1" }, + ]); + }); + + it("passes images through — Pi renders them", () => { + expect(toPiContent(result([{ type: "image", data: "aGk=", mimeType: "image/png" }]))).toEqual([ + { type: "image", data: "aGk=", mimeType: "image/png" }, + ]); + }); + + it("keeps the text of an inline text resource", () => { + expect( + toPiContent( + result([ + { type: "resource", resource: { uri: "file:///notes.txt", text: "hello from a file" } }, + ]), + ), + ).toEqual([{ type: "text", text: "hello from a file" }]); + }); + + it("names what it dropped for content Pi cannot carry", () => { + const content = toPiContent( + result([ + { type: "audio", data: "aGk=", mimeType: "audio/mpeg" }, + { + type: "resource", + resource: { uri: "file:///clip.bin", mimeType: "application/octet-stream", blob: "aGk=" }, + }, + ]), + ); + // Executor precedes each binary block with its own "File output: …" line, + // so the model still knows what exists; only the bytes are gone. + expect(content).toEqual([ + { + type: "text", + text: "[audio/mpeg audio omitted — Pi tool results carry text and images only]", + }, + { + type: "text", + text: "[resource file:///clip.bin omitted — Pi tool results carry text and images only]", + }, + ]); + }); + + it("never returns empty content", () => { + expect(toPiContent(result([]))).toEqual([ + { type: "text", text: "Executor returned no content." }, + ]); + }); +}); + +describe("resultText", () => { + it("joins the text blocks, which is all a thrown error can carry", () => { + expect( + resultText( + result([ + { type: "text", text: "Error: policy blocked github.createIssue" }, + { type: "image", data: "aGk=", mimeType: "image/png" }, + { type: "text", text: "Ask the user to approve it." }, + ]), + ), + ).toBe("Error: policy blocked github.createIssue\nAsk the user to approve it."); + }); + + it("is empty when there is no text to report", () => { + expect(resultText(result([{ type: "image", data: "aGk=", mimeType: "image/png" }]))).toBe(""); + }); +}); diff --git a/packages/hosts/pi/src/result.ts b/packages/hosts/pi/src/result.ts new file mode 100644 index 0000000000..05962168d0 --- /dev/null +++ b/packages/hosts/pi/src/result.ts @@ -0,0 +1,51 @@ +// --------------------------------------------------------------------------- +// MCP tool results -> what Pi can put in front of a model. +// +// Pi's `AgentToolResult.content` is `(TextContent | ImageContent)[]`. There is +// no audio and no resource block, and — importantly — no error flag: the +// harness marks a call failed exactly when `execute` throws +// (pi-agent-core, harness/execution/tools.js). So an MCP `isError` result has +// to leave here as a thrown error carrying its text in the message, because +// the harness discards the content and keeps only the message. +// --------------------------------------------------------------------------- + +import type { ImageContent, TextContent } from "@earendil-works/pi-ai"; +import type { CallToolResult, ContentBlock } from "@modelcontextprotocol/sdk/types.js"; + +export type PiContent = TextContent | ImageContent; + +const text = (value: string): TextContent => ({ type: "text", text: value }); + +/** + * Executor already precedes every binary block with a `File output: name + * (mime, bytes)` line (see `outputFileContent` in + * packages/hosts/mcp/src/tool-server.ts), so dropping the payload costs the + * bytes and nothing else: the model still knows the file exists and what it is. + */ +const omitted = (what: string): TextContent => + text(`[${what} omitted — Pi tool results carry text and images only]`); + +const blockToPiContent = (block: ContentBlock): PiContent => { + if (block.type === "text") return text(block.text); + if (block.type === "image") return { type: "image", data: block.data, mimeType: block.mimeType }; + if (block.type === "audio") return omitted(`${block.mimeType} audio`); + if (block.type === "resource_link") return omitted(`resource ${block.uri}`); + // Text resources carry their content inline; binary ones carry a blob. + if ("text" in block.resource && typeof block.resource.text === "string") { + return text(block.resource.text); + } + return omitted(`resource ${block.resource.uri}`); +}; + +/** Flatten a result's text for a message the harness will keep. */ +export const resultText = (result: CallToolResult): string => + result.content + .filter((block): block is Extract => block.type === "text") + .map((block) => block.text) + .join("\n") + .trim(); + +export const toPiContent = (result: CallToolResult): PiContent[] => { + const content = result.content.map(blockToPiContent); + return content.length > 0 ? content : [text("Executor returned no content.")]; +}; diff --git a/packages/hosts/pi/src/schema.test.ts b/packages/hosts/pi/src/schema.test.ts new file mode 100644 index 0000000000..6c92eac1cf --- /dev/null +++ b/packages/hosts/pi/src/schema.test.ts @@ -0,0 +1,103 @@ +// --------------------------------------------------------------------------- +// The contract test. +// +// This package hand-writes TypeBox schemas for tools a DIFFERENT package +// serves. Nothing else in CI would notice if Executor renamed a parameter or +// made an optional one required — Pi would just start sending calls the server +// rejects. So: stand up the real Executor MCP server, ask it what it serves, +// and compare against what we register. +// +// Descriptions are deliberately NOT compared. Ours are thin on purpose (the +// long-form guidance lives behind `skills`), so only the call-breaking shape is +// asserted: parameter names, JSON types, enum values, and which are required. +// --------------------------------------------------------------------------- + +import { describe, expect, it } from "@effect/vitest"; +import { Effect } from "effect"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; +import type { ExecutionEngine } from "@executor-js/execution"; +import { createExecutorMcpServer } from "@executor-js/host-mcp/tool-server"; + +import { EXECUTE_PARAMETERS, RESUME_PARAMETERS, SKILLS_PARAMETERS } from "./tools"; + +/** The narrowest engine `createExecutorMcpServer` will accept. */ +const stubEngine = (): ExecutionEngine => ({ + execute: () => Effect.succeed({ result: "stub" }), + executeWithPause: () => Effect.succeed({ status: "completed", result: { result: "stub" } }), + resume: () => Effect.succeed(null), + isExecutionSettled: undefined, + getPausedExecution: () => Effect.succeed(null), + pausedExecutionCount: () => Effect.succeed(0), + hasPausedExecutions: () => Effect.succeed(false), + getDescription: Effect.succeed("contract-test executor"), + shutdown: Effect.void, +}); + +/** + * Model elicitation is what config.ts pins the endpoint to, and it is what + * makes `resume` take `action`/`content` rather than `executionId` alone. + */ +const withExecutorTools = async (assert: (tools: Map) => void): Promise => { + const server = await Effect.runPromise( + createExecutorMcpServer({ engine: stubEngine(), elicitationMode: { mode: "model" } }), + ); + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + const client = new Client({ name: "pi-contract-test", version: "1.0.0" }); + await server.connect(serverTransport); + await client.connect(clientTransport); + try { + const listed = await client.listTools(); + assert(new Map(listed.tools.map((tool) => [tool.name, tool.inputSchema]))); + } finally { + await clientTransport.close(); + await serverTransport.close(); + } +}; + +type SchemaShape = { + readonly properties: Record; + readonly required: readonly string[]; +}; + +const isRecord = (value: unknown): value is Record => + typeof value === "object" && value !== null; + +/** + * Reduce a JSON Schema to the part a caller can get wrong: each property's + * type (plus enum members, which are equally call-breaking), and the required + * set. TypeBox schemas are JSON Schema at runtime, so both sides go through + * this unchanged. + */ +const shapeOf = (schema: unknown): SchemaShape => { + expect(isRecord(schema), "schema should be an object").toBe(true); + const record = schema as Record; + const properties = isRecord(record.properties) ? record.properties : {}; + const described: Record = {}; + for (const [name, property] of Object.entries(properties)) { + if (!isRecord(property)) continue; + const type = typeof property.type === "string" ? property.type : "unknown"; + described[name] = Array.isArray(property.enum) + ? `${type}(${[...property.enum].sort().join("|")})` + : type; + } + const required = Array.isArray(record.required) + ? [...record.required].filter((name): name is string => typeof name === "string").sort() + : []; + return { properties: described, required }; +}; + +describe("the registered schemas match the tools Executor serves", () => { + it("execute, skills, and resume line up parameter for parameter", async () => { + await withExecutorTools((served) => { + expect( + [...served.keys()], + "Executor should still serve the three tools this package registers", + ).toEqual(expect.arrayContaining(["execute", "skills", "resume"])); + + expect(shapeOf(served.get("execute")), "execute").toEqual(shapeOf(EXECUTE_PARAMETERS)); + expect(shapeOf(served.get("skills")), "skills").toEqual(shapeOf(SKILLS_PARAMETERS)); + expect(shapeOf(served.get("resume")), "resume").toEqual(shapeOf(RESUME_PARAMETERS)); + }); + }); +}); diff --git a/packages/hosts/pi/src/tools.ts b/packages/hosts/pi/src/tools.ts new file mode 100644 index 0000000000..9bd3f33bb1 --- /dev/null +++ b/packages/hosts/pi/src/tools.ts @@ -0,0 +1,144 @@ +// --------------------------------------------------------------------------- +// Executor's core tool surface, as Pi tools. +// +// The set is fixed: `execute`, `skills`, and model-mode `resume` are what +// Executor serves by default, and registering them from a literal (rather than +// mirroring `tools/list` at startup) is what lets Pi start while Executor is +// down. `parameters` mirror the server's own input schemas — schema.test.ts +// asserts that against a real Executor MCP server, so drift fails CI. +// +// Descriptions stay deliberately thin. Executor's long-form guidance lives +// server-side behind `executor_skills`, so a server-side wording fix reaches +// Pi users without republishing this package. +// --------------------------------------------------------------------------- + +import { StringEnum } from "@earendil-works/pi-ai"; +import type { ExtensionAPI, ToolDefinition } from "@earendil-works/pi-coding-agent"; +import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import { + Type, + type Static, + type TObject, + type TOptional, + type TSchema, + type TString, + type TUnsafe, +} from "typebox"; + +import type { ExecutorConnection } from "./connection"; +import { resultText, toPiContent } from "./result"; + +/** Pi has no tool namespacing, so every tool carries the origin in its name. */ +export const TOOL_NAME_PREFIX = "executor_"; + +// The annotations keep the emitted .d.ts portable: without them TypeScript +// names typebox's inferred types through its install path. +export const EXECUTE_PARAMETERS: TObject<{ code: TString }> = Type.Object({ + code: Type.String({ + description: "JavaScript to run inside Executor's sandbox.", + }), +}); + +export const SKILLS_PARAMETERS: TObject<{ name: TOptional }> = Type.Object({ + name: Type.Optional( + Type.String({ + description: 'A doc from Executor\'s own catalog, e.g. "execute". Omit to list them.', + }), + ), +}); + +export const RESUME_PARAMETERS: TObject<{ + executionId: TString; + action: TUnsafe<"accept" | "decline" | "cancel">; + content: TOptional; +}> = Type.Object({ + executionId: Type.String({ + description: "The execution ID from the paused executor_execute result.", + }), + action: StringEnum(["accept", "decline", "cancel"] as const, { + description: "How to respond to the interaction.", + }), + content: Type.Optional( + Type.String({ + description: "JSON-encoded response content for form elicitations.", + }), + ), +}); + +/** The raw MCP result, kept on `details` for logs and custom renderers. */ +type ExecutorToolDetails = CallToolResult; + +const executorTool = (input: { + readonly tool: string; + readonly label: string; + readonly description: string; + readonly parameters: T; + readonly connection: ExecutorConnection; +}): ToolDefinition => ({ + name: `${TOOL_NAME_PREFIX}${input.tool}`, + label: input.label, + description: input.description, + parameters: input.parameters, + execute: async (_toolCallId, params, signal) => { + const result = await input.connection.callTool( + input.tool, + params as Record, + signal, + ); + // Pi marks a call failed only when `execute` throws, and keeps just the + // message — so the server's error text has to ride in it. See result.ts. + if (result.isError === true) { + const message = resultText(result); + throw new Error( + message.length > 0 ? message : `Executor's ${input.tool} tool reported an error.`, + ); + } + return { content: toPiContent(result), details: result }; + }, +}); + +export const registerExecutorTools = (pi: ExtensionAPI, connection: ExecutorConnection): void => { + pi.registerTool( + executorTool({ + tool: "execute", + label: "Executor", + description: [ + "Run JavaScript inside Executor to search your integration catalog and call the tools in it.", + 'Call `executor_skills({ name: "execute" })` first — it returns the guide for writing this code.', + ].join(" "), + parameters: EXECUTE_PARAMETERS, + connection, + }), + ); + + pi.registerTool( + executorTool({ + tool: "skills", + label: "Executor skills", + // Spelled out because Pi models have read this as a general skill reader + // before: github.com/UsefulSoftwareCo/executor/issues/1731. + description: [ + "Documentation for Executor's own tools — how to write code for `executor_execute`.", + "Not a general skill reader: it cannot reach Pi's skills, a SKILL.md on disk, or anything you authored.", + "Call with no name to list the few docs available.", + ].join(" "), + parameters: SKILLS_PARAMETERS, + connection, + }), + ); + + pi.registerTool( + executorTool({ + tool: "resume", + label: "Executor resume", + description: + "Resume an Executor execution that paused for approval, using the executionId from the paused executor_execute result.", + parameters: RESUME_PARAMETERS, + connection, + }), + ); +}; + +export type ExecuteParams = Static; +export type SkillsParams = Static; +export type ResumeParams = Static; diff --git a/packages/hosts/pi/tsconfig.build.json b/packages/hosts/pi/tsconfig.build.json new file mode 100644 index 0000000000..0216b09fc1 --- /dev/null +++ b/packages/hosts/pi/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "dist/types", + "rootDir": "src" + }, + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.test.ts"] +} diff --git a/packages/hosts/pi/tsconfig.json b/packages/hosts/pi/tsconfig.json new file mode 100644 index 0000000000..6b710f8bec --- /dev/null +++ b/packages/hosts/pi/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "lib": ["ES2022"], + "types": ["bun-types", "node"], + "noUnusedLocals": true, + "noImplicitOverride": true + }, + "include": ["src/**/*.ts", "tsup.config.ts", "vitest.config.ts"] +} diff --git a/packages/hosts/pi/tsup.config.ts b/packages/hosts/pi/tsup.config.ts new file mode 100644 index 0000000000..fa9bda4c4e --- /dev/null +++ b/packages/hosts/pi/tsup.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + // Pi discovers extensions from the directories named in package.json's `pi` + // key, so the built entry has to land inside `dist/extensions/`. + entry: { "extensions/executor": "src/extension.ts" }, + format: ["esm"], + // Declarations are emitted to `dist/types` by a separate tsc pass, NOT here. + // Pi loads every file in an extensions directory whose name ends in `.ts` or + // `.js` (pi-coding-agent, core/package-manager.js: `entry.name.endsWith(".ts") + // || entry.name.endsWith(".js")`), and `executor.d.ts` ends in `.ts` — it + // would be picked up and loaded as a second, broken extension. + dts: false, + clean: true, + sourcemap: true, + // Pi bundles these itself and requires them as `"*"` peers. + external: ["@earendil-works/pi-coding-agent", "@earendil-works/pi-ai", "typebox"], +}); diff --git a/packages/hosts/pi/vitest.config.ts b/packages/hosts/pi/vitest.config.ts new file mode 100644 index 0000000000..ae847ff6d9 --- /dev/null +++ b/packages/hosts/pi/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["src/**/*.test.ts"], + }, +}); diff --git a/scripts/publish-packages.ts b/scripts/publish-packages.ts index 62034b988c..61f56a62a4 100644 --- a/scripts/publish-packages.ts +++ b/scripts/publish-packages.ts @@ -41,6 +41,7 @@ const PUBLIC_PACKAGE_DIRS = [ "packages/plugins/mcp", "packages/plugins/onepassword", "packages/plugins/openapi", + "packages/hosts/pi", ] as const; const parseArgs = (argv: ReadonlyArray): { dryRun: boolean; prepareOnly: boolean } => { diff --git a/scripts/smoke-test-packed.ts b/scripts/smoke-test-packed.ts index 4273e2c4e3..bafcc080a3 100644 --- a/scripts/smoke-test-packed.ts +++ b/scripts/smoke-test-packed.ts @@ -47,6 +47,7 @@ const PUBLIC_PACKAGE_DIRS = [ "packages/plugins/mcp", "packages/plugins/onepassword", "packages/plugins/openapi", + "packages/hosts/pi", ] as const; type PackageJson = {