Compare commits

...

4 Commits

Author SHA1 Message Date
Ben Brandt
8420b50523 Update to claude agent sdk 0.2.34 (#293)
Allows us to pass in session id via the SDK as well as use one promise
to get commands + models
2026-02-06 15:07:46 +01:00
Maximilian Krause
24339faf2b feat: implement loadSession by replaying JSONL session files (#285)
Based on the groundwork in #254, this adds support for `loadSession` to
replay messages and tool calls from resumed/loaded sessions through
Claude Code's persisted JSONL files.

- Advertise the `loadSession` capability
- Stream JSONL entries from
`~/.claude/projects/<cwd-encoded>/<sessionId>.jsonl`
- Correctly parse user/assistant messages and tool calls/results into
ACP session updates
- Add unit tests for the new functionality
2026-02-06 14:44:18 +01:00
Elle Najt
8360a21358 test: add tests for cross-tool rule isolation (#291)
## Summary
- Adds tests verifying that permission rules only apply to their
intended tool types
- Tests confirm the fix from #282 (commit d471ccf) works correctly

## Tests added
- Bash rules should not match Edit/Write/Read operations
- Edit rules should not match Bash operations
- Read rules should not match Bash operations

These tests catch the bug where `Bash(./**:*)` would incorrectly allow
file operations on matching paths.

## Verification
Confirmed tests fail on the commit before the fix (6b68ddf) and pass
after.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 00:09:44 +01:00
Ben Brandt
a9985d49e9 Update anthropic types (#292) 2026-02-05 23:08:09 +00:00
9 changed files with 705 additions and 197 deletions

View File

@@ -1,5 +1,10 @@
# Changelog
## 0.16.0
- Update to @anthropic-ai/claude-agent-sdk@0.2.34
- Experimental support for session loading
## 0.15.0
- Update to @anthropic-ai/claude-agent-sdk@0.2.32 (adds support for Opus 4.6 and 1M context Opus)

282
package-lock.json generated
View File

@@ -1,26 +1,26 @@
{
"name": "@zed-industries/claude-code-acp",
"version": "0.14.0",
"version": "0.16.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@zed-industries/claude-code-acp",
"version": "0.14.0",
"version": "0.16.0",
"license": "Apache-2.0",
"dependencies": {
"@agentclientprotocol/sdk": "0.13.1",
"@anthropic-ai/claude-agent-sdk": "^0.2.32",
"@agentclientprotocol/sdk": "0.14.1",
"@anthropic-ai/claude-agent-sdk": "0.2.34",
"@modelcontextprotocol/sdk": "1.26.0",
"diff": "8.0.3",
"minimatch": "10.1.1"
"minimatch": "10.1.2"
},
"bin": {
"claude-code-acp": "dist/index.js"
},
"devDependencies": {
"@anthropic-ai/sdk": "0.72.1",
"@types/node": "25.2.0",
"@anthropic-ai/sdk": "0.73.0",
"@types/node": "25.2.1",
"@typescript-eslint/eslint-plugin": "8.54.0",
"@typescript-eslint/parser": "8.54.0",
"eslint": "9.39.2",
@@ -33,18 +33,18 @@
}
},
"node_modules/@agentclientprotocol/sdk": {
"version": "0.13.1",
"resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-0.13.1.tgz",
"integrity": "sha512-6byvu+F/xc96GBkdAx4hq6/tB3vT63DSBO4i3gYCz8nuyZMerVFna2Gkhm8EHNpZX0J9DjUxzZCW+rnHXUg0FA==",
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-0.14.1.tgz",
"integrity": "sha512-b6r3PS3Nly+Wyw9U+0nOr47bV8tfS476EgyEMhoKvJCZLbgqoDFN7DJwkxL88RR0aiOqOYV1ZnESHqb+RmdH8w==",
"license": "Apache-2.0",
"peerDependencies": {
"zod": "^3.25.0 || ^4.0.0"
}
},
"node_modules/@anthropic-ai/claude-agent-sdk": {
"version": "0.2.32",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.2.32.tgz",
"integrity": "sha512-8AtsSx/M9jxd0ihS08eqa7VireTEuwQy0i1+6ZJX93LECT6Svlf47dPJiAm7JB+BhVMmwTfQeS6x1akIcCfvbQ==",
"version": "0.2.34",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.2.34.tgz",
"integrity": "sha512-QLHd3Nt7bGU7/YH71fXFaztM9fNxGGruzTMrTYJkbm5gYJl5ZyU2zGyoE5VpWC0e1QU0yYdNdBVgqSYDcJGufg==",
"license": "SEE LICENSE IN README.md",
"engines": {
"node": ">=18.0.0"
@@ -64,9 +64,9 @@
}
},
"node_modules/@anthropic-ai/sdk": {
"version": "0.72.1",
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.72.1.tgz",
"integrity": "sha512-MiUnue7qN7DvLIoYHgkedN2z05mRf2CutBzjXXY2krzOhG2r/rIfISS2uVkNLikgToB5hYIzw+xp2jdOtRkqYQ==",
"version": "0.73.0",
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.73.0.tgz",
"integrity": "sha512-URURVzhxXGJDGUGFunIOtBlSl7KWvZiAAKY/ttTkZAkXT9bTPqdk2eK0b8qqSxXpikh3QKPnPYpiyX98zf5ebw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -108,9 +108,9 @@
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz",
"integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
"integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==",
"cpu": [
"ppc64"
],
@@ -125,9 +125,9 @@
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz",
"integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
"integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==",
"cpu": [
"arm"
],
@@ -142,9 +142,9 @@
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz",
"integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
"integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==",
"cpu": [
"arm64"
],
@@ -159,9 +159,9 @@
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz",
"integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
"integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==",
"cpu": [
"x64"
],
@@ -176,9 +176,9 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz",
"integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
"integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==",
"cpu": [
"arm64"
],
@@ -193,9 +193,9 @@
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz",
"integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
"integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==",
"cpu": [
"x64"
],
@@ -210,9 +210,9 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz",
"integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
"integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==",
"cpu": [
"arm64"
],
@@ -227,9 +227,9 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz",
"integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
"integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==",
"cpu": [
"x64"
],
@@ -244,9 +244,9 @@
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz",
"integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
"integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==",
"cpu": [
"arm"
],
@@ -261,9 +261,9 @@
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz",
"integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
"integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==",
"cpu": [
"arm64"
],
@@ -278,9 +278,9 @@
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz",
"integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
"integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==",
"cpu": [
"ia32"
],
@@ -295,9 +295,9 @@
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz",
"integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
"integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==",
"cpu": [
"loong64"
],
@@ -312,9 +312,9 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz",
"integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
"integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==",
"cpu": [
"mips64el"
],
@@ -329,9 +329,9 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz",
"integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
"integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==",
"cpu": [
"ppc64"
],
@@ -346,9 +346,9 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz",
"integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
"integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==",
"cpu": [
"riscv64"
],
@@ -363,9 +363,9 @@
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz",
"integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
"integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==",
"cpu": [
"s390x"
],
@@ -380,9 +380,9 @@
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz",
"integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz",
"integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==",
"cpu": [
"x64"
],
@@ -397,9 +397,9 @@
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz",
"integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
"integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==",
"cpu": [
"arm64"
],
@@ -414,9 +414,9 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz",
"integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
"integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==",
"cpu": [
"x64"
],
@@ -431,9 +431,9 @@
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz",
"integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
"integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==",
"cpu": [
"arm64"
],
@@ -448,9 +448,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz",
"integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
"integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==",
"cpu": [
"x64"
],
@@ -465,9 +465,9 @@
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz",
"integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
"integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==",
"cpu": [
"arm64"
],
@@ -482,9 +482,9 @@
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz",
"integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
"integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==",
"cpu": [
"x64"
],
@@ -499,9 +499,9 @@
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz",
"integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
"integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==",
"cpu": [
"arm64"
],
@@ -516,9 +516,9 @@
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz",
"integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
"integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==",
"cpu": [
"ia32"
],
@@ -533,9 +533,9 @@
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz",
"integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz",
"integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==",
"cpu": [
"x64"
],
@@ -1609,11 +1609,12 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "25.2.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.0.tgz",
"integrity": "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w==",
"version": "25.2.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.1.tgz",
"integrity": "sha512-CPrnr8voK8vC6eEtyRzvMpgp3VyVRhgclonE7qYi6P9sXwYb59ucfrnmFBTaP0yUi8Gk4yZg/LlTJULGxvTNsg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
@@ -1653,6 +1654,7 @@
"integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.54.0",
"@typescript-eslint/types": "8.54.0",
@@ -2007,6 +2009,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -2441,9 +2444,9 @@
}
},
"node_modules/esbuild": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz",
"integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
"integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ -2454,32 +2457,32 @@
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.27.2",
"@esbuild/android-arm": "0.27.2",
"@esbuild/android-arm64": "0.27.2",
"@esbuild/android-x64": "0.27.2",
"@esbuild/darwin-arm64": "0.27.2",
"@esbuild/darwin-x64": "0.27.2",
"@esbuild/freebsd-arm64": "0.27.2",
"@esbuild/freebsd-x64": "0.27.2",
"@esbuild/linux-arm": "0.27.2",
"@esbuild/linux-arm64": "0.27.2",
"@esbuild/linux-ia32": "0.27.2",
"@esbuild/linux-loong64": "0.27.2",
"@esbuild/linux-mips64el": "0.27.2",
"@esbuild/linux-ppc64": "0.27.2",
"@esbuild/linux-riscv64": "0.27.2",
"@esbuild/linux-s390x": "0.27.2",
"@esbuild/linux-x64": "0.27.2",
"@esbuild/netbsd-arm64": "0.27.2",
"@esbuild/netbsd-x64": "0.27.2",
"@esbuild/openbsd-arm64": "0.27.2",
"@esbuild/openbsd-x64": "0.27.2",
"@esbuild/openharmony-arm64": "0.27.2",
"@esbuild/sunos-x64": "0.27.2",
"@esbuild/win32-arm64": "0.27.2",
"@esbuild/win32-ia32": "0.27.2",
"@esbuild/win32-x64": "0.27.2"
"@esbuild/aix-ppc64": "0.27.3",
"@esbuild/android-arm": "0.27.3",
"@esbuild/android-arm64": "0.27.3",
"@esbuild/android-x64": "0.27.3",
"@esbuild/darwin-arm64": "0.27.3",
"@esbuild/darwin-x64": "0.27.3",
"@esbuild/freebsd-arm64": "0.27.3",
"@esbuild/freebsd-x64": "0.27.3",
"@esbuild/linux-arm": "0.27.3",
"@esbuild/linux-arm64": "0.27.3",
"@esbuild/linux-ia32": "0.27.3",
"@esbuild/linux-loong64": "0.27.3",
"@esbuild/linux-mips64el": "0.27.3",
"@esbuild/linux-ppc64": "0.27.3",
"@esbuild/linux-riscv64": "0.27.3",
"@esbuild/linux-s390x": "0.27.3",
"@esbuild/linux-x64": "0.27.3",
"@esbuild/netbsd-arm64": "0.27.3",
"@esbuild/netbsd-x64": "0.27.3",
"@esbuild/openbsd-arm64": "0.27.3",
"@esbuild/openbsd-x64": "0.27.3",
"@esbuild/openharmony-arm64": "0.27.3",
"@esbuild/sunos-x64": "0.27.3",
"@esbuild/win32-arm64": "0.27.3",
"@esbuild/win32-ia32": "0.27.3",
"@esbuild/win32-x64": "0.27.3"
}
},
"node_modules/escape-html": {
@@ -2507,6 +2510,7 @@
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -2799,6 +2803,7 @@
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
"license": "MIT",
"peer": true,
"dependencies": {
"accepts": "^2.0.0",
"body-parser": "^2.2.1",
@@ -3133,10 +3138,11 @@
}
},
"node_modules/hono": {
"version": "4.11.7",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.11.7.tgz",
"integrity": "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==",
"version": "4.11.8",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.11.8.tgz",
"integrity": "sha512-eVkB/CYCCei7K2WElZW9yYQFWssG0DhaDhVvr7wy5jJ22K+ck8fWW0EsLpB0sITUTvPnc97+rrbQqIr5iqiy9Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=16.9.0"
}
@@ -3455,12 +3461,12 @@
}
},
"node_modules/minimatch": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz",
"integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==",
"version": "10.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz",
"integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==",
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/brace-expansion": "^5.0.0"
"@isaacs/brace-expansion": "^5.0.1"
},
"engines": {
"node": "20 || >=22"
@@ -3684,6 +3690,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -3904,9 +3911,9 @@
"license": "MIT"
},
"node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"dev": true,
"license": "ISC",
"bin": {
@@ -4286,6 +4293,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -4342,6 +4350,7 @@
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
@@ -4565,6 +4574,7 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}

