Compare commits

..

7 Commits

Author SHA1 Message Date
acp-release-bot[bot]
0a88de2a83 chore(main): release 0.60.0 (#884)
🤖 I have created a release *beep* *boop*
---


##
[0.60.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.59.0...v0.60.0)
(2026-07-20)


### Features

* **deps:** Update to claude-agent-sdk 0.3.215
([#890](https://github.com/agentclientprotocol/claude-agent-acp/issues/890))
([92548f0](92548f0435))
* implement configurable LLM providers
([#853](https://github.com/agentclientprotocol/claude-agent-acp/issues/853))
([82cd692](82cd692e50))


### Bug Fixes

* parse Agent/Task trailers without regex
([#879](https://github.com/agentclientprotocol/claude-agent-acp/issues/879))
([06c3d7b](06c3d7bdbd))
* remove ~15s stall on session/new and model switch by seeding the
context window synchronously
([#894](https://github.com/agentclientprotocol/claude-agent-acp/issues/894))
([ff9b96d](ff9b96d462))
* Silence missing PostToolUse callbacks
([#895](https://github.com/agentclientprotocol/claude-agent-acp/issues/895))
([1887ada](1887ada215)),
closes
[#889](https://github.com/agentclientprotocol/claude-agent-acp/issues/889)

---
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-07-20 14:51:04 +02:00
Luca Dibattista
ff9b96d462 fix: remove ~15s stall on session/new and model switch by seeding the context window synchronously (#894)
## Summary

`session/new` and switching models via `configOptions` regressed to ~15s
each in v0.59.0. This removes the blocking call responsible and seeds
the context window synchronously instead, restoring sub-second behavior
on both paths.

## Root cause

v0.59.0 added an inline `await query.getContextUsage()` on the
session-creation and model-switch paths. That SDK control request is not
serviced by the CLI until the session's first prompt turn has run —
before then it stalls ~15s. And because SDK control requests are
serialized over a single stdin/stdout channel, an awaited `setModel`
queued behind the wedged `getContextUsage` inherited the full stall. So
both paths blocked ~15s before returning.

Verified with the real CLI (setModel alone pre-prompt: ~1ms;
getContextUsage alone pre-prompt: ~16s; getContextUsage-then-setModel:
both resolve together at ~16s, confirming the serialization).

## Fix

Drop `getContextUsage` from both hot paths. The context window is seeded
synchronously by `immediateContextWindow(...)`:

1. an authoritative value from a per-(provider, model) cache, if
learned; else
2. a synchronous text heuristic (`\b1m\b` → 1,000,000); else
3. `DEFAULT_CONTEXT_WINDOW` (200,000).

The authoritative window is learned the moment it's actually available —
from each turn's `result.modelUsage` — and written into the cache, so
later sessions/switches to the same model seed the correct window
immediately with no IPC. A model that has never run a turn shows the
heuristic/default window until its first response, matching pre-0.59.0
behavior.

### Cache design

- **Keyed on `(provider, resolved model id)`.** The resolved id
(`ModelInfo.resolvedModel`, e.g. `claude-sonnet-5[1m]`) matches the
`result.modelUsage` keys, so the write key and the read keys line up.
Keying on the resolved id (not the picker value) dedups aliases that
resolve to the same model; keying on the provider keeps two providers
that reuse an id string for different windows from crossing wires
(per-session provider config, #853). The provider is captured at session
creation, since the effective provider can change process-wide over the
adapter's lifetime.
- **Non-positive/NaN windows are ignored** — they neither overwrite the
session's learned window nor enter the cache.

## Impact (measured, real CLI)

| Path | Before | After |
| --- | --- | --- |
| `session/new` | ~17.4s | ~0.9s |
| model switch (before first prompt) | ~15s | ~3ms |
| model switch (after a turn) | ~14.7s | <1ms |

## Tests

- New end-to-end cache test: a turn's authoritative window (learned
under the resolved id even when the assistant message reports the bare
id) is served on a later switch with no `getContextUsage`.
- New provider-isolation test: the same resolved id on a different
provider does not read another provider's cached window; the same
provider does.
- New non-positive-window test: a nonsensical `contextWindow` is ignored
and never poisons the cache.
- Rewrote the existing context-window tests to the new contract (assert
no `getContextUsage` on session/new and switch; window from
cache/inference/default).

Closes #886
Closes #880

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2026-07-20 12:48:40 +00:00
Ben Brandt
1887ada215 fix: Silence missing PostToolUse callbacks (#895)
Closes #889
2026-07-20 11:43:08 +00:00
José Gurruchaga
06c3d7bdbd fix: parse Agent/Task trailers without regex (#879)
## Summary

Closes:
https://github.com/agentclientprotocol/claude-agent-acp/issues/878
Fixes the potential ReDoS warning reported by CodeQL in
`stripAgentTrailer()`.

- Replaces regex-based trailer stripping with bounded linear parsing
- Preserves valid `<usage>…</usage>` and `agentId: … (…)` trailer
handling
- Adds coverage for malformed trailers, which are left unchanged

## Testing

- `npm test -- --run src/tests/tools.test.ts`

---------

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 11:21:52 +00:00
dependabot[bot]
92548f0435 feat(deps): Update to claude-agent-sdk 0.3.215 (#890)
Bumps the minor group with 7 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
|
[@anthropic-ai/claude-agent-sdk](https://github.com/anthropics/claude-agent-sdk-typescript)
| `0.3.207` | `0.3.212` |
|
[@anthropic-ai/sdk](https://github.com/anthropics/anthropic-sdk-typescript)
| `0.111.0` | `0.112.1` |
|
[@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin)
| `8.63.0` | `8.64.0` |
|
[@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser)
| `8.63.0` | `8.64.0` |
|
[express-rate-limit](https://github.com/express-rate-limit/express-rate-limit)
| `8.5.2` | `8.6.0` |
| [obug](https://github.com/sxzz/obug) | `2.1.3` | `2.1.4` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) |
`8.1.4` | `8.1.5` |


Updates `@anthropic-ai/claude-agent-sdk` from 0.3.207 to 0.3.212
<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.212</h2>
<h2>What's changed</h2>
<ul>
<li>Fixed dash-leading <code>resumeSessionAt</code> and
<code>sessionId</code> values being passed to the CLI as separate argv
tokens; both now use equals-form (<code>--flag=value</code>)</li>
<li>Agent tool output now includes the resolved model when a mid-turn
model swap changed the subagent's model</li>
</ul>
<h2>Update</h2>
<pre lang="sh"><code>npm install @anthropic-ai/claude-agent-sdk@0.3.212
# or
yarn add @anthropic-ai/claude-agent-sdk@0.3.212
# or
pnpm add @anthropic-ai/claude-agent-sdk@0.3.212
# or
bun add @anthropic-ai/claude-agent-sdk@0.3.212
</code></pre>
<h2>v0.3.211</h2>
<h2>What's changed</h2>
<ul>
<li>Fixed <code>--replay-user-messages</code> with
<code>--include-partial-messages</code> emitting the turn-start user
replay after the first content block instead of before the turn's
content events</li>
<li>Added <code>SDKAssistantMessage.timestamp</code> (ISO-8601) to the
live stream, matching <code>SDKUserMessage</code>; older emitters omit
it, consumers should fall back to receive time</li>
<li>Added rate-limit message prefix buckets
(<code>USAGE_LIMIT_ERROR_PREFIXES</code> and siblings) as
<code>@alpha</code> exports for classifying rate-limit messages without
hand-mirrored lists</li>
<li>Improved process-exit errors to include the CLI's stderr output, so
a failed child reports its actual cause instead of only an exit
code</li>
</ul>
<h2>Update</h2>
<pre lang="sh"><code>npm install @anthropic-ai/claude-agent-sdk@0.3.211
# or
yarn add @anthropic-ai/claude-agent-sdk@0.3.211
# or
pnpm add @anthropic-ai/claude-agent-sdk@0.3.211
# or
bun add @anthropic-ai/claude-agent-sdk@0.3.211
</code></pre>
<h2>v0.3.210</h2>
<h2>What's changed</h2>
<ul>
<li>Added <code>timedOutAfterMs</code> to <code>BashToolOutput</code>,
set when a command is auto-backgrounded on timeout</li>
</ul>
<h2>Update</h2>
<pre lang="sh"><code>npm install @anthropic-ai/claude-agent-sdk@0.3.210
# or
yarn add @anthropic-ai/claude-agent-sdk@0.3.210
# or
&lt;/tr&gt;&lt;/table&gt; 
</code></pre>
</blockquote>
<p>... (truncated)</p>
</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.212</h2>
<ul>
<li>Fixed dash-leading <code>resumeSessionAt</code> and
<code>sessionId</code> values being passed to the CLI as separate argv
tokens; both now use equals-form (<code>--flag=value</code>)</li>
<li>Agent tool output now includes the resolved model when a mid-turn
model swap changed the subagent's model</li>
</ul>
<h2>0.3.211</h2>
<ul>
<li>Fixed <code>--replay-user-messages</code> with
<code>--include-partial-messages</code> emitting the turn-start user
replay after the first content block instead of before the turn's
content events</li>
<li>Added <code>SDKAssistantMessage.timestamp</code> (ISO-8601) to the
live stream, matching <code>SDKUserMessage</code>; older emitters omit
it, consumers should fall back to receive time</li>
<li>Added rate-limit message prefix buckets
(<code>USAGE_LIMIT_ERROR_PREFIXES</code> and siblings) as
<code>@alpha</code> exports for classifying rate-limit messages without
hand-mirrored lists</li>
<li>Improved process-exit errors to include the CLI's stderr output, so
a failed child reports its actual cause instead of only an exit
code</li>
</ul>
<h2>0.3.210</h2>
<ul>
<li>Added <code>timedOutAfterMs</code> to <code>BashToolOutput</code>,
set when a command is auto-backgrounded on timeout</li>
</ul>
<h2>0.3.209</h2>
<ul>
<li>Updated to parity with Claude Code v2.1.209</li>
</ul>
<h2>0.3.208</h2>
<ul>
<li>Fixed a caller abort during a pending SDK hook callback being
converted into hook success, which let PreToolUse-gated tools execute
after the abort</li>
<li>Fixed a per-query resource leak in the SDK's process tracking when
spawning the CLI fails (nonexistent or inaccessible executable
path)</li>
<li>Fixed an SDK <code>UserPromptSubmit</code> hook callback exceeding
its timeout killing the entire query with an empty error; it now blocks
the prompt with a clear timeout message and the session continues</li>
<li>Fixed <code>extraArgs</code> values that look like flags (e.g.
<code>resume: '--version'</code>) being parsed as their own CLI flags;
dash-leading values are now bound with equals-form argv</li>
<li>Fixed an abort-listener leak: streaming queries sharing one
<code>AbortController</code> no longer accumulate <code>abort</code>
listeners on its signal after each completed query</li>
<li>Fixed <code>createSdkMcpServer</code> docs pointing at a nonexistent
env var; the MCP tool-call timeout knob is
<code>MCP_TOOL_TIMEOUT</code></li>
<li>Fixed an uncaught exception when writing to stdin after the Claude
Code subprocess has exited</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="f1206afbf0"><code>f1206af</code></a>
chore: Update CHANGELOG.md</li>
<li><a
href="d7f244a55e"><code>d7f244a</code></a>
chore: Update CHANGELOG.md</li>
<li><a
href="018efaa0c3"><code>018efaa</code></a>
chore: Update CHANGELOG.md</li>
<li><a
href="d9eca208ac"><code>d9eca20</code></a>
chore: Update CHANGELOG.md</li>
<li><a
href="34426a18f0"><code>34426a1</code></a>
Escape untrusted issue fields in the Slack notification workflow (<a
href="https://redirect.github.com/anthropics/claude-agent-sdk-typescript/issues/375">#375</a>)</li>
<li><a
href="96e19a548a"><code>96e19a5</code></a>
chore: Update CHANGELOG.md</li>
<li><a
href="02b0475071"><code>02b0475</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.207...v0.3.212">compare
view</a></li>
</ul>
</details>
<br />

Updates `@anthropic-ai/sdk` from 0.111.0 to 0.112.1
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/anthropics/anthropic-sdk-typescript/releases">@​anthropic-ai/sdk's
releases</a>.</em></p>
<blockquote>
<h2>sdk: v0.112.1</h2>
<h2>0.112.1 (2026-07-16)</h2>
<p>Full Changelog: <a
href="https://github.com/anthropics/anthropic-sdk-typescript/compare/sdk-v0.112.0...sdk-v0.112.1">sdk-v0.112.0...sdk-v0.112.1</a></p>
<h2>sdk: v0.112.0</h2>
<h2>0.112.0 (2026-07-16)</h2>
<p>Full Changelog: <a
href="https://github.com/anthropics/anthropic-sdk-typescript/compare/sdk-v0.111.0...sdk-v0.112.0">sdk-v0.111.0...sdk-v0.112.0</a></p>
<h3>Features</h3>
<ul>
<li><strong>api:</strong> add support for MCP Tunnels (<a
href="597ac4181f">597ac41</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/anthropics/anthropic-sdk-typescript/blob/main/CHANGELOG.md">@​anthropic-ai/sdk's
changelog</a>.</em></p>
<blockquote>
<h2>0.112.1 (2026-07-16)</h2>
<p>Full Changelog: <a
href="https://github.com/anthropics/anthropic-sdk-typescript/compare/sdk-v0.112.0...sdk-v0.112.1">sdk-v0.112.0...sdk-v0.112.1</a></p>
<h2>0.112.0 (2026-07-16)</h2>
<p>Full Changelog: <a
href="https://github.com/anthropics/anthropic-sdk-typescript/compare/sdk-v0.111.0...sdk-v0.112.0">sdk-v0.111.0...sdk-v0.112.0</a></p>
<h3>Features</h3>
<ul>
<li><strong>api:</strong> add support for MCP Tunnels (<a
href="597ac4181f">597ac41</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="dd61964bbe"><code>dd61964</code></a>
chore: release main</li>
<li><a
href="b48ac4b46a"><code>b48ac4b</code></a>
ci: force version bump (<a
href="https://redirect.github.com/anthropics/anthropic-sdk-typescript/issues/225">#225</a>)</li>
<li><a
href="4aac845a7a"><code>4aac845</code></a>
chore: release main</li>
<li><a
href="8db6f8449c"><code>8db6f84</code></a>
ci: Update readme to force release (<a
href="https://redirect.github.com/anthropics/anthropic-sdk-typescript/issues/224">#224</a>)</li>
<li><a
href="d4aa171e5e"><code>d4aa171</code></a>
chore: release main</li>
<li><a
href="bc2d2674b3"><code>bc2d267</code></a>
codegen metadata</li>
<li><a
href="d05ddf6961"><code>d05ddf6</code></a>
feat(api): add support for MCP Tunnels</li>
<li>See full diff in <a
href="https://github.com/anthropics/anthropic-sdk-typescript/compare/sdk-v0.111.0...sdk-v0.112.1">compare
view</a></li>
</ul>
</details>
<br />

Updates `@typescript-eslint/eslint-plugin` from 8.63.0 to 8.64.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/typescript-eslint/typescript-eslint/releases">@​typescript-eslint/eslint-plugin's
releases</a>.</em></p>
<blockquote>
<h2>v8.64.0</h2>
<h2>8.64.0 (2026-07-13)</h2>
<h3>🚀 Features</h3>
<ul>
<li>support parsing <code>import defer</code> (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12513">#12513</a>)</li>
<li><strong>eslint-plugin:</strong> [no-loop-func] support
<code>using</code> / <code>await using</code> declarations and deprecate
the rule (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12500">#12500</a>)</li>
<li><strong>typescript-estree:</strong> throw for invalid definite
assignment in class properties (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12543">#12543</a>)</li>
</ul>
<h3>🩹 Fixes</h3>
<ul>
<li><strong>eslint-plugin:</strong> [require-array-sort-compare] handle
constrained arrays (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12512">#12512</a>)</li>
</ul>
<h3>❤️ Thank You</h3>
<ul>
<li>Evyatar Daud <a
href="https://github.com/StyleShit"><code>@​StyleShit</code></a></li>
<li>송재욱</li>
</ul>
<p>See <a
href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.64.0">GitHub
Releases</a> for more information.</p>
<p>You can read about our <a
href="https://typescript-eslint.io/users/versioning">versioning
strategy</a> and <a
href="https://typescript-eslint.io/users/releases">releases</a> on our
website.</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md">@​typescript-eslint/eslint-plugin's
changelog</a>.</em></p>
<blockquote>
<h2>8.64.0 (2026-07-13)</h2>
<h3>🚀 Features</h3>
<ul>
<li><strong>eslint-plugin:</strong> [no-loop-func] support
<code>using</code> / <code>await using</code> declarations and deprecate
the rule (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12500">#12500</a>)</li>
<li><strong>typescript-estree:</strong> throw for invalid definite
assignment in class properties (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12543">#12543</a>)</li>
</ul>
<h3>🩹 Fixes</h3>
<ul>
<li><strong>eslint-plugin:</strong> [require-array-sort-compare] handle
constrained arrays (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12512">#12512</a>)</li>
</ul>
<h3>❤️ Thank You</h3>
<ul>
<li>Evyatar Daud <a
href="https://github.com/StyleShit"><code>@​StyleShit</code></a></li>
<li>송재욱</li>
</ul>
<p>See <a
href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.64.0">GitHub
Releases</a> for more information.</p>
<p>You can read about our <a
href="https://typescript-eslint.io/users/versioning">versioning
strategy</a> and <a
href="https://typescript-eslint.io/users/releases">releases</a> on our
website.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="414d9abbf6"><code>414d9ab</code></a>
chore(release): publish 8.64.0</li>
<li><a
href="bcfe16fd2c"><code>bcfe16f</code></a>
feat(eslint-plugin): [no-loop-func] support <code>using</code> /
<code>await using</code> declarati...</li>
<li><a
href="5b5bfdeb8f"><code>5b5bfde</code></a>
fix(eslint-plugin): [require-array-sort-compare] handle constrained
arrays (#...</li>
<li><a
href="321856cdad"><code>321856c</code></a>
chore: cleanup cspell config (<a
href="https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin/issues/12526">#12526</a>)</li>
<li><a
href="b418cf577f"><code>b418cf5</code></a>
feat(typescript-estree): throw for invalid definite assignment in class
prope...</li>
<li><a
href="737de2b569"><code>737de2b</code></a>
docs: align unified-signatures correct example parameter (<a
href="https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin/issues/12506">#12506</a>)</li>
<li><a
href="a63c17ab74"><code>a63c17a</code></a>
docs: fix shadowed parameter name in unified-signatures example (<a
href="https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin/issues/12502">#12502</a>)</li>
<li>See full diff in <a
href="https://github.com/typescript-eslint/typescript-eslint/commits/v8.64.0/packages/eslint-plugin">compare
view</a></li>
</ul>
</details>
<br />

Updates `@typescript-eslint/parser` from 8.63.0 to 8.64.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/typescript-eslint/typescript-eslint/releases">@​typescript-eslint/parser's
releases</a>.</em></p>
<blockquote>
<h2>v8.64.0</h2>
<h2>8.64.0 (2026-07-13)</h2>
<h3>🚀 Features</h3>
<ul>
<li>support parsing <code>import defer</code> (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12513">#12513</a>)</li>
<li><strong>eslint-plugin:</strong> [no-loop-func] support
<code>using</code> / <code>await using</code> declarations and deprecate
the rule (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12500">#12500</a>)</li>
<li><strong>typescript-estree:</strong> throw for invalid definite
assignment in class properties (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12543">#12543</a>)</li>
</ul>
<h3>🩹 Fixes</h3>
<ul>
<li><strong>eslint-plugin:</strong> [require-array-sort-compare] handle
constrained arrays (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12512">#12512</a>)</li>
</ul>
<h3>❤️ Thank You</h3>
<ul>
<li>Evyatar Daud <a
href="https://github.com/StyleShit"><code>@​StyleShit</code></a></li>
<li>송재욱</li>
</ul>
<p>See <a
href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.64.0">GitHub
Releases</a> for more information.</p>
<p>You can read about our <a
href="https://typescript-eslint.io/users/versioning">versioning
strategy</a> and <a
href="https://typescript-eslint.io/users/releases">releases</a> on our
website.</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md">@​typescript-eslint/parser's
changelog</a>.</em></p>
<blockquote>
<h2>8.64.0 (2026-07-13)</h2>
<p>This was a version bump only for parser to align it with other
projects, there were no code changes.</p>
<p>See <a
href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.64.0">GitHub
Releases</a> for more information.</p>
<p>You can read about our <a
href="https://typescript-eslint.io/users/versioning">versioning
strategy</a> and <a
href="https://typescript-eslint.io/users/releases">releases</a> on our
website.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="414d9abbf6"><code>414d9ab</code></a>
chore(release): publish 8.64.0</li>
<li>See full diff in <a
href="https://github.com/typescript-eslint/typescript-eslint/commits/v8.64.0/packages/parser">compare
view</a></li>
</ul>
</details>
<br />

Updates `@anthropic-ai/claude-agent-sdk-darwin-arm64` from 0.3.207 to
0.3.212

Updates `@anthropic-ai/claude-agent-sdk-darwin-x64` from 0.3.207 to
0.3.212

Updates `@anthropic-ai/claude-agent-sdk-linux-arm64` from 0.3.207 to
0.3.212

Updates `@anthropic-ai/claude-agent-sdk-linux-arm64-musl` from 0.3.207
to 0.3.212

Updates `@anthropic-ai/claude-agent-sdk-linux-x64` from 0.3.207 to
0.3.212

Updates `@anthropic-ai/claude-agent-sdk-linux-x64-musl` from 0.3.207 to
0.3.212

Updates `@anthropic-ai/claude-agent-sdk-win32-arm64` from 0.3.207 to
0.3.212

Updates `@anthropic-ai/claude-agent-sdk-win32-x64` from 0.3.207 to
0.3.212

Updates `@typescript-eslint/project-service` from 8.63.0 to 8.64.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/typescript-eslint/typescript-eslint/releases">@​typescript-eslint/project-service's
releases</a>.</em></p>
<blockquote>
<h2>v8.64.0</h2>
<h2>8.64.0 (2026-07-13)</h2>
<h3>🚀 Features</h3>
<ul>
<li>support parsing <code>import defer</code> (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12513">#12513</a>)</li>
<li><strong>eslint-plugin:</strong> [no-loop-func] support
<code>using</code> / <code>await using</code> declarations and deprecate
the rule (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12500">#12500</a>)</li>
<li><strong>typescript-estree:</strong> throw for invalid definite
assignment in class properties (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12543">#12543</a>)</li>
</ul>
<h3>🩹 Fixes</h3>
<ul>
<li><strong>eslint-plugin:</strong> [require-array-sort-compare] handle
constrained arrays (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12512">#12512</a>)</li>
</ul>
<h3>❤️ Thank You</h3>
<ul>
<li>Evyatar Daud <a
href="https://github.com/StyleShit"><code>@​StyleShit</code></a></li>
<li>송재욱</li>
</ul>
<p>See <a
href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.64.0">GitHub
Releases</a> for more information.</p>
<p>You can read about our <a
href="https://typescript-eslint.io/users/versioning">versioning
strategy</a> and <a
href="https://typescript-eslint.io/users/releases">releases</a> on our
website.</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/project-service/CHANGELOG.md">@​typescript-eslint/project-service's
changelog</a>.</em></p>
<blockquote>
<h2>8.64.0 (2026-07-13)</h2>
<p>This was a version bump only for project-service to align it with
other projects, there were no code changes.</p>
<p>See <a
href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.64.0">GitHub
Releases</a> for more information.</p>
<p>You can read about our <a
href="https://typescript-eslint.io/users/versioning">versioning
strategy</a> and <a
href="https://typescript-eslint.io/users/releases">releases</a> on our
website.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="414d9abbf6"><code>414d9ab</code></a>
chore(release): publish 8.64.0</li>
<li>See full diff in <a
href="https://github.com/typescript-eslint/typescript-eslint/commits/v8.64.0/packages/project-service">compare
view</a></li>
</ul>
</details>
<br />

Updates `@typescript-eslint/scope-manager` from 8.63.0 to 8.64.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/typescript-eslint/typescript-eslint/releases">@​typescript-eslint/scope-manager's
releases</a>.</em></p>
<blockquote>
<h2>v8.64.0</h2>
<h2>8.64.0 (2026-07-13)</h2>
<h3>🚀 Features</h3>
<ul>
<li>support parsing <code>import defer</code> (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12513">#12513</a>)</li>
<li><strong>eslint-plugin:</strong> [no-loop-func] support
<code>using</code> / <code>await using</code> declarations and deprecate
the rule (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12500">#12500</a>)</li>
<li><strong>typescript-estree:</strong> throw for invalid definite
assignment in class properties (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12543">#12543</a>)</li>
</ul>
<h3>🩹 Fixes</h3>
<ul>
<li><strong>eslint-plugin:</strong> [require-array-sort-compare] handle
constrained arrays (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12512">#12512</a>)</li>
</ul>
<h3>❤️ Thank You</h3>
<ul>
<li>Evyatar Daud <a
href="https://github.com/StyleShit"><code>@​StyleShit</code></a></li>
<li>송재욱</li>
</ul>
<p>See <a
href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.64.0">GitHub
Releases</a> for more information.</p>
<p>You can read about our <a
href="https://typescript-eslint.io/users/versioning">versioning
strategy</a> and <a
href="https://typescript-eslint.io/users/releases">releases</a> on our
website.</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/scope-manager/CHANGELOG.md">@​typescript-eslint/scope-manager's
changelog</a>.</em></p>
<blockquote>
<h2>8.64.0 (2026-07-13)</h2>
<h3>🚀 Features</h3>
<ul>
<li>support parsing <code>import defer</code> (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12513">#12513</a>)</li>
</ul>
<h3>❤️ Thank You</h3>
<ul>
<li>Evyatar Daud <a
href="https://github.com/StyleShit"><code>@​StyleShit</code></a></li>
</ul>
<p>See <a
href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.64.0">GitHub
Releases</a> for more information.</p>
<p>You can read about our <a
href="https://typescript-eslint.io/users/versioning">versioning
strategy</a> and <a
href="https://typescript-eslint.io/users/releases">releases</a> on our
website.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="414d9abbf6"><code>414d9ab</code></a>
chore(release): publish 8.64.0</li>
<li><a
href="07af21b912"><code>07af21b</code></a>
feat: support parsing <code>import defer</code> (<a
href="https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/scope-manager/issues/12513">#12513</a>)</li>
<li>See full diff in <a
href="https://github.com/typescript-eslint/typescript-eslint/commits/v8.64.0/packages/scope-manager">compare
view</a></li>
</ul>
</details>
<br />

Updates `@typescript-eslint/tsconfig-utils` from 8.63.0 to 8.64.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/typescript-eslint/typescript-eslint/releases">@​typescript-eslint/tsconfig-utils's
releases</a>.</em></p>
<blockquote>
<h2>v8.64.0</h2>
<h2>8.64.0 (2026-07-13)</h2>
<h3>🚀 Features</h3>
<ul>
<li>support parsing <code>import defer</code> (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12513">#12513</a>)</li>
<li><strong>eslint-plugin:</strong> [no-loop-func] support
<code>using</code> / <code>await using</code> declarations and deprecate
the rule (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12500">#12500</a>)</li>
<li><strong>typescript-estree:</strong> throw for invalid definite
assignment in class properties (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12543">#12543</a>)</li>
</ul>
<h3>🩹 Fixes</h3>
<ul>
<li><strong>eslint-plugin:</strong> [require-array-sort-compare] handle
constrained arrays (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12512">#12512</a>)</li>
</ul>
<h3>❤️ Thank You</h3>
<ul>
<li>Evyatar Daud <a
href="https://github.com/StyleShit"><code>@​StyleShit</code></a></li>
<li>송재욱</li>
</ul>
<p>See <a
href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.64.0">GitHub
Releases</a> for more information.</p>
<p>You can read about our <a
href="https://typescript-eslint.io/users/versioning">versioning
strategy</a> and <a
href="https://typescript-eslint.io/users/releases">releases</a> on our
website.</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/tsconfig-utils/CHANGELOG.md">@​typescript-eslint/tsconfig-utils's
changelog</a>.</em></p>
<blockquote>
<h2>8.64.0 (2026-07-13)</h2>
<p>This was a version bump only for tsconfig-utils to align it with
other projects, there were no code changes.</p>
<p>See <a
href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.64.0">GitHub
Releases</a> for more information.</p>
<p>You can read about our <a
href="https://typescript-eslint.io/users/versioning">versioning
strategy</a> and <a
href="https://typescript-eslint.io/users/releases">releases</a> on our
website.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="414d9abbf6"><code>414d9ab</code></a>
chore(release): publish 8.64.0</li>
<li>See full diff in <a
href="https://github.com/typescript-eslint/typescript-eslint/commits/v8.64.0/packages/tsconfig-utils">compare
view</a></li>
</ul>
</details>
<br />

Updates `@typescript-eslint/type-utils` from 8.63.0 to 8.64.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/typescript-eslint/typescript-eslint/releases">@​typescript-eslint/type-utils's
releases</a>.</em></p>
<blockquote>
<h2>v8.64.0</h2>
<h2>8.64.0 (2026-07-13)</h2>
<h3>🚀 Features</h3>
<ul>
<li>support parsing <code>import defer</code> (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12513">#12513</a>)</li>
<li><strong>eslint-plugin:</strong> [no-loop-func] support
<code>using</code> / <code>await using</code> declarations and deprecate
the rule (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12500">#12500</a>)</li>
<li><strong>typescript-estree:</strong> throw for invalid definite
assignment in class properties (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12543">#12543</a>)</li>
</ul>
<h3>🩹 Fixes</h3>
<ul>
<li><strong>eslint-plugin:</strong> [require-array-sort-compare] handle
constrained arrays (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12512">#12512</a>)</li>
</ul>
<h3>❤️ Thank You</h3>
<ul>
<li>Evyatar Daud <a
href="https://github.com/StyleShit"><code>@​StyleShit</code></a></li>
<li>송재욱</li>
</ul>
<p>See <a
href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.64.0">GitHub
Releases</a> for more information.</p>
<p>You can read about our <a
href="https://typescript-eslint.io/users/versioning">versioning
strategy</a> and <a
href="https://typescript-eslint.io/users/releases">releases</a> on our
website.</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/type-utils/CHANGELOG.md">@​typescript-eslint/type-utils's
changelog</a>.</em></p>
<blockquote>
<h2>8.64.0 (2026-07-13)</h2>
<p>This was a version bump only for type-utils to align it with other
projects, there were no code changes.</p>
<p>See <a
href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.64.0">GitHub
Releases</a> for more information.</p>
<p>You can read about our <a
href="https://typescript-eslint.io/users/versioning">versioning
strategy</a> and <a
href="https://typescript-eslint.io/users/releases">releases</a> on our
website.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="414d9abbf6"><code>414d9ab</code></a>
chore(release): publish 8.64.0</li>
<li>See full diff in <a
href="https://github.com/typescript-eslint/typescript-eslint/commits/v8.64.0/packages/type-utils">compare
view</a></li>
</ul>
</details>
<br />

Updates `@typescript-eslint/types` from 8.63.0 to 8.64.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/typescript-eslint/typescript-eslint/releases">@​typescript-eslint/types's
releases</a>.</em></p>
<blockquote>
<h2>v8.64.0</h2>
<h2>8.64.0 (2026-07-13)</h2>
<h3>🚀 Features</h3>
<ul>
<li>support parsing <code>import defer</code> (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12513">#12513</a>)</li>
<li><strong>eslint-plugin:</strong> [no-loop-func] support
<code>using</code> / <code>await using</code> declarations and deprecate
the rule (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12500">#12500</a>)</li>
<li><strong>typescript-estree:</strong> throw for invalid definite
assignment in class properties (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12543">#12543</a>)</li>
</ul>
<h3>🩹 Fixes</h3>
<ul>
<li><strong>eslint-plugin:</strong> [require-array-sort-compare] handle
constrained arrays (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12512">#12512</a>)</li>
</ul>
<h3>❤️ Thank You</h3>
<ul>
<li>Evyatar Daud <a
href="https://github.com/StyleShit"><code>@​StyleShit</code></a></li>
<li>송재욱</li>
</ul>
<p>See <a
href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.64.0">GitHub
Releases</a> for more information.</p>
<p>You can read about our <a
href="https://typescript-eslint.io/users/versioning">versioning
strategy</a> and <a
href="https://typescript-eslint.io/users/releases">releases</a> on our
website.</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/types/CHANGELOG.md">@​typescript-eslint/types's
changelog</a>.</em></p>
<blockquote>
<h2>8.64.0 (2026-07-13)</h2>
<p>This was a version bump only for types to align it with other
projects, there were no code changes.</p>
<p>See <a
href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.64.0">GitHub
Releases</a> for more information.</p>
<p>You can read about our <a
href="https://typescript-eslint.io/users/versioning">versioning
strategy</a> and <a
href="https://typescript-eslint.io/users/releases">releases</a> on our
website.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="414d9abbf6"><code>414d9ab</code></a>
chore(release): publish 8.64.0</li>
<li>See full diff in <a
href="https://github.com/typescript-eslint/typescript-eslint/commits/v8.64.0/packages/types">compare
view</a></li>
</ul>
</details>
<br />

Updates `@typescript-eslint/typescript-estree` from 8.63.0 to 8.64.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/typescript-eslint/typescript-eslint/releases">@​typescript-eslint/typescript-estree's
releases</a>.</em></p>
<blockquote>
<h2>v8.64.0</h2>
<h2>8.64.0 (2026-07-13)</h2>
<h3>🚀 Features</h3>
<ul>
<li>support parsing <code>import defer</code> (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12513">#12513</a>)</li>
<li><strong>eslint-plugin:</strong> [no-loop-func] support
<code>using</code> / <code>await using</code> declarations and deprecate
the rule (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12500">#12500</a>)</li>
<li><strong>typescript-estree:</strong> throw for invalid definite
assignment in class properties (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12543">#12543</a>)</li>
</ul>
<h3>🩹 Fixes</h3>
<ul>
<li><strong>eslint-plugin:</strong> [require-array-sort-compare] handle
constrained arrays (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12512">#12512</a>)</li>
</ul>
<h3>❤️ Thank You</h3>
<ul>
<li>Evyatar Daud <a
href="https://github.com/StyleShit"><code>@​StyleShit</code></a></li>
<li>송재욱</li>
</ul>
<p>See <a
href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.64.0">GitHub
Releases</a> for more information.</p>
<p>You can read about our <a
href="https://typescript-eslint.io/users/versioning">versioning
strategy</a> and <a
href="https://typescript-eslint.io/users/releases">releases</a> on our
website.</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-estree/CHANGELOG.md">@​typescript-eslint/typescript-estree's
changelog</a>.</em></p>
<blockquote>
<h2>8.64.0 (2026-07-13)</h2>
<h3>🚀 Features</h3>
<ul>
<li><strong>typescript-estree:</strong> throw for invalid definite
assignment in class properties (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12543">#12543</a>)</li>
<li>support parsing <code>import defer</code> (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12513">#12513</a>)</li>
</ul>
<h3>❤️ Thank You</h3>
<ul>
<li>Evyatar Daud <a
href="https://github.com/StyleShit"><code>@​StyleShit</code></a></li>
</ul>
<p>See <a
href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.64.0">GitHub
Releases</a> for more information.</p>
<p>You can read about our <a
href="https://typescript-eslint.io/users/versioning">versioning
strategy</a> and <a
href="https://typescript-eslint.io/users/releases">releases</a> on our
website.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="414d9abbf6"><code>414d9ab</code></a>
chore(release): publish 8.64.0</li>
<li><a
href="b418cf577f"><code>b418cf5</code></a>
feat(typescript-estree): throw for invalid definite assignment in class
prope...</li>
<li><a
href="07af21b912"><code>07af21b</code></a>
feat: support parsing <code>import defer</code> (<a
href="https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-estree/issues/12513">#12513</a>)</li>
<li>See full diff in <a
href="https://github.com/typescript-eslint/typescript-eslint/commits/v8.64.0/packages/typescript-estree">compare
view</a></li>
</ul>
</details>
<br />

Updates `@typescript-eslint/utils` from 8.63.0 to 8.64.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/typescript-eslint/typescript-eslint/releases">@​typescript-eslint/utils's
releases</a>.</em></p>
<blockquote>
<h2>v8.64.0</h2>
<h2>8.64.0 (2026-07-13)</h2>
<h3>🚀 Features</h3>
<ul>
<li>support parsing <code>import defer</code> (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12513">#12513</a>)</li>
<li><strong>eslint-plugin:</strong> [no-loop-func] support
<code>using</code> / <code>await using</code> declarations and deprecate
the rule (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12500">#12500</a>)</li>
<li><strong>typescript-estree:</strong> throw for invalid definite
assignment in class properties (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12543">#12543</a>)</li>
</ul>
<h3>🩹 Fixes</h3>
<ul>
<li><strong>eslint-plugin:</strong> [require-array-sort-compare] handle
constrained arrays (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12512">#12512</a>)</li>
</ul>
<h3>❤️ Thank You</h3>
<ul>
<li>Evyatar Daud <a
href="https://github.com/StyleShit"><code>@​StyleShit</code></a></li>
<li>송재욱</li>
</ul>
<p>See <a
href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.64.0">GitHub
Releases</a> for more information.</p>
<p>You can read about our <a
href="https://typescript-eslint.io/users/versioning">versioning
strategy</a> and <a
href="https://typescript-eslint.io/users/releases">releases</a> on our
website.</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/utils/CHANGELOG.md">@​typescript-eslint/utils's
changelog</a>.</em></p>
<blockquote>
<h2>8.64.0 (2026-07-13)</h2>
<p>This was a version bump only for utils to align it with other
projects, there were no code changes.</p>
<p>See <a
href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.64.0">GitHub
Releases</a> for more information.</p>
<p>You can read about our <a
href="https://typescript-eslint.io/users/versioning">versioning
strategy</a> and <a
href="https://typescript-eslint.io/users/releases">releases</a> on our
website.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="414d9abbf6"><code>414d9ab</code></a>
chore(release): publish 8.64.0</li>
<li><a
href="321856cdad"><code>321856c</code></a>
chore: cleanup cspell config (<a
href="https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/utils/issues/12526">#12526</a>)</li>
<li>See full diff in <a
href="https://github.com/typescript-eslint/typescript-eslint/commits/v8.64.0/packages/utils">compare
view</a></li>
</ul>
</details>
<br />

Updates `@typescript-eslint/visitor-keys` from 8.63.0 to 8.64.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/typescript-eslint/typescript-eslint/releases">@​typescript-eslint/visitor-keys's
releases</a>.</em></p>
<blockquote>
<h2>v8.64.0</h2>
<h2>8.64.0 (2026-07-13)</h2>
<h3>🚀 Features</h3>
<ul>
<li>support parsing <code>import defer</code> (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12513">#12513</a>)</li>
<li><strong>eslint-plugin:</strong> [no-loop-func] support
<code>using</code> / <code>await using</code> declarations and deprecate
the rule (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12500">#12500</a>)</li>
<li><strong>typescript-estree:</strong> throw for invalid definite
assignment in class properties (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12543">#12543</a>)</li>
</ul>
<h3>🩹 Fixes</h3>
<ul>
<li><strong>eslint-plugin:</strong> [require-array-sort-compare] handle
constrained arrays (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12512">#12512</a>)</li>
</ul>
<h3>❤️ Thank You</h3>
<ul>
<li>Evyatar Daud <a
href="https://github.com/StyleShit"><code>@​StyleShit</code></a></li>
<li>송재욱</li>
</ul>
<p>See <a
href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.64.0">GitHub
Releases</a> for more information.</p>
<p>You can read about our <a
href="https://typescript-eslint.io/users/versioning">versioning
strategy</a> and <a
href="https://typescript-eslint.io/users/releases">releases</a> on our
website.</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/visitor-keys/CHANGELOG.md">@​typescript-eslint/visitor-keys's
changelog</a>.</em></p>
<blockquote>
<h2>8.64.0 (2026-07-13)</h2>
<p>This was a version bump only for visitor-keys to align it with other
projects, there were no code changes.</p>
<p>See <a
href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.64.0">GitHub
Releases</a> for more information.</p>
<p>You can read about our <a
href="https://typescript-eslint.io/users/versioning">versioning
strategy</a> and <a
href="https://typescript-eslint.io/users/releases">releases</a> on our
website.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="414d9abbf6"><code>414d9ab</code></a>
chore(release): publish 8.64.0</li>
<li>See full diff in <a
href="https://github.com/typescript-eslint/typescript-eslint/commits/v8.64.0/packages/visitor-keys">compare
view</a></li>
</ul>
</details>
<br />

Updates `express-rate-limit` from 8.5.2 to 8.6.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/express-rate-limit/express-rate-limit/releases">express-rate-limit's
releases</a>.</em></p>
<blockquote>
<h2>v8.6.0</h2>
<p>You can view the changelog <a
href="https://express-rate-limit.mintlify.app/reference/changelog">here</a>.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="fffb3c4d6e"><code>fffb3c4</code></a>
8.6.0</li>
<li><a
href="f366b2d360"><code>f366b2d</code></a>
docs: debugging guide, time constants, &amp; v8.6.0 changelog (<a
href="https://redirect.github.com/express-rate-limit/express-rate-limit/issues/652">#652</a>)</li>
<li><a
href="593ddd2f4f"><code>593ddd2</code></a>
fix: make debug output easier to read (<a
href="https://redirect.github.com/express-rate-limit/express-rate-limit/issues/653">#653</a>)</li>
<li><a
href="ef8c1295ff"><code>ef8c129</code></a>
fix: Pin safe version of <code>@​asyncapi/specs</code> dev dep (<a
href="https://redirect.github.com/express-rate-limit/express-rate-limit/issues/659">#659</a>)</li>
<li><a
href="7b05e0dbd8"><code>7b05e0d</code></a>
feat: add time constants to support more readable values for windowMs
(<a
href="https://redirect.github.com/express-rate-limit/express-rate-limit/issues/655">#655</a>)</li>
<li><a
href="863e730c2f"><code>863e730</code></a>
chore(deps-dev): bump the development-dependencies group with 3 updates
(<a
href="https://redirect.github.com/express-rate-limit/express-rate-limit/issues/657">#657</a>)</li>
<li><a
href="e0e711e4c0"><code>e0e711e</code></a>
fix: correct wording in usage documentation for express-rate-limit (<a
href="https://redirect.github.com/express-rate-limit/express-rate-limit/issues/656">#656</a>)</li>
<li><a
href="fcd3aa7d5e"><code>fcd3aa7</code></a>
chore(deps-dev): bump the development-dependencies group with 3 updates
(<a
href="https://redirect.github.com/express-rate-limit/express-rate-limit/issues/651">#651</a>)</li>
<li><a
href="99d4298bda"><code>99d4298</code></a>
feat: use <code>debug</code> for debug logging (<a
href="https://redirect.github.com/express-rate-limit/express-rate-limit/issues/641">#641</a>)</li>
<li><a
href="23e4ddef55"><code>23e4dde</code></a>
feat: Run validations once each (<a
href="https://redirect.github.com/express-rate-limit/express-rate-limit/issues/650">#650</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/express-rate-limit/express-rate-limit/compare/v8.5.2...v8.6.0">compare
view</a></li>
</ul>
</details>
<br />

Updates `obug` from 2.1.3 to 2.1.4
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/sxzz/obug/releases">obug's
releases</a>.</em></p>
<blockquote>
<h2>v2.1.4</h2>
<h3>   🐞 Bug Fixes</h3>
<ul>
<li>Fallbacks for when env variables are disallowed  -  by <a
href="https://github.com/vrugtehagel"><code>@​vrugtehagel</code></a> and
<a href="https://github.com/sxzz"><code>@​sxzz</code></a> in <a
href="https://redirect.github.com/sxzz/obug/issues/5">sxzz/obug#5</a> <a
href="https://github.com/sxzz/obug/commit/faf1f47"><!-- raw HTML omitted
-->(faf1f)<!-- raw HTML omitted --></a></li>
</ul>
<h5>    <a
href="https://github.com/sxzz/obug/compare/v2.1.3...v2.1.4">View changes
on GitHub</a></h5>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="e50bedcb5f"><code>e50bedc</code></a>
chore: release v2.1.4</li>
<li><a
href="12086c0ec0"><code>12086c0</code></a>
chore: upgrade deps</li>
<li><a
href="faf1f47e4f"><code>faf1f47</code></a>
fix: fallbacks for when env variables are disallowed (<a
href="https://redirect.github.com/sxzz/obug/issues/5">#5</a>)</li>
<li>See full diff in <a
href="https://github.com/sxzz/obug/compare/v2.1.3...v2.1.4">compare
view</a></li>
</ul>
</details>
<br />

Updates `vite` from 8.1.4 to 8.1.5
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/vitejs/vite/releases">vite's
releases</a>.</em></p>
<blockquote>
<h2>v8.1.5</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v8.1.5/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md">vite's
changelog</a>.</em></p>
<blockquote>
<h2><!-- raw HTML omitted --><a
href="https://github.com/vitejs/vite/compare/v8.1.4...v8.1.5">8.1.5</a>
(2026-07-16)<!-- raw HTML omitted --></h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>bundled-dev:</strong> avoid duplicated <code>buildEnd</code>
(<a
href="https://redirect.github.com/vitejs/vite/issues/22931">#22931</a>)
(<a
href="8100320970">8100320</a>)</li>
<li><strong>client:</strong> overlay error message format align rolldown
(<a
href="https://redirect.github.com/vitejs/vite/issues/22869">#22869</a>)
(<a
href="5a72b87807">5a72b87</a>)</li>
<li><strong>deps:</strong> update all non-major dependencies (<a
href="https://redirect.github.com/vitejs/vite/issues/22921">#22921</a>)
(<a
href="fef682d3f0">fef682d</a>)</li>
<li><strong>deps:</strong> update rolldown-related dependencies (<a
href="https://redirect.github.com/vitejs/vite/issues/22922">#22922</a>)
(<a
href="3c345e475a">3c345e4</a>)</li>
<li><strong>module-runner:</strong> don't crash stack-trace source
mapping when globalThis.Buffer is absent (<a
href="https://redirect.github.com/vitejs/vite/issues/22945">#22945</a>)
(<a
href="f8b38e316b">f8b38e3</a>)</li>
<li><strong>optimizer:</strong> respect importer module format for
dynamic import interop with CJS deps (<a
href="https://redirect.github.com/vitejs/vite/issues/22951">#22951</a>)
(<a
href="6c08c39ac4">6c08c39</a>)</li>
<li><strong>ssr:</strong> scope switch-case declarations to the switch,
not the function (<a
href="https://redirect.github.com/vitejs/vite/issues/22893">#22893</a>)
(<a
href="b59a73f76f">b59a73f</a>)</li>
</ul>
<h3>Documentation</h3>
<ul>
<li><strong>build:</strong> fix incorrect <code>@default</code> for
build.cssMinify (<a
href="https://redirect.github.com/vitejs/vite/issues/22948">#22948</a>)
(<a
href="c88c236186">c88c236</a>)</li>
<li><strong>build:</strong> fix incorrect <code>@default</code> for
build.lib.formats (<a
href="https://redirect.github.com/vitejs/vite/issues/22911">#22911</a>)
(<a
href="369ed609a4">369ed60</a>)</li>
</ul>
<h3>Tests</h3>
<ul>
<li>avoid scanner scanning all files under <code>__tests__</code> (<a
href="https://redirect.github.com/vitejs/vite/issues/22912">#22912</a>)
(<a
href="c961cae286">c961cae</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="5e7fe129a4"><code>5e7fe12</code></a>
release: v8.1.5</li>
<li><a
href="6c08c39ac4"><code>6c08c39</code></a>
fix(optimizer): respect importer module format for dynamic import
interop wit...</li>
<li><a
href="5a72b87807"><code>5a72b87</code></a>
fix(client): overlay error message format align rolldown (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22869">#22869</a>)</li>
<li><a
href="f8b38e316b"><code>f8b38e3</code></a>
fix(module-runner): don't crash stack-trace source mapping when
globalThis.Bu...</li>
<li><a
href="8100320970"><code>8100320</code></a>
fix(bundled-dev): avoid duplicated <code>buildEnd</code> (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22931">#22931</a>)</li>
<li><a
href="c88c236186"><code>c88c236</code></a>
docs(build): fix incorrect <code>@default</code> for build.cssMinify (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22948">#22948</a>)</li>
<li><a
href="b59a73f76f"><code>b59a73f</code></a>
fix(ssr): scope switch-case declarations to the switch, not the function
(<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22">#22</a>...</li>
<li><a
href="fef682d3f0"><code>fef682d</code></a>
fix(deps): update all non-major dependencies (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22921">#22921</a>)</li>
<li><a
href="3c345e475a"><code>3c345e4</code></a>
fix(deps): update rolldown-related dependencies (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22922">#22922</a>)</li>
<li><a
href="369ed609a4"><code>369ed60</code></a>
docs(build): fix incorrect <code>@default</code> for build.lib.formats
(<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22911">#22911</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/vitejs/vite/commits/v8.1.5/packages/vite">compare
view</a></li>
</ul>
</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-07-20 11:02:00 +00:00
Ilia Shulgin
cc5d5e8b80 build: Trigger registry update after publishing new version (#888) 2026-07-17 10:18:20 +02:00
Mark Tkachenko
82cd692e50 feat: implement configurable LLM providers (#853)
According to [custom endpoints
rfd](https://github.com/agentclientprotocol/agent-client-protocol/blob/main/docs/rfds/custom-llm-endpoint.mdx)
Implement the Configurable LLM Providers RFD so clients can discover and
route the agent's LLM traffic through their own proxy or gateway without
relying solely on the ad-hoc gateway auth _meta mechanism.
2026-07-16 17:37:08 +02:00
12 changed files with 1759 additions and 359 deletions

View File

@@ -53,3 +53,28 @@ jobs:
- run: npm ci
- run: npm run build
- run: npm publish
trigger-registry-update:
needs: publish-npm
runs-on: ubuntu-latest
environment: release
permissions: {}
steps:
- name: Generate token scoped to the registry repo
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
id: registry-token
with:
app-id: ${{ secrets.REGISTRY_UPDATER_APP_ID }}
private-key: ${{ secrets.REGISTRY_UPDATER_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: registry
- name: Dispatch registry version update
env:
GH_TOKEN: ${{ steps.registry-token.outputs.token }}
run: |
gh workflow run update-versions.yml \
--repo ${{ github.repository_owner }}/registry \
--ref main \
-f apply=true \
-f agents=claude-acp

View File

@@ -1,3 +1,3 @@
{
".": "0.59.0"
".": "0.60.0"
}

View File

@@ -1,5 +1,20 @@
# Changelog
## [0.60.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.59.0...v0.60.0) (2026-07-20)
### Features
* **deps:** Update to claude-agent-sdk 0.3.215 ([#890](https://github.com/agentclientprotocol/claude-agent-acp/issues/890)) ([92548f0](https://github.com/agentclientprotocol/claude-agent-acp/commit/92548f043547b0ddac95921ece020e69f7c12c5f))
* implement configurable LLM providers ([#853](https://github.com/agentclientprotocol/claude-agent-acp/issues/853)) ([82cd692](https://github.com/agentclientprotocol/claude-agent-acp/commit/82cd692e500eedec182f817b18cebb005c4b98ce))
### Bug Fixes
* parse Agent/Task trailers without regex ([#879](https://github.com/agentclientprotocol/claude-agent-acp/issues/879)) ([06c3d7b](https://github.com/agentclientprotocol/claude-agent-acp/commit/06c3d7bdbd8cc9415c8cabac060a50e0951c758b))
* remove ~15s stall on session/new and model switch by seeding the context window synchronously ([#894](https://github.com/agentclientprotocol/claude-agent-acp/issues/894)) ([ff9b96d](https://github.com/agentclientprotocol/claude-agent-acp/commit/ff9b96d462831b1c3b96722ea20215ff6e529cb1))
* Silence missing PostToolUse callbacks ([#895](https://github.com/agentclientprotocol/claude-agent-acp/issues/895)) ([1887ada](https://github.com/agentclientprotocol/claude-agent-acp/commit/1887ada215b27bb1025d9b7696a46ae7a4ac0f7a)), closes [#889](https://github.com/agentclientprotocol/claude-agent-acp/issues/889)
## [0.59.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.58.1...v0.59.0) (2026-07-13)

339
package-lock.json generated
View File

@@ -1,28 +1,28 @@
{
"name": "@agentclientprotocol/claude-agent-acp",
"version": "0.59.0",
"version": "0.60.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@agentclientprotocol/claude-agent-acp",
"version": "0.59.0",
"version": "0.60.0",
"license": "Apache-2.0",
"dependencies": {
"@agentclientprotocol/sdk": "1.2.1",
"@anthropic-ai/claude-agent-sdk": "0.3.207",
"@anthropic-ai/claude-agent-sdk": "0.3.215",
"zod": "^3.25.0 || ^4.0.0"
},
"bin": {
"claude-agent-acp": "dist/index.js"
},
"devDependencies": {
"@anthropic-ai/sdk": "0.111.0",
"@anthropic-ai/sdk": "0.112.3",
"@eslint/js": "10.0.1",
"@tsconfig/node22": "22.0.5",
"@types/node": "26.1.1",
"@typescript-eslint/eslint-plugin": "8.63.0",
"@typescript-eslint/parser": "8.63.0",
"@typescript-eslint/eslint-plugin": "8.64.0",
"@typescript-eslint/parser": "8.64.0",
"eslint": "10.7.0",
"eslint-config-prettier": "10.1.8",
"globals": "17.7.0",
@@ -45,22 +45,22 @@
}
},
"node_modules/@anthropic-ai/claude-agent-sdk": {
"version": "0.3.207",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.207.tgz",
"integrity": "sha512-y0PkQRmQBi96MHiN5Xzfq+GaddxCZCqI/cXEQBLYBLXGa4i1nDSlulQqkMBj2RorrrSGQJ6Wdw+uhu6OfHNPzA==",
"version": "0.3.215",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.215.tgz",
"integrity": "sha512-fBktJCwfu8ZeOSnnSLcWVkIHyp/pjouJsGVGtRnQ0HkcRuOReIbRcB/6n2O5dYV331Ok+MfdGHiPaTORj7pCtQ==",
"license": "SEE LICENSE IN README.md",
"engines": {
"node": ">=18.0.0"
},
"optionalDependencies": {
"@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.207",
"@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.207",
"@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.207",
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.207",
"@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.207",
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.207",
"@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.207",
"@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.207"
"@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.215",
"@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.215",
"@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.215",
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.215",
"@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.215",
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.215",
"@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.215",
"@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.215"
},
"peerDependencies": {
"@anthropic-ai/sdk": ">=0.93.0",
@@ -69,9 +69,9 @@
}
},
"node_modules/@anthropic-ai/claude-agent-sdk-darwin-arm64": {
"version": "0.3.207",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.207.tgz",
"integrity": "sha512-08xSo1FDx8h0aLhL5tvcRxa2SMmcUV3aDWeZiEJVTclyiDAs61BgTjAxCg+SZcu1CndjJO8cfO0yM5dhamxz3g==",
"version": "0.3.215",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.215.tgz",
"integrity": "sha512-KIOe3N/ypVIdsI7fnJUHT0Djei1RH01TbxK6LI2mgoHKZ6NVDt/Q9kQ1+On3b/l899RhE6C1SMAiQ0iB4sbkjA==",
"cpu": [
"arm64"
],
@@ -82,9 +82,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-darwin-x64": {
"version": "0.3.207",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.207.tgz",
"integrity": "sha512-1o7K4EYqyCixZ/oeOZSh7AzSy6TM86xoOuf4VuORjPSS31hBnoqY0NGZd27+2VDs9LGtsdksmsTqcNGx9xd1hA==",
"version": "0.3.215",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.215.tgz",
"integrity": "sha512-cyjfQgkgF/zZbSJP6uJpPXoIJE/gYFOgz+u/SjklvakcO56BpEnsbdl/oHVu5G8GCw69V11hKFZ8T8YsVOiv2w==",
"cpu": [
"x64"
],
@@ -95,9 +95,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64": {
"version": "0.3.207",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.207.tgz",
"integrity": "sha512-X4uezYOifDiNTTmmugfRCdg3nNamrr1LFRY9hg30vWYTShL+bbN+nfC3KaFfSYCl4GTtsEEUbYdOTC2F3bBpcA==",
"version": "0.3.215",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.215.tgz",
"integrity": "sha512-lb7ayxZeWLiEhlOjzF8oX+DdJHrVgR1hvwkwRdYo+LgTT3hSxv6h43sheS3hZ1I9LYvOTKWa2E0O1EnffOzyCg==",
"cpu": [
"arm64"
],
@@ -111,9 +111,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64-musl": {
"version": "0.3.207",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.207.tgz",
"integrity": "sha512-oPj+g2DslhH4Y9nCTs7al4t9wZv78FZwLFQwOCg99BXuz1o0ZOpKmxyvR7J9eBR+GPszeMMS8gYplQTiZC9o2w==",
"version": "0.3.215",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.215.tgz",
"integrity": "sha512-dXMR8afbZCFWUKQGyvn9swDYYvMtZWGqqbZ994ahS7HvGN4AgF63uvojHVErW1Xxn2601Bm+eCYQt+BSLk6iZQ==",
"cpu": [
"arm64"
],
@@ -127,9 +127,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-linux-x64": {
"version": "0.3.207",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.207.tgz",
"integrity": "sha512-Kg6BPH8Ee0ny/oEUWJmvT1jCRBne4jVpRSOMsJcYp1Fav1rMEgpU219oJJs+LWwx4ifuuLtNWedqJNnVw7mnKg==",
"version": "0.3.215",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.215.tgz",
"integrity": "sha512-Iivu3oq7hIEc81dpTu1W0GJ/kCE8TRtj9tb+wdZCp1grsuGEJiS3Bh1tLxCrhLOTDxPggK6xZGVN/RPJh84ywA==",
"cpu": [
"x64"
],
@@ -143,9 +143,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-linux-x64-musl": {
"version": "0.3.207",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.207.tgz",
"integrity": "sha512-uRv+D5oG/7EYr41FAJ9IPo2pZYBe2ZMaA6nSHCeizsgPxCSMtl5bNppmU21+jZJvo4hivObEgkGFERAhdGqygg==",
"version": "0.3.215",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.215.tgz",
"integrity": "sha512-jSmrjSupnWtw8/I1Wmr32J6lB02gFXytmNbse4kcLEgH6UGtdZckKnDVnh/ejHma4QE9WGSn54xLsSFnjHVrRg==",
"cpu": [
"x64"
],
@@ -159,9 +159,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-win32-arm64": {
"version": "0.3.207",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.207.tgz",
"integrity": "sha512-9fWpUzfkXlPAg2tf8JpQe7w9avFaomAUbfAwyAmykQgSIf66LwaJjvI5hNqhNqczRKyfsXPn3ei2S5HKlmFP+Q==",
"version": "0.3.215",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.215.tgz",
"integrity": "sha512-9Xtpwd4DzfObOycDkfYfQ7clhWoFu2fmRqmMZWyQRyk2mEHWR3gLBwUgpfjYb6m4Fke9eTmIx2lXDG2i1OaBzA==",
"cpu": [
"arm64"
],
@@ -172,9 +172,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-win32-x64": {
"version": "0.3.207",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.207.tgz",
"integrity": "sha512-YPjVT0q6aXEM2MgN4CI6/9fqiTXwETji+4NoPOzCYuqAkhXZqp30Jsk7/NHqYGNNSfURKrsuAoliKB0rsbpbjg==",
"version": "0.3.215",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.215.tgz",
"integrity": "sha512-DkPwdc1zS6KIf6YNKXhlqAw95wnjG8Uh5bSXRrFA+MxsaWedlVFeja6zFM5uZ84ouGrcHEeU3PGUcGaKR1U4cQ==",
"cpu": [
"x64"
],
@@ -185,9 +185,9 @@
]
},
"node_modules/@anthropic-ai/sdk": {
"version": "0.111.0",
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.111.0.tgz",
"integrity": "sha512-1hUqKi+uJQoS5X90+InwHbFAXMvgq0DnsC5hVLEeSRaODiU5WvmqDAcVCmGS2wC0pN9Z8jtWCbWw7JLzeDdm/Q==",
"version": "0.112.3",
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.112.3.tgz",
"integrity": "sha512-wjcozJlitVIuBEw9cj/xBuRznwkhcLmXmNzlFoeHbh4AvrDG3HGZrdvEOTTmobcbhjGkfOpKbmDTCQ4s9LQvCg==",
"license": "MIT",
"dependencies": {
"json-schema-to-ts": "^3.1.1",
@@ -944,17 +944,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.63.0.tgz",
"integrity": "sha512-rvwSgqT+DHpWdzfSzPatRLm02a0GlESt++9iy3hLCDY4BgkaLcl8LBi9Yh7XGFBpwcBE/K3024QuXWTpbz4FfQ==",
"version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.64.0.tgz",
"integrity": "sha512-CGvQPBxN3wZLu6Rz2kFUpZeoCm78xUic92ck39KPePkO1NPOwjCqdQnm5Q87tpWw9vcBvW8XLrDXjH9PWYtJ3Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.12.2",
"@typescript-eslint/scope-manager": "8.63.0",
"@typescript-eslint/type-utils": "8.63.0",
"@typescript-eslint/utils": "8.63.0",
"@typescript-eslint/visitor-keys": "8.63.0",
"@typescript-eslint/scope-manager": "8.64.0",
"@typescript-eslint/type-utils": "8.64.0",
"@typescript-eslint/utils": "8.64.0",
"@typescript-eslint/visitor-keys": "8.64.0",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.5.0"
@@ -967,22 +967,22 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.63.0",
"@typescript-eslint/parser": "^8.64.0",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.63.0.tgz",
"integrity": "sha512-gwh4gvvlaVDKKxyfxMG+Gnu1u9X0OQBwyGLkbwB65dIzBKnxeRiJlNFqlI3zwVhNXJIs6qV7mlFCn/BIajlVig==",
"version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.64.0.tgz",
"integrity": "sha512-KA0OshtlcCCXmbfqyZkM5pV3/WNraJf7DkJRLpyrmwPtud57H5BDX7C3k0LPSPxpprfRL+cJDGabF10mvNCoCw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.63.0",
"@typescript-eslint/types": "8.63.0",
"@typescript-eslint/typescript-estree": "8.63.0",
"@typescript-eslint/visitor-keys": "8.63.0",
"@typescript-eslint/scope-manager": "8.64.0",
"@typescript-eslint/types": "8.64.0",
"@typescript-eslint/typescript-estree": "8.64.0",
"@typescript-eslint/visitor-keys": "8.64.0",
"debug": "^4.4.3"
},
"engines": {
@@ -998,14 +998,14 @@
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.63.0.tgz",
"integrity": "sha512-e5dh0/UI0ok53AlZ5wRkXCB32z/f2jUZqPR/ygAw5WYaSw8j9EoJWlS7wQjr/dmOaqWjnPIn2m+HhVPCMWGZVQ==",
"version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.64.0.tgz",
"integrity": "sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.63.0",
"@typescript-eslint/types": "^8.63.0",
"@typescript-eslint/tsconfig-utils": "^8.64.0",
"@typescript-eslint/types": "^8.64.0",
"debug": "^4.4.3"
},
"engines": {
@@ -1020,14 +1020,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.63.0.tgz",
"integrity": "sha512-uUyfMWCnDSN8bCpcrY8nGP2BLkQ9Xn0GsipcONcpIDWhwhO4ZSyHvyS14U3X75mzxWxL3I2UZIrenTzdzcJO8A==",
"version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.64.0.tgz",
"integrity": "sha512-CXEaFdYXjSTgKhisNkwCcJwTP8Pl+fmRrEQrri4nm3vU743bALrxzLmq7fHG/7e6a5xO0lDYeURpZmBuhHk54w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.63.0",
"@typescript-eslint/visitor-keys": "8.63.0"
"@typescript-eslint/types": "8.64.0",
"@typescript-eslint/visitor-keys": "8.64.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1038,9 +1038,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.63.0.tgz",
"integrity": "sha512-sUAbkulqBAsncKnbRP3+7CtQFRKicexnj7ZwNC6ddCR7EmrXvjvdCYMJbUIqMd6lwoEriZjwLo08aS5tSjVMHg==",
"version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.64.0.tgz",
"integrity": "sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1055,15 +1055,15 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.63.0.tgz",
"integrity": "sha512-Nzzh/OGxVCOjObjaj1CQF2RUasyYy2Jfuh+zZ3PjLzG2fYRriAiZLib9UKtO+CpQAS3YHiAS+ckZDclwqI1TPA==",
"version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.64.0.tgz",
"integrity": "sha512-XWG4Fmmv/6SvyS9nH8jWrKs6terwJvE8cyRt1CzYYqzp9OrPhCT4cMc/f7C6RZCwG+qMmiffJS1/qJP8G1URtg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.63.0",
"@typescript-eslint/typescript-estree": "8.63.0",
"@typescript-eslint/utils": "8.63.0",
"@typescript-eslint/types": "8.64.0",
"@typescript-eslint/typescript-estree": "8.64.0",
"@typescript-eslint/utils": "8.64.0",
"debug": "^4.4.3",
"ts-api-utils": "^2.5.0"
},
@@ -1080,9 +1080,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.63.0.tgz",
"integrity": "sha512-xyLtl9DUBBFrcJS4x2pIqGLH68/tC2uOa4Z7pUteW09D3bXnnXUom4dyPikzWgB7llmIc1zoeI3aoUdC4rPK/Q==",
"version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.64.0.tgz",
"integrity": "sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1094,16 +1094,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.63.0.tgz",
"integrity": "sha512-ygBkU+B7ex5UI/gKhaqexWev79uISfIv7XQCRNYO/jmD8rGLPyWLAb3KMRT6nd8Gt9bmUBi9+iX6tBdYfOY81Q==",
"version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.64.0.tgz",
"integrity": "sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.63.0",
"@typescript-eslint/tsconfig-utils": "8.63.0",
"@typescript-eslint/types": "8.63.0",
"@typescript-eslint/visitor-keys": "8.63.0",
"@typescript-eslint/project-service": "8.64.0",
"@typescript-eslint/tsconfig-utils": "8.64.0",
"@typescript-eslint/types": "8.64.0",
"@typescript-eslint/visitor-keys": "8.64.0",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
@@ -1122,16 +1122,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.63.0.tgz",
"integrity": "sha512-fUKaeAvrTuQg/Tgt3nliAUSZHJM6DlCcfyEmxCvlX8kieWSStBX+5O5Fnidtc3i2JrH+9c/GL4RY2iasd/GPTA==",
"version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.64.0.tgz",
"integrity": "sha512-aJUGVB3+U0htrrCjoA8qukw8cm8fNCGAxK/tVoS70k8aeb7DETKeFozRiVFIwEeN9WJLsjaP3ph8I60tY2XZoQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.63.0",
"@typescript-eslint/types": "8.63.0",
"@typescript-eslint/typescript-estree": "8.63.0"
"@typescript-eslint/scope-manager": "8.64.0",
"@typescript-eslint/types": "8.64.0",
"@typescript-eslint/typescript-estree": "8.64.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1146,13 +1146,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.63.0.tgz",
"integrity": "sha512-UexrHGnGTpbuQHct2ExOc2ZcFbGUS9FOesCxxqdBGcpI1BxYu/LZ6U8Aq6/72XtF/qRBk9nhuGHFJIXXMhPMdw==",
"version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.64.0.tgz",
"integrity": "sha512-mrtuL8Nsn6gi2H4mo5KMTp823M+3Q19Ew/i+Zlikq20tIMm99C3Ez0dCmkWWnxut20esQvTg8aUSEhMcAOXhEw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.63.0",
"@typescript-eslint/types": "8.64.0",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
@@ -2069,12 +2069,13 @@
}
},
"node_modules/express-rate-limit": {
"version": "8.5.2",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz",
"integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==",
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.0.tgz",
"integrity": "sha512-XKJXDsASUOo0LLtFwW5hCcQGH0N4WQc/Rn8/Pvoia+TJFOkkFPvrtW9lZOeeNcxQJspvOIERMwiRLsVFlhHEkA==",
"license": "MIT",
"peer": true,
"dependencies": {
"debug": "^4.4.3",
"ip-address": "^10.2.0"
},
"engines": {
@@ -2114,9 +2115,9 @@
"license": "Unlicense"
},
"node_modules/fast-uri": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz",
"integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==",
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz",
"integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==",
"funding": [
{
"type": "github",
@@ -2371,9 +2372,9 @@
}
},
"node_modules/hono": {
"version": "4.12.30",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.30.tgz",
"integrity": "sha512-emn+JoJjrN9YTpRDS5it/UI2SO9BAE37T6I3d963RxcZ81G9A4pr2SZTEiiaiKbzx+NKRg5BZ89fCL7gCJCUog==",
"version": "4.12.31",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.31.tgz",
"integrity": "sha512-zJIHFrl6bq3RDd2YusFNCDlM8qUprxKswyi/OPzPyzKDdyBXDqWx8bZlZ7R+saTdSTatUmb3O7K4SspGPaEOQg==",
"license": "MIT",
"peer": true,
"engines": {
@@ -2419,9 +2420,9 @@
}
},
"node_modules/ignore": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
"integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz",
"integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2577,9 +2578,9 @@
}
},
"node_modules/lightningcss": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
"integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==",
"dev": true,
"license": "MPL-2.0",
"dependencies": {
@@ -2593,23 +2594,23 @@
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"lightningcss-android-arm64": "1.32.0",
"lightningcss-darwin-arm64": "1.32.0",
"lightningcss-darwin-x64": "1.32.0",
"lightningcss-freebsd-x64": "1.32.0",
"lightningcss-linux-arm-gnueabihf": "1.32.0",
"lightningcss-linux-arm64-gnu": "1.32.0",
"lightningcss-linux-arm64-musl": "1.32.0",
"lightningcss-linux-x64-gnu": "1.32.0",
"lightningcss-linux-x64-musl": "1.32.0",
"lightningcss-win32-arm64-msvc": "1.32.0",
"lightningcss-win32-x64-msvc": "1.32.0"
"lightningcss-android-arm64": "1.33.0",
"lightningcss-darwin-arm64": "1.33.0",
"lightningcss-darwin-x64": "1.33.0",
"lightningcss-freebsd-x64": "1.33.0",
"lightningcss-linux-arm-gnueabihf": "1.33.0",
"lightningcss-linux-arm64-gnu": "1.33.0",
"lightningcss-linux-arm64-musl": "1.33.0",
"lightningcss-linux-x64-gnu": "1.33.0",
"lightningcss-linux-x64-musl": "1.33.0",
"lightningcss-win32-arm64-msvc": "1.33.0",
"lightningcss-win32-x64-msvc": "1.33.0"
}
},
"node_modules/lightningcss-android-arm64": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
"integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz",
"integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==",
"cpu": [
"arm64"
],
@@ -2628,9 +2629,9 @@
}
},
"node_modules/lightningcss-darwin-arm64": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
"integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz",
"integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==",
"cpu": [
"arm64"
],
@@ -2649,9 +2650,9 @@
}
},
"node_modules/lightningcss-darwin-x64": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
"integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz",
"integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==",
"cpu": [
"x64"
],
@@ -2670,9 +2671,9 @@
}
},
"node_modules/lightningcss-freebsd-x64": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
"integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz",
"integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==",
"cpu": [
"x64"
],
@@ -2691,9 +2692,9 @@
}
},
"node_modules/lightningcss-linux-arm-gnueabihf": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
"integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz",
"integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==",
"cpu": [
"arm"
],
@@ -2712,9 +2713,9 @@
}
},
"node_modules/lightningcss-linux-arm64-gnu": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
"integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz",
"integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==",
"cpu": [
"arm64"
],
@@ -2736,9 +2737,9 @@
}
},
"node_modules/lightningcss-linux-arm64-musl": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
"integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz",
"integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==",
"cpu": [
"arm64"
],
@@ -2760,9 +2761,9 @@
}
},
"node_modules/lightningcss-linux-x64-gnu": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
"integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz",
"integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==",
"cpu": [
"x64"
],
@@ -2784,9 +2785,9 @@
}
},
"node_modules/lightningcss-linux-x64-musl": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
"integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz",
"integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==",
"cpu": [
"x64"
],
@@ -2808,9 +2809,9 @@
}
},
"node_modules/lightningcss-win32-arm64-msvc": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
"integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz",
"integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==",
"cpu": [
"arm64"
],
@@ -2829,9 +2830,9 @@
}
},
"node_modules/lightningcss-win32-x64-msvc": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
"integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz",
"integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==",
"cpu": [
"x64"
],
@@ -3024,9 +3025,9 @@
}
},
"node_modules/obug": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz",
"integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==",
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz",
"integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==",
"dev": true,
"funding": [
"https://github.com/sponsors/sxzz",
@@ -3188,9 +3189,9 @@
}
},
"node_modules/postcss": {
"version": "8.5.19",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz",
"integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==",
"version": "8.5.20",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.20.tgz",
"integrity": "sha512-lW616l85ucIQL+FocMmL7pQFPqBmwejrCMg+iPxyImlrANNJG9NHq/RkyCZopDhd8C3LA03PHRJDjkbGu8vvug==",
"dev": true,
"funding": [
{
@@ -3208,7 +3209,7 @@
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.12",
"nanoid": "^3.3.16",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@@ -3826,16 +3827,16 @@
}
},
"node_modules/vite": {
"version": "8.1.4",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.4.tgz",
"integrity": "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ==",
"version": "8.1.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz",
"integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==",
"dev": true,
"license": "MIT",
"dependencies": {
"lightningcss": "^1.32.0",
"picomatch": "^4.0.5",
"postcss": "^8.5.16",
"rolldown": "~1.1.4",
"postcss": "^8.5.17",
"rolldown": "~1.1.5",
"tinyglobby": "^0.2.17"
},
"bin": {

View File

@@ -3,7 +3,7 @@
"publishConfig": {
"access": "public"
},
"version": "0.59.0",
"version": "0.60.0",
"description": "An ACP-compatible coding agent powered by the Claude Agent SDK (TypeScript)",
"main": "dist/lib.js",
"types": "dist/lib.d.ts",
@@ -64,16 +64,16 @@
},
"dependencies": {
"@agentclientprotocol/sdk": "1.2.1",
"@anthropic-ai/claude-agent-sdk": "0.3.207",
"@anthropic-ai/claude-agent-sdk": "0.3.215",
"zod": "^3.25.0 || ^4.0.0"
},
"devDependencies": {
"@anthropic-ai/sdk": "0.111.0",
"@anthropic-ai/sdk": "0.112.3",
"@eslint/js": "10.0.1",
"@tsconfig/node22": "22.0.5",
"@types/node": "26.1.1",
"@typescript-eslint/eslint-plugin": "8.63.0",
"@typescript-eslint/parser": "8.63.0",
"@typescript-eslint/eslint-plugin": "8.64.0",
"@typescript-eslint/parser": "8.64.0",
"eslint": "10.7.0",
"eslint-config-prettier": "10.1.8",
"globals": "17.7.0",

View File

@@ -9,10 +9,15 @@ import {
CompleteElicitationNotification,
CreateElicitationRequest,
CreateElicitationResponse,
DisableProviderRequest,
DisableProviderResponse,
ForkSessionRequest,
ForkSessionResponse,
InitializeRequest,
InitializeResponse,
ListProvidersRequest,
ListProvidersResponse,
LlmProtocol,
ListSessionsRequest,
ListSessionsResponse,
LoadSessionRequest,
@@ -25,9 +30,12 @@ import {
PermissionOption,
PromptRequest,
PromptResponse,
ProviderInfo,
ReadTextFileRequest,
ReadTextFileResponse,
RequestError,
SetProviderRequest,
SetProviderResponse,
RequestPermissionRequest,
RequestPermissionResponse,
ResumeSessionRequest,
@@ -51,6 +59,7 @@ import {
AgentInfo,
CanUseTool,
deleteSession,
EffortLevel,
FastModeState,
getSessionInfo,
getSessionMessages,
@@ -66,7 +75,6 @@ import {
PermissionUpdate,
Query,
query,
Settings,
SDKAssistantMessageError,
SDKMessage,
SDKMessageOrigin,
@@ -385,11 +393,32 @@ type Session = {
emitRawSDKMessages: boolean | SDKMessageFilter[];
/** Context window size of the session's current model, carried across
* prompts so mid-stream usage_update notifications report a correct `size`
* before the turn's first result message arrives. Seeded from the SDK's
* getContextUsage report at session creation (DEFAULT_CONTEXT_WINDOW when
* that and the text heuristic both fail), refreshed the same way on model
* switches, and confirmed by each result's modelUsage. */
* before the turn's first result message arrives. Seeded synchronously at
* session creation and on model switches from the per-model cache or the
* text heuristic (DEFAULT_CONTEXT_WINDOW when both miss; on session/load the
* resumed session's own `getContextUsage` report wins, see
* `readResumedLiveModel`), then confirmed — and the cache populated — by each
* result's modelUsage. No extra `getContextUsage` IPC is on these paths: on a
* fresh session it stalls until the first turn runs (see the seeding call
* sites and `contextWindowCache`). */
contextWindowSize: number;
/** Whether `contextWindowSize` came from an authoritative source (the
* cross-session cache, a resumed session's `getContextUsage` report, or a
* `result.modelUsage`) rather than the text heuristic / default. Guards the
* mid-stream `message_start` heuristic upgrade: an authoritative window that
* happens to equal DEFAULT_CONTEXT_WINDOW must not be mistaken for "unseeded"
* and clobbered by a "1m" text match. */
contextWindowAuthoritative: boolean;
/** Stable identifier of the LLM backend this session's query was created
* against, derived from the routing-relevant vars of the exact `env` handed
* to the SDK at query creation (see {@link providerCacheKeyFor}). The context
* window is a property of (model id, backend) — the same resolved model id
* can name different windows behind different base URLs, routing headers, or
* credentials — so this scopes the module-global `contextWindowCache` per
* backend. Captured from the query's own env (not re-resolved later) because
* the process-wide provider config can change while a session is being
* created, while the query stays baked to the env it was created with. */
providerCacheKey: string;
/** Accumulated task list for the session, keyed by task ID. Task IDs are
* per-session, so this state must not be shared across sessions. */
taskState: TaskState;
@@ -658,6 +687,50 @@ type GatewayAuthMeta = {
type GatewayAuthRequest = AuthenticateRequest & { _meta?: GatewayAuthMeta };
/**
* The single provider ID this agent exposes via `providers/*`. Claude Code has
* one LLM backend selected by protocol (anthropic / bedrock / vertex), so there
* is exactly one configurable provider.
*/
const PROVIDER_ID = "main";
/**
* Protocols the `main` provider can be configured with. These mirror the
* env-var mappings understood by {@link createEnvForProvider}.
*/
const SUPPORTED_PROTOCOLS: LlmProtocol[] = ["anthropic", "bedrock", "vertex"];
/**
* Vertex needs project + region that the standard `providers/set` payload
* (`apiType`/`baseUrl`/`headers`) does not model, so clients pass them through
* `_meta.claudeCode.vertex`. Required only when `apiType === "vertex"`.
*/
type SetProviderMeta = {
claudeCode?: {
vertex?: {
projectId: string;
region: string;
};
};
};
/**
* Resolved, non-secret + secret routing config for the `main` provider. This is
* the shared shape produced by both `providers/set` and the legacy gateway auth
* path, and consumed by {@link createEnvForProvider}. `null` means the provider
* is unconfigured (no client-managed routing in effect).
*/
type ProviderConfig = {
apiType: LlmProtocol;
baseUrl: string;
headers: Record<string, string>;
/** Present only for `apiType === "vertex"`. */
vertex?: {
projectId: string;
region: string;
};
};
/**
* Extra metadata that the agent provides for each tool_call / tool_update update.
*/
@@ -1128,6 +1201,10 @@ export class ClaudeAcpAgent {
clientCapabilities?: ClientCapabilities;
logger: Logger;
gatewayAuthRequest?: GatewayAuthRequest;
/** Client-managed LLM routing set via `providers/set`. Process-scoped and
* never persisted to disk (see the Configurable LLM Providers RFD). When
* set, it takes precedence over {@link gatewayAuthRequest}. */
providerConfig?: ProviderConfig;
/** Grace period before a `session/cancel` forces a wedged prompt loop to
* return "cancelled". See {@link DEFAULT_FORCE_CANCEL_GRACE_MS}. Mutable so
* tests can shrink it. */
@@ -1268,6 +1345,10 @@ export class ClaudeAcpAgent {
auth: {
logout: {},
},
// Client-managed LLM routing via `providers/list`, `providers/set`, and
// `providers/disable`. Advertised unconditionally; there is no client
// capability prerequisite for the provider methods.
providers: {},
loadSession: true,
sessionCapabilities: {
additionalDirectories: {},
@@ -1405,11 +1486,121 @@ export class ClaudeAcpAgent {
throw new Error("Method not implemented.");
}
/**
* `providers/list` — returns the single client-configurable custom gateway
* provider (`main`). `current` carries only non-secret routing (never headers,
* which may hold secrets); only `apiType`/`baseUrl` are surfaced for UI
* display, and is `null` when the provider is not configured/disabled. The
* provider is optional (`required: false`): while disabled/unconfigured the
* agent falls back to its own default routing (normal Claude login).
*/
async unstable_listProviders(_params: ListProvidersRequest): Promise<ListProvidersResponse> {
const config = this.resolveProviderConfig();
const provider: ProviderInfo = {
providerId: PROVIDER_ID,
supported: SUPPORTED_PROTOCOLS,
required: false,
current: config ? { apiType: config.apiType, baseUrl: config.baseUrl } : null,
};
return { providers: [provider] };
}
/**
* `providers/set` — replace the full configuration for the `main` provider.
* Rejects unknown IDs, unsupported protocols, and empty/invalid base URLs with
* `invalid_params`. Config is process-scoped and applies to sessions created or
* loaded after this call.
*/
async unstable_setProvider(params: SetProviderRequest): Promise<SetProviderResponse> {
if (params.providerId !== PROVIDER_ID) {
throw RequestError.invalidParams(
{ providerId: params.providerId },
`Unknown provider ID "${params.providerId}"; expected "${PROVIDER_ID}".`,
);
}
if (!SUPPORTED_PROTOCOLS.includes(params.apiType)) {
throw RequestError.invalidParams(
{ apiType: params.apiType, supported: SUPPORTED_PROTOCOLS },
`Unsupported apiType "${params.apiType}" for provider "${PROVIDER_ID}".`,
);
}
if (!isValidBaseUrl(params.baseUrl)) {
throw RequestError.invalidParams(
{ baseUrl: params.baseUrl },
"baseUrl must be a non-empty absolute http(s) URL.",
);
}
const config: ProviderConfig = {
apiType: params.apiType,
baseUrl: params.baseUrl,
headers: params.headers ?? {},
};
// Vertex requires project + region, which the standard payload cannot
// carry, so they arrive via `_meta.claudeCode.vertex`.
if (params.apiType === "vertex") {
const vertex = (params._meta as SetProviderMeta | undefined)?.claudeCode?.vertex;
if (
!vertex ||
typeof vertex.projectId !== "string" ||
vertex.projectId.trim() === "" ||
typeof vertex.region !== "string" ||
vertex.region.trim() === ""
) {
throw RequestError.invalidParams(
undefined,
"vertex apiType requires non-empty `_meta.claudeCode.vertex.projectId` and `_meta.claudeCode.vertex.region`.",
);
}
config.vertex = { projectId: vertex.projectId, region: vertex.region };
}
this.providerConfig = config;
return {};
}
/**
* `providers/disable` — disabling the `main` provider clears any client-managed
* routing (both a `providers/set` config and the legacy gateway auth request),
* so the agent reverts to its own default routing and `providers/list` reports
* `current: null`. Disabling any other (unknown) ID is treated as a successful
* no-op per the RFD's idempotency rule.
*/
async unstable_disableProvider(params: DisableProviderRequest): Promise<DisableProviderResponse> {
if (params.providerId === PROVIDER_ID) {
this.providerConfig = undefined;
this.gatewayAuthRequest = undefined;
}
// Unknown provider: idempotent success.
return {};
}
/**
* Resolve the effective client-managed routing config. `providers/set` takes
* precedence; otherwise fall back to the legacy gateway auth request. Returns
* `null` when neither is configured.
*/
resolveProviderConfig(): ProviderConfig | null {
if (this.providerConfig) {
return this.providerConfig;
}
return gatewayRequestToProviderConfig(this.gatewayAuthRequest);
}
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.
// Clear in-memory gateway credentials supplied via `authenticate` and any
// provider routing set via `providers/set`. Neither touches the on-disk
// credential store, so dropping these references is the whole logout for
// those paths.
this.gatewayAuthRequest = undefined;
this.providerConfig = undefined;
// Learned context windows are per-account state too: 1M-context
// entitlement is gated per org/tier, and an OAuth re-login is invisible to
// the env-derived provider cache key, so windows learned under the old
// login must not seed sessions under the next. Worst case of clearing is
// re-learning on each model's next turn.
contextWindowCache.clear();
// 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.
@@ -2726,12 +2917,43 @@ export class ClaudeAcpAgent {
const matchingModelUsage = lastAssistantModel
? getMatchingModelUsage(message.modelUsage, lastAssistantModel)
: null;
// Only overwrite when we have an authoritative value — a miss
// (e.g. a turn with no top-level assistant message) would
// otherwise discard the window learned on a prior turn and
// leave the next prompt's mid-stream updates reporting 200k.
if (matchingModelUsage) {
session.contextWindowSize = matchingModelUsage.contextWindow;
// Only overwrite when we have an authoritative, sane value. A miss
// (e.g. a turn with no top-level assistant message), or a
// nonsensical non-positive/NaN window (observed from third-party
// backends), would otherwise discard the window learned on a prior
// turn and leave the next prompt's mid-stream updates reporting a
// wrong size. `cacheContextWindow` applies the same `> 0` guard, so
// a bad value never reaches the cross-session cache either.
if (
matchingModelUsage &&
typeof matchingModelUsage.usage.contextWindow === "number" &&
matchingModelUsage.usage.contextWindow > 0
) {
session.contextWindowSize = matchingModelUsage.usage.contextWindow;
session.contextWindowAuthoritative = true;
// Authoritative: fold it into the cross-session cache keyed on
// (this session's provider, the resolved model id —
// matchingModelUsage.key, e.g. "claude-sonnet-5[1m]") so a later
// session/new or switch on the same provider that resolves to
// this model seeds the correct window synchronously, with no
// getContextUsage IPC.
cacheContextWindow(
contextWindowCacheKey(session.providerCacheKey, matchingModelUsage.key),
matchingModelUsage.usage.contextWindow,
);
// Also cache under the assistant message's own (bare) spelling.
// Seed-time reads fall back to a picker value / verbatim live id
// when a row carries no resolvedModel (the synthesized
// out-of-allowlist resume row sets it undefined on purpose), and
// those spellings match `.model` from the assistant message, not
// the decorated modelUsage key — without this entry such rows
// could never hit the cache.
if (lastAssistantModel && lastAssistantModel !== matchingModelUsage.key) {
cacheContextWindow(
contextWindowCacheKey(session.providerCacheKey, lastAssistantModel),
matchingModelUsage.usage.contextWindow,
);
}
}
// Send usage_update notification
@@ -3003,11 +3225,18 @@ export class ClaudeAcpAgent {
const model = message.event.message.model;
if (model && model !== "<synthetic>") {
lastAssistantModel = model;
// Only upgrade from the default — once the SDK has given us
// an authoritative window (seeded at session creation,
// refreshed on model switches in `applyConfigOptionValue`,
// confirmed by each `result`), trust it over the heuristic.
if (session.contextWindowSize === DEFAULT_CONTEXT_WINDOW) {
// Only upgrade from the heuristic default — once we have an
// authoritative window (cache-seeded at session creation or
// on a model switch, read from the resumed session on
// session/load, confirmed by each `result`), trust it over
// the heuristic. The flag, not the value, is the sentinel: an
// authoritative window can legitimately equal
// DEFAULT_CONTEXT_WINDOW (e.g. a backend serving a 200k lane
// under a "[1m]"-spelled id) and must not be clobbered.
if (
!session.contextWindowAuthoritative &&
session.contextWindowSize === DEFAULT_CONTEXT_WINDOW
) {
const inferred = inferContextWindowFromModel(model);
if (inferred !== null) {
session.contextWindowSize = inferred;
@@ -3339,7 +3568,20 @@ export class ClaudeAcpAgent {
_meta: {
claudeCode: {
toolName: message.tool_name,
toolResponse: { elapsedTimeSeconds: message.elapsed_time_seconds },
toolResponse: {
elapsedTimeSeconds: message.elapsed_time_seconds,
// For Agent/Task calls: the subagent's type, and — when
// the subagent is waiting out an API rate-limit retry —
// the SDK's retry counters (attempt, max_retries,
// retry_delay_ms, …), forwarded verbatim so clients can
// show why a spawn looks stalled.
...(message.subagent_type !== undefined && {
subagentType: message.subagent_type,
}),
...(message.subagent_retry !== undefined && {
subagentRetry: message.subagent_retry,
}),
},
},
} satisfies ToolUpdateMeta,
},
@@ -4021,7 +4263,11 @@ export class ClaudeAcpAgent {
}
canUseTool(sessionId: string): CanUseTool {
return async (toolName, toolInput, { signal, suggestions, toolUseID, agentID }) => {
return async (
toolName,
toolInput,
{ signal, suggestions, toolUseID, agentID, matchedAskRule },
) => {
const alwaysAllowLabel = describeAlwaysAllow(suggestions, toolName);
const supportsTerminalOutput = this.clientCapabilities?._meta?.["terminal_output"] === true;
const session = this.sessions[sessionId];
@@ -4157,7 +4403,14 @@ export class ClaudeAcpAgent {
}
}
if (session.modes.currentModeId === "bypassPermissions") {
// In bypass mode the CLI skips permission checks itself; the asks that
// still reach canUseTool are the ones it insists on prompting for even
// under --dangerously-skip-permissions. Keep auto-allowing those —
// bypass means bypass — EXCEPT rule-forced asks (`matchedAskRule`): the
// user explicitly configured a permissions.ask rule for this tool, and
// the SDK's guidance is that hosts running auto-approval must treat such
// asks as a human prompt. Fall through to the normal request below.
if (session.modes.currentModeId === "bypassPermissions" && !matchedAskRule) {
return {
behavior: "allow",
updatedInput: toolInput,
@@ -4401,22 +4654,21 @@ export class ClaudeAcpAgent {
// carries no "1m" token.
const newModelInfo = session.modelInfos.find((m) => m.value === value);
if (session.models.currentModelId !== value) {
// The cached context window was learned for the previous model. The
// SDK is already running the new model here (user-driven switches call
// `query.setModel` before this, and the refusal-fallback sync only
// reconciles a switch the SDK already made), so ask it for the new
// window; fall back to the text heuristic so mid-stream updates
// between now and the next `result` reflect the user's selection
// instead of the old model's window.
session.contextWindowSize =
(await fetchContextWindowSize(session.query, this.logger)) ??
inferContextWindowFromModel(
value,
newModelInfo?.resolvedModel,
newModelInfo?.displayName,
newModelInfo?.description,
) ??
DEFAULT_CONTEXT_WINDOW;
// Seed the new model's context window WITHOUT any IPC on the switch
// path: cached authoritative value if we've already learned it (from a
// prior turn's `result.modelUsage`), else the text heuristic, else the
// default. We deliberately do NOT call `getContextUsage` here — before
// a fresh session's first prompt turn that control request is not
// serviced (~15s stall, issues #886/#880), and (because SDK control
// requests are serialized over one channel) it would drag the awaited
// `setModel` down with it. The authoritative window arrives on the
// first `result.modelUsage` for the model and is cached from there;
// until then a switched-to alias that has never run a turn shows the
// heuristic/default window, which self-corrects on its first response
// (matches pre-0.59.0 behavior).
const seeded = immediateContextWindow(session.providerCacheKey, value, newModelInfo);
session.contextWindowSize = seeded.size;
session.contextWindowAuthoritative = seeded.authoritative;
}
session.models = { ...session.models, currentModelId: value };
@@ -4826,6 +5078,29 @@ export class ClaudeAcpAgent {
// the same Map that the streaming message handler will read from.
const taskState: TaskState = new Map();
// The exact env the query will be created with. Built (and the provider
// cache key derived from it, below) in one place so the key always
// describes the backend this query actually talks to: `providers/set`,
// `providers/disable`, and `logout` mutate the process-wide provider
// config concurrently, so re-resolving it after any of the awaits between
// here and the session registration could disagree with the env baked
// into the query.
const env = {
...process.env,
...userProvidedOptions?.env,
// Client-managed LLM routing: `providers/set` config wins, else the
// legacy gateway auth request. Baked into the query at creation, so it
// only affects sessions started after the change (matching the RFD).
...createEnvForProvider(this.resolveProviderConfig()),
// Opt-in to session state events like when the agent is idle
CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS: "1",
};
// Scopes the context-window cache to this query's backend (see
// `contextWindowCache`). Derived from the same `env` object handed to the
// SDK, so per-session `_meta` env routing and ambient process-env routing
// are distinguished exactly as the CLI will see them.
const providerCacheKey = providerCacheKeyFor(env);
const options: Options = {
systemPrompt,
settingSources: ["user", "project", "local"],
@@ -4842,13 +5117,7 @@ export class ClaudeAcpAgent {
...(modelConfig.availableModels && { availableModels: modelConfig.availableModels }),
},
}),
env: {
...process.env,
...userProvidedOptions?.env,
...createEnvForGateway(this.gatewayAuthRequest),
// Opt-in to session state events like when the agent is idle
CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS: "1",
},
env,
// Override certain fields that must be controlled by ACP
cwd: params.cwd,
includePartialMessages: true,
@@ -4890,7 +5159,7 @@ export class ClaudeAcpAgent {
...(userProvidedOptions?.hooks?.PostToolUse || []),
{
hooks: [
createPostToolUseHook(this.logger, {
createPostToolUseHook({
onEnterPlanMode: async () => {
await this.client.sessionUpdate({
sessionId,
@@ -5015,7 +5284,7 @@ export class ClaudeAcpAgent {
)
: initializationResult.models;
const models = await getAvailableModels(
const { modelState: models, resumedContextWindow } = await getAvailableModels(
q,
allowedModels,
initializationResult.models,
@@ -5139,31 +5408,40 @@ export class ClaudeAcpAgent {
initialEffort.currentValue !== "default"
) {
await q.applyFlagSettings({
effortLevel: initialEffort.currentValue as Settings["effortLevel"],
effortLevel: toSdkEffortLevel(initialEffort.currentValue),
});
}
// Seed the context window from the SDK's authoritative report. Text
// inference alone misses aliases that resolve to extended-context models
// with no "1m" token anywhere in their id or description (e.g. `sonnet` →
// claude-sonnet-5, natively ~1M): those streamed `usage_update.size:
// 200000` until the first result's modelUsage corrected it — again on
// every process restart or session re-creation, since the learned window
// lives only on the Session (issue #596).
// Seed the context window WITHOUT any extra IPC on the session/new path.
// On session/load, the resumed session's own `getContextUsage` report — a
// response `getAvailableModels` already awaited to learn the live model
// (resumed sessions ARE serviced pre-turn, unlike fresh ones) — is
// authoritative and wins. Otherwise: the cached authoritative window if a
// prior turn has learned it for this model (`result.modelUsage`,
// cross-session), else the text heuristic, else the default. We
// deliberately do NOT issue a getContextUsage call here: on a fresh
// session that control request is not serviced until the first prompt
// turn runs, so awaiting it — as 0.59.0 did — made session/new take ~15s
// (issues #886/#880). The authoritative window arrives on the first
// `result.modelUsage` and is cached from there.
//
// Text inference alone misses aliases that resolve to extended-context
// models with no "1m" token anywhere in their id or description (e.g.
// `sonnet` → claude-sonnet-5, natively ~1M): those stream
// `usage_update.size: 200000` until the first result's modelUsage corrects
// it — but the cache means only the FIRST session to ever run a turn on such
// a model eats that window, not every fresh session after a process
// restart (issue #596; a post-restart session/load is covered by the
// resumed report above).
//
// The inference fallback is deliberately keyed to the allowlisted entry: a
// fallback-resolved sibling's resolvedModel/displayName/description can
// describe a different context lane than the verbatim live id (e.g. an
// "opus[1m]" row matched for a bare 200k id), so on the fallback path only
// the id itself is a trustworthy window signal.
const contextWindowSize =
(await fetchContextWindowSize(q, this.logger)) ??
inferContextWindowFromModel(
models.currentModelId,
allowlistedModelInfo?.resolvedModel,
allowlistedModelInfo?.displayName,
allowlistedModelInfo?.description,
) ??
DEFAULT_CONTEXT_WINDOW;
const seededWindow =
resumedContextWindow !== null
? { size: resumedContextWindow, authoritative: true }
: immediateContextWindow(providerCacheKey, models.currentModelId, allowlistedModelInfo);
this.sessions[sessionId] = {
query: q,
@@ -5187,7 +5465,9 @@ export class ClaudeAcpAgent {
fastModeEnabled,
abortController,
emitRawSDKMessages: sessionMeta?.claudeCode?.emitRawSDKMessages ?? false,
contextWindowSize,
contextWindowSize: seededWindow.size,
contextWindowAuthoritative: seededWindow.authoritative,
providerCacheKey,
taskState,
toolUseCache: {},
emittedToolCalls: new Set(),
@@ -5289,29 +5569,81 @@ function snapshotFromUsage(usage: {
};
}
function createEnvForGateway(request?: GatewayAuthRequest) {
/**
* Adapt a legacy gateway `authenticate` request into the shared
* {@link ProviderConfig} shape. Returns `null` when no gateway request is
* present. `methodId` selects the protocol: `gateway-bedrock` → bedrock,
* otherwise anthropic.
*/
function gatewayRequestToProviderConfig(request?: GatewayAuthRequest): ProviderConfig | null {
if (!request?._meta) {
return null;
}
return {
apiType: request.methodId === "gateway-bedrock" ? "bedrock" : "anthropic",
baseUrl: request._meta.gateway.baseUrl,
headers: request._meta.gateway.headers,
};
}
/**
* Map a resolved provider config into the Claude Code env vars that redirect API
* traffic and inject headers. Returns an empty object when routing is
* unconfigured. The token/bypass placeholders (`" "`) are required so the CLI
* skips its normal login/credential checks when a gateway is in use.
*/
function createEnvForProvider(config: ProviderConfig | null): Record<string, string> {
if (!config) {
return {};
}
const customHeaders = Object.entries(request._meta.gateway.headers)
const customHeaders = Object.entries(config.headers)
.map(([key, value]) => `${key}: ${value}`)
.join("\n");
if (request.methodId === "gateway-bedrock") {
if (config.apiType === "bedrock") {
return {
CLAUDE_CODE_USE_BEDROCK: "1",
AWS_BEARER_TOKEN_BEDROCK: " ", // Must be non-empty to bypass pass configuration check
ANTHROPIC_BEDROCK_BASE_URL: request._meta.gateway.baseUrl,
ANTHROPIC_BEDROCK_BASE_URL: config.baseUrl,
ANTHROPIC_CUSTOM_HEADERS: customHeaders,
};
}
if (config.apiType === "vertex") {
// `config.vertex` is guaranteed present for vertex by `unstable_setProvider`
// validation; fall back to empty strings defensively.
return {
CLAUDE_CODE_USE_VERTEX: "1",
ANTHROPIC_VERTEX_BASE_URL: config.baseUrl,
ANTHROPIC_VERTEX_PROJECT_ID: config.vertex?.projectId ?? "",
CLOUD_ML_REGION: config.vertex?.region ?? "",
ANTHROPIC_CUSTOM_HEADERS: customHeaders,
};
}
return {
ANTHROPIC_BASE_URL: request._meta.gateway.baseUrl,
ANTHROPIC_BASE_URL: config.baseUrl,
ANTHROPIC_CUSTOM_HEADERS: customHeaders,
ANTHROPIC_AUTH_TOKEN: " ", // Must be specified to bypass claude login requirement
};
}
/**
* Validate a provider base URL: must be a non-empty absolute http(s) URL.
*/
function isValidBaseUrl(baseUrl: string): boolean {
if (typeof baseUrl !== "string" || baseUrl.trim() === "") {
return false;
}
let parsed: URL;
try {
parsed = new URL(baseUrl);
} catch {
return false;
}
return parsed.protocol === "http:" || parsed.protocol === "https:";
}
/**
* Build the list of permission modes the agent will advertise for the given
* model. `auto` is gated by `ModelInfo.supportsAutoMode === true`, which is
@@ -5371,9 +5703,12 @@ function buildAvailableModes(modelInfo: ModelInfo | undefined): SessionModeState
// and only clears a key when an explicit `null` is sent — see
// `applyFlagSettings` in @anthropic-ai/claude-agent-sdk. Mapping both the
// `"default"` sentinel and `undefined` (effort option absent for the model) to
// `null` ensures any previously-applied flag is actually cleared.
function toSdkEffortLevel(value: string | undefined): Settings["effortLevel"] | null {
return value === undefined || value === "default" ? null : (value as Settings["effortLevel"]);
// `null` ensures any previously-applied flag is actually cleared. Typed as
// `EffortLevel` (not `Settings["effortLevel"]`): the picker offers whatever
// `supportedEffortLevels` reports, which includes the session-scoped `"max"`
// that the persisted Settings shape deliberately excludes.
function toSdkEffortLevel(value: string | undefined): EffortLevel | null {
return value === undefined || value === "default" ? null : (value as EffortLevel);
}
// `supportedAgents()` always returns Claude Code's built-in subagents — the
@@ -5899,20 +6234,29 @@ export function applyAvailableModelsAllowlist(
/** Read the model a resumed session is actually running (via the
* `getContextUsage` control request — the same source `/context` prints) and
* map it onto the picker. Best-effort: a control-request failure is logged
* and returns null so callers keep their current choice; failing the whole
* session/load over an unreadable report would be worse. */
* map it onto the picker, along with the report's authoritative context
* window (`rawMaxTokens`). Resumed sessions get this request serviced before
* any turn runs in the new process — unlike fresh sessions, where it stalls
* until the first prompt turn (issues #886/#880) — so the same response that
* restores the live model (issue #845) also seeds the window for free,
* covering post-restart reloads of models the text heuristic misses (issue
* #596). Best-effort: a control-request failure is logged and returns nulls
* so callers keep their current choice; failing the whole session/load over
* an unreadable report would be worse. */
async function readResumedLiveModel(
query: Query,
models: ModelInfo[],
logger: Logger,
): Promise<ModelInfo | null> {
): Promise<{ model: ModelInfo | null; contextWindow: number | null }> {
try {
const liveModel = (await query.getContextUsage()).model;
return liveModel ? matchResumedModel(models, liveModel) : null;
const usage = await query.getContextUsage();
return {
model: usage.model ? matchResumedModel(models, usage.model) : null,
contextWindow: usage.rawMaxTokens > 0 ? usage.rawMaxTokens : null,
};
} catch (error) {
logger.error("Failed to read the resumed session's live model:", error);
return null;
return { model: null, contextWindow: null };
}
}
@@ -5923,11 +6267,16 @@ async function getAvailableModels(
settingsManager: SettingsManager,
logger: Logger,
isResumedSession: boolean,
): Promise<SessionModelState> {
): Promise<{ modelState: SessionModelState; resumedContextWindow: number | null }> {
const settings = settingsManager.getSettings();
let currentModel = models[0];
let resolvedFromInput: string | undefined;
// The context window reported alongside a resumed session's live model.
// Only ever non-null on the paths where `currentModel` IS the live model
// (no override, or a failed override re-assert), so the window always
// describes the model the session actually runs.
let resumedContextWindow: number | null = null;
// Model priority (highest to lowest):
// 1. ANTHROPIC_MODEL environment variable
@@ -5956,7 +6305,9 @@ async function getAvailableModels(
// the SDK is already running this model, and pushing a picker alias back
// (e.g. "opus[1m]") could change the live model rather than describe it.
if (resolvedFromInput === undefined && isResumedSession) {
currentModel = (await readResumedLiveModel(query, models, logger)) ?? currentModel;
const live = await readResumedLiveModel(query, models, logger);
currentModel = live.model ?? currentModel;
resumedContextWindow = live.contextWindow;
}
// Skip the setModel round-trip when we can prove the SDK has already landed
@@ -5989,17 +6340,22 @@ async function getAvailableModels(
// pin the session isn't running.
if (!isResumedSession) throw error;
logger.error(`Failed to re-assert model "${currentModel.value}" on resume:`, error);
currentModel = (await readResumedLiveModel(query, models, logger)) ?? currentModel;
const live = await readResumedLiveModel(query, models, logger);
currentModel = live.model ?? currentModel;
resumedContextWindow = live.contextWindow;
}
}
return {
availableModels: models.map((model) => ({
modelId: model.value,
name: model.displayName,
description: model.description,
})),
currentModelId: currentModel.value,
modelState: {
availableModels: models.map((model) => ({
modelId: model.value,
name: model.displayName,
description: model.description,
})),
currentModelId: currentModel.value,
},
resumedContextWindow,
};
}
@@ -6130,6 +6486,12 @@ export function promptToClaude(prompt: PromptRequest): SDKUserMessage {
},
session_id: prompt.sessionId,
parent_tool_use_id: null,
// ACP prompts are the user's own input relayed by the client. Stamp the
// provenance explicitly: per the SDK, a host wrapping keyboard input must
// send `{kind: "human"}` — an absent `origin` is treated as unattributed
// and fails closed at the CLI's strict isHuman() trust gates (e.g. the
// ultracode keyword opt-in honors only human-originated turns).
origin: { kind: "human" },
};
}
@@ -6872,6 +7234,9 @@ export function runAcp() {
agent.setSessionConfigOption(ctx.params),
)
.onRequest(methods.agent.authenticate, (ctx) => agent.authenticate(ctx.params))
.onRequest(methods.agent.providers.list, (ctx) => agent.unstable_listProviders(ctx.params))
.onRequest(methods.agent.providers.set, (ctx) => agent.unstable_setProvider(ctx.params))
.onRequest(methods.agent.providers.disable, (ctx) => agent.unstable_disableProvider(ctx.params))
.onRequest(methods.agent.logout, (ctx) => agent.logout(ctx.params))
.onRequest(methods.agent.session.prompt, (ctx) =>
runPromptWithCancellation(agent, ctx.params, ctx.signal),
@@ -6891,9 +7256,9 @@ function commonPrefixLength(a: string, b: string) {
return i;
}
/** Best-effort first guess of a model's context window, used only as a
* fallback when the SDK's authoritative `getContextUsage` is unavailable (and
* until a `result` message arrives with the `modelUsage` value).
/** Best-effort first guess of a model's context window, used to seed the
* window synchronously (via `immediateContextWindow`) until a `result` message
* arrives with the authoritative `modelUsage` value.
*
* Anthropic 1M-context variants encode "1m" as a distinct token in the SDK
* model ID (e.g., "claude-opus-4-6-1m"), which `\b1m\b` catches without also
@@ -6903,9 +7268,12 @@ function commonPrefixLength(a: string, b: string) {
* "claude-opus-4-8[1m]", "Opus 4.7 (1M context)"), so callers pass those too.
* This text scan can't catch every model — some resolve to extended-context
* models with no "1m" anywhere (e.g. `sonnet` → claude-sonnet-5, natively
* ~1M) — which is why `fetchContextWindowSize` is preferred wherever a live
* query is available. A miss falls back to the default window and is
* corrected by `result.modelUsage` within one turn. */
* ~1M). Such a miss falls back to the default window and is corrected by
* `result.modelUsage` (and cached) within one turn. We do NOT consult the
* SDK's `getContextUsage` to close that gap: on a fresh session it is not
* serviced before the first prompt turn (issues #886/#880, see
* `contextWindowCache`; resumed sessions do get it, via
* `readResumedLiveModel`). */
function inferContextWindowFromModel(...texts: Array<string | undefined>): number | null {
if (texts.some((text) => text != null && /\b1m\b/i.test(text))) return 1_000_000;
return null;
@@ -6926,28 +7294,112 @@ async function fetchContextUsedTokens(query: Query, logger: Logger): Promise<num
}
}
/** Fetch the current model's full context window (`rawMaxTokens`) via the
* `getContextUsage` control request — the same source `/context` prints.
* This is the only pre-`result` signal that covers semantic aliases whose
* text carries no "1m" token (e.g. `sonnet` → claude-sonnet-5, natively
* ~1M), so it's the primary window source at session creation and on model
* switches; `inferContextWindowFromModel` remains the fallback. A returned
* window is still superseded by each `result.modelUsage.contextWindow`.
/** Cross-session cache of authoritative context windows, keyed by
* `${providerCacheKey}\0${modelId}` (see {@link contextWindowCacheKey}).
* The window is a property of (model id, backend): the same resolved model id
* (e.g. "claude-sonnet-5[1m]", the spelling of the `result.modelUsage` keys)
* can name different context lanes behind different base URLs, routing
* headers, or credentials, so the key carries both. Caching it module-level
* lets a later session/new or switch that resolves to the same (backend,
* model) — in this session or any other, within the adapter's lifetime — seed
* the correct window synchronously with no IPC. Keying on the resolved id
* (rather than the picker value) means aliases that resolve to the same
* concrete model share one entry; the result handler additionally writes the
* bare assistant-message spelling so seed-time reads that fall back to a
* verbatim live id (rows without `resolvedModel`) can hit too.
*
* (Older CLIs under-reported extended 1M windows here — commit 20ef663
* dropped the field for that reason — but the CLI vendored by the pinned
* SDK reports them correctly again.) Returns `null` on any control-request
* failure or a nonsensical (non-positive) window. */
async function fetchContextWindowSize(query: Query, logger: Logger): Promise<number | null> {
try {
const usage = await query.getContextUsage();
return usage.rawMaxTokens > 0 ? usage.rawMaxTokens : null;
} catch (error) {
logger.error("Failed to fetch context window size from SDK:", error);
return null;
* Populated authoritatively by each `result.modelUsage` a turn confirms (see
* the consumer's result handler). We deliberately never populate it from a
* fresh session's `getContextUsage`: before that session's first prompt turn
* has run the control request is not serviced (it stalls ~15s, and serializes
* ahead of an awaited `setModel` — issues #886/#880, regressed in 0.59.0), so
* it can neither beat the first `result` nor be issued cheaply before one.
* Resumed sessions are the exception — their report IS serviced pre-turn, and
* the session/load path seeds (but does not cache) the window from the same
* response that restores the live model, see `readResumedLiveModel`.
* Cleared on `logout`: 1M-context entitlement can differ per account/tier, so
* windows learned under one login must not seed sessions under the next. */
const contextWindowCache = new Map<string, number>();
/** The env vars that determine which LLM backend — and which context lane on
* it — a query's API traffic reaches: endpoint selection (base URLs and the
* Bedrock/Vertex switches with their project/region), routing/beta headers
* (an `anthropic-beta: context-1m-…` header flips the same model id at the
* same endpoint between context lanes), and credential identity (extended
* context is entitlement-gated per account). Used to derive the
* provider-cache key from the exact env a query is created with, so
* `providers/set` config, per-session `_meta` env overrides, and ambient
* process env are all distinguished exactly as the CLI will see them. */
const PROVIDER_ROUTING_ENV_VARS = [
"ANTHROPIC_BASE_URL",
"ANTHROPIC_BEDROCK_BASE_URL",
"ANTHROPIC_VERTEX_BASE_URL",
"CLAUDE_CODE_USE_BEDROCK",
"CLAUDE_CODE_USE_VERTEX",
"ANTHROPIC_VERTEX_PROJECT_ID",
"CLOUD_ML_REGION",
"AWS_REGION",
"ANTHROPIC_CUSTOM_HEADERS",
"ANTHROPIC_API_KEY",
"ANTHROPIC_AUTH_TOKEN",
] as const;
/** Stable identifier for the LLM backend a session's query is created against,
* used to scope {@link contextWindowCache} per backend. Positional `\0`-join
* of {@link PROVIDER_ROUTING_ENV_VARS} values, so no segment can masquerade
* as another and unset vars everywhere yield one stable "default" bucket.
* Header/credential values can be secrets; the key only ever lives as an
* in-memory Map key and is never logged or surfaced. Over-keying is the safe
* side: a var change that didn't really change the backend costs one cache
* miss (heuristic seed until the next result), while under-keying would serve
* one backend's window for another's. */
function providerCacheKeyFor(env: Record<string, string | undefined>): string {
return PROVIDER_ROUTING_ENV_VARS.map((name) => env[name] ?? "").join("\0");
}
/** Compose the `contextWindowCache` key from a session's provider key and a
* model id. `\0`-joined so the model segment can't collide with a provider
* segment. */
function contextWindowCacheKey(providerCacheKey: string, modelId: string): string {
return `${providerCacheKey}\0${modelId}`;
}
function cacheContextWindow(modelKey: string, window: number): void {
if (window > 0) {
contextWindowCache.set(modelKey, window);
}
}
/** The context window to report *right now* for a model, with NO IPC on the
* critical path: the cached authoritative value if we've learned it (from a
* prior turn's `result.modelUsage`, this or any session on the same backend),
* else the text heuristic over the model row's identity strings, else the
* default. Derives the cache key itself — `modelInfo?.resolvedModel ?? modelId`,
* the same rule at every seed site — so read keys can't drift from the write
* site's spelling. `authoritative` reports whether the value came from the
* cache: an authoritative window can legitimately equal
* DEFAULT_CONTEXT_WINDOW, so the value alone can't tell the caller. */
function immediateContextWindow(
providerCacheKey: string,
modelId: string,
modelInfo?: Pick<ModelInfo, "resolvedModel" | "displayName" | "description">,
): { size: number; authoritative: boolean } {
const cached = contextWindowCache.get(
contextWindowCacheKey(providerCacheKey, modelInfo?.resolvedModel ?? modelId),
);
if (cached !== undefined) return { size: cached, authoritative: true };
return {
size:
inferContextWindowFromModel(
modelId,
modelInfo?.resolvedModel,
modelInfo?.displayName,
modelInfo?.description,
) ?? DEFAULT_CONTEXT_WINDOW,
authoritative: false,
};
}
/** Translate the legacy `MAX_THINKING_TOKENS` env var into the SDK's `thinking`
* option. The `maxThinkingTokens` option it used to feed is deprecated and
* reduced to on/off on current models, so map the value to explicit thinking
@@ -6994,6 +7446,12 @@ function getMatchingModelUsage(modelUsage: Record<string, ModelUsage>, currentMo
}
if (bestKey) {
return modelUsage[bestKey];
// `bestKey` is the SDK's resolved model id (e.g. "claude-sonnet-5[1m]"),
// the same spelling as ModelInfo.resolvedModel — the primary key the
// window is cached under. `currentModel` (the assistant message's
// `.model`) can be the bare form (e.g. "claude-sonnet-5"); the result
// handler caches under that spelling too, for seed-time reads that fall
// back to a bare id (rows without `resolvedModel`).
return { key: bestKey, usage: modelUsage[bestKey] };
}
}

View File

@@ -157,6 +157,8 @@ function mockSessionState(overrides: Record<string, any> = {}) {
abortController: new AbortController(),
emitRawSDKMessages: false,
contextWindowSize: 200000,
contextWindowAuthoritative: false,
providerCacheKey: "default",
taskState: new Map(),
toolUseCache: {},
emittedToolCalls: new Set(),
@@ -1947,6 +1949,8 @@ describe("permission request cancellation", () => {
abortController: new AbortController(),
emitRawSDKMessages: false,
contextWindowSize: 200000,
contextWindowAuthoritative: false,
providerCacheKey: "default",
taskState: new Map(),
toolUseCache: {},
emittedToolCalls: new Set(),
@@ -2300,6 +2304,63 @@ describe("tool_call emitted before permission request", () => {
});
});
describe("canUseTool in bypassPermissions mode", () => {
function setup() {
const events: string[] = [];
const mockClient = {
sessionUpdate: async () => {},
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({
modes: { currentModeId: "bypassPermissions", availableModes: [] },
});
// The tool_call was already surfaced by the streamed tool_use chunk, so
// the permission flow (when taken) goes straight to requestPermission.
agent.sessions["session-1"]!.emittedToolCalls.add("tool-1");
return { agent, events };
}
it("auto-allows asks that carry no matchedAskRule", async () => {
const { agent, events } = setup();
const result = await agent.canUseTool("session-1")("Bash", { command: "ls" }, {
signal: new AbortController().signal,
suggestions: [],
toolUseID: "tool-1",
} as any);
expect(events).toEqual([]);
expect(result).toMatchObject({ behavior: "allow" });
});
// The asks that still reach canUseTool in bypass mode are the ones the CLI
// insists on prompting for even under --dangerously-skip-permissions. When
// one was forced by the user's own permissions.ask rule (matchedAskRule),
// honoring that rule beats bypass: it must go to the client instead of
// being silently auto-allowed.
it("prompts the client when the ask was forced by a permissions.ask rule", async () => {
const { agent, events } = setup();
const result = await agent.canUseTool("session-1")("Bash", { command: "terraform destroy" }, {
signal: new AbortController().signal,
suggestions: [],
toolUseID: "tool-1",
matchedAskRule: {
source: "projectSettings",
toolName: "Bash",
ruleContent: "Bash(terraform:*)",
},
} as any);
expect(events).toEqual(["permission"]);
expect(result).toMatchObject({ behavior: "allow" });
});
});
describe("subagent permission attribution (issue #851)", () => {
// A background subagent's permission requests reach canUseTool with only an
// `agentID`; the streamed subagent messages carry `parent_tool_use_id`
@@ -3730,6 +3791,8 @@ describe("session/close", () => {
abortController: new AbortController(),
emitRawSDKMessages: false,
contextWindowSize: 200000,
contextWindowAuthoritative: false,
providerCacheKey: "default",
taskState: new Map(),
toolUseCache: {},
emittedToolCalls: new Set(),
@@ -3820,6 +3883,8 @@ describe("session/delete", () => {
abortController: new AbortController(),
emitRawSDKMessages: false,
contextWindowSize: 200000,
contextWindowAuthoritative: false,
providerCacheKey: "default",
taskState: new Map(),
toolUseCache: {},
emittedToolCalls: new Set(),
@@ -3927,6 +3992,8 @@ describe("getOrCreateSession param change detection", () => {
abortController: new AbortController(),
emitRawSDKMessages: false,
contextWindowSize: 200000,
contextWindowAuthoritative: false,
providerCacheKey: "default",
taskState: new Map(),
toolUseCache: {},
emittedToolCalls: new Set(),
@@ -5045,6 +5112,359 @@ describe("usage_update computation", () => {
expect(usageUpdate.update.size).toBe(200000);
expect(session.contextWindowSize).toBe(200000);
});
it("caches the turn's authoritative window under the resolved id and serves it on a later switch, with no getContextUsage IPC", async () => {
// End-to-end for the cross-session context-window cache:
// - WRITE: a turn's result.modelUsage is the only authoritative window. The
// assistant message reports the BARE model id ("…-9") while modelUsage is
// keyed by the RESOLVED id ("…-9[1m]"); the cache must be written under the
// resolved key (matched by getMatchingModelUsage), the same spelling as
// ModelInfo.resolvedModel — otherwise a later read never hits.
// - READ: switching to a picker value whose resolvedModel is that key seeds
// the window synchronously from the cache, with NO getContextUsage.
// 777_000 is chosen so it can only come from the cache: text inference on the
// resolved id "…-9[1m]" would yield 1_000_000 (the "1m" token), the default
// is 200_000, and the pre-switch sentinel is 123_456.
//
// The `contextWindowCache` is module-global and this file does not
// vi.resetModules() per test, so it persists across tests. This test stays
// isolated by using a unique resolved id ("claude-cachehit-probe-9[1m]") that
// no other test writes or reads — the convention here, since a statically
// imported module's cache can't be cleared from a test.
const RESOLVED_ID = "claude-cachehit-probe-9[1m]";
const { agent } = createMockAgentWithCapture();
injectSession(agent, [
createAssistantMessage({ model: "claude-cachehit-probe-9" }),
createResultMessageWithModel({
modelUsage: {
[RESOLVED_ID]: {
inputTokens: 1,
outputTokens: 1,
cacheReadInputTokens: 0,
cacheCreationInputTokens: 0,
webSearchRequests: 0,
costUSD: 0,
contextWindow: 777_000,
maxOutputTokens: 16384,
},
},
}),
{ type: "system", subtype: "session_state_changed", state: "idle" },
]);
await agent.prompt({ sessionId: "test-session", prompt: [{ type: "text", text: "go" }] });
const session = agent.sessions["test-session"]!;
// The turn learned the window from modelUsage even though the assistant
// message carried the bare id — confirms the write matched on the resolved key.
expect(session.contextWindowSize).toBe(777_000);
// Now switch to a picker value that resolves to the cached id. Seed a
// sentinel and a getContextUsage spy first: a cache miss would surface as
// 1_000_000 (inference on the "1m" id), and any IPC as a spy call.
const getContextUsage = vi.fn(async () => ({ rawMaxTokens: 200000 }));
(session.query as any).getContextUsage = getContextUsage;
session.contextWindowSize = 123_456;
session.models = { currentModelId: "default", availableModels: [] };
session.modelInfos = [
{
value: "probe-alias",
displayName: "Probe",
description: "probe model",
resolvedModel: RESOLVED_ID,
},
] as any;
session.configOptions = [
{
id: "model",
name: "Model",
type: "select",
category: "model",
currentValue: "default",
options: [{ value: "probe-alias", name: "Probe" }],
},
] as any;
await agent.setSessionConfigOption({
sessionId: "test-session",
configId: "model",
value: "probe-alias",
});
expect(getContextUsage).not.toHaveBeenCalled();
expect(session.contextWindowSize).toBe(777_000);
});
it("scopes the window cache per provider: a switch on a different provider does not read another provider's window", async () => {
// The window is a property of (model id, provider). A turn on provider-A
// learns 500_000 for RESOLVED_ID; a switch to the SAME resolved id on
// provider-B must NOT read it (falls to inference → 1_000_000 for the "1m"
// id), while a switch on provider-A DOES read it (500_000). All three
// outcomes are distinct: cached 500_000 vs inference 1_000_000 vs default.
// Uses a unique resolved id ("claude-provkey-probe[1m]") so the module-global
// cache (not reset per test in this file) can't cross this test with others.
const RESOLVED_ID = "claude-provkey-probe[1m]";
const { agent } = createMockAgentWithCapture();
// Provider-A session learns the authoritative window on a turn.
injectSession(agent, [
createAssistantMessage({ model: "claude-provkey-probe" }),
createResultMessageWithModel({
modelUsage: {
[RESOLVED_ID]: {
inputTokens: 1,
outputTokens: 1,
cacheReadInputTokens: 0,
cacheCreationInputTokens: 0,
webSearchRequests: 0,
costUSD: 0,
contextWindow: 500_000,
maxOutputTokens: 16384,
},
},
}),
{ type: "system", subtype: "session_state_changed", state: "idle" },
]);
const sessionA = agent.sessions["test-session"]!;
sessionA.providerCacheKey = "apiType-A https://a.example";
await agent.prompt({ sessionId: "test-session", prompt: [{ type: "text", text: "go" }] });
expect(sessionA.contextWindowSize).toBe(500_000);
const modelInfos = [
{
value: "alias",
displayName: "Alias",
description: "probe model",
resolvedModel: RESOLVED_ID,
},
];
const configOptions = [
{
id: "model",
name: "Model",
type: "select",
category: "model",
currentValue: "default",
options: [{ value: "alias", name: "Alias" }],
},
];
// A DIFFERENT provider seeing the same resolved id must not inherit A's
// window — it falls to inference (1_000_000), not the cached 500_000.
agent.sessions["session-B"] = mockSessionState({
providerCacheKey: "apiType-B https://b.example",
models: { currentModelId: "default", availableModels: [] },
modelInfos,
configOptions,
query: {
setModel: vi.fn(async () => {}),
setPermissionMode: vi.fn(async () => {}),
applyFlagSettings: vi.fn(async () => {}),
getContextUsage: vi.fn(async () => ({ rawMaxTokens: 200000 })),
supportedCommands: vi.fn(async () => []),
},
});
await agent.setSessionConfigOption({
sessionId: "session-B",
configId: "model",
value: "alias",
});
expect(agent.sessions["session-B"]!.contextWindowSize).toBe(1_000_000);
// The SAME provider (A) switching to that id DOES read the cached window.
sessionA.contextWindowSize = 123_456; // sentinel
sessionA.models = { currentModelId: "default", availableModels: [] };
sessionA.modelInfos = modelInfos as any;
sessionA.configOptions = configOptions as any;
await agent.setSessionConfigOption({
sessionId: "test-session",
configId: "model",
value: "alias",
});
expect(sessionA.contextWindowSize).toBe(500_000);
});
it("ignores a nonsensical (non-positive) reported window: keeps the prior window and doesn't poison the cache", async () => {
// A result.modelUsage that reports a non-positive contextWindow (observed
// from third-party backends) must not overwrite the window learned earlier,
// nor be written to the cross-session cache. Unique id keeps this isolated
// from other tests sharing the module-global cache (no resetModules here).
const RESOLVED_ID = "claude-nonpositive-probe[1m]";
const { agent } = createMockAgentWithCapture();
injectSession(agent, [
createAssistantMessage({ model: "claude-nonpositive-probe" }),
createResultMessageWithModel({
modelUsage: {
[RESOLVED_ID]: {
inputTokens: 1,
outputTokens: 1,
cacheReadInputTokens: 0,
cacheCreationInputTokens: 0,
webSearchRequests: 0,
costUSD: 0,
contextWindow: 0, // nonsensical
maxOutputTokens: 16384,
},
},
}),
{ type: "system", subtype: "session_state_changed", state: "idle" },
]);
const session = agent.sessions["test-session"]!;
session.contextWindowSize = 900_000; // a window learned on a prior turn
await agent.prompt({ sessionId: "test-session", prompt: [{ type: "text", text: "go" }] });
// The bad window was ignored — the prior window is preserved.
expect(session.contextWindowSize).toBe(900_000);
// And it never reached the cache: a switch to that id falls to inference
// (1_000_000 for the "1m" id), not the bad 0.
session.contextWindowSize = 123_456; // sentinel
session.models = { currentModelId: "default", availableModels: [] };
session.modelInfos = [
{ value: "alias", displayName: "Alias", description: "probe", resolvedModel: RESOLVED_ID },
] as any;
session.configOptions = [
{
id: "model",
name: "Model",
type: "select",
category: "model",
currentValue: "default",
options: [{ value: "alias", name: "Alias" }],
},
] as any;
await agent.setSessionConfigOption({
sessionId: "test-session",
configId: "model",
value: "alias",
});
expect(session.contextWindowSize).toBe(1_000_000);
});
it("does not let the message_start heuristic clobber an authoritative 200k window", async () => {
// An authoritative window can legitimately equal DEFAULT_CONTEXT_WINDOW
// (e.g. a third-party backend serving a 200k lane under a "[1m]"-spelled
// id). The message_start upgrade must key off the authoritative flag, not
// the value — otherwise the "1m" text match overwrites the cache-seeded
// 200k with 1M mid-stream on every turn.
const { agent, updates } = createMockAgentWithCapture();
injectSession(agent, [
createStreamEvent("message_start", {
model: "claude-authprobe-1[1m]",
usage: {
input_tokens: 100,
output_tokens: 10,
cache_read_input_tokens: 0,
cache_creation_input_tokens: 0,
},
}),
// Empty modelUsage: the result settles the turn without supplying a
// window of its own, so the assertion isolates the message_start path.
createResultMessageWithModel({ modelUsage: {} }),
{ type: "system", subtype: "session_state_changed", state: "idle" },
]);
const session = agent.sessions["test-session"]!;
// Simulate a cache-seeded authoritative window that equals the default.
session.contextWindowSize = 200000;
session.contextWindowAuthoritative = true;
await agent.prompt({ sessionId: "test-session", prompt: [{ type: "text", text: "go" }] });
expect(session.contextWindowSize).toBe(200000);
const usageUpdates = updates.filter((u: any) => u.update?.sessionUpdate === "usage_update");
for (const u of usageUpdates) {
expect(u.update.size).toBe(200000);
}
});
it("still upgrades a heuristic default window from the message_start model id", async () => {
// Companion to the authoritative-200k test: with no authoritative seed the
// old behavior stands — a "1m"-carrying live model id upgrades the default
// mid-stream so usage_update reports the right size before the result.
const { agent } = createMockAgentWithCapture();
injectSession(agent, [
createStreamEvent("message_start", {
model: "claude-authprobe-2[1m]",
usage: {
input_tokens: 100,
output_tokens: 10,
cache_read_input_tokens: 0,
cache_creation_input_tokens: 0,
},
}),
// Empty modelUsage: settles the turn without a window of its own.
createResultMessageWithModel({ modelUsage: {} }),
{ type: "system", subtype: "session_state_changed", state: "idle" },
]);
const session = agent.sessions["test-session"]!;
expect(session.contextWindowAuthoritative).toBe(false);
await agent.prompt({ sessionId: "test-session", prompt: [{ type: "text", text: "go" }] });
expect(session.contextWindowSize).toBe(1_000_000);
});
it("caches the turn's window under the bare assistant-message id too, so resolvedModel-less rows can hit", async () => {
// Seed-time reads fall back to the picker value / verbatim live id when a
// model row carries no resolvedModel (the synthesized out-of-allowlist
// resume row sets it undefined on purpose). Those spellings match the
// assistant message's bare `.model`, not the "[1m]"-decorated modelUsage
// key, so the result handler must write both spellings — otherwise such
// rows silently never hit the cache. 555_000 can only come from the cache:
// inference on the bare id (no "1m" token) yields the 200_000 default, and
// the pre-switch sentinel is 123_456.
const BARE_ID = "claude-bareprobe-7";
const { agent } = createMockAgentWithCapture();
injectSession(agent, [
createAssistantMessage({ model: BARE_ID }),
createResultMessageWithModel({
modelUsage: {
[`${BARE_ID}[1m]`]: {
inputTokens: 1,
outputTokens: 1,
cacheReadInputTokens: 0,
cacheCreationInputTokens: 0,
webSearchRequests: 0,
costUSD: 0,
contextWindow: 555_000,
maxOutputTokens: 16384,
},
},
}),
{ type: "system", subtype: "session_state_changed", state: "idle" },
]);
const session = agent.sessions["test-session"]!;
await agent.prompt({ sessionId: "test-session", prompt: [{ type: "text", text: "go" }] });
expect(session.contextWindowSize).toBe(555_000);
// Switch to a row registered under the bare id with NO resolvedModel —
// the shape of the out-of-allowlist resume row.
session.contextWindowSize = 123_456; // sentinel
session.contextWindowAuthoritative = false;
session.models = { currentModelId: "default", availableModels: [] };
session.modelInfos = [{ value: BARE_ID, displayName: "Bare", description: "probe" }] as any;
session.configOptions = [
{
id: "model",
name: "Model",
type: "select",
category: "model",
currentValue: "default",
options: [{ value: BARE_ID, name: "Bare" }],
},
] as any;
await agent.setSessionConfigOption({
sessionId: "test-session",
configId: "model",
value: BARE_ID,
});
expect(session.contextWindowSize).toBe(555_000);
expect(session.contextWindowAuthoritative).toBe(true);
});
});
describe("assembled assistant text fallback", () => {
@@ -6396,6 +6816,8 @@ describe("post-error recovery", () => {
abortController: new AbortController(),
emitRawSDKMessages: false,
contextWindowSize: 200000,
contextWindowAuthoritative: false,
providerCacheKey: "default",
taskState: new Map(),
toolUseCache: {},
emittedToolCalls: new Set(),
@@ -9107,6 +9529,8 @@ describe("session/cancel wedge recovery (issue #680)", () => {
abortController: new AbortController(),
emitRawSDKMessages: false,
contextWindowSize: 200000,
contextWindowAuthoritative: false,
providerCacheKey: "default",
taskState: new Map(),
toolUseCache: {},
emittedToolCalls: new Set(),
@@ -10376,6 +10800,8 @@ describe("agent selection config option", () => {
abortController: new AbortController(),
emitRawSDKMessages: false,
contextWindowSize: 200000,
contextWindowAuthoritative: false,
providerCacheKey: "default",
taskState: new Map(),
toolUseCache: {},
emittedToolCalls: new Set(),

View File

@@ -7,7 +7,7 @@ import * as os from "node:os";
import * as path from "node:path";
let capturedOptions: Options | undefined;
let contextUsageResult: (() => Promise<{ rawMaxTokens: number }>) | undefined;
let contextUsageResult: (() => Promise<{ rawMaxTokens: number; model?: string }>) | undefined;
vi.mock("@anthropic-ai/claude-agent-sdk", async () => {
const actual = await vi.importActual<typeof import("@anthropic-ai/claude-agent-sdk")>(
"@anthropic-ai/claude-agent-sdk",
@@ -625,38 +625,101 @@ describe("createSession options merging", () => {
return (agent as unknown as { sessions: Record<string, any> }).sessions[sessionId];
}
it("seeds contextWindowSize from the SDK's getContextUsage report", async () => {
// Aliases like `sonnet` can resolve to an extended-context model with no
// "1m" token anywhere in id/displayName/description, so the authoritative
// report must win over text inference (issue #596).
it("does not call getContextUsage during session creation", async () => {
// getContextUsage stalls until the session's first prompt turn has run
// (it is not serviced pre-turn), so session/new must never call it —
// awaiting it inline is what regressed session/new latency in 0.59.0.
const ctxSpy = vi.fn(async () => ({ rawMaxTokens: 967000 }));
contextUsageResult = ctxSpy;
await agent.newSession({ cwd: process.cwd(), mcpServers: [] });
expect(ctxSpy).not.toHaveBeenCalled();
});
it("seeds contextWindowSize from text inference, falling back to the default when it misses", async () => {
// The mock model ("claude-sonnet-4-6" / "Claude Sonnet" / "Fast") carries
// no "1m" token anywhere, so inference misses and the window falls back to
// the default; the authoritative value arrives later via result.modelUsage.
contextUsageResult = async () => ({ rawMaxTokens: 967000 });
const response = await agent.newSession({ cwd: process.cwd(), mcpServers: [] });
expect(sessionFor(response.sessionId).contextWindowSize).toBe(967000);
expect(sessionFor(response.sessionId).contextWindowSize).toBe(200000);
expect(sessionFor(response.sessionId).contextWindowAuthoritative).toBe(false);
});
it("falls back to the default window when getContextUsage fails and inference misses", async () => {
// getContextUsage rejects, and the mock model ("claude-sonnet-4-6" /
// "Claude Sonnet" / "Fast") has no "1m" hint.
contextUsageResult = () => Promise.reject(new Error("no context usage mocked"));
// The rejection is deliberate — capture the agent's warning instead of
// letting it hit the console.
const errorSpy = vi.fn();
(agent as any).logger = { log: () => {}, error: errorSpy };
it("session/load seeds the window from the resumed session's getContextUsage report", async () => {
// Resumed sessions get getContextUsage serviced pre-turn (issue #845 uses
// it to restore the live model), and the same response carries the
// authoritative window (`rawMaxTokens`). After a process restart the
// module cache is empty and text inference misses natively-1M aliases, so
// discarding this in-hand value would replay the issue-#596 flicker on
// every reload — the flagship scenario. 888_000 can only come from the
// report: inference on the mock model yields null → 200_000 default.
contextUsageResult = async () => ({ rawMaxTokens: 888_000, model: "claude-sonnet-4-6" });
const response = await agent.newSession({ cwd: process.cwd(), mcpServers: [] });
await (
agent as unknown as {
createSession: (params: object, opts: { resume?: string }) => Promise<unknown>;
}
).createSession({ cwd: process.cwd(), mcpServers: [] }, { resume: "resumed-window-probe" });
expect(sessionFor(response.sessionId).contextWindowSize).toBe(200000);
expect(errorSpy).toHaveBeenCalled();
const session = sessionFor("resumed-window-probe");
expect(session.contextWindowSize).toBe(888_000);
expect(session.contextWindowAuthoritative).toBe(true);
});
it("ignores a nonsensical (non-positive) reported window", async () => {
contextUsageResult = async () => ({ rawMaxTokens: 0 });
it("scopes providerCacheKey by per-session env routing", async () => {
// The context-window cache key must distinguish backends exactly as the
// CLI will see them: a session routed to a proxy via _meta env shares a
// model id spelling with default-routed sessions but not a context lane,
// so it must land in its own cache bucket (and two default-routed
// sessions must share one).
const r1 = await agent.newSession({ cwd: process.cwd(), mcpServers: [] });
const r2 = await agent.newSession({
cwd: process.cwd(),
mcpServers: [],
_meta: {
claudeCode: {
options: { env: { ANTHROPIC_BASE_URL: "https://window-probe-proxy.example" } },
},
},
});
const r3 = await agent.newSession({ cwd: process.cwd(), mcpServers: [] });
const response = await agent.newSession({ cwd: process.cwd(), mcpServers: [] });
expect(sessionFor(r2.sessionId).providerCacheKey).not.toBe(
sessionFor(r1.sessionId).providerCacheKey,
);
expect(sessionFor(r3.sessionId).providerCacheKey).toBe(
sessionFor(r1.sessionId).providerCacheKey,
);
});
expect(sessionFor(response.sessionId).contextWindowSize).toBe(200000);
it("scopes providerCacheKey by provider headers: same endpoint, different headers → different buckets", async () => {
// Two providers/set configs sharing apiType+baseUrl but differing in
// headers (e.g. an `anthropic-beta: context-1m-…` routing header) can
// serve different context lanes for the same model id, so they must not
// share a window-cache bucket.
await agent.unstable_setProvider({
providerId: "main",
apiType: "anthropic",
baseUrl: "https://gw.example",
headers: {},
});
const plain = await agent.newSession({ cwd: process.cwd(), mcpServers: [] });
await agent.unstable_setProvider({
providerId: "main",
apiType: "anthropic",
baseUrl: "https://gw.example",
headers: { "anthropic-beta": "context-1m-2025-08-07" },
});
const beta = await agent.newSession({ cwd: process.cwd(), mcpServers: [] });
expect(sessionFor(beta.sessionId).providerCacheKey).not.toBe(
sessionFor(plain.sessionId).providerCacheKey,
);
});
});

303
src/tests/providers.test.ts Normal file
View File

@@ -0,0 +1,303 @@
import { describe, expect, it, Mock, vi, afterEach, beforeEach } from "vitest";
import { AcpClient, ClaudeAcpAgent } from "../acp-agent.js";
const mockQuery = vi.hoisted(() =>
vi.fn(() => ({
initializationResult: vi.fn().mockResolvedValue({
models: [
{ value: "id", displayName: "name", description: "description", supportsAutoMode: true },
],
}),
setModel: vi.fn(),
setPermissionMode: vi.fn(),
supportedCommands: vi.fn().mockResolvedValue([]),
})),
);
vi.mock("@anthropic-ai/claude-agent-sdk", async () => ({
...(await vi.importActual<typeof import("@anthropic-ai/claude-agent-sdk")>(
"@anthropic-ai/claude-agent-sdk",
)),
query: mockQuery,
}));
// `logout` shells out to `claude auth logout`; make execFile succeed so the
// real logout body runs (and clears provider config) without a live CLI.
vi.mock("node:child_process", async () => {
const actual = await vi.importActual<typeof import("node:child_process")>("node:child_process");
return {
...actual,
execFile: vi.fn((_file: string, _args: string[], cb: (...a: unknown[]) => void) =>
cb(null, { stdout: "", stderr: "" }),
),
};
});
describe("providers", () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.runAllTimers();
vi.useRealTimers();
vi.unstubAllGlobals();
vi.resetAllMocks();
});
async function createAgentMock(): Promise<[ClaudeAcpAgent, Mock]> {
const connectionMock = {
sessionUpdate: async (_: any) => {},
} as AcpClient;
const agent = new ClaudeAcpAgent(connectionMock);
return [agent, mockQuery];
}
it("advertises the providers capability in initialize", async () => {
const [agent] = await createAgentMock();
const response = await agent.initialize({ protocolVersion: 1, clientCapabilities: {} });
expect(response.agentCapabilities?.providers).toEqual({});
});
it("lists a single optional 'main' provider, unconfigured by default", async () => {
const [agent] = await createAgentMock();
const response = await agent.unstable_listProviders({});
expect(response.providers).toEqual([
{
providerId: "main",
supported: ["anthropic", "bedrock", "vertex"],
required: false,
current: null,
},
]);
});
it("reflects apiType/baseUrl after set, and never echoes headers", async () => {
const [agent] = await createAgentMock();
await agent.unstable_setProvider({
providerId: "main",
apiType: "anthropic",
baseUrl: "https://gateway.example/v1",
headers: { authorization: "Bearer secret" },
});
const response = await agent.unstable_listProviders({});
expect(response.providers[0].current).toEqual({
apiType: "anthropic",
baseUrl: "https://gateway.example/v1",
});
// headers must not leak through list
expect(JSON.stringify(response.providers)).not.toContain("secret");
});
it("rejects set for an unknown providerId", async () => {
const [agent] = await createAgentMock();
await expect(
agent.unstable_setProvider({
providerId: "openai",
apiType: "anthropic",
baseUrl: "https://gateway.example",
}),
).rejects.toMatchObject({ code: -32602 });
});
it("rejects set for an unsupported apiType", async () => {
const [agent] = await createAgentMock();
await expect(
agent.unstable_setProvider({
providerId: "main",
apiType: "openai",
baseUrl: "https://gateway.example",
}),
).rejects.toMatchObject({ code: -32602 });
});
it("rejects set for an empty or non-http baseUrl", async () => {
const [agent] = await createAgentMock();
await expect(
agent.unstable_setProvider({ providerId: "main", apiType: "anthropic", baseUrl: "" }),
).rejects.toMatchObject({ code: -32602 });
await expect(
agent.unstable_setProvider({
providerId: "main",
apiType: "anthropic",
baseUrl: "ftp://nope.example",
}),
).rejects.toMatchObject({ code: -32602 });
});
it("disables the 'main' provider by clearing config and reporting current: null", async () => {
const [agent] = await createAgentMock();
await agent.unstable_setProvider({
providerId: "main",
apiType: "anthropic",
baseUrl: "https://gateway.example/v1",
});
expect((await agent.unstable_listProviders({})).providers[0].current).not.toBeNull();
await expect(agent.unstable_disableProvider({ providerId: "main" })).resolves.toEqual({});
expect((await agent.unstable_listProviders({})).providers[0].current).toBeNull();
});
it("treats disabling an unknown provider as an idempotent no-op", async () => {
const [agent] = await createAgentMock();
await expect(agent.unstable_disableProvider({ providerId: "openai" })).resolves.toEqual({});
});
it("routes anthropic provider config into session env", async () => {
const [agent, mockQuery] = await createAgentMock();
await agent.initialize({ protocolVersion: 1, clientCapabilities: {} });
await agent.unstable_setProvider({
providerId: "main",
apiType: "anthropic",
baseUrl: "https://gateway.example",
headers: { "x-api-key": "test" },
});
await agent.newSession({ cwd: process.cwd(), mcpServers: [] });
expect(mockQuery).toHaveBeenCalledWith(
expect.objectContaining({
options: expect.objectContaining({
env: expect.objectContaining({
ANTHROPIC_AUTH_TOKEN: " ",
ANTHROPIC_BASE_URL: "https://gateway.example",
ANTHROPIC_CUSTOM_HEADERS: "x-api-key: test",
}),
}),
}),
);
});
it("routes bedrock provider config into session env", async () => {
const [agent, mockQuery] = await createAgentMock();
await agent.initialize({ protocolVersion: 1, clientCapabilities: {} });
await agent.unstable_setProvider({
providerId: "main",
apiType: "bedrock",
baseUrl: "https://gateway.example",
headers: { "custom-header": "test" },
});
await agent.newSession({ cwd: process.cwd(), mcpServers: [] });
expect(mockQuery).toHaveBeenCalledWith(
expect.objectContaining({
options: expect.objectContaining({
env: expect.objectContaining({
CLAUDE_CODE_USE_BEDROCK: "1",
AWS_BEARER_TOKEN_BEDROCK: " ",
ANTHROPIC_BEDROCK_BASE_URL: "https://gateway.example",
ANTHROPIC_CUSTOM_HEADERS: "custom-header: test",
}),
}),
}),
);
});
it("accepts vertex config via _meta and routes it into session env", async () => {
const [agent, mockQuery] = await createAgentMock();
await agent.initialize({ protocolVersion: 1, clientCapabilities: {} });
await agent.unstable_setProvider({
providerId: "main",
apiType: "vertex",
baseUrl: "https://vertex.example",
headers: { "custom-header": "test" },
_meta: { claudeCode: { vertex: { projectId: "my-project", region: "us-east5" } } },
});
// list surfaces apiType/baseUrl but not the _meta extras
const listed = await agent.unstable_listProviders({});
expect(listed.providers[0].current).toEqual({
apiType: "vertex",
baseUrl: "https://vertex.example",
});
await agent.newSession({ cwd: process.cwd(), mcpServers: [] });
expect(mockQuery).toHaveBeenCalledWith(
expect.objectContaining({
options: expect.objectContaining({
env: expect.objectContaining({
CLAUDE_CODE_USE_VERTEX: "1",
ANTHROPIC_VERTEX_BASE_URL: "https://vertex.example",
ANTHROPIC_VERTEX_PROJECT_ID: "my-project",
CLOUD_ML_REGION: "us-east5",
ANTHROPIC_CUSTOM_HEADERS: "custom-header: test",
}),
}),
}),
);
});
it("rejects vertex set without _meta project/region", async () => {
const [agent] = await createAgentMock();
await expect(
agent.unstable_setProvider({
providerId: "main",
apiType: "vertex",
baseUrl: "https://vertex.example",
}),
).rejects.toMatchObject({ code: -32602 });
// partial _meta (missing region) is also rejected
await expect(
agent.unstable_setProvider({
providerId: "main",
apiType: "vertex",
baseUrl: "https://vertex.example",
_meta: { claudeCode: { vertex: { projectId: "my-project" } } } as any,
}),
).rejects.toMatchObject({ code: -32602 });
});
it("providers/set takes precedence over gateway auth", async () => {
const [agent, mockQuery] = await createAgentMock();
await agent.initialize({
protocolVersion: 1,
clientCapabilities: { auth: { terminal: true, _meta: { gateway: true } } } as any,
});
// Gateway auth first...
await agent.authenticate({
methodId: "gateway",
_meta: { gateway: { baseUrl: "https://gateway.example", headers: {} } },
});
// ...then providers/set wins.
await agent.unstable_setProvider({
providerId: "main",
apiType: "anthropic",
baseUrl: "https://provider.example",
});
await agent.newSession({ cwd: process.cwd(), mcpServers: [] });
expect(mockQuery).toHaveBeenCalledWith(
expect.objectContaining({
options: expect.objectContaining({
env: expect.objectContaining({
ANTHROPIC_BASE_URL: "https://provider.example",
}),
}),
}),
);
});
it("clears provider config on logout", async () => {
const [agent] = await createAgentMock();
// Avoids resolving the native CLI binary in claudeCliPath().
vi.stubEnv("CLAUDE_CODE_EXECUTABLE", "/bin/true");
await agent.unstable_setProvider({
providerId: "main",
apiType: "anthropic",
baseUrl: "https://provider.example",
});
expect((await agent.unstable_listProviders({})).providers[0].current).not.toBeNull();
await agent.logout({});
expect((await agent.unstable_listProviders({})).providers[0].current).toBeNull();
});
});

View File

@@ -936,30 +936,16 @@ describe("session config options", () => {
return (agent as unknown as { sessions: Record<string, any> }).sessions[SESSION_ID];
}
it("refreshes contextWindowSize from getContextUsage when the model changes", async () => {
it("sets the window from text inference on model switch, without any getContextUsage IPC", async () => {
// getContextUsage stalls until the session's first prompt turn, so the
// switch path must never call it; the window is seeded from the text
// heuristic (here via the new model's resolvedModel) and later confirmed
// by result.modelUsage.
const session = getSession();
session.query.getContextUsage = vi.fn(async () => ({ rawMaxTokens: 967000 }));
await agent.setSessionConfigOption({
sessionId: SESSION_ID,
configId: "model",
value: "claude-sonnet-4-6",
});
expect(session.query.getContextUsage).toHaveBeenCalled();
expect(session.contextWindowSize).toBe(967000);
});
it("falls back to resolvedModel text inference when getContextUsage fails", async () => {
const session = getSession();
session.query.getContextUsage = vi.fn().mockRejectedValue(new Error("boom"));
session.modelInfos = session.modelInfos.map((m: ModelInfo) =>
m.value === "claude-sonnet-4-6" ? { ...m, resolvedModel: "claude-sonnet-5[1m]" } : m,
);
// The rejection is deliberate — capture the agent's warning instead of
// letting it hit the console.
const errorSpy = vi.fn();
(agent as any).logger = { log: () => {}, error: errorSpy };
await agent.setSessionConfigOption({
sessionId: SESSION_ID,
@@ -967,18 +953,17 @@ describe("session config options", () => {
value: "claude-sonnet-4-6",
});
expect(session.query.getContextUsage).not.toHaveBeenCalled();
expect(session.contextWindowSize).toBe(1_000_000);
expect(errorSpy).toHaveBeenCalled();
});
it("falls back to the default window when the SDK and inference both miss", async () => {
it("falls back to the default window when inference misses, without any getContextUsage IPC", async () => {
const session = getSession();
session.contextWindowSize = 1_000_000;
session.query.getContextUsage = vi.fn().mockRejectedValue(new Error("boom"));
// The rejection is deliberate — capture the agent's warning instead of
// letting it hit the console.
const errorSpy = vi.fn();
(agent as any).logger = { log: () => {}, error: errorSpy };
// Present but must NOT be called; the switch never consults it.
session.query.getContextUsage = vi.fn(async () => ({ rawMaxTokens: 967000 }));
// claude-sonnet-4-6 carries no "1m" token in its id, resolvedModel,
// displayName, or description, so inference misses → default window.
await agent.setSessionConfigOption({
sessionId: SESSION_ID,
@@ -986,8 +971,22 @@ describe("session config options", () => {
value: "claude-sonnet-4-6",
});
expect(session.query.getContextUsage).not.toHaveBeenCalled();
expect(session.contextWindowSize).toBe(200000);
expect(errorSpy).toHaveBeenCalled();
});
it("does not call getContextUsage even when switching to a fresh model", async () => {
const session = getSession();
const spy = vi.fn(async () => ({ rawMaxTokens: 967000 }));
session.query.getContextUsage = spy;
await agent.setSessionConfigOption({
sessionId: SESSION_ID,
configId: "model",
value: "claude-sonnet-4-6",
});
expect(spy).not.toHaveBeenCalled();
});
it("keeps the learned window when re-asserting the current model", async () => {

View File

@@ -1,4 +1,4 @@
import { describe, it, expect, beforeEach } from "vitest";
import { describe, it, expect, beforeEach, vi } from "vitest";
import { ClientCapabilities } from "@agentclientprotocol/sdk";
import { ImageBlockParam, ToolResultBlockParam } from "@anthropic-ai/sdk/resources";
import {
@@ -1024,7 +1024,7 @@ describe("Bash terminal output", () => {
);
// Fire PostToolUse hook with a structuredPatch in tool_response
const hook = createPostToolUseHook(mockLogger);
const hook = createPostToolUseHook();
await hook(
{
hook_event_name: "PostToolUse",
@@ -1102,7 +1102,7 @@ describe("Bash terminal output", () => {
mockLogger,
);
const hook = createPostToolUseHook(mockLogger);
const hook = createPostToolUseHook();
await hook(
{
hook_event_name: "PostToolUse",
@@ -1182,7 +1182,7 @@ describe("Bash terminal output", () => {
mockLogger,
);
const hook = createPostToolUseHook(mockLogger);
const hook = createPostToolUseHook();
await hook(
{
hook_event_name: "PostToolUse",
@@ -1203,6 +1203,36 @@ describe("Bash terminal output", () => {
expect(hookUpdate.content).toBeUndefined();
expect(hookUpdate.locations).toBeUndefined();
});
// Regression for issue #889: tool uses that never register a callback
// (TodoWrite/Task* are rendered as plan updates, not tool_calls) fire the
// PostToolUse hook too. That's expected — the hook must stay silent
// instead of spamming "No onPostToolUseHook found" to stderr.
it("should not log an error when no callback is registered for the tool use", async () => {
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
try {
const hook = createPostToolUseHook();
const result = await hook(
{
hook_event_name: "PostToolUse",
tool_name: "TodoWrite",
tool_input: { todos: [] },
tool_response: { success: true },
tool_use_id: "toolu_todo_no_callback",
session_id: "test-session",
transcript_path: "/tmp/test",
cwd: "/tmp",
},
"toolu_todo_no_callback",
{ signal: AbortSignal.abort() },
);
expect(result).toEqual({ continue: true });
expect(errorSpy).not.toHaveBeenCalled();
} finally {
errorSpy.mockRestore();
}
});
});
describe("post-tool-use hook sends diff content for Write tool", () => {
@@ -1239,7 +1269,7 @@ describe("Bash terminal output", () => {
mockLogger,
);
const hook = createPostToolUseHook(mockLogger);
const hook = createPostToolUseHook();
await hook(
{
hook_event_name: "PostToolUse",
@@ -1315,7 +1345,7 @@ describe("Bash terminal output", () => {
mockLogger,
);
const hook = createPostToolUseHook(mockLogger);
const hook = createPostToolUseHook();
await hook(
{
hook_event_name: "PostToolUse",
@@ -1438,7 +1468,7 @@ describe("Bash terminal output", () => {
expect((resultNotifications[1].update as any).status).toBe("completed");
// Step 3: Fire the PostToolUse hook (simulates what Claude Code SDK does)
const hook = createPostToolUseHook(mockLogger);
const hook = createPostToolUseHook();
await hook(
{
hook_event_name: "PostToolUse",
@@ -1519,7 +1549,7 @@ describe("Bash terminal output", () => {
);
// Fire hook
const hook = createPostToolUseHook(mockLogger);
const hook = createPostToolUseHook();
await hook(
{
hook_event_name: "PostToolUse",
@@ -2275,6 +2305,62 @@ describe("Agent/Task tool_result rendering from tool_use_result", () => {
]);
});
it("leaves malformed trailers alone", () => {
// Incomplete trailers are ordinary report text, not metadata to strip.
const malformedResult: ToolResultBlockParam = {
type: "tool_result",
tool_use_id: "toolu_agent",
content: [
{ type: "text", text: "Report.\n<usage>missing closing tag" },
{ type: "text", text: "Report.\nagentId: abc-123 (missing closing paren" },
],
};
const update = toolUpdateFromToolResult(malformedResult, agentToolUse, false);
expect(update.content).toEqual([
{ type: "content", content: { type: "text", text: "Report.\n<usage>missing closing tag" } },
{
type: "content",
content: { type: "text", text: "Report.\nagentId: abc-123 (missing closing paren" },
},
]);
});
it("strips only the trailer when the report itself mentions <usage>", () => {
const result: ToolResultBlockParam = {
type: "tool_result",
tool_use_id: "toolu_agent",
content: "Grep for <usage> found 3 hits.\n<usage>subagent_tokens: 5</usage>",
};
const update = toolUpdateFromToolResult(result, agentToolUse, false);
expect(update.content).toEqual([
{ type: "content", content: { type: "text", text: "Grep for <usage> found 3 hits." } },
]);
});
it("handles adversarial trailer-shaped input in linear time", () => {
// Regression: the old regex-based strip backtracked quadratically on
// these shapes (CodeQL js/polynomial-redos) — at this size it would
// blow the test timeout rather than merely run slow.
const result: ToolResultBlockParam = {
type: "tool_result",
tool_use_id: "toolu_agent",
content: [
{ type: "text", text: "agentId: - (".repeat(20000) },
{ type: "text", text: "<usage>".repeat(30000) },
],
};
const update = toolUpdateFromToolResult(result, agentToolUse, false);
// Neither is a real trailer, so both come through unchanged.
expect(update.content).toEqual([
{ type: "content", content: { type: "text", text: "agentId: - (".repeat(20000) } },
{ type: "content", content: { type: "text", text: "<usage>".repeat(30000) } },
]);
});
it("falls back (trailer-stripped) when tool_use_result is the async_launched variant", () => {
const update = toolUpdateFromToolResult(rawResult, agentToolUse, false, {
status: "async_launched",

View File

@@ -60,7 +60,6 @@ import {
BetaWebSearchToolResultBlockParam,
} from "@anthropic-ai/sdk/resources/beta.mjs";
import path from "node:path";
import { Logger } from "./acp-agent.js";
/**
* Union of all possible content types that can appear in tool results from the Anthropic SDK.
@@ -515,9 +514,42 @@ function structuredResult<T extends object>(toolUseResult: unknown): T | undefin
* matching rather than mangle the report.
*/
function stripAgentTrailer(text: string): string {
return text
.replace(/\n?<usage>[\s\S]*?<\/usage>\s*$/, "")
.replace(/\n?agentId: [\w-]+ \([^)]*\)\s*$/, "");
return stripAgentIdLine(stripUsageBlock(text));
}
const USAGE_OPEN = "<usage>";
const USAGE_CLOSE = "</usage>";
/** Remove a trailing `<usage>…</usage>` block, plus trailing whitespace and
* one preceding newline. Matches from the *last* `<usage>` so a report that
* merely mentions the marker earlier isn't truncated at the mention. */
function stripUsageBlock(text: string): string {
const body = text.trimEnd();
if (!body.endsWith(USAGE_CLOSE)) {
return text;
}
const open = body.lastIndexOf(USAGE_OPEN, body.length - USAGE_CLOSE.length - USAGE_OPEN.length);
if (open === -1) {
return text;
}
return body.slice(0, open > 0 && body[open - 1] === "\n" ? open - 1 : open);
}
/** The continuation line, anchored to a whole line so the regex has a single
* start position and no ambiguous repetition (`[\w-]+` can't consume the
* following space, `[^)]*` can't consume the closing paren) — it runs in
* linear time on any input. */
const AGENT_ID_LINE = /^agentId: [\w-]+ \([^)]*\)$/;
/** Remove a final `agentId: <id> (…)` line, plus trailing whitespace and the
* newline that preceded the line. */
function stripAgentIdLine(text: string): string {
const body = text.trimEnd();
const lineStart = body.lastIndexOf("\n") + 1;
if (!AGENT_ID_LINE.test(body.slice(lineStart))) {
return text;
}
return body.slice(0, Math.max(lineStart - 1, 0));
}
/** Apply {@link stripAgentTrailer} across a raw tool_result `content` (plain
@@ -1184,12 +1216,7 @@ export const registerHookCallback = (
/* A callback for Claude Code that is called when receiving a PostToolUse hook */
export const createPostToolUseHook =
(
logger: Logger = console,
options?: {
onEnterPlanMode?: () => Promise<void>;
},
): HookCallback =>
(options?: { onEnterPlanMode?: () => Promise<void> }): HookCallback =>
async (input: any, toolUseID: string | undefined): Promise<{ continue: boolean }> => {
if (input.hook_event_name === "PostToolUse") {
// Handle EnterPlanMode tool - notify client of mode change after successful execution
@@ -1201,11 +1228,8 @@ export const createPostToolUseHook =
const onPostToolUseHook = toolUseCallbacks[toolUseID]?.onPostToolUseHook;
if (onPostToolUseHook) {
await onPostToolUseHook(toolUseID, input.tool_input, input.tool_response);
delete toolUseCallbacks[toolUseID]; // Cleanup after execution
} else {
logger.error(`No onPostToolUseHook found for tool use ID: ${toolUseID}`);
delete toolUseCallbacks[toolUseID];
}
delete toolUseCallbacks[toolUseID]; // Cleanup after execution
}
}
return { continue: true };