Compare commits

...

13 Commits

Author SHA1 Message Date
acp-release-bot[bot]
59a7e9367b chore(main): release 0.69.0 (#1007)
🤖 I have created a release *beep* *boop*
---


##
[0.69.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.68.0...v0.69.0)
(2026-08-16)


### Features

* report changed files to AIR
([#1001](https://github.com/agentclientprotocol/claude-agent-acp/issues/1001))
([450d6b1](450d6b19dc))

---
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-08-16 11:18:05 +02:00
Sergey Ignatov
450d6b19dc feat: report changed files to AIR (#1001)
## Summary

- negotiate the opt-in AIR `_meta.jetbrains.air.agentFileChangeReport`
capability
- assign a unique `requestId` to each prompt and return canonical,
workspace-bounded changed paths in response metadata
- add a hidden Stop hook and an internal MCP reporting tool so Claude
can report files changed through shell commands, Git operations, and
generators

The audit is best-effort and is inactive unless the client advertises
the capability.

## Safety

- accept reports only for the active session and matching turn request
- canonicalize filesystem aliases, including `/tmp` to `/private/tmp` on
macOS
- reject paths outside the session working directory and deduplicate
valid paths

## Tests

- `npm run test:run`
- `npm run build`
- `npm run check`
2026-08-16 11:15:49 +02:00
acp-release-bot[bot]
5de5d4a2c8 chore(main): release 0.68.0 (#1000)
🤖 I have created a release *beep* *boop*
---


##
[0.68.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.67.0...v0.68.0)
(2026-08-14)


### Features

* align typed session failures with AIR protocol
([#992](https://github.com/agentclientprotocol/claude-agent-acp/issues/992))
([0581b9c](0581b9cf39))

---
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-08-14 17:32:04 +04:00
nikita-ashihmin
0581b9cf39 feat: align typed session failures with AIR protocol (#992)
## Summary

- align `_meta.jetbrains.air.sessionFailure` with AIR's
transcript-record protocol: `id`, `revision`, broad `category`, explicit
`severity`, `title`, optional `details`, and `actions`
- forward Claude's actual user-facing assistant/result text as `title`;
use adapter-authored fallback text only when Claude provides no message
(transport failure, worker EOF, or an internal invariant)
- reduce categories to `connection`, `access`, `limit`, `request`,
`service`, and `unknown`
- limit suggested actions to `retry`, `login`, and `new_session`
- update one transcript record through `id + revision`, keep resolved
entries in history, and remove clear/tombstone updates
- suppress duplicate assistant failure prose only for clients that
negotiated typed failures; preserve legacy ACP behavior otherwise
- restore synthetic usage-limit failures from session history with their
exact stored Claude text
- document negotiation, delivery carriers, identity/revision behavior,
mappings, recovery, and compatibility in the extension spec

## Validation

- `npm run check`
- `npm run build`
- `npm run test:run` — 777 passed, 26 skipped
- `git diff --check`

## Tracking

[IJAI-993](https://youtrack.jetbrains.com/issue/IJAI-993)
2026-08-14 17:29:39 +04:00
Sergey Shkredov
11dd73b871 Restore native provider state after overrides (#996)
### Summary
- expose one `main` provider slot because Anthropic, Bedrock, and Vertex
routing are mutually exclusive
- advertise all three supported API types on that slot and replace the
active configuration on each `providers/set`
- restore native Claude routing when the slot is disabled
- keep provider headers out of list responses and preserve per-protocol
validation

### Testing
- `npm run test:run` (767 passed, 20 skipped)
- `npm run build`
- `npm run check`

Co-authored-by: Junie <junie@jetbrains.com>
2026-08-14 10:13:38 +00:00
acp-release-bot[bot]
e4dba808ea chore(main): release 0.67.0 (#975)
🤖 I have created a release *beep* *boop*
---


##
[0.67.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.66.0...v0.67.0)
(2026-08-14)


### Features

* **deps:** Update to @anthropic-ai/claude-agent-sdk v0.3.232
([#993](https://github.com/agentclientprotocol/claude-agent-acp/issues/993))
([de0d0e2](de0d0e2b7d))
* expose typed session failures for AIR
([#979](https://github.com/agentclientprotocol/claude-agent-acp/issues/979))
([8157ee1](8157ee113e))
* publish the model fallback as a warning advisory
([#990](https://github.com/agentclientprotocol/claude-agent-acp/issues/990))
([35aaddb](35aaddb3d5))
* surface resolved model name in default model option description
([#982](https://github.com/agentclientprotocol/claude-agent-acp/issues/982))
([ec73cd8](ec73cd8560))
* surface Skill tool calls with name and kind in _meta
([#986](https://github.com/agentclientprotocol/claude-agent-acp/issues/986))
([1f09e9a](1f09e9a3ca))


### Bug Fixes

* preserve task plans across prompts
([#974](https://github.com/agentclientprotocol/claude-agent-acp/issues/974))
([1afa940](1afa940a2c))
* show a pending title while Claude prepares a file
([#978](https://github.com/agentclientprotocol/claude-agent-acp/issues/978))
([3df1ede](3df1ede89f))

---
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-08-14 00:40:45 +00:00
dependabot[bot]
de0d0e2b7d feat(deps): Update to @anthropic-ai/claude-agent-sdk v0.3.232 (#993)
Bumps the minor group with 12 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
|
[@anthropic-ai/claude-agent-sdk](https://github.com/anthropics/claude-agent-sdk-typescript)
| `0.3.220` | `0.3.226` |
|
[@anthropic-ai/sdk](https://github.com/anthropics/anthropic-sdk-typescript)
| `0.115.0` | `0.116.0` |
|
[@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node)
| `26.1.2` | `26.2.0` |
|
[@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin)
| `8.65.0` | `8.66.0` |
|
[@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser)
| `8.65.0` | `8.66.0` |
| [eslint](https://github.com/eslint/eslint) | `10.8.0` | `10.8.1` |
|
[express-rate-limit](https://github.com/express-rate-limit/express-rate-limit)
| `8.6.1` | `8.6.2` |
| [hono](https://github.com/honojs/hono) | `4.13.0` | `4.13.2` |
| [ip-address](https://github.com/beaugunderson/ip-address) | `10.4.0` |
`10.5.0` |
| [postcss](https://github.com/postcss/postcss) | `8.5.25` | `8.5.26` |
|
[rolldown](https://github.com/rolldown/rolldown/tree/HEAD/packages/rolldown)
| `1.2.2` | `1.2.4` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) |
`8.2.0` | `8.2.1` |


Updates `@anthropic-ai/claude-agent-sdk` from 0.3.220 to 0.3.226
<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.226</h2>
<h2>What's changed</h2>
<ul>
<li>Updated to parity with Claude Code v2.1.226</li>
</ul>
<h2>Update</h2>
<pre lang="sh"><code>npm install @anthropic-ai/claude-agent-sdk@0.3.226
# or
yarn add @anthropic-ai/claude-agent-sdk@0.3.226
# or
pnpm add @anthropic-ai/claude-agent-sdk@0.3.226
# or
bun add @anthropic-ai/claude-agent-sdk@0.3.226
</code></pre>
<h2>v0.3.225</h2>
<h2>What's changed</h2>
<ul>
<li>Fixed background subagents in headless/SDK sessions never resuming
when a background shell command or Monitor they left running completed,
so the subagent never saw the result</li>
</ul>
<h2>Update</h2>
<pre lang="sh"><code>npm install @anthropic-ai/claude-agent-sdk@0.3.225
# or
yarn add @anthropic-ai/claude-agent-sdk@0.3.225
# or
pnpm add @anthropic-ai/claude-agent-sdk@0.3.225
# or
bun add @anthropic-ai/claude-agent-sdk@0.3.225
</code></pre>
<h2>v0.3.224</h2>
<h2>What's changed</h2>
<ul>
<li>Added <code>crossSessionInbound</code> and <code>dialogExpiry</code>
settings: cross-session messages sent to a session running with bypassed
permissions are held for your approval, and messages to other sessions
auto-deliver</li>
<li>Added <code>subkind: 'peer-send-message'</code> to the
<code>task-notification</code> member of <code>SDKMessageOrigin</code>,
marking a notification raised by a cross-session
<code>SendMessage</code></li>
<li>Added <code>source: 'archive'</code> plugin config variant to
<code>Settings</code>, with <code>url</code> and optional
<code>sha256</code>, for installing plugins from a zip over HTTPS</li>
<li>Added sandbox credential-masking fields to <code>Settings</code>:
<code>decode: 'jwt'</code> with <code>maskClaims</code>,
<code>extract</code>/<code>onExtractNoMatch</code> on
<code>envVars</code>, and <code>awsPairs</code>/<code>sigv4</code> for
AWS SigV4 re-signing</li>
<li>Fixed long (&gt;200 char) project paths resolving to another
project's session directory under a shared sanitized prefix; session
list/get/rename/tag/fork/delete and <code>/resume</code> no longer cross
projects</li>
</ul>
<h2>Update</h2>
<pre lang="sh"><code>npm install @anthropic-ai/claude-agent-sdk@0.3.224
# or
yarn add @anthropic-ai/claude-agent-sdk@0.3.224
# 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.226</h2>
<ul>
<li>Updated to parity with Claude Code v2.1.226</li>
</ul>
<h2>0.3.225</h2>
<ul>
<li>Fixed background subagents in headless/SDK sessions never resuming
when a background shell command or Monitor they left running completed,
so the subagent never saw the result</li>
</ul>
<h2>0.3.224</h2>
<ul>
<li>Added <code>crossSessionInbound</code> and <code>dialogExpiry</code>
settings: cross-session messages sent to a session running with bypassed
permissions are held for your approval, and messages to other sessions
auto-deliver</li>
<li>Added <code>subkind: 'peer-send-message'</code> to the
<code>task-notification</code> member of <code>SDKMessageOrigin</code>,
marking a notification raised by a cross-session
<code>SendMessage</code></li>
<li>Added <code>source: 'archive'</code> plugin config variant to
<code>Settings</code>, with <code>url</code> and optional
<code>sha256</code>, for installing plugins from a zip over HTTPS</li>
<li>Added sandbox credential-masking fields to <code>Settings</code>:
<code>decode: 'jwt'</code> with <code>maskClaims</code>,
<code>extract</code>/<code>onExtractNoMatch</code> on
<code>envVars</code>, and <code>awsPairs</code>/<code>sigv4</code> for
AWS SigV4 re-signing</li>
<li>Fixed long (&gt;200 char) project paths resolving to another
project's session directory under a shared sanitized prefix; session
list/get/rename/tag/fork/delete and <code>/resume</code> no longer cross
projects</li>
</ul>
<h2>0.3.223</h2>
<ul>
<li>Added <code>resumeDropsTurn</code> option: with
<code>resumeSessionAt</code>, declares the turn a truncating resume
intends to drop; the CLI refuses the resume if anything else would be
discarded</li>
<li>Result messages for repeated 529 overload failures now include
<code>api_error_status: 529</code>, so SDK consumers can detect overload
terminations structurally instead of matching message text</li>
<li>Bare headless (<code>-p</code> / SDK <code>query()</code> without
<code>canUseTool</code>) now emits <code>system/permission_denied</code>
stream events when a tool call is auto-denied</li>
<li>Documented <code>usage</code> vs <code>modelUsage</code> on
stream-json results: <code>usage</code> is main-loop-only and per-turn;
<code>modelUsage</code> is cumulative, covers all query-pipeline calls,
and is the field for cost accounting</li>
</ul>
<h2>0.3.222</h2>
<ul>
<li>Fixed <code>query({ sessionStore, resume })</code> not carrying user
<code>settings.json</code> (<code>apiKeyHelper</code>, <code>env</code>,
<code>hooks</code>, <code>permissions</code>) into the resumed
subprocess</li>
</ul>
<h2>0.3.221</h2>
<ul>
<li>Improved <code>skills</code> option validation: malformed names
(delimiters or control characters) and wildcard-form names are rejected
with a clear error; use <code>skills: 'all'</code> to enable every
skill</li>
<li>Fixed external MCP servers passed via the <code>mcpServers</code>
option not being connected before the first turn, which caused the model
to emit tool calls as literal text</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="d13c50c54d"><code>d13c50c</code></a>
chore: Update CHANGELOG.md</li>
<li><a
href="4cae4b7634"><code>4cae4b7</code></a>
chore: Update CHANGELOG.md</li>
<li><a
href="86e3e9464e"><code>86e3e94</code></a>
chore: Update CHANGELOG.md</li>
<li><a
href="064793e6da"><code>064793e</code></a>
chore: Update CHANGELOG.md</li>
<li><a
href="6e143f440c"><code>6e143f4</code></a>
chore: Update CHANGELOG.md</li>
<li><a
href="31fbbfd403"><code>31fbbfd</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.220...v0.3.226">compare
view</a></li>
</ul>
</details>
<br />

Updates `@anthropic-ai/sdk` from 0.115.0 to 0.116.0
<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.116.0</h2>
<h2>0.116.0 (2026-08-07)</h2>
<p>Full Changelog: <a
href="https://github.com/anthropics/anthropic-sdk-typescript/compare/sdk-v0.115.0...sdk-v0.116.0">sdk-v0.115.0...sdk-v0.116.0</a></p>
<h3>Features</h3>
<ul>
<li><strong>api:</strong> add
<code>mid-conversation-tool-changes-2026-07-01</code> beta (<a
href="70e04f689b">70e04f6</a>)</li>
<li><strong>api:</strong> add support for session budgets, advisor tool,
pinned inference location and skills auto-loading from GitHub (<a
href="1336a4a903">1336a4a</a>)</li>
<li><strong>tools:</strong> make bash timeout and abort errors matchable
by class (<a
href="https://redirect.github.com/anthropics/anthropic-sdk-typescript/issues/259">#259</a>)
(<a
href="201b12e8ac">201b12e</a>)</li>
</ul>
<h3>Bug Fixes</h3>
<ul>
<li><strong>client:</strong> use hardcoded User-Agent strings instead of
constructor names (<a
href="0b97f04042">0b97f04</a>)</li>
</ul>
<h3>Chores</h3>
<ul>
<li><strong>api:</strong> remove retired Claude Opus 4.1 models (<a
href="11b685c66f">11b685c</a>)</li>
<li><strong>docs:</strong> small updates to descriptions (<a
href="413cc5f120">413cc5f</a>)</li>
<li><strong>docs:</strong> updates to a few documentation strings (<a
href="c38a1de5fc">c38a1de</a>)</li>
<li><strong>internal/deps:</strong> bump <code>@​babel/core</code> and
<code>@​babel/helpers</code> to 7.29.7 (<a
href="https://redirect.github.com/anthropics/anthropic-sdk-typescript/issues/261">#261</a>)
(<a
href="3164934680">3164934</a>)</li>
<li><strong>internal/deps:</strong> install tsc-multi from the npm
registry (<a
href="https://redirect.github.com/anthropics/anthropic-sdk-typescript/issues/265">#265</a>)
(<a
href="c787f9022c">c787f90</a>)</li>
<li><strong>internal:</strong> codegen related update (<a
href="73b3d61009">73b3d61</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.116.0 (2026-08-07)</h2>
<p>Full Changelog: <a
href="https://github.com/anthropics/anthropic-sdk-typescript/compare/sdk-v0.115.0...sdk-v0.116.0">sdk-v0.115.0...sdk-v0.116.0</a></p>
<h3>Features</h3>
<ul>
<li><strong>api:</strong> add
<code>mid-conversation-tool-changes-2026-07-01</code> beta (<a
href="70e04f689b">70e04f6</a>)</li>
<li><strong>api:</strong> add support for session budgets, advisor tool,
pinned inference location and skills auto-loading from GitHub (<a
href="1336a4a903">1336a4a</a>)</li>
<li><strong>tools:</strong> make bash timeout and abort errors matchable
by class (<a
href="https://redirect.github.com/anthropics/anthropic-sdk-typescript/issues/259">#259</a>)
(<a
href="201b12e8ac">201b12e</a>)</li>
</ul>
<h3>Bug Fixes</h3>
<ul>
<li><strong>client:</strong> use hardcoded User-Agent strings instead of
constructor names (<a
href="0b97f04042">0b97f04</a>)</li>
</ul>
<h3>Chores</h3>
<ul>
<li><strong>api:</strong> remove retired Claude Opus 4.1 models (<a
href="11b685c66f">11b685c</a>)</li>
<li><strong>docs:</strong> small updates to descriptions (<a
href="413cc5f120">413cc5f</a>)</li>
<li><strong>docs:</strong> updates to a few documentation strings (<a
href="c38a1de5fc">c38a1de</a>)</li>
<li><strong>internal/deps:</strong> bump <code>@​babel/core</code> and
<code>@​babel/helpers</code> to 7.29.7 (<a
href="https://redirect.github.com/anthropics/anthropic-sdk-typescript/issues/261">#261</a>)
(<a
href="3164934680">3164934</a>)</li>
<li><strong>internal/deps:</strong> install tsc-multi from the npm
registry (<a
href="https://redirect.github.com/anthropics/anthropic-sdk-typescript/issues/265">#265</a>)
(<a
href="c787f9022c">c787f90</a>)</li>
<li><strong>internal:</strong> codegen related update (<a
href="73b3d61009">73b3d61</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="ed02a89f5b"><code>ed02a89</code></a>
chore: release main</li>
<li><a
href="d72ff6a1c8"><code>d72ff6a</code></a>
feat(api): add support for session budgets, advisor tool, pinned
inference lo...</li>
<li><a
href="dbfed51cc3"><code>dbfed51</code></a>
chore(internal/deps): refresh provider package dev tooling to
mirror-availabl...</li>
<li><a
href="b7e95bfe0f"><code>b7e95bf</code></a>
chore(docs): updates to a few documentation strings</li>
<li><a
href="9efb7bd454"><code>9efb7bd</code></a>
chore(api): remove retired Claude Opus 4.1 models</li>
<li><a
href="342d422c7d"><code>342d422</code></a>
chore(internal/deps): install tsc-multi from the npm registry (<a
href="https://redirect.github.com/anthropics/anthropic-sdk-typescript/issues/265">#265</a>)</li>
<li><a
href="501955ca31"><code>501955c</code></a>
chore(internal/deps): bump <code>@​babel/core</code> and
<code>@​babel/helpers</code> to 7.29.7 (<a
href="https://redirect.github.com/anthropics/anthropic-sdk-typescript/issues/261">#261</a>)</li>
<li><a
href="6641c5e46f"><code>6641c5e</code></a>
feat(tools): make bash timeout and abort errors matchable by class (<a
href="https://redirect.github.com/anthropics/anthropic-sdk-typescript/issues/259">#259</a>)</li>
<li><a
href="e421c43ea6"><code>e421c43</code></a>
chore(docs): small updates to descriptions</li>
<li><a
href="c0a963eff5"><code>c0a963e</code></a>
feat(api): add <code>mid-conversation-tool-changes-2026-07-01</code>
beta</li>
<li>Additional commits viewable in <a
href="https://github.com/anthropics/anthropic-sdk-typescript/compare/sdk-v0.115.0...sdk-v0.116.0">compare
view</a></li>
</ul>
</details>
<br />

Updates `@types/node` from 26.1.2 to 26.2.0
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node">compare
view</a></li>
</ul>
</details>
<br />

Updates `@typescript-eslint/eslint-plugin` from 8.65.0 to 8.66.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.66.0</h2>
<h2>8.66.0 (2026-08-03)</h2>
<h3>🚀 Features</h3>
<ul>
<li><strong>typescript-estree:</strong> handle import.defer() as
ImportExpression (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12609">#12609</a>)</li>
</ul>
<h3>🩹 Fixes</h3>
<ul>
<li><strong>eslint-plugin:</strong> [no-unnecessary-type-conversion]
ignore shadowed built-ins (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12590">#12590</a>)</li>
<li><strong>eslint-plugin:</strong> [prefer-nullish-coalescing] handle
shadowed Boolean calls (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12591">#12591</a>)</li>
<li><strong>eslint-plugin:</strong> [no-useless-default-assignment]
don't report defaults used by other overloads (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12607">#12607</a>)</li>
<li><strong>eslint-plugin:</strong> [no-unnecessary-type-parameters]
check MappedType key remapping (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12588">#12588</a>)</li>
<li><strong>eslint-plugin:</strong> [class-literal-property-style]
preserve type annotations and don't drop decorators (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12617">#12617</a>)</li>
<li><strong>website:</strong> list
<code>onUnsupportedTypeScriptVersion</code> in parser options (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12585">#12585</a>)</li>
</ul>
<h3>❤️ Thank You</h3>
<ul>
<li>Evyatar Daud <a
href="https://github.com/StyleShit"><code>@​StyleShit</code></a></li>
<li>Kirk Waiblinger <a
href="https://github.com/kirkwaiblinger"><code>@​kirkwaiblinger</code></a></li>
<li>Steffen Schroeder</li>
<li>송재욱</li>
</ul>
<p>See <a
href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.66.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.66.0 (2026-08-03)</h2>
<h3>🩹 Fixes</h3>
<ul>
<li><strong>eslint-plugin:</strong> [class-literal-property-style]
preserve type annotations and don't drop decorators (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12617">#12617</a>)</li>
<li><strong>eslint-plugin:</strong> [no-unnecessary-type-parameters]
check MappedType key remapping (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12588">#12588</a>)</li>
<li><strong>eslint-plugin:</strong> [no-useless-default-assignment]
don't report defaults used by other overloads (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12607">#12607</a>)</li>
<li><strong>eslint-plugin:</strong> [prefer-nullish-coalescing] handle
shadowed Boolean calls (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12591">#12591</a>)</li>
<li><strong>eslint-plugin:</strong> [no-unnecessary-type-conversion]
ignore shadowed built-ins (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12590">#12590</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.66.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="e51b11ba3a"><code>e51b11b</code></a>
chore(release): publish 8.66.0</li>
<li><a
href="20cb15e6db"><code>20cb15e</code></a>
chore(eslint-plugin): split prefer-optional-chain.test.ts in multiple
files (...</li>
<li><a
href="780a8c52d5"><code>780a8c5</code></a>
docs(eslint-plugin): [member-ordering] add lacking commas in default
member t...</li>
<li><a
href="08dd427202"><code>08dd427</code></a>
fix(eslint-plugin): [class-literal-property-style] preserve type
annotations ...</li>
<li><a
href="6be25d3b45"><code>6be25d3</code></a>
fix(eslint-plugin): [no-unnecessary-type-parameters] check MappedType
key rem...</li>
<li><a
href="8826d0ebf7"><code>8826d0e</code></a>
fix(eslint-plugin): [no-useless-default-assignment] don't report
defaults use...</li>
<li><a
href="7aecc1c44e"><code>7aecc1c</code></a>
fix(eslint-plugin): [prefer-nullish-coalescing] handle shadowed Boolean
calls...</li>
<li><a
href="5ffa76049a"><code>5ffa760</code></a>
fix(eslint-plugin): [no-unnecessary-type-conversion] ignore shadowed
built-in...</li>
<li><a
href="f282f84df6"><code>f282f84</code></a>
chore(eslint-plugin): fix all
<code>eslint-plugin/require-test-error-positions</code> (#...</li>
<li><a
href="c8cafa4ae9"><code>c8cafa4</code></a>
docs: [unified-signatures] caveats regarding signatures merging (<a
href="https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin/issues/12581">#12581</a>)</li>
<li>See full diff in <a
href="https://github.com/typescript-eslint/typescript-eslint/commits/v8.66.0/packages/eslint-plugin">compare
view</a></li>
</ul>
</details>
<br />

Updates `@typescript-eslint/parser` from 8.65.0 to 8.66.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.66.0</h2>
<h2>8.66.0 (2026-08-03)</h2>
<h3>🚀 Features</h3>
<ul>
<li><strong>typescript-estree:</strong> handle import.defer() as
ImportExpression (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12609">#12609</a>)</li>
</ul>
<h3>🩹 Fixes</h3>
<ul>
<li><strong>eslint-plugin:</strong> [no-unnecessary-type-conversion]
ignore shadowed built-ins (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12590">#12590</a>)</li>
<li><strong>eslint-plugin:</strong> [prefer-nullish-coalescing] handle
shadowed Boolean calls (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12591">#12591</a>)</li>
<li><strong>eslint-plugin:</strong> [no-useless-default-assignment]
don't report defaults used by other overloads (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12607">#12607</a>)</li>
<li><strong>eslint-plugin:</strong> [no-unnecessary-type-parameters]
check MappedType key remapping (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12588">#12588</a>)</li>
<li><strong>eslint-plugin:</strong> [class-literal-property-style]
preserve type annotations and don't drop decorators (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12617">#12617</a>)</li>
<li><strong>website:</strong> list
<code>onUnsupportedTypeScriptVersion</code> in parser options (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12585">#12585</a>)</li>
</ul>
<h3>❤️ Thank You</h3>
<ul>
<li>Evyatar Daud <a
href="https://github.com/StyleShit"><code>@​StyleShit</code></a></li>
<li>Kirk Waiblinger <a
href="https://github.com/kirkwaiblinger"><code>@​kirkwaiblinger</code></a></li>
<li>Steffen Schroeder</li>
<li>송재욱</li>
</ul>
<p>See <a
href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.66.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.66.0 (2026-08-03)</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.66.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="e51b11ba3a"><code>e51b11b</code></a>
chore(release): publish 8.66.0</li>
<li>See full diff in <a
href="https://github.com/typescript-eslint/typescript-eslint/commits/v8.66.0/packages/parser">compare
view</a></li>
</ul>
</details>
<br />

Updates `eslint` from 10.8.0 to 10.8.1
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/eslint/eslint/releases">eslint's
releases</a>.</em></p>
<blockquote>
<h2>v10.8.1</h2>
<h2>Bug Fixes</h2>
<ul>
<li><a
href="18eb0a7e78"><code>18eb0a7</code></a>
fix: prevent ASI hazard in <code>no-unused-labels</code> autofix (<a
href="https://redirect.github.com/eslint/eslint/issues/21173">#21173</a>)
(dongkyu lee)</li>
<li><a
href="151ba3f583"><code>151ba3f</code></a>
fix: false positives in <code>getter-return</code> and
<code>accessor-pairs</code> (<a
href="https://redirect.github.com/eslint/eslint/issues/21163">#21163</a>)
(Grit)</li>
<li><a
href="6898df9364"><code>6898df9</code></a>
fix: ignore meta-property names in <code>id-denylist</code> (<a
href="https://redirect.github.com/eslint/eslint/issues/21166">#21166</a>)
(Pixel)</li>
<li><a
href="4d7db6628e"><code>4d7db66</code></a>
fix: ignore meta-property names in <code>id-match</code> (<a
href="https://redirect.github.com/eslint/eslint/issues/21167">#21167</a>)
(Pixel)</li>
<li><a
href="677214e7ee"><code>677214e</code></a>
fix: handle ASI hazards in no-unused-vars removeVar suggestion (<a
href="https://redirect.github.com/eslint/eslint/issues/20935">#20935</a>)
(kuldeep kumar)</li>
</ul>
<h2>Documentation</h2>
<ul>
<li><a
href="7d0cbf81cf"><code>7d0cbf8</code></a>
docs: Update README (GitHub Actions Bot)</li>
<li><a
href="0a05812adb"><code>0a05812</code></a>
docs: add missing backticks to <code>no-duplicate-imports.js</code> (<a
href="https://redirect.github.com/eslint/eslint/issues/21183">#21183</a>)
(Lee Daeun)</li>
<li><a
href="678c90b55d"><code>678c90b</code></a>
docs: Update README (GitHub Actions Bot)</li>
<li><a
href="8a104242e8"><code>8a10424</code></a>
docs: Update README (GitHub Actions Bot)</li>
<li><a
href="69bb948061"><code>69bb948</code></a>
docs: Update README (GitHub Actions Bot)</li>
</ul>
<h2>Chores</h2>
<ul>
<li><a
href="0a14800505"><code>0a14800</code></a>
chore: update github/codeql-action action to v4.37.4 (<a
href="https://redirect.github.com/eslint/eslint/issues/21196">#21196</a>)
(renovate[bot])</li>
<li><a
href="05adcb1354"><code>05adcb1</code></a>
test: fix failing ecosystem test for <code>eslint-plugin-unicorn</code>
(<a
href="https://redirect.github.com/eslint/eslint/issues/21191">#21191</a>)
(Lazizbek Ergashev)</li>
<li><a
href="5611035665"><code>5611035</code></a>
test: add error locations info to <code>no-void</code> (<a
href="https://redirect.github.com/eslint/eslint/issues/21185">#21185</a>)
(Lee Daeun)</li>
<li><a
href="ee47333aa6"><code>ee47333</code></a>
ci: bump github/codeql-action from 4 to 4.37.3 (<a
href="https://redirect.github.com/eslint/eslint/issues/21176">#21176</a>)
(dependabot[bot])</li>
<li><a
href="f131c034ad"><code>f131c03</code></a>
chore: improve ecosystem test failure reporting (<a
href="https://redirect.github.com/eslint/eslint/issues/20937">#20937</a>)
(crimsonjay0)</li>
<li><a
href="1f6eddee60"><code>1f6edde</code></a>
chore: update ecosystem plugins (<a
href="https://redirect.github.com/eslint/eslint/issues/21182">#21182</a>)
(ESLint Bot)</li>
<li><a
href="d3266fb26c"><code>d3266fb</code></a>
chore: unpin <code>webpack</code> dependency (<a
href="https://redirect.github.com/eslint/eslint/issues/21172">#21172</a>)
(Francesco Trotta)</li>
<li><a
href="65a6519cc8"><code>65a6519</code></a>
chore: add allowScripts field to package.json (<a
href="https://redirect.github.com/eslint/eslint/issues/21092">#21092</a>)
(GiHoon Noh)</li>
<li><a
href="22e5256853"><code>22e5256</code></a>
ci: add <code>triage:no</code> label to Dependabot PRs (<a
href="https://redirect.github.com/eslint/eslint/issues/21141">#21141</a>)
(lumir)</li>
<li><a
href="55c9038836"><code>55c9038</code></a>
ci: bump actions/labeler from 6 to 7 (<a
href="https://redirect.github.com/eslint/eslint/issues/21159">#21159</a>)
(dependabot[bot])</li>
<li><a
href="7280e78183"><code>7280e78</code></a>
chore: update dependency prettier to v3.9.6 (<a
href="https://redirect.github.com/eslint/eslint/issues/21162">#21162</a>)
(renovate[bot])</li>
<li><a
href="eddbad6015"><code>eddbad6</code></a>
test: fix failing ecosystem test for <code>eslint-plugin-unicorn</code>
(<a
href="https://redirect.github.com/eslint/eslint/issues/21156">#21156</a>)
(Francesco Trotta)</li>
<li><a
href="60a178d5ef"><code>60a178d</code></a>
chore: update ecosystem plugins (<a
href="https://redirect.github.com/eslint/eslint/issues/21150">#21150</a>)
(ESLint Bot)</li>
<li><a
href="f9f61dc611"><code>f9f61dc</code></a>
test: add error locations to <code>no-unreachable</code> (<a
href="https://redirect.github.com/eslint/eslint/issues/21151">#21151</a>)
(JIYEON)</li>
<li><a
href="d08629382b"><code>d086293</code></a>
test: add error locations to <code>no-undef</code> (<a
href="https://redirect.github.com/eslint/eslint/issues/21147">#21147</a>)
(JIYEON)</li>
<li><a
href="cc01b67061"><code>cc01b67</code></a>
test: add error locations to <code>no-useless-catch</code> (<a
href="https://redirect.github.com/eslint/eslint/issues/21144">#21144</a>)
(devoil)</li>
<li><a
href="688e75ede0"><code>688e75e</code></a>
chore: add missing backticks in JSDoc (<a
href="https://redirect.github.com/eslint/eslint/issues/21143">#21143</a>)
(Bo Hyun Kim)</li>
<li><a
href="7c1e17543a"><code>7c1e175</code></a>
test: add error locations to <code>require-await</code> (<a
href="https://redirect.github.com/eslint/eslint/issues/21145">#21145</a>)
(Grit)</li>
<li><a
href="588a26ddce"><code>588a26d</code></a>
test: add error locations to <code>no-extra-label</code> (<a
href="https://redirect.github.com/eslint/eslint/issues/21139">#21139</a>)
(dongkyu lee)</li>
<li><a
href="059aa89574"><code>059aa89</code></a>
test: add error locations to <code>no-useless-concat</code> (<a
href="https://redirect.github.com/eslint/eslint/issues/21140">#21140</a>)
(dongkyu lee)</li>
<li><a
href="5a452a8ba5"><code>5a452a8</code></a>
test: add error locations to <code>no-const-assign</code> (<a
href="https://redirect.github.com/eslint/eslint/issues/21138">#21138</a>)
(dongkyu lee)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="c049dc3c42"><code>c049dc3</code></a>
10.8.1</li>
<li><a
href="a3f7826941"><code>a3f7826</code></a>
Build: changelog update for 10.8.1</li>
<li><a
href="18eb0a7e78"><code>18eb0a7</code></a>
fix: prevent ASI hazard in <code>no-unused-labels</code> autofix (<a
href="https://redirect.github.com/eslint/eslint/issues/21173">#21173</a>)</li>
<li><a
href="0a14800505"><code>0a14800</code></a>
chore: update github/codeql-action action to v4.37.4 (<a
href="https://redirect.github.com/eslint/eslint/issues/21196">#21196</a>)</li>
<li><a
href="7d0cbf81cf"><code>7d0cbf8</code></a>
docs: Update README</li>
<li><a
href="05adcb1354"><code>05adcb1</code></a>
test: fix failing ecosystem test for <code>eslint-plugin-unicorn</code>
(<a
href="https://redirect.github.com/eslint/eslint/issues/21191">#21191</a>)</li>
<li><a
href="5611035665"><code>5611035</code></a>
test: add error locations info to <code>no-void</code> (<a
href="https://redirect.github.com/eslint/eslint/issues/21185">#21185</a>)</li>
<li><a
href="ee47333aa6"><code>ee47333</code></a>
ci: bump github/codeql-action from 4 to 4.37.3 (<a
href="https://redirect.github.com/eslint/eslint/issues/21176">#21176</a>)</li>
<li><a
href="f131c034ad"><code>f131c03</code></a>
chore: improve ecosystem test failure reporting (<a
href="https://redirect.github.com/eslint/eslint/issues/20937">#20937</a>)</li>
<li><a
href="0a05812adb"><code>0a05812</code></a>
docs: add missing backticks to <code>no-duplicate-imports.js</code> (<a
href="https://redirect.github.com/eslint/eslint/issues/21183">#21183</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/eslint/eslint/compare/v10.8.0...v10.8.1">compare
view</a></li>
</ul>
</details>
<br />

Updates `@anthropic-ai/claude-agent-sdk-darwin-arm64` from 0.3.220 to
0.3.226

Updates `@anthropic-ai/claude-agent-sdk-darwin-x64` from 0.3.220 to
0.3.226

Updates `@anthropic-ai/claude-agent-sdk-linux-arm64` from 0.3.220 to
0.3.226

Updates `@anthropic-ai/claude-agent-sdk-linux-arm64-musl` from 0.3.220
to 0.3.226

Updates `@anthropic-ai/claude-agent-sdk-linux-x64` from 0.3.220 to
0.3.226

Updates `@anthropic-ai/claude-agent-sdk-linux-x64-musl` from 0.3.220 to
0.3.226

Updates `@anthropic-ai/claude-agent-sdk-win32-arm64` from 0.3.220 to
0.3.226

Updates `@anthropic-ai/claude-agent-sdk-win32-x64` from 0.3.220 to
0.3.226

Updates `@typescript-eslint/project-service` from 8.65.0 to 8.66.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.66.0</h2>
<h2>8.66.0 (2026-08-03)</h2>
<h3>🚀 Features</h3>
<ul>
<li><strong>typescript-estree:</strong> handle import.defer() as
ImportExpression (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12609">#12609</a>)</li>
</ul>
<h3>🩹 Fixes</h3>
<ul>
<li><strong>eslint-plugin:</strong> [no-unnecessary-type-conversion]
ignore shadowed built-ins (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12590">#12590</a>)</li>
<li><strong>eslint-plugin:</strong> [prefer-nullish-coalescing] handle
shadowed Boolean calls (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12591">#12591</a>)</li>
<li><strong>eslint-plugin:</strong> [no-useless-default-assignment]
don't report defaults used by other overloads (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12607">#12607</a>)</li>
<li><strong>eslint-plugin:</strong> [no-unnecessary-type-parameters]
check MappedType key remapping (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12588">#12588</a>)</li>
<li><strong>eslint-plugin:</strong> [class-literal-property-style]
preserve type annotations and don't drop decorators (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12617">#12617</a>)</li>
<li><strong>website:</strong> list
<code>onUnsupportedTypeScriptVersion</code> in parser options (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12585">#12585</a>)</li>
</ul>
<h3>❤️ Thank You</h3>
<ul>
<li>Evyatar Daud <a
href="https://github.com/StyleShit"><code>@​StyleShit</code></a></li>
<li>Kirk Waiblinger <a
href="https://github.com/kirkwaiblinger"><code>@​kirkwaiblinger</code></a></li>
<li>Steffen Schroeder</li>
<li>송재욱</li>
</ul>
<p>See <a
href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.66.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.66.0 (2026-08-03)</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.66.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="e51b11ba3a"><code>e51b11b</code></a>
chore(release): publish 8.66.0</li>
<li>See full diff in <a
href="https://github.com/typescript-eslint/typescript-eslint/commits/v8.66.0/packages/project-service">compare
view</a></li>
</ul>
</details>
<br />

Updates `@typescript-eslint/scope-manager` from 8.65.0 to 8.66.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.66.0</h2>
<h2>8.66.0 (2026-08-03)</h2>
<h3>🚀 Features</h3>
<ul>
<li><strong>typescript-estree:</strong> handle import.defer() as
ImportExpression (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12609">#12609</a>)</li>
</ul>
<h3>🩹 Fixes</h3>
<ul>
<li><strong>eslint-plugin:</strong> [no-unnecessary-type-conversion]
ignore shadowed built-ins (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12590">#12590</a>)</li>
<li><strong>eslint-plugin:</strong> [prefer-nullish-coalescing] handle
shadowed Boolean calls (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12591">#12591</a>)</li>
<li><strong>eslint-plugin:</strong> [no-useless-default-assignment]
don't report defaults used by other overloads (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12607">#12607</a>)</li>
<li><strong>eslint-plugin:</strong> [no-unnecessary-type-parameters]
check MappedType key remapping (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12588">#12588</a>)</li>
<li><strong>eslint-plugin:</strong> [class-literal-property-style]
preserve type annotations and don't drop decorators (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12617">#12617</a>)</li>
<li><strong>website:</strong> list
<code>onUnsupportedTypeScriptVersion</code> in parser options (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12585">#12585</a>)</li>
</ul>
<h3>❤️ Thank You</h3>
<ul>
<li>Evyatar Daud <a
href="https://github.com/StyleShit"><code>@​StyleShit</code></a></li>
<li>Kirk Waiblinger <a
href="https://github.com/kirkwaiblinger"><code>@​kirkwaiblinger</code></a></li>
<li>Steffen Schroeder</li>
<li>송재욱</li>
</ul>
<p>See <a
href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.66.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.66.0 (2026-08-03)</h2>
<p>This was a version bump only for scope-manager 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.66.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="e51b11ba3a"><code>e51b11b</code></a>
chore(release): publish 8.66.0</li>
<li><a
href="0c94e088e9"><code>0c94e08</code></a>
chore: add CI check to ensure snapshots dirs don't have stale files (<a
href="https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/scope-manager/issues/12623">#12623</a>)</li>
<li>See full diff in <a
href="https://github.com/typescript-eslint/typescript-eslint/commits/v8.66.0/packages/scope-manager">compare
view</a></li>
</ul>
</details>
<br />

Updates `@typescript-eslint/tsconfig-utils` from 8.65.0 to 8.66.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.66.0</h2>
<h2>8.66.0 (2026-08-03)</h2>
<h3>🚀 Features</h3>
<ul>
<li><strong>typescript-estree:</strong> handle import.defer() as
ImportExpression (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12609">#12609</a>)</li>
</ul>
<h3>🩹 Fixes</h3>
<ul>
<li><strong>eslint-plugin:</strong> [no-unnecessary-type-conversion]
ignore shadowed built-ins (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12590">#12590</a>)</li>
<li><strong>eslint-plugin:</strong> [prefer-nullish-coalescing] handle
shadowed Boolean calls (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12591">#12591</a>)</li>
<li><strong>eslint-plugin:</strong> [no-useless-default-assignment]
don't report defaults used by other overloads (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12607">#12607</a>)</li>
<li><strong>eslint-plugin:</strong> [no-unnecessary-type-parameters]
check MappedType key remapping (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12588">#12588</a>)</li>
<li><strong>eslint-plugin:</strong> [class-literal-property-style]
preserve type annotations and don't drop decorators (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12617">#12617</a>)</li>
<li><strong>website:</strong> list
<code>onUnsupportedTypeScriptVersion</code> in parser options (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12585">#12585</a>)</li>
</ul>
<h3>❤️ Thank You</h3>
<ul>
<li>Evyatar Daud <a
href="https://github.com/StyleShit"><code>@​StyleShit</code></a></li>
<li>Kirk Waiblinger <a
href="https://github.com/kirkwaiblinger"><code>@​kirkwaiblinger</code></a></li>
<li>Steffen Schroeder</li>
<li>송재욱</li>
</ul>
<p>See <a
href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.66.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.66.0 (2026-08-03)</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.66.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="e51b11ba3a"><code>e51b11b</code></a>
chore(release): publish 8.66.0</li>
<li>See full diff in <a
href="https://github.com/typescript-eslint/typescript-eslint/commits/v8.66.0/packages/tsconfig-utils">compare
view</a></li>
</ul>
</details>
<br />

Updates `@typescript-eslint/type-utils` from 8.65.0 to 8.66.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.66.0</h2>
<h2>8.66.0 (2026-08-03)</h2>
<h3>🚀 Features</h3>
<ul>
<li><strong>typescript-estree:</strong> handle import.defer() as
ImportExpression (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12609">#12609</a>)</li>
</ul>
<h3>🩹 Fixes</h3>
<ul>
<li><strong>eslint-plugin:</strong> [no-unnecessary-type-conversion]
ignore shadowed built-ins (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12590">#12590</a>)</li>
<li><strong>eslint-plugin:</strong> [prefer-nullish-coalescing] handle
shadowed Boolean calls (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12591">#12591</a>)</li>
<li><strong>eslint-plugin:</strong> [no-useless-default-assignment]
don't report defaults used by other overloads (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12607">#12607</a>)</li>
<li><strong>eslint-plugin:</strong> [no-unnecessary-type-parameters]
check MappedType key remapping (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12588">#12588</a>)</li>
<li><strong>eslint-plugin:</strong> [class-literal-property-style]
preserve type annotations and don't drop decorators (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12617">#12617</a>)</li>
<li><strong>website:</strong> list
<code>onUnsupportedTypeScriptVersion</code> in parser options (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12585">#12585</a>)</li>
</ul>
<h3>❤️ Thank You</h3>
<ul>
<li>Evyatar Daud <a
href="https://github.com/StyleShit"><code>@​StyleShit</code></a></li>
<li>Kirk Waiblinger <a
href="https://github.com/kirkwaiblinger"><code>@​kirkwaiblinger</code></a></li>
<li>Steffen Schroeder</li>
<li>송재욱</li>
</ul>
<p>See <a
href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.66.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.66.0 (2026-08-03)</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.66.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="e51b11ba3a"><code>e51b11b</code></a>
chore(release): publish 8.66.0</li>
<li>See full diff in <a
href="https://github.com/typescript-eslint/typescript-eslint/commits/v8.66.0/packages/type-utils">compare
view</a></li>
</ul>
</details>
<br />

Updates `@typescript-eslint/types` from 8.65.0 to 8.66.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.66.0</h2>
<h2>8.66.0 (2026-08-03)</h2>
<h3>🚀 Features</h3>
<ul>
<li><strong>typescript-estree:</strong> handle import.defer() as
ImportExpression (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12609">#12609</a>)</li>
</ul>
<h3>🩹 Fixes</h3>
<ul>
<li><strong>eslint-plugin:</strong> [no-unnecessary-type-conversion]
ignore shadowed built-ins (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12590">#12590</a>)</li>
<li><strong>eslint-plugin:</strong> [prefer-nullish-coalescing] handle
shadowed Boolean calls (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12591">#12591</a>)</li>
<li><strong>eslint-plugin:</strong> [no-useless-default-assignment]
don't report defaults used by other overloads (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12607">#12607</a>)</li>
<li><strong>eslint-plugin:</strong> [no-unnecessary-type-parameters]
check MappedType key remapping (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12588">#12588</a>)</li>
<li><strong>eslint-plugin:</strong> [class-literal-property-style]
preserve type annotations and don't drop decorators (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12617">#12617</a>)</li>
<li><strong>website:</strong> list
<code>onUnsupportedTypeScriptVersion</code> in parser options (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12585">#12585</a>)</li>
</ul>
<h3>❤️ Thank You</h3>
<ul>
<li>Evyatar Daud <a
href="https://github.com/StyleShit"><code>@​StyleShit</code></a></li>
<li>Kirk Waiblinger <a
href="https://github.com/kirkwaiblinger"><code>@​kirkwaiblinger</code></a></li>
<li>Steffen Schroeder</li>
<li>송재욱</li>
</ul>
<p>See <a
href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.66.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.66.0 (2026-08-03)</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.66.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="e51b11ba3a"><code>e51b11b</code></a>
chore(release): publish 8.66.0</li>
<li><a
href="918411c1b6"><code>918411c</code></a>
chore(types): fix <code>build</code> nx task inputs (<a
href="https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/types/issues/12634">#12634</a>)</li>
<li>See full diff in <a
href="https://github.com/typescript-eslint/typescript-eslint/commits/v8.66.0/packages/types">compare
view</a></li>
</ul>
</details>
<br />

Updates `@typescript-eslint/typescript-estree` from 8.65.0 to 8.66.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.66.0</h2>
<h2>8.66.0 (2026-08-03)</h2>
<h3>🚀 Features</h3>
<ul>
<li><strong>typescript-estree:</strong> handle import.defer() as
ImportExpression (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12609">#12609</a>)</li>
</ul>
<h3>🩹 Fixes</h3>
<ul>
<li><strong>eslint-plugin:</strong> [no-unnecessary-type-conversion]
ignore shadowed built-ins (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12590">#12590</a>)</li>
<li><strong>eslint-plugin:</strong> [prefer-nullish-coalescing] handle
shadowed Boolean calls (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12591">#12591</a>)</li>
<li><strong>eslint-plugin:</strong> [no-useless-default-assignment]
don't report defaults used by other overloads (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12607">#12607</a>)</li>
<li><strong>eslint-plugin:</strong> [no-unnecessary-type-parameters]
check MappedType key remapping (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12588">#12588</a>)</li>
<li><strong>eslint-plugin:</strong> [class-literal-property-style]
preserve type annotations and don't drop decorators (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12617">#12617</a>)</li>
<li><strong>website:</strong> list
<code>onUnsupportedTypeScriptVersion</code> in parser options (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12585">#12585</a>)</li>
</ul>
<h3>❤️ Thank You</h3>
<ul>
<li>Evyatar Daud <a
href="https://github.com/StyleShit"><code>@​StyleShit</code></a></li>
<li>Kirk Waiblinger <a
href="https://github.com/kirkwaiblinger"><code>@​kirkwaiblinger</code></a></li>
<li>Steffen Schroeder</li>
<li>송재욱</li>
</ul>
<p>See <a
href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.66.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.66.0 (2026-08-03)</h2>
<h3>🚀 Features</h3>
<ul>
<li><strong>typescript-estree:</strong> handle import.defer() as
ImportExpression (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12609">#12609</a>)</li>
</ul>
<h3>❤️ Thank You</h3>
<ul>
<li>Evyatar Daud <a
href="https://github.com/StyleShit"><code>@​StyleShit</code></a></li>
<li>Kirk Waiblinger <a
href="https://github.com/kirkwaiblinger"><code>@​kirkwaiblinger</code></a></li>
</ul>
<p>See <a
href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.66.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="e51b11ba3a"><code>e51b11b</code></a>
chore(release): publish 8.66.0</li>
<li><a
href="e88c943f01"><code>e88c943</code></a>
feat(typescript-estree): handle import.defer() as ImportExpression (<a
href="https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-estree/issues/12609">#12609</a>)</li>
<li>See full diff in <a
href="https://github.com/typescript-eslint/typescript-eslint/commits/v8.66.0/packages/typescript-estree">compare
view</a></li>
</ul>
</details>
<br />

Updates `@typescript-eslint/utils` from 8.65.0 to 8.66.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.66.0</h2>
<h2>8.66.0 (2026-08-03)</h2>
<h3>🚀 Features</h3>
<ul>
<li><strong>typescript-estree:</strong> handle import.defer() as
ImportExpression (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12609">#12609</a>)</li>
</ul>
<h3>🩹 Fixes</h3>
<ul>
<li><strong>eslint-plugin:</strong> [no-unnecessary-type-conversion]
ignore shadowed built-ins (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12590">#12590</a>)</li>
<li><strong>eslint-plugin:</strong> [prefer-nullish-coalescing] handle
shadowed Boolean calls (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12591">#12591</a>)</li>
<li><strong>eslint-plugin:</strong> [no-useless-default-assignment]
don't report defaults used by other overloads (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12607">#12607</a>)</li>
<li><strong>eslint-plugin:</strong> [no-unnecessary-type-parameters]
check MappedType key remapping (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12588">#12588</a>)</li>
<li><strong>eslint-plugin:</strong> [class-literal-property-style]
preserve type annotations and don't drop decorators (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12617">#12617</a>)</li>
<li><strong>website:</strong> list
<code>onUnsupportedTypeScriptVersion</code> in parser options (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12585">#12585</a>)</li>
</ul>
<h3>❤️ Thank You</h3>
<ul>
<li>Evyatar Daud <a
href="https://github.com/StyleShit"><code>@​StyleShit</code></a></li>
<li>Kirk Waiblinger <a
href="https://github.com/kirkwaiblinger"><code>@​kirkwaiblinger</code></a></li>
<li>Steffen Schroeder</li>
<li>송재욱</li>
</ul>
<p>See <a
href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.66.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.66.0 (2026-08-03)</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.66.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="e51b11ba3a"><code>e51b11b</code></a>
chore(release): publish 8.66.0</li>
<li>See full diff in <a
href="https://github.com/typescript-eslint/typescript-eslint/commits/v8.66.0/packages/utils">compare
view</a></li>
</ul>
</details>
<br />

Updates `@typescript-eslint/visitor-keys` from 8.65.0 to 8.66.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.66.0</h2>
<h2>8.66.0 (2026-08-03)</h2>
<h3>🚀 Features</h3>
<ul>
<...

_Description has been truncated_

---------

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-08-14 02:39:32 +02:00
nikita-ashihmin
35aaddb3d5 feat: publish the model fallback as a warning advisory (#990)
## Problem

When the API refuses a turn, the SDK retries on the fallback model and
makes the swap persistent for the session. We flag that with a
transcript line:

```ts
text: `**Model fallback:** ${message.original_model} declined this request${category}; retried with ${message.fallback_model}. ${outcome}${explanation}`,
```

It goes out as an `agent_message_chunk`, so the client cannot tell a
session-level advisory from something the model actually said — the bold
label is the only severity signal available, and it is just characters
inside the assistant's own text.

The comment above that code already states the stakes: *"Without a
notice the user just sees regenerated output."* The notice matters; the
channel it uses does not fit it.

## Change

Routes it through the AIR typed session-failure extension instead, as a
new `advisory` category:

- `severity?: "error" | "warning"` on the record. **Optional, and absent
means `error`**, so a client that predates this keeps treating every
record it receives as a failure.
- The advisory carries its own wording (`safeMessage`) rather than the
canned per-category message, so the client renders the explanation
verbatim in a warning banner.
- **Clients that did not negotiate the `sessionFailure` capability keep
the exact transcript line they get today.**

### Advisories are not turn failures

Two existing behaviours would have mangled them, so both are now
guarded:

- `createSessionFailure` clears every other active record when a new one
is published. An advisory no longer does that, and is no longer cleared
by one — otherwise a model-fallback notice would erase an actionable
error, or vice versa.
- `clearFailuresFromEarlierTurns` clears everything whose `turnId` is
not the active turn. Advisories carry no `turnId`, so without the guard
every turn boundary would sweep them away.

They therefore live under their own `:notice:` id namespace,
session-scoped, one slot, newest wins.

## Scoped out

`**Fast mode turned off:**` (`syncFastMode…`) is the same pattern and
deserves the same treatment, but it sits in a class method while the
failure machinery is closure-scoped inside `prompt()`. Lifting that out
is a separate change rather than something to smuggle into this one.

## Tests

`npm run build`, `npm run check`, and `npm run test:run` (**766
passed**) are clean.

The existing `model_refusal_fallback` tests still assert the `**Model
fallback:**` transcript line and still pass — they never call
`initialize`, so they exercise the no-capability path, which is exactly
the backward-compatibility guarantee.

One test added for the typed path, asserting that the advisory is
published with `severity: "warning"`, that its `safeMessage` carries the
real model ids and refusal category, that it uses the `:notice:` id
namespace, that it contains no `**` markdown lead-in, and that the
transcript line is **not** also emitted.

Client side: IJAI-993.
2026-08-12 22:16:28 +04:00
nikita-ashihmin
1f09e9a3ca feat: surface Skill tool calls with name and kind in _meta (#986)
## Summary

- Add `skill?: string` to `ToolUpdateMeta.claudeCode`, populated from
`toolUse.input.skill` in `claudeCodeMetaFromToolUse`. Lets ACP clients
detect skill invocations from structured metadata rather than parsing
the title string.
- Add `case "Skill":` to `toolInfoFromToolUse`: sets `title` to `"Load
skill: <name>"` and returns empty content — readable fallback for
clients that don't read `_meta`.
- Add `case "Skill":` to `toolUpdateFromToolResult`: returns `{}` to
suppress the raw `"Launching skill: <name>"` result text, matching the
pattern used for `Edit`/`Write`.

## Motivation

Without this, the Skill tool falls through to the `default` case and
renders as a generic `kind: "other"` block with title `"Skill"` and an
expand control showing the raw result text. With this change, clients
that read `_meta.claudeCode.skill` can render the block with the
appropriate icon, the skill name, and no expand control.

## Test plan

- [ ] Invoke a skill (e.g. `/commits`) and confirm
`_meta.claudeCode.skill` is present in the emitted `tool_call` update
- [ ] Confirm title is `"Load skill: commits"` in the ACP event stream
- [ ] Confirm no content is emitted for the skill tool result
2026-08-12 14:51:31 +04:00
nikita-ashihmin
ec73cd8560 feat: surface resolved model name in default model option description (#982)
## Summary

- When the SDK's `"default"` model entry carries a `resolvedModel` (e.g.
`"claude-sonnet-5"`), `buildConfigOptions` now sets the option's
`description` to the human-readable `displayName` of the matching named
model (e.g. `"Claude Sonnet 5"`)
- Falls back to the raw `resolvedModel` string if no named entry shares
that resolved ID
- No behaviour change when `resolvedModel` is absent

This lets ACP clients show the actual model behind "Default" — e.g.
"Default (Claude Sonnet 5)" — instead of just "Default (recommended)".

## Test plan

- [ ] `buildConfigOptions model option resolved description > sets
description to the named model's displayName when resolvedModel matches`
- [ ] `buildConfigOptions model option resolved description > falls back
to resolvedModel itself when no named model shares it`
- [ ] `buildConfigOptions model option resolved description > leaves
description undefined when default model has no resolvedModel`
- [ ] All 753 existing tests still pass

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

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-11 13:21:42 +04:00
nikita-ashihmin
8157ee113e feat: expose typed session failures for AIR (#979)
## Summary

- Negotiate the JetBrains AIR `sessionFailure` extension when the client
advertises an integer version `>= 1` and the capability name.
- Return turn-terminal failures only on `PromptResponse._meta`; publish
idle and asynchronous session health failures through `session/update`.
- Preserve legacy ACP behavior for clients that do not negotiate the
extension.
- Emit bounded fixed safe messages without SDK/provider details, paths,
credentials, or stack traces.

## Recovery contract

Failures use `_meta.jetbrains.air.sessionFailure` with a stable id,
monotonic revision, phase, category, source, safe message, retryability,
and optional real turn attribution. Session-scoped ids include a
consumer epoch, so replayed tombstones from a dead consumer cannot
suppress failures after restart. Stale consumers cannot publish into a
replacement session.

AIR owns localized action presentation. Claude supplies typed category
and retryability facts. Billing maps to quota exhaustion, max-output
limits map to context exhaustion, and the irreversibly closed Claude
Query reports non-retryable `transport_lost` so AIR offers New Session.

A held-open turn keeps its already recorded successful `PromptResponse`,
while worker shutdown or transport death also emits a separate
session-scoped health failure without `turnId`; the next prompt is
proven to fail because the Query is closed. Worker-shutdown remains
armed through buffered trailing frames and is classified only when EOF
proves the live tail.

## Validation

- 750 passed, 25 skipped.
- Build passed.
- ESLint passed.
- Changed files pass Prettier; the repository-wide check is blocked only
by an unrelated untracked local proposal file.
- Tests cover negotiation, taxonomy, sanitization, terminal/asynchronous
carrier separation, stale consumers, worker replay/live-tail behavior,
held turns, process/transport failure, restart-safe ids, clear
revisions, and capability-off behavior.
2026-08-10 14:15:44 +04:00
nikita-ashihmin
3df1ede89f fix: show a pending title while Claude prepares a file (#978)
## Summary

- show `Preparing file…` instead of the bare `Write` title while
streamed input has no `file_path`
- refine the same tool call to `Write <display path>` as soon as the
path is available
- cover both partial-field refinement and
complete-input/consolidated-message delivery

## Tests

- `npm run lint`
- `npx prettier --check src/tools.ts src/tests/acp-agent.test.ts`
- `npm run build`
- `npm run test:run` — 716 passed, 25 skipped

YouTrack: https://youtrack.jetbrains.com/issue/IJAI-841
2026-08-09 22:07:58 +04:00
nikita-ashihmin
1afa940a2c fix: preserve task plans across prompts (#974)
## Summary

- republish unfinished Task* plan snapshots when a follow-up prompt
starts
- avoid resurfacing fully completed historical plans on unrelated
prompts
- clear stale task state when Claude starts a fresh conversation
- reconcile task state from authoritative TaskList results after resume
or history compaction
- consume structured Task* outputs during live sessions and parse Claude
Code textual output when replaying history
- reject logically failed TaskUpdate results instead of persisting the
requested mutation
- keep scanning structured tool output past unrelated JSON blocks
- use Claude activeForm for in-progress TodoWrite and Task* entries

This keeps ACP clients from losing an unfinished task list at prompt
boundaries, restores state when incremental history is unavailable, and
prevents plans from leaking across conversation resets.

## Tests

- npm run test:run (713 passed, 20 skipped)
- npm run check
- npm run build
- git diff --check
2026-08-08 14:23:28 +04:00
17 changed files with 5769 additions and 392 deletions

View File

@@ -1,3 +1,3 @@
{
".": "0.66.0"
".": "0.69.0"
}

View File

@@ -1,5 +1,36 @@
# Changelog
## [0.69.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.68.0...v0.69.0) (2026-08-16)
### Features
* report changed files to AIR ([#1001](https://github.com/agentclientprotocol/claude-agent-acp/issues/1001)) ([450d6b1](https://github.com/agentclientprotocol/claude-agent-acp/commit/450d6b19dc46a041128356a6fa3cfa3ce6a5a382))
## [0.68.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.67.0...v0.68.0) (2026-08-14)
### Features
* align typed session failures with AIR protocol ([#992](https://github.com/agentclientprotocol/claude-agent-acp/issues/992)) ([0581b9c](https://github.com/agentclientprotocol/claude-agent-acp/commit/0581b9cf397ffd88f2830db721c2d8e3689045e4))
## [0.67.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.66.0...v0.67.0) (2026-08-14)
### Features
* **deps:** Update to @anthropic-ai/claude-agent-sdk v0.3.232 ([#993](https://github.com/agentclientprotocol/claude-agent-acp/issues/993)) ([de0d0e2](https://github.com/agentclientprotocol/claude-agent-acp/commit/de0d0e2b7d185c521c3d1ac7b86e4312c919abfe))
* expose typed session failures for AIR ([#979](https://github.com/agentclientprotocol/claude-agent-acp/issues/979)) ([8157ee1](https://github.com/agentclientprotocol/claude-agent-acp/commit/8157ee113e705750be4eb6cc787bdd12f1db84ff))
* publish the model fallback as a warning advisory ([#990](https://github.com/agentclientprotocol/claude-agent-acp/issues/990)) ([35aaddb](https://github.com/agentclientprotocol/claude-agent-acp/commit/35aaddb3d5b14eca316582ab285af8a660150dae))
* surface resolved model name in default model option description ([#982](https://github.com/agentclientprotocol/claude-agent-acp/issues/982)) ([ec73cd8](https://github.com/agentclientprotocol/claude-agent-acp/commit/ec73cd8560be7d5e8b9741e404d7d45e17336996))
* surface Skill tool calls with name and kind in _meta ([#986](https://github.com/agentclientprotocol/claude-agent-acp/issues/986)) ([1f09e9a](https://github.com/agentclientprotocol/claude-agent-acp/commit/1f09e9a3cae787b0effdfae0e7205ef7fe22b9dc))
### Bug Fixes
* preserve task plans across prompts ([#974](https://github.com/agentclientprotocol/claude-agent-acp/issues/974)) ([1afa940](https://github.com/agentclientprotocol/claude-agent-acp/commit/1afa940a2c8c0f4c610f4f64d30c0961642907b0))
* show a pending title while Claude prepares a file ([#978](https://github.com/agentclientprotocol/claude-agent-acp/issues/978)) ([3df1ede](https://github.com/agentclientprotocol/claude-agent-acp/commit/3df1ede89f217312bc237124dc1eccc10c860f99))
## [0.66.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.65.0...v0.66.0) (2026-08-07)

View File

@@ -17,6 +17,7 @@ This tool implements an ACP agent by using the official [Claude Agent SDK](https
- Custom [Slash commands](https://docs.anthropic.com/en/docs/claude-code/slash-commands)
- Client MCP servers
- Session-scoped long-running goals through the provider-neutral [goal extension](docs/goal-extension.md)
- Structured errors, recovery, and warnings through the opt-in [session failure extension](docs/session-failure-extension.md)
Learn more about the [Agent Client Protocol](https://agentclientprotocol.com/).

View File

@@ -0,0 +1,208 @@
# Session failure extension
This document defines the provider-neutral experimental `sessionFailure` extension implemented by
`claude-agent-acp`. Warnings and errors are durable transcript entries shown in order beside user,
agent, and tool messages. They are not assistant-authored text and not ephemeral chat banners.
## Capability negotiation
The extension is opt-in. A client advertises it in `initialize`:
```json
{
"clientCapabilities": {
"_meta": {
"jetbrains": {
"air": {
"version": 1,
"capabilities": ["sessionFailure"]
}
}
}
}
}
```
The adapter enables the extension only when `version` is a finite integer greater than or equal to
`1` and `capabilities` contains `sessionFailure`. Otherwise it preserves the legacy ACP behavior.
## Wire record
The record is placed under `_meta.jetbrains.air.sessionFailure`:
```json
{
"_meta": {
"jetbrains": {
"air": {
"version": 1,
"sessionFailure": {
"id": "prompt-uuid:error",
"revision": 1,
"category": "limit",
"severity": "error",
"title": "You've hit your individual spend limit · run /usage-credits to ask your admin for a higher limit",
"actions": []
}
}
}
}
}
```
| Field | Required | Type | Meaning |
| ---------- | -------: | -------------------- | ----------------------------------------------------------------------- |
| `id` | yes | non-empty string | Stable identity of one logical incident. |
| `revision` | yes | positive integer | Monotonically increasing version of that incident. |
| `category` | yes | category enum | Broad machine-readable visual group. |
| `severity` | yes | `warning` or `error` | Inline warning or error presentation. |
| `title` | yes | string | Complete normal user-facing presentation. |
| `details` | no | string | Long explanation used only when required text is too large for `title`. |
| `actions` | yes | ordered string array | Recovery actions recommended by the adapter. |
There are no `phase`, `source`, `safeMessage`, `retryable`, `retryAfterMs`, `turnId`, retry-counter,
or provider-code fields. Retry progress belongs in `title` when Claude supplies it. Transcript order
and persistence time come from the receiving ACP event.
## Identity, revisions, and history
`id` identifies one occurrence, not an error type.
- The first record for an incident creates one transcript entry at the current stream position.
- The same `id` with a higher `revision` updates that entry in place without moving it.
- The same or a lower revision is idempotently ignored by the client.
- A later independent occurrence uses a new `id`, even when its category and text are identical.
- Consecutive updates for the same notice may reuse one id and increment its revision.
- Different ids are never deduplicated by comparing title or category.
Live turn failures use `<turnId>:error`. Session-scoped incidents use an adapter-session epoch plus an
incident sequence. Notices have their own `:notice:` namespace. Replayed usage-limit failures recover
the persisted user-message UUID used as the live turn id, so the same occurrence keeps the same id.
Malformed history without a preceding user message falls back to
`<sessionId>:history-error:<messageUuid>`.
Resolved records remain in transcript history. Recovery removes only the adapter's internal active
state; it does not publish a clear tombstone and does not delete or rewrite the historical entry. A
producer may publish a higher revision only when it has a real new user-facing status to show.
## Delivery surfaces
A turn-terminal failure is attached to the successful ACP `PromptResponse` in `_meta`; the response
uses `stopReason: end_turn`. Session-scoped, replay-restored, warning, and background incidents use a
`session_info_update`. The schema is identical on both carriers.
A warning does not end a turn. An error record does not independently invent ACP lifecycle state;
the prompt response or transport lifecycle remains authoritative. If no capable client negotiated
the extension, existing JSON-RPC errors and transcript text remain unchanged.
## Categories
Categories are deliberately broad and drive only iconography and generic accessibility labels.
| Category | Covers |
| ------------ | --------------------------------------------------------------------- |
| `connection` | Lost transport, stopped or shutting-down worker, unavailable runtime. |
| `access` | Authentication required, rejected credentials, denied access. |
| `limit` | Rate, quota, token, context, turn, or configured budget limit. |
| `request` | Invalid input, unsupported model or operation, rejected request. |
| `service` | Provider overload, provider failure, adapter/internal failure. |
| `unknown` | A warning or failure that does not fit another group. |
Claude SDK conditions map to these groups:
| Claude condition | Category |
| ---------------------------------------------------------------------------------------- | ---------------------------------- |
| `authentication_failed`, `oauth_org_not_allowed`, synthetic login message | `access` |
| `billing_error`, `rate_limit`, `max_output_tokens`, usage/spend limit, budget/turn limit | `limit` |
| `invalid_request`, `model_not_found` | `request` |
| `overloaded`, `server_error`, unknown provider error, adapter internal error | `service` |
| API retry without an HTTP response, query transport loss, worker shutdown | `connection` |
| model fallback notice | `unknown` with `severity: warning` |
Unknown SDK error kinds degrade to `service`; they never become success. Category does not determine
message text or client behavior beyond presentation.
## Severity
- `warning` means the operation may still succeed and normally has no actions while recovery is in
progress. It does not terminate the turn. SDK `api_retry` progress reuses the active turn failure id;
a later terminal failure updates that record with a higher revision.
- `error` means the operation cannot continue without user action or another request.
Severity is always explicit.
## Title and details
`title` is the complete normal presentation: what happened, retry progress when present, and a short
next step. The adapter must not replace a real Claude message with category-specific canned wording.
For Claude SDK failures the title is copied from the user-facing text Claude already emitted:
- top-level assistant error text, including synthetic usage-limit and login messages;
- otherwise the terminal SDK result or error text;
- the exact model-fallback notice for warning records.
Adapter-authored fallback text is allowed only when no Claude user-facing message exists, such as a
query iterator throwing, worker-shutdown EOF, or an internal missing-result invariant. Raw thrown
exceptions, stack traces, transport URLs, tokens, headers, environment values, and private paths are
never used as titles.
`details` is reserved exclusively for required explanations too large to fit reasonably in `title`.
If the complete message fits in `title`, `details` must be omitted. It is not a place for status text,
retry counters, provider payloads, or diagnostics.
## Actions
Version 1 defines only:
| Action | Meaning |
| ------------- | -------------------------------------------------- |
| `retry` | Retry the operation associated with this incident. |
| `login` | Start authentication. |
| `new_session` | Start a fresh agent session/runtime. |
The adapter supplies action ordering. The client filters actions it cannot safely execute and ignores
unknown or duplicate values. The client must not infer actions from category.
Current Claude policies are:
- authentication: `login`;
- provider overload/error and recoverable internal error: `retry`;
- transport loss or worker shutdown: `new_session`;
- context or configured session-budget exhaustion: `new_session`;
- account quota and invalid request: no action.
## Internal recovery
Recovery policy is internal adapter state and is not serialized:
- restored quota remains active until a real model answer;
- authentication remains active until successful `auth_status`;
- transport loss and worker shutdown remain active until runtime replacement;
- other turn-scoped failures stop being active at a later confirmed attempt boundary;
- notices are not cleared merely by generic success.
Recovery never removes the transcript record. Publishing another incident is not evidence that an
older one recovered.
## History replay
`session/load` scans top-level assistant history for SDK synthetic usage-limit messages. It recognizes
only `<synthetic>` messages beginning with SDK-owned stable prefixes, not arbitrary model prose. Every
recognized occurrence is replayed as a typed record at its original transcript position. The latest
matching message stays active until a later real-model answer proves recovery; older and recovered
records remain historical typed entries.
For a capable client, replay suppresses duplicate assistant prose and restores typed errors using the
exact stored Claude text as `title`. Legacy clients receive the original transcript and no typed record.
## Verification
The executable contract lives primarily in `src/tests/acp-agent.test.ts`, under `usage-limit failure
replay`, `stop reason propagation`, and `model refusal fallback handling`.
```sh
npm run build
npm run check
npm run test:run
```

406
package-lock.json generated
View File

@@ -1,31 +1,31 @@
{
"name": "@agentclientprotocol/claude-agent-acp",
"version": "0.66.0",
"version": "0.69.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@agentclientprotocol/claude-agent-acp",
"version": "0.66.0",
"version": "0.69.0",
"license": "Apache-2.0",
"dependencies": {
"@agentclientprotocol/sdk": "1.3.0",
"@anthropic-ai/claude-agent-sdk": "0.3.220",
"@anthropic-ai/claude-agent-sdk": "0.3.232",
"zod": "^3.25.0 || ^4.0.0"
},
"bin": {
"claude-agent-acp": "dist/index.js"
},
"devDependencies": {
"@anthropic-ai/sdk": "0.115.0",
"@anthropic-ai/sdk": "0.117.1",
"@eslint/js": "10.0.1",
"@tsconfig/node22": "22.0.5",
"@types/node": "26.1.2",
"@typescript-eslint/eslint-plugin": "8.65.0",
"@typescript-eslint/parser": "8.65.0",
"eslint": "10.8.0",
"@types/node": "26.2.0",
"@typescript-eslint/eslint-plugin": "8.67.0",
"@typescript-eslint/parser": "8.67.0",
"eslint": "10.8.1",
"eslint-config-prettier": "10.1.8",
"globals": "17.9.0",
"globals": "17.11.0",
"prettier": "3.9.6",
"ts-node": "10.9.2",
"typescript": "6.0.3",
@@ -45,22 +45,22 @@
}
},
"node_modules/@anthropic-ai/claude-agent-sdk": {
"version": "0.3.220",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.220.tgz",
"integrity": "sha512-glc7SdwPkOkLw8oxwLo9PKTdLJGqW/PIR4urWXFoRtX9YllwozsEVc5Tc1+EvLSkfrsxPJqQWqOgpjUOQXf1oA==",
"version": "0.3.232",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.232.tgz",
"integrity": "sha512-8od7hJk9fZnF1/oYYiR9PvroGbZRQrpmNgKirjHNGoj5ur5YcAZLohI70XVUAUe3KvjB1msLxtkvmlAT9sqFAg==",
"license": "SEE LICENSE IN README.md",
"engines": {
"node": ">=18.0.0"
},
"optionalDependencies": {
"@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.220",
"@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.220",
"@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.220",
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.220",
"@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.220",
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.220",
"@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.220",
"@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.220"
"@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.232",
"@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.232",
"@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.232",
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.232",
"@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.232",
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.232",
"@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.232",
"@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.232"
},
"peerDependencies": {
"@anthropic-ai/sdk": ">=0.93.0",
@@ -69,9 +69,9 @@
}
},
"node_modules/@anthropic-ai/claude-agent-sdk-darwin-arm64": {
"version": "0.3.220",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.220.tgz",
"integrity": "sha512-7VxlbEosK7DODiOnsjoVd0DSJzbnaPrM2jelMHI0y8zx1UnLS3WC6EFUXbvy74F2sXqEznh2tzn7EKWInaRN6Q==",
"version": "0.3.232",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.232.tgz",
"integrity": "sha512-+/4PX+dwmQAjlOlooocwa3kClulZfMo133xQH3LYDlK7D5bzze16lwlDGPVAYGEarpvXg7G5JK8QjfWAJ2HYbg==",
"cpu": [
"arm64"
],
@@ -82,9 +82,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-darwin-x64": {
"version": "0.3.220",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.220.tgz",
"integrity": "sha512-X9RwDsSmbF6ultKZroaip+DL8WRgC64gHbrAwrRlAFSPNZV7zmJyP2ur8rW7KrxqmtuehdMMkw8+SAC/6hD2PA==",
"version": "0.3.232",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.232.tgz",
"integrity": "sha512-EHZ1Y3aGyZ2mFZ6QLR1bM3/HiIn2cLrPjU+k3/oCIW6omJFodfzf410aWjDPSnMj7CE4d6t7MSjBWekMUbcv0g==",
"cpu": [
"x64"
],
@@ -95,9 +95,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64": {
"version": "0.3.220",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.220.tgz",
"integrity": "sha512-WkROPwWskqhKR9XgnmseHQ6rLi9zM9qt57IWoToIjL/eXOqDWipp7JXZ1L5ud+LrA42dunHPZfBwD/vXZ+A7LA==",
"version": "0.3.232",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.232.tgz",
"integrity": "sha512-wW2opwA5s7gLghjU6B2ADMAtoc7bAZMevUzi4g+1PXMJ8MGcPvbnx92EDYJrVDcZmAl1+fz19XyPsaShlisTzw==",
"cpu": [
"arm64"
],
@@ -111,9 +111,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64-musl": {
"version": "0.3.220",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.220.tgz",
"integrity": "sha512-OHoZOZ8Cf2TBr6oXIXPwyvUxj9jrq2w8E4poA8dMpacXszcPSPiCQCMuuOh4aWJzfeJE1+TtWxhKMVb2csXyZQ==",
"version": "0.3.232",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.232.tgz",
"integrity": "sha512-XkLcb9UT/l42Rtw7KBApzgxUe/kwoWJ9KCPcVEnYojzIvVR+AwBCl/QReNU5+6c72w48dYBMmLebI64gQbN0tg==",
"cpu": [
"arm64"
],
@@ -127,9 +127,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-linux-x64": {
"version": "0.3.220",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.220.tgz",
"integrity": "sha512-tkTJFnpR9VifvWX2fmkCAPkT6+8Wk/gVu8B5jsVekKZPiZoWRHmMXO30BnZn+f0TZhgYP+82PSX3S8crH1kn+w==",
"version": "0.3.232",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.232.tgz",
"integrity": "sha512-6Px1xDiwQyLkSxwRQ34/kPA8WMXQ2rHYGkwockND7+9yMw+ShI3AfLkyi9G7JtJHObWFT6B82eSHjDS/Fy+9hQ==",
"cpu": [
"x64"
],
@@ -143,9 +143,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-linux-x64-musl": {
"version": "0.3.220",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.220.tgz",
"integrity": "sha512-K+FWj+LcGhC1Z7wqeWoLxm1iemcba5xKpLLFVwYm4V6HyMx3ruYd/2r2TiQtjT+JWeNFWIys0ScHiItR6vWAiA==",
"version": "0.3.232",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.232.tgz",
"integrity": "sha512-L1x2ge9NpXMLTczmT44TKPQ88PHE+gsCakQMVEOa8rXFvfBoqvcpxM0DT8wrqYWUHhQEYR8NXxQTP9a1NVRj8Q==",
"cpu": [
"x64"
],
@@ -159,9 +159,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-win32-arm64": {
"version": "0.3.220",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.220.tgz",
"integrity": "sha512-rIwgq0UwQExWl6KrHUyC4w5KwpL9l6nd95aUTx6RitexaAuEw//xtfTVLnuE4hDDQZFkzEwpdKc3nxDWoGcUbA==",
"version": "0.3.232",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.232.tgz",
"integrity": "sha512-fDiuwL5dm1elOy7fNp4Qmdor8so4R8npj2pUGQA1G/xKfJhaK236aTWezvZid3L/O7cRdFeN9IVofOAlWLQcsw==",
"cpu": [
"arm64"
],
@@ -172,9 +172,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-win32-x64": {
"version": "0.3.220",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.220.tgz",
"integrity": "sha512-MuOuXhbr66HlGaWXD2f3w0k2PsvmnbkwcUZ0dAe2poFLdl72GC2dapwwOBefxm9QmoNqk9+jmv/dSKGOVWyvLw==",
"version": "0.3.232",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.232.tgz",
"integrity": "sha512-Hc/9uy1BI9mqKVyB1b/zoUnm3MFgtVNzQY6p5zgaq9DaIjCKDpR4a4L1aDZM4lMqUcWFMZM+u7juOhh+m39NBQ==",
"cpu": [
"x64"
],
@@ -185,9 +185,9 @@
]
},
"node_modules/@anthropic-ai/sdk": {
"version": "0.115.0",
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.115.0.tgz",
"integrity": "sha512-BJrFIVyjNuU8lfDyIJTvlRYzgQg+zEl78BxE7fq8esULsGz9IRQvGtW5spq3tydmtjQb/GFdooKGdGsetpx+lQ==",
"version": "0.117.1",
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.117.1.tgz",
"integrity": "sha512-Yn2QlXfyCiKJ5YGCOOay7ZE78ISvII2XY621WMCiflmG8IYgwx59IBwPExxki3Xk9jKUtnD/Sj6UvplWr0rZxg==",
"license": "MIT",
"dependencies": {
"json-schema-to-ts": "^3.1.1",
@@ -491,9 +491,9 @@
}
},
"node_modules/@oxc-project/types": {
"version": "0.142.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.142.0.tgz",
"integrity": "sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ==",
"version": "0.144.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.144.0.tgz",
"integrity": "sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg==",
"dev": true,
"license": "MIT",
"funding": {
@@ -501,9 +501,9 @@
}
},
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.2.tgz",
"integrity": "sha512-l7x215OGvo1s52JWmR8U/DAVzEDWBCIbTm28aeJV/WDTSHgcKXaZTuBT0hJMs5NggilfJTW3clZVvd24yfKJxA==",
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.4.tgz",
"integrity": "sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA==",
"cpu": [
"arm64"
],
@@ -518,9 +518,9 @@
}
},
"node_modules/@rolldown/binding-darwin-arm64": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.2.tgz",
"integrity": "sha512-9u9Xv6c1AJZT0FfwH5vrMG5Jjcwhc1MlyrPu0XfTqkzsmqfks2M6W/o5XwAJgVVN/jHpqqngC1WevHKKTIUtIA==",
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.4.tgz",
"integrity": "sha512-Dc5mPD8F5F/FS8i01syd7FTF6yB2fVthH/TRkjwJkzUK6EpoxHtqvZQP5Zwq80/5z19TWYHIg1KOHboCgVx/aQ==",
"cpu": [
"arm64"
],
@@ -535,9 +535,9 @@
}
},
"node_modules/@rolldown/binding-darwin-x64": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.2.tgz",
"integrity": "sha512-9W1mbGZAfW3oqd85bhBkmpyHCCzL1TeG/zFFP3vg7b0rlly8cxOcre5nXwz+LHazCwac2MNWgPdPCHndABjpWQ==",
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.4.tgz",
"integrity": "sha512-fpDm4oBo6SqLvWUYCmFhdde3U9KH2fRNNMeAnAPAIwxRL345xutL0EtEUcuoxsoazdJGv/MuDBQHlCDrtbvqOg==",
"cpu": [
"x64"
],
@@ -552,9 +552,9 @@
}
},
"node_modules/@rolldown/binding-freebsd-x64": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.2.tgz",
"integrity": "sha512-0p1lhiCSCyaerFwtrdZQUx7NqGk6LQnaRKWX7tFQqwQgvX0rjM15cIkm3pax1UpEakK14C4mOxx/jSqCBdBRqQ==",
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.4.tgz",
"integrity": "sha512-rSJoreDE/HoIzoaib6MTp5jQtCTdMHKIvItAKT/ImS6Y6Ww76oUaeMyp4Vc/fAgd/ehji068IxetHXAnqUwN9A==",
"cpu": [
"x64"
],
@@ -569,9 +569,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.2.tgz",
"integrity": "sha512-e+cOJXrJ2L3zx6YzqPg+f6Wbk3V1cKB8bOhbaYdVYN3DdquzNdRAmrbETz1qnt5yp/c7JNlNjmITiA2cVneQ7w==",
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.4.tgz",
"integrity": "sha512-/jm8OGHgn7oGaJu3i/qZI9spUGcJ+y/lk43ttQ/iO1tOd9NissG6o97bighBCiL+BKRngmcDuR6ikfwYdJmVuQ==",
"cpu": [
"arm"
],
@@ -586,9 +586,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-gnu": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.2.tgz",
"integrity": "sha512-JsSMsj6sNat/MuhG5fnBD7QgbtpHKVe30x5/bAVirDHdhoQRXJkF6xc0Jqk8O4fiCUQAzMOoH9wZi3m60c8wtg==",
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.4.tgz",
"integrity": "sha512-tIP06BeD9EqvECBrPZ+sqdPlYrT+aYaAiu1wYziVx5elRK/ftm33JxVDy2bXGbr6J0CrtirCkR87/X5a2euEng==",
"cpu": [
"arm64"
],
@@ -606,9 +606,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-musl": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.2.tgz",
"integrity": "sha512-B5G/zJdHaoJn9vD50eGHWkiWfmq8Uhi3IiLPJTzmZTrAalk1bztUikSXo0qga18ibE0IXboyeMUnhPjhAJ45wQ==",
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.4.tgz",
"integrity": "sha512-Ql1Q0EQqVThvn9VAVlwNzsUvbSFtCMGjLpRRi4pk5i7NZZ4n5ISiLMjHYtus4VQ2PvkSw24zyaCVsiS+sXPj1w==",
"cpu": [
"arm64"
],
@@ -626,9 +626,9 @@
}
},
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.2.tgz",
"integrity": "sha512-6mC/awzKka8W6EoekjegpfGkjz8jXWDX63pqu/HYVpyKtZfu65Jsh4QAH3Kej3CAv/c1oGX7psTmFEbr0mDxLA==",
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.4.tgz",
"integrity": "sha512-GjbjXD4XXfN19D0LZNbmiCBUoDiRACsYHr0yaIbbn8aFsXjHZifcYqu/W5Er5X2X990WjHXFrxarn5chzItorQ==",
"cpu": [
"ppc64"
],
@@ -646,9 +646,9 @@
}
},
"node_modules/@rolldown/binding-linux-s390x-gnu": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.2.tgz",
"integrity": "sha512-412MX9fJLdA1IK28EZnc8jYv2HRTleOZgfLQumJ5zy7OeJLZlg/CETwFaXjNmGVxG51cFHpKLqb5LKvBC+HsHA==",
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.4.tgz",
"integrity": "sha512-p5WR0NOwaRmJ/B1b6IjEFLLivwEsf3PrdBIhRbhTCQisbo2SvHHpG4ELB/+FgQNnB88LTOF86upmJmbvZdQ2lw==",
"cpu": [
"s390x"
],
@@ -666,9 +666,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-gnu": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.2.tgz",
"integrity": "sha512-Q/+HI/ToJafZ1iCqGgVQXUEkIjufHCTF0gBQ2a5o3cg7GJ2h0qyq3nvvSmU+bGda2/7ygXpTY4TM6gO9OhQ0ZA==",
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.4.tgz",
"integrity": "sha512-4/GyVjmhR+Tc6HLJvwc1sOhPqAZtySiSMesOZyX6JQ5XBxoTDEMKQzvo07NIK6nTon/SivlZqvhzvuVBNQhObQ==",
"cpu": [
"x64"
],
@@ -686,9 +686,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-musl": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.2.tgz",
"integrity": "sha512-ZKp/w41n6wCvxzxQHtQSbuphfX3Y4cCvbjkKHusrLx4lh+JWLTU7StSltO/DKARISzbj368d+qUaCjI8K2wzXw==",
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.4.tgz",
"integrity": "sha512-l9eeLsCNvPpmSXUej0etw/J1eqV0Jj1D5G/xG6YTijmE6dkv6E2QezgWbTfQk63v952DPqrjOCoiqxq7Bw0YUQ==",
"cpu": [
"x64"
],
@@ -706,9 +706,9 @@
}
},
"node_modules/@rolldown/binding-openharmony-arm64": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.2.tgz",
"integrity": "sha512-pxE6xD4KS3eAROkKK5yrhB9/3+vhlhVGMvlQLbdpzrBGDbKrnzx3RLwPaHvasLo6jgaiBLn7e04Df9C4tYhjmA==",
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.4.tgz",
"integrity": "sha512-e0F355MSTMm3+UOqtV3L24gFUp2N5m1f8L/7d56deik6va+AXdrt9F8LbzGpeWGWRbZEDq4m8NVnJDeBtf9DZg==",
"cpu": [
"arm64"
],
@@ -723,9 +723,9 @@
}
},
"node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.2.tgz",
"integrity": "sha512-4MqEue5re+xIZzAWsB8sj0P1kqZySWqIuN4t6QaIO/YA6SFwySOLruvWQFzfmqk8LBK2P30KCSJwf6mJCZZ5/A==",
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.4.tgz",
"integrity": "sha512-AWLi0uBRYh6QlE7OKhiz+phZC0qwtij2QZmhmOdsLdFn64m7oMpooE9ICE3lhm9xMb4SpDo2WbHcxX1iFLFtqw==",
"cpu": [
"arm64"
],
@@ -740,9 +740,9 @@
}
},
"node_modules/@rolldown/binding-win32-x64-msvc": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.2.tgz",
"integrity": "sha512-NweNxxD0Nf9t8v7kodun45Ijp3EIwYY+uydPP6qBEYvfBqhIjN6dZMzlQja3tqX/aLs3F3Uz+AxDpKgRhpOZQg==",
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.4.tgz",
"integrity": "sha512-UwSDJOg3dqCAejWdxclJjCsh3Qq4vLYMDxmyHqo1btz3stK2VqgwNd3mm5tuIwzSlGIQ/1H9Hr+Zn09mrezNqQ==",
"cpu": [
"x64"
],
@@ -851,9 +851,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "26.1.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.2.tgz",
"integrity": "sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==",
"version": "26.2.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz",
"integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -861,17 +861,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz",
"integrity": "sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==",
"version": "8.67.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.67.0.tgz",
"integrity": "sha512-Un7Heoyj65NREbKAyIrFxeM143NZpExWmy1Nep4DLeQOeLlTeumPjoNKnBrU5D5moWXbPJgRa5Uwcdu0faVNGQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.12.2",
"@typescript-eslint/scope-manager": "8.65.0",
"@typescript-eslint/type-utils": "8.65.0",
"@typescript-eslint/utils": "8.65.0",
"@typescript-eslint/visitor-keys": "8.65.0",
"@typescript-eslint/scope-manager": "8.67.0",
"@typescript-eslint/type-utils": "8.67.0",
"@typescript-eslint/utils": "8.67.0",
"@typescript-eslint/visitor-keys": "8.67.0",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.5.0"
@@ -884,22 +884,22 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.65.0",
"@typescript-eslint/parser": "^8.67.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.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.65.0.tgz",
"integrity": "sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==",
"version": "8.67.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.67.0.tgz",
"integrity": "sha512-fUBfTuuEulWqX6V8+O3PtScV01tzYYRUDTAirHFKoRAt7nOzoGiPt0M/bB47wWNy0coOOcgEwAMUtBpykMxl6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.65.0",
"@typescript-eslint/types": "8.65.0",
"@typescript-eslint/typescript-estree": "8.65.0",
"@typescript-eslint/visitor-keys": "8.65.0",
"@typescript-eslint/scope-manager": "8.67.0",
"@typescript-eslint/types": "8.67.0",
"@typescript-eslint/typescript-estree": "8.67.0",
"@typescript-eslint/visitor-keys": "8.67.0",
"debug": "^4.4.3"
},
"engines": {
@@ -915,14 +915,14 @@
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz",
"integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==",
"version": "8.67.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.67.0.tgz",
"integrity": "sha512-cvE8c7ulYeXN9fYuszhCeCsbzyVEXuhrRCybnBre7TUmqb5nRmBfQAwCj0O3WJFDeyAZt4VYv51vMCC9LHSdYw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.65.0",
"@typescript-eslint/types": "^8.65.0",
"@typescript-eslint/tsconfig-utils": "^8.67.0",
"@typescript-eslint/types": "^8.67.0",
"debug": "^4.4.3"
},
"engines": {
@@ -937,14 +937,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz",
"integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==",
"version": "8.67.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.67.0.tgz",
"integrity": "sha512-EgvsleTwS4E+WzzSvem8fAUubLwatMNF1B5hHSLQxcvs7q2dtRhGyujHwLJSYlG41niJ7GP24Aha2+0mb1b2kg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.65.0",
"@typescript-eslint/visitor-keys": "8.65.0"
"@typescript-eslint/types": "8.67.0",
"@typescript-eslint/visitor-keys": "8.67.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -955,9 +955,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz",
"integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==",
"version": "8.67.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.67.0.tgz",
"integrity": "sha512-vV+LUSv5njUWsknE71fqKTlXUva+R76SaeORd6Zojcunk/6DvKFXONU3BrAs2H49mbygUXt6gbYunzwqNwlhdg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -972,15 +972,15 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.65.0.tgz",
"integrity": "sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==",
"version": "8.67.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.67.0.tgz",
"integrity": "sha512-aVWDXbRmdXO9siTfX4ditQI1T9+zVcNazT48EJCD0v40/9RIFoUgZ05CmGEq9H2gixRpjUn/iplwvlcvutJW/Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.65.0",
"@typescript-eslint/typescript-estree": "8.65.0",
"@typescript-eslint/utils": "8.65.0",
"@typescript-eslint/types": "8.67.0",
"@typescript-eslint/typescript-estree": "8.67.0",
"@typescript-eslint/utils": "8.67.0",
"debug": "^4.4.3",
"ts-api-utils": "^2.5.0"
},
@@ -997,9 +997,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz",
"integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==",
"version": "8.67.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.67.0.tgz",
"integrity": "sha512-sBtgslww8nsMYUjhdPBiSyUqSzT8uR6g93A2QXnQC8+cGdjz0CyaOdqHDRJb1AtORbZCNUJBBeFA/tNR2uQmww==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1011,16 +1011,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz",
"integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==",
"version": "8.67.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.67.0.tgz",
"integrity": "sha512-EKQBCE9yNlRJYm7jdTW5AhDacDUmSwQb0FAJAmK2EKYrNXIsa2vxcSZx6PvJ/dEdI6lS+Y9W+EXckLj0iPFGcw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.65.0",
"@typescript-eslint/tsconfig-utils": "8.65.0",
"@typescript-eslint/types": "8.65.0",
"@typescript-eslint/visitor-keys": "8.65.0",
"@typescript-eslint/project-service": "8.67.0",
"@typescript-eslint/tsconfig-utils": "8.67.0",
"@typescript-eslint/types": "8.67.0",
"@typescript-eslint/visitor-keys": "8.67.0",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
@@ -1039,16 +1039,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.65.0.tgz",
"integrity": "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==",
"version": "8.67.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.67.0.tgz",
"integrity": "sha512-U9D1FdwEWBwok3hxxSdhclMb0twvt9QnjIQ0VfQ1AiX2epnpSgv2ubVDsayOFyY8K6FX+AQ7E0FKWVG3iKsj1A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.65.0",
"@typescript-eslint/types": "8.65.0",
"@typescript-eslint/typescript-estree": "8.65.0"
"@typescript-eslint/scope-manager": "8.67.0",
"@typescript-eslint/types": "8.67.0",
"@typescript-eslint/typescript-estree": "8.67.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1063,13 +1063,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz",
"integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==",
"version": "8.67.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.67.0.tgz",
"integrity": "sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.65.0",
"@typescript-eslint/types": "8.67.0",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
@@ -1344,9 +1344,9 @@
}
},
"node_modules/body-parser/node_modules/content-type": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz",
"integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz",
"integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
"license": "MIT",
"peer": true,
"engines": {
@@ -1658,9 +1658,9 @@
}
},
"node_modules/eslint": {
"version": "10.8.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.0.tgz",
"integrity": "sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==",
"version": "10.8.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.1.tgz",
"integrity": "sha512-wqA7W2jbsC/BnV9Iv1UZpKVFkO1AdNoSmYW8NWG4HNOBbkAMvIqDZ27pI2f07dqn583NcIC44ckjAcOXDL1QbQ==",
"dev": true,
"license": "MIT",
"workspaces": [
@@ -1922,9 +1922,9 @@
}
},
"node_modules/eventsource-parser": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz",
"integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==",
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz",
"integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==",
"license": "MIT",
"peer": true,
"engines": {
@@ -1986,9 +1986,9 @@
}
},
"node_modules/express-rate-limit": {
"version": "8.6.1",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.1.tgz",
"integrity": "sha512-0D493aP61w0TJ2A0wy27riRsO7FMQ7FK+KUHOKCSfPvYo0R55aiC6emCVgFUeShH0fq0ICPVzNcgoS+BsbXQCA==",
"version": "8.6.2",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.2.tgz",
"integrity": "sha512-YH4ru+eOJxQABscKFfRCy9R7x9QFGdezclVMwwgFFndzS2Xnm0uo6B0ABZsLhcpeptGv2qvuJVWlQr9gQZoC3A==",
"license": "MIT",
"peer": true,
"dependencies": {
@@ -2237,9 +2237,9 @@
}
},
"node_modules/globals": {
"version": "17.9.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-17.9.0.tgz",
"integrity": "sha512-m/MvAW61QVU5VDNF1Vj8axt016h8w7L5TU1e9zlab7XIttAT2YAlCwl75K1fOqvMM9apmD7lbCIRhpfkhmxhCg==",
"version": "17.11.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-17.11.0.tgz",
"integrity": "sha512-Z2I8hM+PbJDXQDq3Icgpzv+mPdwr68iZUU9d5WW4FuXfDUQfkZaZuvjMv42/5crNyw154+9+VWXbYrUgDXbxNw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2289,9 +2289,9 @@
}
},
"node_modules/hono": {
"version": "4.13.0",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.13.0.tgz",
"integrity": "sha512-jhunvfHWxd7J5EFfSgH4xsYJzSe/lfqbUCxiyyeaQasUsXeEHXtzVid+7EOGByc5JnFa23SSFL3Y2RV/z1T+eQ==",
"version": "4.13.2",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.13.2.tgz",
"integrity": "sha512-JydRilDRkYBQMt9qR9U92mXxmbGqsqSn/IKOrh4e7/gEbn+0zSr8igTu0obwJoNGN4sez28DIql7FBHWydoJpA==",
"license": "MIT",
"peer": true,
"engines": {
@@ -2364,9 +2364,9 @@
"peer": true
},
"node_modules/ip-address": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.4.0.tgz",
"integrity": "sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ==",
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.5.0.tgz",
"integrity": "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==",
"license": "MIT",
"peer": true,
"engines": {
@@ -2887,9 +2887,9 @@
"license": "MIT"
},
"node_modules/nanoid": {
"version": "3.3.17",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.17.tgz",
"integrity": "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==",
"version": "3.3.18",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
"integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
"dev": true,
"funding": [
{
@@ -3110,9 +3110,9 @@
}
},
"node_modules/postcss": {
"version": "8.5.25",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz",
"integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==",
"version": "8.5.26",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
"integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==",
"dev": true,
"funding": [
{
@@ -3130,7 +3130,7 @@
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.16",
"nanoid": "^3.3.17",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@@ -3246,13 +3246,13 @@
}
},
"node_modules/rolldown": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.2.tgz",
"integrity": "sha512-opwpo1tQBAcpSUJDt94B7hhLNGOKjCdE//XXjeLrnx9b83bjnw45tXdg1b09yEw/VLFBJGZpwRULMmOZo7ol+A==",
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.4.tgz",
"integrity": "sha512-rSr7irW0K7QRWzjdJXqZowkcRdDtjRduh43rBltnVKd0VFq839l1lJoDvGJb6gl7+4rTTCrPWu+YfujUL8Ug7w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@oxc-project/types": "=0.142.0",
"@oxc-project/types": "=0.144.0",
"@rolldown/pluginutils": "^1.0.0"
},
"bin": {
@@ -3262,20 +3262,20 @@
"node": "^20.19.0 || >=22.12.0"
},
"optionalDependencies": {
"@rolldown/binding-android-arm64": "1.2.2",
"@rolldown/binding-darwin-arm64": "1.2.2",
"@rolldown/binding-darwin-x64": "1.2.2",
"@rolldown/binding-freebsd-x64": "1.2.2",
"@rolldown/binding-linux-arm-gnueabihf": "1.2.2",
"@rolldown/binding-linux-arm64-gnu": "1.2.2",
"@rolldown/binding-linux-arm64-musl": "1.2.2",
"@rolldown/binding-linux-ppc64-gnu": "1.2.2",
"@rolldown/binding-linux-s390x-gnu": "1.2.2",
"@rolldown/binding-linux-x64-gnu": "1.2.2",
"@rolldown/binding-linux-x64-musl": "1.2.2",
"@rolldown/binding-openharmony-arm64": "1.2.2",
"@rolldown/binding-win32-arm64-msvc": "1.2.2",
"@rolldown/binding-win32-x64-msvc": "1.2.2"
"@rolldown/binding-android-arm64": "1.2.4",
"@rolldown/binding-darwin-arm64": "1.2.4",
"@rolldown/binding-darwin-x64": "1.2.4",
"@rolldown/binding-freebsd-x64": "1.2.4",
"@rolldown/binding-linux-arm-gnueabihf": "1.2.4",
"@rolldown/binding-linux-arm64-gnu": "1.2.4",
"@rolldown/binding-linux-arm64-musl": "1.2.4",
"@rolldown/binding-linux-ppc64-gnu": "1.2.4",
"@rolldown/binding-linux-s390x-gnu": "1.2.4",
"@rolldown/binding-linux-x64-gnu": "1.2.4",
"@rolldown/binding-linux-x64-musl": "1.2.4",
"@rolldown/binding-openharmony-arm64": "1.2.4",
"@rolldown/binding-win32-arm64-msvc": "1.2.4",
"@rolldown/binding-win32-x64-msvc": "1.2.4"
}
},
"node_modules/router": {
@@ -3667,9 +3667,9 @@
}
},
"node_modules/type-is/node_modules/content-type": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz",
"integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz",
"integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
"license": "MIT",
"peer": true,
"engines": {
@@ -3739,16 +3739,16 @@
}
},
"node_modules/vite": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.2.0.tgz",
"integrity": "sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ==",
"version": "8.2.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz",
"integrity": "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==",
"dev": true,
"license": "MIT",
"dependencies": {
"lightningcss": "^1.33.0",
"picomatch": "^4.0.5",
"postcss": "^8.5.23",
"rolldown": "~1.2.0",
"postcss": "^8.5.25",
"rolldown": "~1.2.1",
"tinyglobby": "^0.2.17"
},
"bin": {

View File

@@ -3,7 +3,7 @@
"publishConfig": {
"access": "public"
},
"version": "0.66.0",
"version": "0.69.0",
"description": "An ACP-compatible coding agent powered by the Claude Agent SDK (TypeScript)",
"main": "dist/lib.js",
"types": "dist/lib.d.ts",
@@ -66,19 +66,19 @@
},
"dependencies": {
"@agentclientprotocol/sdk": "1.3.0",
"@anthropic-ai/claude-agent-sdk": "0.3.220",
"@anthropic-ai/claude-agent-sdk": "0.3.232",
"zod": "^3.25.0 || ^4.0.0"
},
"devDependencies": {
"@anthropic-ai/sdk": "0.115.0",
"@anthropic-ai/sdk": "0.117.1",
"@eslint/js": "10.0.1",
"@tsconfig/node22": "22.0.5",
"@types/node": "26.1.2",
"@typescript-eslint/eslint-plugin": "8.65.0",
"@typescript-eslint/parser": "8.65.0",
"eslint": "10.8.0",
"@types/node": "26.2.0",
"@typescript-eslint/eslint-plugin": "8.67.0",
"@typescript-eslint/parser": "8.67.0",
"eslint": "10.8.1",
"eslint-config-prettier": "10.1.8",
"globals": "17.9.0",
"globals": "17.11.0",
"prettier": "3.9.6",
"ts-node": "10.9.2",
"typescript": "6.0.3",

File diff suppressed because it is too large Load Diff

482
src/file-change-audit.ts Normal file
View File

@@ -0,0 +1,482 @@
import {
createSdkMcpServer,
type HookCallback,
type McpSdkServerConfigWithInstance,
type SdkMcpToolDefinition,
} from "@anthropic-ai/claude-agent-sdk";
import * as fs from "node:fs";
import * as path from "node:path";
import { z } from "zod";
export const AGENT_FILE_CHANGE_REPORT_CAPABILITY = "agentFileChangeReport";
export const FILE_CHANGE_AUDIT_SERVER_NAME = "claude_agent_acp";
export const FILE_CHANGE_AUDIT_TOOL_NAME = "report_changed_files";
export const FILE_CHANGE_AUDIT_WIRE_TOOL_NAME = `mcp__${FILE_CHANGE_AUDIT_SERVER_NAME}__${FILE_CHANGE_AUDIT_TOOL_NAME}`;
const FILE_CHANGE_AUDIT_MARKER = "claude-agent-acp-file-change-audit";
const MAX_REPORTED_PATHS = 1024;
const MAX_REPORTED_PATH_LENGTH = 4096;
export const AGENT_FILE_CHANGE_REPORT_MAX_BYTES = 256 * 1024;
const REQUEST_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
export type FileChangeAuditTurnState = {
requestId: string;
phase: "requested" | "collecting" | "finished";
};
export type FileChangeAuditWorkspace = {
cwd: string;
additionalDirectories: string[];
};
export type AgentFileChangeReport = {
paths: string[];
complete: boolean;
uncertainty?: string;
};
export type AgentFileChangeReportResult = {
version: 1;
requestId: string;
} & (
| {
status: "reported";
paths: string[];
declaredComplete: boolean;
truncated: boolean;
uncertainty?: string;
}
| {
status: "unavailable";
reason: FileChangeReportUnavailableReason;
}
);
export type FileChangeReportUnavailableReason =
"cancelled" | "timeout" | "invalidOutput" | "notReported" | "providerError";
const JETBRAINS_META_KEY = "jetbrains";
const AIR_META_KEY = "air";
const AIR_EXTENSION_VERSION = 1;
const AIR_EXTENSION_CAPABILITIES_KEY = "capabilities";
const AIR_AGENT_FILE_CHANGE_REPORT_KEY = "agentFileChangeReport";
type FileChangeAuditSupportOptions = {
cwd: string;
additionalDirectories: string[];
getActiveState: () => FileChangeAuditTurnState | undefined;
publish: (result: AgentFileChangeReportResult) => Promise<void>;
logError: (message: string) => void;
};
export type FileChangeAuditSupport = {
mcpServer: McpSdkServerConfigWithInstance;
preToolUseHook: HookCallback;
stopHook: HookCallback;
finishUnavailable: (
state: FileChangeAuditTurnState,
reason: FileChangeReportUnavailableReason,
) => Promise<void>;
};
const reportInputSchema = {
paths: z
.array(z.string())
.describe(
"Workspace file paths changed during this turn. Use absolute paths or paths relative to cwd.",
),
complete: z
.boolean()
.describe("True only when paths contains every workspace file changed during this turn."),
uncertainty: z
.string()
.trim()
.min(1)
.max(2000)
.optional()
.describe("Why the report may be incomplete or uncertain."),
};
export function agentFileChangeReportRequestId(meta: unknown): string | undefined {
const air = airExtension(meta);
const request = air?.agentFileChangeReportRequest;
if (!request || typeof request !== "object" || Array.isArray(request)) return undefined;
const requestRecord = request as Record<string, unknown>;
const requestKeys = Object.keys(requestRecord);
if (
requestKeys.length !== 2 ||
!requestKeys.includes("version") ||
!requestKeys.includes("requestId") ||
requestRecord.version !== 1
)
return undefined;
const requestId = requestRecord.requestId;
return typeof requestId === "string" && REQUEST_ID_PATTERN.test(requestId)
? requestId
: undefined;
}
export function supportsAgentFileChangeReport(capabilities: unknown): boolean {
if (!capabilities || typeof capabilities !== "object" || Array.isArray(capabilities)) {
return false;
}
const air = airExtension((capabilities as Record<string, unknown>)._meta);
const advertised = air?.[AIR_EXTENSION_CAPABILITIES_KEY];
return (
air?.version === AIR_EXTENSION_VERSION &&
Array.isArray(advertised) &&
advertised.includes(AGENT_FILE_CHANGE_REPORT_CAPABILITY)
);
}
export function agentFileChangeReportMeta(
result: AgentFileChangeReportResult,
): Record<string, unknown> {
return {
[JETBRAINS_META_KEY]: {
[AIR_META_KEY]: {
version: AIR_EXTENSION_VERSION,
[AIR_AGENT_FILE_CHANGE_REPORT_KEY]: result,
},
},
};
}
function airExtension(meta: unknown): Record<string, unknown> | undefined {
if (!meta || typeof meta !== "object" || Array.isArray(meta)) return undefined;
const jetbrains = (meta as Record<string, unknown>)[JETBRAINS_META_KEY];
if (!jetbrains || typeof jetbrains !== "object" || Array.isArray(jetbrains)) return undefined;
const air = (jetbrains as Record<string, unknown>)[AIR_META_KEY];
return air && typeof air === "object" && !Array.isArray(air)
? (air as Record<string, unknown>)
: undefined;
}
export function createFileChangeAuditTurnState(requestId: string): FileChangeAuditTurnState {
return {
requestId,
phase: "requested",
};
}
export function isFileChangeAuditReportPhase(state: FileChangeAuditTurnState | undefined): boolean {
return state?.phase === "collecting" || state?.phase === "finished";
}
export function isFileChangeAuditTool(toolName: string): boolean {
return toolName === FILE_CHANGE_AUDIT_WIRE_TOOL_NAME;
}
export function containsFileChangeAuditMarker(text: string): boolean {
return text.includes(`<${FILE_CHANGE_AUDIT_MARKER}>`);
}
export function createFileChangeAuditSupport(
options: FileChangeAuditSupportOptions,
): FileChangeAuditSupport {
const workspace = normalizeWorkspace(options.cwd, options.additionalDirectories);
const finishUnavailable = async (
state: FileChangeAuditTurnState,
reason: FileChangeReportUnavailableReason,
) => {
// Claim the terminal outcome before the first await. The report tool, a
// Stop hook, cancellation, and stream teardown can race, but only the
// winner may publish for this request id. Publication remains fail-open:
// an audit transport failure must never hold the user prompt open.
if (state.phase === "finished") return;
state.phase = "finished";
try {
await options.publish({
version: 1,
requestId: state.requestId,
status: "unavailable",
reason,
});
} catch (error) {
options.logError(`Failed to publish unavailable report ${state.requestId}: ${error}`);
}
};
const tool: SdkMcpToolDefinition<typeof reportInputSchema> = {
name: FILE_CHANGE_AUDIT_TOOL_NAME,
description:
"Internal audit tool. Call it only when a stop-hook message explicitly requests the file-change audit.",
inputSchema: reportInputSchema,
_meta: {
"anthropic/alwaysLoad": true,
"claude/endTurn": true,
},
handler: async (report) => {
const state = options.getActiveState();
if (!state || state.phase !== "collecting") {
return {
isError: true,
content: [
{
type: "text",
text: `<${FILE_CHANGE_AUDIT_MARKER}>No file-change audit is active.</${FILE_CHANGE_AUDIT_MARKER}>`,
},
],
};
}
// Flip before publishing: a client transport failure must not make the
// model retry the report and produce duplicates. Delivery is audit-only
// and fail-open; the normal turn must still finish.
state.phase = "finished";
const normalized = normalizeReportedPaths(report.paths, workspace);
const result = fitReportedAgentFileChangeReport({
version: 1,
requestId: state.requestId,
status: "reported",
paths: normalized.paths,
declaredComplete: report.complete && !normalized.truncated,
truncated: normalized.truncated,
...(report.uncertainty ? { uncertainty: report.uncertainty } : {}),
});
try {
await options.publish(result);
} catch (error) {
options.logError(`Failed to publish file-change report ${state.requestId}: ${error}`);
}
return {
content: [
{
type: "text",
text: `<${FILE_CHANGE_AUDIT_MARKER}>File-change audit recorded.</${FILE_CHANGE_AUDIT_MARKER}>`,
},
],
structuredContent: result,
};
},
};
// canUseTool is not guaranteed to run for tools that the current permission
// mode auto-allows (notably bypassPermissions). A PreToolUse hook is the
// enforcement boundary that keeps the hidden continuation read-only in every
// mode: it can submit the report, but cannot inspect or mutate more files.
const preToolUseHook: HookCallback = async (input) => {
if (input.hook_event_name !== "PreToolUse") return {};
const state = options.getActiveState();
const isReportTool = isFileChangeAuditTool(input.tool_name);
if (!isReportTool && !isFileChangeAuditReportPhase(state)) return {};
const allowReport = state?.phase === "collecting" && isReportTool;
return {
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: allowReport ? "allow" : "deny",
...(!allowReport
? {
permissionDecisionReason:
"Only the internal file-change report is allowed during the audit.",
}
: {}),
},
};
};
const stopHook: HookCallback = async (input) => {
if (input.hook_event_name !== "Stop") return {};
const state = options.getActiveState();
if (!state || state.phase === "finished") return {};
// A Stop can be only a pause while a background generator is still
// running. Keep the request pending so a later final Stop can include its
// writes instead of declaring a prematurely complete path set.
if (state.phase === "requested" && (input.background_tasks?.length ?? 0) > 0) return {};
if (state.phase === "collecting" || input.stop_hook_active) {
await finishUnavailable(state, "notReported");
return {};
}
// Set the phase before returning the continuation so every following SDK
// message is hidden even if the next stream starts immediately.
state.phase = "collecting";
return {
hookSpecificOutput: {
hookEventName: "Stop",
additionalContext:
`<${FILE_CHANGE_AUDIT_MARKER}>` +
`Before stopping, call ${FILE_CHANGE_AUDIT_WIRE_TOOL_NAME} exactly once. ` +
"Report every workspace file changed during this user turn, including files changed " +
"by commands, generators, version-control operations, and child processes. " +
"Do not call any other tool and do not emit user-facing prose. " +
"Set complete to false and explain uncertainty when you are not sure the list is complete." +
`</${FILE_CHANGE_AUDIT_MARKER}>`,
},
};
};
return {
mcpServer: createSdkMcpServer({
name: FILE_CHANGE_AUDIT_SERVER_NAME,
version: "1.0.0",
tools: [tool],
alwaysLoad: true,
}),
preToolUseHook,
stopHook,
finishUnavailable,
};
}
function normalizeWorkspace(
cwd: string,
additionalDirectories: string[],
): FileChangeAuditWorkspace {
const normalizedCwd = canonicalizeWorkspaceRoot(path.resolve(cwd));
const seen = new Set([pathKey(normalizedCwd)]);
const normalizedAdditionalDirectories: string[] = [];
for (const directory of additionalDirectories) {
const normalized = canonicalizeWorkspaceRoot(path.resolve(normalizedCwd, directory));
const key = pathKey(normalized);
if (seen.has(key)) continue;
seen.add(key);
normalizedAdditionalDirectories.push(normalized);
}
return {
cwd: normalizedCwd,
additionalDirectories: normalizedAdditionalDirectories,
};
}
function normalizeReportedPaths(
reportedPaths: string[],
workspace: FileChangeAuditWorkspace,
): { paths: string[]; truncated: boolean } {
const roots = [workspace.cwd, ...workspace.additionalDirectories];
const seen = new Set<string>();
const result: string[] = [];
let totalBytes = 0;
let truncated = false;
for (const reportedPath of reportedPaths) {
if (
reportedPath.trim().length === 0 ||
reportedPath.length > MAX_REPORTED_PATH_LENGTH ||
hasControlCharacter(reportedPath)
) {
truncated = true;
continue;
}
let normalized: string;
try {
normalized = canonicalizeReportedPath(path.resolve(workspace.cwd, reportedPath));
} catch {
truncated = true;
continue;
}
if (normalized.length > MAX_REPORTED_PATH_LENGTH || hasControlCharacter(normalized)) {
truncated = true;
continue;
}
if (!roots.some((root) => isWithinRoot(normalized, root))) {
truncated = true;
continue;
}
const key = pathKey(normalized);
if (roots.some((root) => pathKey(root) === key)) {
truncated = true;
continue;
}
if (seen.has(key)) {
continue;
}
const pathBytes = Buffer.byteLength(normalized, "utf8");
if (
result.length >= MAX_REPORTED_PATHS ||
totalBytes + pathBytes > AGENT_FILE_CHANGE_REPORT_MAX_BYTES
) {
truncated = true;
continue;
}
seen.add(key);
result.push(normalized);
totalBytes += pathBytes;
}
return { paths: result, truncated };
}
/** Keep the complete serialized report within AIR's wire limit. */
function fitReportedAgentFileChangeReport(
report: Extract<AgentFileChangeReportResult, { status: "reported" }>,
): Extract<AgentFileChangeReportResult, { status: "reported" }> {
const paths = [...report.paths];
let fitted = report;
while (Buffer.byteLength(JSON.stringify(fitted), "utf8") > AGENT_FILE_CHANGE_REPORT_MAX_BYTES) {
if (paths.length === 0) {
// The fixed fields and bounded uncertainty are far below the cap. Keep
// this loud if the wire contract changes without updating this helper.
throw new Error("Agent file-change report exceeds the wire limit without paths");
}
paths.pop();
fitted = {
...report,
paths: [...paths],
declaredComplete: false,
truncated: true,
};
}
return fitted;
}
function isWithinRoot(candidate: string, root: string): boolean {
const relative = path.relative(root, candidate);
return !path.isAbsolute(relative) && !relative.startsWith(`..${path.sep}`) && relative !== "..";
}
/**
* Resolve filesystem aliases in a workspace root, including macOS' /tmp ->
* /private/tmp alias. For a root that does not exist yet, resolve its nearest
* existing ancestor and retain the missing suffix.
*/
function canonicalizeWorkspaceRoot(value: string): string {
return canonicalizeFromExistingAncestor(value);
}
/**
* Canonicalize the parent but not the leaf itself. A changed path may be
* deleted, or it may be a symlink whose node (rather than target) changed.
*/
function canonicalizeReportedPath(value: string): string {
const parent = canonicalizeFromExistingAncestor(path.dirname(value));
return path.resolve(parent, path.basename(value));
}
function canonicalizeFromExistingAncestor(value: string): string {
const original = path.resolve(value);
let current = original;
const missingSegments: string[] = [];
while (true) {
try {
const canonical = fs.realpathSync.native(current);
return path.resolve(canonical, ...missingSegments.reverse());
} catch (error) {
if (!isMissingPathError(error)) return original;
}
const parent = path.dirname(current);
if (parent === current) return original;
missingSegments.push(path.basename(current));
current = parent;
}
}
function isMissingPathError(error: unknown): boolean {
if (typeof error !== "object" || error === null || !("code" in error)) return false;
return error.code === "ENOENT" || error.code === "ENOTDIR";
}
function pathKey(value: string): string {
return process.platform === "win32" ? value.toLowerCase() : value;
}
function hasControlCharacter(value: string): boolean {
for (let index = 0; index < value.length; index++) {
const code = value.charCodeAt(index);
if (code <= 0x1f || (code >= 0x7f && code <= 0x9f)) return true;
}
return false;
}

View File

@@ -0,0 +1,463 @@
import type { ClientCapabilities, SessionNotification } from "@agentclientprotocol/sdk";
import {
getSessionMessages,
type SDKAssistantMessageError,
USAGE_LIMIT_ERROR_PREFIXES,
} from "@anthropic-ai/claude-agent-sdk";
import { randomUUID } from "node:crypto";
const JETBRAINS_META_KEY = "jetbrains";
const AIR_META_KEY = "air";
const AIR_EXTENSION_VERSION_KEY = "version";
const AIR_EXTENSION_CAPABILITIES_KEY = "capabilities";
const AIR_SESSION_FAILURE_KEY = "sessionFailure";
const AIR_EXTENSION_VERSION = 1;
export function airSessionFailureCapabilityMeta(...additionalCapabilities: string[]) {
return {
[JETBRAINS_META_KEY]: {
[AIR_META_KEY]: {
[AIR_EXTENSION_VERSION_KEY]: AIR_EXTENSION_VERSION,
[AIR_EXTENSION_CAPABILITIES_KEY]: [AIR_SESSION_FAILURE_KEY, ...additionalCapabilities],
},
},
};
}
export type ClaudeFailureKind =
| "advisory"
| "auth_required"
| "bad_request"
| "budget_exhausted"
| "context_exhausted"
| "internal_error"
| "overloaded"
| "provider_error"
| "quota_exhausted"
| "rate_limited"
| "transport_lost"
| "worker_shutdown";
type AirSessionFailureCategory =
"connection" | "access" | "limit" | "request" | "service" | "unknown";
type AirSessionFailureSeverity = "warning" | "error";
type AirSessionFailureAction = "retry" | "login" | "new_session";
type SessionFailureRecoveryPolicy =
"never" | "next_attempt" | "real_model_success" | "auth_status" | "runtime_rebind";
export type PublishedSessionFailure = {
id: string;
revision: number;
kind: ClaudeFailureKind;
category: AirSessionFailureCategory;
turnId?: string;
severity: AirSessionFailureSeverity;
title: string;
details?: string;
actions: AirSessionFailureAction[];
recoveryPolicy: SessionFailureRecoveryPolicy;
};
type SessionFailureOptions = {
turnId?: string;
sessionScoped?: boolean;
title?: string;
details?: string;
severity?: AirSessionFailureSeverity;
};
export type SessionFailureState = {
epoch: string;
revisions: Map<string, number>;
active: Map<string, PublishedSessionFailure>;
nextIncident?: number;
lastNotice?: { title: string; id: string };
};
type Logger = {
error: (...args: unknown[]) => void;
};
export function createSessionFailureState(): SessionFailureState {
return {
epoch: randomUUID(),
revisions: new Map(),
active: new Map(),
};
}
const AIR_FAILURE_POLICY: Record<
ClaudeFailureKind,
{
category: AirSessionFailureCategory;
actions: AirSessionFailureAction[];
fallbackTitle: string;
}
> = {
advisory: {
category: "unknown",
actions: [],
fallbackTitle: "Claude reported a notice.",
},
auth_required: {
category: "access",
actions: ["login"],
fallbackTitle: "Sign in to continue using Claude.",
},
bad_request: {
category: "request",
actions: [],
fallbackTitle: "Claude could not process this request.",
},
budget_exhausted: {
category: "limit",
actions: ["new_session"],
fallbackTitle: "This Claude session reached its configured budget.",
},
context_exhausted: {
category: "limit",
actions: ["new_session"],
fallbackTitle: "This Claude turn reached its configured limit.",
},
internal_error: {
category: "service",
actions: ["retry", "new_session"],
fallbackTitle: "Claude Agent encountered an internal error.",
},
overloaded: {
category: "service",
actions: ["retry"],
fallbackTitle: "Claude is temporarily overloaded.",
},
provider_error: {
category: "service",
actions: ["retry"],
fallbackTitle: "The model provider reported an error.",
},
quota_exhausted: {
category: "limit",
actions: [],
fallbackTitle: "The Claude account has no available quota.",
},
rate_limited: {
category: "limit",
actions: ["retry"],
fallbackTitle: "Claude is temporarily rate limited.",
},
transport_lost: {
category: "connection",
actions: ["new_session"],
fallbackTitle: "The connection to Claude was lost.",
},
worker_shutdown: {
category: "connection",
actions: ["new_session"],
fallbackTitle: "The Claude worker is shutting down.",
},
};
export function sessionFailureMeta(failure: PublishedSessionFailure) {
return {
[JETBRAINS_META_KEY]: {
[AIR_META_KEY]: {
[AIR_EXTENSION_VERSION_KEY]: AIR_EXTENSION_VERSION,
[AIR_SESSION_FAILURE_KEY]: {
id: failure.id,
revision: failure.revision,
category: failure.category,
severity: failure.severity,
title: failure.title,
...(failure.details ? { details: failure.details } : {}),
actions: failure.actions,
},
},
},
};
}
/** `getSessionMessages` deliberately exposes only the API message and strips
* transcript-level `error` / `isApiErrorMessage` fields. The SDK exports the
* exact stable prefixes used by its synthetic usage-limit errors, so replay
* can recover this one typed failure without matching arbitrary model prose. */
export function assistantMessageText(apiMessage: unknown): string | undefined {
if (!apiMessage || typeof apiMessage !== "object") return undefined;
const { content } = apiMessage as { content?: unknown };
if (typeof content === "string") return content || undefined;
if (!Array.isArray(content)) return undefined;
const text = content
.map((block) =>
block &&
typeof block === "object" &&
"type" in block &&
block.type === "text" &&
"text" in block &&
typeof block.text === "string"
? block.text
: "",
)
.join("");
return text || undefined;
}
export function isSyntheticUsageLimitMessage(apiMessage: unknown): boolean {
if (!apiMessage || typeof apiMessage !== "object") return false;
const { model } = apiMessage as { model?: unknown };
if (model !== "<synthetic>") return false;
const text = assistantMessageText(apiMessage);
if (!text) return false;
return USAGE_LIMIT_ERROR_PREFIXES.some((prefix) => text.startsWith(prefix));
}
export function activeUsageLimitMessage(
messages: Awaited<ReturnType<typeof getSessionMessages>>,
): { uuid: string; title: string } | undefined {
let active: { uuid: string; title: string } | undefined;
for (const message of messages) {
if (message.type !== "assistant" || message.parent_tool_use_id !== null) continue;
if (isSyntheticUsageLimitMessage(message.message)) {
const title = assistantMessageText(message.message);
if (title) active = { uuid: message.uuid, title };
continue;
}
const model =
message.message && typeof message.message === "object" && "model" in message.message
? message.message.model
: undefined;
// A later real model answer proves the account recovered. Synthetic
// local-command/interruption messages do not: they can be appended while
// the provider remains unavailable.
if (model !== undefined && model !== "<synthetic>") active = undefined;
}
return active;
}
export function supportsAirSessionFailures(capabilities?: ClientCapabilities): boolean {
const jetbrains = capabilities?._meta?.[JETBRAINS_META_KEY] as
Record<string, unknown> | undefined;
const air = jetbrains?.[AIR_META_KEY] as Record<string, unknown> | undefined;
const version = air?.[AIR_EXTENSION_VERSION_KEY];
const advertised = air?.[AIR_EXTENSION_CAPABILITIES_KEY];
return (
typeof version === "number" &&
Number.isFinite(version) &&
Number.isInteger(version) &&
version >= AIR_EXTENSION_VERSION &&
Array.isArray(advertised) &&
advertised.some((capability) => capability === AIR_SESSION_FAILURE_KEY)
);
}
function sessionFailureRecoveryPolicy(
kind: ClaudeFailureKind,
turnId?: string,
): SessionFailureRecoveryPolicy {
switch (kind) {
case "advisory":
return "never";
case "auth_required":
return "auth_status";
case "transport_lost":
case "worker_shutdown":
return "runtime_rebind";
case "quota_exhausted":
return "real_model_success";
default:
return turnId ? "next_attempt" : "real_model_success";
}
}
/** Owns the AIR failure lifecycle for both the live consumer and history replay.
* Published revisions become active only after delivery succeeds; recovery only
* removes internal active state because transcript records are historical. */
export class SessionFailureController {
private readonly sessionId: string;
private readonly state: SessionFailureState;
private readonly capabilities?: ClientCapabilities;
private readonly isCurrent: () => boolean;
private readonly sendUpdate: (notification: SessionNotification) => Promise<void>;
private readonly logger: Logger;
constructor(options: {
sessionId: string;
state: SessionFailureState;
capabilities?: ClientCapabilities;
isCurrent: () => boolean;
sendUpdate: (notification: SessionNotification) => Promise<void>;
logger: Logger;
}) {
this.sessionId = options.sessionId;
this.state = options.state;
this.capabilities = options.capabilities;
this.isCurrent = options.isCurrent;
this.sendUpdate = options.sendUpdate;
this.logger = options.logger;
}
private isSupported(): boolean {
return supportsAirSessionFailures(this.capabilities);
}
private async emit(failure: PublishedSessionFailure): Promise<boolean> {
if (!this.isCurrent()) {
this.logger.error(
`Session ${this.sessionId}: ignored AIR session failure from a stale consumer`,
);
return false;
}
try {
await this.sendUpdate({
sessionId: this.sessionId,
update: {
sessionUpdate: "session_info_update",
_meta: sessionFailureMeta(failure),
},
});
return true;
} catch (error) {
this.logger.error(
`Session ${this.sessionId}: failed to publish AIR session failure: ${error}`,
);
return false;
}
}
recordActive(failure: PublishedSessionFailure): void {
this.state.revisions.set(failure.id, failure.revision);
this.state.active.set(failure.id, failure);
this.state.lastNotice =
failure.kind === "advisory" ? { title: failure.title, id: failure.id } : undefined;
}
async clear(
shouldClear: (failure: PublishedSessionFailure) => boolean = () => true,
): Promise<boolean> {
if (!this.isSupported()) return true;
this.state.lastNotice = undefined;
for (const failure of [...this.state.active.values()]) {
if (!shouldClear(failure)) continue;
this.state.active.delete(failure.id);
}
return true;
}
async prepare(
kind: ClaudeFailureKind,
failureOptions: SessionFailureOptions = {},
): Promise<PublishedSessionFailure | undefined> {
if (!this.isSupported() || !this.isCurrent()) return undefined;
const policy = AIR_FAILURE_POLICY[kind];
const title = failureOptions.title || policy.fallbackTitle;
if (kind === "advisory") {
const id =
this.state.lastNotice?.title === title
? this.state.lastNotice.id
: `${this.sessionId}:notice:${this.state.epoch}:${this.state.nextIncident ?? 1}`;
if (this.state.lastNotice?.title !== title) {
this.state.nextIncident = (this.state.nextIncident ?? 1) + 1;
}
return {
id,
revision: (this.state.revisions.get(id) ?? 0) + 1,
kind,
category: policy.category,
severity: "warning",
title,
...(failureOptions.details ? { details: failureOptions.details } : {}),
actions: policy.actions,
recoveryPolicy: sessionFailureRecoveryPolicy(kind),
};
}
const id =
failureOptions.turnId && !failureOptions.sessionScoped
? `${failureOptions.turnId}:error`
: `${this.sessionId}:session-error:${this.state.epoch}:${this.state.nextIncident ?? 1}`;
if (!failureOptions.turnId || failureOptions.sessionScoped) {
this.state.nextIncident = (this.state.nextIncident ?? 1) + 1;
}
return {
id,
revision: (this.state.revisions.get(id) ?? 0) + 1,
kind,
category: policy.category,
severity: failureOptions.severity ?? "error",
title,
...(failureOptions.details ? { details: failureOptions.details } : {}),
actions: failureOptions.severity === "warning" ? [] : policy.actions,
recoveryPolicy: sessionFailureRecoveryPolicy(
kind,
failureOptions.turnId && !failureOptions.sessionScoped ? failureOptions.turnId : undefined,
),
...(failureOptions.turnId && !failureOptions.sessionScoped
? { turnId: failureOptions.turnId }
: {}),
};
}
async publish(
kind: ClaudeFailureKind,
failureOptions: SessionFailureOptions = {},
): Promise<void> {
const failure = await this.prepare(kind, failureOptions);
if (!failure) return;
if (await this.emit(failure)) this.recordActive(failure);
}
async restore(
id: string,
kind: ClaudeFailureKind,
title: string,
active = true,
): Promise<boolean> {
if (!this.isSupported() || !this.isCurrent()) return false;
const policy = AIR_FAILURE_POLICY[kind];
const failure: PublishedSessionFailure = {
id,
revision: (this.state.revisions.get(id) ?? 0) + 1,
kind,
category: policy.category,
severity: "error",
title,
actions: policy.actions,
recoveryPolicy: sessionFailureRecoveryPolicy(kind),
};
if (!(await this.emit(failure))) return false;
this.state.revisions.set(failure.id, failure.revision);
if (active) {
this.state.active.set(failure.id, failure);
} else {
this.state.active.delete(failure.id);
}
return true;
}
}
export function providerFailureCategory(
errorKind?: SDKAssistantMessageError,
isUsageLimit = false,
): ClaudeFailureKind {
if (isUsageLimit) return "quota_exhausted";
switch (errorKind) {
case "authentication_failed":
case "oauth_org_not_allowed":
return "auth_required";
case "billing_error":
return "quota_exhausted";
case "rate_limit":
return "rate_limited";
case "overloaded":
return "overloaded";
case "invalid_request":
case "model_not_found":
return "bad_request";
case "max_output_tokens":
return "context_exhausted";
case "server_error":
case "unknown":
case undefined:
return "provider_error";
default:
return "provider_error";
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -795,7 +795,7 @@ describe("createSession options merging", () => {
return { onUserDialog: capturedOptions!.onUserDialog!, createElicitation };
}
const signal = () => ({ signal: new AbortController().signal });
const signal = () => ({ signal: new AbortController().signal, requestId: "1" });
it("renders the prompt as a form elicitation and maps the retry choice", async () => {
const { onUserDialog, createElicitation } = await setupDialog();

View File

@@ -0,0 +1,604 @@
import type {
HookCallback,
Options,
PermissionResult,
SDKUserMessage,
} from "@anthropic-ai/claude-agent-sdk";
import type { SessionNotification } from "@agentclientprotocol/sdk";
import * as path from "node:path";
import { randomUUID } from "node:crypto";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { AcpClient, ClaudeAcpAgent as ClaudeAcpAgentType } from "../acp-agent.js";
import {
AGENT_FILE_CHANGE_REPORT_CAPABILITY,
FILE_CHANGE_AUDIT_SERVER_NAME,
FILE_CHANGE_AUDIT_TOOL_NAME,
FILE_CHANGE_AUDIT_WIRE_TOOL_NAME,
type AgentFileChangeReport,
type AgentFileChangeReportResult,
} from "../file-change-audit.js";
import type { Pushable } from "../utils.js";
let observedStopOutputs: unknown[] = [];
let observedPreToolDecision: unknown;
let observedCanUseToolDecision: PermissionResult | null | undefined;
let replayMessages: unknown[] = [];
type AuditScenario =
"reported" | "successWithoutReport" | "providerError" | "localOnly" | "waitForCancel";
let auditScenario: AuditScenario = "reported";
let auditTurnActivated = Promise.resolve();
let resolveAuditTurnActivated = () => {};
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",
);
return {
...actual,
getSessionMessages: vi.fn(async () => replayMessages),
query: ({ prompt, options }: { prompt: Pushable<SDKUserMessage>; options: Options }) => {
let resolveInterrupt = () => {};
const interrupted = new Promise<void>((resolve) => {
resolveInterrupt = resolve;
});
return Object.assign(runAuditedTurn(prompt, options, interrupted), {
initializationResult: async () => ({
models: [
{
value: "claude-sonnet-4-6",
displayName: "Claude Sonnet",
description: "Fast",
supportsAutoMode: true,
},
],
}),
setModel: vi.fn(async () => {}),
setPermissionMode: vi.fn(async () => {}),
supportedAgents: vi.fn(async () => []),
supportedCommands: vi.fn(async () => []),
getContextUsage: vi.fn(async () => ({ totalTokens: 0, rawMaxTokens: 200000 })),
interrupt: vi.fn(async () => {
resolveInterrupt();
return undefined;
}),
close: vi.fn(),
});
},
};
});
vi.mock("../tools.js", async () => ({
...(await vi.importActual<typeof import("../tools.js")>("../tools.js")),
registerHookCallback: vi.fn(),
}));
type RegisteredAuditTool = {
handler: (
report: AgentFileChangeReport,
) => Promise<{ content: unknown; structuredContent?: AgentFileChangeReportResult }>;
};
function hookFrom(options: Options, event: "PreToolUse" | "Stop"): HookCallback {
const matcher = options.hooks?.[event]?.at(-1);
const hook = matcher?.hooks.at(-1);
if (!hook) throw new Error(`Missing ${event} audit hook`);
return hook;
}
function auditToolFrom(options: Options): RegisteredAuditTool {
const server = options.mcpServers?.[FILE_CHANGE_AUDIT_SERVER_NAME] as unknown as {
instance: { _registeredTools: Record<string, RegisteredAuditTool> };
};
const tool = server.instance._registeredTools[FILE_CHANGE_AUDIT_TOOL_NAME];
if (!tool) throw new Error("Missing internal audit tool");
return tool;
}
function assistantMessage(content: unknown[]) {
return {
type: "assistant" as const,
parent_tool_use_id: null,
uuid: randomUUID(),
session_id: "sdk-session",
message: {
role: "assistant" as const,
model: "claude-sonnet-4-6",
stop_reason: "end_turn",
usage: {
input_tokens: 1,
output_tokens: 1,
cache_read_input_tokens: 0,
cache_creation_input_tokens: 0,
},
content,
},
};
}
function successResult() {
return {
type: "result" as const,
subtype: "success" as const,
stop_reason: "end_turn",
is_error: false,
result: "",
errors: [],
duration_ms: 0,
duration_api_ms: 0,
num_turns: 2,
total_cost_usd: 0,
usage: {
input_tokens: 2,
output_tokens: 2,
cache_read_input_tokens: 0,
cache_creation_input_tokens: 0,
},
modelUsage: {},
permission_denials: [],
uuid: randomUUID(),
session_id: "sdk-session",
};
}
async function* runAuditedTurn(
input: Pushable<SDKUserMessage>,
options: Options,
interrupted: Promise<void>,
) {
const iterator = input[Symbol.asyncIterator]();
const { value: userMessage } = await iterator.next();
if (!userMessage) return;
if (auditScenario === "localOnly") {
yield { ...successResult(), result: "Local command output" };
return;
}
yield {
type: "user" as const,
message: userMessage.message,
parent_tool_use_id: null,
uuid: userMessage.uuid,
session_id: "sdk-session",
isReplay: true,
};
resolveAuditTurnActivated();
if (auditScenario === "successWithoutReport") {
yield assistantMessage([{ type: "text", text: "Visible answer" }]);
yield successResult();
return;
}
if (auditScenario === "providerError") {
yield {
...successResult(),
is_error: true,
result: "Provider failed",
errors: ["Provider failed"],
};
return;
}
if (auditScenario === "waitForCancel") {
await interrupted;
yield {
type: "system" as const,
subtype: "session_state_changed" as const,
state: "idle" as const,
uuid: randomUUID(),
session_id: "sdk-session",
};
return;
}
yield assistantMessage([{ type: "text", text: "Visible answer" }]);
const hookOptions = { signal: new AbortController().signal };
const stopHook = hookFrom(options, "Stop");
observedStopOutputs.push(
await stopHook(
{ hook_event_name: "Stop", stop_hook_active: false } as Parameters<HookCallback>[0],
undefined,
hookOptions,
),
);
const preToolUseHook = hookFrom(options, "PreToolUse");
observedPreToolDecision = await preToolUseHook(
{
hook_event_name: "PreToolUse",
tool_name: "Bash",
tool_input: { command: "git status" },
tool_use_id: "blocked-tool",
} as Parameters<HookCallback>[0],
"blocked-tool",
hookOptions,
);
observedCanUseToolDecision = await options.canUseTool?.(
"Bash",
{ command: "git status" },
{
signal: hookOptions.signal,
toolUseID: "blocked-tool",
requestId: "permission-request",
},
);
const toolUseId = "audit-tool-use";
yield assistantMessage([
{ type: "text", text: "Audit prose must stay hidden" },
{
type: "tool_use",
id: toolUseId,
name: FILE_CHANGE_AUDIT_WIRE_TOOL_NAME,
input: { paths: ["src/changed.ts"], complete: true },
},
]);
const toolResult = await auditToolFrom(options).handler({
paths: ["src/changed.ts"],
complete: true,
});
yield {
type: "user" as const,
message: {
role: "user" as const,
content: [
{
type: "tool_result",
tool_use_id: toolUseId,
content: toolResult.content,
},
],
},
parent_tool_use_id: null,
uuid: randomUUID(),
session_id: "sdk-session",
};
observedStopOutputs.push(
await stopHook(
{ hook_event_name: "Stop", stop_hook_active: true } as Parameters<HookCallback>[0],
undefined,
hookOptions,
),
);
yield successResult();
}
describe("agent file-change audit integration", () => {
beforeEach(() => {
observedStopOutputs = [];
observedPreToolDecision = undefined;
observedCanUseToolDecision = undefined;
replayMessages = [];
auditScenario = "reported";
auditTurnActivated = new Promise<void>((resolve) => {
resolveAuditTurnActivated = resolve;
});
});
const negotiatedCapabilities = {
_meta: {
jetbrains: {
air: {
version: 1,
capabilities: [AGENT_FILE_CHANGE_REPORT_CAPABILITY, "sessionFailure"],
},
},
},
};
async function createAuditAgent(updates: SessionNotification[]) {
const client = {
sessionUpdate: async (notification: SessionNotification) => void updates.push(notification),
requestPermission: async () => ({ outcome: { outcome: "cancelled" } }),
readTextFile: async () => ({ content: "" }),
writeTextFile: async () => ({}),
} as unknown as AcpClient;
const { ClaudeAcpAgent } = await import("../acp-agent.js");
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(client, {
log: () => {},
error: () => {},
});
await agent.initialize({
protocolVersion: 1,
clientCapabilities: negotiatedCapabilities,
});
const { sessionId } = await agent.newSession({ cwd: process.cwd(), mcpServers: [] });
return { agent, sessionId };
}
function fileChangeTerminals(updates: SessionNotification[]): AgentFileChangeReportResult[] {
return updates.flatMap((notification) => {
if (notification.update.sessionUpdate !== "session_info_update") return [];
const meta = notification.update._meta as
| {
jetbrains?: {
air?: { agentFileChangeReport?: AgentFileChangeReportResult };
};
}
| undefined;
const report = meta?.jetbrains?.air?.agentFileChangeReport;
return report ? [report] : [];
});
}
function auditedPrompt(sessionId: string, requestId: string, text: string) {
return {
sessionId,
prompt: [{ type: "text" as const, text }],
_meta: {
jetbrains: {
air: {
agentFileChangeReportRequest: { version: 1, requestId },
},
},
},
};
}
it.each([
{
name: "normal success without a Stop report",
scenario: "successWithoutReport" as const,
prompt: "Finish normally",
reason: "notReported" as const,
rejects: false,
},
{
name: "a local-only command",
scenario: "localOnly" as const,
prompt: "/context",
reason: "notReported" as const,
rejects: false,
},
{
name: "a provider error",
scenario: "providerError" as const,
prompt: "Call the provider",
reason: "providerError" as const,
rejects: false,
},
])("publishes one terminal for $name", async ({ scenario, prompt, reason, rejects }) => {
auditScenario = scenario;
const updates: SessionNotification[] = [];
const { agent, sessionId } = await createAuditAgent(updates);
const requestId = `request-${scenario}`;
const result = agent.prompt(auditedPrompt(sessionId, requestId, prompt));
if (rejects) {
await expect(result).rejects.toBeDefined();
} else {
await expect(result).resolves.toBeDefined();
}
expect(fileChangeTerminals(updates)).toEqual([
{
version: 1,
requestId,
status: "unavailable",
reason,
},
]);
await agent.dispose();
expect(fileChangeTerminals(updates)).toHaveLength(1);
});
it.each([
{ name: "cancel", close: false },
{ name: "session close", close: true },
])("publishes one cancelled terminal and settles the prompt on $name", async ({ close }) => {
auditScenario = "waitForCancel";
const updates: SessionNotification[] = [];
const { agent, sessionId } = await createAuditAgent(updates);
const requestId = close ? "request-close" : "request-cancel";
const result = agent.prompt(auditedPrompt(sessionId, requestId, "Wait"));
await auditTurnActivated;
if (close) {
await agent.closeSession({ sessionId });
} else {
await agent.cancel({ sessionId });
}
await expect(result).resolves.toMatchObject({ stopReason: "cancelled" });
expect(fileChangeTerminals(updates)).toEqual([
{
version: 1,
requestId,
status: "unavailable",
reason: "cancelled",
},
]);
await agent.dispose();
expect(fileChangeTerminals(updates)).toHaveLength(1);
});
it("negotiates an opt-in turn, publishes one report, and hides the audit continuation", async () => {
const updates: SessionNotification[] = [];
const client = {
sessionUpdate: async (notification: SessionNotification) => void updates.push(notification),
requestPermission: async () => ({ outcome: { outcome: "cancelled" } }),
readTextFile: async () => ({ content: "" }),
writeTextFile: async () => ({}),
} as unknown as AcpClient;
const { ClaudeAcpAgent } = await import("../acp-agent.js");
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(client, {
log: () => {},
error: () => {},
});
const capabilities = {
_meta: {
jetbrains: {
air: {
version: 1,
capabilities: [AGENT_FILE_CHANGE_REPORT_CAPABILITY],
},
},
},
};
const initialize = await agent.initialize({
protocolVersion: 1,
clientCapabilities: capabilities,
});
expect(initialize._meta).toMatchObject({
jetbrains: {
air: {
version: 1,
capabilities: expect.arrayContaining([AGENT_FILE_CHANGE_REPORT_CAPABILITY]),
},
},
});
const cwd = process.cwd();
const { sessionId } = await agent.newSession({ cwd, mcpServers: [] });
const response = await agent.prompt({
sessionId,
prompt: [{ type: "text", text: "Change the file" }],
_meta: {
jetbrains: {
air: {
agentFileChangeReportRequest: { version: 1, requestId: "request-integration" },
},
},
},
});
expect(response.stopReason).toBe("end_turn");
expect(observedStopOutputs).toHaveLength(2);
expect(observedStopOutputs[0]).toMatchObject({
hookSpecificOutput: {
hookEventName: "Stop",
additionalContext: expect.stringContaining(FILE_CHANGE_AUDIT_WIRE_TOOL_NAME),
},
});
expect(observedStopOutputs[1]).toEqual({});
expect(observedPreToolDecision).toMatchObject({
hookSpecificOutput: { permissionDecision: "deny" },
});
expect(observedCanUseToolDecision).toMatchObject({ behavior: "deny" });
const reportUpdates = updates.filter((notification) => {
if (notification.update.sessionUpdate !== "session_info_update") return false;
const meta = notification.update._meta as
{ jetbrains?: { air?: { agentFileChangeReport?: unknown } } } | undefined;
return meta?.jetbrains?.air?.agentFileChangeReport !== undefined;
});
expect(reportUpdates).toEqual([
{
sessionId,
update: {
sessionUpdate: "session_info_update",
_meta: {
jetbrains: {
air: {
version: 1,
agentFileChangeReport: {
version: 1,
requestId: "request-integration",
status: "reported",
paths: [path.join(cwd, "src/changed.ts")],
declaredComplete: true,
truncated: false,
},
},
},
},
},
},
]);
const serializedUpdates = JSON.stringify(updates);
expect(serializedUpdates).toContain("Visible answer");
expect(serializedUpdates).not.toContain("Audit prose must stay hidden");
expect(serializedUpdates).not.toContain(FILE_CHANGE_AUDIT_WIRE_TOOL_NAME);
await agent.dispose();
const firstStop = observedStopOutputs[0] as {
hookSpecificOutput?: { additionalContext?: string };
};
const auditContext = firstStop.hookSpecificOutput?.additionalContext;
if (!auditContext) throw new Error("Missing audit continuation context");
const replayToolUseId = "replay-audit-tool";
const deniedReplayToolUseId = "replay-denied-tool";
const replayUserMessage = (content: unknown) => ({
type: "user" as const,
message: { role: "user" as const, content },
parent_tool_use_id: null,
uuid: randomUUID(),
session_id: "replay-session",
});
replayMessages = [
replayUserMessage("Original prompt"),
assistantMessage([{ type: "text", text: "Visible persisted answer" }]),
replayUserMessage(auditContext),
assistantMessage([{ type: "text", text: "Separate audit prose must stay hidden" }]),
assistantMessage([
{
type: "tool_use",
id: deniedReplayToolUseId,
name: "Bash",
input: { command: "git status" },
},
]),
replayUserMessage([
{
type: "tool_result",
tool_use_id: deniedReplayToolUseId,
content: "denied",
is_error: true,
},
]),
assistantMessage([{ type: "text", text: "Prose after denial must stay hidden" }]),
assistantMessage([
{
type: "tool_use",
id: replayToolUseId,
name: FILE_CHANGE_AUDIT_WIRE_TOOL_NAME,
input: { paths: ["src/changed.ts"], complete: true },
},
]),
replayUserMessage([
{
type: "tool_result",
tool_use_id: replayToolUseId,
content: "recorded",
},
]),
replayUserMessage("Follow-up prompt"),
assistantMessage([{ type: "text", text: "Follow-up answer" }]),
];
const replayUpdates: SessionNotification[] = [];
const replayAgent = new ClaudeAcpAgent(
{
...client,
sessionUpdate: async (notification: SessionNotification) =>
void replayUpdates.push(notification),
} as AcpClient,
{ log: () => {}, error: () => {} },
);
await replayAgent.initialize({
protocolVersion: 1,
clientCapabilities: capabilities,
});
await replayAgent.loadSession({
sessionId: "replay-session",
cwd,
mcpServers: [],
});
const serializedReplay = JSON.stringify(replayUpdates);
expect(serializedReplay).toContain("Original prompt");
expect(serializedReplay).toContain("Visible persisted answer");
expect(serializedReplay).toContain("Follow-up prompt");
expect(serializedReplay).toContain("Follow-up answer");
expect(serializedReplay).not.toContain("Separate audit prose must stay hidden");
expect(serializedReplay).not.toContain("Prose after denial must stay hidden");
expect(serializedReplay).not.toContain(FILE_CHANGE_AUDIT_WIRE_TOOL_NAME);
expect(serializedReplay).not.toContain("claude-agent-acp-file-change-audit");
await replayAgent.dispose();
});
});

View File

@@ -0,0 +1,367 @@
import type { HookCallback } from "@anthropic-ai/claude-agent-sdk";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { describe, expect, it, vi } from "vitest";
import {
AGENT_FILE_CHANGE_REPORT_MAX_BYTES,
agentFileChangeReportRequestId,
createFileChangeAuditSupport,
createFileChangeAuditTurnState,
FILE_CHANGE_AUDIT_TOOL_NAME,
FILE_CHANGE_AUDIT_WIRE_TOOL_NAME,
type AgentFileChangeReport,
type AgentFileChangeReportResult,
type FileChangeAuditTurnState,
} from "../file-change-audit.js";
type RegisteredAuditTool = {
_meta?: Record<string, unknown>;
handler: (report: AgentFileChangeReport) => Promise<{
isError?: boolean;
structuredContent?: AgentFileChangeReportResult;
}>;
};
function auditToolHandler(
support: ReturnType<typeof createFileChangeAuditSupport>,
): RegisteredAuditTool {
const server = support.mcpServer as unknown as {
instance: { _registeredTools: Record<string, RegisteredAuditTool> };
};
return server.instance._registeredTools[FILE_CHANGE_AUDIT_TOOL_NAME];
}
function stopInput(stopHookActive = false): Parameters<HookCallback>[0] {
return {
hook_event_name: "Stop",
stop_hook_active: stopHookActive,
} as Parameters<HookCallback>[0];
}
function createSupport(options: {
state: FileChangeAuditTurnState;
cwd?: string;
additionalDirectories?: string[];
publish?: (result: AgentFileChangeReportResult) => Promise<void>;
logError?: (message: string) => void;
}) {
return createFileChangeAuditSupport({
cwd: options.cwd ?? process.cwd(),
additionalDirectories: options.additionalDirectories ?? [],
getActiveState: () => options.state,
publish: options.publish ?? (async () => {}),
logError: options.logError ?? (() => {}),
});
}
describe("agent file-change audit", () => {
it("accepts only the versioned prompt request shape", () => {
expect(
agentFileChangeReportRequestId({
jetbrains: {
air: {
agentFileChangeReportRequest: { version: 1, requestId: "turn:42_a-b.c" },
},
},
}),
).toBe("turn:42_a-b.c");
for (const request of [
{ version: 2, requestId: "turn-42" },
{ version: 1, requestId: "contains spaces" },
{ version: 1, requestId: "x".repeat(129) },
{ version: 1 },
{ version: 1, requestId: "turn-42", extra: true },
null,
]) {
expect(
agentFileChangeReportRequestId({
jetbrains: { air: { agentFileChangeReportRequest: request } },
}),
).toBeUndefined();
}
});
it("continues Stop at most once and reports a missing tool call once", async () => {
const state = createFileChangeAuditTurnState("request-1");
const publish = vi.fn(async (_result: AgentFileChangeReportResult) => {});
const support = createSupport({ state, publish });
const hookOptions = { signal: new AbortController().signal };
const preToolInput = (toolName: string) =>
({
hook_event_name: "PreToolUse",
tool_name: toolName,
tool_input: {},
tool_use_id: "tool-1",
}) as Parameters<HookCallback>[0];
await expect(
support.preToolUseHook(preToolInput(FILE_CHANGE_AUDIT_WIRE_TOOL_NAME), "tool-1", hookOptions),
).resolves.toMatchObject({
hookSpecificOutput: { permissionDecision: "deny" },
});
const pausedStop = {
...stopInput(),
background_tasks: [{ id: "generator", type: "shell", status: "running", description: "" }],
} as Parameters<HookCallback>[0];
expect(await support.stopHook(pausedStop, undefined, hookOptions)).toEqual({});
expect(state.phase).toBe("requested");
const first = await support.stopHook(stopInput(), undefined, hookOptions);
expect(state.phase).toBe("collecting");
expect(first).toMatchObject({
hookSpecificOutput: {
hookEventName: "Stop",
additionalContext: expect.stringContaining(FILE_CHANGE_AUDIT_WIRE_TOOL_NAME),
},
});
await expect(
support.preToolUseHook(preToolInput("Bash"), "tool-1", hookOptions),
).resolves.toMatchObject({
hookSpecificOutput: { permissionDecision: "deny" },
});
await expect(
support.preToolUseHook(preToolInput(FILE_CHANGE_AUDIT_WIRE_TOOL_NAME), "tool-1", hookOptions),
).resolves.toMatchObject({
hookSpecificOutput: { permissionDecision: "allow" },
});
expect(await support.stopHook(stopInput(true), undefined, hookOptions)).toEqual({});
expect(state.phase).toBe("finished");
expect(publish).toHaveBeenCalledWith({
version: 1,
requestId: "request-1",
status: "unavailable",
reason: "notReported",
});
await support.stopHook(stopInput(true), undefined, hookOptions);
expect(publish).toHaveBeenCalledTimes(1);
});
it("publishes only the first unavailable terminal and rejects a late report", async () => {
const state = createFileChangeAuditTurnState("request-terminal-race");
state.phase = "collecting";
const publish = vi.fn(async (_result: AgentFileChangeReportResult) => {});
const support = createSupport({ state, publish });
await Promise.all([
support.finishUnavailable(state, "cancelled"),
support.finishUnavailable(state, "providerError"),
]);
expect(publish).toHaveBeenCalledTimes(1);
expect(publish).toHaveBeenCalledWith({
version: 1,
requestId: "request-terminal-race",
status: "unavailable",
reason: "cancelled",
});
await expect(
auditToolHandler(support).handler({ paths: ["src/late.ts"], complete: true }),
).resolves.toMatchObject({ isError: true });
expect(publish).toHaveBeenCalledTimes(1);
});
it("normalizes roots, drops invalid paths, and deduplicates without truncation", async () => {
const cwd = path.join(os.tmpdir(), "file-audit-project");
const additionalRoot = path.join(os.tmpdir(), "file-audit-shared");
const canonicalTempRoot = fs.realpathSync.native(os.tmpdir());
const canonicalCwd = path.join(canonicalTempRoot, "file-audit-project");
const canonicalAdditionalRoot = path.join(canonicalTempRoot, "file-audit-shared");
const outside = path.join(os.tmpdir(), "file-audit-outside", "outside.txt");
const state = createFileChangeAuditTurnState("request-2");
state.phase = "collecting";
const published: AgentFileChangeReportResult[] = [];
const support = createSupport({
state,
cwd,
additionalDirectories: [additionalRoot],
publish: async (result) => void published.push(result),
});
const tool = auditToolHandler(support);
expect(tool._meta).toMatchObject({
"anthropic/alwaysLoad": true,
"claude/endTurn": true,
});
const result = await tool.handler({
paths: [
"src/a.ts",
"src/a.ts",
cwd,
additionalRoot,
path.join(additionalRoot, "generated.ts"),
outside,
"bad\u0000path",
"bad\u0085path",
"x".repeat(4090),
],
complete: true,
uncertainty: "Generated files may be missing",
});
expect(result.isError).not.toBe(true);
expect(published).toEqual([
{
version: 1,
requestId: "request-2",
status: "reported",
paths: [
path.join(canonicalCwd, "src/a.ts"),
path.join(canonicalAdditionalRoot, "generated.ts"),
],
declaredComplete: false,
truncated: true,
uncertainty: "Generated files may be missing",
},
]);
expect(await tool.handler({ paths: ["src/late.ts"], complete: true })).toMatchObject({
isError: true,
});
expect(published).toHaveLength(1);
const duplicateOnlyState = createFileChangeAuditTurnState("request-3");
duplicateOnlyState.phase = "collecting";
const duplicateOnlyReports: AgentFileChangeReportResult[] = [];
const duplicateOnlyTool = auditToolHandler(
createSupport({
state: duplicateOnlyState,
cwd,
publish: async (report) => void duplicateOnlyReports.push(report),
}),
);
await duplicateOnlyTool.handler({ paths: ["src/a.ts", "src/a.ts"], complete: true });
expect(duplicateOnlyReports[0]).toMatchObject({
declaredComplete: true,
truncated: false,
paths: [path.join(canonicalCwd, "src/a.ts")],
});
});
it("accepts canonical paths under a symlinked workspace root", async () => {
if (process.platform === "win32") return;
const realRoot = fs.mkdtempSync(path.join(os.tmpdir(), "file-audit-real-"));
const linkedRoot = `${realRoot}-link`;
fs.symlinkSync(realRoot, linkedRoot, "dir");
try {
const state = createFileChangeAuditTurnState("request-symlink-root");
state.phase = "collecting";
const reports: AgentFileChangeReportResult[] = [];
const tool = auditToolHandler(
createSupport({
state,
cwd: linkedRoot,
publish: async (report) => void reports.push(report),
}),
);
const canonicalPath = path.join(fs.realpathSync.native(realRoot), "generated.ts");
await tool.handler({
paths: [canonicalPath, path.join(linkedRoot, "generated.ts")],
complete: true,
});
expect(reports[0]).toMatchObject({
paths: [canonicalPath],
declaredComplete: true,
truncated: false,
});
} finally {
fs.unlinkSync(linkedRoot);
fs.rmSync(realRoot, { recursive: true, force: true });
}
});
it("caps path count and total UTF-8 bytes", async () => {
const cwd = path.join(os.tmpdir(), "file-audit-caps");
const countState = createFileChangeAuditTurnState("request-count");
countState.phase = "collecting";
const countReports: AgentFileChangeReportResult[] = [];
await auditToolHandler(
createSupport({
state: countState,
cwd,
publish: async (report) => void countReports.push(report),
}),
).handler({
paths: Array.from({ length: 1030 }, (_, index) => `src/file-${index}.ts`),
complete: true,
});
expect(countReports[0]).toMatchObject({
declaredComplete: false,
truncated: true,
});
expect(countReports[0].status).toBe("reported");
if (countReports[0].status !== "reported") throw new Error("Expected a reported result");
expect(countReports[0].paths).toHaveLength(1024);
const byteState = createFileChangeAuditTurnState("request-bytes");
byteState.phase = "collecting";
const byteReports: AgentFileChangeReportResult[] = [];
await auditToolHandler(
createSupport({
state: byteState,
cwd,
publish: async (report) => void byteReports.push(report),
}),
).handler({
paths: Array.from(
{ length: 1000 },
(_, index) => `generated/${index}-${"x".repeat(280)}.txt`,
),
complete: true,
});
expect(byteReports[0]).toMatchObject({
declaredComplete: false,
truncated: true,
});
expect(byteReports[0].status).toBe("reported");
if (byteReports[0].status !== "reported") throw new Error("Expected a reported result");
expect(byteReports[0].paths.length).toBeLessThan(1000);
expect(Buffer.byteLength(JSON.stringify(byteReports[0]), "utf8")).toBeLessThanOrEqual(
AGENT_FILE_CHANGE_REPORT_MAX_BYTES,
);
});
it("fails open when report publication fails", async () => {
const state = createFileChangeAuditTurnState("request-4");
state.phase = "collecting";
const logError = vi.fn();
const tool = auditToolHandler(
createSupport({
state,
publish: async () => {
throw new Error("transport closed");
},
logError,
}),
);
await expect(tool.handler({ paths: ["src/a.ts"], complete: true })).resolves.toMatchObject({
structuredContent: { requestId: "request-4", status: "reported" },
});
expect(state.phase).toBe("finished");
expect(logError).toHaveBeenCalledWith(expect.stringContaining("transport closed"));
const unavailableState = createFileChangeAuditTurnState("request-unavailable-failure");
const unavailableSupport = createSupport({
state: unavailableState,
publish: async () => {
throw new Error("transport closed");
},
logError,
});
await expect(
unavailableSupport.finishUnavailable(unavailableState, "providerError"),
).resolves.toBeUndefined();
expect(unavailableState.phase).toBe("finished");
expect(logError).toHaveBeenCalledWith(expect.stringContaining("request-unavailable-failure"));
});
});

View File

@@ -0,0 +1,3 @@
{"type":"user","uuid":"11111111-1111-4111-8111-111111111111","parentUuid":null,"sessionId":"aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa","timestamp":"2026-01-01T00:00:00.000Z","message":{"role":"user","content":[{"type":"text","text":"Create a short HTML page"}]}}
{"type":"assistant","uuid":"22222222-2222-4222-8222-222222222222","parentUuid":"11111111-1111-4111-8111-111111111111","sessionId":"aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa","timestamp":"2026-01-01T00:00:01.000Z","error":"rate_limit","isApiErrorMessage":true,"message":{"id":"msg_usage_limit","type":"message","role":"assistant","model":"<synthetic>","content":[{"type":"text","text":"You've hit your individual spend limit · run /usage-credits to ask your admin for a higher limit"}],"stop_reason":"stop_sequence","stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0,"cache_read_input_tokens":0,"cache_creation_input_tokens":0}}}
{"type":"assistant","uuid":"33333333-3333-4333-8333-333333333333","parentUuid":"22222222-2222-4222-8222-222222222222","sessionId":"aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa","timestamp":"2026-01-01T00:00:02.000Z","message":{"id":"msg_local_command","type":"message","role":"assistant","model":"<synthetic>","content":[{"type":"text","text":"No response requested."}],"stop_reason":"stop_sequence","stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0,"cache_read_input_tokens":0,"cache_creation_input_tokens":0}}}

View File

@@ -59,7 +59,7 @@ describe("providers", () => {
expect(response.agentCapabilities?.providers).toEqual({});
});
it("lists a single optional 'main' provider, unconfigured by default", async () => {
it("lists one mutually exclusive provider slot with native routing", async () => {
const [agent] = await createAgentMock();
const response = await agent.unstable_listProviders({});
expect(response.providers).toEqual([
@@ -67,7 +67,7 @@ describe("providers", () => {
providerId: "main",
supported: ["anthropic", "bedrock", "vertex"],
required: false,
current: null,
current: { apiType: "anthropic", baseUrl: "https://api.anthropic.com" },
},
]);
});
@@ -126,7 +126,7 @@ describe("providers", () => {
).rejects.toMatchObject({ code: -32602 });
});
it("disables the 'main' provider by clearing config and reporting current: null", async () => {
it("restores native routing after the last override is disabled", async () => {
const [agent] = await createAgentMock();
await agent.unstable_setProvider({
providerId: "main",
@@ -137,7 +137,36 @@ describe("providers", () => {
await expect(agent.unstable_disableProvider({ providerId: "main" })).resolves.toEqual({});
expect((await agent.unstable_listProviders({})).providers[0].current).toBeNull();
expect((await agent.unstable_listProviders({})).providers[0].current).toEqual({
apiType: "anthropic",
baseUrl: "https://api.anthropic.com",
});
});
it("replaces the active api type when the single slot is set again", async () => {
const [agent] = await createAgentMock();
await agent.unstable_setProvider({
providerId: "main",
apiType: "anthropic",
baseUrl: "https://anthropic.example",
});
await agent.unstable_setProvider({
providerId: "main",
apiType: "bedrock",
baseUrl: "https://bedrock.example",
});
const overridden = await agent.unstable_listProviders({});
expect(overridden.providers[0].current).toEqual({
apiType: "bedrock",
baseUrl: "https://bedrock.example",
});
await agent.unstable_disableProvider({ providerId: "main" });
expect((await agent.unstable_listProviders({})).providers[0].current).toEqual({
apiType: "anthropic",
baseUrl: "https://api.anthropic.com",
});
});
it("treats disabling an unknown provider as an idempotent no-op", async () => {
@@ -298,6 +327,9 @@ describe("providers", () => {
await agent.logout({});
expect((await agent.unstable_listProviders({})).providers[0].current).toBeNull();
expect((await agent.unstable_listProviders({})).providers[0].current).toEqual({
apiType: "anthropic",
baseUrl: "https://api.anthropic.com",
});
});
});

View File

@@ -1,4 +1,7 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
import { describe, it, expect, afterEach, beforeEach, vi } from "vitest";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import * as path from "node:path";
import { ClientCapabilities } from "@agentclientprotocol/sdk";
import { ImageBlockParam, ToolResultBlockParam } from "@anthropic-ai/sdk/resources";
import {
@@ -18,8 +21,10 @@ import {
toolInfoFromToolUse,
planEntries,
applyTaskCreate,
applyTaskList,
applyTaskUpdate,
parseTaskCreateOutput,
parseTaskListOutput,
taskStateToPlanEntries,
TaskState,
} from "../tools.js";
@@ -1817,6 +1822,14 @@ describe("planEntries - undefined input regression", () => {
{ content: "Task 2", status: "completed", priority: "medium" },
]);
});
it("uses activeForm while a todo is in progress", () => {
expect(
planEntries({
todos: [{ content: "Run tests", status: "in_progress", activeForm: "Running tests" }],
}),
).toEqual([{ content: "Running tests", status: "in_progress", priority: "medium" }]);
});
});
describe("toAcpNotifications - TodoWrite with undefined input regression", () => {
@@ -1884,6 +1897,20 @@ describe("parseTaskCreateOutput", () => {
expect(parsed).toEqual({ task: { id: "2", subject: "Y" } });
});
it("continues past unrelated JSON blocks", () => {
const parsed = parseTaskCreateOutput([
{ type: "text", text: JSON.stringify({ metadata: "unrelated" }) },
{ type: "text", text: JSON.stringify({ task: { id: "2", subject: "Y" } }) },
]);
expect(parsed).toEqual({ task: { id: "2", subject: "Y" } });
});
it("parses the human-readable TaskCreate format used in session history", () => {
expect(parseTaskCreateOutput("Task #42 created successfully: Run tests")).toEqual({
task: { id: "42", subject: "Run tests" },
});
});
it("returns undefined for non-JSON content", () => {
expect(parseTaskCreateOutput("not json")).toBeUndefined();
expect(parseTaskCreateOutput([{ type: "text", text: "not json" }])).toBeUndefined();
@@ -1918,11 +1945,15 @@ describe("applyTaskCreate / applyTaskUpdate", () => {
it("updates fields by task ID and keeps insertion order in plan entries", () => {
const state: TaskState = new Map();
applyTaskCreate(state, { subject: "A", description: "" }, { task: { id: "1", subject: "A" } });
applyTaskCreate(
state,
{ subject: "A", description: "", activeForm: "Working on A" },
{ task: { id: "1", subject: "A" } },
);
applyTaskCreate(state, { subject: "B", description: "" }, { task: { id: "2", subject: "B" } });
applyTaskUpdate(state, { taskId: "1", status: "in_progress" });
expect(taskStateToPlanEntries(state)).toEqual([
{ content: "A", status: "in_progress", priority: "medium" },
{ content: "Working on A", status: "in_progress", priority: "medium" },
{ content: "B", status: "pending", priority: "medium" },
]);
});
@@ -1952,6 +1983,84 @@ describe("applyTaskCreate / applyTaskUpdate", () => {
// update is dropped instead of synthesizing a blank placeholder.
expect(state.has("5")).toBe(false);
});
it("rebuilds task state from TaskList while preserving richer local fields", () => {
const state: TaskState = new Map([
[
"1",
{
subject: "Old subject",
status: "pending",
activeForm: "Running the task",
description: "Details",
},
],
["deleted", { subject: "Stale", status: "pending" }],
]);
const output = parseTaskListOutput(
JSON.stringify({
tasks: [
{ id: "1", subject: "Current subject", status: "in_progress", blockedBy: [] },
{ id: "2", subject: "New task", status: "pending", blockedBy: [] },
],
}),
);
expect(output).toBeDefined();
applyTaskList(state, output!);
expect([...state.entries()]).toEqual([
[
"1",
{
subject: "Current subject",
status: "in_progress",
activeForm: "Running the task",
description: "Details",
},
],
[
"2",
{
subject: "New task",
status: "pending",
activeForm: undefined,
description: undefined,
},
],
]);
});
it("finds a TaskList snapshot after an unrelated JSON block", () => {
expect(
parseTaskListOutput([
{ type: "text", text: JSON.stringify({ metadata: "unrelated" }) },
{
type: "text",
text: JSON.stringify({
tasks: [{ id: "1", subject: "Recovered", status: "pending", blockedBy: [] }],
}),
},
]),
).toEqual({
tasks: [{ id: "1", subject: "Recovered", status: "pending", blockedBy: [] }],
});
});
it("parses the human-readable TaskList format used in session history", () => {
expect(
parseTaskListOutput(
"#1 [in_progress] Run tests\n#2 [pending] Write release notes [blocked by #1]",
),
).toEqual({
tasks: [
{ id: "1", subject: "Run tests", status: "in_progress", blockedBy: [] },
{ id: "2", subject: "Write release notes", status: "pending", blockedBy: ["1"] },
],
});
expect(parseTaskListOutput("No tasks found")).toEqual({ tasks: [] });
});
});
describe("toAcpNotifications - Task* tools", () => {
@@ -2112,10 +2221,9 @@ describe("toAcpNotifications - Task* tools", () => {
});
});
it("suppresses TaskList and TaskGet tool_result without touching task state", () => {
it("uses the structured TaskList result as an authoritative plan snapshot", () => {
const toolUseCache: ToolUseCache = {
"list-1": { type: "tool_use", id: "list-1", name: "TaskList", input: {} },
"get-1": { type: "tool_use", id: "get-1", name: "TaskGet", input: { taskId: "1" } },
};
const taskState: TaskState = new Map([
["1", { subject: "Existing", status: "in_progress" as const }],
@@ -2123,8 +2231,93 @@ describe("toAcpNotifications - Task* tools", () => {
const notifications = toAcpNotifications(
[
{ type: "tool_result", tool_use_id: "list-1", content: "...", is_error: false },
{ type: "tool_result", tool_use_id: "get-1", content: "...", is_error: false },
{
type: "tool_result",
tool_use_id: "list-1",
content: "#2 [pending] Recovered",
is_error: false,
},
] as any,
"user",
"test-session",
toolUseCache,
mockClient,
mockLogger,
{
taskState,
toolUseResult: {
tasks: [{ id: "2", subject: "Recovered", status: "pending", blockedBy: [] }],
},
},
);
expect(notifications).toHaveLength(1);
expect(notifications[0].update).toMatchObject({
sessionUpdate: "plan",
entries: [{ content: "Recovered", status: "pending", priority: "medium" }],
});
expect([...taskState.keys()]).toEqual(["2"]);
});
it("does not apply a logically failed TaskUpdate", () => {
const toolUseCache: ToolUseCache = {
"update-1": {
type: "tool_use",
id: "update-1",
name: "TaskUpdate",
input: { taskId: "1", status: "completed" },
},
};
const taskState: TaskState = new Map([["1", { subject: "Existing", status: "pending" }]]);
const notifications = toAcpNotifications(
[
{
type: "tool_result",
tool_use_id: "update-1",
content: "Task #1 not found",
is_error: false,
},
] as any,
"user",
"test-session",
toolUseCache,
mockClient,
mockLogger,
{
taskState,
toolUseResult: {
success: false,
taskId: "1",
updatedFields: [],
error: "Task not found",
},
},
);
expect(notifications).toHaveLength(0);
expect(taskState.get("1")).toEqual({ subject: "Existing", status: "pending" });
});
it("does not apply a replayed TaskUpdate failure without structured output", () => {
const toolUseCache: ToolUseCache = {
"update-1": {
type: "tool_use",
id: "update-1",
name: "TaskUpdate",
input: { taskId: "1", status: "completed" },
},
};
const taskState: TaskState = new Map([["1", { subject: "Existing", status: "pending" }]]);
const notifications = toAcpNotifications(
[
{
type: "tool_result",
tool_use_id: "update-1",
content: "Task #1 not found",
is_error: false,
},
] as any,
"user",
"test-session",
@@ -2135,7 +2328,7 @@ describe("toAcpNotifications - Task* tools", () => {
);
expect(notifications).toHaveLength(0);
expect(taskState.get("1")).toEqual({ subject: "Existing", status: "in_progress" });
expect(taskState.get("1")).toEqual({ subject: "Existing", status: "pending" });
});
it("does not apply TaskCreate/TaskUpdate when the tool_result reports an error", () => {
@@ -2984,3 +3177,147 @@ describe("structured tool_use_result rendering (Read/Bash/WebSearch)", () => {
});
});
});
describe("Skill tool rendering", () => {
const mockLogger: Logger = { log: () => {}, error: () => {} };
describe("toolInfoFromToolUse", () => {
it("sets title to 'Load skill: <name>' and returns empty content", () => {
const info = toolInfoFromToolUse(
{ name: "Skill", id: "toolu_1", input: { skill: "commits" } },
false,
);
expect(info.title).toBe("Load skill: commits");
expect(info.kind).toBe("other");
expect(info.content).toEqual([]);
});
it("falls back to 'Load skill' when skill name is absent", () => {
const info = toolInfoFromToolUse({ name: "Skill", id: "toolu_2", input: {} }, false);
expect(info.title).toBe("Load skill");
expect(info.content).toEqual([]);
});
it("does not throw when input is undefined", () => {
const info = toolInfoFromToolUse({ name: "Skill", id: "toolu_3", input: undefined }, false);
expect(info.title).toBe("Load skill");
expect(info.content).toEqual([]);
});
});
describe("toolUpdateFromToolResult", () => {
it("suppresses the raw 'Launching skill' result text", () => {
const toolUse = {
type: "tool_use",
id: "toolu_4",
name: "Skill",
input: { skill: "commits" },
};
const toolResult = {
type: "tool_result" as const,
tool_use_id: "toolu_4",
content: "Launching skill: commits",
is_error: false,
};
const update = toolUpdateFromToolResult(toolResult, toolUse, false);
expect(update).toEqual({});
});
});
describe("_meta.claudeCode.skill in tool_call notification", () => {
it("includes skill name in _meta.claudeCode when Skill tool is invoked", () => {
const notifications = toAcpNotifications(
[
{ type: "tool_use", id: "toolu_5", name: "Skill", input: { skill: "commits", args: "" } },
] as any,
"assistant",
"test-session",
{},
{} as AcpClient,
mockLogger,
);
expect(notifications[0]?.update).toMatchObject({
sessionUpdate: "tool_call",
_meta: { claudeCode: { toolName: "Skill", skill: "commits" } },
});
});
it("omits skill from _meta.claudeCode when skill name is missing", () => {
const notifications = toAcpNotifications(
[{ type: "tool_use", id: "toolu_6", name: "Skill", input: {} }] as any,
"assistant",
"test-session",
{},
{} as AcpClient,
mockLogger,
);
const meta = (notifications[0]?.update as any)?._meta?.claudeCode;
expect(meta).toBeDefined();
expect(meta.skill).toBeUndefined();
});
});
describe("_meta.claudeCode.skillPath", () => {
const skillMeta = (skill: string, cwd?: string) =>
(
toAcpNotifications(
[{ type: "tool_use", id: "toolu_skill_path", name: "Skill", input: { skill } }] as any,
"assistant",
"test-session",
{},
{} as AcpClient,
mockLogger,
cwd ? { cwd } : undefined,
)[0]?.update as any
)?._meta?.claudeCode;
let root: string;
beforeEach(() => {
root = mkdtempSync(path.join(tmpdir(), "acp-skill-path-"));
});
afterEach(() => {
rmSync(root, { recursive: true, force: true });
});
const writeSkill = (relativeDir: string) => {
const dir = path.join(root, relativeDir);
mkdirSync(dir, { recursive: true });
const file = path.join(dir, "SKILL.md");
writeFileSync(file, "# skill\n");
return file;
};
it("resolves a project-level .claude/skills skill", () => {
const file = writeSkill(".claude/skills/commits");
expect(skillMeta("commits", root).skillPath).toBe(file);
});
it("resolves a project-level .agents/skills skill", () => {
const file = writeSkill(".agents/skills/commits");
expect(skillMeta("commits", root).skillPath).toBe(file);
});
it("resolves a directory-scoped skill spelled prefix:name", () => {
const file = writeSkill("apps/web/.claude/skills/deploy");
expect(skillMeta("apps/web:deploy", root).skillPath).toBe(file);
});
it("resolves a plugin skill spelled plugin:name", () => {
const file = writeSkill(".claude/plugins/reviewer/skills/audit");
expect(skillMeta("reviewer:audit", root).skillPath).toBe(file);
});
it("omits skillPath when no known layout holds the skill", () => {
const meta = skillMeta("nonexistent", root);
expect(meta.skill).toBe("nonexistent");
expect(meta.skillPath).toBeUndefined();
});
it("omits skillPath when the session has no cwd", () => {
writeSkill(".claude/skills/commits");
expect(skillMeta("commits").skillPath).toBeUndefined();
});
});
});

View File

@@ -21,7 +21,9 @@ import {
ReportFindingsInput,
TaskCreateInput,
TaskCreateOutput,
TaskListOutput,
TaskUpdateInput,
TaskUpdateOutput,
TodoWriteInput,
WebFetchInput,
WebSearchInput,
@@ -216,7 +218,7 @@ export function toolInfoFromToolUse(
}
const displayPath = input?.file_path ? toDisplayPath(input.file_path, cwd) : undefined;
return {
title: displayPath ? `Write ${displayPath}` : "Write",
title: displayPath ? `Write ${displayPath}` : "Preparing file…",
kind: "edit",
content,
locations: input?.file_path ? [{ path: input.file_path }] : [],
@@ -439,6 +441,16 @@ export function toolInfoFromToolUse(
};
}
case "Skill": {
const input = toolUse.input as { skill?: string; args?: string } | undefined;
const skillName = input?.skill;
return {
title: skillName ? `Load skill: ${skillName}` : "Load skill",
kind: "other",
content: [],
};
}
case "AskUserQuestion": {
const input = toolUse.input as Partial<AskUserQuestionInput> | undefined;
const questions = Array.isArray(input?.questions) ? input.questions : [];
@@ -861,6 +873,10 @@ export function toolUpdateFromToolResult(
);
}
case "Skill": {
return {};
}
case "Edit": // Edit is handled in hooks
case "Write": {
return {};
@@ -1030,7 +1046,7 @@ export type ClaudePlanEntry = {
export function planEntries(input: { todos: ClaudePlanEntry[] } | undefined): PlanEntry[] {
return (input?.todos ?? []).map((todo) => ({
content: todo.content,
content: todo.status === "in_progress" && todo.activeForm ? todo.activeForm : todo.content,
status: todo.status,
priority: "medium",
}));
@@ -1052,31 +1068,29 @@ export type TaskEntry = {
export type TaskState = Map<string, TaskEntry>;
/**
* Best-effort parse of a TaskCreate tool_result content into the structured
* TaskCreateOutput. The SDK delivers tool outputs either as a string or as
* an array of TextBlockParam-like blocks containing JSON text; try both.
* Best-effort parse of a structured Task* tool_result. The SDK delivers tool
* outputs either as a string or as an array of TextBlockParam-like blocks
* containing JSON text; try both.
*/
export function parseTaskCreateOutput(content: unknown): TaskCreateOutput | undefined {
const tryParse = (text: string): TaskCreateOutput | undefined => {
function parseJsonToolOutput<T>(
content: unknown,
isExpectedOutput: (value: unknown) => value is T,
): T | undefined {
const tryParse = (text: string): T | undefined => {
try {
const parsed = JSON.parse(text);
if (
parsed &&
typeof parsed === "object" &&
parsed.task &&
typeof parsed.task.id === "string"
) {
return parsed as TaskCreateOutput;
}
const parsed: unknown = JSON.parse(text);
return isExpectedOutput(parsed) ? parsed : undefined;
} catch {
// ignore
return undefined;
}
return undefined;
};
if (typeof content === "string") {
return tryParse(content);
}
if (content && typeof content === "object" && !Array.isArray(content)) {
return isExpectedOutput(content) ? content : undefined;
}
if (Array.isArray(content)) {
for (const block of content) {
if (block && typeof block === "object" && "type" in block && block.type === "text") {
@@ -1091,6 +1105,119 @@ export function parseTaskCreateOutput(content: unknown): TaskCreateOutput | unde
return undefined;
}
function toolOutputTexts(content: unknown): string[] {
if (typeof content === "string") return [content];
if (!Array.isArray(content)) return [];
return content.flatMap((block) =>
block &&
typeof block === "object" &&
"type" in block &&
block.type === "text" &&
"text" in block &&
typeof block.text === "string"
? [block.text]
: [],
);
}
export function parseTaskCreateOutput(content: unknown): TaskCreateOutput | undefined {
const structured = parseJsonToolOutput(content, (parsed): parsed is TaskCreateOutput =>
Boolean(
parsed &&
typeof parsed === "object" &&
"task" in parsed &&
parsed.task &&
typeof parsed.task === "object" &&
"id" in parsed.task &&
typeof parsed.task.id === "string",
),
);
if (structured) return structured;
for (const text of toolOutputTexts(content)) {
const match = /^Task #(\S+) created successfully: (.+)$/.exec(text.trim());
if (match) return { task: { id: match[1], subject: match[2] } };
}
return undefined;
}
export function parseTaskListOutput(content: unknown): TaskListOutput | undefined {
const validStatuses = new Set(["pending", "in_progress", "completed"]);
const structured = parseJsonToolOutput(content, (parsed): parsed is TaskListOutput =>
Boolean(
parsed &&
typeof parsed === "object" &&
"tasks" in parsed &&
Array.isArray(parsed.tasks) &&
parsed.tasks.every(
(task) =>
task &&
typeof task === "object" &&
typeof task.id === "string" &&
typeof task.subject === "string" &&
typeof task.status === "string" &&
validStatuses.has(task.status),
),
),
);
if (structured) return structured;
for (const text of toolOutputTexts(content)) {
if (text.trim() === "No tasks found") return { tasks: [] };
const tasks: TaskListOutput["tasks"] = [];
const lines = text.trim().split("\n");
for (const line of lines) {
const match =
/^#(\S+) \[(pending|in_progress|completed)\] (.+?)(?: \(([^()]*)\))?(?: \[blocked by ((?:#[^,\]]+(?:, )?)+)\])?$/.exec(
line,
);
if (!match) {
tasks.length = 0;
break;
}
tasks.push({
id: match[1],
subject: match[3],
status: match[2] as TaskListOutput["tasks"][number]["status"],
...(match[4] ? { owner: match[4] } : {}),
blockedBy: match[5] ? match[5].split(", ").map((id) => id.slice(1)) : [],
});
}
if (tasks.length > 0) return { tasks };
}
return undefined;
}
export function parseTaskUpdateOutput(
content: unknown,
expectedTaskId?: string,
): TaskUpdateOutput | undefined {
const structured = parseJsonToolOutput(content, (parsed): parsed is TaskUpdateOutput =>
Boolean(
parsed &&
typeof parsed === "object" &&
"success" in parsed &&
typeof parsed.success === "boolean" &&
"taskId" in parsed &&
typeof parsed.taskId === "string" &&
"updatedFields" in parsed &&
Array.isArray(parsed.updatedFields) &&
parsed.updatedFields.every((field) => typeof field === "string"),
),
);
if (structured) return structured;
for (const text of toolOutputTexts(content)) {
const notFound = /^Task #(\S+) not found$/.exec(text.trim());
const taskId = notFound?.[1] ?? expectedTaskId;
if (taskId && (notFound || text.trim() === "Failed to delete task")) {
return { success: false, taskId, updatedFields: [], error: text.trim() };
}
}
return undefined;
}
export function applyTaskCreate(
state: TaskState,
input: TaskCreateInput | undefined,
@@ -1125,9 +1252,23 @@ export function applyTaskUpdate(state: TaskState, input: TaskUpdateInput | undef
});
}
export function applyTaskList(state: TaskState, output: TaskListOutput): void {
const previous = new Map(state);
state.clear();
for (const task of output.tasks) {
const existing = previous.get(task.id);
state.set(task.id, {
subject: task.subject,
status: task.status,
activeForm: existing?.activeForm,
description: existing?.description,
});
}
}
export function taskStateToPlanEntries(state: TaskState): PlanEntry[] {
return Array.from(state.values()).map((task) => ({
content: task.subject,
content: task.status === "in_progress" && task.activeForm ? task.activeForm : task.subject,
status: task.status,
priority: "medium",
}));