diff --git a/package-lock.json b/package-lock.json index bc63735b8..493016c9d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -126,6 +126,26 @@ "zod": "^4.0.0" } }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.90.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.90.0.tgz", + "integrity": "sha512-MzZtPabJF1b0FTDl6Z6H5ljphPwACLGP13lu8MTiB8jXaW/YXlpOp+Po2cVou3MPM5+f5toyLnul9whKCy7fBg==", + "license": "MIT", + "dependencies": { + "json-schema-to-ts": "^3.1.1" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, "node_modules/@aws-crypto/crc32": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", @@ -8443,31 +8463,31 @@ } }, "node_modules/@mariozechner/clipboard": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard/-/clipboard-0.3.2.tgz", - "integrity": "sha512-IHQpksNjo7EAtGuHFU+tbWDp5LarH3HU/8WiB9O70ZEoBPHOg0/6afwSLK0QyNMMmx4Bpi/zl6+DcBXe95nWYA==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard/-/clipboard-0.3.3.tgz", + "integrity": "sha512-e7jASirzfm+ROiOGFh843+cFZTy3DfzP+jldCvh8RnEk0C3QihDTn7dd7Yh7KAJydwIJ18FJSZ2swHvCJhk18g==", "license": "MIT", "optional": true, "engines": { "node": ">= 10" }, "optionalDependencies": { - "@mariozechner/clipboard-darwin-arm64": "0.3.2", - "@mariozechner/clipboard-darwin-universal": "0.3.2", - "@mariozechner/clipboard-darwin-x64": "0.3.2", - "@mariozechner/clipboard-linux-arm64-gnu": "0.3.2", - "@mariozechner/clipboard-linux-arm64-musl": "0.3.2", - "@mariozechner/clipboard-linux-riscv64-gnu": "0.3.2", - "@mariozechner/clipboard-linux-x64-gnu": "0.3.2", - "@mariozechner/clipboard-linux-x64-musl": "0.3.2", - "@mariozechner/clipboard-win32-arm64-msvc": "0.3.2", - "@mariozechner/clipboard-win32-x64-msvc": "0.3.2" + "@mariozechner/clipboard-darwin-arm64": "0.3.3", + "@mariozechner/clipboard-darwin-universal": "0.3.3", + "@mariozechner/clipboard-darwin-x64": "0.3.3", + "@mariozechner/clipboard-linux-arm64-gnu": "0.3.3", + "@mariozechner/clipboard-linux-arm64-musl": "0.3.3", + "@mariozechner/clipboard-linux-riscv64-gnu": "0.3.3", + "@mariozechner/clipboard-linux-x64-gnu": "0.3.3", + "@mariozechner/clipboard-linux-x64-musl": "0.3.3", + "@mariozechner/clipboard-win32-arm64-msvc": "0.3.3", + "@mariozechner/clipboard-win32-x64-msvc": "0.3.3" } }, "node_modules/@mariozechner/clipboard-darwin-arm64": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-arm64/-/clipboard-darwin-arm64-0.3.2.tgz", - "integrity": "sha512-uBf6K7Je1ihsgvmWxA8UCGCeI+nbRVRXoarZdLjl6slz94Zs1tNKFZqx7aCI5O1i3e0B6ja82zZ06BWrl0MCVw==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-arm64/-/clipboard-darwin-arm64-0.3.3.tgz", + "integrity": "sha512-+zhuZGXqVrdkbIRdnwiZNbTJ7V3elq/A+C5d5laJoyhJgWs41eO5NUMkBkj6f23F2L4PRXEhdn5/ktlPx+bG3Q==", "cpu": [ "arm64" ], @@ -8481,9 +8501,9 @@ } }, "node_modules/@mariozechner/clipboard-darwin-universal": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-universal/-/clipboard-darwin-universal-0.3.2.tgz", - "integrity": "sha512-mxSheKTW2U9LsBdXy0SdmdCAE5HqNS9QUmpNHLnfJ+SsbFKALjEZc5oRrVMXxGQSirDvYf5bjmRyT0QYYonnlg==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-universal/-/clipboard-darwin-universal-0.3.3.tgz", + "integrity": "sha512-x9aRfTyndVqpEQ44LNNCK/EXZd9y8rWkLQgNhmWpby9PXrjPhNxfjUc2Db4mt4nJjU/4zzO8F5v/XyzlUGSdhQ==", "license": "MIT", "optional": true, "os": [ @@ -8494,9 +8514,9 @@ } }, "node_modules/@mariozechner/clipboard-darwin-x64": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-x64/-/clipboard-darwin-x64-0.3.2.tgz", - "integrity": "sha512-U1BcVEoidvwIp95+HJswSW+xr28EQiHR7rZjH6pn8Sja5yO4Yoe3yCN0Zm8Lo72BbSOK/fTSq0je7CJpaPCspg==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-x64/-/clipboard-darwin-x64-0.3.3.tgz", + "integrity": "sha512-6ut/NawB0KiYPCwrirgNp6Br62LntL978q7G6d/Rs2pmPvQb53bP96eUMYl+Y3a7Qk13bGZ4w9rVPFxRE9m9ag==", "cpu": [ "x64" ], @@ -8510,9 +8530,9 @@ } }, "node_modules/@mariozechner/clipboard-linux-arm64-gnu": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-gnu/-/clipboard-linux-arm64-gnu-0.3.2.tgz", - "integrity": "sha512-BsinwG3yWTIjdgNCxsFlip7LkfwPk+ruw/aFCXHUg/fb5XC/Ksp+YMQ7u0LUtiKzIv/7LMXgZInJQH6gxbAaqQ==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-gnu/-/clipboard-linux-arm64-gnu-0.3.3.tgz", + "integrity": "sha512-gf3dH4kBddU1AOyHVB53mjLUFfJAKlTmxTMw51jdeg7eE7IjfEBXVvM4bifMtBxbWkT0eA0FUZ1C0KQ6Z5l6pw==", "cpu": [ "arm64" ], @@ -8526,9 +8546,9 @@ } }, "node_modules/@mariozechner/clipboard-linux-arm64-musl": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-musl/-/clipboard-linux-arm64-musl-0.3.2.tgz", - "integrity": "sha512-0/Gi5Xq2V6goXBop19ePoHvXsmJD9SzFlO3S+d6+T2b+BlPcpOu3Oa0wTjl+cZrLAAEzA86aPNBI+VVAFDFPKw==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-musl/-/clipboard-linux-arm64-musl-0.3.3.tgz", + "integrity": "sha512-o1paj2+zmAQ/LaPS85XJCxhNowNQpxYM2cGY6pWvB5Kqmz6hZjl6CzDg5tbf1hZkn/Em6jpOaE2UtMxKdELBDA==", "cpu": [ "arm64" ], @@ -8542,9 +8562,9 @@ } }, "node_modules/@mariozechner/clipboard-linux-riscv64-gnu": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-riscv64-gnu/-/clipboard-linux-riscv64-gnu-0.3.2.tgz", - "integrity": "sha512-2AFFiXB24qf0zOZsxI1GJGb9wQGlOJyN6UwoXqmKS3dpQi/l6ix30IzDDA4c4ZcCcx4D+9HLYXhC1w7Sov8pXA==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-riscv64-gnu/-/clipboard-linux-riscv64-gnu-0.3.3.tgz", + "integrity": "sha512-dkEhE4ekePJwMbBq9HP1//CFMNmDzA/iV9AXqBfvL5CWmmDIRXqh4A3YZt3tWO/HdMerX+xNCEiR7WiOsIG+UA==", "cpu": [ "riscv64" ], @@ -8558,9 +8578,9 @@ } }, "node_modules/@mariozechner/clipboard-linux-x64-gnu": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-gnu/-/clipboard-linux-x64-gnu-0.3.2.tgz", - "integrity": "sha512-v6fVnsn7WMGg73Dab8QMwyFce7tzGfgEixKgzLP8f1GJqkJZi5zO4k4FOHzSgUufgLil63gnxvMpjWkgfeQN7A==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-gnu/-/clipboard-linux-x64-gnu-0.3.3.tgz", + "integrity": "sha512-lT2yANtTLlEtFBIH3uGoRa/CQas/eBoLNi3qr9axQFoRgF4RGPSJ66yHOSnMECBneTIb1Iqv3UxokTfX27CdoQ==", "cpu": [ "x64" ], @@ -8574,9 +8594,9 @@ } }, "node_modules/@mariozechner/clipboard-linux-x64-musl": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-musl/-/clipboard-linux-x64-musl-0.3.2.tgz", - "integrity": "sha512-xVUtnoMQ8v2JVyfJLKKXACA6avdnchdbBkTsZs8BgJQo29qwCp5NIHAUO8gbJ40iaEGToW5RlmVk2M9V0HsHEw==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-musl/-/clipboard-linux-x64-musl-0.3.3.tgz", + "integrity": "sha512-saq/MCB0QHK/7ZZLjAZ0QkbY944dyjOsur8gneGCfMitt+GOiE1CU4OUipHC4b6x8UDY9bRLsR4aBaxu22OFPA==", "cpu": [ "x64" ], @@ -8590,9 +8610,9 @@ } }, "node_modules/@mariozechner/clipboard-win32-arm64-msvc": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-arm64-msvc/-/clipboard-win32-arm64-msvc-0.3.2.tgz", - "integrity": "sha512-AEgg95TNi8TGgak2wSXZkXKCvAUTjWoU1Pqb0ON7JHrX78p616XUFNTJohtIon3e0w6k0pYPZeCuqRCza/Tqeg==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-arm64-msvc/-/clipboard-win32-arm64-msvc-0.3.3.tgz", + "integrity": "sha512-cGuvSj0/2X2w983yEcKw+i+r1EBej6ZZIN+fXG3eY2G/HaIQpbXpLvMxKyZ9LKtbZx+Z6q/gELEoSBMLML6BaQ==", "cpu": [ "arm64" ], @@ -8606,9 +8626,9 @@ } }, "node_modules/@mariozechner/clipboard-win32-x64-msvc": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-x64-msvc/-/clipboard-win32-x64-msvc-0.3.2.tgz", - "integrity": "sha512-tGRuYpZwDOD7HBrCpyRuhGnHHSCknELvqwKKUG4JSfSB7JIU7LKRh6zx6fMUOQd8uISK35TjFg5UcNih+vJhFA==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-x64-msvc/-/clipboard-win32-x64-msvc-0.3.3.tgz", + "integrity": "sha512-5hvaEq/bgYovTIGx43O/S7loIHYV3ue90WcV1dz0wdMXroVKZKeU/yfwM0PALQA1OcrEHiGXGySFReXr72lGtA==", "cpu": [ "x64" ], @@ -8634,6 +8654,330 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/@mariozechner/pi-agent-core": { + "version": "0.70.2", + "resolved": "https://registry.npmjs.org/@mariozechner/pi-agent-core/-/pi-agent-core-0.70.2.tgz", + "integrity": "sha512-g1hIdKyDwmQOoBGO0R4OhpemKeMENeK0vE5FJtuQKqEcsdCAkVBgZAK6aZUARYZVxMA718JS6WPLFWoddzjD7g==", + "license": "MIT", + "dependencies": { + "@mariozechner/pi-ai": "^0.70.2", + "typebox": "^1.1.24" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@mariozechner/pi-ai": { + "version": "0.70.2", + "resolved": "https://registry.npmjs.org/@mariozechner/pi-ai/-/pi-ai-0.70.2.tgz", + "integrity": "sha512-+30LRPjXsXF+oI96DvGWMbdPGeqoLJvadh6UPev7wx2DzhC9FEqXkQcoMZ0usbCm7E9pl8ua8a9s/pQ5ikaUbg==", + "license": "MIT", + "dependencies": { + "@anthropic-ai/sdk": "^0.90.0", + "@aws-sdk/client-bedrock-runtime": "^3.1030.0", + "@google/genai": "^1.40.0", + "@mistralai/mistralai": "^2.2.0", + "chalk": "^5.6.2", + "openai": "6.26.0", + "partial-json": "^0.1.7", + "proxy-agent": "^6.5.0", + "typebox": "^1.1.24", + "undici": "^7.19.1", + "zod-to-json-schema": "^3.24.6" + }, + "bin": { + "pi-ai": "dist/cli.js" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@mariozechner/pi-ai/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@mariozechner/pi-ai/node_modules/openai": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.26.0.tgz", + "integrity": "sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==", + "license": "Apache-2.0", + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/@mariozechner/pi-coding-agent": { + "version": "0.70.2", + "resolved": "https://registry.npmjs.org/@mariozechner/pi-coding-agent/-/pi-coding-agent-0.70.2.tgz", + "integrity": "sha512-asfNqV89HKAmKvJ1wENBY/UQMIf77kLtkzBrvXnMQV4YbH7D/6KT+VeVzPG6zm5PAZP2UtdLY9B9Cge7IxH37w==", + "license": "MIT", + "dependencies": { + "@mariozechner/jiti": "^2.6.2", + "@mariozechner/pi-agent-core": "^0.70.2", + "@mariozechner/pi-ai": "^0.70.2", + "@mariozechner/pi-tui": "^0.70.2", + "@silvia-odwyer/photon-node": "^0.3.4", + "chalk": "^5.5.0", + "cli-highlight": "^2.1.11", + "diff": "^8.0.2", + "extract-zip": "^2.0.1", + "file-type": "^21.1.1", + "glob": "^13.0.1", + "hosted-git-info": "^9.0.2", + "ignore": "^7.0.5", + "marked": "^15.0.12", + "minimatch": "^10.2.3", + "proper-lockfile": "^4.1.2", + "strip-ansi": "^7.1.0", + "typebox": "^1.1.24", + "undici": "^7.19.1", + "uuid": "^14.0.0", + "yaml": "^2.8.2" + }, + "bin": { + "pi": "dist/cli.js" + }, + "engines": { + "node": ">=20.6.0" + }, + "optionalDependencies": { + "@mariozechner/clipboard": "^0.3.3" + } + }, + "node_modules/@mariozechner/pi-coding-agent/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@mariozechner/pi-coding-agent/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@mariozechner/pi-coding-agent/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@mariozechner/pi-coding-agent/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@mariozechner/pi-coding-agent/node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@mariozechner/pi-coding-agent/node_modules/hosted-git-info": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.2.tgz", + "integrity": "sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg==", + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@mariozechner/pi-coding-agent/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@mariozechner/pi-coding-agent/node_modules/lru-cache": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", + "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@mariozechner/pi-coding-agent/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@mariozechner/pi-coding-agent/node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@mariozechner/pi-coding-agent/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@mariozechner/pi-coding-agent/node_modules/uuid": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz", + "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/@mariozechner/pi-tui": { + "version": "0.70.2", + "resolved": "https://registry.npmjs.org/@mariozechner/pi-tui/-/pi-tui-0.70.2.tgz", + "integrity": "sha512-PtKC0NepnrYcqMx6MXkWTrBzC9tI62KeC6w940oT46lCbfvgmfqXciR15+9BZpxxc1H4jd3CMrKsmOPVeUqZ0A==", + "license": "MIT", + "dependencies": { + "@types/mime-types": "^2.1.4", + "chalk": "^5.5.0", + "get-east-asian-width": "^1.3.0", + "marked": "^15.0.12", + "mime-types": "^3.0.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "optionalDependencies": { + "koffi": "^2.9.0" + } + }, + "node_modules/@mariozechner/pi-tui/node_modules/@types/mime-types": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.4.tgz", + "integrity": "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==", + "license": "MIT" + }, + "node_modules/@mariozechner/pi-tui/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@mariozechner/pi-tui/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@mistralai/mistralai": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.1.tgz", + "integrity": "sha512-uKU8CZmL2RzYKmplsU01hii4p3pe4HqJefpWNRWXm1Tcm0Sm4xXfwSLIy4k7ZCPlbETCGcp69E7hZs+WOJ5itQ==", + "license": "Apache-2.0", + "dependencies": { + "ws": "^8.18.0", + "zod": "^3.25.0 || ^4.0.0", + "zod-to-json-schema": "^3.25.0" + } + }, "node_modules/@modelcontextprotocol/sdk": { "version": "1.27.1", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.27.1.tgz", @@ -36216,6 +36560,12 @@ "node": ">= 0.6" } }, + "node_modules/typebox": { + "version": "1.1.33", + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.33.tgz", + "integrity": "sha512-+/MWwlQ1q2GSVwoxi/+u5JsHkgLQKcCN2Nsjree9c+K7GJu40qbaHrFETmfV1i9Fs1TcOVfynW+jJvIWcXtvjw==", + "license": "MIT" + }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -38357,9 +38707,9 @@ "@getpaseo/highlight": "0.1.63-beta.1", "@getpaseo/relay": "0.1.63-beta.1", "@isaacs/ttlcache": "^2.1.4", - "@mariozechner/pi-agent-core": "^0.67.68", - "@mariozechner/pi-ai": "^0.67.68", - "@mariozechner/pi-coding-agent": "^0.67.68", + "@mariozechner/pi-agent-core": "^0.70.2", + "@mariozechner/pi-ai": "^0.70.2", + "@mariozechner/pi-coding-agent": "^0.70.2", "@modelcontextprotocol/sdk": "^1.20.1", "@opencode-ai/sdk": "1.2.6", "@sctg/sentencepiece-js": "^1.1.0", @@ -38401,26 +38751,6 @@ "vitest": "^3.2.4" } }, - "packages/server/node_modules/@anthropic-ai/sdk": { - "version": "0.90.0", - "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.90.0.tgz", - "integrity": "sha512-MzZtPabJF1b0FTDl6Z6H5ljphPwACLGP13lu8MTiB8jXaW/YXlpOp+Po2cVou3MPM5+f5toyLnul9whKCy7fBg==", - "license": "MIT", - "dependencies": { - "json-schema-to-ts": "^3.1.1" - }, - "bin": { - "anthropic-ai-sdk": "bin/cli" - }, - "peerDependencies": { - "zod": "^3.25.0 || ^4.0.0" - }, - "peerDependenciesMeta": { - "zod": { - "optional": true - } - } - }, "packages/server/node_modules/@isaacs/ttlcache": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@isaacs/ttlcache/-/ttlcache-2.1.4.tgz", @@ -38430,147 +38760,6 @@ "node": ">=12" } }, - "packages/server/node_modules/@mariozechner/pi-agent-core": { - "version": "0.67.68", - "resolved": "https://registry.npmjs.org/@mariozechner/pi-agent-core/-/pi-agent-core-0.67.68.tgz", - "integrity": "sha512-anwFuzeUL7Qjbyih4DWY7w1zrOTrBxaz1L6+duLUuuzpHOun0EiP4KWIGTXPT5oJA7ZaeRNTyXJ7PlWfGQG33g==", - "license": "MIT", - "dependencies": { - "@mariozechner/pi-ai": "^0.67.68" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "packages/server/node_modules/@mariozechner/pi-ai": { - "version": "0.67.68", - "resolved": "https://registry.npmjs.org/@mariozechner/pi-ai/-/pi-ai-0.67.68.tgz", - "integrity": "sha512-DWWQmcb3IV3mbGXmzYBScfKA6kA52n/stY029eiBikrIxVT7DGLG6n7KSvTA2R4qBSgi1iFL3nGHtwxmtIn6Lg==", - "license": "MIT", - "dependencies": { - "@anthropic-ai/sdk": "^0.90.0", - "@aws-sdk/client-bedrock-runtime": "^3.1030.0", - "@google/genai": "^1.40.0", - "@mistralai/mistralai": "^2.2.0", - "@sinclair/typebox": "^0.34.41", - "ajv": "^8.17.1", - "ajv-formats": "^3.0.1", - "chalk": "^5.6.2", - "openai": "6.26.0", - "partial-json": "^0.1.7", - "proxy-agent": "^6.5.0", - "undici": "^7.19.1", - "zod-to-json-schema": "^3.24.6" - }, - "bin": { - "pi-ai": "dist/cli.js" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "packages/server/node_modules/@mariozechner/pi-ai/node_modules/openai": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-6.26.0.tgz", - "integrity": "sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==", - "license": "Apache-2.0", - "bin": { - "openai": "bin/cli" - }, - "peerDependencies": { - "ws": "^8.18.0", - "zod": "^3.25 || ^4.0" - }, - "peerDependenciesMeta": { - "ws": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "packages/server/node_modules/@mariozechner/pi-coding-agent": { - "version": "0.67.68", - "resolved": "https://registry.npmjs.org/@mariozechner/pi-coding-agent/-/pi-coding-agent-0.67.68.tgz", - "integrity": "sha512-Bai2yUBpgjftqGvg3GNV9pxcMAatqJwQYsqM+7j39+tm+IpoW7hbMBnzXZQvykAwXIrTXpZFF5yp5ajeDI5Atg==", - "license": "MIT", - "dependencies": { - "@mariozechner/jiti": "^2.6.2", - "@mariozechner/pi-agent-core": "^0.67.68", - "@mariozechner/pi-ai": "^0.67.68", - "@mariozechner/pi-tui": "^0.67.68", - "@silvia-odwyer/photon-node": "^0.3.4", - "ajv": "^8.17.1", - "chalk": "^5.5.0", - "cli-highlight": "^2.1.11", - "diff": "^8.0.2", - "extract-zip": "^2.0.1", - "file-type": "^21.1.1", - "glob": "^13.0.1", - "hosted-git-info": "^9.0.2", - "ignore": "^7.0.5", - "marked": "^15.0.12", - "minimatch": "^10.2.3", - "proper-lockfile": "^4.1.2", - "strip-ansi": "^7.1.0", - "undici": "^7.19.1", - "uuid": "^11.1.0", - "yaml": "^2.8.2" - }, - "bin": { - "pi": "dist/cli.js" - }, - "engines": { - "node": ">=20.6.0" - }, - "optionalDependencies": { - "@mariozechner/clipboard": "^0.3.2" - } - }, - "packages/server/node_modules/@mariozechner/pi-coding-agent/node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/esm/bin/uuid" - } - }, - "packages/server/node_modules/@mariozechner/pi-tui": { - "version": "0.67.68", - "resolved": "https://registry.npmjs.org/@mariozechner/pi-tui/-/pi-tui-0.67.68.tgz", - "integrity": "sha512-RhcMaGz88lNOm5+9yx+YCIfXZALLbMxB2cwsoHzyOzs+OZAItw8tz6xJZPAnX0RHY+ENQEGMMDY9TF6pxxnkbA==", - "license": "MIT", - "dependencies": { - "@types/mime-types": "^2.1.4", - "chalk": "^5.5.0", - "get-east-asian-width": "^1.3.0", - "marked": "^15.0.12", - "mime-types": "^3.0.1" - }, - "engines": { - "node": ">=20.0.0" - }, - "optionalDependencies": { - "koffi": "^2.9.0" - } - }, - "packages/server/node_modules/@mistralai/mistralai": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.0.tgz", - "integrity": "sha512-JQUGIXjFWnw/J9LpTSf/ZXwVW3Sh8FBAcfTo5QvAHqkl4CfSiIwnjRJhMoAFcP6ncCe84YPU1ncDGX+p3OXnfg==", - "license": "Apache-2.0", - "dependencies": { - "ws": "^8.18.0", - "zod": "^3.25.0 || ^4.0.0", - "zod-to-json-schema": "^3.25.0" - } - }, "packages/server/node_modules/@modelcontextprotocol/sdk": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.20.1.tgz", @@ -38658,18 +38847,6 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "license": "MIT" }, - "packages/server/node_modules/@sinclair/typebox": { - "version": "0.34.49", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", - "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", - "license": "MIT" - }, - "packages/server/node_modules/@types/mime-types": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.4.tgz", - "integrity": "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==", - "license": "MIT" - }, "packages/server/node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -38699,23 +38876,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "packages/server/node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, "packages/server/node_modules/ansi-regex": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", @@ -38728,15 +38888,6 @@ "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "packages/server/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, "packages/server/node_modules/body-parser": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", @@ -38757,30 +38908,6 @@ "node": ">=18" } }, - "packages/server/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "packages/server/node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "packages/server/node_modules/content-disposition": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", @@ -38828,44 +38955,6 @@ "node": ">= 0.8" } }, - "packages/server/node_modules/glob": { - "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", - "license": "BlueOak-1.0.0", - "dependencies": { - "minimatch": "^10.2.2", - "minipass": "^7.1.3", - "path-scurry": "^2.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "packages/server/node_modules/hosted-git-info": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.2.tgz", - "integrity": "sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg==", - "license": "ISC", - "dependencies": { - "lru-cache": "^11.1.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "packages/server/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, "packages/server/node_modules/isexe": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", @@ -38875,15 +38964,6 @@ "node": ">=18" } }, - "packages/server/node_modules/lru-cache": { - "version": "11.3.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", - "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, "packages/server/node_modules/media-typer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", @@ -38917,21 +38997,6 @@ "node": ">= 0.6" } }, - "packages/server/node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.5" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "packages/server/node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", @@ -38956,22 +39021,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "packages/server/node_modules/path-scurry": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "packages/server/node_modules/raw-body": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", diff --git a/packages/app/src/components/provider-diagnostic-sheet.tsx b/packages/app/src/components/provider-diagnostic-sheet.tsx index 1c646c171..6aefebf19 100644 --- a/packages/app/src/components/provider-diagnostic-sheet.tsx +++ b/packages/app/src/components/provider-diagnostic-sheet.tsx @@ -1,4 +1,4 @@ -import { AlertCircle, RotateCw, Search } from "lucide-react-native"; +import { AlertCircle, Plus, RotateCw, Search, Trash2 } from "lucide-react-native"; import { useCallback, useEffect, useMemo, useState } from "react"; import { ActivityIndicator, @@ -13,11 +13,13 @@ import { AdaptiveModalSheet, AdaptiveTextInput } from "@/components/adaptive-mod import { LoadingSpinner } from "@/components/ui/loading-spinner"; import { isWeb } from "@/constants/platform"; import { Fonts } from "@/constants/theme"; +import { useDaemonConfig } from "@/hooks/use-daemon-config"; import { useProvidersSnapshot } from "@/hooks/use-providers-snapshot"; import { useHostRuntimeClient } from "@/runtime/host-runtime"; import { resolveProviderLabel } from "@/utils/provider-definitions"; import { formatTimeAgo } from "@/utils/time"; import type { AgentModelDefinition, AgentProvider } from "@server/server/agent/agent-sdk-types"; +import type { ProviderProfileModel } from "@server/server/agent/provider-launch-config"; interface ProviderDiagnosticSheetProps { provider: string; @@ -43,6 +45,182 @@ function ModelRow({ model, isFirst }: { model: AgentModelDefinition; isFirst: bo ); } +function AdditionalModelRow(props: { + model: ProviderProfileModel; + isFirst: boolean; + deleting: boolean; + onDelete: (modelId: string) => void; +}) { + const { theme } = useUnistyles(); + const { model, isFirst, deleting, onDelete } = props; + const rowStyle = useMemo( + () => [sheetStyles.additionalModelRow, !isFirst && sheetStyles.modelRowBorder], + [isFirst], + ); + const handleDelete = useCallback(() => onDelete(model.id), [model.id, onDelete]); + const deleteButtonStyle = useCallback( + ({ hovered, pressed }: PressableStateCallbackType & { hovered?: boolean }) => [ + sheetStyles.iconButton, + (Boolean(hovered) || pressed) && sheetStyles.iconButtonHovered, + deleting ? sheetStyles.disabled : null, + ], + [deleting], + ); + + return ( + + + + {model.label} + + + {model.id} + + + + + + + ); +} + +function AdditionalModelsEditor(props: { + provider: string; + serverId: string; + refresh: (providers?: AgentProvider[]) => Promise; +}) { + const { provider, serverId, refresh } = props; + const { theme } = useUnistyles(); + const { config, patchConfig } = useDaemonConfig(serverId); + const [input, setInput] = useState(""); + const [error, setError] = useState(null); + const [saving, setSaving] = useState(false); + const [deletingModelId, setDeletingModelId] = useState(null); + const providerConfig = config?.providers?.[provider]; + const additionalModels = useMemo( + () => providerConfig?.additionalModels ?? [], + [providerConfig?.additionalModels], + ); + const trimmedInput = input.trim(); + const canAdd = + trimmedInput.length > 0 && !additionalModels.some((model) => model.id === trimmedInput); + + const patchAdditionalModels = useCallback( + async (nextModels: ProviderProfileModel[]) => { + await patchConfig({ + providers: { + [provider]: { + additionalModels: nextModels, + }, + }, + }); + await refresh([provider as AgentProvider]); + }, + [patchConfig, provider, refresh], + ); + + const handleAdd = useCallback(() => { + if (!canAdd) { + return; + } + + setError(null); + setSaving(true); + void patchAdditionalModels([ + ...additionalModels, + { + id: trimmedInput, + label: trimmedInput, + }, + ]) + .then(() => setInput("")) + .catch((err) => { + setError(err instanceof Error ? err.message : "Failed to save model"); + }) + .finally(() => setSaving(false)); + }, [additionalModels, canAdd, patchAdditionalModels, trimmedInput]); + + const handleDelete = useCallback( + (modelId: string) => { + setError(null); + setDeletingModelId(modelId); + void patchAdditionalModels(additionalModels.filter((model) => model.id !== modelId)) + .catch((err) => { + setError(err instanceof Error ? err.message : "Failed to delete model"); + }) + .finally(() => { + setDeletingModelId((current) => (current === modelId ? null : current)); + }); + }, + [additionalModels, patchAdditionalModels], + ); + + const addButtonStyle = useCallback( + ({ hovered, pressed }: PressableStateCallbackType & { hovered?: boolean }) => [ + sheetStyles.addModelButton, + (Boolean(hovered) || pressed) && sheetStyles.addModelButtonHovered, + !canAdd || saving ? sheetStyles.disabled : null, + ], + [canAdd, saving], + ); + + return ( + + Additional Models + + + + + + {saving ? ( + + ) : ( + + )} + + + {error ? {error} : null} + {additionalModels.length > 0 ? ( + + {additionalModels.map((model, index) => ( + + ))} + + ) : null} + + ); +} + function DiagnosticCodeBlock(props: { loading: boolean; diagnostic: string | null; @@ -255,6 +433,8 @@ export function ProviderDiagnosticSheet({ + + Models @@ -308,6 +488,10 @@ const sheetStyles = StyleSheet.create((theme) => ({ fontSize: theme.fontSize.sm, color: theme.colors.foregroundMuted, }, + errorText: { + fontSize: theme.fontSize.xs, + color: theme.colors.destructive, + }, iconButton: { width: 30, height: 30, @@ -380,6 +564,50 @@ const sheetStyles = StyleSheet.create((theme) => ({ borderRadius: theme.borderRadius.md, paddingHorizontal: theme.spacing[3], }, + addModelRow: { + flexDirection: "row", + alignItems: "center", + gap: theme.spacing[2], + }, + addModelInputContainer: { + flex: 1, + minWidth: 0, + backgroundColor: theme.colors.surface2, + borderRadius: theme.borderRadius.md, + paddingHorizontal: theme.spacing[3], + }, + addModelInput: { + paddingVertical: theme.spacing[2], + color: theme.colors.foreground, + fontSize: theme.fontSize.sm, + }, + addModelButton: { + width: 34, + height: 34, + borderRadius: theme.borderRadius.full, + alignItems: "center", + justifyContent: "center", + backgroundColor: theme.colors.accent, + }, + addModelButtonHovered: { + backgroundColor: theme.colors.accentBright, + }, + additionalModelsList: { + borderTopWidth: 1, + borderTopColor: theme.colors.border, + }, + additionalModelRow: { + minHeight: 48, + paddingVertical: theme.spacing[2], + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + gap: theme.spacing[2], + }, + additionalModelText: { + flex: 1, + minWidth: 0, + }, searchInput: { flex: 1, paddingVertical: theme.spacing[2], @@ -419,3 +647,7 @@ const sheetStyles = StyleSheet.create((theme) => ({ const DIAGNOSTIC_SHEET_SNAP_POINTS = ["50%", "85%"]; const DIAGNOSTIC_SEARCH_INPUT_STYLE = [sheetStyles.searchInput, isWeb && { outlineStyle: "none" }]; +const DIAGNOSTIC_ADD_MODEL_INPUT_STYLE = [ + sheetStyles.addModelInput, + isWeb && { outlineStyle: "none" }, +]; diff --git a/packages/server/package.json b/packages/server/package.json index 3b5310be8..3f2665f4a 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -61,9 +61,9 @@ "@getpaseo/highlight": "0.1.63-beta.1", "@getpaseo/relay": "0.1.63-beta.1", "@isaacs/ttlcache": "^2.1.4", - "@mariozechner/pi-agent-core": "^0.67.68", - "@mariozechner/pi-ai": "^0.67.68", - "@mariozechner/pi-coding-agent": "^0.67.68", + "@mariozechner/pi-agent-core": "^0.70.2", + "@mariozechner/pi-ai": "^0.70.2", + "@mariozechner/pi-coding-agent": "^0.70.2", "@modelcontextprotocol/sdk": "^1.20.1", "@opencode-ai/sdk": "1.2.6", "@sctg/sentencepiece-js": "^1.1.0", diff --git a/packages/server/src/server/agent/agent-loading.ts b/packages/server/src/server/agent/agent-loading.ts index 01ae00630..6e434e905 100644 --- a/packages/server/src/server/agent/agent-loading.ts +++ b/packages/server/src/server/agent/agent-loading.ts @@ -7,6 +7,7 @@ import { buildConfigOverrides, buildSessionConfig, extractTimestamps, + isStoredAgentProviderAvailable, toAgentPersistenceHandle, } from "../persistence-hooks.js"; @@ -40,7 +41,11 @@ export async function ensureAgentLoaded( } const validProviders = deps.validProviders ?? deps.agentManager.getRegisteredProviderIds(); - const handle = toAgentPersistenceHandle(deps.logger, validProviders, record.persistence); + if (!isStoredAgentProviderAvailable(record, validProviders)) { + throw new Error(`Agent ${agentId} references unavailable provider '${record.provider}'`); + } + + const handle = toAgentPersistenceHandle(validProviders, record.persistence); let snapshot: ManagedAgent; if (handle) { @@ -54,7 +59,6 @@ export async function ensureAgentLoaded( } else { const config = buildSessionConfig(record, { validProviders, - logger: deps.logger, }); if (!config) { throw new Error(`Agent ${agentId} references unavailable provider '${record.provider}'`); diff --git a/packages/server/src/server/agent/agent-manager.test.ts b/packages/server/src/server/agent/agent-manager.test.ts index 6f0dc54e9..0ce7a2200 100644 --- a/packages/server/src/server/agent/agent-manager.test.ts +++ b/packages/server/src/server/agent/agent-manager.test.ts @@ -78,6 +78,11 @@ class TestAgentClient implements AgentClient { id: "gpt-5.4-mini", label: "GPT-5.4 Mini", }, + { + provider: "codex", + id: "gpt-5.2-codex", + label: "GPT-5.2 Codex", + }, ]; } @@ -787,6 +792,86 @@ test("createAgent fails when cwd does not exist", async () => { ).rejects.toThrow("Working directory does not exist"); }); +test("createAgent reports configured providers when provider is unknown", async () => { + const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-")); + const storagePath = join(workdir, "agents"); + const storage = new AgentStorage(storagePath, logger); + const manager = new AgentManager({ + clients: { + codex: new TestAgentClient(), + }, + registry: storage, + logger, + }); + + await expect( + manager.createAgent({ + provider: "missing-provider", + cwd: workdir, + }), + ).rejects.toThrow("Unknown provider 'missing-provider'. Configured providers: codex."); +}); + +test("createAgent reports available providers when selected provider is unavailable", async () => { + const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-")); + const storagePath = join(workdir, "agents"); + const storage = new AgentStorage(storagePath, logger); + + class UnavailableCodexClient extends TestAgentClient { + override async isAvailable(): Promise { + return false; + } + } + + const manager = new AgentManager({ + clients: { + codex: new UnavailableCodexClient(), + claude: new TestAgentClient(), + }, + registry: storage, + logger, + }); + + await expect( + manager.createAgent({ + provider: "codex", + cwd: workdir, + }), + ).rejects.toThrow( + "Provider 'codex' is not available. Available providers: claude. Use one of those providers, or install/configure 'codex'.", + ); +}); + +test("createAgent passes explicit model strings through to the provider", async () => { + const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-")); + const storagePath = join(workdir, "agents"); + const storage = new AgentStorage(storagePath, logger); + class CaptureModelClient extends TestAgentClient { + lastConfig: AgentSessionConfig | null = null; + + override async createSession(config: AgentSessionConfig): Promise { + this.lastConfig = config; + return new TestAgentSession(config); + } + } + const client = new CaptureModelClient(); + const manager = new AgentManager({ + clients: { + codex: client, + }, + registry: storage, + logger, + }); + + await manager.createAgent({ + provider: "codex", + cwd: workdir, + model: "not-a-real-model", + }); + + expect(client.lastConfig?.model).toBe("not-a-real-model"); +}); + test("resumeAgentFromPersistence keeps metadata config, applies overrides, and passes launch env", async () => { const workdir = mkdtempSync(join(tmpdir(), "agent-manager-resume-")); const storagePath = join(workdir, "agents"); diff --git a/packages/server/src/server/agent/agent-manager.ts b/packages/server/src/server/agent/agent-manager.ts index 31f6bdfd6..cf39b7f4e 100644 --- a/packages/server/src/server/agent/agent-manager.ts +++ b/packages/server/src/server/agent/agent-manager.ts @@ -61,6 +61,10 @@ interface TimeoutOptions { onLateError?: (error: unknown) => void; } +function formatProviderList(providers: readonly string[]): string { + return providers.length > 0 ? providers.join(", ") : "none"; +} + export { AGENT_LIFECYCLE_STATUSES, type AgentLifecycleStatus }; export type { AgentTimelineCursor, @@ -716,13 +720,9 @@ export class AgentManager { }; const normalizedConfig = await this.normalizeConfig(injectedConfig); const launchContext = this.buildLaunchContext(resolvedAgentId); - const client = this.requireClient(normalizedConfig.provider); - const available = await client.isAvailable(); - if (!available) { - throw new Error( - `Provider '${normalizedConfig.provider}' is not available. Please ensure the CLI is installed.`, - ); - } + const client = await this.requireAvailableClient({ + provider: normalizedConfig.provider, + }); const session = await client.createSession(normalizedConfig, launchContext); return this.registerSession(session, normalizedConfig, resolvedAgentId, { labels: options?.labels, @@ -3079,6 +3079,37 @@ export class AgentManager { }; } + private async requireAvailableClient(options: { provider: AgentProvider }): Promise { + const client = this.clients.get(options.provider); + if (!client) { + const configuredProviders = Array.from(this.clients.keys()); + throw new Error( + `Unknown provider '${options.provider}'. Configured providers: ${formatProviderList( + configuredProviders, + )}.`, + ); + } + + let unavailableReason: string | null = null; + try { + const available = await client.isAvailable(); + if (available) { + return client; + } + } catch (error) { + unavailableReason = error instanceof Error ? error.message : String(error); + } + + const availableProviders = (await this.listProviderAvailability()) + .filter((entry) => entry.available) + .map((entry) => entry.provider); + const providerList = formatProviderList(availableProviders); + const reason = unavailableReason ? ` Reason: ${unavailableReason}.` : ""; + throw new Error( + `Provider '${options.provider}' is not available.${reason} Available providers: ${providerList}. Use one of those providers, or install/configure '${options.provider}'.`, + ); + } + private requireClient(provider: AgentProvider): AgentClient { const client = this.clients.get(provider); if (!client) { diff --git a/packages/server/src/server/agent/agent-projections.ts b/packages/server/src/server/agent/agent-projections.ts index b793bcacb..3296a9362 100644 --- a/packages/server/src/server/agent/agent-projections.ts +++ b/packages/server/src/server/agent/agent-projections.ts @@ -7,16 +7,14 @@ import type { AgentMode, AgentPermissionRequest, AgentPersistenceHandle, + AgentProvider, AgentSessionConfig, AgentRuntimeInfo, AgentUsage, } from "./agent-sdk-types.js"; import type { ManagedAgent } from "./agent-manager.js"; import type { JsonValue } from "../json-utils.js"; -import type { Logger } from "pino"; -import { buildProviderRegistry } from "./provider-registry.js"; -import { coerceAgentProvider, toAgentPersistenceHandle } from "../persistence-hooks.js"; - +import { isStoredAgentProviderAvailable, toAgentPersistenceHandle } from "../persistence-hooks.js"; export type { ManagedAgent }; interface ProjectionOptions { @@ -143,15 +141,11 @@ export function toAgentPayload( return payload; } -function buildStoredRuntimeInfo( - record: StoredAgentRecord, - providerRegistry: ReturnType, - logger: Logger, -): AgentRuntimeInfo | undefined { +function buildStoredRuntimeInfo(record: StoredAgentRecord): AgentRuntimeInfo | undefined { if (!record.runtimeInfo) return undefined; const ri = record.runtimeInfo; const runtimeInfo: AgentRuntimeInfo = { - provider: coerceAgentProvider(logger, providerRegistry, ri.provider, record.id), + provider: ri.provider, sessionId: ri.sessionId, }; if (Object.prototype.hasOwnProperty.call(ri, "model")) { @@ -169,10 +163,19 @@ function buildStoredRuntimeInfo( return runtimeInfo; } +function buildStoredPersistenceHandle( + record: StoredAgentRecord, + validProviders: Iterable, +): AgentPersistenceHandle | null { + if (!isStoredAgentProviderAvailable(record, validProviders)) { + return null; + } + return toAgentPersistenceHandle(validProviders, record.persistence); +} + export function buildStoredAgentPayload( record: StoredAgentRecord, - providerRegistry: ReturnType, - logger: Logger, + validProviders: Iterable, ): AgentSnapshotPayload { const defaultCapabilities = { supportsStreaming: false, @@ -187,12 +190,13 @@ export function buildStoredAgentPayload( const updatedAt = new Date(resolveStoredAgentPayloadUpdatedAt(record)); const lastUserMessageAt = record.lastUserMessageAt ? new Date(record.lastUserMessageAt) : null; - const provider = coerceAgentProvider(logger, providerRegistry, record.provider, record.id); - const runtimeInfo = buildStoredRuntimeInfo(record, providerRegistry, logger); + const runtimeInfo = buildStoredRuntimeInfo(record); + const providerAvailable = isStoredAgentProviderAvailable(record, validProviders); + const persistence = buildStoredPersistenceHandle(record, validProviders); return { id: record.id, - provider, + provider: record.provider, cwd: record.cwd, model: record.config?.model ?? null, thinkingOptionId: record.config?.thinkingOptionId ?? null, @@ -209,13 +213,14 @@ export function buildStoredAgentPayload( currentModeId: record.lastModeId ?? null, availableModes: [], pendingPermissions: [], - persistence: toAgentPersistenceHandle(logger, providerRegistry, record.persistence), + persistence, title: record.title ?? record.config?.title ?? null, requiresAttention: record.requiresAttention ?? false, attentionReason: record.attentionReason ?? null, attentionTimestamp: record.attentionTimestamp ?? null, archivedAt: record.archivedAt ?? null, labels: normalizeLabels(record.labels), + ...(providerAvailable ? {} : { providerUnavailable: true }), }; } @@ -238,6 +243,7 @@ export function toAgentListItemPayload(agent: AgentSnapshotPayload): AgentListIt attentionReason: agent.attentionReason ?? null, attentionTimestamp: agent.attentionTimestamp ?? null, labels: agent.labels, + ...(agent.providerUnavailable ? { providerUnavailable: true } : {}), }; } diff --git a/packages/server/src/server/agent/mcp-server.test.ts b/packages/server/src/server/agent/mcp-server.test.ts index 234299571..cab35af87 100644 --- a/packages/server/src/server/agent/mcp-server.test.ts +++ b/packages/server/src/server/agent/mcp-server.test.ts @@ -141,13 +141,32 @@ function createTestDeps(): TestDeps { } function createProviderDefinition(overrides: Partial): ProviderDefinition { + const provider = (overrides.id ?? "claude") as AgentProvider; return { - id: "claude", + id: provider, label: "Claude", description: "Test provider", defaultModeId: "default", modes: [], - createClient: vi.fn() as ProviderDefinition["createClient"], + createClient: vi.fn(() => ({ + provider, + capabilities: { + supportsStreaming: false, + supportsSessionPersistence: false, + supportsDynamicModes: false, + supportsMcpServers: false, + supportsReasoningStream: false, + supportsToolInvocations: false, + }, + createSession: async () => { + throw new Error("createSession is not used by this MCP provider test"); + }, + resumeSession: async () => { + throw new Error("resumeSession is not used by this MCP provider test"); + }, + listModels: vi.fn().mockResolvedValue([]), + isAvailable: vi.fn().mockResolvedValue(true), + })), fetchModels: vi.fn().mockResolvedValue([]), fetchModes: vi.fn().mockResolvedValue([]), ...overrides, @@ -913,7 +932,7 @@ describe("create_agent MCP tool", () => { describe("create_schedule MCP tool", () => { const logger = createTestLogger(); - it("preserves default new-agent schedule behavior without requiring provider", async () => { + it("requires provider for new-agent schedules", async () => { const { agentManager, agentStorage } = createTestDeps(); const create = vi.fn(async (input: CreateScheduleInput) => createStoredSchedule(input)); const server = await createAgentMcpServer({ @@ -924,31 +943,14 @@ describe("create_schedule MCP tool", () => { }); const tool = registeredTool(server, "create_schedule"); - const response = await tool.callback({ - prompt: "say hello", - every: "5m", - name: "Default schedule", - }); - - expect(create).toHaveBeenCalledWith( - expect.objectContaining({ + await expect( + tool.callback({ prompt: "say hello", - target: { - type: "new-agent", - config: { - provider: "claude", - cwd: process.cwd(), - }, - }, + every: "5m", + name: "Default schedule", }), - ); - expect(response.structuredContent.target).toEqual({ - type: "new-agent", - config: { - provider: "claude", - cwd: process.cwd(), - }, - }); + ).rejects.toThrow("provider is required when target is new-agent"); + expect(create).not.toHaveBeenCalled(); }); it("keeps create_schedule provider overrides compatible with provider and provider/model forms", async () => { @@ -1035,11 +1037,13 @@ describe("provider listing MCP tool", () => { { id: "claude", label: "Claude", + status: "available", modes: [{ id: "default", label: "Default", description: "Built-in mode" }], }, { id: "zai", label: "ZAI", + status: "available", modes: [{ id: "default", label: "Default", description: "Custom mode" }], }, ], diff --git a/packages/server/src/server/agent/mcp-server.ts b/packages/server/src/server/agent/mcp-server.ts index 0283eba2b..1b0f6c8f1 100644 --- a/packages/server/src/server/agent/mcp-server.ts +++ b/packages/server/src/server/agent/mcp-server.ts @@ -22,6 +22,7 @@ import { import { curateAgentActivity } from "./activity-curator.js"; import type { AgentStorage } from "./agent-storage.js"; import { ensureAgentLoaded } from "./agent-loading.js"; +import { isStoredAgentProviderAvailable } from "../persistence-hooks.js"; import { appendTimelineItemIfAgentKnown, emitLiveTimelineItemIfAgentKnown, @@ -159,6 +160,15 @@ function resolveAgentListActivityTime(agent: AgentListItemPayload): number { ); } +function resolveRegisteredProviderIds( + agentManager: AgentManager, + providerRegistry: Record | null | undefined, +): AgentProvider[] { + return providerRegistry + ? (Object.keys(providerRegistry) as AgentProvider[]) + : agentManager.getRegisteredProviderIds(); +} + function compareAgentListItems(a: AgentListItemPayload, b: AgentListItemPayload): number { const attentionDelta = Number(b.requiresAttention ?? false) - Number(a.requiresAttention ?? false); @@ -289,13 +299,6 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom version: "2.0.0", }); - const requireProviderRegistry = (): Record => { - if (!providerRegistry) { - throw new Error("Provider registry is required to load stored agent records"); - } - return providerRegistry; - }; - const resolveCallerAgent = () => { if (!callerAgentId) { return null; @@ -389,6 +392,10 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom }; const resolveNewAgentScheduleTarget = (params?: { provider?: string; cwd?: string }) => { + if (!params?.provider?.trim()) { + throw new Error("provider is required when target is new-agent"); + } + const callerAgent = resolveCallerAgent(); if (callerAgent) { return { @@ -399,7 +406,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom const resolvedProviderModel = resolveScheduleProviderAndModel({ provider: params?.provider, - defaultProvider: "claude", + defaultProvider: params.provider as AgentProvider, }); return { type: "new-agent" as const, @@ -675,7 +682,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom { title: "Create agent", description: - "Create an agent tied to a working directory. Requires provider/model, for example codex/gpt-5.4. Optionally run an initial prompt immediately or create a git worktree for the agent.", + "Create an agent tied to a working directory. Requires provider/model, for example codex/gpt-5.4. Do not guess; call list_providers and list_models first if uncertain. Optionally run an initial prompt immediately or create a git worktree for the agent.", inputSchema: createAgentInputSchema, outputSchema: { agentId: z.string(), @@ -1052,8 +1059,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom const structuredSnapshot = buildStoredAgentPayload( record, - requireProviderRegistry(), - childLogger, + resolveRegisteredProviderIds(agentManager, providerRegistry), ); return { content: [], @@ -1100,10 +1106,15 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom ); const liveIds = new Set(liveSnapshots.map((snapshot) => snapshot.id)); const storedRecords = await agentStorage.list(); + const registeredProviderIds = resolveRegisteredProviderIds(agentManager, providerRegistry); const storedAgents = storedRecords .filter((record) => !record.internal && !liveIds.has(record.id)) .filter((record) => includeArchived || !record.archivedAt) - .map((record) => buildStoredAgentPayload(record, requireProviderRegistry(), childLogger)); + .filter( + (record) => + includeArchived || isStoredAgentProviderAvailable(record, registeredProviderIds), + ) + .map((record) => buildStoredAgentPayload(record, registeredProviderIds)); const agents = [...liveAgents, ...storedAgents] .map(toAgentListItemPayload) .filter((agent) => !requestedCwd || isSameOrDescendantPath(requestedCwd, agent.cwd)) @@ -1631,26 +1642,49 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom "list_providers", { title: "List providers", - description: "List available agent providers and their modes.", + description: "List configured agent providers, availability, and their modes.", inputSchema: {}, outputSchema: { providers: z.array(ProviderSummarySchema), }, }, - async () => ({ - content: [], - structuredContent: ensureValidJson({ - providers: Object.values(providerRegistry ?? {}).map((provider) => ({ - id: provider.id, - label: provider.label, - modes: provider.modes.map((mode) => ({ - id: mode.id, - label: mode.label, - ...(mode.description ? { description: mode.description } : {}), - })), - })), - }), - }), + async () => { + const providers = await Promise.all( + Object.values(providerRegistry ?? {}).map(async (provider) => { + let status = "unknown"; + let error: string | undefined; + try { + status = (await provider.createClient(childLogger).isAvailable()) + ? "available" + : "unavailable"; + } catch (availabilityError) { + status = "unavailable"; + error = + availabilityError instanceof Error + ? availabilityError.message + : String(availabilityError); + } + const summary = { + id: provider.id, + label: provider.label, + status, + modes: provider.modes.map((mode) => ({ + id: mode.id, + label: mode.label, + ...(mode.description ? { description: mode.description } : {}), + })), + }; + if (error) { + return Object.assign(summary, { error }); + } + return summary; + }), + ); + return { + content: [], + structuredContent: ensureValidJson({ providers }), + }; + }, ); server.registerTool( diff --git a/packages/server/src/server/agent/provider-snapshot-manager.test.ts b/packages/server/src/server/agent/provider-snapshot-manager.test.ts index 5d52542ea..b90211cf6 100644 --- a/packages/server/src/server/agent/provider-snapshot-manager.test.ts +++ b/packages/server/src/server/agent/provider-snapshot-manager.test.ts @@ -1047,6 +1047,49 @@ describe("ProviderSnapshotManager", () => { manager.destroy(); }); + test("replaceRegistry removes providers that were disabled at runtime", async () => { + const { registry } = createRegistry([ + createMockProvider({ + provider: "codex", + fetchModels: async () => [createModel("codex", "gpt-5.2")], + }), + createMockProvider({ + provider: "claude", + fetchModels: async () => [createModel("claude", "sonnet")], + }), + ]); + const manager = new ProviderSnapshotManager(registry, createTestLogger()); + const listener = vi.fn<(entries: ProviderSnapshotEntry[], cwd: string) => void>(); + manager.on("change", listener); + + manager.getSnapshot(projectCwd); + + await vi.waitFor(() => { + expect(getProviderEntry(manager.getSnapshot(projectCwd), "claude")?.status).toBe("ready"); + expect(getProviderEntry(manager.getSnapshot(projectCwd), "codex")?.status).toBe("ready"); + }); + + const { registry: nextRegistry } = createRegistry([ + createMockProvider({ + provider: "codex", + fetchModels: async () => [createModel("codex", "gpt-5.2")], + }), + ]); + manager.replaceRegistry(nextRegistry); + + expect(manager.getSnapshot(projectCwd).map((entry) => entry.provider)).toEqual(["codex"]); + expect(listener).toHaveBeenCalledWith( + expect.arrayContaining([ + expect.objectContaining({ + provider: "codex", + }), + ]), + homedir(), + ); + + manager.destroy(); + }); + test("snapshot includes user-defined providers from the registry", async () => { const { registry } = createRegistry([ createMockProvider({ provider: "claude" }), diff --git a/packages/server/src/server/agent/provider-snapshot-manager.ts b/packages/server/src/server/agent/provider-snapshot-manager.ts index 97ce7787d..071224715 100644 --- a/packages/server/src/server/agent/provider-snapshot-manager.ts +++ b/packages/server/src/server/agent/provider-snapshot-manager.ts @@ -39,12 +39,14 @@ export class ProviderSnapshotManager { private readonly ttlMs: number; private readonly refreshTimeoutMs: number; private readonly now: () => number; + private providerRegistry: Record; constructor( - private readonly providerRegistry: Record, + providerRegistry: Record, private readonly logger: Logger, options: ProviderSnapshotManagerOptions = {}, ) { + this.providerRegistry = providerRegistry; this.ttlMs = options.ttlMs ?? DEFAULT_SNAPSHOT_TTL_MS; this.refreshTimeoutMs = options.refreshTimeoutMs ?? DEFAULT_REFRESH_TIMEOUT_MS; this.now = options.now ?? Date.now; @@ -144,6 +146,18 @@ export class ProviderSnapshotManager { this.providerLoads.clear(); } + replaceRegistry(providerRegistry: Record): void { + this.providerRegistry = providerRegistry; + + for (const cwd of this.snapshots.keys()) { + this.providerLoads.delete(cwd); + this.lastCheckedAts.delete(cwd); + this.snapshots.set(cwd, this.createLoadingEntries()); + this.emitChange(cwd); + void this.warmUp(cwd); + } + } + private createLoadingEntries(): Map { const entries = new Map(); for (const provider of this.getProviderIds()) { diff --git a/packages/server/src/server/agent/providers/acp-agent.test.ts b/packages/server/src/server/agent/providers/acp-agent.test.ts index 7e8ec5b3d..13a31f707 100644 --- a/packages/server/src/server/agent/providers/acp-agent.test.ts +++ b/packages/server/src/server/agent/providers/acp-agent.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test, vi } from "vitest"; -import type { PromptResponse, SessionUpdate } from "@agentclientprotocol/sdk"; +import type { PromptResponse, SessionConfigOption, SessionUpdate } from "@agentclientprotocol/sdk"; import { ACPAgentClient, @@ -21,6 +21,18 @@ interface ACPSessionInternals { translateSessionUpdate(update: SessionUpdate): unknown; } +interface ACPModelSelectionInternals { + sessionId: string | null; + connection: { + setSessionConfigOption: (input: { + sessionId: string; + configId: string; + value: string; + }) => Promise; + }; + configOptions: SessionConfigOption[]; +} + function createSession(): ACPAgentSession { return new ACPAgentSession( { @@ -44,6 +56,32 @@ function createSession(): ACPAgentSession { ); } +test("ACP setModel forwards model ids that are absent from the advertised catalog", async () => { + const session = createSession(); + const setSessionConfigOption = vi.fn(async () => undefined); + const internals = session as unknown as ACPModelSelectionInternals; + internals.sessionId = "session-1"; + internals.connection = { setSessionConfigOption }; + internals.configOptions = [ + { + id: "model-option", + name: "Model", + category: "model", + type: "select", + currentValue: "sonnet", + options: [{ value: "sonnet", name: "Sonnet" }], + }, + ]; + + await session.setModel("new-provider-model"); + + expect(setSessionConfigOption).toHaveBeenCalledWith({ + sessionId: "session-1", + configId: "model-option", + value: "new-provider-model", + }); +}); + describe("createLoggedNdJsonStream", () => { test("routes malformed ACP stdout through the provider logger instead of console.error", async () => { const input = new TransformStream(); @@ -71,6 +109,10 @@ describe("createLoggedNdJsonStream", () => { expect(parsed.value).toEqual({ jsonrpc: "2.0", method: "ok", params: {} }); expect(logger.warn).toHaveBeenCalledWith( expect.objectContaining({ + err: { + type: "SyntaxError", + message: "ACP stdout line was not valid JSON", + }, provider: "gemini", }), "ACP agent emitted non-JSON stdout; ignoring line", @@ -82,6 +124,39 @@ describe("createLoggedNdJsonStream", () => { reader.releaseLock(); consoleError.mockRestore(); }); + + test("does not log terminal control sequences from malformed ACP stdout", async () => { + const input = new TransformStream(); + const output = new TransformStream(); + const logger = { + warn: vi.fn(), + }; + + const stream = createLoggedNdJsonStream(output.writable, input.readable, { + logger: logger as unknown as ReturnType, + provider: "gemini", + }); + const reader = stream.readable.getReader(); + const writer = input.writable.getWriter(); + + await writer.write(new TextEncoder().encode('\u001b[1G\u001b[0JEn\n{"ok":true}\n')); + + const parsed = await reader.read(); + + expect(parsed.value).toEqual({ ok: true }); + expect(JSON.stringify(logger.warn.mock.calls)).not.toContain("\u001b"); + expect(JSON.stringify(logger.warn.mock.calls)).not.toContain("[1G"); + expect(logger.warn.mock.calls[0]?.[0]).toEqual({ + err: { + type: "SyntaxError", + message: "ACP stdout line was not valid JSON", + }, + provider: "gemini", + }); + + await writer.close(); + reader.releaseLock(); + }); }); describe("mapACPUsage", () => { diff --git a/packages/server/src/server/agent/providers/acp-agent.ts b/packages/server/src/server/agent/providers/acp-agent.ts index 4c480c1e5..fe37675a6 100644 --- a/packages/server/src/server/agent/providers/acp-agent.ts +++ b/packages/server/src/server/agent/providers/acp-agent.ts @@ -113,6 +113,13 @@ const COPILOT_AUTOPILOT_MODE = "https://agentclientprotocol.com/protocol/session // NO_BROWSER is honored by Gemini CLI; other ACP agents ignore it. const PROBE_ENV: Record = { NO_BROWSER: "true" }; +function summarizeMalformedACPStdoutError(error: unknown): { type: string; message: string } { + return { + type: error instanceof Error ? error.name : typeof error, + message: "ACP stdout line was not valid JSON", + }; +} + export function createLoggedNdJsonStream( output: WritableStream, input: ReadableStream, @@ -151,7 +158,7 @@ export function createLoggedNdJsonStream( } catch (error) { options.logger.warn( { - err: error, + err: summarizeMalformedACPStdoutError(error), provider: options.provider, }, "ACP agent emitted non-JSON stdout; ignoring line", @@ -696,7 +703,6 @@ export class ACPAgentSession implements AgentSession, ACPClient { private sessionId: string | null = null; private currentMode: string | null = null; private availableModes: AgentMode[]; - private availableModels: AgentModelDefinition[] = []; private currentModel: string | null = null; private thinkingOptionId: string | null = null; private currentTitle: string | null = null; @@ -1065,11 +1071,6 @@ export class ACPAgentSession implements AgentSession, ACPClient { return; } - const modelExists = this.availableModels.some((model) => model.id === modelId); - if (!modelExists && this.availableModels.length > 0) { - throw new Error(`Unknown ${this.provider} model '${modelId}'`); - } - if ("unstable_setSessionModel" in this.connection) { try { await this.connection.unstable_setSessionModel({ @@ -1467,7 +1468,6 @@ export class ACPAgentSession implements AgentSession, ACPClient { const modeInfo = deriveModesFromACP(this.defaultModes, transformed.modes, this.configOptions); this.availableModes = modeInfo.modes; - this.availableModels = this.deriveAvailableModels(transformed.models); this.currentMode = modeInfo.currentModeId ?? this.currentMode; this.currentModel = @@ -1598,24 +1598,12 @@ export class ACPAgentSession implements AgentSession, ACPClient { this.configOptions = update.configOptions; const modeInfo = deriveModesFromACP(this.defaultModes, null, this.configOptions); this.availableModes = modeInfo.modes; - this.availableModels = this.deriveAvailableModels(null); this.currentMode = modeInfo.currentModeId ?? this.currentMode; this.currentModel = deriveCurrentConfigValue(this.configOptions, "model") ?? this.currentModel; this.thinkingOptionId = deriveCurrentConfigValue(this.configOptions, "thought_level") ?? this.thinkingOptionId; } - private deriveAvailableModels( - models: SessionModelState | null | undefined, - ): AgentModelDefinition[] { - const availableModels = deriveModelDefinitionsFromACP( - this.provider, - models, - this.configOptions, - ); - return this.modelTransformer ? this.modelTransformer(availableModels) : availableModels; - } - private handleSessionInfoUpdate(update: SessionInfoUpdate): void { if ("title" in update) { this.currentTitle = update.title ?? null; diff --git a/packages/server/src/server/agent/providers/pi-direct-agent.test.ts b/packages/server/src/server/agent/providers/pi-direct-agent.test.ts index 8445c9e7f..f773e1000 100644 --- a/packages/server/src/server/agent/providers/pi-direct-agent.test.ts +++ b/packages/server/src/server/agent/providers/pi-direct-agent.test.ts @@ -38,6 +38,7 @@ function createPiSession(prompt: () => Promise): PiDirectSessionAdapter { abort: vi.fn(), dispose: vi.fn(), getSessionStats: vi.fn(() => ({})), + setModel: vi.fn(), setThinkingLevel: vi.fn(), }; } @@ -47,7 +48,13 @@ function createPiModel(provider: string, id: string): Model { provider, id, name: id, + api: "openai-completions", + baseUrl: "https://example.invalid/v1", reasoning: true, + input: ["text"], + cost: { input: 1, output: 2, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 200000, + maxTokens: 8192, } as Model; } @@ -76,6 +83,37 @@ describe("PiDirectAgentSession", () => { }, ]); }); + + test("setModel creates a minimal model for new ids under a known provider", async () => { + const sdkSession = createPiSession(async () => undefined); + const session = new PiDirectAgentSession( + sdkSession, + { + find: vi.fn(() => undefined), + getAll: vi.fn(() => [createPiModel("openrouter", "known-model")]), + }, + { + provider: "pi", + cwd: "/tmp/paseo-pi-test", + }, + ); + + await session.setModel("openrouter/blabal"); + + expect(sdkSession.setModel).toHaveBeenCalledWith({ + id: "blabal", + name: "blabal", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://example.invalid/v1", + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 128000, + maxTokens: 16384, + compat: undefined, + }); + }); }); describe("PiDirectAgentClient", () => { diff --git a/packages/server/src/server/agent/providers/pi-direct-agent.ts b/packages/server/src/server/agent/providers/pi-direct-agent.ts index c860e4673..754f39686 100644 --- a/packages/server/src/server/agent/providers/pi-direct-agent.ts +++ b/packages/server/src/server/agent/providers/pi-direct-agent.ts @@ -787,7 +787,13 @@ function findModelInRegistry( parsedReference: PiModelReference, ): Model | undefined { if (parsedReference.provider) { - return registry.find(parsedReference.provider, parsedReference.id); + return ( + registry.find(parsedReference.provider, parsedReference.id) ?? + createProviderModelFallback(registry, { + provider: parsedReference.provider, + id: parsedReference.id, + }) + ); } return registry.getAll().find((entry) => { @@ -798,6 +804,32 @@ function findModelInRegistry( }); } +function createProviderModelFallback( + registry: PiDirectModelRegistry, + parsedReference: { provider: string; id: string }, +): Model | undefined { + const providerDefault = registry + .getAll() + .find((model) => model.provider === parsedReference.provider); + if (!providerDefault) { + return undefined; + } + + return { + id: parsedReference.id, + name: parsedReference.id, + api: providerDefault.api, + provider: parsedReference.provider, + baseUrl: providerDefault.baseUrl, + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 128000, + maxTokens: 16384, + compat: providerDefault.compat, + }; +} + export class PiDirectAgentSession implements AgentSession { readonly provider = PI_PROVIDER; readonly capabilities = PI_CAPABILITIES; diff --git a/packages/server/src/server/bootstrap.ts b/packages/server/src/server/bootstrap.ts index d8592db65..8aa106106 100644 --- a/packages/server/src/server/bootstrap.ts +++ b/packages/server/src/server/bootstrap.ts @@ -225,6 +225,15 @@ export async function createPaseoDaemon( config.paseoHome, { mcp: { injectIntoAgents: config.mcpInjectIntoAgents ?? true }, + providers: Object.fromEntries( + Object.entries(config.providerOverrides ?? {}).map(([providerId, override]) => [ + providerId, + { + ...(override.enabled !== undefined ? { enabled: override.enabled } : {}), + ...(override.additionalModels ? { additionalModels: override.additionalModels } : {}), + }, + ]), + ), }, logger, ); diff --git a/packages/server/src/server/daemon-config-store.test.ts b/packages/server/src/server/daemon-config-store.test.ts new file mode 100644 index 000000000..bce53cf37 --- /dev/null +++ b/packages/server/src/server/daemon-config-store.test.ts @@ -0,0 +1,140 @@ +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, test } from "vitest"; + +import { DaemonConfigStore, applyMutableProviderConfigToOverrides } from "./daemon-config-store.js"; +import { loadPersistedConfig } from "./persisted-config.js"; + +describe("applyMutableProviderConfigToOverrides", () => { + test("merges mutable provider fields onto provider overrides", () => { + expect( + applyMutableProviderConfigToOverrides( + { + gemini: { + extends: "acp", + label: "Gemini", + command: ["gemini", "--acp"], + }, + }, + { + gemini: { enabled: false }, + claude: { + additionalModels: [ + { + id: "claude-custom", + label: "claude-custom", + }, + ], + }, + }, + ), + ).toEqual({ + gemini: { + extends: "acp", + label: "Gemini", + command: ["gemini", "--acp"], + enabled: false, + }, + claude: { + additionalModels: [ + { + id: "claude-custom", + label: "claude-custom", + }, + ], + }, + }); + }); +}); + +describe("DaemonConfigStore", () => { + const tempDirs: string[] = []; + + afterEach(() => { + for (const dir of tempDirs) { + rmSync(dir, { recursive: true, force: true }); + } + }); + + test("patch persists provider enabled flags into config.json", () => { + const paseoHome = mkdtempSync(path.join(tmpdir(), "paseo-daemon-config-store-")); + tempDirs.push(paseoHome); + + const initial = loadPersistedConfig(paseoHome); + initial.agents = { + providers: { + gemini: { + extends: "acp", + label: "Gemini", + command: ["gemini", "--acp"], + }, + }, + }; + const configPath = path.join(paseoHome, "config.json"); + // Reuse the validated serializer through the store path by seeding the file directly. + // This keeps the test focused on the merge behavior. + const seeded = JSON.stringify(initial, null, 2) + "\n"; + writeFileSync(configPath, seeded); + + const store = new DaemonConfigStore( + paseoHome, + { + mcp: { injectIntoAgents: false }, + providers: {}, + }, + undefined, + ); + + store.patch({ + providers: { + gemini: { enabled: false }, + }, + }); + + const persisted = loadPersistedConfig(paseoHome); + expect(persisted.agents?.providers?.gemini).toEqual({ + extends: "acp", + label: "Gemini", + command: ["gemini", "--acp"], + enabled: false, + }); + }); + + test("patch persists provider additional models into config.json", () => { + const paseoHome = mkdtempSync(path.join(tmpdir(), "paseo-daemon-config-store-")); + tempDirs.push(paseoHome); + + const store = new DaemonConfigStore( + paseoHome, + { + mcp: { injectIntoAgents: false }, + providers: {}, + }, + undefined, + ); + + store.patch({ + providers: { + claude: { + additionalModels: [ + { + id: "claude-custom", + label: "claude-custom", + }, + ], + }, + }, + }); + + const persisted = loadPersistedConfig(paseoHome); + expect(persisted.agents?.providers?.claude).toEqual({ + additionalModels: [ + { + id: "claude-custom", + label: "claude-custom", + }, + ], + }); + }); +}); diff --git a/packages/server/src/server/daemon-config-store.ts b/packages/server/src/server/daemon-config-store.ts index c99cfe1f2..c5149c31d 100644 --- a/packages/server/src/server/daemon-config-store.ts +++ b/packages/server/src/server/daemon-config-store.ts @@ -9,6 +9,7 @@ export type { MutableDaemonConfig, MutableDaemonConfigPatch } from "../shared/me type MutableDaemonConfig = import("../shared/messages.js").MutableDaemonConfig; type MutableDaemonConfigPatch = import("../shared/messages.js").MutableDaemonConfigPatch; +type ProviderOverride = import("./agent/provider-launch-config.js").ProviderOverride; interface LoggerLike { child(bindings: Record): LoggerLike; @@ -57,6 +58,30 @@ function isEqualValue(a: unknown, b: unknown): boolean { return JSON.stringify(a) === JSON.stringify(b); } +export function applyMutableProviderConfigToOverrides( + baseOverrides: Record | undefined, + mutableProviders: MutableDaemonConfig["providers"] | undefined, +): Record | undefined { + if (!baseOverrides && (!mutableProviders || Object.keys(mutableProviders).length === 0)) { + return undefined; + } + + const nextOverrides: Record = { ...baseOverrides }; + for (const [providerId, providerConfig] of Object.entries(mutableProviders ?? {})) { + nextOverrides[providerId] = { + ...nextOverrides[providerId], + }; + if (providerConfig.enabled !== undefined) { + nextOverrides[providerId].enabled = providerConfig.enabled; + } + if (providerConfig.additionalModels !== undefined) { + nextOverrides[providerId].additionalModels = providerConfig.additionalModels; + } + } + + return nextOverrides; +} + export class DaemonConfigStore { private current: MutableDaemonConfig; private readonly paseoHome: string; @@ -148,6 +173,14 @@ function mergeMutableConfigIntoPersistedConfig(params: { mutable: MutableDaemonConfig; }): PersistedConfig { const { persisted, mutable } = params; + const providerOverrides = applyMutableProviderConfigToOverrides( + persisted.agents?.providers as Record | undefined, + mutable.providers, + ); + const persistedAgents = persisted.agents as + | ({ providers?: Record } & Record) + | undefined; + return { ...persisted, daemon: { @@ -157,5 +190,12 @@ function mergeMutableConfigIntoPersistedConfig(params: { injectIntoAgents: mutable.mcp.injectIntoAgents, }, }, - }; + agents: + providerOverrides && Object.keys(providerOverrides).length > 0 + ? { + ...persistedAgents, + providers: providerOverrides, + } + : persisted.agents, + } as PersistedConfig; } diff --git a/packages/server/src/server/persistence-hooks.test.ts b/packages/server/src/server/persistence-hooks.test.ts index 0e0f9aa3e..129584b48 100644 --- a/packages/server/src/server/persistence-hooks.test.ts +++ b/packages/server/src/server/persistence-hooks.test.ts @@ -1,13 +1,10 @@ -import type { Logger } from "pino"; -import { describe, expect, test, vi } from "vitest"; +import { describe, expect, test } from "vitest"; import type { StoredAgentRecord } from "./agent/agent-storage.js"; -import { buildConfigOverrides, buildSessionConfig } from "./persistence-hooks.js"; - -const testLogger = { - child: () => testLogger, - error: vi.fn(), - warn: vi.fn(), -} as unknown as Logger; +import { + buildConfigOverrides, + buildSessionConfig, + toAgentPersistenceHandle, +} from "./persistence-hooks.js"; function createRecord(overrides?: Partial): StoredAgentRecord { const now = new Date().toISOString(); @@ -127,12 +124,16 @@ describe("persistence hooks", () => { expect( buildSessionConfig(record, { validProviders: ["claude", "codex"], - logger: testLogger, }), ).toBeNull(); - expect(testLogger.warn).toHaveBeenCalledWith( - { agentId: "agent-missing-provider", provider: "zai" }, - "Skipping persisted agent with unknown provider 'zai'", - ); + }); + + test("toAgentPersistenceHandle rejects handles for unavailable providers", () => { + const handle = toAgentPersistenceHandle(["claude", "codex"], { + provider: "gemini", + sessionId: "session-123", + }); + + expect(handle).toBeNull(); }); }); diff --git a/packages/server/src/server/persistence-hooks.ts b/packages/server/src/server/persistence-hooks.ts index 0d84bf79a..b5785cd2d 100644 --- a/packages/server/src/server/persistence-hooks.ts +++ b/packages/server/src/server/persistence-hooks.ts @@ -13,8 +13,6 @@ interface LoggerLike { warn(...args: unknown[]): void; } -const DEFAULT_AGENT_PROVIDER = "claude"; - function getLogger(logger: LoggerLike): LoggerLike { return logger.child({ module: "persistence" }); } @@ -24,7 +22,6 @@ type AgentManagerStateSource = Pick; interface BuildSessionConfigOptions { validProviders?: Iterable; - logger?: LoggerLike; } type RegisteredProviders = ReturnType | Iterable; @@ -85,10 +82,6 @@ export function buildSessionConfig( const validProviders = options?.validProviders; const isValidProvider = validProviders ? new Set(validProviders).has(record.provider) : true; if (!isValidProvider) { - options?.logger?.warn( - { agentId: record.id, provider: record.provider }, - `Skipping persisted agent with unknown provider '${record.provider}'`, - ); return null; } const overrides = buildConfigOverrides(record); @@ -106,6 +99,13 @@ export function buildSessionConfig( }; } +export function isStoredAgentProviderAvailable( + record: StoredAgentRecord, + validProviders?: Iterable, +): boolean { + return buildSessionConfig(record, { validProviders }) !== null; +} + export function extractTimestamps(record: StoredAgentRecord): { createdAt: Date; updatedAt: Date; @@ -134,24 +134,7 @@ export function isRegisteredProvider( return hasRegisteredProvider(providerRegistry, value); } -export function coerceAgentProvider( - logger: LoggerLike, - providerRegistry: ReturnType, - value: string, - agentId?: string, -): AgentProvider { - if (isRegisteredProvider(providerRegistry, value)) { - return value; - } - logger.warn( - { value, agentId, defaultProvider: DEFAULT_AGENT_PROVIDER }, - `Unknown provider '${value}' for agent ${agentId ?? "unknown"}; defaulting to '${DEFAULT_AGENT_PROVIDER}'`, - ); - return DEFAULT_AGENT_PROVIDER; -} - export function toAgentPersistenceHandle( - logger: LoggerLike, registeredProviders: RegisteredProviders, handle: StoredAgentRecord["persistence"], ): AgentPersistenceHandle | null { @@ -160,11 +143,9 @@ export function toAgentPersistenceHandle( } const provider = handle.provider; if (!hasRegisteredProvider(registeredProviders, provider)) { - logger.warn({ provider }, `Ignoring persistence handle with unknown provider '${provider}'`); return null; } if (!handle.sessionId) { - logger.warn("Ignoring persistence handle missing sessionId"); return null; } return { diff --git a/packages/server/src/server/session.test.ts b/packages/server/src/server/session.test.ts index b3233eda0..c1d1846f4 100644 --- a/packages/server/src/server/session.test.ts +++ b/packages/server/src/server/session.test.ts @@ -221,7 +221,13 @@ function createSessionForTest(options?: { checkoutDiffManager: checkoutDiffManager as unknown as SessionOptions["checkoutDiffManager"], github: github as unknown as SessionOptions["github"], workspaceGitService: workspaceGitService as unknown as SessionOptions["workspaceGitService"], - daemonConfigStore: {} as unknown as SessionOptions["daemonConfigStore"], + daemonConfigStore: { + get: vi.fn(() => ({ + mcp: { injectIntoAgents: false }, + providers: {}, + })), + onChange: vi.fn(() => () => {}), + } as unknown as SessionOptions["daemonConfigStore"], stt: null, tts: null, terminalManager: (options?.terminalManager ?? null) as SessionOptions["terminalManager"], @@ -357,12 +363,12 @@ describe("session provider refresh cwd routing", () => { const session = createSessionForTest({ messages }); const fetchModels = vi.fn(async () => []); const fetchModes = vi.fn(async () => []); - (session as unknown as { providerRegistry: unknown }).providerRegistry = { + (session as unknown as { getProviderRegistry: () => unknown }).getProviderRegistry = () => ({ codex: { fetchModels, fetchModes, }, - }; + }); await session.handleMessage({ type: "list_provider_models_request", diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index aacdc4648..b55455355 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -68,6 +68,7 @@ import { import { buildConfigOverrides, extractTimestamps, + isStoredAgentProviderAvailable, toAgentPersistenceHandle, } from "./persistence-hooks.js"; import { ensureAgentLoaded } from "./agent/agent-loading.js"; @@ -81,6 +82,7 @@ import type { ScriptHealthState } from "./script-health-monitor.js"; import { spawnWorkspaceScript } from "./worktree-bootstrap.js"; import type { WorkspaceScriptRuntimeStore } from "./workspace-script-runtime-store.js"; import type { DaemonConfigStore } from "./daemon-config-store.js"; +import { applyMutableProviderConfigToOverrides } from "./daemon-config-store.js"; import type { WorkspaceGitRuntimeSnapshot, WorkspaceGitService } from "./workspace-git-service.js"; import { buildProviderRegistry } from "./agent/provider-registry.js"; @@ -801,7 +803,6 @@ export class Session { private readonly mcpBaseUrl: string | null; private readonly downloadTokenStore: DownloadTokenStore; private readonly pushTokenStore: PushTokenStore; - private readonly providerRegistry: ReturnType; private unsubscribeAgentEvents: (() => void) | null = null; private agentUpdatesSubscription: AgentUpdatesSubscriptionState | null = null; private workspaceUpdatesSubscription: WorkspaceUpdatesSubscriptionState | null = null; @@ -947,12 +948,6 @@ export class Session { this.providerOverrides = providerOverrides; this.isDev = isDev === true; this.abortController = new AbortController(); - this.providerRegistry = buildProviderRegistry(this.sessionLogger, { - runtimeSettings: this.agentProviderRuntimeSettings, - providerOverrides: this.providerOverrides, - workspaceGitService: this.workspaceGitService, - isDev: this.isDev, - }); this.initializePerSessionManagers({ tts, stt, dictation }); @@ -1360,8 +1355,27 @@ export class Session { return payload; } - private buildStoredAgentPayload(record: StoredAgentRecord): AgentSnapshotPayload { - return buildStoredAgentPayload(record, this.providerRegistry, this.sessionLogger); + private getProviderRegistry(): ReturnType { + return buildProviderRegistry(this.sessionLogger, { + runtimeSettings: this.agentProviderRuntimeSettings, + providerOverrides: applyMutableProviderConfigToOverrides( + this.providerOverrides, + this.daemonConfigStore.get().providers, + ), + workspaceGitService: this.workspaceGitService, + isDev: this.isDev, + }); + } + + private getRegisteredProviderIds(): AgentProvider[] { + return Object.keys(this.getProviderRegistry()) as AgentProvider[]; + } + + private buildStoredAgentPayload( + record: StoredAgentRecord, + registeredProviderIds = this.getRegisteredProviderIds(), + ): AgentSnapshotPayload { + return buildStoredAgentPayload(record, registeredProviderIds); } private isProviderVisibleToClient(provider: string): boolean { @@ -3082,11 +3096,13 @@ export class Session { if (!record) { throw new Error(`Agent not found: ${agentId}`); } - const handle = toAgentPersistenceHandle( - this.sessionLogger, - this.providerRegistry, - record.persistence, - ); + const providerRegistry = this.getProviderRegistry(); + if ( + !isStoredAgentProviderAvailable(record, Object.keys(providerRegistry) as AgentProvider[]) + ) { + throw new Error(`Agent ${agentId} references unavailable provider '${record.provider}'`); + } + const handle = toAgentPersistenceHandle(providerRegistry, record.persistence); if (!handle) { throw new Error(`Agent ${agentId} cannot be refreshed because it lacks persistence`); } @@ -3183,7 +3199,7 @@ export class Session { if (!manager) { try { - const models = await this.providerRegistry[msg.provider].fetchModels({ + const models = await this.getProviderRegistry()[msg.provider].fetchModels({ cwd, force: false, }); @@ -3315,7 +3331,7 @@ export class Session { } try { - const modes = await this.providerRegistry[msg.provider].fetchModes({ + const modes = await this.getProviderRegistry()[msg.provider].fetchModes({ cwd, force: false, }); @@ -3497,7 +3513,7 @@ export class Session { msg: Extract, ): Promise { try { - const client = this.providerRegistry[msg.provider].createClient(this.sessionLogger); + const client = this.getProviderRegistry()[msg.provider].createClient(this.sessionLogger); const diagnostic = client.getDiagnostic ? (await client.getDiagnostic()).diagnostic : "No diagnostic available for this provider."; @@ -5334,6 +5350,7 @@ export class Session { */ private async listAgentPayloads(filter?: { labels?: Record; + includeUnavailablePersisted?: boolean; }): Promise { // Get live agents with session modes const agentSnapshots = this.agentManager.listAgents(); @@ -5345,9 +5362,15 @@ export class Session { // (excluding internal agents which are for ephemeral system tasks) const registryRecords = await this.agentStorage.list(); const liveIds = new Set(agentSnapshots.map((a) => a.id)); + const registeredProviderIds = this.getRegisteredProviderIds(); const persistedAgents = registryRecords .filter((record) => !liveIds.has(record.id) && !record.internal) - .map((record) => this.buildStoredAgentPayload(record)); + .filter( + (record) => + filter?.includeUnavailablePersisted === true || + isStoredAgentProviderAvailable(record, registeredProviderIds), + ) + .map((record) => this.buildStoredAgentPayload(record, registeredProviderIds)); let agents = [...liveAgents, ...persistedAgents]; @@ -5710,6 +5733,7 @@ export class Session { let agents = await this.listAgentPayloads({ labels: filter?.labels, + includeUnavailablePersisted: request.type === "fetch_agent_history_request", }); const activePlacementsByCwd = scope === "active" ? await this.buildActiveProjectPlacementsByWorkspaceCwd() : null; diff --git a/packages/server/src/server/websocket-server.ts b/packages/server/src/server/websocket-server.ts index 0164d712d..679b0138a 100644 --- a/packages/server/src/server/websocket-server.ts +++ b/packages/server/src/server/websocket-server.ts @@ -13,6 +13,7 @@ import type { LoopService } from "./loop-service.js"; import type { ScheduleService } from "./schedule/service.js"; import type { CheckoutDiffManager, CheckoutDiffMetrics } from "./checkout-diff-manager.js"; import type { DaemonConfigStore, MutableDaemonConfig } from "./daemon-config-store.js"; +import { applyMutableProviderConfigToOverrides } from "./daemon-config-store.js"; import { type ServerInfoStatusPayload, type SessionOutboundMessage, @@ -492,6 +493,17 @@ export class VoiceAssistantWebSocketServer { this.publishSpeechReadiness(snapshot); }) ?? null; this.unsubscribeDaemonConfigChange = this.daemonConfigStore.onChange((config) => { + this.providerOverrides = applyMutableProviderConfigToOverrides( + this.providerOverrides, + config.providers, + ); + this.providerSnapshotManager.replaceRegistry( + buildProviderRegistry(providerSnapshotLogger, { + runtimeSettings: this.agentProviderRuntimeSettings, + providerOverrides: this.providerOverrides, + isDev: this.isDev, + }), + ); this.broadcastDaemonConfigChanged(config); }); diff --git a/packages/server/src/shared/messages.ts b/packages/server/src/shared/messages.ts index 88d4cf3c6..5ad1c4d32 100644 --- a/packages/server/src/shared/messages.ts +++ b/packages/server/src/shared/messages.ts @@ -51,6 +51,22 @@ import { // Mutable daemon config schemas (shared between server store and client) // --------------------------------------------------------------------------- +const MutableDaemonProviderModelSchema = z + .object({ + id: z.string().min(1), + label: z.string().min(1), + description: z.string().optional(), + isDefault: z.boolean().optional(), + }) + .passthrough(); + +const MutableDaemonProviderConfigSchema = z + .object({ + enabled: z.boolean().optional(), + additionalModels: z.array(MutableDaemonProviderModelSchema).optional(), + }) + .passthrough(); + export const MutableDaemonConfigSchema = z .object({ mcp: z @@ -58,12 +74,16 @@ export const MutableDaemonConfigSchema = z injectIntoAgents: z.boolean(), }) .passthrough(), + providers: z.record(z.string(), MutableDaemonProviderConfigSchema).default({}), }) .passthrough(); export const MutableDaemonConfigPatchSchema = z .object({ mcp: MutableDaemonConfigSchema.shape.mcp.partial().optional(), + providers: z + .record(z.string(), MutableDaemonProviderConfigSchema.partial().passthrough()) + .optional(), }) .partial() .passthrough(); @@ -578,6 +598,7 @@ export const AgentSnapshotPayloadSchema = z.object({ attentionReason: z.enum(["finished", "error", "permission"]).nullable().optional(), attentionTimestamp: z.string().nullable().optional(), archivedAt: z.string().nullable().optional(), + providerUnavailable: z.boolean().optional(), }); export type AgentSnapshotPayload = z.infer; @@ -600,6 +621,7 @@ export const AgentListItemPayloadSchema = z.object({ attentionReason: z.enum(["finished", "error", "permission"]).nullable().optional(), attentionTimestamp: z.string().nullable().optional(), labels: z.record(z.string(), z.string()).default({}), + providerUnavailable: z.boolean().optional(), }); export type AgentListItemPayload = z.infer;