View File

@@ -3,7 +3,7 @@
"publishConfig": {
"access": "public"
},
"version": "0.15.0",
"version": "0.16.0",
"description": "An ACP-compatible coding agent powered by the Claude Code SDK (TypeScript)",
"main": "dist/lib.js",
"types": "dist/lib.d.ts",
@@ -60,15 +60,15 @@
"author": "Zed Industries",
"license": "Apache-2.0",
"dependencies": {
"@agentclientprotocol/sdk": "0.13.1",
"@anthropic-ai/claude-agent-sdk": "0.2.32",
"@agentclientprotocol/sdk": "0.14.1",
"@anthropic-ai/claude-agent-sdk": "0.2.34",
"@modelcontextprotocol/sdk": "1.26.0",
"diff": "8.0.3",
"minimatch": "10.1.1"
"minimatch": "10.1.2"
},
"devDependencies": {
"@anthropic-ai/sdk": "0.72.1",
"@types/node": "25.2.0",
"@anthropic-ai/sdk": "0.73.0",
"@types/node": "25.2.1",
"@typescript-eslint/eslint-plugin": "8.54.0",
"@typescript-eslint/parser": "8.54.0",
"eslint": "9.39.2",

View File

@@ -9,6 +9,8 @@ import {
ForkSessionResponse,
InitializeRequest,
InitializeResponse,
LoadSessionRequest,
LoadSessionResponse,
ListSessionsRequest,
ListSessionsResponse,
ndJsonStream,
@@ -37,17 +39,26 @@ import { SettingsManager } from "./settings.js";
import {
CanUseTool,
McpServerConfig,
ModelInfo,
Options,
PermissionMode,
Query,
query,
SDKPartialAssistantMessage,
SDKUserMessage,
SlashCommand,
} from "@anthropic-ai/claude-agent-sdk";
import * as fs from "node:fs";
import * as path from "node:path";
import * as readline from "node:readline";
import * as os from "node:os";
import { nodeToWebReadable, nodeToWebWritable, Pushable, unreachable } from "./utils.js";
import {
encodeProjectPath,
nodeToWebReadable,
nodeToWebWritable,
Pushable,
unreachable,
} from "./utils.js";
import { createMcpServer } from "./mcp-server.js";
import { EDIT_TOOL_NAMES, acpToolNames } from "./tools.js";
import {
@@ -88,6 +99,10 @@ function decodeProjectPath(encodedPath: string): string {
return encodedPath.replace(/-/g, "/");
}
function sessionFilePath(cwd: string, sessionId: string): string {
return path.join(CLAUDE_CONFIG_DIR, "projects", encodeProjectPath(cwd), `${sessionId}.jsonl`);
}
const MAX_TITLE_LENGTH = 128;
function sanitizeTitle(text: string): string {
@@ -118,6 +133,17 @@ type Session = {
settingsManager: SettingsManager;
};
type SessionHistoryEntry = {
type?: string;
isSidechain?: boolean;
sessionId?: string;
message?: {
role?: string;
content?: unknown;
model?: string;
};
};
type BackgroundTerminal =
| {
handle: TerminalHandle;
@@ -228,6 +254,7 @@ export class ClaudeAcpAgent implements Agent {
http: true,
sse: true,
},
loadSession: true,
sessionCapabilities: {
fork: {},
list: {},
@@ -286,6 +313,32 @@ export class ClaudeAcpAgent implements Agent {
return response;
}
async loadSession(params: LoadSessionRequest): Promise<LoadSessionResponse> {
try {
await fs.promises.access(sessionFilePath(params.cwd, params.sessionId));
} catch {
throw new Error("Session not found");
}
const response = await this.createSession(
{
cwd: params.cwd,
mcpServers: params.mcpServers ?? [],
_meta: params._meta,
},
{
resume: params.sessionId,
},
);
await this.replaySessionHistory(params.sessionId, params.cwd);
return {
modes: response.modes,
models: response.models,
};
}
/**
* List Claude Code sessions by parsing JSONL files
* Sessions are stored in ~/.claude/projects/<path-encoded>/
@@ -656,6 +709,71 @@ export class ClaudeAcpAgent implements Agent {
}
}
private async replaySessionHistory(sessionId: string, cwd: string): Promise<void> {
const filePath = sessionFilePath(cwd, sessionId);
const toolUseCache: ToolUseCache = {};
const stream = fs.createReadStream(filePath, { encoding: "utf-8" });
const reader = readline.createInterface({ input: stream, crlfDelay: Infinity });
try {
for await (const line of reader) {
const trimmed = line.trim();
if (!trimmed) {
continue;
}
let entry: SessionHistoryEntry;
try {
entry = JSON.parse(trimmed) as SessionHistoryEntry;
} catch {
continue;
}
if (entry.type !== "user" && entry.type !== "assistant") {
continue;
}
if (entry.isSidechain) {
continue;
}
if (entry.sessionId && entry.sessionId !== sessionId) {
continue;
}
const message = entry.message;
if (!message) {
continue;
}
const role =
message.role === "assistant" ? "assistant" : message.role === "user" ? "user" : null;
if (!role) {
continue;
}
const content = message.content;
if (typeof content !== "string" && !Array.isArray(content)) {
continue;
}
for (const notification of toAcpNotifications(
content,
role,
sessionId,
toolUseCache,
this.client,
this.logger,
{ registerHooks: false },
)) {
await this.client.sessionUpdate(notification);
}
}
} finally {
reader.close();
}
}
async readTextFile(params: ReadTextFileRequest): Promise<ReadTextFileResponse> {
const response = await this.client.readTextFile(params);
return response;
@@ -868,11 +986,6 @@ export class ClaudeAcpAgent implements Agent {
// Extract options from _meta if provided
const userProvidedOptions = (params._meta as NewSessionMeta | undefined)?.claudeCode?.options;
const extraArgs = { ...userProvidedOptions?.extraArgs };
if (creationOpts?.resume === undefined || creationOpts?.forkSession) {
// Set our own session id if not resuming an existing session.
extraArgs["session-id"] = sessionId;
}
// Configure thinking tokens from environment variable
const maxThinkingTokens = process.env.MAX_THINKING_TOKENS
@@ -889,7 +1002,6 @@ export class ClaudeAcpAgent implements Agent {
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: ALLOW_BYPASS,
@@ -936,6 +1048,11 @@ export class ClaudeAcpAgent implements Agent {
...creationOpts,
};
if (creationOpts?.resume === undefined || creationOpts?.forkSession) {
// Set our own session id if not resuming an existing session.
options.sessionId = sessionId;
}
const allowedTools = [];
// Disable this for now, not a great way to expose this over ACP at the moment (in progress work so we can revisit)
const disallowedTools = ["AskUserQuestion"];
@@ -1010,8 +1127,9 @@ export class ClaudeAcpAgent implements Agent {
settingsManager,
};
const availableCommands = await getAvailableSlashCommands(q);
const models = await getAvailableModels(q, settingsManager);
const initializationResult = await q.initializationResult();
const models = await getAvailableModels(q, initializationResult.models, settingsManager);
// Needs to happen after we return the session
setTimeout(() => {
@@ -1019,7 +1137,7 @@ export class ClaudeAcpAgent implements Agent {
sessionId,
update: {
sessionUpdate: "available_commands_update",
availableCommands,
availableCommands: getAvailableSlashCommands(initializationResult.commands),
},
});
}, 0);
@@ -1068,9 +1186,9 @@ export class ClaudeAcpAgent implements Agent {
async function getAvailableModels(
query: Query,
models: ModelInfo[],
settingsManager: SettingsManager,
): Promise<SessionModelState> {
const models = await query.supportedModels();
const settings = settingsManager.getSettings();
let currentModel = models[0];
@@ -1101,7 +1219,7 @@ async function getAvailableModels(
};
}
async function getAvailableSlashCommands(query: Query): Promise<AvailableCommand[]> {
function getAvailableSlashCommands(commands: SlashCommand[]): AvailableCommand[] {
const UNSUPPORTED_COMMANDS = [
"cost",
"keybindings-help",
@@ -1111,7 +1229,6 @@ async function getAvailableSlashCommands(query: Query): Promise<AvailableCommand
"release-notes",
"todos",
];
const commands = await query.supportedCommands();
return commands
.map((command) => {
@@ -1242,7 +1359,9 @@ export function toAcpNotifications(
toolUseCache: ToolUseCache,
client: AgentSideConnection,
logger: Logger,
options?: { registerHooks?: boolean },
): SessionNotification[] {
const registerHooks = options?.registerHooks !== false;
if (typeof content === "string") {
return [
{
@@ -1307,32 +1426,33 @@ export function toAcpNotifications(
};
}
} else {
// Register hook callback to receive the structured output from the hook
registerHookCallback(chunk.id, {
onPostToolUseHook: async (toolUseId, toolInput, toolResponse) => {
const toolUse = toolUseCache[toolUseId];
if (toolUse) {
const update: SessionNotification["update"] = {
_meta: {
claudeCode: {
toolResponse,
toolName: toolUse.name,
},
} satisfies ToolUpdateMeta,
toolCallId: toolUseId,
sessionUpdate: "tool_call_update",
};
await client.sessionUpdate({
sessionId,
update,
});
} else {
logger.error(
`[claude-code-acp] Got a tool response for tool use that wasn't tracked: ${toolUseId}`,
);
}
},
});
if (registerHooks) {
registerHookCallback(chunk.id, {
onPostToolUseHook: async (toolUseId, toolInput, toolResponse) => {
const toolUse = toolUseCache[toolUseId];
if (toolUse) {
const update: SessionNotification["update"] = {
_meta: {
claudeCode: {
toolResponse,
toolName: toolUse.name,
},
} satisfies ToolUpdateMeta,
toolCallId: toolUseId,
sessionUpdate: "tool_call_update",
};
await client.sessionUpdate({
sessionId,
update,
});
} else {
logger.error(
`[claude-code-acp] Got a tool response for tool use that wasn't tracked: ${toolUseId}`,
);
}
},
});
}
let rawInput;
try {
@@ -1396,6 +1516,8 @@ export function toAcpNotifications(
case "citations_delta":
case "signature_delta":
case "container_upload":
case "compaction":
case "compaction_delta":
break;
default:

View File

@@ -607,6 +607,8 @@ describe("tool conversions", () => {
output_tokens: 1,
service_tier: "standard",
server_tool_use: null,
inference_geo: null,
iterations: null,
},
context_management: null,
},
@@ -1001,7 +1003,7 @@ describe.skipIf(!process.env.RUN_INTEGRATION_TESTS)("SDK behavior", () => {
prompt: "hi",
options: {
systemPrompt: { type: "preset", preset: "claude_code" },
extraArgs: { "session-id": sessionId },
sessionId,
settingSources: ["user", "project", "local"],
includePartialMessages: true,
},

View File

@@ -4,6 +4,7 @@ import * as path from "node:path";
import * as os from "node:os";
import { AgentSideConnection } from "@agentclientprotocol/sdk";
import type { ClaudeAcpAgent as ClaudeAcpAgentType } from "../acp-agent.js";
import { encodeProjectPath } from "../utils.js";
describe("unstable_listSessions", () => {
let tempDir: string;
@@ -21,18 +22,6 @@ describe("unstable_listSessions", () => {
} as unknown as AgentSideConnection;
}
// Helper to encode a path like Claude does:
// - Unix: "/Users/test" -> "-Users-test"
// - Windows: "C:\Users\test" -> "C-Users-test"
function encodePath(cwd: string): string {
// Handle Windows paths
if (/^[A-Za-z]:\\/.test(cwd)) {
return cwd.replace(/\\/g, "-").replace(":", "");
}
// Unix paths
return cwd.replace(/\//g, "-");
}
// Helper to write a session file
function writeSessionFile(
cwd: string,
@@ -45,7 +34,7 @@ describe("unstable_listSessions", () => {
isAgentFile?: boolean;
} = {},
): void {
const encodedPath = encodePath(cwd);
const encodedPath = encodeProjectPath(cwd);
const projectDir = path.join(tempDir, "projects", encodedPath);
fs.mkdirSync(projectDir, { recursive: true });
@@ -248,7 +237,7 @@ describe("unstable_listSessions", () => {
it("uses filename as sessionId when not in file content", async () => {
const cwd = "/Users/test/project";
const encodedPath = encodePath(cwd);
const encodedPath = encodeProjectPath(cwd);
const projectDir = path.join(tempDir, "projects", encodedPath);
fs.mkdirSync(projectDir, { recursive: true });
@@ -263,7 +252,7 @@ describe("unstable_listSessions", () => {
it("returns null title when no user message exists", async () => {
const cwd = "/Users/test/project";
const encodedPath = encodePath(cwd);
const encodedPath = encodeProjectPath(cwd);
const projectDir = path.join(tempDir, "projects", encodedPath);
fs.mkdirSync(projectDir, { recursive: true });

View File

@@ -0,0 +1,250 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import * as fs from "node:fs";
import * as path from "node:path";
import * as os from "node:os";
import { AgentSideConnection, SessionNotification } from "@agentclientprotocol/sdk";
import type { ClaudeAcpAgent as ClaudeAcpAgentType } from "../acp-agent.js";
import { encodeProjectPath } from "../utils.js";
const { registerHookCallbackSpy } = vi.hoisted(() => ({
registerHookCallbackSpy: vi.fn(),
}));
vi.mock("../tools.js", async () => {
const actual = await vi.importActual<typeof import("../tools.js")>("../tools.js");
return {
...actual,
registerHookCallback: registerHookCallbackSpy,
};
});
describe("loadSession", () => {
let tempDir: string;
let originalClaudeConfigDir: string | undefined;
let agent: ClaudeAcpAgentType;
let ClaudeAcpAgent: typeof ClaudeAcpAgentType;
let sessionUpdates: SessionNotification[];
let createSessionSpy: ReturnType<typeof vi.fn>;
// Helper to create a mock AgentSideConnection
function createMockClient(): AgentSideConnection {
return {
sessionUpdate: async (notification: SessionNotification) => {
sessionUpdates.push(notification);
},
requestPermission: async () => ({ outcome: { outcome: "cancelled" } }),
readTextFile: async () => ({ content: "" }),
writeTextFile: async () => ({}),
} as unknown as AgentSideConnection;
}
function writeSessionFile(
cwd: string,
sessionId: string,
entries: Array<Record<string, unknown> | string>,
): void {
const encodedPath = encodeProjectPath(cwd);
const projectDir = path.join(tempDir, "projects", encodedPath);
fs.mkdirSync(projectDir, { recursive: true });
const filePath = path.join(projectDir, `${sessionId}.jsonl`);
const lines = entries.map((entry) =>
typeof entry === "string" ? entry : JSON.stringify(entry),
);
fs.writeFileSync(filePath, lines.join("\n"));
}
beforeEach(async () => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "claude-load-session-"));
originalClaudeConfigDir = process.env.CLAUDE_CONFIG_DIR;
process.env.CLAUDE_CONFIG_DIR = tempDir;
sessionUpdates = [];
registerHookCallbackSpy.mockClear();
vi.resetModules();
const acpAgent = await import("../acp-agent.js");
ClaudeAcpAgent = acpAgent.ClaudeAcpAgent;
agent = new ClaudeAcpAgent(createMockClient());
createSessionSpy = vi.fn(async () => ({ modes: null, models: null }));
(agent as unknown as { createSession: typeof createSessionSpy }).createSession =
createSessionSpy;
});
afterEach(() => {
if (originalClaudeConfigDir !== undefined) {
process.env.CLAUDE_CONFIG_DIR = originalClaudeConfigDir;
} else {
delete process.env.CLAUDE_CONFIG_DIR;
}
fs.rmSync(tempDir, { recursive: true, force: true });
});
it("throws when the session file does not exist", async () => {
await expect(
agent.loadSession({
cwd: "/Users/test/project",
sessionId: "missing-session",
mcpServers: [],
}),
).rejects.toThrow("Session not found");
expect(createSessionSpy).not.toHaveBeenCalled();
});
it("replays history and returns modes/models from createSession", async () => {
const cwd = "/Users/test/project";
const sessionId = "session-123";
writeSessionFile(cwd, sessionId, [
{ type: "summary", summary: "ignored" },
{
type: "user",
sessionId,
message: { role: "user", content: "Hello" },
},
{
type: "assistant",
sessionId,
message: {
role: "assistant",
content: [{ type: "thinking", thinking: "Thinking" }],
},
},
{
type: "assistant",
sessionId,
message: {
role: "assistant",
content: [{ type: "text", text: "Hi" }],
},
},
{
type: "assistant",
sessionId,
message: {
role: "assistant",
content: [
{
type: "tool_use",
id: "tool-1",
name: "Read",
input: { file_path: "/tmp/example.txt" },
},
],
},
},
{
type: "user",
sessionId,
message: {
role: "user",
content: [{ type: "tool_result", tool_use_id: "tool-1", content: "ok" }],
},
},
]);
const result = await agent.loadSession({ cwd, sessionId, mcpServers: [] });
expect(result).toEqual({ modes: null, models: null });
expect(createSessionSpy).toHaveBeenCalledWith(
expect.objectContaining({ cwd, mcpServers: [] }),
{ resume: sessionId },
);
expect(registerHookCallbackSpy).not.toHaveBeenCalled();
expect(sessionUpdates.map((notification) => notification.update.sessionUpdate)).toEqual([
"user_message_chunk",
"agent_thought_chunk",
"agent_message_chunk",
"tool_call",
"tool_call_update",
]);
expect(sessionUpdates[0]?.update).toMatchObject({
sessionUpdate: "user_message_chunk",
content: { type: "text", text: "Hello" },
});
expect(sessionUpdates[1]?.update).toMatchObject({
sessionUpdate: "agent_thought_chunk",
content: { type: "text", text: "Thinking" },
});
expect(sessionUpdates[2]?.update).toMatchObject({
sessionUpdate: "agent_message_chunk",
content: { type: "text", text: "Hi" },
});
expect(sessionUpdates[3]?.update).toMatchObject({
sessionUpdate: "tool_call",
toolCallId: "tool-1",
status: "pending",
});
expect(sessionUpdates[4]?.update).toMatchObject({
sessionUpdate: "tool_call_update",
toolCallId: "tool-1",
status: "completed",
});
});
it("skips sidechain, mismatched sessionId, invalid, and unsupported entries", async () => {
const cwd = "/Users/test/project";
const sessionId = "session-456";
writeSessionFile(cwd, sessionId, [
"{not valid json}",
{ type: "summary", summary: "ignore" },
{
type: "user",
sessionId,
isSidechain: true,
message: { role: "user", content: "Sidechain" },
},
{
type: "assistant",
sessionId: "other-session",
message: { role: "assistant", content: "Wrong session" },
},
{
type: "assistant",
sessionId,
message: { role: "system", content: "Unsupported role" },
},
{
type: "user",
sessionId,
message: { role: "user", content: 123 },
},
{
type: "user",
sessionId,
message: { role: "user", content: "Keep me" },
},
]);
await agent.loadSession({ cwd, sessionId, mcpServers: [] });
expect(sessionUpdates).toHaveLength(1);
expect(sessionUpdates[0]?.update).toMatchObject({
sessionUpdate: "user_message_chunk",
content: { type: "text", text: "Keep me" },
});
});
it("handles Windows-style paths", async () => {
const cwd = "C:\\Users\\test\\project";
const sessionId = "win-session";
writeSessionFile(cwd, sessionId, [
{
type: "user",
sessionId,
message: { role: "user", content: "Hello" },
},
]);
await agent.loadSession({ cwd, sessionId, mcpServers: [] });
expect(sessionUpdates).toHaveLength(1);
expect(sessionUpdates[0]?.sessionId).toBe(sessionId);
});
});

View File

@@ -649,4 +649,119 @@ describe("SettingsManager", () => {
await fs.promises.rm(tempDir2, { recursive: true, force: true });
});
});
describe("cross-tool rule isolation", () => {
// These tests verify the fix in commit d471ccf:
// Permission rules should only match their intended tool type.
// A Bash rule should not match Edit/Write/Read operations, and vice versa.
it("should not apply Bash rules to Edit tool calls", 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: {
// This Bash rule has an argument that looks like a glob pattern.
// Before the fix, it would incorrectly match Edit operations
// because the argument would be interpreted as a file glob.
allow: ["Bash(./**:*)"],
},
}),
);
settingsManager = new SettingsManager(tempDir);
await settingsManager.initialize();
// Bash rule should NOT allow Edit operations
const result = settingsManager.checkPermission("mcp__acp__Edit", {
file_path: path.join(tempDir, "any-file.txt"),
});
expect(result.decision).toBe("ask");
});
it("should not apply Bash rules to Write tool calls", 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(./**:*)"],
},
}),
);
settingsManager = new SettingsManager(tempDir);
await settingsManager.initialize();
const result = settingsManager.checkPermission("mcp__acp__Write", {
file_path: path.join(tempDir, "any-file.txt"),
});
expect(result.decision).toBe("ask");
});
it("should not apply Bash rules to Read tool calls", 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(./**:*)"],
},
}),
);
settingsManager = new SettingsManager(tempDir);
await settingsManager.initialize();
const result = settingsManager.checkPermission("mcp__acp__Read", {
file_path: path.join(tempDir, "any-file.txt"),
});
expect(result.decision).toBe("ask");
});
it("should not apply Edit rules to Bash tool calls", 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"],
},
}),
);
settingsManager = new SettingsManager(tempDir);
await settingsManager.initialize();
const result = settingsManager.checkPermission("mcp__acp__Bash", {
command: "echo hello",
});
expect(result.decision).toBe("ask");
});
it("should not apply Read rules to Bash tool calls", 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__Bash", {
command: "ls -la",
});
expect(result.decision).toBe("ask");
});
});
});

View File

@@ -169,3 +169,18 @@ export function extractLinesWithByteLimit(
linesRead: linesSeen,
};
}
// Helper to encode a path like Claude does:
// - Unix: "/Users/test" -> "-Users-test"
// - Windows: "C:\Users\test" -> "C-Users-test"
export function encodeProjectPath(cwd: string): string {
const windowsPathMatch = cwd.match(/^([A-Za-z]):[\\/]/);
if (windowsPathMatch) {
const driveLetter = windowsPathMatch[1];
const rest = cwd.slice(2);
return `${driveLetter}${rest.replace(/[\\/]/g, "-")}`;
}
// Unix paths
return cwd.replace(/\//g, "-");
}