mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-10-27 12:40:32 +00:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e5f0b4830 | ||
|
|
172565f19d | ||
|
|
09106474ea | ||
|
|
2b60bad254 | ||
|
|
e81ecbd96e | ||
|
|
e1f08905f7 | ||
|
|
634f1ef6a4 | ||
|
|
86d333b8cc | ||
|
|
9f6fef55dd | ||
|
|
c3e1418d89 | ||
|
|
3b4cd4b2d5 | ||
|
|
9320e05891 | ||
|
|
3f44543019 | ||
|
|
4db3d855bf | ||
|
|
e57893f6d0 |
2
.github/workflows/publish.yml
vendored
2
.github/workflows/publish.yml
vendored
@@ -32,5 +32,3 @@ jobs:
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090
|
||||
with:
|
||||
draft: true
|
||||
|
||||
39
CHANGELOG.md
39
CHANGELOG.md
@@ -1,5 +1,44 @@
|
||||
# Changelog
|
||||
|
||||
## 0.12.5
|
||||
|
||||
- Update to @anthropic-ai/claude-agent-sdk@v0.1.70
|
||||
- Unstable implementation of resuming sessions
|
||||
|
||||
## 0.12.4
|
||||
|
||||
- Update to @anthropic-ai/claude-agent-sdk@v0.1.67
|
||||
- Better respect permissions specified in settings files
|
||||
- Unstable implementation of forking
|
||||
|
||||
## 0.12.3
|
||||
|
||||
- Update to @anthropic-ai/claude-agent-sdk@v0.1.65
|
||||
- Update to @agentclientprotocol/sdk@0.9.0
|
||||
- Allow agent to write plans and todos to its config directory
|
||||
- Fix experimental resume ids
|
||||
|
||||
## 0.12.2
|
||||
|
||||
- Fix duplicate tool use IDs error
|
||||
|
||||
## 0.12.1
|
||||
|
||||
- Update to @anthropic-ai/claude-agent-sdk@v0.1.61
|
||||
- Update to @agentclientprotocol/sdk@0.8.0
|
||||
|
||||
## 0.12.0
|
||||
|
||||
- Update to @anthropic-ai/claude-agent-sdk@v0.1.59
|
||||
- Brings Opus to Claude Pro plans
|
||||
- Support "Don't Ask" profile
|
||||
- Unify ACP + Claude Code session ids
|
||||
|
||||
## 0.11.0
|
||||
|
||||
- Update to @anthropic-ai/claude-agent-sdk@v0.1.57
|
||||
- Removed dependency on @anthropic-ai/claude-code since this is no longer needed
|
||||
|
||||
## 0.10.10
|
||||
|
||||
- Update to @agentclientprotocol/sdk@0.7.0
|
||||
|
||||
497
package-lock.json
generated
497
package-lock.json
generated
@@ -1,52 +1,50 @@
|
||||
{
|
||||
"name": "@zed-industries/claude-code-acp",
|
||||
"version": "0.10.10",
|
||||
"version": "0.12.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@zed-industries/claude-code-acp",
|
||||
"version": "0.10.10",
|
||||
"version": "0.12.5",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "0.7.0",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.1.55",
|
||||
"@anthropic-ai/claude-code": "2.0.55",
|
||||
"@modelcontextprotocol/sdk": "1.23.0",
|
||||
"@agentclientprotocol/sdk": "0.12.0",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.1.70",
|
||||
"@modelcontextprotocol/sdk": "1.25.0",
|
||||
"diff": "8.0.2",
|
||||
"uuid": "13.0.0"
|
||||
"minimatch": "^10.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"claude-code-acp": "dist/index.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@anthropic-ai/sdk": "0.71.0",
|
||||
"@types/node": "24.10.1",
|
||||
"@types/uuid": "10.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.48.0",
|
||||
"@typescript-eslint/parser": "8.48.0",
|
||||
"eslint": "9.39.1",
|
||||
"@anthropic-ai/sdk": "0.71.2",
|
||||
"@types/node": "25.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "8.50.0",
|
||||
"@typescript-eslint/parser": "8.50.0",
|
||||
"eslint": "9.39.2",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"globals": "16.5.0",
|
||||
"prettier": "3.7.3",
|
||||
"prettier": "3.7.4",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.9.3",
|
||||
"vitest": "4.0.14"
|
||||
"vitest": "4.0.15"
|
||||
}
|
||||
},
|
||||
"node_modules/@agentclientprotocol/sdk": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-0.7.0.tgz",
|
||||
"integrity": "sha512-lZrBwaP7qpDgwvhZOl61te9bpigpOXJfVTwLWqjMsnZI7oe5rgHdxGWfTHcqiRy4ITg9qbz3ApgYsxKoZpYMhg==",
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-0.12.0.tgz",
|
||||
"integrity": "sha512-V8uH/KK1t7utqyJmTA7y7DzKu6+jKFIXM+ZVouz8E55j8Ej2RV42rEvPKn3/PpBJlliI5crcGk1qQhZ7VwaepA==",
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"zod": "^3.25.0 || ^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk": {
|
||||
"version": "0.1.55",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.1.55.tgz",
|
||||
"integrity": "sha512-nwlxPjn/gc7I+iOGYY7AGtM2xcjzJFCxF9Bnr0xH1JNaNx+QXLM3h/wmzSvuEOKeJgPymf1GMBs4DZ3jyd/Z7Q==",
|
||||
"version": "0.1.70",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.1.70.tgz",
|
||||
"integrity": "sha512-4jpFPDX8asys6skO1r3Pzh0Fe9nbND2ASYTWuyFB5iN9bWEL6WScTFyGokjql3M2TkEp9ZGuB2YYpTCdaqT9Sw==",
|
||||
"license": "SEE LICENSE IN README.md",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
@@ -65,32 +63,10 @@
|
||||
"zod": "^3.24.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-code": {
|
||||
"version": "2.0.55",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code/-/claude-code-2.0.55.tgz",
|
||||
"integrity": "sha512-IVY6J2KgTP5BiCbLmuP3kAl8jbXfd6yGoXtvc0L0eiZwxJUMa+cubUU0U8qHRnVkNmDAis+O4P00KmeuGzSLWg==",
|
||||
"license": "SEE LICENSE IN README.md",
|
||||
"bin": {
|
||||
"claude": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-darwin-arm64": "^0.33.5",
|
||||
"@img/sharp-darwin-x64": "^0.33.5",
|
||||
"@img/sharp-linux-arm": "^0.33.5",
|
||||
"@img/sharp-linux-arm64": "^0.33.5",
|
||||
"@img/sharp-linux-x64": "^0.33.5",
|
||||
"@img/sharp-linuxmusl-arm64": "^0.33.5",
|
||||
"@img/sharp-linuxmusl-x64": "^0.33.5",
|
||||
"@img/sharp-win32-x64": "^0.33.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/sdk": {
|
||||
"version": "0.71.0",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.71.0.tgz",
|
||||
"integrity": "sha512-go1XeWXmpxuiTkosSXpb8tokLk2ZLkIRcXpbWVwJM6gH5OBtHOVsfPfGuqI1oW7RRt4qc59EmYbrXRZ0Ng06Jw==",
|
||||
"version": "0.71.2",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.71.2.tgz",
|
||||
"integrity": "sha512-TGNDEUuEstk/DKu0/TflXAEt+p+p/WhTlFzEnoosvbaDU2LTjm42igSdlL0VijrKpWejtOKxX0b8A7uc+XiSAQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -617,17 +593,6 @@
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/config-array/node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/config-array/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
@@ -708,17 +673,6 @@
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc/node_modules/globals": {
|
||||
"version": "14.0.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
|
||||
@@ -763,9 +717,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "9.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz",
|
||||
"integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==",
|
||||
"version": "9.39.2",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz",
|
||||
"integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -799,6 +753,18 @@
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@hono/node-server": {
|
||||
"version": "1.19.7",
|
||||
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.7.tgz",
|
||||
"integrity": "sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.14.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"hono": "^4"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanfs/core": {
|
||||
"version": "0.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||
@@ -1136,6 +1102,27 @@
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/balanced-match": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
|
||||
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/brace-expansion": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
|
||||
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@isaacs/balanced-match": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
@@ -1165,11 +1152,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/sdk": {
|
||||
"version": "1.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.23.0.tgz",
|
||||
"integrity": "sha512-MCGd4K9aZKvuSqdoBkdMvZNcYXCkZRYVs/Gh92mdV5IHbctX9H9uIvd4X93+9g8tBbXv08sxc/QHXTzf8y65bA==",
|
||||
"version": "1.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.0.tgz",
|
||||
"integrity": "sha512-z0Zhn/LmQ3yz91dEfd5QgS7DpSjA4pk+3z2++zKgn5L6iDFM9QapsVoAQSbKLvlrFsZk9+ru6yHHWNq2lCYJKQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^1.19.7",
|
||||
"ajv": "^8.17.1",
|
||||
"ajv-formats": "^3.0.1",
|
||||
"content-type": "^1.0.5",
|
||||
@@ -1179,6 +1167,8 @@
|
||||
"eventsource-parser": "^3.0.0",
|
||||
"express": "^5.0.1",
|
||||
"express-rate-limit": "^7.5.0",
|
||||
"jose": "^6.1.1",
|
||||
"json-schema-typed": "^8.0.2",
|
||||
"pkce-challenge": "^5.0.0",
|
||||
"raw-body": "^3.0.0",
|
||||
"zod": "^3.25 || ^4.0",
|
||||
@@ -1576,9 +1566,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
|
||||
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
|
||||
"version": "25.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.2.tgz",
|
||||
"integrity": "sha512-gWEkeiyYE4vqjON/+Obqcoeffmk0NF15WSBwSs7zwVA2bAbTaE0SJ7P0WNGoJn8uE7fiaV5a7dKYIJriEqOrmA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
@@ -1586,26 +1576,18 @@
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/uuid": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
|
||||
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.48.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.0.tgz",
|
||||
"integrity": "sha512-XxXP5tL1txl13YFtrECECQYeZjBZad4fyd3cFV4a19LkAY/bIp9fev3US4S5fDVV2JaYFiKAZ/GRTOLer+mbyQ==",
|
||||
"version": "8.50.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.50.0.tgz",
|
||||
"integrity": "sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.10.0",
|
||||
"@typescript-eslint/scope-manager": "8.48.0",
|
||||
"@typescript-eslint/type-utils": "8.48.0",
|
||||
"@typescript-eslint/utils": "8.48.0",
|
||||
"@typescript-eslint/visitor-keys": "8.48.0",
|
||||
"graphemer": "^1.4.0",
|
||||
"@typescript-eslint/scope-manager": "8.50.0",
|
||||
"@typescript-eslint/type-utils": "8.50.0",
|
||||
"@typescript-eslint/utils": "8.50.0",
|
||||
"@typescript-eslint/visitor-keys": "8.50.0",
|
||||
"ignore": "^7.0.0",
|
||||
"natural-compare": "^1.4.0",
|
||||
"ts-api-utils": "^2.1.0"
|
||||
@@ -1618,23 +1600,23 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.48.0",
|
||||
"@typescript-eslint/parser": "^8.50.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.48.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.48.0.tgz",
|
||||
"integrity": "sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==",
|
||||
"version": "8.50.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.50.0.tgz",
|
||||
"integrity": "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.48.0",
|
||||
"@typescript-eslint/types": "8.48.0",
|
||||
"@typescript-eslint/typescript-estree": "8.48.0",
|
||||
"@typescript-eslint/visitor-keys": "8.48.0",
|
||||
"@typescript-eslint/scope-manager": "8.50.0",
|
||||
"@typescript-eslint/types": "8.50.0",
|
||||
"@typescript-eslint/typescript-estree": "8.50.0",
|
||||
"@typescript-eslint/visitor-keys": "8.50.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1650,14 +1632,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.48.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.48.0.tgz",
|
||||
"integrity": "sha512-Ne4CTZyRh1BecBf84siv42wv5vQvVmgtk8AuiEffKTUo3DrBaGYZueJSxxBZ8fjk/N3DrgChH4TOdIOwOwiqqw==",
|
||||
"version": "8.50.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.50.0.tgz",
|
||||
"integrity": "sha512-Cg/nQcL1BcoTijEWyx4mkVC56r8dj44bFDvBdygifuS20f3OZCHmFbjF34DPSi07kwlFvqfv/xOLnJ5DquxSGQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.48.0",
|
||||
"@typescript-eslint/types": "^8.48.0",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.50.0",
|
||||
"@typescript-eslint/types": "^8.50.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1672,14 +1654,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.48.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.48.0.tgz",
|
||||
"integrity": "sha512-uGSSsbrtJrLduti0Q1Q9+BF1/iFKaxGoQwjWOIVNJv0o6omrdyR8ct37m4xIl5Zzpkp69Kkmvom7QFTtue89YQ==",
|
||||
"version": "8.50.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.50.0.tgz",
|
||||
"integrity": "sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.48.0",
|
||||
"@typescript-eslint/visitor-keys": "8.48.0"
|
||||
"@typescript-eslint/types": "8.50.0",
|
||||
"@typescript-eslint/visitor-keys": "8.50.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1690,9 +1672,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.48.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.0.tgz",
|
||||
"integrity": "sha512-WNebjBdFdyu10sR1M4OXTt2OkMd5KWIL+LLfeH9KhgP+jzfDV/LI3eXzwJ1s9+Yc0Kzo2fQCdY/OpdusCMmh6w==",
|
||||
"version": "8.50.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.50.0.tgz",
|
||||
"integrity": "sha512-vxd3G/ybKTSlm31MOA96gqvrRGv9RJ7LGtZCn2Vrc5htA0zCDvcMqUkifcjrWNNKXHUU3WCkYOzzVSFBd0wa2w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1707,15 +1689,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.48.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.48.0.tgz",
|
||||
"integrity": "sha512-zbeVaVqeXhhab6QNEKfK96Xyc7UQuoFWERhEnj3mLVnUWrQnv15cJNseUni7f3g557gm0e46LZ6IJ4NJVOgOpw==",
|
||||
"version": "8.50.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.50.0.tgz",
|
||||
"integrity": "sha512-7OciHT2lKCewR0mFoBrvZJ4AXTMe/sYOe87289WAViOocEmDjjv8MvIOT2XESuKj9jp8u3SZYUSh89QA4S1kQw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.48.0",
|
||||
"@typescript-eslint/typescript-estree": "8.48.0",
|
||||
"@typescript-eslint/utils": "8.48.0",
|
||||
"@typescript-eslint/types": "8.50.0",
|
||||
"@typescript-eslint/typescript-estree": "8.50.0",
|
||||
"@typescript-eslint/utils": "8.50.0",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^2.1.0"
|
||||
},
|
||||
@@ -1732,9 +1714,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.48.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.0.tgz",
|
||||
"integrity": "sha512-cQMcGQQH7kwKoVswD1xdOytxQR60MWKM1di26xSUtxehaDs/32Zpqsu5WJlXTtTTqyAVK8R7hvsUnIXRS+bjvA==",
|
||||
"version": "8.50.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.50.0.tgz",
|
||||
"integrity": "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1746,16 +1728,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.48.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.0.tgz",
|
||||
"integrity": "sha512-ljHab1CSO4rGrQIAyizUS6UGHHCiAYhbfcIZ1zVJr5nMryxlXMVWS3duFPSKvSUbFPwkXMFk1k0EMIjub4sRRQ==",
|
||||
"version": "8.50.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.50.0.tgz",
|
||||
"integrity": "sha512-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.48.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.48.0",
|
||||
"@typescript-eslint/types": "8.48.0",
|
||||
"@typescript-eslint/visitor-keys": "8.48.0",
|
||||
"@typescript-eslint/project-service": "8.50.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.50.0",
|
||||
"@typescript-eslint/types": "8.50.0",
|
||||
"@typescript-eslint/visitor-keys": "8.50.0",
|
||||
"debug": "^4.3.4",
|
||||
"minimatch": "^9.0.4",
|
||||
"semver": "^7.6.0",
|
||||
@@ -1773,17 +1755,43 @@
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.48.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.48.0.tgz",
|
||||
"integrity": "sha512-yTJO1XuGxCsSfIVt1+1UrLHtue8xz16V8apzPYI06W0HbEbEWHxHXgZaAgavIkoh+GeV6hKKd5jm0sS6OYxWXQ==",
|
||||
"version": "8.50.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.50.0.tgz",
|
||||
"integrity": "sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.7.0",
|
||||
"@typescript-eslint/scope-manager": "8.48.0",
|
||||
"@typescript-eslint/types": "8.48.0",
|
||||
"@typescript-eslint/typescript-estree": "8.48.0"
|
||||
"@typescript-eslint/scope-manager": "8.50.0",
|
||||
"@typescript-eslint/types": "8.50.0",
|
||||
"@typescript-eslint/typescript-estree": "8.50.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1798,13 +1806,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.48.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.48.0.tgz",
|
||||
"integrity": "sha512-T0XJMaRPOH3+LBbAfzR2jalckP1MSG/L9eUtY0DEzUyVaXJ/t6zN0nR7co5kz0Jko/nkSYCBRkz1djvjajVTTg==",
|
||||
"version": "8.50.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.50.0.tgz",
|
||||
"integrity": "sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.48.0",
|
||||
"@typescript-eslint/types": "8.50.0",
|
||||
"eslint-visitor-keys": "^4.2.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1829,16 +1837,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "4.0.14",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.14.tgz",
|
||||
"integrity": "sha512-RHk63V3zvRiYOWAV0rGEBRO820ce17hz7cI2kDmEdfQsBjT2luEKB5tCOc91u1oSQoUOZkSv3ZyzkdkSLD7lKw==",
|
||||
"version": "4.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.15.tgz",
|
||||
"integrity": "sha512-Gfyva9/GxPAWXIWjyGDli9O+waHDC0Q0jaLdFP1qPAUUfo1FEXPXUfUkp3eZA0sSq340vPycSyOlYUeM15Ft1w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/spy": "4.0.14",
|
||||
"@vitest/utils": "4.0.14",
|
||||
"@vitest/spy": "4.0.15",
|
||||
"@vitest/utils": "4.0.15",
|
||||
"chai": "^6.2.1",
|
||||
"tinyrainbow": "^3.0.3"
|
||||
},
|
||||
@@ -1847,13 +1855,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/mocker": {
|
||||
"version": "4.0.14",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.14.tgz",
|
||||
"integrity": "sha512-RzS5NujlCzeRPF1MK7MXLiEFpkIXeMdQ+rN3Kk3tDI9j0mtbr7Nmuq67tpkOJQpgyClbOltCXMjLZicJHsH5Cg==",
|
||||
"version": "4.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.15.tgz",
|
||||
"integrity": "sha512-CZ28GLfOEIFkvCFngN8Sfx5h+Se0zN+h4B7yOsPVCcgtiO7t5jt9xQh2E1UkFep+eb9fjyMfuC5gBypwb07fvQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/spy": "4.0.14",
|
||||
"@vitest/spy": "4.0.15",
|
||||
"estree-walker": "^3.0.3",
|
||||
"magic-string": "^0.30.21"
|
||||
},
|
||||
@@ -1874,9 +1882,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/pretty-format": {
|
||||
"version": "4.0.14",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.14.tgz",
|
||||
"integrity": "sha512-SOYPgujB6TITcJxgd3wmsLl+wZv+fy3av2PpiPpsWPZ6J1ySUYfScfpIt2Yv56ShJXR2MOA6q2KjKHN4EpdyRQ==",
|
||||
"version": "4.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.15.tgz",
|
||||
"integrity": "sha512-SWdqR8vEv83WtZcrfLNqlqeQXlQLh2iilO1Wk1gv4eiHKjEzvgHb2OVc3mIPyhZE6F+CtfYjNlDJwP5MN6Km7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1887,13 +1895,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/runner": {
|
||||
"version": "4.0.14",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.14.tgz",
|
||||
"integrity": "sha512-BsAIk3FAqxICqREbX8SetIteT8PiaUL/tgJjmhxJhCsigmzzH8xeadtp7LRnTpCVzvf0ib9BgAfKJHuhNllKLw==",
|
||||
"version": "4.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.15.tgz",
|
||||
"integrity": "sha512-+A+yMY8dGixUhHmNdPUxOh0la6uVzun86vAbuMT3hIDxMrAOmn5ILBHm8ajrqHE0t8R9T1dGnde1A5DTnmi3qw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/utils": "4.0.14",
|
||||
"@vitest/utils": "4.0.15",
|
||||
"pathe": "^2.0.3"
|
||||
},
|
||||
"funding": {
|
||||
@@ -1901,13 +1909,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/snapshot": {
|
||||
"version": "4.0.14",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.14.tgz",
|
||||
"integrity": "sha512-aQVBfT1PMzDSA16Y3Fp45a0q8nKexx6N5Amw3MX55BeTeZpoC08fGqEZqVmPcqN0ueZsuUQ9rriPMhZ3Mu19Ag==",
|
||||
"version": "4.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.15.tgz",
|
||||
"integrity": "sha512-A7Ob8EdFZJIBjLjeO0DZF4lqR6U7Ydi5/5LIZ0xcI+23lYlsYJAfGn8PrIWTYdZQRNnSRlzhg0zyGu37mVdy5g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "4.0.14",
|
||||
"@vitest/pretty-format": "4.0.15",
|
||||
"magic-string": "^0.30.21",
|
||||
"pathe": "^2.0.3"
|
||||
},
|
||||
@@ -1916,9 +1924,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/spy": {
|
||||
"version": "4.0.14",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.14.tgz",
|
||||
"integrity": "sha512-JmAZT1UtZooO0tpY3GRyiC/8W7dCs05UOq9rfsUUgEZEdq+DuHLmWhPsrTt0TiW7WYeL/hXpaE07AZ2RCk44hg==",
|
||||
"version": "4.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.15.tgz",
|
||||
"integrity": "sha512-+EIjOJmnY6mIfdXtE/bnozKEvTC4Uczg19yeZ2vtCz5Yyb0QQ31QWVQ8hswJ3Ysx/K2EqaNsVanjr//2+P3FHw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
@@ -1926,13 +1934,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/utils": {
|
||||
"version": "4.0.14",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.14.tgz",
|
||||
"integrity": "sha512-hLqXZKAWNg8pI+SQXyXxWCTOpA3MvsqcbVeNgSi8x/CSN2wi26dSzn1wrOhmCmFjEvN9p8/kLFRHa6PI8jHazw==",
|
||||
"version": "4.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.15.tgz",
|
||||
"integrity": "sha512-HXjPW2w5dxhTD0dLwtYHDnelK3j8sR8cWIaLxr22evTyY6q8pRCjZSmhRWVjBaOVXChQd6AwMzi9pucorXCPZA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "4.0.14",
|
||||
"@vitest/pretty-format": "4.0.15",
|
||||
"tinyrainbow": "^3.0.3"
|
||||
},
|
||||
"funding": {
|
||||
@@ -2094,13 +2102,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
@@ -2449,9 +2458,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "9.39.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz",
|
||||
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
|
||||
"version": "9.39.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
|
||||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
@@ -2462,7 +2471,7 @@
|
||||
"@eslint/config-helpers": "^0.4.2",
|
||||
"@eslint/core": "^0.17.0",
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@eslint/js": "9.39.1",
|
||||
"@eslint/js": "9.39.2",
|
||||
"@eslint/plugin-kit": "^0.4.1",
|
||||
"@humanfs/node": "^0.16.6",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
@@ -2572,17 +2581,6 @@
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/eslint-visitor-keys": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
|
||||
@@ -2744,9 +2742,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/expect-type": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz",
|
||||
"integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==",
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
|
||||
"integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
@@ -2754,9 +2752,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-5.2.0.tgz",
|
||||
"integrity": "sha512-XdpJDLxfztVY59X0zPI6sibRiGcxhTPXRD3IhJmjKf2jwMvkRGV1j7loB8U+heeamoU3XvihAaGRTR4aXXUN3A==",
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
||||
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
@@ -3055,13 +3053,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/graphemer": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
|
||||
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
@@ -3096,6 +3087,16 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/hono": {
|
||||
"version": "4.11.1",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.11.1.tgz",
|
||||
"integrity": "sha512-KsFcH0xxHes0J4zaQgWbYwmz3UPOOskdqZmItstUG93+Wk1ePBLkLGwbP9zlmh1BFUiL8Qp+Xfu9P7feJWpGNg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||
@@ -3117,9 +3118,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
|
||||
"integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz",
|
||||
"integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
@@ -3219,6 +3220,15 @@
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/jose": {
|
||||
"version": "6.1.3",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz",
|
||||
"integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
@@ -3259,6 +3269,12 @@
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-schema-typed": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz",
|
||||
"integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/json-stable-stringify-without-jsonify": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
|
||||
@@ -3386,16 +3402,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"version": "10.1.1",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz",
|
||||
"integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
"@isaacs/brace-expansion": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
@@ -3673,9 +3688,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.7.3",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.3.tgz",
|
||||
"integrity": "sha512-QgODejq9K3OzoBbuyobZlUhznP5SKwPqp+6Q6xw6o8gnhr4O85L2U915iM2IDcfF2NPXVaM9zlo9tdwipnYwzg==",
|
||||
"version": "3.7.4",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz",
|
||||
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@@ -4056,11 +4071,14 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyexec": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
|
||||
"integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz",
|
||||
"integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.15",
|
||||
@@ -4240,19 +4258,6 @@
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
|
||||
"integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist-node/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/v8-compile-cache-lib": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||
@@ -4270,9 +4275,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.2.6",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.6.tgz",
|
||||
"integrity": "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==",
|
||||
"version": "7.2.7",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.7.tgz",
|
||||
"integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
@@ -4346,19 +4351,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vitest": {
|
||||
"version": "4.0.14",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.14.tgz",
|
||||
"integrity": "sha512-d9B2J9Cm9dN9+6nxMnnNJKJCtcyKfnHj15N6YNJfaFHRLua/d3sRKU9RuKmO9mB0XdFtUizlxfz/VPbd3OxGhw==",
|
||||
"version": "4.0.15",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.15.tgz",
|
||||
"integrity": "sha512-n1RxDp8UJm6N0IbJLQo+yzLZ2sQCDyl1o0LeugbPWf8+8Fttp29GghsQBjYJVmWq3gBFfe9Hs1spR44vovn2wA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/expect": "4.0.14",
|
||||
"@vitest/mocker": "4.0.14",
|
||||
"@vitest/pretty-format": "4.0.14",
|
||||
"@vitest/runner": "4.0.14",
|
||||
"@vitest/snapshot": "4.0.14",
|
||||
"@vitest/spy": "4.0.14",
|
||||
"@vitest/utils": "4.0.14",
|
||||
"@vitest/expect": "4.0.15",
|
||||
"@vitest/mocker": "4.0.15",
|
||||
"@vitest/pretty-format": "4.0.15",
|
||||
"@vitest/runner": "4.0.15",
|
||||
"@vitest/snapshot": "4.0.15",
|
||||
"@vitest/spy": "4.0.15",
|
||||
"@vitest/utils": "4.0.15",
|
||||
"es-module-lexer": "^1.7.0",
|
||||
"expect-type": "^1.2.2",
|
||||
"magic-string": "^0.30.21",
|
||||
@@ -4367,7 +4372,7 @@
|
||||
"picomatch": "^4.0.3",
|
||||
"std-env": "^3.10.0",
|
||||
"tinybench": "^2.9.0",
|
||||
"tinyexec": "^0.3.2",
|
||||
"tinyexec": "^1.0.2",
|
||||
"tinyglobby": "^0.2.15",
|
||||
"tinyrainbow": "^3.0.3",
|
||||
"vite": "^6.0.0 || ^7.0.0",
|
||||
@@ -4386,10 +4391,10 @@
|
||||
"@edge-runtime/vm": "*",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
|
||||
"@vitest/browser-playwright": "4.0.14",
|
||||
"@vitest/browser-preview": "4.0.14",
|
||||
"@vitest/browser-webdriverio": "4.0.14",
|
||||
"@vitest/ui": "4.0.14",
|
||||
"@vitest/browser-playwright": "4.0.15",
|
||||
"@vitest/browser-preview": "4.0.15",
|
||||
"@vitest/browser-webdriverio": "4.0.15",
|
||||
"@vitest/ui": "4.0.15",
|
||||
"happy-dom": "*",
|
||||
"jsdom": "*"
|
||||
},
|
||||
|
||||
26
package.json
26
package.json
@@ -3,7 +3,7 @@
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"version": "0.10.10",
|
||||
"version": "0.12.5",
|
||||
"description": "An ACP-compatible coding agent powered by the Claude Code SDK (TypeScript)",
|
||||
"main": "dist/lib.js",
|
||||
"bin": {
|
||||
@@ -51,25 +51,23 @@
|
||||
"author": "Zed Industries",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "0.7.0",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.1.55",
|
||||
"@anthropic-ai/claude-code": "2.0.55",
|
||||
"@modelcontextprotocol/sdk": "1.23.0",
|
||||
"@agentclientprotocol/sdk": "0.12.0",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.1.70",
|
||||
"@modelcontextprotocol/sdk": "1.25.0",
|
||||
"diff": "8.0.2",
|
||||
"uuid": "13.0.0"
|
||||
"minimatch": "^10.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@anthropic-ai/sdk": "0.71.0",
|
||||
"@types/node": "24.10.1",
|
||||
"@types/uuid": "10.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.48.0",
|
||||
"@typescript-eslint/parser": "8.48.0",
|
||||
"eslint": "9.39.1",
|
||||
"@anthropic-ai/sdk": "0.71.2",
|
||||
"@types/node": "25.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "8.50.0",
|
||||
"@typescript-eslint/parser": "8.50.0",
|
||||
"eslint": "9.39.2",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"globals": "16.5.0",
|
||||
"prettier": "3.7.3",
|
||||
"prettier": "3.7.4",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.9.3",
|
||||
"vitest": "4.0.14"
|
||||
"vitest": "4.0.15"
|
||||
}
|
||||
}
|
||||
|
||||
490
src/acp-agent.ts
490
src/acp-agent.ts
@@ -5,6 +5,8 @@ import {
|
||||
AvailableCommand,
|
||||
CancelNotification,
|
||||
ClientCapabilities,
|
||||
ForkSessionRequest,
|
||||
ForkSessionResponse,
|
||||
InitializeRequest,
|
||||
InitializeResponse,
|
||||
ndJsonStream,
|
||||
@@ -15,6 +17,8 @@ import {
|
||||
ReadTextFileRequest,
|
||||
ReadTextFileResponse,
|
||||
RequestError,
|
||||
ResumeSessionRequest,
|
||||
ResumeSessionResponse,
|
||||
SessionModelState,
|
||||
SessionNotification,
|
||||
SetSessionModelRequest,
|
||||
@@ -26,6 +30,7 @@ import {
|
||||
WriteTextFileRequest,
|
||||
WriteTextFileResponse,
|
||||
} from "@agentclientprotocol/sdk";
|
||||
import { SettingsManager } from "./settings.js";
|
||||
import {
|
||||
CanUseTool,
|
||||
McpServerConfig,
|
||||
@@ -39,9 +44,9 @@ import {
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import * as os from "node:os";
|
||||
import { v7 as uuidv7 } from "uuid";
|
||||
import { nodeToWebReadable, nodeToWebWritable, Pushable, unreachable } from "./utils.js";
|
||||
import { createMcpServer, EDIT_TOOL_NAMES, toolNames } from "./mcp-server.js";
|
||||
import { createMcpServer } from "./mcp-server.js";
|
||||
import { EDIT_TOOL_NAMES, acpToolNames } from "./tools.js";
|
||||
import {
|
||||
toolInfoFromToolUse,
|
||||
planEntries,
|
||||
@@ -49,10 +54,14 @@ import {
|
||||
ClaudePlanEntry,
|
||||
registerHookCallback,
|
||||
createPostToolUseHook,
|
||||
createPreToolUseHook,
|
||||
} from "./tools.js";
|
||||
import { ContentBlockParam } from "@anthropic-ai/sdk/resources";
|
||||
import { BetaContentBlock, BetaRawContentBlockDelta } from "@anthropic-ai/sdk/resources/beta.mjs";
|
||||
import packageJson from "../package.json" with { type: "json" };
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
export const CLAUDE_CONFIG_DIR = process.env.CLAUDE ?? path.join(os.homedir(), ".claude");
|
||||
|
||||
/**
|
||||
* Logger interface for customizing logging output
|
||||
@@ -67,6 +76,7 @@ type Session = {
|
||||
input: Pushable<SDKUserMessage>;
|
||||
cancelled: boolean;
|
||||
permissionMode: PermissionMode;
|
||||
settingsManager: SettingsManager;
|
||||
};
|
||||
|
||||
type BackgroundTerminal =
|
||||
@@ -180,6 +190,11 @@ export class ClaudeAcpAgent implements Agent {
|
||||
http: true,
|
||||
sse: true,
|
||||
},
|
||||
sessionCapabilities: {
|
||||
// TODO: announce fork capability when sessionId handling is fixed
|
||||
// fork: {},
|
||||
resume: {},
|
||||
},
|
||||
},
|
||||
agentInfo: {
|
||||
name: packageJson.name,
|
||||
@@ -189,6 +204,7 @@ export class ClaudeAcpAgent implements Agent {
|
||||
authMethods: [authMethod],
|
||||
};
|
||||
}
|
||||
|
||||
async newSession(params: NewSessionRequest): Promise<NewSessionResponse> {
|
||||
if (
|
||||
fs.existsSync(path.resolve(os.homedir(), ".claude.json.backup")) &&
|
||||
@@ -197,213 +213,39 @@ export class ClaudeAcpAgent implements Agent {
|
||||
throw RequestError.authRequired();
|
||||
}
|
||||
|
||||
const sessionId = uuidv7();
|
||||
const input = new Pushable<SDKUserMessage>();
|
||||
|
||||
const mcpServers: Record<string, McpServerConfig> = {};
|
||||
if (Array.isArray(params.mcpServers)) {
|
||||
for (const server of params.mcpServers) {
|
||||
if ("type" in server) {
|
||||
mcpServers[server.name] = {
|
||||
type: server.type,
|
||||
url: server.url,
|
||||
headers: server.headers
|
||||
? Object.fromEntries(server.headers.map((e) => [e.name, e.value]))
|
||||
: undefined,
|
||||
};
|
||||
} else {
|
||||
mcpServers[server.name] = {
|
||||
type: "stdio",
|
||||
command: server.command,
|
||||
args: server.args,
|
||||
env: server.env
|
||||
? Object.fromEntries(server.env.map((e) => [e.name, e.value]))
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only add the acp MCP server if built-in tools are not disabled
|
||||
if (!params._meta?.disableBuiltInTools) {
|
||||
const server = createMcpServer(this, sessionId, this.clientCapabilities);
|
||||
mcpServers["acp"] = {
|
||||
type: "sdk",
|
||||
name: "acp",
|
||||
instance: server,
|
||||
};
|
||||
}
|
||||
|
||||
let systemPrompt: Options["systemPrompt"] = { type: "preset", preset: "claude_code" };
|
||||
if (params._meta?.systemPrompt) {
|
||||
const customPrompt = params._meta.systemPrompt;
|
||||
if (typeof customPrompt === "string") {
|
||||
systemPrompt = customPrompt;
|
||||
} else if (
|
||||
typeof customPrompt === "object" &&
|
||||
"append" in customPrompt &&
|
||||
typeof customPrompt.append === "string"
|
||||
) {
|
||||
systemPrompt.append = customPrompt.append;
|
||||
}
|
||||
}
|
||||
|
||||
const permissionMode = "default";
|
||||
|
||||
// Extract options from _meta if provided
|
||||
const userProvidedOptions = (params._meta as NewSessionMeta | undefined)?.claudeCode?.options;
|
||||
|
||||
const options: Options = {
|
||||
systemPrompt,
|
||||
settingSources: ["user", "project", "local"],
|
||||
stderr: (err) => this.logger.error(err),
|
||||
...userProvidedOptions,
|
||||
// Override certain fields that must be controlled by ACP
|
||||
cwd: params.cwd,
|
||||
includePartialMessages: true,
|
||||
mcpServers: { ...(userProvidedOptions?.mcpServers || {}), ...mcpServers },
|
||||
// If we want bypassPermissions to be an option, we have to allow it here.
|
||||
// But it doesn't work in root mode, so we only activate it if it will work.
|
||||
allowDangerouslySkipPermissions: !IS_ROOT,
|
||||
permissionMode,
|
||||
canUseTool: this.canUseTool(sessionId),
|
||||
// note: although not documented by the types, passing an absolute path
|
||||
// here works to find zed's managed node version.
|
||||
executable: process.execPath as any,
|
||||
...(process.env.CLAUDE_CODE_EXECUTABLE && {
|
||||
pathToClaudeCodeExecutable: process.env.CLAUDE_CODE_EXECUTABLE,
|
||||
}),
|
||||
hooks: {
|
||||
...userProvidedOptions?.hooks,
|
||||
PostToolUse: [
|
||||
...(userProvidedOptions?.hooks?.PostToolUse || []),
|
||||
{
|
||||
hooks: [createPostToolUseHook(this.logger)],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const allowedTools = [];
|
||||
const disallowedTools = [];
|
||||
|
||||
// Check if built-in tools should be disabled
|
||||
const disableBuiltInTools = params._meta?.disableBuiltInTools === true;
|
||||
|
||||
if (!disableBuiltInTools) {
|
||||
if (this.clientCapabilities?.fs?.readTextFile) {
|
||||
allowedTools.push(toolNames.read);
|
||||
disallowedTools.push("Read");
|
||||
}
|
||||
if (this.clientCapabilities?.fs?.writeTextFile) {
|
||||
disallowedTools.push("Write", "Edit");
|
||||
}
|
||||
if (this.clientCapabilities?.terminal) {
|
||||
allowedTools.push(toolNames.bashOutput, toolNames.killShell);
|
||||
disallowedTools.push("Bash", "BashOutput", "KillShell");
|
||||
}
|
||||
} else {
|
||||
// When built-in tools are disabled, explicitly disallow all of them
|
||||
disallowedTools.push(
|
||||
toolNames.read,
|
||||
toolNames.write,
|
||||
toolNames.edit,
|
||||
toolNames.bash,
|
||||
toolNames.bashOutput,
|
||||
toolNames.killShell,
|
||||
"Read",
|
||||
"Write",
|
||||
"Edit",
|
||||
"Bash",
|
||||
"BashOutput",
|
||||
"KillShell",
|
||||
"Glob",
|
||||
"Grep",
|
||||
"Task",
|
||||
"TodoWrite",
|
||||
"ExitPlanMode",
|
||||
"WebSearch",
|
||||
"WebFetch",
|
||||
"AskUserQuestion",
|
||||
"SlashCommand",
|
||||
"Skill",
|
||||
"NotebookEdit",
|
||||
);
|
||||
}
|
||||
|
||||
if (allowedTools.length > 0) {
|
||||
options.allowedTools = allowedTools;
|
||||
}
|
||||
if (disallowedTools.length > 0) {
|
||||
options.disallowedTools = disallowedTools;
|
||||
}
|
||||
|
||||
// Handle abort controller from meta options
|
||||
const abortController = userProvidedOptions?.abortController;
|
||||
if (abortController?.signal.aborted) {
|
||||
throw new Error("Cancelled");
|
||||
}
|
||||
|
||||
const q = query({
|
||||
prompt: input,
|
||||
options,
|
||||
return await this.createSession(params, {
|
||||
// Revisit these meta values once we support resume
|
||||
resume: (params._meta as NewSessionMeta | undefined)?.claudeCode?.options?.resume,
|
||||
});
|
||||
}
|
||||
|
||||
this.sessions[sessionId] = {
|
||||
query: q,
|
||||
input: input,
|
||||
cancelled: false,
|
||||
permissionMode,
|
||||
};
|
||||
|
||||
const availableCommands = await getAvailableSlashCommands(q);
|
||||
const models = await getAvailableModels(q);
|
||||
|
||||
// Needs to happen after we return the session
|
||||
setTimeout(() => {
|
||||
this.client.sessionUpdate({
|
||||
sessionId,
|
||||
update: {
|
||||
sessionUpdate: "available_commands_update",
|
||||
availableCommands,
|
||||
},
|
||||
});
|
||||
}, 0);
|
||||
|
||||
const availableModes = [
|
||||
async forkSession(params: ForkSessionRequest): Promise<ForkSessionResponse> {
|
||||
return await this.createSession(
|
||||
{
|
||||
id: "default",
|
||||
name: "Always Ask",
|
||||
description: "Prompts for permission on first use of each tool",
|
||||
cwd: params.cwd,
|
||||
mcpServers: params.mcpServers ?? [],
|
||||
_meta: params._meta,
|
||||
},
|
||||
{
|
||||
id: "acceptEdits",
|
||||
name: "Accept Edits",
|
||||
description: "Automatically accepts file edit permissions for the session",
|
||||
resume: params.sessionId,
|
||||
forkSession: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async resumeSession(params: ResumeSessionRequest): Promise<ResumeSessionResponse> {
|
||||
const response = await this.createSession(
|
||||
{
|
||||
cwd: params.cwd,
|
||||
mcpServers: params.mcpServers ?? [],
|
||||
_meta: params._meta,
|
||||
},
|
||||
{
|
||||
id: "plan",
|
||||
name: "Plan Mode",
|
||||
description: "Claude can analyze but not modify files or execute commands",
|
||||
resume: params.sessionId,
|
||||
},
|
||||
];
|
||||
// Only works in non-root mode
|
||||
if (!IS_ROOT) {
|
||||
availableModes.push({
|
||||
id: "bypassPermissions",
|
||||
name: "Bypass Permissions",
|
||||
description: "Skips all permission prompts",
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
sessionId,
|
||||
models,
|
||||
modes: {
|
||||
currentModeId: permissionMode,
|
||||
availableModes,
|
||||
},
|
||||
};
|
||||
return response;
|
||||
}
|
||||
|
||||
async authenticate(_params: AuthenticateRequest): Promise<void> {
|
||||
@@ -428,6 +270,7 @@ export class ClaudeAcpAgent implements Agent {
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
switch (message.type) {
|
||||
case "system":
|
||||
switch (message.subtype) {
|
||||
@@ -595,6 +438,7 @@ export class ClaudeAcpAgent implements Agent {
|
||||
case "default":
|
||||
case "acceptEdits":
|
||||
case "bypassPermissions":
|
||||
case "dontAsk":
|
||||
case "plan":
|
||||
this.sessions[params.sessionId].permissionMode = params.modeId;
|
||||
try {
|
||||
@@ -626,7 +470,7 @@ export class ClaudeAcpAgent implements Agent {
|
||||
}
|
||||
|
||||
canUseTool(sessionId: string): CanUseTool {
|
||||
return async (toolName, toolInput, { suggestions, toolUseID }) => {
|
||||
return async (toolName, toolInput, { signal, suggestions, toolUseID }) => {
|
||||
const session = this.sessions[sessionId];
|
||||
if (!session) {
|
||||
return {
|
||||
@@ -659,6 +503,9 @@ export class ClaudeAcpAgent implements Agent {
|
||||
},
|
||||
});
|
||||
|
||||
if (signal.aborted || response.outcome?.outcome === "cancelled") {
|
||||
throw new Error("Tool use aborted");
|
||||
}
|
||||
if (
|
||||
response.outcome?.outcome === "selected" &&
|
||||
(response.outcome.optionId === "default" || response.outcome.optionId === "acceptEdits")
|
||||
@@ -722,6 +569,9 @@ export class ClaudeAcpAgent implements Agent {
|
||||
).title,
|
||||
},
|
||||
});
|
||||
if (signal.aborted || response.outcome?.outcome === "cancelled") {
|
||||
throw new Error("Tool use aborted");
|
||||
}
|
||||
if (
|
||||
response.outcome?.outcome === "selected" &&
|
||||
(response.outcome.optionId === "allow" || response.outcome.optionId === "allow_always")
|
||||
@@ -754,6 +604,244 @@ export class ClaudeAcpAgent implements Agent {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private async createSession(
|
||||
params: NewSessionRequest,
|
||||
creationOpts: { resume?: string; forkSession?: boolean } = {},
|
||||
): Promise<NewSessionResponse> {
|
||||
const sessionId = creationOpts.resume ?? randomUUID();
|
||||
const input = new Pushable<SDKUserMessage>();
|
||||
|
||||
const settingsManager = new SettingsManager(params.cwd, {
|
||||
logger: this.logger,
|
||||
});
|
||||
await settingsManager.initialize();
|
||||
|
||||
const mcpServers: Record<string, McpServerConfig> = {};
|
||||
if (Array.isArray(params.mcpServers)) {
|
||||
for (const server of params.mcpServers) {
|
||||
if ("type" in server) {
|
||||
mcpServers[server.name] = {
|
||||
type: server.type,
|
||||
url: server.url,
|
||||
headers: server.headers
|
||||
? Object.fromEntries(server.headers.map((e) => [e.name, e.value]))
|
||||
: undefined,
|
||||
};
|
||||
} else {
|
||||
mcpServers[server.name] = {
|
||||
type: "stdio",
|
||||
command: server.command,
|
||||
args: server.args,
|
||||
env: server.env
|
||||
? Object.fromEntries(server.env.map((e) => [e.name, e.value]))
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only add the acp MCP server if built-in tools are not disabled
|
||||
if (!params._meta?.disableBuiltInTools) {
|
||||
const server = createMcpServer(this, sessionId, this.clientCapabilities);
|
||||
mcpServers["acp"] = {
|
||||
type: "sdk",
|
||||
name: "acp",
|
||||
instance: server,
|
||||
};
|
||||
}
|
||||
|
||||
let systemPrompt: Options["systemPrompt"] = { type: "preset", preset: "claude_code" };
|
||||
if (params._meta?.systemPrompt) {
|
||||
const customPrompt = params._meta.systemPrompt;
|
||||
if (typeof customPrompt === "string") {
|
||||
systemPrompt = customPrompt;
|
||||
} else if (
|
||||
typeof customPrompt === "object" &&
|
||||
"append" in customPrompt &&
|
||||
typeof customPrompt.append === "string"
|
||||
) {
|
||||
systemPrompt.append = customPrompt.append;
|
||||
}
|
||||
}
|
||||
|
||||
const permissionMode = "default";
|
||||
|
||||
// Extract options from _meta if provided
|
||||
const userProvidedOptions = (params._meta as NewSessionMeta | undefined)?.claudeCode?.options;
|
||||
const extraArgs = { ...userProvidedOptions?.extraArgs };
|
||||
if (creationOpts?.resume === undefined) {
|
||||
// Set our own session id if not resuming an existing session.
|
||||
// TODO: find a way to make this work for fork
|
||||
extraArgs["session-id"] = sessionId;
|
||||
}
|
||||
|
||||
const options: Options = {
|
||||
systemPrompt,
|
||||
settingSources: ["user", "project", "local"],
|
||||
stderr: (err) => this.logger.error(err),
|
||||
...userProvidedOptions,
|
||||
// Override certain fields that must be controlled by ACP
|
||||
cwd: params.cwd,
|
||||
includePartialMessages: true,
|
||||
mcpServers: { ...(userProvidedOptions?.mcpServers || {}), ...mcpServers },
|
||||
extraArgs,
|
||||
// If we want bypassPermissions to be an option, we have to allow it here.
|
||||
// But it doesn't work in root mode, so we only activate it if it will work.
|
||||
allowDangerouslySkipPermissions: !IS_ROOT,
|
||||
permissionMode,
|
||||
canUseTool: this.canUseTool(sessionId),
|
||||
// note: although not documented by the types, passing an absolute path
|
||||
// here works to find zed's managed node version.
|
||||
executable: process.execPath as any,
|
||||
...(process.env.CLAUDE_CODE_EXECUTABLE && {
|
||||
pathToClaudeCodeExecutable: process.env.CLAUDE_CODE_EXECUTABLE,
|
||||
}),
|
||||
hooks: {
|
||||
...userProvidedOptions?.hooks,
|
||||
PreToolUse: [
|
||||
...(userProvidedOptions?.hooks?.PreToolUse || []),
|
||||
{
|
||||
hooks: [createPreToolUseHook(settingsManager, this.logger)],
|
||||
},
|
||||
],
|
||||
PostToolUse: [
|
||||
...(userProvidedOptions?.hooks?.PostToolUse || []),
|
||||
{
|
||||
hooks: [createPostToolUseHook(this.logger)],
|
||||
},
|
||||
],
|
||||
},
|
||||
...creationOpts,
|
||||
};
|
||||
|
||||
const allowedTools = [];
|
||||
const disallowedTools = [];
|
||||
|
||||
// Check if built-in tools should be disabled
|
||||
const disableBuiltInTools = params._meta?.disableBuiltInTools === true;
|
||||
|
||||
if (!disableBuiltInTools) {
|
||||
if (this.clientCapabilities?.fs?.readTextFile) {
|
||||
allowedTools.push(acpToolNames.read);
|
||||
disallowedTools.push("Read");
|
||||
}
|
||||
if (this.clientCapabilities?.fs?.writeTextFile) {
|
||||
disallowedTools.push("Write", "Edit");
|
||||
}
|
||||
if (this.clientCapabilities?.terminal) {
|
||||
allowedTools.push(acpToolNames.bashOutput, acpToolNames.killShell);
|
||||
disallowedTools.push("Bash", "BashOutput", "KillShell");
|
||||
}
|
||||
} else {
|
||||
// When built-in tools are disabled, explicitly disallow all of them
|
||||
disallowedTools.push(
|
||||
acpToolNames.read,
|
||||
acpToolNames.write,
|
||||
acpToolNames.edit,
|
||||
acpToolNames.bash,
|
||||
acpToolNames.bashOutput,
|
||||
acpToolNames.killShell,
|
||||
"Read",
|
||||
"Write",
|
||||
"Edit",
|
||||
"Bash",
|
||||
"BashOutput",
|
||||
"KillShell",
|
||||
"Glob",
|
||||
"Grep",
|
||||
"Task",
|
||||
"TodoWrite",
|
||||
"ExitPlanMode",
|
||||
"WebSearch",
|
||||
"WebFetch",
|
||||
"AskUserQuestion",
|
||||
"SlashCommand",
|
||||
"Skill",
|
||||
"NotebookEdit",
|
||||
);
|
||||
}
|
||||
|
||||
if (allowedTools.length > 0) {
|
||||
options.allowedTools = allowedTools;
|
||||
}
|
||||
if (disallowedTools.length > 0) {
|
||||
options.disallowedTools = disallowedTools;
|
||||
}
|
||||
|
||||
// Handle abort controller from meta options
|
||||
const abortController = userProvidedOptions?.abortController;
|
||||
if (abortController?.signal.aborted) {
|
||||
throw new Error("Cancelled");
|
||||
}
|
||||
|
||||
const q = query({
|
||||
prompt: input,
|
||||
options,
|
||||
});
|
||||
|
||||
this.sessions[sessionId] = {
|
||||
query: q,
|
||||
input: input,
|
||||
cancelled: false,
|
||||
permissionMode,
|
||||
settingsManager,
|
||||
};
|
||||
|
||||
const availableCommands = await getAvailableSlashCommands(q);
|
||||
const models = await getAvailableModels(q);
|
||||
|
||||
// Needs to happen after we return the session
|
||||
setTimeout(() => {
|
||||
this.client.sessionUpdate({
|
||||
sessionId,
|
||||
update: {
|
||||
sessionUpdate: "available_commands_update",
|
||||
availableCommands,
|
||||
},
|
||||
});
|
||||
}, 0);
|
||||
|
||||
const availableModes = [
|
||||
{
|
||||
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 (!IS_ROOT) {
|
||||
availableModes.push({
|
||||
id: "bypassPermissions",
|
||||
name: "Bypass Permissions",
|
||||
description: "Bypass all permission checks",
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
sessionId,
|
||||
models,
|
||||
modes: {
|
||||
currentModeId: permissionMode,
|
||||
availableModes,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function getAvailableModels(query: Query): Promise<SessionModelState> {
|
||||
|
||||
18
src/lib.ts
18
src/lib.ts
@@ -15,8 +15,22 @@ export {
|
||||
Pushable,
|
||||
unreachable,
|
||||
} from "./utils.js";
|
||||
export { createMcpServer, toolNames } from "./mcp-server.js";
|
||||
export { toolInfoFromToolUse, planEntries, toolUpdateFromToolResult } from "./tools.js";
|
||||
export { createMcpServer } from "./mcp-server.js";
|
||||
export {
|
||||
toolInfoFromToolUse,
|
||||
planEntries,
|
||||
toolUpdateFromToolResult,
|
||||
createPreToolUseHook,
|
||||
acpToolNames as toolNames,
|
||||
} from "./tools.js";
|
||||
export {
|
||||
SettingsManager,
|
||||
type ClaudeCodeSettings,
|
||||
type PermissionSettings,
|
||||
type PermissionDecision,
|
||||
type PermissionCheckResult,
|
||||
type SettingsManagerOptions,
|
||||
} from "./settings.js";
|
||||
|
||||
// Export types
|
||||
export type { ClaudePlanEntry } from "./tools.js";
|
||||
|
||||
@@ -1,10 +1,24 @@
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import {
|
||||
BashInput,
|
||||
FileEditInput,
|
||||
FileReadInput,
|
||||
FileWriteInput,
|
||||
KillShellInput,
|
||||
} from "@anthropic-ai/claude-agent-sdk/sdk-tools.js";
|
||||
import { z } from "zod";
|
||||
import { ClaudeAcpAgent } from "./acp-agent.js";
|
||||
import { ClientCapabilities, TerminalOutputResponse } from "@agentclientprotocol/sdk";
|
||||
import { CLAUDE_CONFIG_DIR, ClaudeAcpAgent } from "./acp-agent.js";
|
||||
import {
|
||||
ClientCapabilities,
|
||||
ReadTextFileResponse,
|
||||
TerminalOutputResponse,
|
||||
} from "@agentclientprotocol/sdk";
|
||||
import * as diff from "diff";
|
||||
import * as path from "node:path";
|
||||
import * as fs from "node:fs/promises";
|
||||
|
||||
import { sleep, unreachable, extractLinesWithByteLimit } from "./utils.js";
|
||||
import { acpToolNames } from "./tools.js";
|
||||
|
||||
export const SYSTEM_REMINDER = `
|
||||
|
||||
@@ -23,23 +37,67 @@ const unqualifiedToolNames = {
|
||||
bashOutput: "BashOutput",
|
||||
};
|
||||
|
||||
const SERVER_PREFIX = "mcp__acp__";
|
||||
export const toolNames = {
|
||||
read: SERVER_PREFIX + unqualifiedToolNames.read,
|
||||
edit: SERVER_PREFIX + unqualifiedToolNames.edit,
|
||||
write: SERVER_PREFIX + unqualifiedToolNames.write,
|
||||
bash: SERVER_PREFIX + unqualifiedToolNames.bash,
|
||||
killShell: SERVER_PREFIX + unqualifiedToolNames.killShell,
|
||||
bashOutput: SERVER_PREFIX + unqualifiedToolNames.bashOutput,
|
||||
};
|
||||
|
||||
export const EDIT_TOOL_NAMES = [toolNames.edit, toolNames.write];
|
||||
|
||||
export function createMcpServer(
|
||||
agent: ClaudeAcpAgent,
|
||||
sessionId: string,
|
||||
clientCapabilities: ClientCapabilities | undefined,
|
||||
): McpServer {
|
||||
/**
|
||||
* This checks if a given path is related to internal agent persistence and if the agent should be allowed to read/write from here.
|
||||
* We let the agent do normal fs operations on these paths so that it can persist its state.
|
||||
* However, we block access to settings files for security reasons.
|
||||
*/
|
||||
function internalPath(file_path: string) {
|
||||
return (
|
||||
file_path.startsWith(CLAUDE_CONFIG_DIR) &&
|
||||
!file_path.startsWith(path.join(CLAUDE_CONFIG_DIR, "settings.json")) &&
|
||||
!file_path.startsWith(path.join(CLAUDE_CONFIG_DIR, "session-env"))
|
||||
);
|
||||
}
|
||||
|
||||
async function readTextFile(input: FileReadInput): Promise<ReadTextFileResponse> {
|
||||
if (internalPath(input.file_path)) {
|
||||
const content = await fs.readFile(input.file_path, "utf8");
|
||||
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (input.offset != null || input.limit != null) {
|
||||
const lines = content.split("\n");
|
||||
|
||||
// Apply offset and limit if provided
|
||||
const offset = input.offset ?? 1;
|
||||
const limit = input.limit ?? lines.length;
|
||||
|
||||
// Extract the requested lines (offset is 1-based)
|
||||
const startIndex = Math.max(0, offset - 1);
|
||||
const endIndex = Math.min(lines.length, startIndex + limit);
|
||||
const selectedLines = lines.slice(startIndex, endIndex);
|
||||
|
||||
return { content: selectedLines.join("\n") };
|
||||
} else {
|
||||
return { content };
|
||||
}
|
||||
}
|
||||
|
||||
return agent.readTextFile({
|
||||
sessionId,
|
||||
path: input.file_path,
|
||||
line: input.offset,
|
||||
limit: input.limit,
|
||||
});
|
||||
}
|
||||
|
||||
async function writeTextFile(input: FileWriteInput): Promise<void> {
|
||||
if (internalPath(input.file_path)) {
|
||||
await fs.writeFile(input.file_path, input.content, "utf8");
|
||||
} else {
|
||||
await agent.writeTextFile({
|
||||
sessionId,
|
||||
path: input.file_path,
|
||||
content: input.content,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Create MCP server
|
||||
const server = new McpServer({ name: "acp", version: "1.0.0" }, { capabilities: { tools: {} } });
|
||||
|
||||
@@ -50,7 +108,7 @@ export function createMcpServer(
|
||||
title: unqualifiedToolNames.read,
|
||||
description: `Reads the content of the given file in the project.
|
||||
|
||||
In sessions with ${toolNames.read} always use it instead of Read as it contains the most up-to-date contents.
|
||||
In sessions with ${acpToolNames.read} always use it instead of Read as it contains the most up-to-date contents.
|
||||
|
||||
Reads a file from the local filesystem. If the User provides a path to a file assume that path is valid. It is okay to read a file that does not exist; an error will be returned.
|
||||
|
||||
@@ -60,7 +118,7 @@ Usage:
|
||||
- You can optionally specify a line offset and limit (especially handy for long files), but it's recommended to read the whole file by not providing these parameters
|
||||
- Any files larger than ${defaults.maxFileSize} bytes will be truncated
|
||||
- This tool allows Claude Code to read images (eg PNG, JPG, etc). When reading an image file the contents are presented visually as Claude Code is a multimodal LLM.
|
||||
- This tool can only read files, not directories. To read a directory, use an ls command via the ${toolNames.bash} tool.
|
||||
- This tool can only read files, not directories. To read a directory, use an ls command via the ${acpToolNames.bash} tool.
|
||||
- You have the capability to call multiple tools in a single response. It is always better to speculatively read multiple files as a batch that are potentially useful.`,
|
||||
inputSchema: {
|
||||
file_path: z.string().describe("The absolute path to the file to read"),
|
||||
@@ -87,7 +145,7 @@ Usage:
|
||||
idempotentHint: false,
|
||||
},
|
||||
},
|
||||
async (input) => {
|
||||
async (input: FileReadInput) => {
|
||||
try {
|
||||
const session = agent.sessions[sessionId];
|
||||
if (!session) {
|
||||
@@ -101,12 +159,7 @@ Usage:
|
||||
};
|
||||
}
|
||||
|
||||
const readResponse = await agent.readTextFile({
|
||||
sessionId,
|
||||
path: input.file_path,
|
||||
line: input.offset,
|
||||
limit: input.limit,
|
||||
});
|
||||
const readResponse = await readTextFile(input);
|
||||
|
||||
if (typeof readResponse?.content !== "string") {
|
||||
throw new Error(`No file contents for ${input.file_path}.`);
|
||||
@@ -117,7 +170,7 @@ Usage:
|
||||
|
||||
// Construct informative message about what was read
|
||||
let readInfo = "";
|
||||
if (input.offset > 1 || result.wasLimited) {
|
||||
if ((input.offset && input.offset > 1) || result.wasLimited) {
|
||||
readInfo = "\n\n<file-read-info>";
|
||||
|
||||
if (result.wasLimited) {
|
||||
@@ -162,12 +215,12 @@ Usage:
|
||||
title: unqualifiedToolNames.write,
|
||||
description: `Writes a file to the local filesystem..
|
||||
|
||||
In sessions with ${toolNames.write} always use it instead of Write as it will
|
||||
In sessions with ${acpToolNames.write} always use it instead of Write as it will
|
||||
allow the user to conveniently review changes.
|
||||
|
||||
Usage:
|
||||
- This tool will overwrite the existing file if there is one at the provided path.
|
||||
- If this is an existing file, you MUST use the ${toolNames.read} tool first to read the file's contents. This tool will fail if you did not read the file first.
|
||||
- If this is an existing file, you MUST use the ${acpToolNames.read} tool first to read the file's contents. This tool will fail if you did not read the file first.
|
||||
- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.
|
||||
- NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.
|
||||
- Only use emojis if the user explicitly requests it. Avoid writing emojis to files unless asked.`,
|
||||
@@ -185,7 +238,7 @@ Usage:
|
||||
idempotentHint: false,
|
||||
},
|
||||
},
|
||||
async (input) => {
|
||||
async (input: FileWriteInput) => {
|
||||
try {
|
||||
const session = agent.sessions[sessionId];
|
||||
if (!session) {
|
||||
@@ -198,11 +251,7 @@ Usage:
|
||||
],
|
||||
};
|
||||
}
|
||||
await agent.writeTextFile({
|
||||
sessionId,
|
||||
path: input.file_path,
|
||||
content: input.content,
|
||||
});
|
||||
await writeTextFile(input);
|
||||
|
||||
return {
|
||||
content: [],
|
||||
@@ -226,11 +275,11 @@ Usage:
|
||||
title: unqualifiedToolNames.edit,
|
||||
description: `Performs exact string replacements in files.
|
||||
|
||||
In sessions with ${toolNames.edit} always use it instead of Edit as it will
|
||||
In sessions with ${acpToolNames.edit} always use it instead of Edit as it will
|
||||
allow the user to conveniently review changes.
|
||||
|
||||
Usage:
|
||||
- You must use your \`${toolNames.read}\` tool at least once in the conversation before editing. This tool will error if you attempt an edit without reading the file.
|
||||
- You must use your \`${acpToolNames.read}\` tool at least once in the conversation before editing. This tool will error if you attempt an edit without reading the file.
|
||||
- When editing text from Read tool output, ensure you preserve the exact indentation (tabs/spaces) as it appears.
|
||||
- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.
|
||||
- Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked.
|
||||
@@ -246,7 +295,7 @@ Usage:
|
||||
.boolean()
|
||||
.default(false)
|
||||
.optional()
|
||||
.describe("Replace all occurences of old_string (default false)"),
|
||||
.describe("Replace all occurrences of old_string (default false)"),
|
||||
},
|
||||
annotations: {
|
||||
title: "Edit file",
|
||||
@@ -256,7 +305,7 @@ Usage:
|
||||
idempotentHint: false,
|
||||
},
|
||||
},
|
||||
async (input) => {
|
||||
async (input: FileEditInput) => {
|
||||
try {
|
||||
const session = agent.sessions[sessionId];
|
||||
if (!session) {
|
||||
@@ -270,9 +319,8 @@ Usage:
|
||||
};
|
||||
}
|
||||
|
||||
const readResponse = await agent.readTextFile({
|
||||
sessionId,
|
||||
path: input.file_path,
|
||||
const readResponse = await readTextFile({
|
||||
file_path: input.file_path,
|
||||
});
|
||||
|
||||
if (typeof readResponse?.content !== "string") {
|
||||
@@ -289,11 +337,7 @@ Usage:
|
||||
|
||||
const patch = diff.createPatch(input.file_path, readResponse.content, newContent);
|
||||
|
||||
await agent.writeTextFile({
|
||||
sessionId,
|
||||
path: input.file_path,
|
||||
content: newContent,
|
||||
});
|
||||
await writeTextFile({ file_path: input.file_path, content: newContent });
|
||||
|
||||
return {
|
||||
content: [
|
||||
@@ -324,13 +368,10 @@ Usage:
|
||||
title: unqualifiedToolNames.bash,
|
||||
description: `Executes a bash command
|
||||
|
||||
In sessions with ${toolNames.bash} always use it instead of Bash`,
|
||||
In sessions with ${acpToolNames.bash} always use it instead of Bash`,
|
||||
inputSchema: {
|
||||
command: z.string().describe("The command to execute"),
|
||||
timeout: z
|
||||
.number()
|
||||
.default(2 * 60 * 1000)
|
||||
.describe(`Optional timeout in milliseconds (max ${2 * 60 * 1000})`),
|
||||
timeout: z.number().describe(`Optional timeout in milliseconds (max ${2 * 60 * 1000})`),
|
||||
description: z.string().optional()
|
||||
.describe(`Clear, concise description of what this command does in 5-10 words, in active voice. Examples:
|
||||
Input: ls
|
||||
@@ -348,11 +389,11 @@ Output: Create directory 'foo'`),
|
||||
.boolean()
|
||||
.default(false)
|
||||
.describe(
|
||||
`Set to true to run this command in the background. The tool returns an \`id\` that can be used with the \`${toolNames.bashOutput}\` tool to retrieve the current output, or the \`${toolNames.killShell}\` tool to stop it early.`,
|
||||
`Set to true to run this command in the background. The tool returns an \`id\` that can be used with the \`${acpToolNames.bashOutput}\` tool to retrieve the current output, or the \`${acpToolNames.killShell}\` tool to stop it early.`,
|
||||
),
|
||||
},
|
||||
},
|
||||
async (input, extra) => {
|
||||
async (input: BashInput, extra) => {
|
||||
const session = agent.sessions[sessionId];
|
||||
if (!session) {
|
||||
return {
|
||||
@@ -406,7 +447,7 @@ Output: Create directory 'foo'`),
|
||||
const statusPromise = Promise.race([
|
||||
handle.waitForExit().then((exitStatus) => ({ status: "exited" as const, exitStatus })),
|
||||
abortPromise.then(() => ({ status: "aborted" as const, exitStatus: null })),
|
||||
sleep(input.timeout).then(async () => {
|
||||
sleep(input.timeout ?? 2 * 60 * 1000).then(async () => {
|
||||
if (agent.backgroundTerminals[handle.id]?.status === "started") {
|
||||
await handle.kill();
|
||||
}
|
||||
@@ -475,23 +516,25 @@ Output: Create directory 'foo'`),
|
||||
{
|
||||
title: unqualifiedToolNames.bashOutput,
|
||||
description: `- Retrieves output from a running or completed background bash shell
|
||||
- Takes a shell_id parameter identifying the shell
|
||||
- Takes a bash_id parameter identifying the shell
|
||||
- Always returns only new output since the last check
|
||||
- Returns stdout and stderr output along with shell status
|
||||
- Use this tool when you need to monitor or check the output of a long-running shell
|
||||
|
||||
In sessions with ${toolNames.bashOutput} always use it instead of BashOutput.`,
|
||||
In sessions with ${acpToolNames.bashOutput} always use it for output from Bash commands instead of TaskOutput.`,
|
||||
inputSchema: {
|
||||
shell_id: z
|
||||
bash_id: z
|
||||
.string()
|
||||
.describe(`The id of the background bash command as returned by \`${toolNames.bash}\``),
|
||||
.describe(
|
||||
`The id of the background bash command as returned by \`${acpToolNames.bash}\``,
|
||||
),
|
||||
},
|
||||
},
|
||||
async (input) => {
|
||||
const bgTerm = agent.backgroundTerminals[input.shell_id];
|
||||
const bgTerm = agent.backgroundTerminals[input.bash_id];
|
||||
|
||||
if (!bgTerm) {
|
||||
throw new Error(`Unknown shell ${input.shell_id}`);
|
||||
throw new Error(`Unknown shell ${input.bash_id}`);
|
||||
}
|
||||
|
||||
if (bgTerm.status === "started") {
|
||||
@@ -535,14 +578,16 @@ In sessions with ${toolNames.bashOutput} always use it instead of BashOutput.`,
|
||||
- Returns a success or failure status
|
||||
- Use this tool when you need to terminate a long-running shell
|
||||
|
||||
In sessions with ${toolNames.killShell} always use it instead of KillShell.`,
|
||||
In sessions with ${acpToolNames.killShell} always use it instead of KillShell.`,
|
||||
inputSchema: {
|
||||
shell_id: z
|
||||
.string()
|
||||
.describe(`The id of the background bash command as returned by \`${toolNames.bash}\``),
|
||||
.describe(
|
||||
`The id of the background bash command as returned by \`${acpToolNames.bash}\``,
|
||||
),
|
||||
},
|
||||
},
|
||||
async (input) => {
|
||||
async (input: KillShellInput) => {
|
||||
const bgTerm = agent.backgroundTerminals[input.shell_id];
|
||||
|
||||
if (!bgTerm) {
|
||||
|
||||
522
src/settings.ts
Normal file
522
src/settings.ts
Normal file
@@ -0,0 +1,522 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import * as os from "node:os";
|
||||
import { minimatch } from "minimatch";
|
||||
import { ACP_TOOL_NAME_PREFIX, acpToolNames } from "./tools.js";
|
||||
import { CLAUDE_CONFIG_DIR } from "./acp-agent.js";
|
||||
|
||||
/**
|
||||
* Permission rule format examples:
|
||||
* - "Read" - matches all Read tool calls
|
||||
* - "Read(./.env)" - matches specific path
|
||||
* - "Read(./.env.*)" - glob pattern
|
||||
* - "Read(./secrets/**)" - recursive glob
|
||||
* - "Bash(npm run lint)" - exact command prefix
|
||||
* - "Bash(npm run:*)" - command prefix with wildcard
|
||||
*
|
||||
* Docs: https://code.claude.com/docs/en/iam#tool-specific-permission-rules
|
||||
*/
|
||||
|
||||
export interface PermissionSettings {
|
||||
allow?: string[];
|
||||
deny?: string[];
|
||||
ask?: string[];
|
||||
additionalDirectories?: string[];
|
||||
defaultMode?: string;
|
||||
}
|
||||
|
||||
export interface ClaudeCodeSettings {
|
||||
permissions?: PermissionSettings;
|
||||
env?: Record<string, string>;
|
||||
}
|
||||
|
||||
export type PermissionDecision = "allow" | "deny" | "ask";
|
||||
|
||||
export interface PermissionCheckResult {
|
||||
decision: PermissionDecision;
|
||||
rule?: string;
|
||||
source?: "allow" | "deny" | "ask";
|
||||
}
|
||||
|
||||
interface ParsedRule {
|
||||
toolName: string;
|
||||
argument?: string;
|
||||
isWildcard?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shell operators that can be used for command chaining/injection
|
||||
* These should cause a prefix match to fail to prevent bypasses like:
|
||||
* - "safe-cmd && malicious-cmd"
|
||||
* - "safe-cmd; malicious-cmd"
|
||||
* - "safe-cmd | malicious-cmd"
|
||||
* - "safe-cmd || malicious-cmd"
|
||||
* - "$(malicious-cmd)"
|
||||
* - "`malicious-cmd`"
|
||||
*/
|
||||
const SHELL_OPERATORS = ["&&", "||", ";", "|", "$(", "`", "\n"];
|
||||
|
||||
/**
|
||||
* Checks if a string contains shell operators that could allow command chaining
|
||||
*/
|
||||
function containsShellOperator(str: string): boolean {
|
||||
return SHELL_OPERATORS.some((op) => str.includes(op));
|
||||
}
|
||||
|
||||
/*
|
||||
* Tools that modify files. Per Claude Code docs:
|
||||
* "Edit rules apply to all built-in tools that edit files."
|
||||
* This means an Edit(...) rule should match Write, MultiEdit, etc.
|
||||
*/
|
||||
const FILE_EDITING_TOOLS = [acpToolNames.edit, acpToolNames.write];
|
||||
|
||||
/**
|
||||
* Tools that read files. Per Claude Code docs:
|
||||
* "Claude will make a best-effort attempt to apply Read rules to all built-in tools
|
||||
* that read files like Grep and Glob."
|
||||
* This means a Read(...) rule should match Grep, Glob, etc.
|
||||
*/
|
||||
const FILE_READING_TOOLS = [acpToolNames.read];
|
||||
|
||||
/**
|
||||
* Functions to extract the relevant argument from tool input for permission matching
|
||||
*/
|
||||
const TOOL_ARG_ACCESSORS: Record<string, (input: unknown) => string | undefined> = {
|
||||
mcp__acp__Read: (input) => (input as { file_path?: string })?.file_path,
|
||||
mcp__acp__Edit: (input) => (input as { file_path?: string })?.file_path,
|
||||
mcp__acp__Write: (input) => (input as { file_path?: string })?.file_path,
|
||||
mcp__acp__Bash: (input) => (input as { command?: string })?.command,
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses a permission rule string into its components
|
||||
* Examples:
|
||||
* "Read" -> { toolName: "Read" }
|
||||
* "Read(./.env)" -> { toolName: "Read", argument: "./.env" }
|
||||
* "Bash(npm run:*)" -> { toolName: "Bash", argument: "npm run", isWildcard: true }
|
||||
*/
|
||||
function parseRule(rule: string): ParsedRule {
|
||||
const match = rule.match(/^(\w+)(?:\((.+)\))?$/);
|
||||
if (!match) {
|
||||
return { toolName: rule };
|
||||
}
|
||||
|
||||
const [, toolName, argument] = match;
|
||||
|
||||
if (argument && argument.endsWith(":*")) {
|
||||
return {
|
||||
toolName,
|
||||
argument: argument.slice(0, -2),
|
||||
isWildcard: true,
|
||||
};
|
||||
}
|
||||
|
||||
return { toolName, argument };
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a path for comparison:
|
||||
* - Expands ~ to home directory
|
||||
* - Resolves relative paths against cwd
|
||||
* - Normalizes path separators
|
||||
*/
|
||||
function normalizePath(filePath: string, cwd: string): string {
|
||||
if (filePath.startsWith("~/")) {
|
||||
filePath = path.join(os.homedir(), filePath.slice(2));
|
||||
} else if (filePath.startsWith("./")) {
|
||||
filePath = path.join(cwd, filePath.slice(2));
|
||||
} else if (!path.isAbsolute(filePath)) {
|
||||
filePath = path.join(cwd, filePath);
|
||||
}
|
||||
return path.normalize(filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a file path matches a glob pattern
|
||||
*/
|
||||
function matchesGlob(pattern: string, filePath: string, cwd: string): boolean {
|
||||
const normalizedPattern = normalizePath(pattern, cwd);
|
||||
const normalizedPath = normalizePath(filePath, cwd);
|
||||
|
||||
return minimatch(normalizedPath, normalizedPattern, {
|
||||
dot: true,
|
||||
matchBase: false,
|
||||
nocase: process.platform === "win32",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a tool invocation matches a parsed permission rule
|
||||
*/
|
||||
function matchesRule(rule: ParsedRule, toolName: string, toolInput: unknown, cwd: string): boolean {
|
||||
// Per Claude Code docs:
|
||||
// - "Edit rules apply to all built-in tools that edit files."
|
||||
// - "Claude will make a best-effort attempt to apply Read rules to all built-in tools
|
||||
// that read files like Grep, Glob, and LS."
|
||||
const ruleAppliesToTool =
|
||||
rule.toolName === "Bash" ||
|
||||
(rule.toolName === "Edit" && FILE_EDITING_TOOLS.includes(toolName)) ||
|
||||
(rule.toolName === "Read" && FILE_READING_TOOLS.includes(toolName));
|
||||
|
||||
if (!ruleAppliesToTool) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!rule.argument) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const argAccessor = TOOL_ARG_ACCESSORS[toolName];
|
||||
if (!argAccessor) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const actualArg = argAccessor(toolInput);
|
||||
if (!actualArg) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (toolName === acpToolNames.bash) {
|
||||
// Per Claude Code docs: https://code.claude.com/docs/en/iam#tool-specific-permission-rules
|
||||
// - Bash(npm run build) matches the EXACT command "npm run build"
|
||||
// - Bash(npm run test:*) matches commands STARTING WITH "npm run test"
|
||||
// The :* suffix enables prefix matching, without it the match is exact
|
||||
//
|
||||
// Also from docs: "Claude Code is aware of shell operators (like &&) so a prefix match
|
||||
// rule like Bash(safe-cmd:*) won't give it permission to run the command safe-cmd && other-cmd"
|
||||
if (rule.isWildcard) {
|
||||
if (!actualArg.startsWith(rule.argument)) {
|
||||
return false;
|
||||
}
|
||||
// Check that the matched prefix isn't followed by shell operators that could
|
||||
// allow command chaining/injection
|
||||
const remainder = actualArg.slice(rule.argument.length);
|
||||
if (containsShellOperator(remainder)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return actualArg === rule.argument;
|
||||
}
|
||||
|
||||
// For file-based tools (Read, Edit, Write), use glob matching
|
||||
return matchesGlob(rule.argument, actualArg, cwd);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads and parses a JSON settings file, returning an empty object if not found or invalid
|
||||
*/
|
||||
async function loadSettingsFile(filePath: string | null): Promise<ClaudeCodeSettings> {
|
||||
if (!filePath) {
|
||||
return {};
|
||||
}
|
||||
|
||||
try {
|
||||
const content = await fs.promises.readFile(filePath, "utf-8");
|
||||
return JSON.parse(content) as ClaudeCodeSettings;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the enterprise settings path based on the current platform
|
||||
*/
|
||||
export function getManagedSettingsPath(): string {
|
||||
switch (process.platform) {
|
||||
case "darwin":
|
||||
return "/Library/Application Support/ClaudeCode/managed-settings.json";
|
||||
case "linux":
|
||||
return "/etc/claude-code/managed-settings.json";
|
||||
case "win32":
|
||||
return "C:\\Program Files\\ClaudeCode\\managed-settings.json";
|
||||
default:
|
||||
return "/etc/claude-code/managed-settings.json";
|
||||
}
|
||||
}
|
||||
|
||||
export interface SettingsManagerOptions {
|
||||
onChange?: () => void;
|
||||
logger?: { log: (...args: any[]) => void; error: (...args: any[]) => void };
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages Claude Code settings from multiple sources with proper precedence.
|
||||
*
|
||||
* Settings are loaded from (in order of increasing precedence):
|
||||
* 1. User settings (~/.claude/settings.json)
|
||||
* 2. Project settings (<cwd>/.claude/settings.json)
|
||||
* 3. Local project settings (<cwd>/.claude/settings.local.json)
|
||||
* 4. Enterprise managed settings (platform-specific path)
|
||||
*
|
||||
* The manager watches all settings files for changes and automatically reloads.
|
||||
*/
|
||||
export class SettingsManager {
|
||||
private cwd: string;
|
||||
private userSettings: ClaudeCodeSettings = {};
|
||||
private projectSettings: ClaudeCodeSettings = {};
|
||||
private localSettings: ClaudeCodeSettings = {};
|
||||
private enterpriseSettings: ClaudeCodeSettings = {};
|
||||
private mergedSettings: ClaudeCodeSettings = {};
|
||||
private watchers: fs.FSWatcher[] = [];
|
||||
private onChange?: () => void;
|
||||
private logger: { log: (...args: any[]) => void; error: (...args: any[]) => void };
|
||||
private initialized = false;
|
||||
private debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
constructor(cwd: string, options?: SettingsManagerOptions) {
|
||||
this.cwd = cwd;
|
||||
this.onChange = options?.onChange;
|
||||
this.logger = options?.logger ?? console;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the settings manager by loading all settings and setting up file watchers
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
if (this.initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.loadAllSettings();
|
||||
this.setupWatchers();
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the user settings file
|
||||
*/
|
||||
private getUserSettingsPath(): string {
|
||||
return path.join(CLAUDE_CONFIG_DIR, "settings.json");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the project settings file
|
||||
*/
|
||||
private getProjectSettingsPath(): string {
|
||||
return path.join(this.cwd, ".claude", "settings.json");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the local project settings file
|
||||
*/
|
||||
private getLocalSettingsPath(): string {
|
||||
return path.join(this.cwd, ".claude", "settings.local.json");
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads settings from all sources
|
||||
*/
|
||||
private async loadAllSettings(): Promise<void> {
|
||||
const [userSettings, projectSettings, localSettings, enterpriseSettings] = await Promise.all([
|
||||
loadSettingsFile(this.getUserSettingsPath()),
|
||||
loadSettingsFile(this.getProjectSettingsPath()),
|
||||
loadSettingsFile(this.getLocalSettingsPath()),
|
||||
loadSettingsFile(getManagedSettingsPath()),
|
||||
]);
|
||||
|
||||
this.userSettings = userSettings;
|
||||
this.projectSettings = projectSettings;
|
||||
this.localSettings = localSettings;
|
||||
this.enterpriseSettings = enterpriseSettings;
|
||||
|
||||
this.mergeSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges all settings sources with proper precedence.
|
||||
* For permissions, rules from all sources are combined.
|
||||
* Deny rules always take precedence during permission checks.
|
||||
*/
|
||||
private mergeSettings(): void {
|
||||
const allSettings = [
|
||||
this.userSettings,
|
||||
this.projectSettings,
|
||||
this.localSettings,
|
||||
this.enterpriseSettings,
|
||||
];
|
||||
|
||||
const merged: ClaudeCodeSettings = {
|
||||
permissions: {
|
||||
allow: [],
|
||||
deny: [],
|
||||
ask: [],
|
||||
},
|
||||
};
|
||||
|
||||
for (const settings of allSettings) {
|
||||
if (settings.permissions) {
|
||||
if (settings.permissions.allow) {
|
||||
merged.permissions!.allow!.push(...settings.permissions.allow);
|
||||
}
|
||||
if (settings.permissions.deny) {
|
||||
merged.permissions!.deny!.push(...settings.permissions.deny);
|
||||
}
|
||||
if (settings.permissions.ask) {
|
||||
merged.permissions!.ask!.push(...settings.permissions.ask);
|
||||
}
|
||||
if (settings.permissions.additionalDirectories) {
|
||||
merged.permissions!.additionalDirectories = [
|
||||
...(merged.permissions!.additionalDirectories || []),
|
||||
...settings.permissions.additionalDirectories,
|
||||
];
|
||||
}
|
||||
if (settings.permissions.defaultMode) {
|
||||
merged.permissions!.defaultMode = settings.permissions.defaultMode;
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.env) {
|
||||
merged.env = { ...merged.env, ...settings.env };
|
||||
}
|
||||
}
|
||||
|
||||
this.mergedSettings = merged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up file watchers for all settings files
|
||||
*/
|
||||
private setupWatchers(): void {
|
||||
const paths = [
|
||||
this.getUserSettingsPath(),
|
||||
this.getProjectSettingsPath(),
|
||||
this.getLocalSettingsPath(),
|
||||
getManagedSettingsPath(),
|
||||
];
|
||||
|
||||
for (const filePath of paths) {
|
||||
if (!filePath) continue;
|
||||
|
||||
try {
|
||||
const dir = path.dirname(filePath);
|
||||
const filename = path.basename(filePath);
|
||||
|
||||
if (fs.existsSync(dir)) {
|
||||
const watcher = fs.watch(dir, (eventType, changedFilename) => {
|
||||
if (changedFilename === filename) {
|
||||
this.handleSettingsChange();
|
||||
}
|
||||
});
|
||||
|
||||
watcher.on("error", (error) => {
|
||||
this.logger.error(`Settings watcher error for ${filePath}:`, error);
|
||||
});
|
||||
|
||||
this.watchers.push(watcher);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to set up watcher for ${filePath}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles settings file changes with debouncing to avoid rapid reloads
|
||||
*/
|
||||
private handleSettingsChange(): void {
|
||||
if (this.debounceTimer) {
|
||||
clearTimeout(this.debounceTimer);
|
||||
}
|
||||
|
||||
this.debounceTimer = setTimeout(async () => {
|
||||
this.debounceTimer = null;
|
||||
try {
|
||||
await this.loadAllSettings();
|
||||
this.onChange?.();
|
||||
} catch (error) {
|
||||
this.logger.error("Failed to reload settings:", error);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a tool invocation is allowed based on the loaded settings.
|
||||
*
|
||||
* @param toolName - The tool name (can be ACP-prefixed like mcp__acp__Read or plain like Read)
|
||||
* @param toolInput - The tool input object
|
||||
* @returns The permission decision and matching rule info
|
||||
*/
|
||||
checkPermission(toolName: string, toolInput: unknown): PermissionCheckResult {
|
||||
if (!toolName.startsWith(ACP_TOOL_NAME_PREFIX)) {
|
||||
return { decision: "ask" };
|
||||
}
|
||||
|
||||
const permissions = this.mergedSettings.permissions;
|
||||
|
||||
if (!permissions) {
|
||||
return { decision: "ask" };
|
||||
}
|
||||
|
||||
// Check deny rules first (highest priority)
|
||||
for (const rule of permissions.deny || []) {
|
||||
const parsed = parseRule(rule);
|
||||
if (matchesRule(parsed, toolName, toolInput, this.cwd)) {
|
||||
return { decision: "deny", rule, source: "deny" };
|
||||
}
|
||||
}
|
||||
|
||||
// Check allow rules
|
||||
for (const rule of permissions.allow || []) {
|
||||
const parsed = parseRule(rule);
|
||||
if (matchesRule(parsed, toolName, toolInput, this.cwd)) {
|
||||
return { decision: "allow", rule, source: "allow" };
|
||||
}
|
||||
}
|
||||
|
||||
// Check ask rules
|
||||
for (const rule of permissions.ask || []) {
|
||||
const parsed = parseRule(rule);
|
||||
if (matchesRule(parsed, toolName, toolInput, this.cwd)) {
|
||||
return { decision: "ask", rule, source: "ask" };
|
||||
}
|
||||
}
|
||||
|
||||
// No matching rule - default to ask
|
||||
return { decision: "ask" };
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current merged settings
|
||||
*/
|
||||
getSettings(): ClaudeCodeSettings {
|
||||
return this.mergedSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current working directory
|
||||
*/
|
||||
getCwd(): string {
|
||||
return this.cwd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the working directory and reloads project-specific settings
|
||||
*/
|
||||
async setCwd(cwd: string): Promise<void> {
|
||||
if (this.cwd === cwd) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dispose();
|
||||
this.cwd = cwd;
|
||||
this.initialized = false;
|
||||
await this.initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes of file watchers and cleans up resources
|
||||
*/
|
||||
dispose(): void {
|
||||
if (this.debounceTimer) {
|
||||
clearTimeout(this.debounceTimer);
|
||||
this.debounceTimer = null;
|
||||
}
|
||||
|
||||
for (const watcher of this.watchers) {
|
||||
watcher.close();
|
||||
}
|
||||
this.watchers = [];
|
||||
this.initialized = false;
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import { nodeToWebWritable, nodeToWebReadable } from "../utils.js";
|
||||
import { markdownEscape, toolInfoFromToolUse, toolUpdateFromToolResult } from "../tools.js";
|
||||
import { toAcpNotifications, promptToClaude } from "../acp-agent.js";
|
||||
import { query, SDKAssistantMessage } from "@anthropic-ai/claude-agent-sdk";
|
||||
import { randomUUID } from "crypto";
|
||||
|
||||
describe.skipIf(!process.env.RUN_INTEGRATION_TESTS)("ACP subprocess integration", () => {
|
||||
let child: ReturnType<typeof spawn>;
|
||||
@@ -492,50 +493,6 @@ describe("tool conversions", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle WebFetch tool calls", () => {
|
||||
const tool_use = {
|
||||
type: "tool_use",
|
||||
id: "toolu_01LxEjDn8ci9SAc3qG7LbbXV",
|
||||
name: "WebFetch",
|
||||
input: {
|
||||
url: "https://agentclientprotocol.com",
|
||||
prompt:
|
||||
"Please provide a comprehensive summary of the content on this page, including what the Agent Client Protocol is, its main features, documentation links, and any other relevant information.",
|
||||
},
|
||||
};
|
||||
|
||||
expect(toolInfoFromToolUse(tool_use, {})).toStrictEqual({
|
||||
kind: "fetch",
|
||||
title: "Fetch https://agentclientprotocol.com",
|
||||
content: [
|
||||
{
|
||||
content: {
|
||||
text: "Please provide a comprehensive summary of the content on this page, including what the Agent Client Protocol is, its main features, documentation links, and any other relevant information.",
|
||||
type: "text",
|
||||
},
|
||||
type: "content",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle WebSearch tool calls", () => {
|
||||
const tool_use = {
|
||||
type: "tool_use",
|
||||
id: "toolu_01NYMwiZFbdoQFxYxuQDFZXQ",
|
||||
name: "WebSearch",
|
||||
input: {
|
||||
query: "agentclientprotocol.com",
|
||||
},
|
||||
};
|
||||
|
||||
expect(toolInfoFromToolUse(tool_use, {})).toStrictEqual({
|
||||
kind: "fetch",
|
||||
title: '"agentclientprotocol.com"',
|
||||
content: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle KillBash entries", () => {
|
||||
const tool_use = {
|
||||
type: "tool_use",
|
||||
@@ -830,17 +787,29 @@ describe("prompt conversion", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("SDK behavior", () => {
|
||||
it.skipIf(!process.env.RUN_INTEGRATION_TESTS)(
|
||||
"query has a 'default' model",
|
||||
async () => {
|
||||
const q = query({ prompt: "hi" });
|
||||
const models = await q.supportedModels();
|
||||
const defaultModel = models.find((m) => m.value === "default");
|
||||
expect(defaultModel).toBeDefined();
|
||||
},
|
||||
10000,
|
||||
);
|
||||
describe.skipIf(!process.env.RUN_INTEGRATION_TESTS)("SDK behavior", () => {
|
||||
it("query has a 'default' model", async () => {
|
||||
const q = query({ prompt: "hi" });
|
||||
const models = await q.supportedModels();
|
||||
const defaultModel = models.find((m) => m.value === "default");
|
||||
expect(defaultModel).toBeDefined();
|
||||
}, 10000);
|
||||
|
||||
it("custom session id", async () => {
|
||||
const sessionId = randomUUID();
|
||||
const q = query({
|
||||
prompt: "hi",
|
||||
options: {
|
||||
systemPrompt: { type: "preset", preset: "claude_code" },
|
||||
extraArgs: { "session-id": sessionId },
|
||||
settingSources: ["user", "project", "local"],
|
||||
includePartialMessages: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { value } = await q.next();
|
||||
expect(value).toMatchObject({ type: "system", subtype: "init", session_id: sessionId });
|
||||
}, 10000);
|
||||
});
|
||||
|
||||
describe("permission requests", () => {
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { createMcpServer } from "../mcp-server.js";
|
||||
|
||||
const SESSION_ID = "session-id";
|
||||
|
||||
function getTool(server: any, name: string) {
|
||||
const tools = server?._registeredTools;
|
||||
if (!tools || !tools[name]) {
|
||||
throw new Error(`Tool ${name} not registered`);
|
||||
}
|
||||
return tools[name];
|
||||
}
|
||||
|
||||
function createAgent(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
sessions: {
|
||||
[SESSION_ID]: {},
|
||||
},
|
||||
readTextFile: vi.fn(),
|
||||
writeTextFile: vi.fn(),
|
||||
backgroundTerminals: {},
|
||||
clientCapabilities: {},
|
||||
client: {
|
||||
sessionUpdate: vi.fn(),
|
||||
createTerminal: vi.fn(),
|
||||
requestPermission: vi.fn(),
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("MCP server read-related tools", () => {
|
||||
it("reports missing file contents instead of crashing the read tool", async () => {
|
||||
const agent = createAgent();
|
||||
agent.readTextFile.mockResolvedValue({});
|
||||
|
||||
const server = createMcpServer(agent as any, SESSION_ID, {
|
||||
fs: { readTextFile: true },
|
||||
});
|
||||
|
||||
const readTool = getTool(server, "Read");
|
||||
|
||||
const result = await readTool.callback({
|
||||
file_path: "/tmp/file.txt",
|
||||
offset: 1,
|
||||
limit: 2000,
|
||||
});
|
||||
|
||||
expect(result.content[0].text).toBe("Reading file failed: No file contents for /tmp/file.txt.");
|
||||
});
|
||||
|
||||
it("reports missing contents inside the edit tool and skips writes", async () => {
|
||||
const agent = createAgent();
|
||||
agent.readTextFile.mockResolvedValue({});
|
||||
|
||||
const server = createMcpServer(agent as any, SESSION_ID, {
|
||||
fs: { readTextFile: true, writeTextFile: true },
|
||||
});
|
||||
|
||||
const editTool = getTool(server, "Edit");
|
||||
|
||||
const result = await editTool.callback({
|
||||
file_path: "/tmp/file.txt",
|
||||
old_string: "foo",
|
||||
new_string: "bar",
|
||||
replace_all: false,
|
||||
});
|
||||
|
||||
expect(result.content[0].text).toBe("Editing file failed: No file contents for /tmp/file.txt.");
|
||||
expect(agent.writeTextFile).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
617
src/tests/settings.test.ts
Normal file
617
src/tests/settings.test.ts
Normal file
@@ -0,0 +1,617 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { SettingsManager } from "../settings.js";
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import * as os from "node:os";
|
||||
|
||||
describe("SettingsManager", () => {
|
||||
let tempDir: string;
|
||||
let settingsManager: SettingsManager;
|
||||
|
||||
beforeEach(async () => {
|
||||
tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "settings-test-"));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
settingsManager?.dispose();
|
||||
await fs.promises.rm(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe("permission checking", () => {
|
||||
it("should return 'ask' when no settings exist", async () => {
|
||||
settingsManager = new SettingsManager(tempDir);
|
||||
await settingsManager.initialize();
|
||||
|
||||
const result = settingsManager.checkPermission("mcp__acp__Read", {
|
||||
file_path: "/some/file.txt",
|
||||
});
|
||||
expect(result.decision).toBe("ask");
|
||||
});
|
||||
|
||||
it("should return 'ask' for non-ACP tools (permission checks only apply to mcp__acp__* tools)", async () => {
|
||||
const claudeDir = path.join(tempDir, ".claude");
|
||||
await fs.promises.mkdir(claudeDir, { recursive: true });
|
||||
await fs.promises.writeFile(
|
||||
path.join(claudeDir, "settings.json"),
|
||||
JSON.stringify({
|
||||
permissions: {
|
||||
deny: ["Read"],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
settingsManager = new SettingsManager(tempDir);
|
||||
await settingsManager.initialize();
|
||||
|
||||
// Non-ACP tools should always return 'ask' regardless of rules
|
||||
const result = settingsManager.checkPermission("Read", { file_path: "/some/file.txt" });
|
||||
expect(result.decision).toBe("ask");
|
||||
});
|
||||
|
||||
it("should allow tool use when matching allow rule exists", async () => {
|
||||
const claudeDir = path.join(tempDir, ".claude");
|
||||
await fs.promises.mkdir(claudeDir, { recursive: true });
|
||||
await fs.promises.writeFile(
|
||||
path.join(claudeDir, "settings.json"),
|
||||
JSON.stringify({
|
||||
permissions: {
|
||||
allow: ["Read"],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
settingsManager = new SettingsManager(tempDir);
|
||||
await settingsManager.initialize();
|
||||
|
||||
const result = settingsManager.checkPermission("mcp__acp__Read", {
|
||||
file_path: "/some/file.txt",
|
||||
});
|
||||
expect(result.decision).toBe("allow");
|
||||
expect(result.rule).toBe("Read");
|
||||
});
|
||||
|
||||
it("should deny tool use when matching deny rule exists", async () => {
|
||||
const claudeDir = path.join(tempDir, ".claude");
|
||||
await fs.promises.mkdir(claudeDir, { recursive: true });
|
||||
await fs.promises.writeFile(
|
||||
path.join(claudeDir, "settings.json"),
|
||||
JSON.stringify({
|
||||
permissions: {
|
||||
deny: ["Read(./.env)"],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
settingsManager = new SettingsManager(tempDir);
|
||||
await settingsManager.initialize();
|
||||
|
||||
const result = settingsManager.checkPermission("mcp__acp__Read", {
|
||||
file_path: path.join(tempDir, ".env"),
|
||||
});
|
||||
expect(result.decision).toBe("deny");
|
||||
expect(result.rule).toBe("Read(./.env)");
|
||||
});
|
||||
|
||||
it("should prioritize deny rules over allow rules", async () => {
|
||||
const claudeDir = path.join(tempDir, ".claude");
|
||||
await fs.promises.mkdir(claudeDir, { recursive: true });
|
||||
await fs.promises.writeFile(
|
||||
path.join(claudeDir, "settings.json"),
|
||||
JSON.stringify({
|
||||
permissions: {
|
||||
allow: ["Read"],
|
||||
deny: ["Read(./.env)"],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
settingsManager = new SettingsManager(tempDir);
|
||||
await settingsManager.initialize();
|
||||
|
||||
// .env should be denied
|
||||
const envResult = settingsManager.checkPermission("mcp__acp__Read", {
|
||||
file_path: path.join(tempDir, ".env"),
|
||||
});
|
||||
expect(envResult.decision).toBe("deny");
|
||||
|
||||
// other files should be allowed
|
||||
const otherResult = settingsManager.checkPermission("mcp__acp__Read", {
|
||||
file_path: path.join(tempDir, "other.txt"),
|
||||
});
|
||||
expect(otherResult.decision).toBe("allow");
|
||||
});
|
||||
|
||||
it("should handle ACP-prefixed tool names", async () => {
|
||||
const claudeDir = path.join(tempDir, ".claude");
|
||||
await fs.promises.mkdir(claudeDir, { recursive: true });
|
||||
await fs.promises.writeFile(
|
||||
path.join(claudeDir, "settings.json"),
|
||||
JSON.stringify({
|
||||
permissions: {
|
||||
allow: ["Read"],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
settingsManager = new SettingsManager(tempDir);
|
||||
await settingsManager.initialize();
|
||||
|
||||
const result = settingsManager.checkPermission("mcp__acp__Read", {
|
||||
file_path: "/some/file.txt",
|
||||
});
|
||||
expect(result.decision).toBe("allow");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Bash permission rules", () => {
|
||||
it("should match exact Bash commands without :* wildcard", async () => {
|
||||
const claudeDir = path.join(tempDir, ".claude");
|
||||
await fs.promises.mkdir(claudeDir, { recursive: true });
|
||||
await fs.promises.writeFile(
|
||||
path.join(claudeDir, "settings.json"),
|
||||
JSON.stringify({
|
||||
permissions: {
|
||||
// Per docs: Bash(npm run build) matches the EXACT command "npm run build"
|
||||
allow: ["Bash(npm run lint)"],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
settingsManager = new SettingsManager(tempDir);
|
||||
await settingsManager.initialize();
|
||||
|
||||
// Exact match should be allowed
|
||||
const exactResult = settingsManager.checkPermission("mcp__acp__Bash", {
|
||||
command: "npm run lint",
|
||||
});
|
||||
expect(exactResult.decision).toBe("allow");
|
||||
|
||||
// Command with extra arguments should NOT match (exact match only)
|
||||
const withArgsResult = settingsManager.checkPermission("mcp__acp__Bash", {
|
||||
command: "npm run lint --fix",
|
||||
});
|
||||
expect(withArgsResult.decision).toBe("ask");
|
||||
|
||||
// Similar command should NOT match (exact match only)
|
||||
const similarResult = settingsManager.checkPermission("mcp__acp__Bash", {
|
||||
command: "npm run linting",
|
||||
});
|
||||
expect(similarResult.decision).toBe("ask");
|
||||
|
||||
// Different command should not match
|
||||
const differentResult = settingsManager.checkPermission("mcp__acp__Bash", {
|
||||
command: "npm run test",
|
||||
});
|
||||
expect(differentResult.decision).toBe("ask");
|
||||
});
|
||||
|
||||
it("should match Bash commands with :* wildcard suffix (prefix matching)", async () => {
|
||||
const claudeDir = path.join(tempDir, ".claude");
|
||||
await fs.promises.mkdir(claudeDir, { recursive: true });
|
||||
await fs.promises.writeFile(
|
||||
path.join(claudeDir, "settings.json"),
|
||||
JSON.stringify({
|
||||
permissions: {
|
||||
// The :* suffix is a convention to make prefix matching explicit
|
||||
allow: ["Bash(npm run:*)"],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
settingsManager = new SettingsManager(tempDir);
|
||||
await settingsManager.initialize();
|
||||
|
||||
// Any command starting with "npm run" should match (prefix matching with :*)
|
||||
const lintResult = settingsManager.checkPermission("mcp__acp__Bash", {
|
||||
command: "npm run lint",
|
||||
});
|
||||
expect(lintResult.decision).toBe("allow");
|
||||
|
||||
const testResult = settingsManager.checkPermission("mcp__acp__Bash", {
|
||||
command: "npm run test",
|
||||
});
|
||||
expect(testResult.decision).toBe("allow");
|
||||
|
||||
// Commands with additional args also match
|
||||
const withArgsResult = settingsManager.checkPermission("mcp__acp__Bash", {
|
||||
command: "npm run test --watch",
|
||||
});
|
||||
expect(withArgsResult.decision).toBe("allow");
|
||||
|
||||
// Non-matching command
|
||||
const installResult = settingsManager.checkPermission("mcp__acp__Bash", {
|
||||
command: "npm install",
|
||||
});
|
||||
expect(installResult.decision).toBe("ask");
|
||||
});
|
||||
|
||||
it("should not allow shell operators to bypass prefix matching", async () => {
|
||||
const claudeDir = path.join(tempDir, ".claude");
|
||||
await fs.promises.mkdir(claudeDir, { recursive: true });
|
||||
await fs.promises.writeFile(
|
||||
path.join(claudeDir, "settings.json"),
|
||||
JSON.stringify({
|
||||
permissions: {
|
||||
allow: ["Bash(safe-cmd:*)"],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
settingsManager = new SettingsManager(tempDir);
|
||||
await settingsManager.initialize();
|
||||
|
||||
// Normal prefix match should work
|
||||
const normalResult = settingsManager.checkPermission("mcp__acp__Bash", {
|
||||
command: "safe-cmd --flag",
|
||||
});
|
||||
expect(normalResult.decision).toBe("allow");
|
||||
|
||||
// Shell operators should NOT be allowed (per docs: Claude Code is aware of shell operators)
|
||||
const andResult = settingsManager.checkPermission("mcp__acp__Bash", {
|
||||
command: "safe-cmd && malicious-cmd",
|
||||
});
|
||||
expect(andResult.decision).toBe("ask");
|
||||
|
||||
const orResult = settingsManager.checkPermission("mcp__acp__Bash", {
|
||||
command: "safe-cmd || malicious-cmd",
|
||||
});
|
||||
expect(orResult.decision).toBe("ask");
|
||||
|
||||
const semicolonResult = settingsManager.checkPermission("mcp__acp__Bash", {
|
||||
command: "safe-cmd; malicious-cmd",
|
||||
});
|
||||
expect(semicolonResult.decision).toBe("ask");
|
||||
|
||||
const pipeResult = settingsManager.checkPermission("mcp__acp__Bash", {
|
||||
command: "safe-cmd | malicious-cmd",
|
||||
});
|
||||
expect(pipeResult.decision).toBe("ask");
|
||||
|
||||
const subshellResult = settingsManager.checkPermission("mcp__acp__Bash", {
|
||||
command: "safe-cmd $(malicious-cmd)",
|
||||
});
|
||||
expect(subshellResult.decision).toBe("ask");
|
||||
|
||||
const backtickResult = settingsManager.checkPermission("mcp__acp__Bash", {
|
||||
command: "safe-cmd `malicious-cmd`",
|
||||
});
|
||||
expect(backtickResult.decision).toBe("ask");
|
||||
|
||||
const newlineResult = settingsManager.checkPermission("mcp__acp__Bash", {
|
||||
command: "safe-cmd\nmalicious-cmd",
|
||||
});
|
||||
expect(newlineResult.decision).toBe("ask");
|
||||
});
|
||||
|
||||
it("should deny dangerous Bash commands", async () => {
|
||||
const claudeDir = path.join(tempDir, ".claude");
|
||||
await fs.promises.mkdir(claudeDir, { recursive: true });
|
||||
await fs.promises.writeFile(
|
||||
path.join(claudeDir, "settings.json"),
|
||||
JSON.stringify({
|
||||
permissions: {
|
||||
deny: ["Bash(curl:*)", "Bash(wget:*)"],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
settingsManager = new SettingsManager(tempDir);
|
||||
await settingsManager.initialize();
|
||||
|
||||
const curlResult = settingsManager.checkPermission("mcp__acp__Bash", {
|
||||
command: "curl https://example.com",
|
||||
});
|
||||
expect(curlResult.decision).toBe("deny");
|
||||
|
||||
const wgetResult = settingsManager.checkPermission("mcp__acp__Bash", {
|
||||
command: "wget https://example.com",
|
||||
});
|
||||
expect(wgetResult.decision).toBe("deny");
|
||||
|
||||
const lsResult = settingsManager.checkPermission("mcp__acp__Bash", { command: "ls -la" });
|
||||
expect(lsResult.decision).toBe("ask");
|
||||
});
|
||||
});
|
||||
|
||||
describe("file path glob matching", () => {
|
||||
it("should match exact file paths", async () => {
|
||||
const claudeDir = path.join(tempDir, ".claude");
|
||||
await fs.promises.mkdir(claudeDir, { recursive: true });
|
||||
await fs.promises.writeFile(
|
||||
path.join(claudeDir, "settings.json"),
|
||||
JSON.stringify({
|
||||
permissions: {
|
||||
deny: ["Read(./.env)"],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
settingsManager = new SettingsManager(tempDir);
|
||||
await settingsManager.initialize();
|
||||
|
||||
const envResult = settingsManager.checkPermission("mcp__acp__Read", {
|
||||
file_path: path.join(tempDir, ".env"),
|
||||
});
|
||||
expect(envResult.decision).toBe("deny");
|
||||
|
||||
const otherResult = settingsManager.checkPermission("mcp__acp__Read", {
|
||||
file_path: path.join(tempDir, ".envrc"),
|
||||
});
|
||||
expect(otherResult.decision).toBe("ask");
|
||||
});
|
||||
|
||||
it("should match glob patterns with single wildcard", async () => {
|
||||
const claudeDir = path.join(tempDir, ".claude");
|
||||
await fs.promises.mkdir(claudeDir, { recursive: true });
|
||||
await fs.promises.writeFile(
|
||||
path.join(claudeDir, "settings.json"),
|
||||
JSON.stringify({
|
||||
permissions: {
|
||||
deny: ["Read(./.env.*)"],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
settingsManager = new SettingsManager(tempDir);
|
||||
await settingsManager.initialize();
|
||||
|
||||
const envLocalResult = settingsManager.checkPermission("mcp__acp__Read", {
|
||||
file_path: path.join(tempDir, ".env.local"),
|
||||
});
|
||||
expect(envLocalResult.decision).toBe("deny");
|
||||
|
||||
const envProdResult = settingsManager.checkPermission("mcp__acp__Read", {
|
||||
file_path: path.join(tempDir, ".env.production"),
|
||||
});
|
||||
expect(envProdResult.decision).toBe("deny");
|
||||
|
||||
// Plain .env should not match .env.*
|
||||
const plainEnvResult = settingsManager.checkPermission("mcp__acp__Read", {
|
||||
file_path: path.join(tempDir, ".env"),
|
||||
});
|
||||
expect(plainEnvResult.decision).toBe("ask");
|
||||
});
|
||||
|
||||
it("should match glob patterns with double wildcard (recursive)", async () => {
|
||||
const claudeDir = path.join(tempDir, ".claude");
|
||||
await fs.promises.mkdir(claudeDir, { recursive: true });
|
||||
await fs.promises.writeFile(
|
||||
path.join(claudeDir, "settings.json"),
|
||||
JSON.stringify({
|
||||
permissions: {
|
||||
deny: ["Read(./secrets/**)"],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
settingsManager = new SettingsManager(tempDir);
|
||||
await settingsManager.initialize();
|
||||
|
||||
const secretResult = settingsManager.checkPermission("mcp__acp__Read", {
|
||||
file_path: path.join(tempDir, "secrets", "api-key.txt"),
|
||||
});
|
||||
expect(secretResult.decision).toBe("deny");
|
||||
|
||||
const nestedSecretResult = settingsManager.checkPermission("mcp__acp__Read", {
|
||||
file_path: path.join(tempDir, "secrets", "deep", "nested", "key.txt"),
|
||||
});
|
||||
expect(nestedSecretResult.decision).toBe("deny");
|
||||
|
||||
const otherResult = settingsManager.checkPermission("mcp__acp__Read", {
|
||||
file_path: path.join(tempDir, "public", "file.txt"),
|
||||
});
|
||||
expect(otherResult.decision).toBe("ask");
|
||||
});
|
||||
|
||||
it("should handle home directory expansion", async () => {
|
||||
const claudeDir = path.join(tempDir, ".claude");
|
||||
await fs.promises.mkdir(claudeDir, { recursive: true });
|
||||
await fs.promises.writeFile(
|
||||
path.join(claudeDir, "settings.json"),
|
||||
JSON.stringify({
|
||||
permissions: {
|
||||
allow: ["Read(~/.zshrc)"],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
settingsManager = new SettingsManager(tempDir);
|
||||
await settingsManager.initialize();
|
||||
|
||||
const zshrcResult = settingsManager.checkPermission("mcp__acp__Read", {
|
||||
file_path: path.join(os.homedir(), ".zshrc"),
|
||||
});
|
||||
expect(zshrcResult.decision).toBe("allow");
|
||||
});
|
||||
});
|
||||
|
||||
describe("settings merging", () => {
|
||||
it("should merge project and local settings", async () => {
|
||||
const claudeDir = path.join(tempDir, ".claude");
|
||||
await fs.promises.mkdir(claudeDir, { recursive: true });
|
||||
|
||||
// Project settings
|
||||
await fs.promises.writeFile(
|
||||
path.join(claudeDir, "settings.json"),
|
||||
JSON.stringify({
|
||||
permissions: {
|
||||
allow: ["Read"],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// Local settings
|
||||
await fs.promises.writeFile(
|
||||
path.join(claudeDir, "settings.local.json"),
|
||||
JSON.stringify({
|
||||
permissions: {
|
||||
deny: ["Read(./.env)"],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
settingsManager = new SettingsManager(tempDir);
|
||||
await settingsManager.initialize();
|
||||
|
||||
// Read should be allowed in general
|
||||
const readResult = settingsManager.checkPermission("mcp__acp__Read", {
|
||||
file_path: path.join(tempDir, "file.txt"),
|
||||
});
|
||||
expect(readResult.decision).toBe("allow");
|
||||
|
||||
// But .env should be denied (local settings take precedence)
|
||||
const envResult = settingsManager.checkPermission("mcp__acp__Read", {
|
||||
file_path: path.join(tempDir, ".env"),
|
||||
});
|
||||
expect(envResult.decision).toBe("deny");
|
||||
});
|
||||
});
|
||||
|
||||
describe("ask rules", () => {
|
||||
it("should return 'ask' for matching ask rules", async () => {
|
||||
const claudeDir = path.join(tempDir, ".claude");
|
||||
await fs.promises.mkdir(claudeDir, { recursive: true });
|
||||
await fs.promises.writeFile(
|
||||
path.join(claudeDir, "settings.json"),
|
||||
JSON.stringify({
|
||||
permissions: {
|
||||
ask: ["Bash(git push:*)"],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
settingsManager = new SettingsManager(tempDir);
|
||||
await settingsManager.initialize();
|
||||
|
||||
const result = settingsManager.checkPermission("mcp__acp__Bash", {
|
||||
command: "git push origin main",
|
||||
});
|
||||
expect(result.decision).toBe("ask");
|
||||
expect(result.rule).toBe("Bash(git push:*)");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Edit and Write tools", () => {
|
||||
it("should handle Edit tool permissions", async () => {
|
||||
const claudeDir = path.join(tempDir, ".claude");
|
||||
await fs.promises.mkdir(claudeDir, { recursive: true });
|
||||
await fs.promises.writeFile(
|
||||
path.join(claudeDir, "settings.json"),
|
||||
JSON.stringify({
|
||||
permissions: {
|
||||
deny: ["Edit(./package-lock.json)"],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
settingsManager = new SettingsManager(tempDir);
|
||||
await settingsManager.initialize();
|
||||
|
||||
const lockFileResult = settingsManager.checkPermission("mcp__acp__Edit", {
|
||||
file_path: path.join(tempDir, "package-lock.json"),
|
||||
});
|
||||
expect(lockFileResult.decision).toBe("deny");
|
||||
|
||||
const otherResult = settingsManager.checkPermission("mcp__acp__Edit", {
|
||||
file_path: path.join(tempDir, "package.json"),
|
||||
});
|
||||
expect(otherResult.decision).toBe("ask");
|
||||
});
|
||||
|
||||
it("should handle mcp__acp__Edit and mcp__acp__Write tool names", async () => {
|
||||
const claudeDir = path.join(tempDir, ".claude");
|
||||
await fs.promises.mkdir(claudeDir, { recursive: true });
|
||||
await fs.promises.writeFile(
|
||||
path.join(claudeDir, "settings.json"),
|
||||
JSON.stringify({
|
||||
permissions: {
|
||||
allow: ["Edit", "Write"],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
settingsManager = new SettingsManager(tempDir);
|
||||
await settingsManager.initialize();
|
||||
|
||||
const editResult = settingsManager.checkPermission("mcp__acp__Edit", {
|
||||
file_path: "/some/file.ts",
|
||||
});
|
||||
expect(editResult.decision).toBe("allow");
|
||||
|
||||
const writeResult = settingsManager.checkPermission("mcp__acp__Write", {
|
||||
file_path: "/some/file.ts",
|
||||
});
|
||||
expect(writeResult.decision).toBe("allow");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getSettings", () => {
|
||||
it("should return merged settings", async () => {
|
||||
const claudeDir = path.join(tempDir, ".claude");
|
||||
await fs.promises.mkdir(claudeDir, { recursive: true });
|
||||
await fs.promises.writeFile(
|
||||
path.join(claudeDir, "settings.json"),
|
||||
JSON.stringify({
|
||||
permissions: {
|
||||
allow: ["Read", "Bash(npm:*)"],
|
||||
deny: ["Read(./.env)"],
|
||||
},
|
||||
env: {
|
||||
FOO: "bar",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
settingsManager = new SettingsManager(tempDir);
|
||||
await settingsManager.initialize();
|
||||
|
||||
const settings = settingsManager.getSettings();
|
||||
expect(settings.permissions?.allow).toContain("Read");
|
||||
expect(settings.permissions?.allow).toContain("Bash(npm:*)");
|
||||
expect(settings.permissions?.deny).toContain("Read(./.env)");
|
||||
expect(settings.env?.FOO).toBe("bar");
|
||||
});
|
||||
});
|
||||
|
||||
describe("setCwd", () => {
|
||||
it("should reload settings when cwd changes", async () => {
|
||||
const claudeDir1 = path.join(tempDir, ".claude");
|
||||
await fs.promises.mkdir(claudeDir1, { recursive: true });
|
||||
await fs.promises.writeFile(
|
||||
path.join(claudeDir1, "settings.json"),
|
||||
JSON.stringify({
|
||||
permissions: {
|
||||
allow: ["Read"],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
settingsManager = new SettingsManager(tempDir);
|
||||
await settingsManager.initialize();
|
||||
|
||||
let result = settingsManager.checkPermission("mcp__acp__Read", { file_path: "/file.txt" });
|
||||
expect(result.decision).toBe("allow");
|
||||
|
||||
// Create a new temp directory with different settings
|
||||
const tempDir2 = await fs.promises.mkdtemp(path.join(os.tmpdir(), "settings-test-2-"));
|
||||
const claudeDir2 = path.join(tempDir2, ".claude");
|
||||
await fs.promises.mkdir(claudeDir2, { recursive: true });
|
||||
await fs.promises.writeFile(
|
||||
path.join(claudeDir2, "settings.json"),
|
||||
JSON.stringify({
|
||||
permissions: {
|
||||
deny: ["Read"],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await settingsManager.setCwd(tempDir2);
|
||||
|
||||
result = settingsManager.checkPermission("mcp__acp__Read", { file_path: "/file.txt" });
|
||||
expect(result.decision).toBe("deny");
|
||||
|
||||
// Cleanup second temp dir
|
||||
await fs.promises.rm(tempDir2, { recursive: true, force: true });
|
||||
});
|
||||
});
|
||||
});
|
||||
96
src/tools.ts
96
src/tools.ts
@@ -1,6 +1,28 @@
|
||||
import { PlanEntry, ToolCallContent, ToolCallLocation, ToolKind } from "@agentclientprotocol/sdk";
|
||||
import { replaceAndCalculateLocation, SYSTEM_REMINDER, toolNames } from "./mcp-server.js";
|
||||
import { replaceAndCalculateLocation, SYSTEM_REMINDER } from "./mcp-server.js";
|
||||
import { ToolResultBlockParam, WebSearchToolResultBlockParam } from "@anthropic-ai/sdk/resources";
|
||||
|
||||
const acpUnqualifiedToolNames = {
|
||||
read: "Read",
|
||||
edit: "Edit",
|
||||
write: "Write",
|
||||
bash: "Bash",
|
||||
killShell: "KillShell",
|
||||
bashOutput: "BashOutput",
|
||||
};
|
||||
|
||||
export const ACP_TOOL_NAME_PREFIX = "mcp__acp__";
|
||||
export const acpToolNames = {
|
||||
read: ACP_TOOL_NAME_PREFIX + acpUnqualifiedToolNames.read,
|
||||
edit: ACP_TOOL_NAME_PREFIX + acpUnqualifiedToolNames.edit,
|
||||
write: ACP_TOOL_NAME_PREFIX + acpUnqualifiedToolNames.write,
|
||||
bash: ACP_TOOL_NAME_PREFIX + acpUnqualifiedToolNames.bash,
|
||||
killShell: ACP_TOOL_NAME_PREFIX + acpUnqualifiedToolNames.killShell,
|
||||
bashOutput: ACP_TOOL_NAME_PREFIX + acpUnqualifiedToolNames.bashOutput,
|
||||
};
|
||||
|
||||
export const EDIT_TOOL_NAMES = [acpToolNames.edit, acpToolNames.write];
|
||||
|
||||
import {
|
||||
BetaBashCodeExecutionToolResultBlockParam,
|
||||
BetaCodeExecutionToolResultBlockParam,
|
||||
@@ -12,6 +34,7 @@ import {
|
||||
} from "@anthropic-ai/sdk/resources/beta.mjs";
|
||||
import { HookCallback } from "@anthropic-ai/claude-agent-sdk";
|
||||
import { Logger } from "./acp-agent.js";
|
||||
import { SettingsManager } from "./settings.js";
|
||||
|
||||
interface ToolInfo {
|
||||
title: string;
|
||||
@@ -75,7 +98,7 @@ export function toolInfoFromToolUse(
|
||||
};
|
||||
|
||||
case "Bash":
|
||||
case toolNames.bash:
|
||||
case acpToolNames.bash:
|
||||
return {
|
||||
title: input?.command ? "`" + input.command.replaceAll("`", "\\`") + "`" : "Terminal",
|
||||
kind: "execute",
|
||||
@@ -91,7 +114,7 @@ export function toolInfoFromToolUse(
|
||||
};
|
||||
|
||||
case "BashOutput":
|
||||
case toolNames.bashOutput:
|
||||
case acpToolNames.bashOutput:
|
||||
return {
|
||||
title: "Tail Logs",
|
||||
kind: "execute",
|
||||
@@ -99,14 +122,14 @@ export function toolInfoFromToolUse(
|
||||
};
|
||||
|
||||
case "KillShell":
|
||||
case toolNames.killShell:
|
||||
case acpToolNames.killShell:
|
||||
return {
|
||||
title: "Kill Process",
|
||||
kind: "execute",
|
||||
content: [],
|
||||
};
|
||||
|
||||
case toolNames.read: {
|
||||
case acpToolNames.read: {
|
||||
let limit = "";
|
||||
if (input.limit) {
|
||||
limit =
|
||||
@@ -152,7 +175,7 @@ export function toolInfoFromToolUse(
|
||||
locations: [],
|
||||
};
|
||||
|
||||
case toolNames.edit:
|
||||
case acpToolNames.edit:
|
||||
case "Edit": {
|
||||
const path = input?.file_path ?? input?.file_path;
|
||||
let oldText = input.old_string ?? null;
|
||||
@@ -198,7 +221,7 @@ export function toolInfoFromToolUse(
|
||||
};
|
||||
}
|
||||
|
||||
case toolNames.write: {
|
||||
case acpToolNames.write: {
|
||||
let content: ToolCallContent[] = [];
|
||||
if (input && input.file_path) {
|
||||
content = [
|
||||
@@ -420,7 +443,7 @@ export function toolUpdateFromToolResult(
|
||||
): ToolUpdate {
|
||||
switch (toolUse?.name) {
|
||||
case "Read":
|
||||
case toolNames.read:
|
||||
case acpToolNames.read:
|
||||
if (Array.isArray(toolResult.content) && toolResult.content.length > 0) {
|
||||
return {
|
||||
content: toolResult.content.map((content: any) => ({
|
||||
@@ -449,11 +472,11 @@ export function toolUpdateFromToolResult(
|
||||
}
|
||||
return {};
|
||||
|
||||
case toolNames.bash:
|
||||
case acpToolNames.bash:
|
||||
case "edit":
|
||||
case "Edit":
|
||||
case toolNames.edit:
|
||||
case toolNames.write:
|
||||
case acpToolNames.edit:
|
||||
case acpToolNames.write:
|
||||
case "Write": {
|
||||
if (
|
||||
"is_error" in toolResult &&
|
||||
@@ -596,3 +619,54 @@ export const createPostToolUseHook =
|
||||
}
|
||||
return { continue: true };
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a PreToolUse hook that checks permissions using the SettingsManager.
|
||||
* This runs before the SDK's built-in permission rules, allowing us to enforce
|
||||
* our own permission settings for ACP-prefixed tools.
|
||||
*/
|
||||
export const createPreToolUseHook =
|
||||
(settingsManager: SettingsManager, logger: Logger = console): HookCallback =>
|
||||
async (input: any, _toolUseID: string | undefined) => {
|
||||
if (input.hook_event_name !== "PreToolUse") {
|
||||
return { continue: true };
|
||||
}
|
||||
|
||||
const toolName = input.tool_name;
|
||||
const toolInput = input.tool_input;
|
||||
|
||||
const permissionCheck = settingsManager.checkPermission(toolName, toolInput);
|
||||
|
||||
if (permissionCheck.decision !== "ask") {
|
||||
logger.log(
|
||||
`[PreToolUseHook] Tool: ${toolName}, Decision: ${permissionCheck.decision}, Rule: ${permissionCheck.rule}`,
|
||||
);
|
||||
}
|
||||
|
||||
switch (permissionCheck.decision) {
|
||||
case "allow":
|
||||
return {
|
||||
continue: true,
|
||||
hookSpecificOutput: {
|
||||
hookEventName: "PreToolUse" as const,
|
||||
permissionDecision: "allow" as const,
|
||||
permissionDecisionReason: `Allowed by settings rule: ${permissionCheck.rule}`,
|
||||
},
|
||||
};
|
||||
|
||||
case "deny":
|
||||
return {
|
||||
continue: true,
|
||||
hookSpecificOutput: {
|
||||
hookEventName: "PreToolUse" as const,
|
||||
permissionDecision: "deny" as const,
|
||||
permissionDecisionReason: `Denied by settings rule: ${permissionCheck.rule}`,
|
||||
},
|
||||
};
|
||||
|
||||
case "ask":
|
||||
default:
|
||||
// Let the normal permission flow continue
|
||||
return { continue: true };
|
||||
}
|
||||
};
|
||||
|
||||
32
src/utils.ts
32
src/utils.ts
@@ -3,8 +3,8 @@
|
||||
import { Readable, Writable } from "node:stream";
|
||||
import { WritableStream, ReadableStream } from "node:stream/web";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { platform } from "node:os";
|
||||
import { Logger } from "./acp-agent.js";
|
||||
import { ClaudeCodeSettings, getManagedSettingsPath } from "./settings.js";
|
||||
|
||||
// Useful for bridging push-based and async-iterator-based code.
|
||||
export class Pushable<T> implements AsyncIterable<T> {
|
||||
@@ -90,39 +90,15 @@ export function sleep(time: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, time));
|
||||
}
|
||||
|
||||
interface ManagedSettings {
|
||||
permissions?: {
|
||||
allow?: string[];
|
||||
deny?: string[];
|
||||
};
|
||||
env?: Record<string, string>;
|
||||
}
|
||||
|
||||
// Following the rules in https://docs.anthropic.com/en/docs/claude-code/settings#settings-files
|
||||
// This can be removed once the SDK supports it natively.
|
||||
function getManagedSettingsPath(): string {
|
||||
const os = platform();
|
||||
switch (os) {
|
||||
case "darwin":
|
||||
return "/Library/Application Support/ClaudeCode/managed-settings.json";
|
||||
case "linux": // including WSL
|
||||
return "/etc/claude-code/managed-settings.json";
|
||||
case "win32":
|
||||
return "C:\\ProgramData\\ClaudeCode\\managed-settings.json";
|
||||
default:
|
||||
return "/etc/claude-code/managed-settings.json";
|
||||
}
|
||||
}
|
||||
|
||||
export function loadManagedSettings(): ManagedSettings | null {
|
||||
export function loadManagedSettings(): ClaudeCodeSettings | null {
|
||||
try {
|
||||
return JSON.parse(readFileSync(getManagedSettingsPath(), "utf8")) as ManagedSettings;
|
||||
return JSON.parse(readFileSync(getManagedSettingsPath(), "utf8")) as ClaudeCodeSettings;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function applyEnvironmentSettings(settings: ManagedSettings): void {
|
||||
export function applyEnvironmentSettings(settings: ClaudeCodeSettings): void {
|
||||
if (settings.env) {
|
||||
for (const [key, value] of Object.entries(settings.env)) {
|
||||
process.env[key] = value;
|
||||
|
||||
Reference in New Issue
Block a user