mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-10-27 12:40:32 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
253ddeb6d4 | ||
|
|
becc3b86e6 | ||
|
|
1f4d6c2900 | ||
|
|
8d7e22026e | ||
|
|
c78ac62d05 | ||
|
|
6ccd37ce94 | ||
|
|
5b9311938d | ||
|
|
9957b54882 | ||
|
|
ec47d3446f | ||
|
|
546040ffe7 | ||
|
|
a52ab4ab64 | ||
|
|
76b4e96508 | ||
|
|
32abd34bc0 | ||
|
|
67af018d02 | ||
|
|
a55f31a6b0 | ||
|
|
f0801d50b2 | ||
|
|
1a9d3cebcd |
7
.github/dependabot.yml
vendored
7
.github/dependabot.yml
vendored
@@ -11,6 +11,13 @@ updates:
|
||||
interval: "daily"
|
||||
allow:
|
||||
- dependency-type: "all"
|
||||
ignore:
|
||||
# Pinned to match the peer range of @anthropic-ai/claude-agent-sdk;
|
||||
# newer minors break type compatibility between the two installed copies.
|
||||
- dependency-name: "@anthropic-ai/sdk"
|
||||
update-types:
|
||||
- "version-update:semver-minor"
|
||||
- "version-update:semver-major"
|
||||
groups:
|
||||
minor:
|
||||
update-types:
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "0.31.0"
|
||||
".": "0.32.0"
|
||||
}
|
||||
|
||||
43
CHANGELOG.md
43
CHANGELOG.md
@@ -1,5 +1,48 @@
|
||||
# Changelog
|
||||
|
||||
## [0.32.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.31.4...v0.32.0) (2026-05-03)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **deps-dev:** Bump nanoid from 3.3.11 to 3.3.12 ([#622](https://github.com/agentclientprotocol/claude-agent-acp/issues/622)) ([c78ac62](https://github.com/agentclientprotocol/claude-agent-acp/commit/c78ac62d05283e5683b87b198923818f9f556a03))
|
||||
* **deps:** Bump the minor group with 2 updates ([#619](https://github.com/agentclientprotocol/claude-agent-acp/issues/619)) ([6ccd37c](https://github.com/agentclientprotocol/claude-agent-acp/commit/6ccd37ce947fbd55691f676344b15ef1266191ad))
|
||||
* **deps:** update to @anthropic-ai/claude-agent-sdk 0.2.126 ([#621](https://github.com/agentclientprotocol/claude-agent-acp/issues/621)) ([becc3b8](https://github.com/agentclientprotocol/claude-agent-acp/commit/becc3b86e60eb33e7823a0ef27d5aa99758750d7))
|
||||
* **deps:** Update to @anthropic-ai/claude-agent-sdk@0.2.123 ([#614](https://github.com/agentclientprotocol/claude-agent-acp/issues/614)) ([5b93119](https://github.com/agentclientprotocol/claude-agent-acp/commit/5b9311938d97f47400debd66f2b4792e50639c6b))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* emit a real diff when Write overwrites an existing file ([#618](https://github.com/agentclientprotocol/claude-agent-acp/issues/618)) ([8d7e220](https://github.com/agentclientprotocol/claude-agent-acp/commit/8d7e22026e15d54208414bd9ad128eb86d41b451))
|
||||
|
||||
## [0.31.4](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.31.3...v0.31.4) (2026-04-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* gate auto mode on model support ([#604](https://github.com/agentclientprotocol/claude-agent-acp/issues/604)) ([ec47d34](https://github.com/agentclientprotocol/claude-agent-acp/commit/ec47d3446fa22e5895621f57c0a4497dbe044505))
|
||||
|
||||
## [0.31.3](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.31.2...v0.31.3) (2026-04-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Rename effort config category to thought_level ([#609](https://github.com/agentclientprotocol/claude-agent-acp/issues/609)) ([76b4e96](https://github.com/agentclientprotocol/claude-agent-acp/commit/76b4e9650832e59a6b4915ad898fc66a065022fa))
|
||||
|
||||
## [0.31.2](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.31.1...v0.31.2) (2026-04-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* tolerate invalid values in settings.json ([#601](https://github.com/agentclientprotocol/claude-agent-acp/issues/601)) ([67af018](https://github.com/agentclientprotocol/claude-agent-acp/commit/67af018d0216238d8f3cd5bad04a55200518239b))
|
||||
|
||||
## [0.31.1](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.31.0...v0.31.1) (2026-04-27)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* detect glibc and use correct binary ([#599](https://github.com/agentclientprotocol/claude-agent-acp/issues/599)) ([f0801d5](https://github.com/agentclientprotocol/claude-agent-acp/commit/f0801d50b2390a3c48150b9b8507d30a70c18b84))
|
||||
|
||||
## [0.31.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.30.0...v0.31.0) (2026-04-24)
|
||||
|
||||
|
||||
|
||||
302
package-lock.json
generated
302
package-lock.json
generated
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"name": "@agentclientprotocol/claude-agent-acp",
|
||||
"version": "0.31.0",
|
||||
"version": "0.32.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@agentclientprotocol/claude-agent-acp",
|
||||
"version": "0.31.0",
|
||||
"version": "0.32.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "0.20.0",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.2.119",
|
||||
"@agentclientprotocol/sdk": "0.21.0",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.2.126",
|
||||
"zod": "^3.25.0 || ^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
@@ -20,11 +20,11 @@
|
||||
"@anthropic-ai/sdk": "0.89.0",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@types/node": "25.6.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.59.0",
|
||||
"@typescript-eslint/parser": "8.59.0",
|
||||
"eslint": "10.2.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.59.1",
|
||||
"@typescript-eslint/parser": "8.59.1",
|
||||
"eslint": "10.3.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"globals": "17.5.0",
|
||||
"globals": "17.6.0",
|
||||
"prettier": "3.8.3",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "6.0.3",
|
||||
@@ -32,18 +32,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@agentclientprotocol/sdk": {
|
||||
"version": "0.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-0.20.0.tgz",
|
||||
"integrity": "sha512-BxEHyE4MvwyOsdyVPub1vEtyrq8E0JSdjC+ckXWimY1VabFCTXdPyXv2y2Omz1j+iod7Z8oBJDXFCJptM0GBqQ==",
|
||||
"version": "0.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-0.21.0.tgz",
|
||||
"integrity": "sha512-ONj+Q8qOdNQp5XbH5jnMwzT9IKZJsSN0p0lkceS4GtUtNOPVLpNzSS8gqQdGMKfBvA0ESbkL8BTaSN1Rc9miEw==",
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"zod": "^3.25.0 || ^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk": {
|
||||
"version": "0.2.119",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.2.119.tgz",
|
||||
"integrity": "sha512-6AvthpsaOTlkn514brSGOcCSLHDXODnU+ExN1O3CJCjxr5RBcmzR057C9EIM0G7IchnXsRfMZgRO1QKsjTXdbA==",
|
||||
"version": "0.2.126",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.2.126.tgz",
|
||||
"integrity": "sha512-4ZrVu0XUEwNG6wxvsLgppRAmSfAf3oeEMEUPhgazb0AXUUe/7W8MxwZKJWOffqSLWaNYzOt3ZCIL7NJY6toqWw==",
|
||||
"license": "SEE LICENSE IN README.md",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.81.0",
|
||||
@@ -53,23 +53,23 @@
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.2.119",
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-x64": "0.2.119",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64": "0.2.119",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.2.119",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64": "0.2.119",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.2.119",
|
||||
"@anthropic-ai/claude-agent-sdk-win32-arm64": "0.2.119",
|
||||
"@anthropic-ai/claude-agent-sdk-win32-x64": "0.2.119"
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.2.126",
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-x64": "0.2.126",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64": "0.2.126",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.2.126",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64": "0.2.126",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.2.126",
|
||||
"@anthropic-ai/claude-agent-sdk-win32-arm64": "0.2.126",
|
||||
"@anthropic-ai/claude-agent-sdk-win32-x64": "0.2.126"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-darwin-arm64": {
|
||||
"version": "0.2.119",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.2.119.tgz",
|
||||
"integrity": "sha512-kxnG37SZqUata2Jcp/YQ0n9Y7o/sinE/8LdG4ltM1gePh+z+0Mfa4vBUUTEBMBFth9PTovKoesIuVuyFpvO/Cw==",
|
||||
"version": "0.2.126",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.2.126.tgz",
|
||||
"integrity": "sha512-JFlJBbeAlx7Ic5s4lGUN9SppobryXk/lIqPCvhp6KrJTQIerh3MIBzxsVIJ0MaDut7jVni/oYgsvDni7NIyqHA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -80,9 +80,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-darwin-x64": {
|
||||
"version": "0.2.119",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.2.119.tgz",
|
||||
"integrity": "sha512-9Aj8g3ELsmZuOFg17TCkikeg/Wt2ucVT8hOOPQUatzLd7BKhydrHLA0RP42nBpWECO1B/n/mPdQ4iS/LS3s2Fg==",
|
||||
"version": "0.2.126",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.2.126.tgz",
|
||||
"integrity": "sha512-J8BpMj16NK9FUaG3HnHSivyp4Xww9DKWHiC8QSHT9oiT8pH5IG7nl0jxyjIq/lY79evlTY+ubgDVWlMUhUAN/g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -93,15 +93,12 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64": {
|
||||
"version": "0.2.119",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.2.119.tgz",
|
||||
"integrity": "sha512-v3o464XkiYehp/OKidQQirxdVb+aGSvdJvHF2zH9p33W8M/NC21zwwh4dhwDnKsyrtBIgkt2CcMwzIl30r0OtA==",
|
||||
"version": "0.2.126",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.2.126.tgz",
|
||||
"integrity": "sha512-LM+mnfQsgI+1i5mYZwIPDDf14NGBu5wbhzm5U8P11dCa2p8sXmKoWpkbO16BFM2NxeW44I/RXCxE5qFsbz4zcg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -109,15 +106,12 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64-musl": {
|
||||
"version": "0.2.119",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.2.119.tgz",
|
||||
"integrity": "sha512-IPGWgtz+gGnD7fxKAvSf913EUT/lYBTBE8EZ7lh3+x5ZP2859LWLmrCm053Lf3nMWo/CWikZsVPwkDVwpz6tIQ==",
|
||||
"version": "0.2.126",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.2.126.tgz",
|
||||
"integrity": "sha512-GO0BnIUw3LQ3XAy+nipAabkN0GwQGPhHB6ITI4XLoR99fLHB3TA6WfyvTf0fnpxd25A+c/+UsAoxz4zBQaHlhA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -125,15 +119,12 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-linux-x64": {
|
||||
"version": "0.2.119",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.2.119.tgz",
|
||||
"integrity": "sha512-9ePt4ZN+hsqDw4AgS4KtcWIGKfL9Oq28kwkrTER/QAcSrVKxiLonp81cCLzg7Ok/IUJu4Cfd71GZbFv/WE54zw==",
|
||||
"version": "0.2.126",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.2.126.tgz",
|
||||
"integrity": "sha512-yaOTDcYCdscxC0LKg9w8IwSa5g+993WggFZJBTZpqvflA2+WMQeTarDnKlsFTCw9XUZkL8XZeBALYJGx0HutuA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -141,15 +132,12 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-linux-x64-musl": {
|
||||
"version": "0.2.119",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.2.119.tgz",
|
||||
"integrity": "sha512-QYxFNAe4FFridPkKhGlNcNBJ0TaIygWYyvfI9g4kX0i+RVbresUWuZVkWY06ioJ0fXoixFJ+HNQBMB7dLrIp8Q==",
|
||||
"version": "0.2.126",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.2.126.tgz",
|
||||
"integrity": "sha512-ByJGO0+mu7EplxSFSCIHd7QWsXdrF3qgtzQ177o/j+oSppLoqR1ot5ktf8aw5oR3CC5lFHg4tqd6TnneQpEoIg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -157,9 +145,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-win32-arm64": {
|
||||
"version": "0.2.119",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.2.119.tgz",
|
||||
"integrity": "sha512-p/TjcKQvkCYtXGPlR+mdyNwqCmvRcQL34Wtq0yUZ+iqmI/eyCe59IJ3AZrE0EZoqmiAevEYzatPIt9sncC9uxw==",
|
||||
"version": "0.2.126",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.2.126.tgz",
|
||||
"integrity": "sha512-gv3MOsOBkCx3LajOOIjD7AKsOtz/qNHsS2oshGt2GVoy7JA3XbCDeCetDjM6SorV4SE+7F/IH0UJdXe5ejI/Zg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -170,9 +158,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-win32-x64": {
|
||||
"version": "0.2.119",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.2.119.tgz",
|
||||
"integrity": "sha512-k98Ju0wtktm6FhqTE/cXlVr6K4kGqBolVjEGzeKkW6ZILc7124euwNapAvkQCwMAavAxS/ZnO3jdKMtHtwTVTA==",
|
||||
"version": "0.2.126",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.2.126.tgz",
|
||||
"integrity": "sha512-oRV75HwyoOd1/t5+kipAM2g62CaElpKGvSBx3Ys4lCwCiFUyOnmet/O+hRXENsY6ShDeQZEcJL2UWljr2d5NQw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -662,9 +650,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -682,9 +667,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -702,9 +684,6 @@
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -722,9 +701,6 @@
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -742,9 +718,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -762,9 +735,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -947,17 +917,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.0.tgz",
|
||||
"integrity": "sha512-HyAZtpdkgZwpq8Sz3FSUvCR4c+ScbuWa9AksK2Jweub7w4M3yTz4O11AqVJzLYjy/B9ZWPyc81I+mOdJU/bDQw==",
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.1.tgz",
|
||||
"integrity": "sha512-BOziFIfE+6osHO9FoJG4zjoHUcvI7fTNBSpdAwrNH0/TLvzjsk2oo8XSSOT2HhqUyhZPfHv4UOffoJ9oEEQ7Ag==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.12.2",
|
||||
"@typescript-eslint/scope-manager": "8.59.0",
|
||||
"@typescript-eslint/type-utils": "8.59.0",
|
||||
"@typescript-eslint/utils": "8.59.0",
|
||||
"@typescript-eslint/visitor-keys": "8.59.0",
|
||||
"@typescript-eslint/scope-manager": "8.59.1",
|
||||
"@typescript-eslint/type-utils": "8.59.1",
|
||||
"@typescript-eslint/utils": "8.59.1",
|
||||
"@typescript-eslint/visitor-keys": "8.59.1",
|
||||
"ignore": "^7.0.5",
|
||||
"natural-compare": "^1.4.0",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
@@ -970,22 +940,22 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.59.0",
|
||||
"@typescript-eslint/parser": "8.59.1",
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.0.tgz",
|
||||
"integrity": "sha512-TI1XGwKbDpo9tRW8UDIXCOeLk55qe9ZFGs8MTKU6/M08HWTw52DD/IYhfQtOEhEdPhLMT26Ka/x7p70nd3dzDg==",
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.1.tgz",
|
||||
"integrity": "sha512-HDQH9O/47Dxi1ceDhBXdaldtf/WV9yRYMjbjCuNk3qnaTD564qwv61Y7+gTxwxRKzSrgO5uhtw584igXVuuZkA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.59.0",
|
||||
"@typescript-eslint/types": "8.59.0",
|
||||
"@typescript-eslint/typescript-estree": "8.59.0",
|
||||
"@typescript-eslint/visitor-keys": "8.59.0",
|
||||
"@typescript-eslint/scope-manager": "8.59.1",
|
||||
"@typescript-eslint/types": "8.59.1",
|
||||
"@typescript-eslint/typescript-estree": "8.59.1",
|
||||
"@typescript-eslint/visitor-keys": "8.59.1",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1001,14 +971,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.0.tgz",
|
||||
"integrity": "sha512-Lw5ITrR5s5TbC19YSvlr63ZfLaJoU6vtKTHyB0GQOpX0W7d5/Ir6vUahWi/8Sps/nOukZQ0IB3SmlxZnjaKVnw==",
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.1.tgz",
|
||||
"integrity": "sha512-+MuHQlHiEr00Of/IQbE/MmEoi44znZHbR/Pz7Opq4HryUOlRi+/44dro9Ycy8Fyo+/024IWtw8m4JUMCGTYxDg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.59.0",
|
||||
"@typescript-eslint/types": "^8.59.0",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.59.1",
|
||||
"@typescript-eslint/types": "^8.59.1",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1023,14 +993,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.0.tgz",
|
||||
"integrity": "sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg==",
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.1.tgz",
|
||||
"integrity": "sha512-LwuHQI4pDOYVKvmH2dkaJo6YZCSgouVgnS/z7yBPKBMvgtBvyLqiLy9Z6b7+m/TRcX1NFYUqZetI5Y+aT4GEfg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.59.0",
|
||||
"@typescript-eslint/visitor-keys": "8.59.0"
|
||||
"@typescript-eslint/types": "8.59.1",
|
||||
"@typescript-eslint/visitor-keys": "8.59.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1041,9 +1011,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.0.tgz",
|
||||
"integrity": "sha512-91Sbl3s4Kb3SybliIY6muFBmHVv+pYXfybC4Oolp3dvk8BvIE3wOPc+403CWIT7mJNkfQRGtdqghzs2+Z91Tqg==",
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.1.tgz",
|
||||
"integrity": "sha512-/0nEyPbX7gRsk0Uwfe4ALwwgxuA66d/l2mhRDNlAvaj4U3juhUtJNq0DsY8M2AYwwb9rEq2hrC3IcIcEt++iJA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1058,15 +1028,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.0.tgz",
|
||||
"integrity": "sha512-3TRiZaQSltGqGeNrJzzr1+8YcEobKH9rHnqIp/1psfKFmhRQDNMGP5hBufanYTGznwShzVLs3Mz+gDN7HkWfXg==",
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.1.tgz",
|
||||
"integrity": "sha512-klWPBR2ciQHS3f++ug/mVnWKPjBUo7icEL3FAO1lhAR1Z1i5NQYZ1EannMSRYcq5qCv5wNALlXr6fksRHyYl7w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.59.0",
|
||||
"@typescript-eslint/typescript-estree": "8.59.0",
|
||||
"@typescript-eslint/utils": "8.59.0",
|
||||
"@typescript-eslint/types": "8.59.1",
|
||||
"@typescript-eslint/typescript-estree": "8.59.1",
|
||||
"@typescript-eslint/utils": "8.59.1",
|
||||
"debug": "^4.4.3",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
},
|
||||
@@ -1083,9 +1053,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.0.tgz",
|
||||
"integrity": "sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A==",
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.1.tgz",
|
||||
"integrity": "sha512-ZDCjgccSdYPw5Bxh+my4Z0lJU96ZDN7jbBzvmEn0FZx3RtU1C7VWl6NbDx94bwY3V5YsgwRzJPOgeY2Q/nLG8A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1097,16 +1067,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.0.tgz",
|
||||
"integrity": "sha512-O9Re9P1BmBLFJyikRbQpLku/QA3/AueZNO9WePLBwQrvkixTmDe8u76B6CYUAITRl/rHawggEqUGn5QIkVRLMw==",
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.1.tgz",
|
||||
"integrity": "sha512-OUd+vJS05sSkOip+BkZ/2NS8RMxrAAJemsC6vU3kmfLyeaJT0TftHkV9mcx2107MmsBVXXexhVu4F0TZXyMl4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.59.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.59.0",
|
||||
"@typescript-eslint/types": "8.59.0",
|
||||
"@typescript-eslint/visitor-keys": "8.59.0",
|
||||
"@typescript-eslint/project-service": "8.59.1",
|
||||
"@typescript-eslint/tsconfig-utils": "8.59.1",
|
||||
"@typescript-eslint/types": "8.59.1",
|
||||
"@typescript-eslint/visitor-keys": "8.59.1",
|
||||
"debug": "^4.4.3",
|
||||
"minimatch": "^10.2.2",
|
||||
"semver": "^7.7.3",
|
||||
@@ -1125,16 +1095,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.0.tgz",
|
||||
"integrity": "sha512-I1R/K7V07XsMJ12Oaxg/O9GfrysGTmCRhvZJBv0RE0NcULMzjqVpR5kRRQjHsz3J/bElU7HwCO7zkqL+MSUz+g==",
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.1.tgz",
|
||||
"integrity": "sha512-3pIeoXhCeYH9FSCBI8P3iNwJlGuzPlYKkTlen2O9T1DSeeg8UG8jstq6BLk+Mda0qup7mgk4z4XL4OzRaxZ8LA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.9.1",
|
||||
"@typescript-eslint/scope-manager": "8.59.0",
|
||||
"@typescript-eslint/types": "8.59.0",
|
||||
"@typescript-eslint/typescript-estree": "8.59.0"
|
||||
"@typescript-eslint/scope-manager": "8.59.1",
|
||||
"@typescript-eslint/types": "8.59.1",
|
||||
"@typescript-eslint/typescript-estree": "8.59.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1149,13 +1119,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.0.tgz",
|
||||
"integrity": "sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q==",
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.1.tgz",
|
||||
"integrity": "sha512-LdDNl6C5iJExcM0Yh0PwAIBb9PrSiCsWamF/JyEZawm3kFDnRoaq3LGE4bpyRao/fWeGKKyw7icx0YxrLFC5Cg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.59.0",
|
||||
"@typescript-eslint/types": "8.59.1",
|
||||
"eslint-visitor-keys": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1672,9 +1642,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/es-module-lexer": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz",
|
||||
"integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz",
|
||||
"integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -1710,9 +1680,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "10.2.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.2.1.tgz",
|
||||
"integrity": "sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q==",
|
||||
"version": "10.3.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.3.0.tgz",
|
||||
"integrity": "sha512-XbEXaRva5cF0ZQB8w6MluHA0kZZfV2DuCMJ3ozyEOHLwDpZX2Lmm/7Pp0xdJmI0GL1W05VH5VwIFHEm1Vcw2gw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2031,9 +2001,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/express-rate-limit": {
|
||||
"version": "8.4.0",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.4.0.tgz",
|
||||
"integrity": "sha512-gDK8yiqKxrGta+3WtON59arrrw6GLmadA1qoFgYXzdcch8fmKDID2XqO8itsi3f1wufXYPT51387dN6cvVBS3Q==",
|
||||
"version": "8.4.1",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.4.1.tgz",
|
||||
"integrity": "sha512-NGVYwQSAyEQgzxX1iCM978PP9AdO/hW93gMcF6ZwQCm+rFvLsBH6w4xcXWTcliS8La5EPRN3p9wzItqBwJrfNw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ip-address": "10.1.0"
|
||||
@@ -2267,9 +2237,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/globals": {
|
||||
"version": "17.5.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-17.5.0.tgz",
|
||||
"integrity": "sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g==",
|
||||
"version": "17.6.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz",
|
||||
"integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -2316,9 +2286,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/hono": {
|
||||
"version": "4.12.15",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.15.tgz",
|
||||
"integrity": "sha512-qM0jDhFEaCBb4TxoW7f53Qrpv9RBiayUHo0S52JudprkhvpjIrGoU1mnnr29Fvd1U335ZFPZQY1wlkqgfGXyLg==",
|
||||
"version": "4.12.16",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.16.tgz",
|
||||
"integrity": "sha512-jN0ZewiNAWSe5khM3EyCmBb250+b40wWbwNILNfEvq84VREWwOIkuUsFONk/3i3nqkz7Oe1PcpM2mwQEK2L9Kg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
@@ -2440,9 +2410,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/jose": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz",
|
||||
"integrity": "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==",
|
||||
"version": "6.2.3",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz",
|
||||
"integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
@@ -2654,9 +2624,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2678,9 +2645,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2702,9 +2666,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2726,9 +2687,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2895,9 +2853,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"version": "3.3.12",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
||||
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -3107,9 +3065,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.10",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
|
||||
"integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
|
||||
"version": "8.5.13",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.13.tgz",
|
||||
"integrity": "sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -3493,9 +3451,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyexec": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.1.tgz",
|
||||
"integrity": "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==",
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz",
|
||||
"integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -3932,9 +3890,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "4.3.6",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.1.tgz",
|
||||
"integrity": "sha512-a6ENMBBGZBsnlSebQ/eKCguSBeGKSf4O7BPnqVPmYGtpBYI7VSqoVqw+QcB7kPRjbqPwhYTpFbVj/RqNz/CT0Q==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
|
||||
14
package.json
14
package.json
@@ -3,7 +3,7 @@
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"version": "0.31.0",
|
||||
"version": "0.32.0",
|
||||
"description": "An ACP-compatible coding agent powered by the Claude Agent SDK (TypeScript)",
|
||||
"main": "dist/lib.js",
|
||||
"types": "dist/lib.d.ts",
|
||||
@@ -60,19 +60,19 @@
|
||||
"author": "Zed Industries",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "0.20.0",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.2.119",
|
||||
"@agentclientprotocol/sdk": "0.21.0",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.2.126",
|
||||
"zod": "^3.25.0 || ^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@anthropic-ai/sdk": "0.89.0",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@types/node": "25.6.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.59.0",
|
||||
"@typescript-eslint/parser": "8.59.0",
|
||||
"eslint": "10.2.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.59.1",
|
||||
"@typescript-eslint/parser": "8.59.1",
|
||||
"eslint": "10.3.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"globals": "17.5.0",
|
||||
"globals": "17.6.0",
|
||||
"prettier": "3.8.3",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "6.0.3",
|
||||
|
||||
317
src/acp-agent.ts
317
src/acp-agent.ts
@@ -73,7 +73,7 @@ import {
|
||||
planEntries,
|
||||
registerHookCallback,
|
||||
toolInfoFromToolUse,
|
||||
toolUpdateFromEditToolResponse,
|
||||
toolUpdateFromDiffToolResponse,
|
||||
toolUpdateFromToolResult,
|
||||
} from "./tools.js";
|
||||
import { nodeToWebReadable, nodeToWebWritable, Pushable, unreachable } from "./utils.js";
|
||||
@@ -274,12 +274,22 @@ export async function claudeCliPath(): Promise<string> {
|
||||
const { createRequire } = await import("node:module");
|
||||
const req = createRequire(import.meta.resolve("@anthropic-ai/claude-agent-sdk"));
|
||||
const ext = process.platform === "win32" ? ".exe" : "";
|
||||
// On linux, both glibc and musl variants may be installed side-by-side
|
||||
// (e.g. bunx hydrates every optional dep), so picking one by trial is
|
||||
// unreliable: the wrong binary segfaults at runtime instead of failing to
|
||||
// spawn. Detect the runtime libc and prefer the matching variant, falling
|
||||
// back to the other only if the preferred one isn't installed.
|
||||
const candidates =
|
||||
process.platform === "linux"
|
||||
? [
|
||||
`@anthropic-ai/claude-agent-sdk-linux-${process.arch}-musl/claude${ext}`,
|
||||
`@anthropic-ai/claude-agent-sdk-linux-${process.arch}/claude${ext}`,
|
||||
]
|
||||
? isMuslLibc()
|
||||
? [
|
||||
`@anthropic-ai/claude-agent-sdk-linux-${process.arch}-musl/claude${ext}`,
|
||||
`@anthropic-ai/claude-agent-sdk-linux-${process.arch}/claude${ext}`,
|
||||
]
|
||||
: [
|
||||
`@anthropic-ai/claude-agent-sdk-linux-${process.arch}/claude${ext}`,
|
||||
`@anthropic-ai/claude-agent-sdk-linux-${process.arch}-musl/claude${ext}`,
|
||||
]
|
||||
: [`@anthropic-ai/claude-agent-sdk-${process.platform}-${process.arch}/claude${ext}`];
|
||||
for (const candidate of candidates) {
|
||||
try {
|
||||
@@ -294,6 +304,15 @@ export async function claudeCliPath(): Promise<string> {
|
||||
);
|
||||
}
|
||||
|
||||
function isMuslLibc(): boolean {
|
||||
// process.report.getReport().header.glibcVersionRuntime is populated when
|
||||
// Node is dynamically linked against glibc, and absent on musl.
|
||||
const report = process.report?.getReport() as
|
||||
| { header?: { glibcVersionRuntime?: string } }
|
||||
| undefined;
|
||||
return !report?.header?.glibcVersionRuntime;
|
||||
}
|
||||
|
||||
function shouldHideClaudeAuth(): boolean {
|
||||
return process.argv.includes("--hide-claude-auth");
|
||||
}
|
||||
@@ -366,29 +385,36 @@ const PERMISSION_MODE_ALIASES: Record<string, PermissionMode> = {
|
||||
bypass: "bypassPermissions",
|
||||
};
|
||||
|
||||
export function resolvePermissionMode(defaultMode?: unknown): PermissionMode {
|
||||
export function resolvePermissionMode(
|
||||
defaultMode?: unknown,
|
||||
logger: Logger = console,
|
||||
): PermissionMode {
|
||||
if (defaultMode === undefined) {
|
||||
return "default";
|
||||
}
|
||||
|
||||
if (typeof defaultMode !== "string") {
|
||||
throw new Error("Invalid permissions.defaultMode: expected a string.");
|
||||
logger.error("Ignoring permissions.defaultMode from settings: expected a string.");
|
||||
return "default";
|
||||
}
|
||||
|
||||
const normalized = defaultMode.trim().toLowerCase();
|
||||
if (normalized === "") {
|
||||
throw new Error("Invalid permissions.defaultMode: expected a non-empty string.");
|
||||
logger.error("Ignoring permissions.defaultMode from settings: expected a non-empty string.");
|
||||
return "default";
|
||||
}
|
||||
|
||||
const mapped = PERMISSION_MODE_ALIASES[normalized];
|
||||
if (!mapped) {
|
||||
throw new Error(`Invalid permissions.defaultMode: ${defaultMode}.`);
|
||||
logger.error(`Ignoring permissions.defaultMode from settings: unknown value '${defaultMode}'.`);
|
||||
return "default";
|
||||
}
|
||||
|
||||
if (mapped === "bypassPermissions" && !ALLOW_BYPASS) {
|
||||
throw new Error(
|
||||
"Invalid permissions.defaultMode: bypassPermissions is not available when running as root.",
|
||||
logger.error(
|
||||
"Ignoring permissions.defaultMode from settings: bypassPermissions is not available when running as root.",
|
||||
);
|
||||
return "default";
|
||||
}
|
||||
|
||||
return mapped;
|
||||
@@ -1283,7 +1309,7 @@ export class ClaudeAcpAgent implements Agent {
|
||||
// effort changes and effort changes induced by a model switch go through
|
||||
// the same path.
|
||||
|
||||
await this.applyConfigOptionValue(session, params.configId, resolvedValue);
|
||||
await this.applyConfigOptionValue(params.sessionId, session, params.configId, resolvedValue);
|
||||
|
||||
return { configOptions: session.configOptions };
|
||||
}
|
||||
@@ -1300,8 +1326,17 @@ export class ClaudeAcpAgent implements Agent {
|
||||
default:
|
||||
throw new Error("Invalid Mode");
|
||||
}
|
||||
|
||||
const session = this.sessions[sessionId];
|
||||
if (!session) {
|
||||
throw new Error("Session not found");
|
||||
}
|
||||
if (!session.modes.availableModes.some((mode) => mode.id === modeId)) {
|
||||
throw new Error(`Mode ${modeId} is not available in this session`);
|
||||
}
|
||||
|
||||
try {
|
||||
await this.sessions[sessionId].query.setPermissionMode(modeId);
|
||||
await session.query.setPermissionMode(modeId);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
if (!error.message) {
|
||||
@@ -1371,7 +1406,7 @@ export class ClaudeAcpAgent implements Agent {
|
||||
}
|
||||
|
||||
if (toolName === "ExitPlanMode") {
|
||||
const options = [
|
||||
const optionsAll = [
|
||||
{ kind: "allow_always", name: 'Yes, and use "auto" mode', optionId: "auto" },
|
||||
{
|
||||
kind: "allow_always",
|
||||
@@ -1382,12 +1417,20 @@ export class ClaudeAcpAgent implements Agent {
|
||||
{ kind: "reject_once", name: "No, keep planning", optionId: "plan" },
|
||||
];
|
||||
if (ALLOW_BYPASS) {
|
||||
options.unshift({
|
||||
optionsAll.unshift({
|
||||
kind: "allow_always",
|
||||
name: "Yes, and bypass permissions",
|
||||
optionId: "bypassPermissions",
|
||||
});
|
||||
}
|
||||
// Filter against the session's currently-advertised modes so we never
|
||||
// present options the active model can't honor (e.g. `auto` on Haiku).
|
||||
// `bypassPermissions` is already covered by `availableModes` via
|
||||
// `buildAvailableModes`/`ALLOW_BYPASS`. The `plan` option is a
|
||||
// "keep planning" reject path; it's always present in `availableModes`.
|
||||
const options = optionsAll.filter((o) =>
|
||||
session.modes.availableModes.some((m) => m.id === o.optionId),
|
||||
);
|
||||
|
||||
const response = await this.client.requestPermission({
|
||||
options,
|
||||
@@ -1406,27 +1449,30 @@ export class ClaudeAcpAgent implements Agent {
|
||||
if (signal.aborted || response.outcome?.outcome === "cancelled") {
|
||||
throw new Error("Tool use aborted");
|
||||
}
|
||||
const selectedMode =
|
||||
response.outcome?.outcome === "selected" ? response.outcome.optionId : undefined;
|
||||
const selectedModeWasOffered = options.some((option) => option.optionId === selectedMode);
|
||||
if (
|
||||
response.outcome?.outcome === "selected" &&
|
||||
(response.outcome.optionId === "default" ||
|
||||
response.outcome.optionId === "acceptEdits" ||
|
||||
response.outcome.optionId === "auto" ||
|
||||
response.outcome.optionId === "bypassPermissions")
|
||||
selectedModeWasOffered &&
|
||||
(selectedMode === "default" ||
|
||||
selectedMode === "acceptEdits" ||
|
||||
selectedMode === "auto" ||
|
||||
selectedMode === "bypassPermissions")
|
||||
) {
|
||||
await this.client.sessionUpdate({
|
||||
sessionId,
|
||||
update: {
|
||||
sessionUpdate: "current_mode_update",
|
||||
currentModeId: response.outcome.optionId,
|
||||
currentModeId: selectedMode,
|
||||
},
|
||||
});
|
||||
await this.updateConfigOption(sessionId, "mode", response.outcome.optionId);
|
||||
await this.updateConfigOption(sessionId, "mode", selectedMode);
|
||||
|
||||
return {
|
||||
behavior: "allow",
|
||||
updatedInput: toolInput,
|
||||
updatedPermissions: suggestions ?? [
|
||||
{ type: "setMode", mode: response.outcome.optionId, destination: "session" },
|
||||
{ type: "setMode", mode: selectedMode, destination: "session" },
|
||||
],
|
||||
};
|
||||
} else {
|
||||
@@ -1524,7 +1570,7 @@ export class ClaudeAcpAgent implements Agent {
|
||||
const session = this.sessions[sessionId];
|
||||
if (!session) return;
|
||||
|
||||
await this.applyConfigOptionValue(session, configId, value);
|
||||
await this.applyConfigOptionValue(sessionId, session, configId, value);
|
||||
|
||||
await this.client.sessionUpdate({
|
||||
sessionId,
|
||||
@@ -1536,13 +1582,16 @@ export class ClaudeAcpAgent implements Agent {
|
||||
}
|
||||
|
||||
private async applyConfigOptionValue(
|
||||
sessionId: string,
|
||||
session: Session,
|
||||
configId: string,
|
||||
value: string,
|
||||
): Promise<void> {
|
||||
// Sync top-level session state
|
||||
if (configId === "mode") {
|
||||
session.modes = { ...session.modes, currentModeId: value };
|
||||
session.configOptions = session.configOptions.map((o) =>
|
||||
o.id === configId && typeof o.currentValue === "string" ? { ...o, currentValue: value } : o,
|
||||
);
|
||||
} else if (configId === "model") {
|
||||
if (session.models.currentModelId !== value) {
|
||||
// The cached context window was learned for the previous model; reset
|
||||
@@ -1552,10 +1601,38 @@ export class ClaudeAcpAgent implements Agent {
|
||||
session.contextWindowSize = inferContextWindowFromModel(value) ?? DEFAULT_CONTEXT_WINDOW;
|
||||
}
|
||||
session.models = { ...session.models, currentModelId: value };
|
||||
}
|
||||
|
||||
// Update configOptions
|
||||
if (configId === "model") {
|
||||
// Recompute availableModes for the new model and clamp the current
|
||||
// mode if the SDK no longer offers it (today: "auto" on Haiku).
|
||||
// `ModelInfo.supportsAutoMode` is the canonical SDK signal.
|
||||
const newModelInfo = session.modelInfos.find((m) => m.value === value);
|
||||
const newAvailableModes = buildAvailableModes(newModelInfo);
|
||||
// Capture BEFORE mutating session.modes so the log message reflects
|
||||
// the invalidated mode rather than "default".
|
||||
const previousModeId = session.modes.currentModeId;
|
||||
let modeDowngraded = false;
|
||||
if (!newAvailableModes.some((m) => m.id === previousModeId)) {
|
||||
session.modes = {
|
||||
availableModes: newAvailableModes,
|
||||
currentModeId: "default",
|
||||
};
|
||||
try {
|
||||
await session.query.setPermissionMode("default");
|
||||
} catch (err) {
|
||||
// Failing the entire model switch over a bookkeeping sync error is
|
||||
// worse UX than logging and continuing; the user explicitly asked
|
||||
// to change models. The next setPermissionMode from the user will
|
||||
// either succeed or surface a fresh error.
|
||||
this.logger.error(
|
||||
`Failed to sync permissionMode to "default" after model switch invalidated "${previousModeId}":`,
|
||||
err,
|
||||
);
|
||||
}
|
||||
modeDowngraded = true;
|
||||
} else {
|
||||
session.modes = { ...session.modes, availableModes: newAvailableModes };
|
||||
}
|
||||
|
||||
// Rebuild config options since effort levels depend on the selected model
|
||||
const effortOpt = session.configOptions.find((o) => o.id === "effort");
|
||||
const currentEffort =
|
||||
@@ -1576,6 +1653,22 @@ export class ClaudeAcpAgent implements Agent {
|
||||
effortLevel: newEffort as Settings["effortLevel"],
|
||||
});
|
||||
}
|
||||
|
||||
// Emit current_mode_update only after session.modes AND
|
||||
// session.configOptions have been fully reconciled. This way, a failure
|
||||
// in the configOptions/effort rebuild above can't leave the client with
|
||||
// a clamped currentModeId but stale configOptions, and the notification
|
||||
// still precedes the caller's config_option_update so order-sensitive
|
||||
// clients update currentModeId before re-rendering the option list.
|
||||
if (modeDowngraded) {
|
||||
await this.client.sessionUpdate({
|
||||
sessionId,
|
||||
update: {
|
||||
sessionUpdate: "current_mode_update",
|
||||
currentModeId: "default",
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
session.configOptions = session.configOptions.map((o) =>
|
||||
o.id === configId && typeof o.currentValue === "string" ? { ...o, currentValue: value } : o,
|
||||
@@ -1701,6 +1794,7 @@ export class ClaudeAcpAgent implements Agent {
|
||||
|
||||
const permissionMode = resolvePermissionMode(
|
||||
settingsManager.getSettings().permissions?.defaultMode,
|
||||
this.logger,
|
||||
);
|
||||
|
||||
// Extract options from _meta if provided
|
||||
@@ -1760,7 +1854,7 @@ export class ClaudeAcpAgent implements Agent {
|
||||
allowDangerouslySkipPermissions: ALLOW_BYPASS,
|
||||
permissionMode,
|
||||
canUseTool: this.canUseTool(sessionId),
|
||||
pathToClaudeCodeExecutable: process.env.CLAUDE_CODE_EXECUTABLE,
|
||||
pathToClaudeCodeExecutable: process.env.CLAUDE_CODE_EXECUTABLE ?? (await claudeCliPath()),
|
||||
extraArgs: {
|
||||
...userProvidedOptions?.extraArgs,
|
||||
"replay-user-messages": "",
|
||||
@@ -1839,46 +1933,52 @@ export class ClaudeAcpAgent implements Agent {
|
||||
);
|
||||
}
|
||||
|
||||
const models = await getAvailableModels(q, initializationResult.models, settingsManager);
|
||||
const models = await getAvailableModels(
|
||||
q,
|
||||
initializationResult.models,
|
||||
settingsManager,
|
||||
this.logger,
|
||||
);
|
||||
|
||||
const availableModes = [
|
||||
{
|
||||
id: "auto",
|
||||
name: "Auto",
|
||||
description: "Use a model classifier to approve/deny permission prompts",
|
||||
},
|
||||
{
|
||||
id: "default",
|
||||
name: "Default",
|
||||
description: "Standard behavior, prompts for dangerous operations",
|
||||
},
|
||||
{
|
||||
id: "acceptEdits",
|
||||
name: "Accept Edits",
|
||||
description: "Auto-accept file edit operations",
|
||||
},
|
||||
{
|
||||
id: "plan",
|
||||
name: "Plan Mode",
|
||||
description: "Planning mode, no actual tool execution",
|
||||
},
|
||||
{
|
||||
id: "dontAsk",
|
||||
name: "Don't Ask",
|
||||
description: "Don't prompt for permissions, deny if not pre-approved",
|
||||
},
|
||||
];
|
||||
// Only works in non-root mode
|
||||
if (ALLOW_BYPASS) {
|
||||
availableModes.push({
|
||||
id: "bypassPermissions",
|
||||
name: "Bypass Permissions",
|
||||
description: "Bypass all permission checks",
|
||||
});
|
||||
// Gate `auto` (and future model-specific modes) on the resolved model's
|
||||
// `ModelInfo`. See `buildAvailableModes` for the canonical SDK signal.
|
||||
const currentModelInfo = initializationResult.models.find(
|
||||
(m) => m.value === models.currentModelId,
|
||||
);
|
||||
const availableModes = buildAvailableModes(currentModelInfo);
|
||||
|
||||
// Clamp `permissionMode` if the resolved session does not offer it. The
|
||||
// common case is `permissions.defaultMode: "auto"` resolving to a model
|
||||
// that does not support auto mode (e.g. Haiku); without this clamp the
|
||||
// SDK would later throw `"auto mode unavailable for this model"` from
|
||||
// `setPermissionMode`. Keep `permissionMode` as the resolved user intent
|
||||
// (matches what was passed into `options.permissionMode` above) and use
|
||||
// `effectiveMode` for the post-clamp value the session actually runs in.
|
||||
let effectiveMode: PermissionMode = permissionMode;
|
||||
if (!availableModes.some((m) => m.id === effectiveMode)) {
|
||||
if (effectiveMode === "auto") {
|
||||
this.logger.error(
|
||||
`permissions.defaultMode "auto" is not available for model ` +
|
||||
`"${models.currentModelId}"; falling back to "default".`,
|
||||
);
|
||||
} else {
|
||||
this.logger.error(
|
||||
`permissions.defaultMode "${effectiveMode}" is not available in ` +
|
||||
`this session; falling back to "default".`,
|
||||
);
|
||||
}
|
||||
effectiveMode = "default";
|
||||
// Sync the SDK so it doesn't keep "auto" cached internally. Wrapped in
|
||||
// try/catch since failing here would abort session creation entirely.
|
||||
try {
|
||||
await q.setPermissionMode("default");
|
||||
} catch (err) {
|
||||
this.logger.error("Failed to sync clamped permissionMode to SDK:", err);
|
||||
}
|
||||
}
|
||||
|
||||
const modes = {
|
||||
currentModeId: permissionMode,
|
||||
currentModeId: effectiveMode,
|
||||
availableModes,
|
||||
};
|
||||
|
||||
@@ -2021,6 +2121,58 @@ function createEnvForGateway(gatewayMeta?: GatewayAuthMeta) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the list of permission modes the agent will advertise for the given
|
||||
* model. `auto` is gated by `ModelInfo.supportsAutoMode === true`, which is
|
||||
* the SDK's model-level availability signal. `undefined`/`false` both exclude
|
||||
* `auto`. `bypassPermissions` is still gated by `ALLOW_BYPASS`.
|
||||
*/
|
||||
function buildAvailableModes(modelInfo: ModelInfo | undefined): SessionModeState["availableModes"] {
|
||||
const modes: SessionModeState["availableModes"] = [];
|
||||
|
||||
// Only advertise "auto" when the SDK reports the model supports it.
|
||||
if (modelInfo?.supportsAutoMode === true) {
|
||||
modes.push({
|
||||
id: "auto",
|
||||
name: "Auto",
|
||||
description: "Use a model classifier to approve/deny permission prompts",
|
||||
});
|
||||
}
|
||||
|
||||
modes.push(
|
||||
{
|
||||
id: "default",
|
||||
name: "Default",
|
||||
description: "Standard behavior, prompts for dangerous operations",
|
||||
},
|
||||
{
|
||||
id: "acceptEdits",
|
||||
name: "Accept Edits",
|
||||
description: "Auto-accept file edit operations",
|
||||
},
|
||||
{
|
||||
id: "plan",
|
||||
name: "Plan Mode",
|
||||
description: "Planning mode, no actual tool execution",
|
||||
},
|
||||
{
|
||||
id: "dontAsk",
|
||||
name: "Don't Ask",
|
||||
description: "Don't prompt for permissions, deny if not pre-approved",
|
||||
},
|
||||
);
|
||||
|
||||
if (ALLOW_BYPASS) {
|
||||
modes.push({
|
||||
id: "bypassPermissions",
|
||||
name: "Bypass Permissions",
|
||||
description: "Bypass all permission checks",
|
||||
});
|
||||
}
|
||||
|
||||
return modes;
|
||||
}
|
||||
|
||||
function buildConfigOptions(
|
||||
modes: SessionModeState,
|
||||
models: SessionModelState,
|
||||
@@ -2087,7 +2239,7 @@ function buildConfigOptions(
|
||||
id: "effort",
|
||||
name: "Effort",
|
||||
description: "Available effort levels for this model",
|
||||
category: "effort",
|
||||
category: "thought_level",
|
||||
type: "select",
|
||||
currentValue: validEffort,
|
||||
options: effortOptions,
|
||||
@@ -2170,10 +2322,27 @@ function resolveModelPreference(models: ModelInfo[], preference: string): ModelI
|
||||
return bestMatch;
|
||||
}
|
||||
|
||||
function resolveSettingsModel(
|
||||
models: ModelInfo[],
|
||||
settingsModel: unknown,
|
||||
logger: Logger,
|
||||
): ModelInfo | null {
|
||||
if (settingsModel === undefined) {
|
||||
return null;
|
||||
}
|
||||
if (typeof settingsModel !== "string") {
|
||||
const typeLabel = settingsModel === null ? "null" : typeof settingsModel;
|
||||
logger.error(`Ignoring model from settings: expected a string, got ${typeLabel}.`);
|
||||
return null;
|
||||
}
|
||||
return resolveModelPreference(models, settingsModel);
|
||||
}
|
||||
|
||||
async function getAvailableModels(
|
||||
query: Query,
|
||||
models: ModelInfo[],
|
||||
settingsManager: SettingsManager,
|
||||
logger: Logger,
|
||||
): Promise<SessionModelState> {
|
||||
const settings = settingsManager.getSettings();
|
||||
|
||||
@@ -2188,8 +2357,8 @@ async function getAvailableModels(
|
||||
if (match) {
|
||||
currentModel = match;
|
||||
}
|
||||
} else if (settings.model) {
|
||||
const match = resolveModelPreference(models, settings.model);
|
||||
} else {
|
||||
const match = resolveSettingsModel(models, settings.model, logger);
|
||||
if (match) {
|
||||
currentModel = match;
|
||||
}
|
||||
@@ -2434,8 +2603,18 @@ export function toAcpNotifications(
|
||||
onPostToolUseHook: async (toolUseId, toolInput, toolResponse) => {
|
||||
const toolUse = toolUseCache[toolUseId];
|
||||
if (toolUse) {
|
||||
// Both `Edit` and `Write` produce a structuredPatch in their
|
||||
// PostToolUse tool_response. For Edit the diff replaces the
|
||||
// optimistic content built at tool_use time. For Write the
|
||||
// optimistic content (built from `input.content` alone with
|
||||
// `oldText: null`) shows "creation" semantics regardless of
|
||||
// whether the file existed; the structuredPatch from the
|
||||
// hook lets us emit the real diff for `type: "update"`. The
|
||||
// helper returns `{}` if the response shape isn't usable.
|
||||
const editDiff =
|
||||
toolUse.name === "Edit" ? toolUpdateFromEditToolResponse(toolResponse) : {};
|
||||
toolUse.name === "Edit" || toolUse.name === "Write"
|
||||
? toolUpdateFromDiffToolResponse(toolResponse)
|
||||
: {};
|
||||
const update: SessionNotification["update"] = {
|
||||
_meta: {
|
||||
claudeCode: {
|
||||
|
||||
@@ -39,7 +39,7 @@ describe("ClaudeAcpAgent settings", () => {
|
||||
initializationResult: async () => ({
|
||||
models: [
|
||||
{
|
||||
value: "claude-sonnet-4-5",
|
||||
value: "claude-sonnet-4-6",
|
||||
displayName: "Claude Sonnet 4.5",
|
||||
description: "Default",
|
||||
},
|
||||
@@ -145,12 +145,12 @@ describe("ClaudeAcpAgent settings", () => {
|
||||
expect(response.modes.currentModeId).toBe("default");
|
||||
});
|
||||
|
||||
it("throws when permissions.defaultMode is not a string", async () => {
|
||||
it("falls back to 'default' when permissions.defaultMode is invalid", async () => {
|
||||
await fs.promises.writeFile(
|
||||
path.join(tempDir, "settings.json"),
|
||||
JSON.stringify({
|
||||
permissions: {
|
||||
defaultMode: 123,
|
||||
defaultMode: "not-a-real-mode",
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -158,18 +158,208 @@ describe("ClaudeAcpAgent settings", () => {
|
||||
const projectDir = path.join(tempDir, "project");
|
||||
await fs.promises.mkdir(projectDir, { recursive: true });
|
||||
|
||||
mockQuery();
|
||||
const { getCapturedOptions } = mockQuery();
|
||||
|
||||
const { ClaudeAcpAgent } = await import("../acp-agent.js");
|
||||
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
|
||||
|
||||
await expect(
|
||||
(agent as any).createSession({
|
||||
const response = await (agent as any).createSession({
|
||||
cwd: projectDir,
|
||||
mcpServers: [],
|
||||
_meta: { disableBuiltInTools: true },
|
||||
});
|
||||
|
||||
// Bad mode is ignored at the usage site; session creation must not throw.
|
||||
expect(getCapturedOptions().permissionMode).toBe("default");
|
||||
expect(response.modes.currentModeId).toBe("default");
|
||||
});
|
||||
|
||||
it("ignores model from settings when it is not a string", async () => {
|
||||
await fs.promises.writeFile(
|
||||
path.join(tempDir, "settings.json"),
|
||||
JSON.stringify({
|
||||
model: 123,
|
||||
}),
|
||||
);
|
||||
|
||||
const projectDir = path.join(tempDir, "project");
|
||||
await fs.promises.mkdir(projectDir, { recursive: true });
|
||||
|
||||
const { setModelSpy } = mockQuery();
|
||||
|
||||
const { ClaudeAcpAgent } = await import("../acp-agent.js");
|
||||
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
|
||||
|
||||
const response = await (agent as any).createSession({
|
||||
cwd: projectDir,
|
||||
mcpServers: [],
|
||||
_meta: { disableBuiltInTools: true },
|
||||
});
|
||||
|
||||
// Bad model is ignored at the usage site; falls back to the first SDK model.
|
||||
expect(setModelSpy).toHaveBeenCalledWith("claude-sonnet-4-6");
|
||||
expect(response.models.currentModelId).toBe("claude-sonnet-4-6");
|
||||
});
|
||||
|
||||
describe("auto mode availability per model", () => {
|
||||
function mockQueryWithModels(models: any[]): {
|
||||
getCapturedOptions: () => any;
|
||||
setModelSpy: ReturnType<typeof vi.fn>;
|
||||
setPermissionModeSpy: ReturnType<typeof vi.fn>;
|
||||
} {
|
||||
let capturedOptions: any;
|
||||
const setModelSpy = vi.fn();
|
||||
const setPermissionModeSpy = vi.fn();
|
||||
querySpy.mockImplementation(({ options }: any) => {
|
||||
capturedOptions = options;
|
||||
return {
|
||||
initializationResult: async () => ({ models }),
|
||||
setModel: setModelSpy,
|
||||
setPermissionMode: setPermissionModeSpy,
|
||||
supportedCommands: async () => [],
|
||||
} as any;
|
||||
});
|
||||
return {
|
||||
getCapturedOptions: () => capturedOptions,
|
||||
setModelSpy,
|
||||
setPermissionModeSpy,
|
||||
};
|
||||
}
|
||||
|
||||
it("omits `auto` from availableModes when the resolved model lacks supportsAutoMode", async () => {
|
||||
const projectDir = path.join(tempDir, "project");
|
||||
await fs.promises.mkdir(projectDir, { recursive: true });
|
||||
|
||||
mockQueryWithModels([
|
||||
{
|
||||
value: "claude-haiku-4-5",
|
||||
displayName: "Claude Haiku",
|
||||
description: "Fast",
|
||||
// supportsAutoMode intentionally omitted
|
||||
},
|
||||
]);
|
||||
|
||||
const { ClaudeAcpAgent } = await import("../acp-agent.js");
|
||||
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
|
||||
|
||||
const response = await (agent as any).createSession({
|
||||
cwd: projectDir,
|
||||
mcpServers: [],
|
||||
_meta: { disableBuiltInTools: true },
|
||||
}),
|
||||
).rejects.toThrow("Invalid permissions.defaultMode");
|
||||
});
|
||||
|
||||
const modeIds: string[] = response.modes.availableModes.map((m: any) => m.id);
|
||||
expect(modeIds).not.toContain("auto");
|
||||
expect(modeIds).toEqual(
|
||||
expect.arrayContaining(["default", "acceptEdits", "plan", "dontAsk"]),
|
||||
);
|
||||
});
|
||||
|
||||
it("includes `auto` when the resolved model has supportsAutoMode: true", async () => {
|
||||
const projectDir = path.join(tempDir, "project");
|
||||
await fs.promises.mkdir(projectDir, { recursive: true });
|
||||
|
||||
mockQueryWithModels([
|
||||
{
|
||||
value: "claude-opus-4-5",
|
||||
displayName: "Claude Opus",
|
||||
description: "Most capable",
|
||||
supportsAutoMode: true,
|
||||
},
|
||||
]);
|
||||
|
||||
const { ClaudeAcpAgent } = await import("../acp-agent.js");
|
||||
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
|
||||
|
||||
const response = await (agent as any).createSession({
|
||||
cwd: projectDir,
|
||||
mcpServers: [],
|
||||
_meta: { disableBuiltInTools: true },
|
||||
});
|
||||
|
||||
const modeIds: string[] = response.modes.availableModes.map((m: any) => m.id);
|
||||
expect(modeIds).toContain("auto");
|
||||
});
|
||||
|
||||
it("clamps permissions.defaultMode='auto' to 'default' on a model that lacks supportsAutoMode", async () => {
|
||||
await fs.promises.writeFile(
|
||||
path.join(tempDir, "settings.json"),
|
||||
JSON.stringify({ permissions: { defaultMode: "auto" } }),
|
||||
);
|
||||
|
||||
const projectDir = path.join(tempDir, "project");
|
||||
await fs.promises.mkdir(projectDir, { recursive: true });
|
||||
|
||||
const { getCapturedOptions, setPermissionModeSpy } = mockQueryWithModels([
|
||||
{
|
||||
value: "claude-haiku-4-5",
|
||||
displayName: "Claude Haiku",
|
||||
description: "Fast",
|
||||
// supportsAutoMode intentionally omitted
|
||||
},
|
||||
]);
|
||||
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
|
||||
try {
|
||||
const { ClaudeAcpAgent } = await import("../acp-agent.js");
|
||||
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
|
||||
|
||||
const response = await (agent as any).createSession({
|
||||
cwd: projectDir,
|
||||
mcpServers: [],
|
||||
_meta: { disableBuiltInTools: true },
|
||||
});
|
||||
|
||||
// Options.permissionMode is built before init resolves, so it still
|
||||
// carries the user-typed value; the SDK was synced via
|
||||
// setPermissionMode after we discovered the model can't honor it.
|
||||
expect(getCapturedOptions().permissionMode).toBe("auto");
|
||||
expect(setPermissionModeSpy).toHaveBeenCalledWith("default");
|
||||
expect(response.modes.currentModeId).toBe("default");
|
||||
expect(response.modes.availableModes.map((m: any) => m.id)).not.toContain("auto");
|
||||
|
||||
// A descriptive warning was logged so operators see the clamp.
|
||||
const messages = errorSpy.mock.calls.map((c) => c.join(" "));
|
||||
expect(messages.some((m) => m.includes("auto") && m.includes("claude-haiku-4-5"))).toBe(
|
||||
true,
|
||||
);
|
||||
} finally {
|
||||
errorSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("does not clamp permissions.defaultMode='auto' on a model that supports auto", async () => {
|
||||
await fs.promises.writeFile(
|
||||
path.join(tempDir, "settings.json"),
|
||||
JSON.stringify({ permissions: { defaultMode: "auto" } }),
|
||||
);
|
||||
|
||||
const projectDir = path.join(tempDir, "project");
|
||||
await fs.promises.mkdir(projectDir, { recursive: true });
|
||||
|
||||
const { getCapturedOptions, setPermissionModeSpy } = mockQueryWithModels([
|
||||
{
|
||||
value: "claude-opus-4-5",
|
||||
displayName: "Claude Opus",
|
||||
description: "Most capable",
|
||||
supportsAutoMode: true,
|
||||
},
|
||||
]);
|
||||
|
||||
const { ClaudeAcpAgent } = await import("../acp-agent.js");
|
||||
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
|
||||
|
||||
const response = await (agent as any).createSession({
|
||||
cwd: projectDir,
|
||||
mcpServers: [],
|
||||
_meta: { disableBuiltInTools: true },
|
||||
});
|
||||
|
||||
expect(getCapturedOptions().permissionMode).toBe("auto");
|
||||
expect(setPermissionModeSpy).not.toHaveBeenCalled();
|
||||
expect(response.modes.currentModeId).toBe("auto");
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves model aliases like opus[1m] to the correct model", async () => {
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
toolInfoFromToolUse,
|
||||
toDisplayPath,
|
||||
toolUpdateFromToolResult,
|
||||
toolUpdateFromEditToolResponse,
|
||||
toolUpdateFromDiffToolResponse,
|
||||
} from "../tools.js";
|
||||
import {
|
||||
toAcpNotifications,
|
||||
@@ -998,17 +998,17 @@ describe("toDisplayPath", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("toolUpdateFromEditToolResponse", () => {
|
||||
describe("toolUpdateFromDiffToolResponse", () => {
|
||||
it("should return empty for non-object input", () => {
|
||||
expect(toolUpdateFromEditToolResponse(null)).toEqual({});
|
||||
expect(toolUpdateFromEditToolResponse(undefined)).toEqual({});
|
||||
expect(toolUpdateFromEditToolResponse("string")).toEqual({});
|
||||
expect(toolUpdateFromDiffToolResponse(null)).toEqual({});
|
||||
expect(toolUpdateFromDiffToolResponse(undefined)).toEqual({});
|
||||
expect(toolUpdateFromDiffToolResponse("string")).toEqual({});
|
||||
});
|
||||
|
||||
it("should return empty when filePath or structuredPatch is missing", () => {
|
||||
expect(toolUpdateFromEditToolResponse({})).toEqual({});
|
||||
expect(toolUpdateFromEditToolResponse({ filePath: "/foo.ts" })).toEqual({});
|
||||
expect(toolUpdateFromEditToolResponse({ structuredPatch: [] })).toEqual({});
|
||||
expect(toolUpdateFromDiffToolResponse({})).toEqual({});
|
||||
expect(toolUpdateFromDiffToolResponse({ filePath: "/foo.ts" })).toEqual({});
|
||||
expect(toolUpdateFromDiffToolResponse({ structuredPatch: [] })).toEqual({});
|
||||
});
|
||||
|
||||
it("should build diff content from a single-hunk structuredPatch", () => {
|
||||
@@ -1025,7 +1025,7 @@ describe("toolUpdateFromEditToolResponse", () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(toolUpdateFromEditToolResponse(toolResponse)).toEqual({
|
||||
expect(toolUpdateFromDiffToolResponse(toolResponse)).toEqual({
|
||||
content: [
|
||||
{
|
||||
type: "diff",
|
||||
@@ -1059,7 +1059,7 @@ describe("toolUpdateFromEditToolResponse", () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(toolUpdateFromEditToolResponse(toolResponse)).toEqual({
|
||||
expect(toolUpdateFromDiffToolResponse(toolResponse)).toEqual({
|
||||
content: [
|
||||
{
|
||||
type: "diff",
|
||||
@@ -1095,7 +1095,7 @@ describe("toolUpdateFromEditToolResponse", () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(toolUpdateFromEditToolResponse(toolResponse)).toEqual({
|
||||
expect(toolUpdateFromDiffToolResponse(toolResponse)).toEqual({
|
||||
content: [
|
||||
{
|
||||
type: "diff",
|
||||
@@ -1114,7 +1114,7 @@ describe("toolUpdateFromEditToolResponse", () => {
|
||||
structuredPatch: [],
|
||||
};
|
||||
|
||||
expect(toolUpdateFromEditToolResponse(toolResponse)).toEqual({});
|
||||
expect(toolUpdateFromDiffToolResponse(toolResponse)).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2502,7 +2502,12 @@ describe("usage_update computation", () => {
|
||||
const session = agent.sessions["test-session"];
|
||||
expect(session.contextWindowSize).toBe(200000);
|
||||
|
||||
await (agent as any).applyConfigOptionValue(session, "model", "claude-opus-4-6-1m");
|
||||
await (agent as any).applyConfigOptionValue(
|
||||
"test-session",
|
||||
session,
|
||||
"model",
|
||||
"claude-opus-4-6-1m",
|
||||
);
|
||||
expect(session.contextWindowSize).toBe(1000000);
|
||||
|
||||
await agent.prompt({ sessionId: "test-session", prompt: [{ type: "text", text: "test" }] });
|
||||
@@ -2587,7 +2592,12 @@ describe("usage_update computation", () => {
|
||||
session.models = { ...session.models, currentModelId: "claude-opus-4-6-1m" };
|
||||
|
||||
// User flips the selector to a 200k model.
|
||||
await (agent as any).applyConfigOptionValue(session, "model", "claude-sonnet-4-6");
|
||||
await (agent as any).applyConfigOptionValue(
|
||||
"test-session",
|
||||
session,
|
||||
"model",
|
||||
"claude-sonnet-4-6",
|
||||
);
|
||||
|
||||
await agent.prompt({ sessionId: "test-session", prompt: [{ type: "text", text: "test" }] });
|
||||
|
||||
|
||||
@@ -15,9 +15,17 @@ vi.mock("@anthropic-ai/claude-agent-sdk", async () => ({
|
||||
capturedOptions = options;
|
||||
return {
|
||||
initializationResult: async () => ({
|
||||
models: [{ value: "claude-sonnet-4-5", displayName: "Claude Sonnet", description: "Fast" }],
|
||||
models: [
|
||||
{
|
||||
value: "claude-sonnet-4-6",
|
||||
displayName: "Claude Sonnet",
|
||||
description: "Fast",
|
||||
supportsAutoMode: true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
setModel: async () => {},
|
||||
setPermissionMode: async () => {},
|
||||
supportedCommands: async () => [],
|
||||
[Symbol.asyncIterator]: async function* () {},
|
||||
};
|
||||
|
||||
@@ -5,9 +5,12 @@ import { AgentSideConnection } from "@agentclientprotocol/sdk";
|
||||
const mockQuery = vi.hoisted(() =>
|
||||
vi.fn(() => ({
|
||||
initializationResult: vi.fn().mockResolvedValue({
|
||||
models: [{ value: "id", displayName: "name", description: "description" }],
|
||||
models: [
|
||||
{ value: "id", displayName: "name", description: "description", supportsAutoMode: true },
|
||||
],
|
||||
}),
|
||||
setModel: vi.fn(),
|
||||
setPermissionMode: vi.fn(),
|
||||
supportedCommands: vi.fn().mockResolvedValue([]),
|
||||
})),
|
||||
);
|
||||
|
||||
@@ -15,10 +15,16 @@ vi.mock("@anthropic-ai/claude-agent-sdk", async () => {
|
||||
return {
|
||||
initializationResult: async () => ({
|
||||
models: [
|
||||
{ value: "claude-sonnet-4-5", displayName: "Claude Sonnet", description: "Fast" },
|
||||
{
|
||||
value: "claude-sonnet-4-6",
|
||||
displayName: "Claude Sonnet",
|
||||
description: "Fast",
|
||||
supportsAutoMode: true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
setModel: async () => {},
|
||||
setPermissionMode: async () => {},
|
||||
supportedCommands: async () => [],
|
||||
[Symbol.asyncIterator]: async function* () {},
|
||||
};
|
||||
@@ -414,7 +420,7 @@ describe("createSession options merging", () => {
|
||||
claudeCode: {
|
||||
options: {
|
||||
settings: {
|
||||
model: "claude-sonnet-4-5",
|
||||
model: "claude-sonnet-4-6",
|
||||
modelOverrides: { "claude-opus-4-6": "meta-value" },
|
||||
},
|
||||
},
|
||||
@@ -424,7 +430,7 @@ describe("createSession options merging", () => {
|
||||
|
||||
// _meta settings take precedence; env var is ignored entirely
|
||||
expect(capturedOptions!.settings).toEqual({
|
||||
model: "claude-sonnet-4-5",
|
||||
model: "claude-sonnet-4-6",
|
||||
modelOverrides: { "claude-opus-4-6": "meta-value" },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { resolvePermissionMode } from "../acp-agent.js";
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { resolvePermissionMode, type Logger } from "../acp-agent.js";
|
||||
|
||||
function mockLogger() {
|
||||
const error = vi.fn<(...args: any[]) => void>();
|
||||
const log = vi.fn<(...args: any[]) => void>();
|
||||
const logger: Logger = { log, error };
|
||||
return { logger, error, log };
|
||||
}
|
||||
|
||||
describe("resolvePermissionMode", () => {
|
||||
it("returns 'default' when no mode is provided", () => {
|
||||
@@ -26,18 +33,25 @@ describe("resolvePermissionMode", () => {
|
||||
expect(resolvePermissionMode(" dontAsk ")).toBe("dontAsk");
|
||||
});
|
||||
|
||||
it("throws on non-string values", () => {
|
||||
expect(() => resolvePermissionMode(123)).toThrow("expected a string");
|
||||
expect(() => resolvePermissionMode(true)).toThrow("expected a string");
|
||||
expect(() => resolvePermissionMode({})).toThrow("expected a string");
|
||||
it("falls back to 'default' and logs on non-string values", () => {
|
||||
for (const value of [123, true, {}]) {
|
||||
const { logger, error } = mockLogger();
|
||||
expect(resolvePermissionMode(value, logger)).toBe("default");
|
||||
expect(error).toHaveBeenCalledWith(expect.stringContaining("expected a string"));
|
||||
}
|
||||
});
|
||||
|
||||
it("throws on empty string", () => {
|
||||
expect(() => resolvePermissionMode("")).toThrow("expected a non-empty string");
|
||||
expect(() => resolvePermissionMode(" ")).toThrow("expected a non-empty string");
|
||||
it("falls back to 'default' and logs on empty string", () => {
|
||||
for (const value of ["", " "]) {
|
||||
const { logger, error } = mockLogger();
|
||||
expect(resolvePermissionMode(value, logger)).toBe("default");
|
||||
expect(error).toHaveBeenCalledWith(expect.stringContaining("expected a non-empty string"));
|
||||
}
|
||||
});
|
||||
|
||||
it("throws on unknown mode", () => {
|
||||
expect(() => resolvePermissionMode("yolo")).toThrow("Invalid permissions.defaultMode: yolo");
|
||||
it("falls back to 'default' and logs on unknown mode", () => {
|
||||
const { logger, error } = mockLogger();
|
||||
expect(resolvePermissionMode("yolo", logger)).toBe("default");
|
||||
expect(error).toHaveBeenCalledWith(expect.stringContaining("yolo"));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,7 +30,7 @@ const MOCK_MODELS = {
|
||||
currentModelId: "claude-opus-4-5",
|
||||
availableModels: [
|
||||
{ modelId: "claude-opus-4-5", name: "Claude Opus", description: "Most capable" },
|
||||
{ modelId: "claude-sonnet-4-5", name: "Claude Sonnet", description: "Balanced" },
|
||||
{ modelId: "claude-sonnet-4-6", name: "Claude Sonnet", description: "Balanced" },
|
||||
],
|
||||
};
|
||||
|
||||
@@ -242,10 +242,10 @@ describe("session config options", () => {
|
||||
await agent.setSessionConfigOption({
|
||||
sessionId: SESSION_ID,
|
||||
configId: "model",
|
||||
value: "claude-sonnet-4-5",
|
||||
value: "claude-sonnet-4-6",
|
||||
});
|
||||
|
||||
expect(setModelSpy).toHaveBeenCalledWith("claude-sonnet-4-5");
|
||||
expect(setModelSpy).toHaveBeenCalledWith("claude-sonnet-4-6");
|
||||
|
||||
const configUpdate = sessionUpdates.find(
|
||||
(n) => n.update.sessionUpdate === "config_option_update",
|
||||
@@ -273,7 +273,7 @@ describe("session config options", () => {
|
||||
value: "sonnet",
|
||||
});
|
||||
|
||||
expect(setModelSpy).toHaveBeenCalledWith("claude-sonnet-4-5");
|
||||
expect(setModelSpy).toHaveBeenCalledWith("claude-sonnet-4-6");
|
||||
});
|
||||
|
||||
it("resolves display name to model ID", async () => {
|
||||
@@ -283,19 +283,19 @@ describe("session config options", () => {
|
||||
value: "Claude Sonnet",
|
||||
});
|
||||
|
||||
expect(setModelSpy).toHaveBeenCalledWith("claude-sonnet-4-5");
|
||||
expect(setModelSpy).toHaveBeenCalledWith("claude-sonnet-4-6");
|
||||
});
|
||||
|
||||
it("still works with exact model ID", async () => {
|
||||
const response = await agent.setSessionConfigOption({
|
||||
sessionId: SESSION_ID,
|
||||
configId: "model",
|
||||
value: "claude-sonnet-4-5",
|
||||
value: "claude-sonnet-4-6",
|
||||
});
|
||||
|
||||
expect(setModelSpy).toHaveBeenCalledWith("claude-sonnet-4-5");
|
||||
expect(setModelSpy).toHaveBeenCalledWith("claude-sonnet-4-6");
|
||||
const modelOption = response.configOptions.find((o) => o.id === "model");
|
||||
expect(modelOption?.currentValue).toBe("claude-sonnet-4-5");
|
||||
expect(modelOption?.currentValue).toBe("claude-sonnet-4-6");
|
||||
});
|
||||
|
||||
it("throws for completely invalid model value", async () => {
|
||||
@@ -384,7 +384,7 @@ describe("session config options", () => {
|
||||
it("sends config_option_update when model is changed via setSessionModel", async () => {
|
||||
await agent.unstable_setSessionModel({
|
||||
sessionId: SESSION_ID,
|
||||
modelId: "claude-sonnet-4-5",
|
||||
modelId: "claude-sonnet-4-6",
|
||||
});
|
||||
|
||||
const configUpdate = sessionUpdates.find(
|
||||
@@ -394,7 +394,7 @@ describe("session config options", () => {
|
||||
expect(configUpdate?.update).toMatchObject({
|
||||
sessionUpdate: "config_option_update",
|
||||
configOptions: expect.arrayContaining([
|
||||
expect.objectContaining({ id: "model", currentValue: "claude-sonnet-4-5" }),
|
||||
expect.objectContaining({ id: "model", currentValue: "claude-sonnet-4-6" }),
|
||||
]),
|
||||
});
|
||||
});
|
||||
@@ -402,7 +402,7 @@ describe("session config options", () => {
|
||||
it("updates stored configOptions currentValue when model changes", async () => {
|
||||
await agent.unstable_setSessionModel({
|
||||
sessionId: SESSION_ID,
|
||||
modelId: "claude-sonnet-4-5",
|
||||
modelId: "claude-sonnet-4-6",
|
||||
});
|
||||
|
||||
const session = (
|
||||
@@ -411,7 +411,7 @@ describe("session config options", () => {
|
||||
}
|
||||
).sessions[SESSION_ID];
|
||||
const modelOption = session.configOptions.find((o) => o.id === "model");
|
||||
expect(modelOption?.currentValue).toBe("claude-sonnet-4-5");
|
||||
expect(modelOption?.currentValue).toBe("claude-sonnet-4-6");
|
||||
});
|
||||
|
||||
it("includes updated effort in config_option_update when model drops effort support", async () => {
|
||||
@@ -425,7 +425,7 @@ describe("session config options", () => {
|
||||
supportedEffortLevels: ["low", "medium", "high"],
|
||||
},
|
||||
{
|
||||
value: "claude-sonnet-4-5",
|
||||
value: "claude-sonnet-4-6",
|
||||
displayName: "Claude Sonnet",
|
||||
description: "Balanced",
|
||||
supportsEffort: false,
|
||||
@@ -434,7 +434,7 @@ describe("session config options", () => {
|
||||
|
||||
await agent.unstable_setSessionModel({
|
||||
sessionId: SESSION_ID,
|
||||
modelId: "claude-sonnet-4-5",
|
||||
modelId: "claude-sonnet-4-6",
|
||||
});
|
||||
|
||||
const configUpdate = sessionUpdates.find(
|
||||
@@ -463,7 +463,7 @@ describe("session config options", () => {
|
||||
supportedEffortLevels: ["low", "medium", "high", "max"],
|
||||
},
|
||||
{
|
||||
value: "claude-sonnet-4-5",
|
||||
value: "claude-sonnet-4-6",
|
||||
displayName: "Claude Sonnet",
|
||||
description: "Balanced",
|
||||
supportsEffort: true,
|
||||
@@ -473,7 +473,7 @@ describe("session config options", () => {
|
||||
|
||||
await agent.unstable_setSessionModel({
|
||||
sessionId: SESSION_ID,
|
||||
modelId: "claude-sonnet-4-5",
|
||||
modelId: "claude-sonnet-4-6",
|
||||
});
|
||||
|
||||
const configUpdate = sessionUpdates.find(
|
||||
@@ -495,7 +495,7 @@ describe("session config options", () => {
|
||||
|
||||
await agent.unstable_setSessionModel({
|
||||
sessionId: SESSION_ID,
|
||||
modelId: "claude-sonnet-4-5",
|
||||
modelId: "claude-sonnet-4-6",
|
||||
});
|
||||
|
||||
const configUpdate = sessionUpdates.find(
|
||||
@@ -532,7 +532,7 @@ describe("session config options", () => {
|
||||
await agent.setSessionConfigOption({
|
||||
sessionId: SESSION_ID,
|
||||
configId: "model",
|
||||
value: "claude-sonnet-4-5",
|
||||
value: "claude-sonnet-4-6",
|
||||
});
|
||||
|
||||
const configUpdates = sessionUpdates.filter(
|
||||
@@ -622,7 +622,7 @@ describe("session config options", () => {
|
||||
supportedEffortLevels: ["low", "medium", "high"],
|
||||
},
|
||||
{
|
||||
value: "claude-sonnet-4-5",
|
||||
value: "claude-sonnet-4-6",
|
||||
displayName: "Claude Sonnet",
|
||||
description: "Balanced",
|
||||
supportsEffort: false,
|
||||
@@ -632,7 +632,7 @@ describe("session config options", () => {
|
||||
const response = await agent.setSessionConfigOption({
|
||||
sessionId: SESSION_ID,
|
||||
configId: "model",
|
||||
value: "claude-sonnet-4-5",
|
||||
value: "claude-sonnet-4-6",
|
||||
});
|
||||
|
||||
const effortOption = response.configOptions.find((o) => o.id === "effort");
|
||||
@@ -650,7 +650,7 @@ describe("session config options", () => {
|
||||
supportedEffortLevels: ["low", "medium", "high"],
|
||||
},
|
||||
{
|
||||
value: "claude-sonnet-4-5",
|
||||
value: "claude-sonnet-4-6",
|
||||
displayName: "Claude Sonnet",
|
||||
description: "Balanced",
|
||||
supportsEffort: false,
|
||||
@@ -660,7 +660,7 @@ describe("session config options", () => {
|
||||
await agent.setSessionConfigOption({
|
||||
sessionId: SESSION_ID,
|
||||
configId: "model",
|
||||
value: "claude-sonnet-4-5",
|
||||
value: "claude-sonnet-4-6",
|
||||
});
|
||||
|
||||
expect(applyFlagSettingsSpy).toHaveBeenCalledWith({ effortLevel: undefined });
|
||||
@@ -669,7 +669,7 @@ describe("session config options", () => {
|
||||
it("adds effort option when switching to a model that supports effort", async () => {
|
||||
const session = (agent as unknown as { sessions: Record<string, any> }).sessions[SESSION_ID];
|
||||
// Start with sonnet (no effort) as current
|
||||
session.models = { ...session.models, currentModelId: "claude-sonnet-4-5" };
|
||||
session.models = { ...session.models, currentModelId: "claude-sonnet-4-6" };
|
||||
session.modelInfos = [
|
||||
{
|
||||
value: "claude-opus-4-5",
|
||||
@@ -679,7 +679,7 @@ describe("session config options", () => {
|
||||
supportedEffortLevels: ["low", "medium", "high"],
|
||||
},
|
||||
{
|
||||
value: "claude-sonnet-4-5",
|
||||
value: "claude-sonnet-4-6",
|
||||
displayName: "Claude Sonnet",
|
||||
description: "Balanced",
|
||||
supportsEffort: false,
|
||||
@@ -715,7 +715,7 @@ describe("session config options", () => {
|
||||
supportedEffortLevels: ["low", "medium", "high", "max"],
|
||||
},
|
||||
{
|
||||
value: "claude-sonnet-4-5",
|
||||
value: "claude-sonnet-4-6",
|
||||
displayName: "Claude Sonnet",
|
||||
description: "Balanced",
|
||||
supportsEffort: true,
|
||||
@@ -726,7 +726,7 @@ describe("session config options", () => {
|
||||
const response = await agent.setSessionConfigOption({
|
||||
sessionId: SESSION_ID,
|
||||
configId: "model",
|
||||
value: "claude-sonnet-4-5",
|
||||
value: "claude-sonnet-4-6",
|
||||
});
|
||||
|
||||
const effortOption = response.configOptions.find((o) => o.id === "effort");
|
||||
@@ -749,7 +749,7 @@ describe("session config options", () => {
|
||||
const response = await agent.setSessionConfigOption({
|
||||
sessionId: SESSION_ID,
|
||||
configId: "model",
|
||||
value: "claude-sonnet-4-5",
|
||||
value: "claude-sonnet-4-6",
|
||||
});
|
||||
|
||||
const effortOption = response.configOptions.find((o) => o.id === "effort");
|
||||
@@ -778,10 +778,10 @@ describe("session config options", () => {
|
||||
await agent.setSessionConfigOption({
|
||||
sessionId: SESSION_ID,
|
||||
configId: "model",
|
||||
value: "claude-sonnet-4-5",
|
||||
value: "claude-sonnet-4-6",
|
||||
});
|
||||
|
||||
expect(setModelSpy).toHaveBeenCalledWith("claude-sonnet-4-5");
|
||||
expect(setModelSpy).toHaveBeenCalledWith("claude-sonnet-4-6");
|
||||
});
|
||||
|
||||
it("setSessionMode also syncs configOptions", async () => {
|
||||
@@ -798,7 +798,7 @@ describe("session config options", () => {
|
||||
it("setSessionModel also syncs configOptions", async () => {
|
||||
await agent.unstable_setSessionModel({
|
||||
sessionId: SESSION_ID,
|
||||
modelId: "claude-sonnet-4-5",
|
||||
modelId: "claude-sonnet-4-6",
|
||||
});
|
||||
|
||||
const session = (
|
||||
@@ -807,8 +807,395 @@ describe("session config options", () => {
|
||||
}
|
||||
).sessions[SESSION_ID];
|
||||
expect(session.configOptions.find((o) => o.id === "model")?.currentValue).toBe(
|
||||
"claude-sonnet-4-5",
|
||||
"claude-sonnet-4-6",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("auto mode availability per model", () => {
|
||||
/**
|
||||
* Augment the session populated by `populateSession()` with a Haiku entry
|
||||
* (no `supportsAutoMode`), Opus + Sonnet entries with `supportsAutoMode:
|
||||
* true`, and seed `availableModes` so it currently includes `auto`. This
|
||||
* exercises the per-model recomputation done by `applyConfigOptionValue`
|
||||
* on a model switch.
|
||||
*/
|
||||
function setupHaikuOpusSession(currentModeId: string = "default") {
|
||||
const session = (agent as unknown as { sessions: Record<string, any> }).sessions[SESSION_ID];
|
||||
session.modelInfos = [
|
||||
{
|
||||
value: "claude-opus-4-5",
|
||||
displayName: "Claude Opus",
|
||||
description: "Most capable",
|
||||
supportsEffort: true,
|
||||
supportedEffortLevels: ["low", "medium", "high"],
|
||||
supportsAutoMode: true,
|
||||
},
|
||||
{
|
||||
value: "claude-sonnet-4-6",
|
||||
displayName: "Claude Sonnet",
|
||||
description: "Balanced",
|
||||
supportsEffort: true,
|
||||
supportedEffortLevels: ["low", "medium", "high"],
|
||||
supportsAutoMode: true,
|
||||
},
|
||||
{
|
||||
value: "claude-haiku-4-5",
|
||||
displayName: "Claude Haiku",
|
||||
description: "Fast",
|
||||
supportsEffort: true,
|
||||
supportedEffortLevels: ["low", "medium", "high"],
|
||||
// supportsAutoMode intentionally omitted
|
||||
},
|
||||
];
|
||||
session.models = {
|
||||
currentModelId: "claude-opus-4-5",
|
||||
availableModels: [
|
||||
{ modelId: "claude-opus-4-5", name: "Claude Opus", description: "Most capable" },
|
||||
{ modelId: "claude-sonnet-4-6", name: "Claude Sonnet", description: "Balanced" },
|
||||
{ modelId: "claude-haiku-4-5", name: "Claude Haiku", description: "Fast" },
|
||||
],
|
||||
};
|
||||
session.modes = {
|
||||
currentModeId,
|
||||
availableModes: [
|
||||
{
|
||||
id: "auto",
|
||||
name: "Auto",
|
||||
description: "Use a model classifier to approve/deny permission prompts",
|
||||
},
|
||||
{
|
||||
id: "default",
|
||||
name: "Default",
|
||||
description: "Standard behavior, prompts for dangerous operations",
|
||||
},
|
||||
{
|
||||
id: "acceptEdits",
|
||||
name: "Accept Edits",
|
||||
description: "Auto-accept file edit operations",
|
||||
},
|
||||
{ id: "plan", name: "Plan Mode", description: "Planning mode" },
|
||||
{
|
||||
id: "dontAsk",
|
||||
name: "Don't Ask",
|
||||
description: "Don't prompt for permissions, deny if not pre-approved",
|
||||
},
|
||||
],
|
||||
};
|
||||
// Reflect the seeded availableModes/availableModels in configOptions so
|
||||
// the pre-state matches what `createSession` would have produced for
|
||||
// Opus, and `setSessionConfigOption` validation can accept the seeded
|
||||
// model ids (notably the new Haiku entry).
|
||||
session.configOptions = session.configOptions.map((o: any) => {
|
||||
if (o.id === "mode") {
|
||||
return {
|
||||
...o,
|
||||
currentValue: currentModeId,
|
||||
options: session.modes.availableModes.map((m: any) => ({
|
||||
value: m.id,
|
||||
name: m.name,
|
||||
description: m.description,
|
||||
})),
|
||||
};
|
||||
}
|
||||
if (o.id === "model") {
|
||||
return {
|
||||
...o,
|
||||
currentValue: session.models.currentModelId,
|
||||
options: session.models.availableModels.map((m: any) => ({
|
||||
value: m.modelId,
|
||||
name: m.name,
|
||||
description: m.description,
|
||||
})),
|
||||
};
|
||||
}
|
||||
return o;
|
||||
});
|
||||
return session;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
populateSession();
|
||||
});
|
||||
|
||||
it("drops `auto` from available modes when switching to Haiku", async () => {
|
||||
setupHaikuOpusSession("default");
|
||||
|
||||
await agent.unstable_setSessionModel({
|
||||
sessionId: SESSION_ID,
|
||||
modelId: "claude-haiku-4-5",
|
||||
});
|
||||
|
||||
const configUpdate = sessionUpdates.find(
|
||||
(n) => n.update.sessionUpdate === "config_option_update",
|
||||
);
|
||||
expect(configUpdate).toBeDefined();
|
||||
const modeOption = (configUpdate?.update as any).configOptions.find(
|
||||
(o: any) => o.id === "mode",
|
||||
);
|
||||
expect(modeOption).toBeDefined();
|
||||
const modeValues = modeOption.options.map((o: any) => o.value);
|
||||
expect(modeValues).not.toContain("auto");
|
||||
expect(modeValues).toEqual(
|
||||
expect.arrayContaining(["default", "acceptEdits", "plan", "dontAsk"]),
|
||||
);
|
||||
});
|
||||
|
||||
it("clamps to `default` and emits current_mode_update when Opus(auto) → Haiku", async () => {
|
||||
setupHaikuOpusSession("auto");
|
||||
|
||||
await agent.unstable_setSessionModel({
|
||||
sessionId: SESSION_ID,
|
||||
modelId: "claude-haiku-4-5",
|
||||
});
|
||||
|
||||
// SDK was synced to "default".
|
||||
expect(setPermissionModeSpy).toHaveBeenCalledWith("default");
|
||||
|
||||
// current_mode_update was emitted before config_option_update so a
|
||||
// client applying notifications in order observes the mode change
|
||||
// before re-rendering the config-option list.
|
||||
const modeUpdateIdx = sessionUpdates.findIndex(
|
||||
(n) => n.update.sessionUpdate === "current_mode_update",
|
||||
);
|
||||
const configUpdateIdx = sessionUpdates.findIndex(
|
||||
(n) => n.update.sessionUpdate === "config_option_update",
|
||||
);
|
||||
expect(modeUpdateIdx).toBeGreaterThanOrEqual(0);
|
||||
expect(configUpdateIdx).toBeGreaterThanOrEqual(0);
|
||||
expect(modeUpdateIdx).toBeLessThan(configUpdateIdx);
|
||||
expect((sessionUpdates[modeUpdateIdx].update as any).currentModeId).toBe("default");
|
||||
|
||||
// configOptions reflect the clamped mode.
|
||||
const modeOption = (sessionUpdates[configUpdateIdx].update as any).configOptions.find(
|
||||
(o: any) => o.id === "mode",
|
||||
);
|
||||
expect(modeOption.currentValue).toBe("default");
|
||||
});
|
||||
|
||||
it("re-adds `auto` when switching from Haiku back to Opus", async () => {
|
||||
const session = setupHaikuOpusSession("default");
|
||||
// Pretend Haiku is the current model with no `auto`.
|
||||
session.models.currentModelId = "claude-haiku-4-5";
|
||||
session.modes.availableModes = session.modes.availableModes.filter(
|
||||
(m: any) => m.id !== "auto",
|
||||
);
|
||||
const modeOpt = session.configOptions.find((o: any) => o.id === "mode");
|
||||
modeOpt.options = session.modes.availableModes.map((m: any) => ({
|
||||
value: m.id,
|
||||
name: m.name,
|
||||
description: m.description,
|
||||
}));
|
||||
|
||||
await agent.unstable_setSessionModel({
|
||||
sessionId: SESSION_ID,
|
||||
modelId: "claude-opus-4-5",
|
||||
});
|
||||
|
||||
const configUpdate = sessionUpdates.find(
|
||||
(n) => n.update.sessionUpdate === "config_option_update",
|
||||
);
|
||||
expect(configUpdate).toBeDefined();
|
||||
const modeOption = (configUpdate?.update as any).configOptions.find(
|
||||
(o: any) => o.id === "mode",
|
||||
);
|
||||
const modeValues = modeOption.options.map((o: any) => o.value);
|
||||
expect(modeValues).toContain("auto");
|
||||
|
||||
// The current mode ("default") is still valid on Opus, so no
|
||||
// current_mode_update should have been emitted by the model switch.
|
||||
const modeUpdates = sessionUpdates.filter(
|
||||
(n) => n.update.sessionUpdate === "current_mode_update",
|
||||
);
|
||||
expect(modeUpdates).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("preserves the current mode when it remains valid after a model switch", async () => {
|
||||
setupHaikuOpusSession("plan");
|
||||
|
||||
await agent.unstable_setSessionModel({
|
||||
sessionId: SESSION_ID,
|
||||
modelId: "claude-haiku-4-5",
|
||||
});
|
||||
|
||||
// `plan` is in availableModes for both Opus and Haiku, so no clamp.
|
||||
expect(setPermissionModeSpy).not.toHaveBeenCalledWith("default");
|
||||
|
||||
const modeUpdates = sessionUpdates.filter(
|
||||
(n) => n.update.sessionUpdate === "current_mode_update",
|
||||
);
|
||||
expect(modeUpdates).toHaveLength(0);
|
||||
|
||||
const configUpdate = sessionUpdates.find(
|
||||
(n) => n.update.sessionUpdate === "config_option_update",
|
||||
);
|
||||
const modeOption = (configUpdate?.update as any).configOptions.find(
|
||||
(o: any) => o.id === "mode",
|
||||
);
|
||||
expect(modeOption.currentValue).toBe("plan");
|
||||
});
|
||||
|
||||
it("clamps mode and emits current_mode_update via setSessionConfigOption(model)", async () => {
|
||||
// Mirrors the unstable_setSessionModel(auto → Haiku) test, but goes
|
||||
// through the request/response API. The `current_mode_update` side
|
||||
// effect must still fire so clients learn about the clamp regardless of
|
||||
// which entry point triggered the model switch.
|
||||
setupHaikuOpusSession("auto");
|
||||
|
||||
const response = await agent.setSessionConfigOption({
|
||||
sessionId: SESSION_ID,
|
||||
configId: "model",
|
||||
value: "claude-haiku-4-5",
|
||||
});
|
||||
|
||||
expect(setPermissionModeSpy).toHaveBeenCalledWith("default");
|
||||
|
||||
const modeUpdates = sessionUpdates.filter(
|
||||
(n) => n.update.sessionUpdate === "current_mode_update",
|
||||
);
|
||||
expect(modeUpdates).toHaveLength(1);
|
||||
expect((modeUpdates[0].update as any).currentModeId).toBe("default");
|
||||
|
||||
// setSessionConfigOption is a request/response API: it returns the new
|
||||
// configOptions in the response rather than emitting a
|
||||
// config_option_update notification.
|
||||
const configUpdates = sessionUpdates.filter(
|
||||
(n) => n.update.sessionUpdate === "config_option_update",
|
||||
);
|
||||
expect(configUpdates).toHaveLength(0);
|
||||
|
||||
const modeOption = response.configOptions.find((o: any) => o.id === "mode");
|
||||
expect(modeOption).toBeDefined();
|
||||
expect((modeOption as any).currentValue).toBe("default");
|
||||
expect((modeOption as any).options.map((o: any) => o.value)).not.toContain("auto");
|
||||
});
|
||||
|
||||
it("rejects direct setSessionMode to `auto` when the active model does not offer it", async () => {
|
||||
const session = setupHaikuOpusSession("default");
|
||||
session.models.currentModelId = "claude-haiku-4-5";
|
||||
session.modes.availableModes = session.modes.availableModes.filter(
|
||||
(mode: any) => mode.id !== "auto",
|
||||
);
|
||||
|
||||
await expect(agent.setSessionMode({ sessionId: SESSION_ID, modeId: "auto" })).rejects.toThrow(
|
||||
"Mode auto is not available in this session",
|
||||
);
|
||||
|
||||
expect(setPermissionModeSpy).not.toHaveBeenCalledWith("auto");
|
||||
expect(sessionUpdates).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ExitPlanMode permission options filtered by availableModes", () => {
|
||||
let capturedPermissionRequest: any;
|
||||
let permissionResponse: any;
|
||||
|
||||
beforeEach(() => {
|
||||
capturedPermissionRequest = null;
|
||||
permissionResponse = { outcome: { outcome: "cancelled" } };
|
||||
// Replace the default mock client with one that captures the
|
||||
// requestPermission call so we can assert on the offered options.
|
||||
(agent as any).client = {
|
||||
sessionUpdate: async (notification: SessionNotification) => {
|
||||
sessionUpdates.push(notification);
|
||||
},
|
||||
requestPermission: async (params: any) => {
|
||||
capturedPermissionRequest = params;
|
||||
return permissionResponse;
|
||||
},
|
||||
readTextFile: async () => ({ content: "" }),
|
||||
writeTextFile: async () => ({}),
|
||||
};
|
||||
populateSession();
|
||||
});
|
||||
|
||||
it("omits the `auto` option on a model without supportsAutoMode", async () => {
|
||||
const session = (agent as unknown as { sessions: Record<string, any> }).sessions[SESSION_ID];
|
||||
// Haiku-shaped session: availableModes does NOT include `auto`.
|
||||
session.modes = {
|
||||
currentModeId: "plan",
|
||||
availableModes: [
|
||||
{ id: "default", name: "Default", description: "Standard" },
|
||||
{ id: "acceptEdits", name: "Accept Edits", description: "Auto-accept edits" },
|
||||
{ id: "plan", name: "Plan Mode", description: "Planning mode" },
|
||||
{ id: "dontAsk", name: "Don't Ask", description: "Deny if not pre-approved" },
|
||||
],
|
||||
};
|
||||
|
||||
const canUseTool = (agent as any).canUseTool(SESSION_ID);
|
||||
const signal = new AbortController().signal;
|
||||
try {
|
||||
await canUseTool(
|
||||
"ExitPlanMode",
|
||||
{ plan: "do stuff" },
|
||||
{ signal, suggestions: undefined, toolUseID: "toolu_1" },
|
||||
);
|
||||
} catch {
|
||||
// The mock client returns `cancelled`, which makes canUseTool throw.
|
||||
// We only care about the captured requestPermission options.
|
||||
}
|
||||
|
||||
expect(capturedPermissionRequest).not.toBeNull();
|
||||
const optionIds = capturedPermissionRequest.options.map((o: any) => o.optionId);
|
||||
expect(optionIds).not.toContain("auto");
|
||||
expect(optionIds).toEqual(expect.arrayContaining(["default", "acceptEdits", "plan"]));
|
||||
});
|
||||
|
||||
it("denies a selected `auto` option if the client did not receive that option", async () => {
|
||||
const session = (agent as unknown as { sessions: Record<string, any> }).sessions[SESSION_ID];
|
||||
session.modes = {
|
||||
currentModeId: "plan",
|
||||
availableModes: [
|
||||
{ id: "default", name: "Default", description: "Standard" },
|
||||
{ id: "acceptEdits", name: "Accept Edits", description: "Auto-accept edits" },
|
||||
{ id: "plan", name: "Plan Mode", description: "Planning mode" },
|
||||
{ id: "dontAsk", name: "Don't Ask", description: "Deny if not pre-approved" },
|
||||
],
|
||||
};
|
||||
permissionResponse = { outcome: { outcome: "selected", optionId: "auto" } };
|
||||
|
||||
const canUseTool = (agent as any).canUseTool(SESSION_ID);
|
||||
const result = await canUseTool(
|
||||
"ExitPlanMode",
|
||||
{ plan: "do stuff" },
|
||||
{ signal: new AbortController().signal, suggestions: undefined, toolUseID: "toolu_2" },
|
||||
);
|
||||
|
||||
expect(capturedPermissionRequest).not.toBeNull();
|
||||
const optionIds = capturedPermissionRequest.options.map((o: any) => o.optionId);
|
||||
expect(optionIds).not.toContain("auto");
|
||||
expect(result.behavior).toBe("deny");
|
||||
expect(sessionUpdates).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("includes the `auto` option on a model with supportsAutoMode", async () => {
|
||||
const session = (agent as unknown as { sessions: Record<string, any> }).sessions[SESSION_ID];
|
||||
session.modes = {
|
||||
currentModeId: "plan",
|
||||
availableModes: [
|
||||
{ id: "auto", name: "Auto", description: "Use a model classifier" },
|
||||
{ id: "default", name: "Default", description: "Standard" },
|
||||
{ id: "acceptEdits", name: "Accept Edits", description: "Auto-accept edits" },
|
||||
{ id: "plan", name: "Plan Mode", description: "Planning mode" },
|
||||
{ id: "dontAsk", name: "Don't Ask", description: "Deny if not pre-approved" },
|
||||
],
|
||||
};
|
||||
|
||||
const canUseTool = (agent as any).canUseTool(SESSION_ID);
|
||||
const signal = new AbortController().signal;
|
||||
try {
|
||||
await canUseTool(
|
||||
"ExitPlanMode",
|
||||
{ plan: "do stuff" },
|
||||
{ signal, suggestions: undefined, toolUseID: "toolu_3" },
|
||||
);
|
||||
} catch {
|
||||
// mock returns cancelled
|
||||
}
|
||||
|
||||
expect(capturedPermissionRequest).not.toBeNull();
|
||||
const optionIds = capturedPermissionRequest.options.map((o: any) => o.optionId);
|
||||
expect(optionIds).toContain("auto");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1056,6 +1056,162 @@ describe("Bash terminal output", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("post-tool-use hook sends diff content for Write tool", () => {
|
||||
// Regression: previously the dispatch only invoked
|
||||
// toolUpdateFromDiffToolResponse for `Edit`, so a Write that
|
||||
// overwrote an existing file (FileWriteOutput.type === "update")
|
||||
// showed a "creation" diff (oldText: null, full new content) at
|
||||
// tool_use time and was never corrected after the tool ran.
|
||||
it("should emit a real diff for Write of an existing file (type: update)", async () => {
|
||||
const toolUseCache: ToolUseCache = {};
|
||||
const hookUpdates: any[] = [];
|
||||
const mockClientWithUpdate = {
|
||||
sessionUpdate: async (notification: any) => {
|
||||
hookUpdates.push(notification);
|
||||
},
|
||||
} as unknown as AgentSideConnection;
|
||||
|
||||
toAcpNotifications(
|
||||
[
|
||||
{
|
||||
type: "tool_use" as const,
|
||||
id: "toolu_write_update",
|
||||
name: "Write",
|
||||
input: {
|
||||
file_path: "/Users/test/project/file.ts",
|
||||
content: "line1\nNEW line2\nline3",
|
||||
},
|
||||
},
|
||||
],
|
||||
"assistant",
|
||||
"test-session",
|
||||
toolUseCache,
|
||||
mockClientWithUpdate,
|
||||
mockLogger,
|
||||
);
|
||||
|
||||
const hook = createPostToolUseHook(mockLogger);
|
||||
await hook(
|
||||
{
|
||||
hook_event_name: "PostToolUse",
|
||||
tool_name: "Write",
|
||||
tool_input: {
|
||||
file_path: "/Users/test/project/file.ts",
|
||||
content: "line1\nNEW line2\nline3",
|
||||
},
|
||||
tool_response: {
|
||||
type: "update",
|
||||
filePath: "/Users/test/project/file.ts",
|
||||
content: "line1\nNEW line2\nline3",
|
||||
originalFile: "line1\nold line2\nline3",
|
||||
structuredPatch: [
|
||||
{
|
||||
oldStart: 1,
|
||||
oldLines: 3,
|
||||
newStart: 1,
|
||||
newLines: 3,
|
||||
lines: [" line1", "-old line2", "+NEW line2", " line3"],
|
||||
},
|
||||
],
|
||||
userModified: false,
|
||||
},
|
||||
tool_use_id: "toolu_write_update",
|
||||
session_id: "test-session",
|
||||
transcript_path: "/tmp/test",
|
||||
cwd: "/tmp",
|
||||
},
|
||||
"toolu_write_update",
|
||||
{ signal: AbortSignal.abort() },
|
||||
);
|
||||
|
||||
expect(hookUpdates).toHaveLength(1);
|
||||
const hookUpdate = hookUpdates[0].update;
|
||||
expect(hookUpdate._meta.claudeCode.toolName).toBe("Write");
|
||||
expect(hookUpdate.content).toEqual([
|
||||
{
|
||||
type: "diff",
|
||||
path: "/Users/test/project/file.ts",
|
||||
oldText: "line1\nold line2\nline3",
|
||||
newText: "line1\nNEW line2\nline3",
|
||||
},
|
||||
]);
|
||||
expect(hookUpdate.locations).toEqual([{ path: "/Users/test/project/file.ts", line: 1 }]);
|
||||
});
|
||||
|
||||
it("should still emit a sensible diff for Write of a brand-new file (type: create)", async () => {
|
||||
const toolUseCache: ToolUseCache = {};
|
||||
const hookUpdates: any[] = [];
|
||||
const mockClientWithUpdate = {
|
||||
sessionUpdate: async (notification: any) => {
|
||||
hookUpdates.push(notification);
|
||||
},
|
||||
} as unknown as AgentSideConnection;
|
||||
|
||||
toAcpNotifications(
|
||||
[
|
||||
{
|
||||
type: "tool_use" as const,
|
||||
id: "toolu_write_create",
|
||||
name: "Write",
|
||||
input: {
|
||||
file_path: "/Users/test/project/new.ts",
|
||||
content: "first\nsecond",
|
||||
},
|
||||
},
|
||||
],
|
||||
"assistant",
|
||||
"test-session",
|
||||
toolUseCache,
|
||||
mockClientWithUpdate,
|
||||
mockLogger,
|
||||
);
|
||||
|
||||
const hook = createPostToolUseHook(mockLogger);
|
||||
await hook(
|
||||
{
|
||||
hook_event_name: "PostToolUse",
|
||||
tool_name: "Write",
|
||||
tool_input: {
|
||||
file_path: "/Users/test/project/new.ts",
|
||||
content: "first\nsecond",
|
||||
},
|
||||
tool_response: {
|
||||
type: "create",
|
||||
filePath: "/Users/test/project/new.ts",
|
||||
content: "first\nsecond",
|
||||
originalFile: null,
|
||||
structuredPatch: [
|
||||
{
|
||||
oldStart: 0,
|
||||
oldLines: 0,
|
||||
newStart: 1,
|
||||
newLines: 2,
|
||||
lines: ["+first", "+second"],
|
||||
},
|
||||
],
|
||||
},
|
||||
tool_use_id: "toolu_write_create",
|
||||
session_id: "test-session",
|
||||
transcript_path: "/tmp/test",
|
||||
cwd: "/tmp",
|
||||
},
|
||||
"toolu_write_create",
|
||||
{ signal: AbortSignal.abort() },
|
||||
);
|
||||
|
||||
expect(hookUpdates).toHaveLength(1);
|
||||
const hookUpdate = hookUpdates[0].update;
|
||||
expect(hookUpdate.content).toEqual([
|
||||
{
|
||||
type: "diff",
|
||||
path: "/Users/test/project/new.ts",
|
||||
oldText: null,
|
||||
newText: "first\nsecond",
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("post-tool-use hook preserves terminal _meta", () => {
|
||||
it("should send terminal_output and terminal_exit as separate notifications, and hook should only have claudeCode", async () => {
|
||||
const clientCapabilities: ClientCapabilities = {
|
||||
|
||||
19
src/tools.ts
19
src/tools.ts
@@ -679,7 +679,7 @@ export function markdownEscape(text: string): string {
|
||||
return escape + "\n" + text + (text.endsWith("\n") ? "" : "\n") + escape;
|
||||
}
|
||||
|
||||
interface EditToolResponseHunk {
|
||||
interface DiffToolResponseHunk {
|
||||
oldStart: number;
|
||||
oldLines: number;
|
||||
newStart: number;
|
||||
@@ -687,23 +687,24 @@ interface EditToolResponseHunk {
|
||||
lines: string[];
|
||||
}
|
||||
|
||||
interface EditToolResponse {
|
||||
interface DiffToolResponse {
|
||||
filePath?: string;
|
||||
structuredPatch?: EditToolResponseHunk[];
|
||||
structuredPatch?: DiffToolResponseHunk[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds diff ToolUpdate content from the structured Edit toolResponse provided
|
||||
* by the PostToolUse hook. Unlike parsing the plain unified diff string, this uses
|
||||
* the pre-parsed structuredPatch which supports multiple replacement sites (replaceAll)
|
||||
* and always includes context lines for better readability.
|
||||
* Builds diff ToolUpdate content from the structured toolResponse provided by
|
||||
* the PostToolUse hook for diff-producing tools (Edit, Write). Unlike parsing
|
||||
* the plain unified diff string, this uses the pre-parsed structuredPatch
|
||||
* which supports multiple replacement sites (replaceAll) and always includes
|
||||
* context lines for better readability.
|
||||
*/
|
||||
export function toolUpdateFromEditToolResponse(toolResponse: unknown): {
|
||||
export function toolUpdateFromDiffToolResponse(toolResponse: unknown): {
|
||||
content?: ToolCallContent[];
|
||||
locations?: ToolCallLocation[];
|
||||
} {
|
||||
if (!toolResponse || typeof toolResponse !== "object") return {};
|
||||
const response = toolResponse as EditToolResponse;
|
||||
const response = toolResponse as DiffToolResponse;
|
||||
if (!response.filePath || !Array.isArray(response.structuredPatch)) return {};
|
||||
|
||||
const content: ToolCallContent[] = [];
|
||||
|
||||
Reference in New Issue
Block a user