Compare commits

...

9 Commits

Author SHA1 Message Date
Ben Brandt
e2a1ac9b9d Update to claude-agent-sdk 0.2.38 (#304) 2026-02-10 21:11:04 +00:00
Ben Brandt
673b0507d7 Make sure updating commands happens at the end of loading (#302) 2026-02-10 13:18:04 +00:00
Ben Brandt
ea00a4014a More permissive load session (#301)
Adds a lookup that checks the expected cwd path first, then
scans all project directories to locate the session JSONL file.
Ultimately matches the behavior we allow for resume
2026-02-10 12:36:02 +00:00
Ben Brandt
ebdd115b76 fix: Incorrect paths for session list (#298)
How Claude Code encodes the paths is lossy, so now we actually check the
cwd from the json files to make sure it is a valid path (otherwise
Claude Code just hangs)
2026-02-09 09:56:02 +00:00
Ben Limmer
7c87fe7a8c fix: merge user-provided disallowedTools instead of overwriting (#295)
## Summary

- User-provided `disallowedTools` from `_meta.claudeCode.options` were
silently overwritten by ACP's internal list
- Now merges both lists, following the same pattern used for `hooks` and
`mcpServers`
- Updated `NewSessionMeta` JSDoc to document the merge behavior

Fixes #294
2026-02-07 09:30:50 +00:00
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
10 changed files with 1177 additions and 308 deletions

View File

@@ -32,3 +32,5 @@ jobs:
- name: Create Release
id: create_release
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090
with:
generate_release_notes: true

View File

@@ -1,5 +1,18 @@
# Changelog
## 0.16.1
- Update to @anthropic-ai/claude-agent-sdk@0.2.38
- Fix incorrect paths for session/list
- Fix available commands after loading a session
- Make loading session more permissive for finding events
- Fix overriding user-provided disallowedTools
## 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)

394
package-lock.json generated
View File

@@ -1,28 +1,28 @@
{
"name": "@zed-industries/claude-code-acp",
"version": "0.14.0",
"version": "0.16.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@zed-industries/claude-code-acp",
"version": "0.14.0",
"version": "0.16.1",
"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.38",
"@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",
"@typescript-eslint/eslint-plugin": "8.54.0",
"@typescript-eslint/parser": "8.54.0",
"@anthropic-ai/sdk": "0.74.0",
"@types/node": "25.2.3",
"@typescript-eslint/eslint-plugin": "8.55.0",
"@typescript-eslint/parser": "8.55.0",
"eslint": "9.39.2",
"eslint-config-prettier": "10.1.8",
"globals": "17.3.0",
@@ -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.38",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.2.38.tgz",
"integrity": "sha512-U1vpf3rlSkw1qUlzC6CBibBA30ouQnla9JnuqYFLQ2zBb1U2NUCXIElrnV7RwWrI5e9ZKCHgR+1uaCwROONo7w==",
"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.74.0",
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.74.0.tgz",
"integrity": "sha512-srbJV7JKsc5cQ6eVuFzjZO7UR3xEPJqPamHFIe29bs38Ij2IripoAhC0S5NslNbaFUYqBKypmmpzMTpqfHEUDw==",
"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,27 +1609,28 @@
"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.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz",
"integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.54.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz",
"integrity": "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==",
"version": "8.55.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.55.0.tgz",
"integrity": "sha512-1y/MVSz0NglV1ijHC8OT49mPJ4qhPYjiK08YUQVbIOyu+5k862LKUHFkpKHWu//zmr7hDR2rhwUm6gnCGNmGBQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.12.2",
"@typescript-eslint/scope-manager": "8.54.0",
"@typescript-eslint/type-utils": "8.54.0",
"@typescript-eslint/utils": "8.54.0",
"@typescript-eslint/visitor-keys": "8.54.0",
"@typescript-eslint/scope-manager": "8.55.0",
"@typescript-eslint/type-utils": "8.55.0",
"@typescript-eslint/utils": "8.55.0",
"@typescript-eslint/visitor-keys": "8.55.0",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.4.0"
@@ -1642,22 +1643,23 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.54.0",
"@typescript-eslint/parser": "^8.55.0",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.54.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.54.0.tgz",
"integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==",
"version": "8.55.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.55.0.tgz",
"integrity": "sha512-4z2nCSBfVIMnbuu8uinj+f0o4qOeggYJLbjpPHka3KH1om7e+H9yLKTYgksTaHcGco+NClhhY2vyO3HsMH1RGw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.54.0",
"@typescript-eslint/types": "8.54.0",
"@typescript-eslint/typescript-estree": "8.54.0",
"@typescript-eslint/visitor-keys": "8.54.0",
"@typescript-eslint/scope-manager": "8.55.0",
"@typescript-eslint/types": "8.55.0",
"@typescript-eslint/typescript-estree": "8.55.0",
"@typescript-eslint/visitor-keys": "8.55.0",
"debug": "^4.4.3"
},
"engines": {
@@ -1673,14 +1675,14 @@
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.54.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.54.0.tgz",
"integrity": "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==",
"version": "8.55.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.55.0.tgz",
"integrity": "sha512-zRcVVPFUYWa3kNnjaZGXSu3xkKV1zXy8M4nO/pElzQhFweb7PPtluDLQtKArEOGmjXoRjnUZ29NjOiF0eCDkcQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.54.0",
"@typescript-eslint/types": "^8.54.0",
"@typescript-eslint/tsconfig-utils": "^8.55.0",
"@typescript-eslint/types": "^8.55.0",
"debug": "^4.4.3"
},
"engines": {
@@ -1695,14 +1697,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.54.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.54.0.tgz",
"integrity": "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==",
"version": "8.55.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.55.0.tgz",
"integrity": "sha512-fVu5Omrd3jeqeQLiB9f1YsuK/iHFOwb04bCtY4BSCLgjNbOD33ZdV6KyEqplHr+IlpgT0QTZ/iJ+wT7hvTx49Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.54.0",
"@typescript-eslint/visitor-keys": "8.54.0"
"@typescript-eslint/types": "8.55.0",
"@typescript-eslint/visitor-keys": "8.55.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1713,9 +1715,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.54.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.54.0.tgz",
"integrity": "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==",
"version": "8.55.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.55.0.tgz",
"integrity": "sha512-1R9cXqY7RQd7WuqSN47PK9EDpgFUK3VqdmbYrvWJZYDd0cavROGn+74ktWBlmJ13NXUQKlZ/iAEQHI/V0kKe0Q==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1730,15 +1732,15 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.54.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.54.0.tgz",
"integrity": "sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==",
"version": "8.55.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.55.0.tgz",
"integrity": "sha512-x1iH2unH4qAt6I37I2CGlsNs+B9WGxurP2uyZLRz6UJoZWDBx9cJL1xVN/FiOmHEONEg6RIufdvyT0TEYIgC5g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.54.0",
"@typescript-eslint/typescript-estree": "8.54.0",
"@typescript-eslint/utils": "8.54.0",
"@typescript-eslint/types": "8.55.0",
"@typescript-eslint/typescript-estree": "8.55.0",
"@typescript-eslint/utils": "8.55.0",
"debug": "^4.4.3",
"ts-api-utils": "^2.4.0"
},
@@ -1755,9 +1757,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.54.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.54.0.tgz",
"integrity": "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==",
"version": "8.55.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.55.0.tgz",
"integrity": "sha512-ujT0Je8GI5BJWi+/mMoR0wxwVEQaxM+pi30xuMiJETlX80OPovb2p9E8ss87gnSVtYXtJoU9U1Cowcr6w2FE0w==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1769,16 +1771,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.54.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.54.0.tgz",
"integrity": "sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==",
"version": "8.55.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.55.0.tgz",
"integrity": "sha512-EwrH67bSWdx/3aRQhCoxDaHM+CrZjotc2UCCpEDVqfCE+7OjKAGWNY2HsCSTEVvWH2clYQK8pdeLp42EVs+xQw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.54.0",
"@typescript-eslint/tsconfig-utils": "8.54.0",
"@typescript-eslint/types": "8.54.0",
"@typescript-eslint/visitor-keys": "8.54.0",
"@typescript-eslint/project-service": "8.55.0",
"@typescript-eslint/tsconfig-utils": "8.55.0",
"@typescript-eslint/types": "8.55.0",
"@typescript-eslint/visitor-keys": "8.55.0",
"debug": "^4.4.3",
"minimatch": "^9.0.5",
"semver": "^7.7.3",
@@ -1823,16 +1825,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.54.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.54.0.tgz",
"integrity": "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==",
"version": "8.55.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.55.0.tgz",
"integrity": "sha512-BqZEsnPGdYpgyEIkDC1BadNY8oMwckftxBT+C8W0g1iKPdeqKZBtTfnvcq0nf60u7MkjFO8RBvpRGZBPw4L2ow==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.54.0",
"@typescript-eslint/types": "8.54.0",
"@typescript-eslint/typescript-estree": "8.54.0"
"@typescript-eslint/scope-manager": "8.55.0",
"@typescript-eslint/types": "8.55.0",
"@typescript-eslint/typescript-estree": "8.55.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1847,13 +1849,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.54.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.54.0.tgz",
"integrity": "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==",
"version": "8.55.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.55.0.tgz",
"integrity": "sha512-AxNRwEie8Nn4eFS1FzDMJWIISMGoXMb037sgCBJ3UR6o0fQTzr2tqN9WT+DkWJPhIdQCfV7T6D387566VtnCJA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.54.0",
"@typescript-eslint/types": "8.55.0",
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
@@ -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.9",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.11.9.tgz",
"integrity": "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ==",
"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.1",
"description": "An ACP-compatible coding agent powered by the Claude Code SDK (TypeScript)",
"main": "dist/lib.js",
"types": "dist/lib.d.ts",
@@ -60,17 +60,17 @@
"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.38",
"@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",
"@typescript-eslint/eslint-plugin": "8.54.0",
"@typescript-eslint/parser": "8.54.0",
"@anthropic-ai/sdk": "0.74.0",
"@types/node": "25.2.3",
"@typescript-eslint/eslint-plugin": "8.55.0",
"@typescript-eslint/parser": "8.55.0",
"eslint": "9.39.2",
"eslint-config-prettier": "10.1.8",
"globals": "17.3.0",

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 {
@@ -68,24 +79,8 @@ import { fileURLToPath } from "node:url";
export const CLAUDE_CONFIG_DIR =
process.env.CLAUDE_CONFIG_DIR ?? path.join(os.homedir(), ".claude");
/**
* Decode a Claude project path encoding back to the original filesystem path.
* Claude encodes paths by replacing path separators with dashes:
* - Unix: "/Users/morse/project" -> "-Users-morse-project"
* - Windows: "C:\Users\morse\project" -> "C-Users-morse-project"
*/
function decodeProjectPath(encodedPath: string): string {
// Check if this looks like a Windows path (starts with drive letter pattern like "C-")
const windowsDriveMatch = encodedPath.match(/^([A-Za-z])-/);
if (windowsDriveMatch) {
// Windows path: "C-Users-morse-project" -> "C:\Users\morse\project"
const driveLetter = windowsDriveMatch[1];
const rest = encodedPath.slice(2); // Skip "C-"
return `${driveLetter}:\\${rest.replace(/-/g, "\\")}`;
}
// Unix path: "-Users-morse-project" -> "/Users/morse/project"
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;
@@ -118,6 +113,17 @@ type Session = {
settingsManager: SettingsManager;
};
type SessionHistoryEntry = {
type?: string;
isSidechain?: boolean;
sessionId?: string;
message?: {
role?: string;
content?: unknown;
model?: string;
};
};
type BackgroundTerminal =
| {
handle: TerminalHandle;
@@ -146,6 +152,7 @@ export type NewSessionMeta = {
* Those parameters will be used and updated to work with ACP:
* - hooks (merged with ACP's hooks)
* - mcpServers (merged with ACP's mcpServers)
* - disallowedTools (merged with ACP's disallowedTools)
*/
options?: Options;
};
@@ -228,6 +235,7 @@ export class ClaudeAcpAgent implements Agent {
http: true,
sse: true,
},
loadSession: true,
sessionCapabilities: {
fork: {},
list: {},
@@ -251,14 +259,19 @@ export class ClaudeAcpAgent implements Agent {
throw RequestError.authRequired();
}
return await this.createSession(params, {
const response = await this.createSession(params, {
// Revisit these meta values once we support resume
resume: (params._meta as NewSessionMeta | undefined)?.claudeCode?.options?.resume,
});
// Needs to happen after we return the session
setTimeout(() => {
this.sendAvailableCommandsUpdate(response.sessionId);
}, 0);
return response;
}
async unstable_forkSession(params: ForkSessionRequest): Promise<ForkSessionResponse> {
return await this.createSession(
const response = await this.createSession(
{
cwd: params.cwd,
mcpServers: params.mcpServers ?? [],
@@ -269,6 +282,11 @@ export class ClaudeAcpAgent implements Agent {
forkSession: true,
},
);
// Needs to happen after we return the session
setTimeout(() => {
this.sendAvailableCommandsUpdate(response.sessionId);
}, 0);
return response;
}
async unstable_resumeSession(params: ResumeSessionRequest): Promise<ResumeSessionResponse> {
@@ -282,10 +300,83 @@ export class ClaudeAcpAgent implements Agent {
resume: params.sessionId,
},
);
// Needs to happen after we return the session
setTimeout(() => {
this.sendAvailableCommandsUpdate(response.sessionId);
}, 0);
return response;
}
/**
* Find a session file by ID, first checking the given cwd's project directory,
* then falling back to scanning all project directories.
* Returns the absolute file path if found, or null if not found.
*/
private async findSessionFile(sessionId: string, cwd: string): Promise<string | null> {
const fileName = `${sessionId}.jsonl`;
// Fast path: check the expected location based on cwd
const expectedPath = sessionFilePath(cwd, sessionId);
try {
await fs.promises.access(expectedPath);
return expectedPath;
} catch {
// Not found at expected path, scan all project directories
}
const claudeDir = path.join(CLAUDE_CONFIG_DIR, "projects");
try {
const projectDirs = await fs.promises.readdir(claudeDir);
for (const encodedPath of projectDirs) {
const projectDir = path.join(claudeDir, encodedPath);
const stat = await fs.promises.stat(projectDir);
if (!stat.isDirectory()) continue;
const candidatePath = path.join(projectDir, fileName);
try {
await fs.promises.access(candidatePath);
return candidatePath;
} catch {
continue;
}
}
} catch {
// projects directory doesn't exist or isn't readable
}
return null;
}
async loadSession(params: LoadSessionRequest): Promise<LoadSessionResponse> {
const filePath = await this.findSessionFile(params.sessionId, params.cwd);
if (!filePath) {
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, filePath);
// Send available commands after replay so it doesn't interleave with history
setTimeout(() => {
this.sendAvailableCommandsUpdate(params.sessionId);
}, 0);
return {
modes: response.modes,
models: response.models,
};
}
/**
* List Claude Code sessions by parsing JSONL files
* Sessions are stored in ~/.claude/projects/<path-encoded>/
@@ -305,6 +396,7 @@ export class ClaudeAcpAgent implements Agent {
// Collect all sessions across all project directories
const allSessions: SessionInfo[] = [];
const encodedCwdFilter = params.cwd ? encodeProjectPath(params.cwd) : null;
try {
const projectDirs = await fs.promises.readdir(claudeDir);
@@ -314,13 +406,9 @@ export class ClaudeAcpAgent implements Agent {
const stat = await fs.promises.stat(projectDir);
if (!stat.isDirectory()) continue;
// Decode the path based on platform:
// - Unix: "-Users-morse-project" -> "/Users/morse/project"
// - Windows: "C-Users-morse-project" -> "C:\Users\morse\project"
const decodedCwd = decodeProjectPath(encodedPath);
// Skip if filtering by cwd and this doesn't match
if (params.cwd && decodedCwd !== params.cwd) continue;
// Path encoding is not always reversible (hyphens can be separators or literals),
// so only use encoded value as a coarse pre-filter.
if (encodedCwdFilter && encodedPath !== encodedCwdFilter) continue;
const files = await fs.promises.readdir(projectDir);
// Filter to user session files only. Skip agent-*.jsonl files which contain
@@ -333,23 +421,31 @@ export class ClaudeAcpAgent implements Agent {
const content = await fs.promises.readFile(filePath, "utf-8");
const lines = content.trim().split("\n").filter(Boolean);
const firstLine = lines[0];
if (!firstLine) continue;
// Parse first line to get session info
const firstEntry = JSON.parse(firstLine);
const sessionId = firstEntry.sessionId || file.replace(".jsonl", "");
const sessionId = file.replace(".jsonl", "");
let parsedAnyEntry = false;
let sessionCwd: string | undefined;
// Find first user message for title
let title: string | undefined;
for (const line of lines) {
try {
const entry = JSON.parse(line);
if (entry.type === "user" && entry.message?.content) {
parsedAnyEntry = true;
if (entry.isSidechain === true) {
continue;
}
const entrySessionId =
typeof entry.sessionId === "string" ? entry.sessionId : undefined;
if (typeof entry.sessionId === "string" && entry.sessionId !== entrySessionId) {
continue;
}
if (typeof entry.cwd === "string") {
sessionCwd = entry.cwd;
}
if (!title && entry.type === "user" && entry.message?.content) {
const msgContent = entry.message.content;
if (typeof msgContent === "string") {
title = sanitizeTitle(msgContent);
break;
}
if (Array.isArray(msgContent) && msgContent.length > 0) {
const first = msgContent[0];
@@ -361,14 +457,29 @@ export class ClaudeAcpAgent implements Agent {
: undefined;
if (text) {
title = sanitizeTitle(text);
break;
}
}
}
// Continue scanning until we have both fields, since cwd can appear
// in later entries even after the first user title-bearing message.
if (title && sessionCwd) {
break;
}
} catch {
// Skip malformed lines
}
}
if (!parsedAnyEntry) continue;
// SessionInfo.cwd is currently required. For entries that do not
// include an explicit cwd in the session JSONL (typically metadata-only files),
// we skip them instead of decoding folder names because path encoding is lossy.
if (!sessionCwd) continue;
// Even after encoded-path pre-filtering, verify per-entry cwd to disambiguate
// collisions such as "/a-b" and "/a/b" that map to the same encoded folder name.
if (params.cwd && sessionCwd !== params.cwd) continue;
// Get file modification time as updatedAt
const fileStat = await fs.promises.stat(filePath);
@@ -376,7 +487,7 @@ export class ClaudeAcpAgent implements Agent {
allSessions.push({
sessionId,
cwd: decodedCwd,
cwd: sessionCwd,
title: title ?? null,
updatedAt,
});
@@ -656,6 +767,70 @@ export class ClaudeAcpAgent implements Agent {
}
}
private async replaySessionHistory(sessionId: string, filePath: string): Promise<void> {
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;
@@ -794,6 +969,19 @@ export class ClaudeAcpAgent implements Agent {
};
}
private async sendAvailableCommandsUpdate(sessionId: string): Promise<void> {
const session = this.sessions[sessionId];
if (!session) return;
const commands = await session.query.supportedCommands();
await this.client.sessionUpdate({
sessionId,
update: {
sessionUpdate: "available_commands_update",
availableCommands: getAvailableSlashCommands(commands),
},
});
}
private async createSession(
params: NewSessionRequest,
creationOpts: { resume?: string; forkSession?: boolean } = {},
@@ -868,11 +1056,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 +1072,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 +1118,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"];
@@ -988,7 +1175,7 @@ export class ClaudeAcpAgent implements Agent {
options.allowedTools = allowedTools;
}
if (disallowedTools.length > 0) {
options.disallowedTools = disallowedTools;
options.disallowedTools = [...(options.disallowedTools || []), ...disallowedTools];
}
// Handle abort controller from meta options
@@ -1010,19 +1197,9 @@ export class ClaudeAcpAgent implements Agent {
settingsManager,
};
const availableCommands = await getAvailableSlashCommands(q);
const models = await getAvailableModels(q, settingsManager);
const initializationResult = await q.initializationResult();
// Needs to happen after we return the session
setTimeout(() => {
this.client.sessionUpdate({
sessionId,
update: {
sessionUpdate: "available_commands_update",
availableCommands,
},
});
}, 0);
const models = await getAvailableModels(q, initializationResult.models, settingsManager);
const availableModes = [
{
@@ -1068,9 +1245,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 +1278,7 @@ async function getAvailableModels(
};
}
async function getAvailableSlashCommands(query: Query): Promise<AvailableCommand[]> {
function getAvailableSlashCommands(commands: SlashCommand[]): AvailableCommand[] {
const UNSUPPORTED_COMMANDS = [
"cost",
"keybindings-help",
@@ -1111,7 +1288,6 @@ async function getAvailableSlashCommands(query: Query): Promise<AvailableCommand
"release-notes",
"todos",
];
const commands = await query.supportedCommands();
return commands
.map((command) => {
@@ -1242,7 +1418,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 +1485,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 +1575,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,9 @@ describe("tool conversions", () => {
output_tokens: 1,
service_tier: "standard",
server_tool_use: null,
inference_geo: null,
iterations: null,
speed: null,
},
context_management: null,
},
@@ -1001,7 +1004,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,
@@ -43,9 +32,10 @@ describe("unstable_listSessions", () => {
mtime?: Date;
malformed?: boolean;
isAgentFile?: boolean;
omitCwdFromEntries?: boolean;
} = {},
): void {
const encodedPath = encodePath(cwd);
const encodedPath = encodeProjectPath(cwd);
const projectDir = path.join(tempDir, "projects", encodedPath);
fs.mkdirSync(projectDir, { recursive: true });
@@ -67,11 +57,25 @@ describe("unstable_listSessions", () => {
const content = options.userMessageArray
? [{ type: "text", text: options.userMessage }]
: options.userMessage;
lines.push(JSON.stringify({ type: "user", message: { content } }));
lines.push(
JSON.stringify({
type: "user",
sessionId,
...(options.omitCwdFromEntries ? {} : { cwd }),
message: { content },
}),
);
}
// Assistant response
lines.push(JSON.stringify({ type: "assistant", message: { content: "Hello!" } }));
lines.push(
JSON.stringify({
type: "assistant",
sessionId,
...(options.omitCwdFromEntries ? {} : { cwd }),
message: { content: "Hello!" },
}),
);
fs.writeFileSync(filePath, lines.join("\n"));
@@ -198,6 +202,66 @@ describe("unstable_listSessions", () => {
expect(result.sessions[0]!.sessionId).toBe("sess-a");
});
it("uses per-entry cwd when filtering even if encoded folder collides", async () => {
const encodedPath = encodeProjectPath("/Users/test/a-b");
const projectDir = path.join(tempDir, "projects", encodedPath);
fs.mkdirSync(projectDir, { recursive: true });
fs.writeFileSync(
path.join(projectDir, "sess-a-b.jsonl"),
[
JSON.stringify({ sessionId: "sess-a-b", type: "init" }),
JSON.stringify({
type: "user",
sessionId: "sess-a-b",
cwd: "/Users/test/a-b",
message: { content: "A-B" },
}),
].join("\n"),
);
fs.writeFileSync(
path.join(projectDir, "sess-a-slash-b.jsonl"),
[
JSON.stringify({ sessionId: "sess-a-slash-b", type: "init" }),
JSON.stringify({
type: "user",
sessionId: "sess-a-slash-b",
cwd: "/Users/test/a/b",
message: { content: "A/B" },
}),
].join("\n"),
);
const result = await agent.unstable_listSessions({ cwd: "/Users/test/a-b" });
expect(result.sessions).toHaveLength(1);
expect(result.sessions[0]!.sessionId).toBe("sess-a-b");
expect(result.sessions[0]!.cwd).toBe("/Users/test/a-b");
});
it("returns the true cwd for paths containing hyphens", async () => {
const cwd = "/Users/test/claude-code-acp";
writeSessionFile(cwd, "hyphen-session", { userMessage: "Hyphen test" });
const result = await agent.unstable_listSessions({});
expect(result.sessions).toHaveLength(1);
expect(result.sessions[0]!.cwd).toBe(cwd);
expect(result.sessions[0]!.sessionId).toBe("hyphen-session");
});
it("filters hyphenated paths by cwd correctly", async () => {
writeSessionFile("/Users/test/claude-code-acp", "hyphen-a", { userMessage: "A" });
writeSessionFile("/Users/test/other-project", "hyphen-b", { userMessage: "B" });
const result = await agent.unstable_listSessions({ cwd: "/Users/test/claude-code-acp" });
expect(result.sessions).toHaveLength(1);
expect(result.sessions[0]!.sessionId).toBe("hyphen-a");
expect(result.sessions[0]!.cwd).toBe("/Users/test/claude-code-acp");
});
it("returns all sessions when cwd is not specified", async () => {
writeSessionFile("/Users/test/projectone", "sess-a", { userMessage: "A" });
writeSessionFile("/Users/test/projecttwo", "sess-b", { userMessage: "B" });
@@ -248,22 +312,168 @@ 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 });
// Write file without sessionId in content
// Write file without sessionId in content (uses filename fallback), but with cwd.
const filePath = path.join(projectDir, "fallback-id.jsonl");
fs.writeFileSync(filePath, JSON.stringify({ type: "init" }) + "\n");
fs.writeFileSync(
filePath,
[
JSON.stringify({ type: "init" }),
JSON.stringify({ type: "assistant", cwd, message: { content: "Hi" } }),
].join("\n"),
);
const result = await agent.unstable_listSessions({});
expect(result.sessions[0]!.sessionId).toBe("fallback-id");
});
it("keeps sessionId stable from filename when later entries have different sessionId", async () => {
const cwd = "/Users/test/project";
const encodedPath = encodeProjectPath(cwd);
const projectDir = path.join(tempDir, "projects", encodedPath);
fs.mkdirSync(projectDir, { recursive: true });
const filePath = path.join(projectDir, "stable-id.jsonl");
fs.writeFileSync(
filePath,
[
JSON.stringify({ type: "init", sessionId: "stable-id" }),
JSON.stringify({
type: "user",
sessionId: "stable-id",
cwd,
message: { content: "Hello" },
}),
JSON.stringify({
type: "assistant",
sessionId: "other-id",
cwd,
message: { content: "Wrong session id from sidechain" },
}),
].join("\n"),
);
const result = await agent.unstable_listSessions({});
expect(result.sessions).toHaveLength(1);
expect(result.sessions[0]!.sessionId).toBe("stable-id");
});
it("derives cwd from entries for the filename sessionId only", async () => {
const cwd = "/Users/test/project";
const encodedPath = encodeProjectPath(cwd);
const projectDir = path.join(tempDir, "projects", encodedPath);
fs.mkdirSync(projectDir, { recursive: true });
const filePath = path.join(projectDir, "target-session.jsonl");
fs.writeFileSync(
filePath,
[
JSON.stringify({
type: "user",
sessionId: "target-session",
cwd,
message: { content: "Correct cwd entry" },
}),
].join("\n"),
);
const result = await agent.unstable_listSessions({});
expect(result.sessions).toHaveLength(1);
expect(result.sessions[0]!.sessionId).toBe("target-session");
expect(result.sessions[0]!.cwd).toBe(cwd);
});
it("keeps scanning after title to find later cwd in same session", async () => {
const cwd = "/Users/test/project";
const encodedPath = encodeProjectPath(cwd);
const projectDir = path.join(tempDir, "projects", encodedPath);
fs.mkdirSync(projectDir, { recursive: true });
const filePath = path.join(projectDir, "late-cwd.jsonl");
fs.writeFileSync(
filePath,
[
JSON.stringify({ type: "init", sessionId: "late-cwd" }),
JSON.stringify({
type: "user",
sessionId: "late-cwd",
message: { content: "Title from first user message" },
}),
JSON.stringify({
type: "assistant",
sessionId: "late-cwd",
cwd,
message: { content: "Assistant entry carries cwd" },
}),
].join("\n"),
);
const result = await agent.unstable_listSessions({});
expect(result.sessions).toHaveLength(1);
expect(result.sessions[0]!.sessionId).toBe("late-cwd");
expect(result.sessions[0]!.cwd).toBe(cwd);
expect(result.sessions[0]!.title).toBe("Title from first user message");
});
it("ignores sidechain entries when deriving cwd and title", async () => {
const cwd = "/Users/test/project";
const encodedPath = encodeProjectPath(cwd);
const projectDir = path.join(tempDir, "projects", encodedPath);
fs.mkdirSync(projectDir, { recursive: true });
const filePath = path.join(projectDir, "sidechain-metadata.jsonl");
fs.writeFileSync(
filePath,
[
JSON.stringify({ type: "init", sessionId: "sidechain-metadata" }),
JSON.stringify({
type: "user",
sessionId: "sidechain-metadata",
isSidechain: true,
cwd: "/Users/test/wrong-project",
message: { content: "Wrong sidechain title" },
}),
JSON.stringify({
type: "user",
sessionId: "sidechain-metadata",
cwd,
message: { content: "Correct primary title" },
}),
].join("\n"),
);
const result = await agent.unstable_listSessions({ cwd });
expect(result.sessions).toHaveLength(1);
expect(result.sessions[0]!.sessionId).toBe("sidechain-metadata");
expect(result.sessions[0]!.cwd).toBe(cwd);
expect(result.sessions[0]!.title).toBe("Correct primary title");
});
it("skips entries without cwd when cwd filter is not provided", async () => {
const cwd = "/Users/test/project";
const encodedPath = encodeProjectPath(cwd);
const projectDir = path.join(tempDir, "projects", encodedPath);
fs.mkdirSync(projectDir, { recursive: true });
const filePath = path.join(projectDir, "no-cwd.jsonl");
fs.writeFileSync(filePath, JSON.stringify({ sessionId: "no-cwd", type: "summary" }) + "\n");
const result = await agent.unstable_listSessions({});
expect(result.sessions).toHaveLength(0);
});
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 });
@@ -273,7 +483,7 @@ describe("unstable_listSessions", () => {
filePath,
[
JSON.stringify({ sessionId: "no-user", type: "init" }),
JSON.stringify({ type: "assistant", message: { content: "Hi" } }),
JSON.stringify({ type: "assistant", cwd, message: { content: "Hi" } }),
].join("\n"),
);

View File

@@ -0,0 +1,320 @@
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("finds session file when cwd differs from the original session cwd", async () => {
const originalCwd = "/Users/test/original-project";
const differentCwd = "/Users/test/different-project";
const sessionId = "cross-cwd-session";
// Session was created in originalCwd
writeSessionFile(originalCwd, sessionId, [
{
type: "user",
sessionId,
cwd: originalCwd,
message: { role: "user", content: "Hello from original" },
},
]);
// Load with a different cwd
await agent.loadSession({ cwd: differentCwd, sessionId, mcpServers: [] });
expect(sessionUpdates).toHaveLength(1);
expect(sessionUpdates[0]?.update).toMatchObject({
sessionUpdate: "user_message_chunk",
content: { type: "text", text: "Hello from original" },
});
expect(createSessionSpy).toHaveBeenCalledWith(expect.objectContaining({ cwd: differentCwd }), {
resume: sessionId,
});
});
it("sends available_commands_update after history replay, not during", async () => {
const cwd = "/Users/test/project";
const sessionId = "session-commands";
// Mock createSession to populate the sessions map with a mock query
createSessionSpy.mockImplementationOnce(async () => {
(agent as unknown as { sessions: Record<string, unknown> }).sessions[sessionId] = {
query: {
supportedCommands: async () => [{ name: "help", description: "Get help" }],
},
};
return { modes: null, models: null };
});
writeSessionFile(cwd, sessionId, [
{
type: "user",
sessionId,
message: { role: "user", content: "Hello" },
},
{
type: "assistant",
sessionId,
message: { role: "assistant", content: [{ type: "text", text: "Hi" }] },
},
]);
await agent.loadSession({ cwd, sessionId, mcpServers: [] });
// Flush the setTimeout + async work inside sendAvailableCommandsUpdate
await new Promise((resolve) => setTimeout(resolve, 10));
const updateTypes = sessionUpdates.map((n) => n.update.sessionUpdate);
// available_commands_update should come AFTER all replay notifications
expect(updateTypes).toEqual([
"user_message_chunk",
"agent_message_chunk",
"available_commands_update",
]);
});
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, "-");
}