Compare commits

...

6 Commits

Author SHA1 Message Date
acp-release-bot[bot]
47fee47709 chore(main): release 0.53.0 (#817)
🤖 I have created a release *beep* *boop*
---


##
[0.53.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.52.0...v0.53.0)
(2026-06-29)


### Features

* Add ACP logout support
([#816](https://github.com/agentclientprotocol/claude-agent-acp/issues/816))
([0a0468c](0a0468c720))
* **deps:** bump @anthropic-ai/claude-agent-sdk to 0.3.195
([#818](https://github.com/agentclientprotocol/claude-agent-acp/issues/818))
([5dd8746](5dd8746237))


### Bug Fixes

* Emit tool_call before permission request
([#820](https://github.com/agentclientprotocol/claude-agent-acp/issues/820))
([c95fc88](c95fc884e8))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: acp-release-bot[bot] <246668977+acp-release-bot[bot]@users.noreply.github.com>
2026-06-29 13:49:41 +00:00
Ben Brandt
c95fc884e8 fix: Emit tool_call before permission request (#820)
The Claude Agent SDK changed the timing of `canUseTool` — it now runs
*before* the assistant message's `tool_use` block streams to us. As a
result we sent a permission request (and `AskUserQuestion` elicitation)
referencing a `toolCallId` the client had not yet seen via a `tool_call`
update, so clients couldn't associate the prompt with a tool call.

This supersedes #815, which fixed the ordering by blocking `canUseTool`
until the stream emitted the `tool_call`. That approach could deadlock
on cancellation (the `await` had no signal/timeout) and woke the waiter
from only one branch, so suppressed tools never resolved it.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 15:48:22 +02:00
dependabot[bot]
5dd8746237 feat(deps): bump @anthropic-ai/claude-agent-sdk to 0.3.195 (#818)
Bumps the minor group with 11 updates:

| Package | From | To |
| --- | --- | --- |
|
[@anthropic-ai/claude-agent-sdk](https://github.com/anthropics/claude-agent-sdk-typescript)
| `0.3.191` | `0.3.193` |
| [prettier](https://github.com/prettier/prettier) | `3.8.4` | `3.8.5` |
| @anthropic-ai/claude-agent-sdk-darwin-arm64 | `0.3.191` | `0.3.193` |
| @anthropic-ai/claude-agent-sdk-darwin-x64 | `0.3.191` | `0.3.193` |
| @anthropic-ai/claude-agent-sdk-linux-arm64 | `0.3.191` | `0.3.193` |
| @anthropic-ai/claude-agent-sdk-linux-arm64-musl | `0.3.191` |
`0.3.193` |
| @anthropic-ai/claude-agent-sdk-linux-x64 | `0.3.191` | `0.3.193` |
| @anthropic-ai/claude-agent-sdk-linux-x64-musl | `0.3.191` | `0.3.193`
|
| @anthropic-ai/claude-agent-sdk-win32-arm64 | `0.3.191` | `0.3.193` |
| @anthropic-ai/claude-agent-sdk-win32-x64 | `0.3.191` | `0.3.193` |
| [range-parser](https://github.com/jshttp/range-parser) | `1.2.1` |
`1.3.0` |

Updates `@anthropic-ai/claude-agent-sdk` from 0.3.191 to 0.3.193
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/anthropics/claude-agent-sdk-typescript/releases">@​anthropic-ai/claude-agent-sdk's
releases</a>.</em></p>
<blockquote>
<h2>v0.3.193</h2>
<h2>What's changed</h2>
<ul>
<li>Added <code>promptSuggestions</code> option to Browser SDK
<code>query()</code> to opt the remote CLI into emitting follow-up
suggestions</li>
<li>Fixed brief console window flashes on Windows when spawning CLI
subprocesses</li>
</ul>
<h2>Update</h2>
<pre lang="sh"><code>npm install @anthropic-ai/claude-agent-sdk@0.3.193
# or
yarn add @anthropic-ai/claude-agent-sdk@0.3.193
# or
pnpm add @anthropic-ai/claude-agent-sdk@0.3.193
# or
bun add @anthropic-ai/claude-agent-sdk@0.3.193
</code></pre>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/anthropics/claude-agent-sdk-typescript/blob/main/CHANGELOG.md">@​anthropic-ai/claude-agent-sdk's
changelog</a>.</em></p>
<blockquote>
<h2>0.3.193</h2>
<ul>
<li>Added <code>promptSuggestions</code> option to Browser SDK
<code>query()</code> to opt the remote CLI into emitting follow-up
suggestions</li>
<li>Fixed brief console window flashes on Windows when spawning CLI
subprocesses</li>
</ul>
<h2>0.3.192</h2>
<ul>
<li>Updated to parity with Claude Code v2.1.192</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="761e2bec6f"><code>761e2be</code></a>
chore: Update CHANGELOG.md</li>
<li>See full diff in <a
href="https://github.com/anthropics/claude-agent-sdk-typescript/compare/v0.3.191...v0.3.193">compare
view</a></li>
</ul>
</details>
<br />

Updates `prettier` from 3.8.4 to 3.8.5
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/prettier/prettier/releases">prettier's
releases</a>.</em></p>
<blockquote>
<h2>3.8.5</h2>
<ul>
<li>Fix Flow variance annotation print (<a
href="https://redirect.github.com/prettier/prettier/pull/19022">#19022</a>
by <a
href="https://github.com/marcoww6"><code>@​marcoww6</code></a>)</li>
</ul>
<p>🔗 <a
href="https://github.com/prettier/prettier/blob/3.8.5/CHANGELOG.md#385">Changelog</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/prettier/prettier/blob/main/CHANGELOG.md">prettier's
changelog</a>.</em></p>
<blockquote>
<h1>3.8.5</h1>
<p><a
href="https://github.com/prettier/prettier/compare/3.8.4...3.8.5">diff</a></p>
<h4>Flow: Support <code>readonly</code> as a variance annotation (<a
href="https://redirect.github.com/prettier/prettier/pull/19022">#19022</a>
by <a
href="https://github.com/marcoww6"><code>@​marcoww6</code></a>)</h4>
<p>Flow now accepts <code>readonly</code> as a property variance
annotation, equivalent to <code>+</code> (covariant/read-only).</p>
<!-- raw HTML omitted -->
<pre lang="jsx"><code>// Input
type T = {
  readonly foo: string,
};
<p>// Prettier 3.8.4<br />
SyntaxError</p>
<p>// Prettier 3.8.5<br />
type T = {<br />
readonly foo: string,<br />
};<br />
</code></pre></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="1ce8c47015"><code>1ce8c47</code></a>
Release 3.8.5</li>
<li><a
href="74923fa831"><code>74923fa</code></a>
Fix variance annotation</li>
<li><a
href="8b4eb339d4"><code>8b4eb33</code></a>
Bump Prettier dependency to 3.8.4</li>
<li><a
href="e9cbdfa1b7"><code>e9cbdfa</code></a>
Clean changelog_unreleased</li>
<li>See full diff in <a
href="https://github.com/prettier/prettier/compare/3.8.4...3.8.5">compare
view</a></li>
</ul>
</details>
<br />

Updates `@anthropic-ai/claude-agent-sdk-darwin-arm64` from 0.3.191 to
0.3.193

Updates `@anthropic-ai/claude-agent-sdk-darwin-x64` from 0.3.191 to
0.3.193

Updates `@anthropic-ai/claude-agent-sdk-linux-arm64` from 0.3.191 to
0.3.193

Updates `@anthropic-ai/claude-agent-sdk-linux-arm64-musl` from 0.3.191
to 0.3.193

Updates `@anthropic-ai/claude-agent-sdk-linux-x64` from 0.3.191 to
0.3.193

Updates `@anthropic-ai/claude-agent-sdk-linux-x64-musl` from 0.3.191 to
0.3.193

Updates `@anthropic-ai/claude-agent-sdk-win32-arm64` from 0.3.191 to
0.3.193

Updates `@anthropic-ai/claude-agent-sdk-win32-x64` from 0.3.191 to
0.3.193

Updates `range-parser` from 1.2.1 to 1.3.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/jshttp/range-parser/releases">range-parser's
releases</a>.</em></p>
<blockquote>
<h2>v1.3.0</h2>
<p><strong>Fixed</strong></p>
<ul>
<li>Improve number parsing (<a
href="https://redirect.github.com/jshttp/range-parser/issues/58">#58</a>)
eba9c7a</li>
<li>Handle invalid start and end byte positions in range parsing (<a
href="https://redirect.github.com/jshttp/range-parser/issues/57">#57</a>)
269cb4e</li>
</ul>
<hr />
<p><a
href="https://github.com/jshttp/range-parser/compare/v1.2.1...v1.3.0">https://github.com/jshttp/range-parser/compare/v1.2.1...v1.3.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="f4bf1736f2"><code>f4bf173</code></a>
Clamp a suffix whose length exceeds the representation (<a
href="https://redirect.github.com/jshttp/range-parser/issues/66">#66</a>)</li>
<li><a
href="b49e00f6d3"><code>b49e00f</code></a>
fix: still show ranges if there are multiple ranges, even if some are
invalid...</li>
<li><a
href="4f3b09123c"><code>4f3b091</code></a>
Remove dependabot config (<a
href="https://redirect.github.com/jshttp/range-parser/issues/60">#60</a>)</li>
<li><a
href="eba9c7a9cb"><code>eba9c7a</code></a>
Improve number parsing (<a
href="https://redirect.github.com/jshttp/range-parser/issues/58">#58</a>)</li>
<li><a
href="269cb4eafa"><code>269cb4e</code></a>
fix: handle invalid start and end byte positions in range parsing (<a
href="https://redirect.github.com/jshttp/range-parser/issues/57">#57</a>)</li>
<li><a
href="098e332bfc"><code>098e332</code></a>
build(deps): bump github/codeql-action from 3.31.2 to 4.31.6 (<a
href="https://redirect.github.com/jshttp/range-parser/issues/50">#50</a>)</li>
<li><a
href="3599369124"><code>3599369</code></a>
build(deps): bump actions/checkout from 3.6.0 to 6.0.0 (<a
href="https://redirect.github.com/jshttp/range-parser/issues/51">#51</a>)</li>
<li><a
href="054ea57071"><code>054ea57</code></a>
build(deps): bump github/codeql-action from 2.23.2 to 4.31.2 (<a
href="https://redirect.github.com/jshttp/range-parser/issues/48">#48</a>)</li>
<li><a
href="d230b16695"><code>d230b16</code></a>
build(deps): bump actions/upload-artifact from 3.1.3 to 5.0.0 (<a
href="https://redirect.github.com/jshttp/range-parser/issues/49">#49</a>)</li>
<li><a
href="54f84f423b"><code>54f84f4</code></a>
chore: add funding to package.json (<a
href="https://redirect.github.com/jshttp/range-parser/issues/42">#42</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/jshttp/range-parser/compare/v1.2.1...v1.3.0">compare
view</a></li>
</ul>
</details>
<details>
<summary>Maintainer changes</summary>
<p>This version was pushed to npm by <a
href="https://www.npmjs.com/~blakeembrey">blakeembrey</a>, a new
releaser for range-parser since your current version.</p>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2026-06-29 10:21:47 +00:00
Ben Brandt
0a0468c720 feat: Add ACP logout support (#816) 2026-06-25 21:42:58 +00:00
acp-release-bot[bot]
e9163f8553 chore(main): release 0.52.0 (#811)
🤖 I have created a release *beep* *boop*
---


##
[0.52.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.51.0...v0.52.0)
(2026-06-25)


### Features

* Add version flag handling
([#813](https://github.com/agentclientprotocol/claude-agent-acp/issues/813))
([9616bda](9616bdac47)),
closes
[#809](https://github.com/agentclientprotocol/claude-agent-acp/issues/809)
* **deps-dev:** bump expect-type from 1.3.0 to 1.4.0 in the minor group
([#814](https://github.com/agentclientprotocol/claude-agent-acp/issues/814))
([61272ac](61272acb30))
* **deps:** Update @anthropic-ai/claude-agent-sdk to 0.3.191
([#810](https://github.com/agentclientprotocol/claude-agent-acp/issues/810))
([228f02e](228f02ecfb))
* Push session title updates at turn end
([#812](https://github.com/agentclientprotocol/claude-agent-acp/issues/812))
([1fe7ec0](1fe7ec09a3))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: acp-release-bot[bot] <246668977+acp-release-bot[bot]@users.noreply.github.com>
2026-06-25 17:11:07 +02:00
dependabot[bot]
61272acb30 feat(deps-dev): bump expect-type from 1.3.0 to 1.4.0 in the minor group (#814)
Bumps the minor group with 1 update:
[expect-type](https://github.com/mmkal/expect-type).

Updates `expect-type` from 1.3.0 to 1.4.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/mmkal/expect-type/releases">expect-type's
releases</a>.</em></p>
<blockquote>
<h2>v1.4.0</h2>
<h2>What's Changed</h2>
<ul>
<li>test: lock non-callable thisParameter fallback by <a
href="https://github.com/mmkal"><code>@​mmkal</code></a> in <a
href="https://redirect.github.com/mmkal/expect-type/pull/214">mmkal/expect-type#214</a></li>
<li>feat: support overloads in <code>thisParameter</code> by <a
href="https://github.com/aryaemami59"><code>@​aryaemami59</code></a> in
<a
href="https://redirect.github.com/mmkal/expect-type/pull/204">mmkal/expect-type#204</a></li>
<li>feat: <code>.branded.inspect</code> to view deep prop types by <a
href="https://github.com/mmkal"><code>@​mmkal</code></a> in <a
href="https://redirect.github.com/mmkal/expect-type/pull/113">mmkal/expect-type#113</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/mmkal/expect-type/compare/v1.3.0...v1.4.0">https://github.com/mmkal/expect-type/compare/v1.3.0...v1.4.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="48743118a1"><code>4874311</code></a>
1.4.0</li>
<li><a
href="27b0e579fd"><code>27b0e57</code></a>
idea: <code>.branded.inspect</code> to view deep prop types (<a
href="https://redirect.github.com/mmkal/expect-type/issues/113">#113</a>)</li>
<li><a
href="2794969582"><code>2794969</code></a>
renovate: update 🌲 (<a
href="https://redirect.github.com/mmkal/expect-type/issues/215">#215</a>)</li>
<li><a
href="29130cdef2"><code>29130cd</code></a>
feat: support overloads in <code>thisParameter</code> (<a
href="https://redirect.github.com/mmkal/expect-type/issues/204">#204</a>)</li>
<li><a
href="658ea8784a"><code>658ea87</code></a>
test: lock non-callable thisParameter fallback (<a
href="https://redirect.github.com/mmkal/expect-type/issues/214">#214</a>)</li>
<li><a
href="22eb69e931"><code>22eb69e</code></a>
renovate: update 🌲 (<a
href="https://redirect.github.com/mmkal/expect-type/issues/212">#212</a>)</li>
<li><a
href="f6ec417af1"><code>f6ec417</code></a>
chore: gate Renovate dependency PRs (<a
href="https://redirect.github.com/mmkal/expect-type/issues/213">#213</a>)</li>
<li><a
href="25403eb822"><code>25403eb</code></a>
renovate: update np, <code>@​types/node</code> and 12 more 🌲 (<a
href="https://redirect.github.com/mmkal/expect-type/issues/133">#133</a>)</li>
<li><a
href="459a1e116b"><code>459a1e1</code></a>
Update <code>@typescript/native-preview</code> as it's stable enough (<a
href="https://redirect.github.com/mmkal/expect-type/issues/186">#186</a>)</li>
<li><a
href="c84e6164c1"><code>c84e616</code></a>
Update pnpm to <code>10.25.0</code> (<a
href="https://redirect.github.com/mmkal/expect-type/issues/187">#187</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/mmkal/expect-type/compare/v1.3.0...v1.4.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=expect-type&package-manager=npm_and_yarn&previous-version=1.3.0&new-version=1.4.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-25 17:08:42 +02:00
9 changed files with 474 additions and 127 deletions

View File

@@ -1,3 +1,3 @@
{
".": "0.51.0"
".": "0.53.0"
}

View File

@@ -1,5 +1,28 @@
# Changelog
## [0.53.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.52.0...v0.53.0) (2026-06-29)
### Features
* Add ACP logout support ([#816](https://github.com/agentclientprotocol/claude-agent-acp/issues/816)) ([0a0468c](https://github.com/agentclientprotocol/claude-agent-acp/commit/0a0468c72092b7ceccc551b4353dd92f9c22cbb8))
* **deps:** bump @anthropic-ai/claude-agent-sdk to 0.3.195 ([#818](https://github.com/agentclientprotocol/claude-agent-acp/issues/818)) ([5dd8746](https://github.com/agentclientprotocol/claude-agent-acp/commit/5dd87462376597eb7b695200f4c716af46e764d6))
### Bug Fixes
* Emit tool_call before permission request ([#820](https://github.com/agentclientprotocol/claude-agent-acp/issues/820)) ([c95fc88](https://github.com/agentclientprotocol/claude-agent-acp/commit/c95fc884e82e5a297d1b17b8965690260867835b))
## [0.52.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.51.0...v0.52.0) (2026-06-25)
### Features
* Add version flag handling ([#813](https://github.com/agentclientprotocol/claude-agent-acp/issues/813)) ([9616bda](https://github.com/agentclientprotocol/claude-agent-acp/commit/9616bdac47505e4a14c36d667fcffc9ae97e1f2a)), closes [#809](https://github.com/agentclientprotocol/claude-agent-acp/issues/809)
* **deps-dev:** bump expect-type from 1.3.0 to 1.4.0 in the minor group ([#814](https://github.com/agentclientprotocol/claude-agent-acp/issues/814)) ([61272ac](https://github.com/agentclientprotocol/claude-agent-acp/commit/61272acb30dcafaa2455d334b11ce2ad97339707))
* **deps:** Update @anthropic-ai/claude-agent-sdk to 0.3.191 ([#810](https://github.com/agentclientprotocol/claude-agent-acp/issues/810)) ([228f02e](https://github.com/agentclientprotocol/claude-agent-acp/commit/228f02ecfb23be16e59e121c6b42c0f2b2f40a4e))
* Push session title updates at turn end ([#812](https://github.com/agentclientprotocol/claude-agent-acp/issues/812)) ([1fe7ec0](https://github.com/agentclientprotocol/claude-agent-acp/commit/1fe7ec09a3a7bcb7501231dae4b0ffe6ef9b70a4))
## [0.51.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.50.0...v0.51.0) (2026-06-24)

126
package-lock.json generated
View File

@@ -1,16 +1,16 @@
{
"name": "@agentclientprotocol/claude-agent-acp",
"version": "0.51.0",
"version": "0.53.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@agentclientprotocol/claude-agent-acp",
"version": "0.51.0",
"version": "0.53.0",
"license": "Apache-2.0",
"dependencies": {
"@agentclientprotocol/sdk": "1.0.0",
"@anthropic-ai/claude-agent-sdk": "0.3.191",
"@anthropic-ai/claude-agent-sdk": "0.3.195",
"zod": "^3.25.0 || ^4.0.0"
},
"bin": {
@@ -23,10 +23,10 @@
"@types/node": "26.0.1",
"@typescript-eslint/eslint-plugin": "8.62.0",
"@typescript-eslint/parser": "8.62.0",
"eslint": "10.5.0",
"eslint": "10.6.0",
"eslint-config-prettier": "10.1.8",
"globals": "17.7.0",
"prettier": "3.8.4",
"prettier": "3.9.1",
"ts-node": "10.9.2",
"typescript": "6.0.3",
"vitest": "4.1.9"
@@ -45,22 +45,22 @@
}
},
"node_modules/@anthropic-ai/claude-agent-sdk": {
"version": "0.3.191",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.191.tgz",
"integrity": "sha512-DM9oYbt+WAb/CiD8l/CO6akqPKfyZ4sL1e+lP2O2eS9IR0f58sf90xWeji7V+/zfnIp+jyH3hJ3V6r7odCdB8g==",
"version": "0.3.195",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.195.tgz",
"integrity": "sha512-FVmXu9pvOMbuBKWrF8YsYQdQ/upOpv5rS8lFAnFO5jbyXT/2hN7kEPd2vd2GJpaMvNcO/KptyQUK5AxjjTz3+w==",
"license": "SEE LICENSE IN README.md",
"engines": {
"node": ">=18.0.0"
},
"optionalDependencies": {
"@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.191",
"@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.191",
"@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.191",
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.191",
"@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.191",
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.191",
"@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.191",
"@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.191"
"@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.195",
"@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.195",
"@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.195",
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.195",
"@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.195",
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.195",
"@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.195",
"@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.195"
},
"peerDependencies": {
"@anthropic-ai/sdk": ">=0.93.0",
@@ -69,9 +69,9 @@
}
},
"node_modules/@anthropic-ai/claude-agent-sdk-darwin-arm64": {
"version": "0.3.191",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.191.tgz",
"integrity": "sha512-rmpi5ONfuuw7oXdzyr9QJB5sK9Rr1MmdOUsiGutVFpcAqqSXpVYLmMvfaarmXvItgfwgGprzrGDfcAeAQHSUGw==",
"version": "0.3.195",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.195.tgz",
"integrity": "sha512-WIMM/8HRCLsTDHFTIwQvvE8WCA/oaMJtdQxsP7iNyfzIGwXbuOyU95V8vYIhZfaO2yaSpbBRncunq4CtR5H4ng==",
"cpu": [
"arm64"
],
@@ -82,9 +82,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-darwin-x64": {
"version": "0.3.191",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.191.tgz",
"integrity": "sha512-b/4pfSFTC2gkc0RoGSJ5B3DWo77qZFMi2hRlx8kwaRDiefeIOnbJu42gDBrs4F8nSJJug77HM9AsG6XHXvt5Fg==",
"version": "0.3.195",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.195.tgz",
"integrity": "sha512-RY7DB+4LXosE0MJ+XELmakfPrDN1YX4lkk9CTDm28jGCVcESRz9kAEqbyaiC48dZcmN9V1NCLutzINGdcr1TBg==",
"cpu": [
"x64"
],
@@ -95,9 +95,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64": {
"version": "0.3.191",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.191.tgz",
"integrity": "sha512-vm526e3InpItmh+Ua8a49eGTpFJvZTYLURX9ppKrtFb+Wo7rOgoXmK/Hzf/crEU7h72xtaD7PIOXm4P+psxO3w==",
"version": "0.3.195",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.195.tgz",
"integrity": "sha512-JuIq5Fnz/F1snl0aqi1gcuRZqPWoPNrL9dJ0DuievCxKkO8hnEz/Mmn5Zos7x1X8HE//ZnEvmQXoEQEZXonJew==",
"cpu": [
"arm64"
],
@@ -111,9 +111,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64-musl": {
"version": "0.3.191",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.191.tgz",
"integrity": "sha512-+Gg7lKwYDr8uiA37EBMYI63if+YGdbrFt2tcRFBB4IhdVf5r1IkKwQ8rEYV6Y0nqGTKoXLh3bRc5QWOe5JwgbQ==",
"version": "0.3.195",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.195.tgz",
"integrity": "sha512-ZmyBA/AFzhgutcxb7dbhCm6GTjJytwNYXTxJoKE2B3A409WCYccjMqeji6vCMNxyyfylglGo5D8dVMIxW9aoug==",
"cpu": [
"arm64"
],
@@ -127,9 +127,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-linux-x64": {
"version": "0.3.191",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.191.tgz",
"integrity": "sha512-wj43/l0Behb4UWYt6Lomq2W7jhmuU7ARsEP0el6gLGCR348PDeqLGcB4dMOcYMGRZv8TYKfnhDwJnX4pJBT1vQ==",
"version": "0.3.195",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.195.tgz",
"integrity": "sha512-s1lNi1cL93luoqsItH+fNO4KpIhdkvnVhWGGQUQ/8ftwa2gfmcIQnOg1hG8Ks+KzeD3UUQ8L9YEVHVADnFI/9A==",
"cpu": [
"x64"
],
@@ -143,9 +143,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-linux-x64-musl": {
"version": "0.3.191",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.191.tgz",
"integrity": "sha512-tUwwIXpPBrzJge3E4SQJQX8PuC+lxPb5xQBmyLsWxyjttlgpFpTG/jWPVQO5qH0HgYaEJS5JALHFiYcXwVrEkg==",
"version": "0.3.195",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.195.tgz",
"integrity": "sha512-nf8Q/LauB+ZOC6QDjxNhbsvwUtYjKYnaWJLTYFwhkmsLujePnety1AtT/1ubaUoq5AM1j297DhMlYTasa79OUA==",
"cpu": [
"x64"
],
@@ -159,9 +159,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-win32-arm64": {
"version": "0.3.191",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.191.tgz",
"integrity": "sha512-J8aTOi7n6AM7tHFlKt6hlJ4rue0TbY2glqcI2r6K3iK7Fmzvt9zfg00+l8fWKOais4K+8jqJCC4WSCUlz6+tKQ==",
"version": "0.3.195",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.195.tgz",
"integrity": "sha512-hbkDE+xPIZzRWm+D+BKrH9uJH6USIZdDIlsyrIlGi3JFHoieYoA1vdUNyldSS9+F3ZqQtfPjr2Qy08IVB6akYA==",
"cpu": [
"arm64"
],
@@ -172,9 +172,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-win32-x64": {
"version": "0.3.191",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.191.tgz",
"integrity": "sha512-1rYlPuM9PS37IPCE/sAG7A7oCrWnIb0J7EY6eQBdImdXHEjwBngZ3E8KbtEM79fUTA3C810LqrnFpjOmNExLMA==",
"version": "0.3.195",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.195.tgz",
"integrity": "sha512-av0piEB3X1Dzhpr8A+DqHVZ9y8s1jpn8enzwX0TKKUPBn5IqLTWC7wD6v66aoUgu4f+g4ThZirmDZA6shyPEZQ==",
"cpu": [
"x64"
],
@@ -1441,9 +1441,9 @@
}
},
"node_modules/brace-expansion": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
"integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1701,9 +1701,9 @@
}
},
"node_modules/es-module-lexer": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz",
"integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.2.0.tgz",
"integrity": "sha512-3lGxdTXCLfe1MYfTz1y2ksAAUM4NAOP6rPEjxGJVKO7TZ5+tvHCaQWGpC4Y3IXvW3ece0Cz1cIP4FWBxOnGCTQ==",
"dev": true,
"license": "MIT"
},
@@ -1741,9 +1741,9 @@
}
},
"node_modules/eslint": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.5.0.tgz",
"integrity": "sha512-1y+7C+vi12bUK1IpZeaV3gsH9fHLBmPvYmPx42pvT/E9yG0IC8g3PUZZgp0+JLJl7ZDK0flc2gc+Aw9dpCvIsQ==",
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.6.0.tgz",
"integrity": "sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==",
"dev": true,
"license": "MIT",
"workspaces": [
@@ -2015,9 +2015,9 @@
}
},
"node_modules/expect-type": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
"integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
"integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
@@ -3188,9 +3188,9 @@
}
},
"node_modules/postcss": {
"version": "8.5.15",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
"version": "8.5.16",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz",
"integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==",
"dev": true,
"funding": [
{
@@ -3227,9 +3227,9 @@
}
},
"node_modules/prettier": {
"version": "3.8.4",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.4.tgz",
"integrity": "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==",
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.1.tgz",
"integrity": "sha512-ppiDo2CSwexck1eyZUwJHg/N3nf1+6IRCv7W/VJ5vaLnVCmB7+3CdRfMwoCHBBX6xTrREDTksZ4OZl5SSf4zXA==",
"dev": true,
"license": "MIT",
"bin": {
@@ -3284,13 +3284,17 @@
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz",
"integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 0.6"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/raw-body": {

View File

@@ -3,7 +3,7 @@
"publishConfig": {
"access": "public"
},
"version": "0.51.0",
"version": "0.53.0",
"description": "An ACP-compatible coding agent powered by the Claude Agent SDK (TypeScript)",
"main": "dist/lib.js",
"types": "dist/lib.d.ts",
@@ -64,7 +64,7 @@
},
"dependencies": {
"@agentclientprotocol/sdk": "1.0.0",
"@anthropic-ai/claude-agent-sdk": "0.3.191",
"@anthropic-ai/claude-agent-sdk": "0.3.195",
"zod": "^3.25.0 || ^4.0.0"
},
"devDependencies": {
@@ -74,10 +74,10 @@
"@types/node": "26.0.1",
"@typescript-eslint/eslint-plugin": "8.62.0",
"@typescript-eslint/parser": "8.62.0",
"eslint": "10.5.0",
"eslint": "10.6.0",
"eslint-config-prettier": "10.1.8",
"globals": "17.7.0",
"prettier": "3.8.4",
"prettier": "3.9.1",
"ts-node": "10.9.2",
"typescript": "6.0.3",
"vitest": "4.1.9"

View File

@@ -17,6 +17,7 @@ import {
ListSessionsResponse,
LoadSessionRequest,
LoadSessionResponse,
LogoutRequest,
methods,
ndJsonStream,
NewSessionRequest,
@@ -74,11 +75,13 @@ import {
} from "@anthropic-ai/claude-agent-sdk";
import { ContentBlockParam } from "@anthropic-ai/sdk/resources";
import { BetaContentBlock, BetaRawContentBlockDelta } from "@anthropic-ai/sdk/resources/beta.mjs";
import { execFile } from "node:child_process";
import { randomUUID } from "node:crypto";
import type { Stats } from "node:fs";
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { promisify } from "node:util";
import packageJson from "../package.json" with { type: "json" };
import {
applyAskElicitationResponse,
@@ -109,6 +112,8 @@ import { nodeToWebReadable, nodeToWebWritable, Pushable, unreachable } from "./u
export const CLAUDE_CONFIG_DIR =
process.env.CLAUDE_CONFIG_DIR ?? path.join(os.homedir(), ".claude");
const execFileAsync = promisify(execFile);
const MAX_TITLE_LENGTH = 256;
function sanitizeTitle(text: string): string {
@@ -275,6 +280,14 @@ type Session = {
* `tool_result` time so a long-running session doesn't accumulate every
* tool call for its whole lifetime. */
toolUseCache: ToolUseCache;
/** Tracks which tool_use ids we've already emitted a `tool_call` for, so the
* second source to encounter a tool call sends a `tool_call_update` instead
* of a duplicate `tool_call`. The SDK can invoke `canUseTool` (→ a permission
* request, which emits the tool_call eagerly so the client has it before
* being asked to approve it) either before or after the assistant message's
* tool_use block streams; this set makes the two paths converge regardless of
* order. Pruned at `tool_result` time alongside `toolUseCache`. */
emittedToolCalls: Set<string>;
/** Maps the ACP `messageId` we expose to clients (see `messageIdForGrouping`)
* to the SDK message uuid that the Agent SDK's rewind/resume APIs key on
* (`Query.rewindFiles` takes a user-message uuid; `resumeSessionAt` takes an
@@ -442,8 +455,7 @@ function isMuslLibc(): boolean {
// process.report.getReport().header.glibcVersionRuntime is populated when
// Node is dynamically linked against glibc, and absent on musl.
const report = process.report?.getReport() as
| { header?: { glibcVersionRuntime?: string } }
| undefined;
{ header?: { glibcVersionRuntime?: string } } | undefined;
return !report?.header?.glibcVersionRuntime;
}
@@ -855,6 +867,9 @@ export class ClaudeAcpAgent {
http: true,
sse: true,
},
auth: {
logout: {},
},
loadSession: true,
sessionCapabilities: {
additionalDirectories: {},
@@ -992,6 +1007,30 @@ export class ClaudeAcpAgent {
throw new Error("Method not implemented.");
}
async logout(_params: LogoutRequest): Promise<void> {
// Clear in-memory gateway credentials supplied via `authenticate`. The
// gateway method never touches the on-disk credential store, so dropping
// this reference is the whole logout for that path.
this.gatewayAuthRequest = undefined;
// For the Claude/Console login methods the credentials live in the native
// CLI's store (keychain or config dir), which only the binary can clear.
// `claude auth logout` is non-interactive and idempotent.
const cliPath = await claudeCliPath();
try {
await execFileAsync(cliPath, ["auth", "logout"]);
} catch (error) {
const stderr =
typeof error === "object" && error && "stderr" in error
? String((error as { stderr: unknown }).stderr).trim()
: undefined;
throw RequestError.internalError(
{ stderr: stderr || undefined },
`claude auth logout failed: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
async prompt(params: PromptRequest): Promise<PromptResponse> {
const session = this.sessions[params.sessionId];
if (!session) {
@@ -1566,6 +1605,7 @@ export class ClaudeAcpAgent {
case "api_retry":
case "thinking_tokens":
case "model_refusal_fallback":
case "model_refusal_no_fallback":
// Todo: process via status api: https://docs.claude.com/en/docs/claude-code/hooks#hook-output
break;
default:
@@ -1865,6 +1905,7 @@ export class ClaudeAcpAgent {
clientCapabilities: this.clientCapabilities,
cwd: session.cwd,
taskState: session.taskState,
emittedToolCalls: session.emittedToolCalls,
messageId: currentStreamMessageId,
},
)) {
@@ -2096,6 +2137,7 @@ export class ClaudeAcpAgent {
parentToolUseId: message.parent_tool_use_id,
cwd: session.cwd,
taskState: session.taskState,
emittedToolCalls: session.emittedToolCalls,
messageId: messageIdForGrouping(message),
},
)) {
@@ -2518,8 +2560,21 @@ export class ClaudeAcpAgent {
* longer leaves the `await` hanging. */
private async requestPermissionFromClient(
params: RequestPermissionRequest,
toolName: string,
signal: AbortSignal,
): Promise<RequestPermissionResponse> {
// The SDK may invoke `canUseTool` (and therefore this permission request)
// before the assistant message's tool_use block streams to us. Some ACP clients
// expect the `tool_call` a permission request references to already exist,
// so emit it now if it hasn't been sent yet. The streamed tool_use chunk
// later refines it with a `tool_call_update` rather than emitting a
// duplicate (see `emittedToolCalls` in `toAcpNotifications`).
await this.ensureToolCallEmitted(
params.sessionId,
toolName,
params.toolCall.toolCallId,
params.toolCall.rawInput,
);
try {
return await this.client.requestPermission(params, signal);
} catch (error) {
@@ -2530,6 +2585,39 @@ export class ClaudeAcpAgent {
}
}
/** Emit the `tool_call` a permission request references if it hasn't been sent
* yet, so the client has the tool call before being asked to approve it. The
* matching streamed tool_use chunk later refines it with a `tool_call_update`
* instead of emitting a duplicate (see `emittedToolCalls`). Built via the same
* `toolCallNotification` helper as the streamed path so the two are identical.
* Tools the stream renders as a plan (TodoWrite) or suppresses (Task*) are
* skipped so a permission prompt for them never surfaces a stray tool_call. */
private async ensureToolCallEmitted(
sessionId: string,
toolName: string,
toolCallId: string,
toolInput: unknown,
): Promise<void> {
const session = this.sessions[sessionId];
if (!session || !shouldEmitToolCall(toolName)) {
return;
}
if (session.emittedToolCalls.has(toolCallId)) {
return;
}
session.emittedToolCalls.add(toolCallId);
const supportsTerminalOutput = this.clientCapabilities?._meta?.["terminal_output"] === true;
await this.client.sessionUpdate({
sessionId,
update: toolCallNotification(
{ id: toolCallId, name: toolName, input: toolInput },
toolInput,
supportsTerminalOutput,
session.cwd,
),
});
}
canUseTool(sessionId: string): CanUseTool {
return async (toolName, toolInput, { signal, suggestions, toolUseID }) => {
const alwaysAllowLabel = describeAlwaysAllow(suggestions, toolName);
@@ -2547,6 +2635,9 @@ export class ClaudeAcpAgent {
// than the interactive dialog). Present it as an ACP form elicitation and
// feed the answers back as updatedInput for the tool's own call() to read.
if (toolName === "AskUserQuestion" && this.clientCapabilities?.elicitation?.form) {
// Like permission requests, the elicitation references this toolUseID, so
// make sure the tool_call has surfaced to the client before we send it.
await this.ensureToolCallEmitted(sessionId, toolName, toolUseID, toolInput);
return this.handleAskUserQuestion(sessionId, toolInput, toolUseID, signal);
}
@@ -2591,6 +2682,7 @@ export class ClaudeAcpAgent {
),
},
},
toolName,
signal,
);
@@ -2663,6 +2755,7 @@ export class ClaudeAcpAgent {
),
},
},
toolName,
signal,
);
if (signal.aborted || response.outcome?.outcome === "cancelled") {
@@ -3401,6 +3494,7 @@ export class ClaudeAcpAgent {
) ?? DEFAULT_CONTEXT_WINDOW,
taskState,
toolUseCache: {},
emittedToolCalls: new Set(),
messageIdToUuid: new Map(),
};
@@ -4117,6 +4211,64 @@ function applyMessageId(
}
}
/** Built-in tools that drive the task list (headless/SDK sessions use these
* instead of TodoWrite). Their tool_use/tool_result are surfaced as `plan`
* snapshots rather than as tool_calls. */
function isTaskTool(toolName: string): boolean {
return (
toolName === "TaskCreate" ||
toolName === "TaskUpdate" ||
toolName === "TaskList" ||
toolName === "TaskGet"
);
}
/** Whether a tool's tool_use surfaces to the client as a standalone
* `tool_call`. TodoWrite is rendered as a `plan` and Task* tools are
* suppressed (their plan snapshot is emitted at tool_result time), so neither
* produces a tool_call. */
function shouldEmitToolCall(toolName: string): boolean {
return toolName !== "TodoWrite" && !isTaskTool(toolName);
}
/** Build the `tool_call` (or, with `refine`, the `tool_call_update`)
* notification for a tool_use. Shared by every site that surfaces a tool call:
* the streamed tool_use path (first encounter → tool_call, later encounter →
* refine) and the permission flow (`ensureToolCallEmitted`), so they can't
* drift. The initial `tool_call` carries `status: "pending"` and, for Bash, the
* `terminal_info` _meta that the later `terminal_output`/`terminal_exit`
* updates key off of; a refining `tool_call_update` carries neither. */
function toolCallNotification(
toolUse: { id: string; name: string; input: unknown },
rawInput: unknown,
supportsTerminalOutput: boolean,
cwd?: string,
refine = false,
): SessionNotification["update"] {
if (refine) {
return {
_meta: { claudeCode: { toolName: toolUse.name } } satisfies ToolUpdateMeta,
toolCallId: toolUse.id,
sessionUpdate: "tool_call_update",
rawInput,
...toolInfoFromToolUse(toolUse, supportsTerminalOutput, cwd),
};
}
return {
_meta: {
claudeCode: { toolName: toolUse.name },
...(toolUse.name === "Bash" && supportsTerminalOutput
? { terminal_info: { terminal_id: toolUse.id } }
: {}),
} satisfies ToolUpdateMeta,
toolCallId: toolUse.id,
sessionUpdate: "tool_call",
rawInput,
status: "pending",
...toolInfoFromToolUse(toolUse, supportsTerminalOutput, cwd),
};
}
/**
* Convert an SDKAssistantMessage (Claude) to a SessionNotification (ACP).
* Only handles text, image, and thinking chunks for now.
@@ -4134,6 +4286,13 @@ export function toAcpNotifications(
parentToolUseId?: string | null;
cwd?: string;
taskState?: TaskState;
// Tracks tool_use ids already emitted as a `tool_call` so a permission
// request (which emits the tool_call eagerly) and the streamed tool_use
// chunk don't both emit one — whichever arrives second emits a
// `tool_call_update` instead. Mutated in place. When omitted, the
// tool_call/update decision falls back to `toolUseCache` presence (the
// historical single-source behavior).
emittedToolCalls?: Set<string>;
// Opaque id identifying the message these chunks belong to (ACP message ids
// are opaque strings — no particular format is required). Attached to
// user/agent message and thought chunks so clients can group streamed chunks
@@ -4221,12 +4380,7 @@ export function toAcpNotifications(
entries: planEntries(chunk.input as { todos: ClaudePlanEntry[] }),
};
}
} else if (
chunk.name === "TaskCreate" ||
chunk.name === "TaskUpdate" ||
chunk.name === "TaskList" ||
chunk.name === "TaskGet"
) {
} else if (isTaskTool(chunk.name)) {
// Task* tool_use is suppressed; the plan update is emitted at
// tool_result time once we have the task ID (for TaskCreate) and
// confirmation that the change took effect.
@@ -4279,39 +4433,31 @@ export function toAcpNotifications(
// ignore if we can't turn it to JSON
}
if (alreadyCached) {
// Second encounter (full assistant message after streaming) —
// send as tool_call_update to refine the existing tool_call
// rather than emitting a duplicate tool_call.
update = {
_meta: {
claudeCode: {
toolName: chunk.name,
},
} satisfies ToolUpdateMeta,
toolCallId: chunk.id,
sessionUpdate: "tool_call_update",
// Emit a `tool_call` only the first time this id surfaces to the
// client; afterwards refine it with a `tool_call_update`. The first
// surface may be this stream chunk OR an earlier permission request
// (see `ensureToolCallEmitted`), so emission is tracked separately
// from `toolUseCache`. Without an `emittedToolCalls` set we fall back
// to cache presence — the historical streaming-only behavior.
const emittedToolCalls = options?.emittedToolCalls;
const alreadyEmitted = emittedToolCalls ? emittedToolCalls.has(chunk.id) : alreadyCached;
emittedToolCalls?.add(chunk.id);
if (alreadyEmitted) {
// Already surfaced (full assistant message after streaming, or a
// permission request emitted it first) — refine with a
// tool_call_update rather than emitting a duplicate tool_call.
update = toolCallNotification(
chunk,
rawInput,
...toolInfoFromToolUse(chunk, supportsTerminalOutput, options?.cwd),
};
supportsTerminalOutput,
options?.cwd,
true,
);
} else {
// First encounter (streaming content_block_start or replay) —
// send as tool_call with terminal_info for Bash tools.
update = {
_meta: {
claudeCode: {
toolName: chunk.name,
},
...(chunk.name === "Bash" && supportsTerminalOutput
? { terminal_info: { terminal_id: chunk.id } }
: {}),
} satisfies ToolUpdateMeta,
toolCallId: chunk.id,
sessionUpdate: "tool_call",
rawInput,
status: "pending",
...toolInfoFromToolUse(chunk, supportsTerminalOutput, options?.cwd),
};
// First surface (streaming content_block_start or replay) — send as
// tool_call (with terminal_info for Bash).
update = toolCallNotification(chunk, rawInput, supportsTerminalOutput, options?.cwd);
}
}
break;
@@ -4325,6 +4471,7 @@ export function toAcpNotifications(
case "bash_code_execution_tool_result":
case "text_editor_code_execution_tool_result":
case "mcp_tool_result": {
options?.emittedToolCalls?.delete(chunk.tool_use_id);
const toolUse = toolUseCache[chunk.tool_use_id];
if (!toolUse) {
logger.error(
@@ -4333,12 +4480,7 @@ export function toAcpNotifications(
break;
}
if (
toolUse.name === "TaskCreate" ||
toolUse.name === "TaskUpdate" ||
toolUse.name === "TaskList" ||
toolUse.name === "TaskGet"
) {
if (isTaskTool(toolUse.name)) {
// Headless/SDK sessions emit Task* tools instead of TodoWrite.
// TaskCreate / TaskUpdate mutate the accumulated task list; TaskList
// and TaskGet are read-only so we just suppress their tool_call /
@@ -4458,6 +4600,7 @@ export function streamEventToAcpNotifications(
clientCapabilities?: ClientCapabilities;
cwd?: string;
taskState?: TaskState;
emittedToolCalls?: Set<string>;
messageId?: string;
},
): SessionNotification[] {
@@ -4476,6 +4619,7 @@ export function streamEventToAcpNotifications(
parentToolUseId: message.parent_tool_use_id,
cwd: options?.cwd,
taskState: options?.taskState,
emittedToolCalls: options?.emittedToolCalls,
messageId: options?.messageId,
},
);
@@ -4492,6 +4636,7 @@ export function streamEventToAcpNotifications(
parentToolUseId: message.parent_tool_use_id,
cwd: options?.cwd,
taskState: options?.taskState,
emittedToolCalls: options?.emittedToolCalls,
messageId: options?.messageId,
},
);
@@ -4572,6 +4717,7 @@ export function runAcp() {
agent.setSessionConfigOption(ctx.params),
)
.onRequest(methods.agent.authenticate, (ctx) => agent.authenticate(ctx.params))
.onRequest(methods.agent.logout, (ctx) => agent.logout(ctx.params))
.onRequest(methods.agent.session.prompt, (ctx) =>
runPromptWithCancellation(agent, ctx.params, ctx.signal),
)

View File

@@ -232,8 +232,7 @@ export function askUserQuestionsToCreateRequest(
/** Outcome of an AskUserQuestion elicitation, decoupled from any transport. */
export type AskUserQuestionOutcome =
| { action: "answered"; updatedInput: Record<string, unknown> }
| { action: "cancel" };
{ action: "answered"; updatedInput: Record<string, unknown> } | { action: "cancel" };
/**
* Fold an ACP elicitation response into the AskUserQuestion tool's input.

View File

@@ -4,10 +4,10 @@ import { resolveSettings } from "@anthropic-ai/claude-agent-sdk";
import { claudeCliPath, runAcp } from "./acp-agent.js";
import packageJson from "../package.json" with { type: "json" };
if (process.argv.includes("--version") || process.argv.includes("-v")) {
console.log(packageJson.version);
process.exit(0);
} else if (process.argv.includes("--cli")) {
// `--cli` is checked first so that `--version`/`-v` (and any other flags) are
// forwarded to the wrapped native CLI rather than swallowed by our own version
// handler below. Our version flag only applies when not delegating.
if (process.argv.includes("--cli")) {
const { spawn } = await import("node:child_process");
const args = process.argv.slice(2).filter((arg) => arg !== "--cli");
const child = spawn(await claudeCliPath(), args, { stdio: "inherit" });
@@ -37,6 +37,9 @@ if (process.argv.includes("--version") || process.argv.includes("-v")) {
console.error(err);
process.exit(1);
});
} else if (process.argv.includes("--version") || process.argv.includes("-v")) {
console.log(packageJson.version);
process.exit(0);
} else {
// Apply env vars from the managed-policy tier before any SDK call so the
// SDK subprocess inherits them. Going through resolveSettings (vs. a raw

View File

@@ -118,6 +118,7 @@ function mockSessionState(overrides: Record<string, any> = {}) {
contextWindowSize: 200000,
taskState: new Map(),
toolUseCache: {},
emittedToolCalls: new Set(),
messageIdToUuid: new Map(),
...overrides,
} as any;
@@ -1814,6 +1815,7 @@ describe("permission request cancellation", () => {
contextWindowSize: 200000,
taskState: new Map(),
toolUseCache: {},
emittedToolCalls: new Set(),
messageIdToUuid: new Map(),
} as any;
return agent.sessions[sessionId]!;
@@ -1835,7 +1837,11 @@ describe("permission request cancellation", () => {
},
} as unknown as AcpClient;
const agent = new ClaudeAcpAgent(mockClient, { log: () => {}, error: () => {} });
injectSession(agent, "session-1");
const session = injectSession(agent, "session-1");
// The tool_call was already surfaced by the streamed tool_use chunk, so the
// permission request goes straight to requestPermission without first
// emitting one.
session.emittedToolCalls.add("tool-1");
const controller = new AbortController();
const pending = agent.canUseTool("session-1")("Bash", { command: "ls" }, {
@@ -1872,6 +1878,136 @@ describe("permission request cancellation", () => {
});
});
describe("tool_call emitted before permission request", () => {
// The SDK can invoke canUseTool before the assistant message's tool_use block
// streams to us. ACP clients expect the tool_call a permission request
// references to already exist, so the permission flow emits it eagerly and the
// streamed chunk later refines it with a tool_call_update (deduped via
// session.emittedToolCalls) rather than emitting a duplicate.
function setup(overrides: Record<string, any> = {}) {
const events: string[] = [];
const updates: SessionNotification[] = [];
const mockClient = {
sessionUpdate: async (n: SessionNotification) => {
events.push(`update:${n.update.sessionUpdate}`);
updates.push(n);
},
requestPermission: async () => {
events.push("permission");
return { outcome: { outcome: "selected", optionId: "allow" } };
},
} as unknown as AcpClient;
const agent = new ClaudeAcpAgent(mockClient, { log: () => {}, error: () => {} });
agent.sessions["session-1"] = mockSessionState(overrides);
return { agent, events, updates, session: agent.sessions["session-1"]! };
}
it("emits the tool_call (then asks permission) when the stream hasn't yet", async () => {
const { agent, events, updates, session } = setup();
const result = await agent.canUseTool("session-1")("Bash", { command: "ls" }, {
signal: new AbortController().signal,
suggestions: [],
toolUseID: "tool-1",
} as any);
// tool_call is sent before the permission request is raised.
expect(events).toEqual(["update:tool_call", "permission"]);
expect(updates[0].update).toMatchObject({
sessionUpdate: "tool_call",
toolCallId: "tool-1",
status: "pending",
});
expect(session.emittedToolCalls.has("tool-1")).toBe(true);
expect(result).toMatchObject({ behavior: "allow" });
});
it("does not re-emit the tool_call when the stream already surfaced it", async () => {
const { agent, events } = setup();
agent.sessions["session-1"]!.emittedToolCalls.add("tool-1");
await agent.canUseTool("session-1")("Bash", { command: "ls" }, {
signal: new AbortController().signal,
suggestions: [],
toolUseID: "tool-1",
} as any);
expect(events).toEqual(["permission"]);
});
it("refines the eagerly-emitted tool_call with a tool_call_update when the chunk streams", () => {
const { session } = setup();
// Permission flow already emitted the tool_call for this id.
session.emittedToolCalls.add("tool-1");
const notifications = toAcpNotifications(
[{ type: "tool_use", id: "tool-1", name: "Bash", input: { command: "ls" } }],
"assistant",
"session-1",
session.toolUseCache,
{} as AcpClient,
console,
{ emittedToolCalls: session.emittedToolCalls },
);
expect(notifications).toHaveLength(1);
expect(notifications[0].update.sessionUpdate).toBe("tool_call_update");
});
it("does not emit a tool_call for suppressed tools (TodoWrite) on a permission request", async () => {
const { agent, events, session } = setup();
await agent.canUseTool("session-1")(
"TodoWrite",
{ todos: [{ content: "x", status: "pending" }] },
{ signal: new AbortController().signal, suggestions: [], toolUseID: "todo-1" } as any,
);
expect(events).toEqual(["permission"]);
expect(session.emittedToolCalls.has("todo-1")).toBe(false);
});
it("includes Bash terminal_info _meta in the eager tool_call so terminal output can attach", async () => {
const { agent, updates } = setup();
// Terminal-capable client (e.g. Zed). The eager tool_call must carry
// terminal_info.terminal_id, otherwise the later terminal_output/terminal_exit
// updates (keyed by terminal_id) have nothing to attach to.
(agent as any).clientCapabilities = { _meta: { terminal_output: true } };
await agent.canUseTool("session-1")("Bash", { command: "ls" }, {
signal: new AbortController().signal,
suggestions: [],
toolUseID: "tool-1",
} as any);
expect(updates[0].update).toMatchObject({
sessionUpdate: "tool_call",
toolCallId: "tool-1",
_meta: { terminal_info: { terminal_id: "tool-1" } },
});
});
it("prunes the emission marker on a tool_result even when the tool_use was never cached", () => {
const { session } = setup();
// Eager-emitted via the permission flow, but the tool_use chunk never
// streamed (e.g. cancelled), so toolUseCache has no entry for it.
session.emittedToolCalls.add("tool-1");
toAcpNotifications(
[{ type: "tool_result", tool_use_id: "tool-1", content: [{ type: "text", text: "x" }] }],
"user",
"session-1",
session.toolUseCache,
{} as AcpClient,
// Silence the expected "tool result for tool use that wasn't tracked" log.
{ log: () => {}, error: () => {} },
{ emittedToolCalls: session.emittedToolCalls },
);
expect(session.emittedToolCalls.has("tool-1")).toBe(false);
});
});
describe("runPromptWithCancellation", () => {
function deferred<T>() {
let resolve!: (value: T) => void;
@@ -2523,6 +2659,24 @@ describe("stop reason propagation", () => {
});
});
describe("logout", () => {
function createMockAgent() {
const mockClient = {
sessionUpdate: async () => {},
} as unknown as AcpClient;
return new ClaudeAcpAgent(mockClient, { log: () => {}, error: () => {} });
}
it("advertises the logout capability during initialize", async () => {
const agent = createMockAgent();
const response = await agent.initialize({
protocolVersion: 1,
clientCapabilities: {},
});
expect(response.agentCapabilities?.auth?.logout).toEqual({});
});
});
describe("session/close", () => {
function createMockAgent() {
const mockClient = {
@@ -2564,6 +2718,7 @@ describe("session/close", () => {
contextWindowSize: 200000,
taskState: new Map(),
toolUseCache: {},
emittedToolCalls: new Set(),
messageIdToUuid: new Map(),
};
return agent.sessions[sessionId]!;
@@ -2649,6 +2804,7 @@ describe("session/delete", () => {
contextWindowSize: 200000,
taskState: new Map(),
toolUseCache: {},
emittedToolCalls: new Set(),
messageIdToUuid: new Map(),
};
return agent.sessions[sessionId]!;
@@ -2751,6 +2907,7 @@ describe("getOrCreateSession param change detection", () => {
contextWindowSize: 200000,
taskState: new Map(),
toolUseCache: {},
emittedToolCalls: new Set(),
messageIdToUuid: new Map(),
};
return agent.sessions[sessionId]!;
@@ -4965,6 +5122,7 @@ describe("post-error recovery", () => {
contextWindowSize: 200000,
taskState: new Map(),
toolUseCache: {},
emittedToolCalls: new Set(),
messageIdToUuid: new Map(),
};
return { interrupt };
@@ -5535,6 +5693,7 @@ describe("session/cancel wedge recovery (issue #680)", () => {
contextWindowSize: 200000,
taskState: new Map(),
toolUseCache: {},
emittedToolCalls: new Set(),
messageIdToUuid: new Map(),
};
return { interrupt };
@@ -5988,6 +6147,7 @@ describe("agent selection config option", () => {
contextWindowSize: 200000,
taskState: new Map(),
toolUseCache: {},
emittedToolCalls: new Set(),
messageIdToUuid: new Map(),
};
return { session: agent.sessions[sessionId]!, applyFlagSettings };

View File

@@ -113,17 +113,17 @@ describe("session config options", () => {
settingsManager: {},
modes: structuredClone(MOCK_MODES),
models: structuredClone(MOCK_MODELS),
modelInfos: MOCK_MODELS.availableModels.map(
(m): ModelInfo => ({
value: m.modelId,
displayName: m.name,
description: m.description,
supportsEffort: true,
supportedEffortLevels: ["low", "medium", "high"],
}),
),
modelInfos: MOCK_MODELS.availableModels.map((m): ModelInfo => ({
value: m.modelId,
displayName: m.name,
description: m.description,
supportsEffort: true,
supportedEffortLevels: ["low", "medium", "high"],
})),
configOptions: structuredClone(MOCK_CONFIG_OPTIONS),
contextWindowSize: 200000,
toolUseCache: {},
emittedToolCalls: new Set(),
};
}
@@ -1113,6 +1113,10 @@ describe("session config options", () => {
],
};
// The tool_call was already surfaced (by the streamed tool_use chunk), so
// the permission request won't re-emit one — keep this focused on options.
session.emittedToolCalls.add("toolu_1");
const canUseTool = (agent as any).canUseTool(SESSION_ID);
const signal = new AbortController().signal;
try {
@@ -1144,6 +1148,10 @@ describe("session config options", () => {
],
};
permissionResponse = { outcome: { outcome: "selected", optionId: "auto" } };
// The tool_call was already surfaced (by the streamed tool_use chunk), so
// the permission request won't re-emit one — the deny path below should
// produce no session updates at all.
session.emittedToolCalls.add("toolu_2");
const canUseTool = (agent as any).canUseTool(SESSION_ID);
const result = await canUseTool(
@@ -1172,6 +1180,10 @@ describe("session config options", () => {
],
};
// The tool_call was already surfaced (by the streamed tool_use chunk), so
// the permission request won't re-emit one — keep this focused on options.
session.emittedToolCalls.add("toolu_3");
const canUseTool = (agent as any).canUseTool(SESSION_ID);
const signal = new AbortController().signal;
try {