Compare commits

...

70 Commits

Author SHA1 Message Date
mark.tkachenko
3dba368bc4 refactor: move the v1 steering and async task stop requests out of acp-agent.ts 2026-11-10 18:39:40 +01:00
Mark Tkachenko
bce309f482 refactor: move the v1 client connection out of acp-agent.ts (#1576)
Moves `ClientConnection` and `ChangedMetaClient` verbatim from
`src/v1/acp-agent.ts` into `src/v1/client-connection.ts`. No behaviour
change.

- Both classes gain `export`; nothing else in them changes.
- `client-connection.ts` imports nothing from `acp-agent.ts`.
- The `acp-client.ts` comment that names where `ClientConnection` lives
now points to the new file.
- Emitted JS differs from `main` only in `dist/v1/acp-agent.js` (the
import and the two classes) plus the new `dist/v1/client-connection.js`.

Review with `git diff --color-moved` to see the move.
2026-11-10 17:54:17 +01:00
acp-release-bot[bot]
6b86f7b46a chore(main): release 0.104.1 (#1575)
🤖 I have created a release *beep* *boop*
---


##
[0.104.1](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.104.0...v0.104.1)
(2026-11-10)


### Bug Fixes

* carry the auth detail in the ACP account label
([#1573](https://github.com/agentclientprotocol/claude-agent-acp/issues/1573))
([3d6eb7b](3d6eb7b5b6))
* distinguish Ultra from Max in auth status
([#1574](https://github.com/agentclientprotocol/claude-agent-acp/issues/1574))
([7d95d30](7d95d30ec3))
* update to @anthropic-ai/claude-agent-sdk 0.3.322
([#1577](https://github.com/agentclientprotocol/claude-agent-acp/issues/1577))
([b89e449](b89e4497da))

---
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-11-10 14:48:59 +01:00
Ben Brandt
b89e4497da fix: update to @anthropic-ai/claude-agent-sdk 0.3.322 (#1577)
- Bumps `@anthropic-ai/claude-agent-sdk` 0.3.321 → 0.3.322.
- Runs `npm update` for in-range lockfile bumps: `rolldown` 1.2.16 →
1.2.18, `vite` 6.2.16 → 6.2.17, `express-rate-limit` 8.3.6 → 8.3.7,
`jose` 8.7.3 → 8.7.4.

TypeScript stays at 6.0.3: 7.0.2 is out, but the latest
`@typescript-eslint/parser` (8.72.2) requires `typescript <6.1.0`.

`npm run build`, `npm run check` and `npm run test:run` pass locally.
One test failed once after `npm update` (not captured); four later full
runs all passed, so it looks flaky.
2026-11-10 14:41:44 +01:00
Ben Brandt
7d95d30ec3 fix: distinguish Ultra from Max in auth status (#1574)
## Summary

- Read the optional `planTier` field from the existing `claude auth
status --json` probe. Display Max subscriptions with `max_20x` as
**Claude Ultra**, while `max_5x` remains **Claude Max**.
- Fall back to the existing subscription label when the tier is missing,
unknown, or malformed. Preserve auth-source precedence and raw plan
metadata.
- Keep the CLI-derived label when a session SDK account read reports the
same identity and plan without a tier. A fresh CLI probe can still
replace the label, including when its tier is absent.
- Cover ACP v1 and v2 notifications and responses, and document the
display behavior.

The SDK AccountInfo omits the plan tier, but the bundled CLI already
emits it in auth-status JSON. This uses that CLI-only field as
best-effort display metadata, without additional probes, direct
credential-file reads, or entitlement checks.

## Validation

- `npm run build`
- `npm run check`
- `npm run test:run` — 4,002 passed, 33 skipped
- Targeted v1 auth-status and v2 protocol tests — 341 passed
- Local bundled CLI smoke check: `subscriptionType: "max"`, `planTier:
"max_20x"` maps to `Claude Ultra`
- Live model integration tests were not run.
2026-11-10 13:25:20 +00:00
Ben Brandt
3d6eb7b5b6 fix: carry the auth detail in the ACP account label (#1573)
## Summary

The ACP `auth/status` account (`label`, `active`, `name`, `email`,
`organization`) has no field for the identity's `detail`: an API key's
source, a gateway's host, or "OAuth token". It was only in
`_meta.claudeCode.detail`, so a client that shows the standard fields
could not tell which API key or gateway is in use.

`toAcpAuthStatus` now appends the detail to the account's `label` in
parentheses:

- `Anthropic API key (ANTHROPIC_API_KEY)`
- `Custom model gateway (gw.example.com)`
- `Claude subscription (OAuth token)`

An identity without a detail keeps its label as it was (`Claude Max`).

## Unchanged

- `_meta.claudeCode` still carries `kind`, `detail` and `plan` on their
own.
- The `_auth/status_update` extension payload (`label` and `detail` as
separate fields).
- The mapping is shared, so v1 and v2 report the same label in
`auth/status` and `auth/status_update`.

## Testing

- `src/tests/v1/auth-status.test.ts` expectations updated for the API
key and gateway labels.
- `npm run test:run`, `npm run check` and `npm run build` pass.
2026-11-10 14:21:33 +01:00
acp-release-bot[bot]
de521df295 chore(main): release 0.104.0 (#1571)
🤖 I have created a release *beep* *boop*
---


##
[0.104.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.103.0...v0.104.0)
(2026-11-10)


### Features

* implement the ACP auth/status RFD next to _auth/status_update
([#1570](https://github.com/agentclientprotocol/claude-agent-acp/issues/1570))
([d202748](d202748976))


### Bug Fixes

* offer Anthropic Console login in remote environments
([#1572](https://github.com/agentclientprotocol/claude-agent-acp/issues/1572))
([c09543d](c09543d85a))

---
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-11-10 13:44:19 +01:00
Mark Tkachenko
c09543d85a fix: offer Anthropic Console login in remote environments (#1572)
## Problem

In a remote environment (`SSH_CONNECTION`, `SSH_CLIENT`, `SSH_TTY`,
`NO_BROWSER` or `CLAUDE_CODE_REMOTE` set), `initialize` advertised only
`claude-login`, the TUI `claude /login`. A user with API billing had no
Anthropic Console option. With `--hide-claude-auth`, the list was empty
and there was no way to log in at all.

There is a second bug. A `_meta["terminal-auth"]` command reruns the
agent with its own flags, for example `--hide-claude-auth --cli auth
login --console`. The `--cli` passthrough removed only `--cli`, so the
Claude CLI failed with `unknown option '--hide-claude-auth'`.

## Fix

- `terminalAuthMethods` (`src/auth-methods.ts`) now offers
`console-login` in remote environments too. Since Claude Code 2.1.126,
`auth login` accepts the OAuth code pasted into the terminal when the
localhost callback can't be reached. The subscription method stays as
`claude-login` on a remote host and `claude-ai-login` locally.
`--hide-claude-auth` hides it in both cases. The change is in the shared
module, so it applies to v1 and v2.
- New `cliArgs()` (`src/agent-command.ts`) removes `--cli` and
`--hide-claude-auth` before the arguments go to the Claude CLI.

## Tests

- `v1/authorization.test.ts`: an `it.each` over the five remote env vars
× typed/`_meta` terminal auth × with/without `--hide-claude-auth`.
- `v2/acp-v2.test.ts`: a remote host with `--hide-claude-auth` gets
exactly one `console-login` terminal method.
- `agent-command.test.ts`: `cliArgs` under Node, a compiled binary, and
`--cli --version`.

Manually checked in a JetBrains IDE through a local launch overlay with
`SSH_CONNECTION` set and `--hide-claude-auth`. Only Anthropic Console
was offered, the terminal login completed, and the chat worked
afterwards.
2026-11-10 13:40:44 +01:00
Ben Brandt
d202748976 feat: implement the ACP auth/status RFD next to _auth/status_update (#1570)
Implements the [auth status
RFD](https://agentclientprotocol.com/rfds/get-auth-state) on ACP v1 and
v2. The `_auth/status_update` extension is unchanged and still sent, so
clients that read it keep working.

## What changes for a client

- `initialize` advertises `auth.status: {}` (v1 under
`agentCapabilities`, v2 under `capabilities`), next to the existing
`_meta.authStatus` marker.
- `auth/status_update` is sent whenever the identity changes, right
after `_auth/status_update`. Each notification is deduplicated on its
own payload.
- `auth/status` answers with the identity the connection holds.
- If an identity is known, it answers at once and starts a `claude auth
status` read in the background. A change that read finds arrives as
`auth/status_update`.
- If none is known yet (a call right after `initialize`), it waits for
the probe `initialize` started, bounded by the existing 5 s timeout, and
answers `unknown` if that publishes nothing.

## Mapping

`toAcpAuthStatus` in `src/auth-status.ts` maps the existing identity:

| Identity | `state` | `accounts` |
| --- | --- | --- |
| not read yet | `unknown` | none |
| `kind: "none"` | `unauthenticated` | none |
| any other kind | `authenticated` | one entry, `active: true`, with
`label`, `email`, `organization` |

The RFD account has no field for the credential type, so `kind`,
`detail` and `plan` go under the account's `_meta.claudeCode`.

## Notes for review

- `state` describes the agent's own login. It does not count a
`providers/set` route, and `--hide-claude-auth` does not change it: a
subscription login is reported as `authenticated` even though the
subscription guard refuses `session/new` for it. The legacy notification
behaves the same way.
- `auth/status_update` goes to every client. The RFD has no client
capability to gate on.
- The background read from `auth/status` is an ordinary probe, so under
`--hide-claude-auth` it can mark a session for recreation when the
account kind changed, as the probe at the next prompt would.
- Probes are now numbered in start order. One that lands after a
later-started probe has published is discarded, so an older read cannot
overwrite a newer one.
- `AcpClient` gained `authStatusUpdate`, so two test doubles needed a
stub.
- 42 scenario golden files change by one line each (the new capability
in `initialize`), and the origin/main baseline comparison allows
`auth.status`.

## Testing

`npm run check`, `npm run build` and `npm run test:run` pass. New tests
cover the mapping, the capability, `auth/status` and
`auth/status_update` on v1 and v2, a gateway login, a first-probe
timeout, out-of-order probes and the dedupe.
2026-11-10 13:02:26 +01:00
acp-release-bot[bot]
a1d83c6031 chore(main): release 0.103.0 (#1559)
🤖 I have created a release *beep* *boop*
---


##
[0.103.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.102.0...v0.103.0)
(2026-11-10)


### ⚠ BREAKING CHANGES

* deep imports of `dist/` files fail with
`ERR_PACKAGE_PATH_NOT_EXPORTED`, and `lib.ts` exports only `serveAcp`,
`AUTH_STATUS_UPDATE_METHOD` and their types.

### Features

* serve ACP v2 with a direct v2 agent and remove the v1 bridge
([#1560](https://github.com/agentclientprotocol/claude-agent-acp/issues/1560))
([cb285cd](cb285cd285))


### Bug Fixes

* give subagent checklists their own plan ID
([#1558](https://github.com/agentclientprotocol/claude-agent-acp/issues/1558))
([2f7e386](2f7e386f36))


### Code Refactoring

* export only lib.ts, with serveAcp and the authStatus wire names
([#1566](https://github.com/agentclientprotocol/claude-agent-acp/issues/1566))
([566e265](566e2652a9))

---
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-11-10 09:35:53 +01:00
Ben Brandt
966aee0f04 refactor: move the test-only tool render and metadata wrappers into src/tests/ (#1569)
`toolInfoFromToolUse` and `toolUpdateFromToolResult` in `src/tools.ts`
and `isLocalCommandMetadata` in `src/message-filters.ts` were public
`lib.ts` exports. #1566 removed them from `lib.ts`, so now only tests
call them. They're thin wrappers: the first two build an
`AcpToolCallRenderer` for one tool use or result, and the third checks
that `stripLocalCommandMetadata` drops everything.

They move unchanged to `src/tests/tool-render.ts`. `tools.test.ts`,
`diff-patch.test.ts` and `v1/acp-agent.test.ts` import them from there.
Production code renders through `AcpToolCallRenderer` directly, as
before. Code changes only: no test changes and no behavior change.

A scan of `src/` for exports finds no export that nothing uses. The only
remaining test-only exports are the deliberate test hooks
`clearDiffMemo` (`diff.ts`) and `sleep` (`utils.ts`).

Test results:
- `npx tsc --noEmit`, `npm run check`, `npm run build`: pass
- `npx vitest run`: 3939 passed / 33 skipped
2026-11-10 08:31:08 +00:00
Ben Brandt
66adc1a4c9 refactor: share the subagent replay's transcript scan between v1 and v2 (#1568)
`src/subagent-replay.ts` (v1, `RfdSubagentReplay`) and
`src/v2/subagent-replay.ts` (`SubagentReplay`) each kept a copy of the
transcript scan. The copies differed only in formatting and variable
names. The scan does four things:
- It registers each Agent or Task launch that has metadata as a child,
and records a launch without metadata as omitted.
- It records each SendMessage and SubagentHandback call.
- It marks a failed hand-back.
- It sets a foreground launch's end from its tool result.

That scan, the `ReplayChild` and `SendMessageCall` types, the child's
end state, and the `contentBlocks`, `inputOf`, `textOf` and
`isDirectory` helpers move to `ReplayTranscript` in
`subagent-replay.ts`. Each replay holds one and reads `children`,
`sendMessages`, `handbacks` and `omitted` from it.

What each replay sends stays where it was. v1 publishes asynchronously
through `Publish`, and v2 sends synchronously through `SessionOutput`,
with the v2-only rules: each child's end also goes on the child's own
stream, and live children get their current state after the response.
Sharing those parts would tie the two protocols together, so I left them
separate. The scan only parses transcripts, so sharing it adds no
dependency between v1 and v2.

v2's `ReplayChild` is the shared type plus `output?: SessionOutput`. Its
`launchEnd` was a narrower `"completed" | "failed" | "cancelled"`, which
`endState` passed through `nativeSubagentState`. That call returns those
three values unchanged, so `replayEndState` gives the same result in
both versions.

No behavior change: 128 lines added and 242 removed, with no test
changes.

Test results:
- `npx tsc --noEmit`, `npm run check`, `npm run build`: pass
- `npx vitest run`: 3939 passed / 33 skipped. The v1 and v2 replay
scenario goldens are unchanged.
- `RUN_INTEGRATION_TESTS=1 npx vitest run`: passes. One run hit the
known load flake "absorbs the interrupt's lagged trailer after
cancelling a held turn", which passes on its own.
2026-11-10 08:19:11 +00:00
Ben Brandt
bc031f362c test: split src/tests/ into v1/, v2/ and shared tests (#1567)
`src/tests/` was flat, so it didn't follow the `src/v1/` / `src/v2/`
layout.

- **`src/tests/v1/`** holds the 37 tests that import `src/v1/`, plus
`session-doubles.ts`, which builds v1 sessions.
- **`src/tests/v2/`** holds the tests of `src/v2/` and `serve.ts`:
`v2.test.ts`, `v2-integration.test.ts`, `acp-v2.test.ts` and
`acp-scenarios-v2.test.ts`.
- **The top level** keeps the tests of shared modules, plus
`helpers.ts`, `setup-no-network.ts`, the scenario harness
(`acp-scenarios/`) and `fixtures/`.

A test goes to `v1/` when it imports the v1 agent, even if it is mostly
about a shared module. For example, `providers.test.ts` and
`fast-mode-config.test.ts` drive `ClaudeAcpAgent` for some of their
cases.

Only paths change. Imports and `vi.mock`/`vi.importActual` specifiers
gain one `../`. The `here`-relative paths to the goldens, the
origin/main recordings, `package.json` and `fixtures/` were updated too.
`AGENTS.md`, `docs/air-extensions.md` and the scenario files' doc
comments now give the new paths. No test, golden file or recording
changed. A script checked that every relative module specifier under
`src/tests/` resolves, because a `vi.mock` path that doesn't resolve
mocks nothing and raises no error.

Test results:
- `npx tsc --noEmit`, `npm run check`, `npm run build`: pass
- `npx vitest run`: 3939 passed / 33 skipped, the same as before the
move.
- `RUN_INTEGRATION_TESTS=1 npx vitest run`: 3964 passed / 8 skipped.
2026-11-10 09:07:09 +01:00
Ben Brandt
566e2652a9 refactor!: export only lib.ts, with serveAcp and the authStatus wire names (#1566)
Stacked on #1564.

The package's `exports` map listed `"./*": "./*"`, so every file under
`dist/` was public, and any file move could break a deep import. It now
lists only `.` (`dist/lib.js`) and `./package.json`.

**`lib.ts` exports:**
- `serveAcp`, which serves ACP v1 and v2 on stdio through the SDK's
protocol router. This is what `index.ts` runs.
- The `AcpProtocols` and `Logger` types.
- `AUTH_STATUS_UPDATE_METHOD` and the `authStatus` payload types, which
a client needs to read the extension's traffic.

**No longer exported:**
- `ClaudeAcpAgent`
- `runAcp`: v1 only and unused, replaced by `serveAcp`, and now deleted.
- `toAcpNotifications`, `streamEventToAcpNotifications`,
`isLocalCommandMetadata`, `stripLocalCommandMetadata`
- `toolInfoFromToolUse`, `toolUpdateFromToolResult`, `toDisplayPath`,
`planEntries`
- `SettingsManager`
- `nodeToWebReadable`, `nodeToWebWritable`, `Pushable`, `unreachable`
- The types `NewSessionMeta`, `SDKMessageFilter`, `ToolUpdateMeta` and
`ClaudePlanEntry`.

`src/acp-agent.ts`, which re-exported `src/v1/acp-agent.ts` for the
`dist/acp-agent.js` path, is gone. So are the re-exports in
`src/v1/acp-agent.ts` that kept moved names on that path. Tests now
import those names from their own modules. The ESLint boundary rules and
`AGENTS.md` drop their `acp-agent.ts` exemptions.

`release-please-config.json` sets `bump-minor-pre-major`, so the `!`
bumps the minor version, the same as a `feat:`.

BREAKING CHANGE: deep imports of `dist/` files fail with
`ERR_PACKAGE_PATH_NOT_EXPORTED`, and `lib.ts` exports only `serveAcp`,
`AUTH_STATUS_UPDATE_METHOD` and their types.

Test results:
- `npx tsc --noEmit`, `npm run check`, `npm run build`: pass
- `npx vitest run`: 3939 passed / 33 skipped. `module-exports.test.ts`
pins the `lib.ts` names and the `exports` map.
- `node scripts/smoke-binary.mjs -- node dist/index.js`: pass
- Resolving the package from its root:
`import("@agentclientprotocol/claude-agent-acp")` gives `serveAcp` and
`AUTH_STATUS_UPDATE_METHOD`, `…/dist/acp-agent.js` throws
`ERR_PACKAGE_PATH_NOT_EXPORTED`, and `…/package.json` resolves.
2026-11-10 07:56:24 +00:00
Ben Brandt
0e7e74d424 chore: lint the import boundaries between v1, v2 and shared modules (#1564)
Stacked on #1562.

Adds ESLint rules for the layout from #1562:

- `src/v2/**` must not import `src/v1/` or `src/acp-agent.ts` (the v1
re-export).
- `src/v1/**` must not import `src/v2/`.
- Shared modules (everything in `src/` except `v1/`, `v2/`, `tests/` and
the entry points `index.ts`, `lib.ts`, `serve.ts`, `binary-entry.ts`,
`acp-agent.ts`) must not import `src/v1/`, `src/v2/` or
`src/acp-agent.ts`.

This uses core `no-restricted-imports` with a `regex` pattern, which
covers static imports and re-exports, type-only ones too. Core
`no-restricted-syntax` covers `import()` calls and `import("...")`
types, which that rule does not check. No new dependencies. AGENTS.md
describes the rule in one sentence.

The tree already passes, so nothing is allow-listed. I checked the rules
with throwaway files covering each import form, at `src/`,
`src/tool-calls/` and `src/v2/` depth. They flagged every violation and
passed the shared import next to them.

No behavior change. Export pins unchanged.

Test results:
- `npx tsc --noEmit`, `npm run check`, `npm run build`: pass
- `npx vitest run`: 3939 passed / 33 skipped, goldens unchanged. The
first run hit the known session-index load flake "reads only the changed
transcript for a subscription". That file passed on its own, and so did
a full rerun.
- `node scripts/build-binary.mjs && node scripts/smoke-binary.mjs`: pass

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-11-10 07:49:27 +00:00
Ben Brandt
f115b90161 refactor: move the v1 agent into src/v1 (#1562)
Stacked on #1561.

Moves the modules that only the v1 agent uses into `src/v1/` with `git
mv`, so the tree reads as `src/v1/` (v1 agent), `src/v2/` (v2 agent) and
the rest of `src/` (shared). No behavior change.

The v1-only set comes from the import graph: modules reachable from
`acp-agent.ts` whose every non-test importer is itself v1-only (entry
points `lib.ts`/`serve.ts` aside).

Moved:
- `acp-agent.ts`, `background-work.ts`, `goal-extension.ts`,
`turn-events.ts`
- `tool-calls/changed-meta-filter.ts` ->
`v1/tool-calls/changed-meta-filter.ts`
- `session-index/*` -> `v1/session-index/*`, except
`session-index/explicit-title.ts`, which stays because
`session-titles.ts` (shared with v2) imports it

Stay shared: `clear-context-coordinator.ts`, `native-subagents.ts` and
`acp-subagents.ts`. v2 and shared modules import all three.

- `src/acp-agent.ts` is now `export * from "./v1/acp-agent.js"`.
`dist/acp-agent.js` exports the same names, and `module-exports.test.ts`
passes unchanged. `lib.ts` and `serve.ts` import `v1/acp-agent.js`
directly.
- Imports in `src/` and tests, path mentions in comments, and AGENTS.md
are updated. Scripts, vitest config and docs referenced no moved path.
- History: `git log --follow src/v1/acp-agent.ts` follows the move.
Because `src/acp-agent.ts` still exists as the re-export, blame needs
`git blame -C`.

Test results:
- `npx tsc --noEmit`, `npm run check`, `npm run build`: pass
- `npx vitest run`: 3939 passed / 33 skipped, goldens unchanged
- `node scripts/build-binary.mjs && node scripts/smoke-binary.mjs`: pass

Co-authored-by: Claude <noreply@anthropic.com>
2026-11-10 08:44:57 +01:00
Ben Brandt
617d5351ed refactor: move the Claude Code to ACP mapping out of acp-agent.ts (#1561)
Stacked on #1560.

The v2 agent in `src/v2/` imported the Claude Code to ACP mapping,
shared types and a few helpers from `acp-agent.ts` (17 imports), so it
loaded the whole v1 agent module. This moves that code into modules that
own it. `acp-agent.ts` imports it back and keeps re-exporting every name
it exported before, so `dist/acp-agent.js` deep imports still work.

What moved where:

- `src/sdk-to-acp.ts`: `toAcpNotifications`,
`streamEventToAcpNotifications`, `messageIdForGrouping`,
`stampParentToolUseId`, the `ToolUseCache` / `StreamedToolInputCache`
types and the streamed tool input scanner.
- `src/message-filters.ts`: `stripLocalCommandMetadata`,
`isLocalCommandMetadata`, `isSyntheticLoginMessage`,
`isReplayHiddenMetaMessage`, `LOCAL_ONLY_COMMANDS` and the transcript
marker scanner.
- `src/prompt-to-claude.ts`: `promptToClaude`,
`SUPPORTED_PROMPT_CONTENT_TYPES`.
- `src/slash-commands.ts`: `getAvailableSlashCommands`.
- `src/result-end.ts` (existing): `AUTONOMOUS_RESULT_ORIGINS`,
`isAbortedResult`.
- `src/logger.ts`, `src/acp-client.ts`, `src/new-session-meta.ts`: the
`Logger`, `AcpClient`, `NewSessionMeta` and `SDKMessageFilter` types.

About 1,360 lines leave `acp-agent.ts` (12,718 to 11,354). The code
moved as-is. Only a few comments that named `acp-agent.ts` as the home
of the shared code were reworded. `src/v2/`, `utils.ts`, `serve.ts`,
`lib.ts` and `session-index/connection.ts` now import from the new
modules. Tests import the moved code from the new modules too. None of
the new modules imports `acp-agent.ts`. `grep -rn acp-agent src/v2`
returns nothing, and a walk of the static imports in `dist/v2` reaches
119 modules, none of them `acp-agent.js`.

No behavior change. The export pins in
`src/tests/module-exports.test.ts` for `lib.js` and `acp-agent.js` pass
unchanged.

Test results:

- `npx tsc --noEmit`, `npm run check` and `npm run build` pass.
- `npx vitest run`: 3939 passed, 33 skipped. That is the 3935 before,
plus the 4 provider tests now on the base branch.
- Goldens unchanged.
- `node scripts/build-binary.mjs && node scripts/smoke-binary.mjs` pass.

Co-authored-by: Claude <noreply@anthropic.com>
2026-11-10 08:40:42 +01:00
Ben Brandt
cb285cd285 feat: serve ACP v2 with a direct v2 agent and remove the v1 bridge (#1560)
## Summary

ACP v2 was served by a bridge (`src/v2/agent.ts`) that converted each v2
request to v1 and ran it through `ClaudeAcpAgent`. Every v2 session
therefore ran the v1 core's turn machinery: held prompts, echo matching,
trailing-idle debt and the hold for background subagents. v2 asks for
none of it. This PR serves v2 with its own agent in `src/v2/`, which
follows Claude Code's own events. A prompt is answered when Claude Code
takes it in, and `state_update` reports foreground work. Background
subagents and tasks are listed in `background` and do not hold the turn.

- **New v2 agent (`src/v2/`).** `V2Agent` and `V2Session` cover:
  - the prompt lifecycle and cancellation
- subagents as child sessions, following the subagents RFD, plus
background lists
  - replay, restarts for provider and route changes, auth and providers
- elicitation, config options, `/remote-control`, and the
`--hide-claude-auth` guard
- **Bridge removed.** `ClaudeAcpAgent` serves ACP v1 only: its `v2`
option, foreground/background hooks and `this.v2` branches are gone. v1
behavior is unchanged, and no v1 test expectation or golden file
changed. The control flow of `acp-agent.ts` is simpler and the file is
about 450 lines shorter.
- **Shared modules.** Protocol-neutral logic moved out of `acp-agent.ts`
into modules both versions use, including:
- `session-model.ts`, `session-effort.ts`, `session-config-options.ts`
and `session-env.ts`
  - `providers.ts`, `auth-status-reporter.ts` and `context-window.ts`
  - `result-end.ts`: one set of rules for reading a result in both cores
  - `prompt-suggestions.ts` and `mcp-servers.ts`
- **JetBrains AIR on v2.** An AIR v2 client gets the AIR extensions that
ACP v2 does not cover:
  - `recommendedValue`
  - the permission presentation
  - question custom answers
  - prompt suggestions
- `agentFileChangeReport`, sent at the end of the turn, before its idle

ACP v2 itself covers subagent sessions, background subagents, async
tasks, plan files, diff patches, compaction, custom instructions and
session failures. See "ACP v2" in `docs/air-extensions.md`.
- **Ported from main.** Main's v2 changes made on the bridge are ported
to the new agent, and main's v2 tests run against it:
  - workflow cards and phases (#1550, #1552)
  - task tool calls (#1556)
  - plan removal on reset (#1554)
  - subagent checklist plan ids (#1558)
  - the Fable alias (#1548)

## Behavior changes for v2 clients

Each one follows the v2 draft spec, where the bridge did not:

- **Background subagents.** They no longer hold the prompt open. They
are listed in `background`, and their work is reported in their child
session's state.
- **`requires_action`.** While the root waits on foreground subagents,
it reports `requires_action` only when every one of them waits on the
user.
- **Resume of an active session.** Its work keeps running, and the new
MCP servers replace the old ones (`setMcpServers`). The bridge tore the
session down.
- **Cancel.** The idle after a cancel waits for Claude Code's aborted
result and carries its usage. A stopped child's open tool calls end
before its idle.
- **`/remote-control`.** It runs as its own turn. Sent while Claude
works, it waits for that work to end.
- **Accepted URL elicitation.** It keeps the work `requires_action`
until Claude Code reports the elicitation complete.
- **Exited Claude Code.** A session whose Claude Code process exited
stays active and answers -32603 with what happened, until it is resumed.
- **Provider change.** `providers/set`/`disable` restarts idle sessions
before it answers. A busy session restarts at its next turn boundary,
and its prompts are not held meanwhile, which the provider RFD allows.

## Workarounds

- **`/remote-control` waits outside the prompt queue.** The adapter's
own `/clear` and plan prompts, which continue a turn in a fresh context,
use their own push queue (`ownPushes`). They must not wait behind a
`/remote-control` that waits for that same turn to end.
- **Restart on additional directories.** A resume with other additional
directories restarts the query at its next turn boundary. The SDK cannot
change the directories of a running query.

## Rollback

`CLAUDE_AGENT_ACP_PROTOCOL=v1` negotiates every client down to v1. The
bridge and its `CLAUDE_AGENT_ACP_V2_CORE` switch are removed.

## Not served on v2 yet

- AIR's tool call keys (`commandTitle`, `subagent`, `skill`, `mcp`), the
session index, and API retry warnings.
- A `replayFrom` cursor other than `start`, which is rejected with
-32602, and steering, which is v1 only.

## Packaging

- `lib.js` exports are unchanged.
- `acp-agent.js` exports no name fewer than before. It adds
`AUTONOMOUS_RESULT_ORIGINS`, `getAvailableSlashCommands` and
`isAbortedResult`, which the v2 agent uses, and the export pin test is
updated.
- Internal `dist/v2/*` modules changed. They are reachable only through
the `"./*"` export and are not documented.

## Testing

- `npm run format:check`, `lint`, `build` and `test:run` pass: 3935
passed, 33 skipped.
- `npm run test:integration` against the real CLI passes: 3960 passed, 8
skipped. This includes `src/tests/v2-integration.test.ts`.
- The v2 golden scenarios (`acp-scenarios-v2.test.ts`) match without
being regenerated.
- The binary builds and `scripts/smoke-binary.mjs` passes.
- Two tests unchanged from main can time out under heavy load: the
session-index file watcher test and v1's "lagged trailer" test. Both
pass on rerun.

## Follow-up

- Split `src/` into v1, v2 and shared modules.
- Move the SDK-to-ACP mapping that `src/v2/` still imports from
`acp-agent.ts` into a shared module.
- Add a lint rule that keeps the boundary.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-11-10 08:34:38 +01:00
Ben Brandt
2f7e386f36 fix: give subagent checklists their own plan ID (#1558)
A subagent's TodoWrite checklist was published with the main checklist's
`planId: "plan"`, so it replaced the main conversation's plan.

This only happened on ACP v1 clients that advertise `plan` but not
`subagents`. With native subagent sessions (every v2 client, AIR, and v1
with `subagents`), the checklist already goes to the child session, and
that's unchanged.

Changes:

- In that case, a subagent's TodoWrite gets `planId:
"plan:<parentToolUseId>"`, so it sits next to the main checklist
(`todoWritePlanUpdate` in `src/plans.ts`).
- An empty subagent TodoWrite sends `plan_removed` for its own plan ID,
not `"plan"`.
- `/clear` also removes the subagent checklist plans it published.
- Nothing is removed when a subagent finishes. The plan operations RFD
now says Agents remove cancelled plans, not finished ones.

Not changed:

- Task* tools keep `"plan"`. Claude Code shares one task list across all
agents in a session, so a per-subagent ID would show the same list
twice.
- Clients without the `plan` capability can hold only one plan, so a
subagent's checklist still replaces it there, as before.

Tests cover v1 (main and subagent plans side by side, then `/clear`) and
v2 (the checklist arrives as `"plan"` in the child session, and the
root's plan is untouched).
2026-11-09 23:22:19 +01:00
acp-release-bot[bot]
8e18eb7c75 chore(main): release 0.102.0 (#1553)
🤖 I have created a release *beep* *boop*
---


##
[0.102.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.101.1...v0.102.0)
(2026-11-09)


### Features

* list workflow phases and agents on the workflow card
([#1552](https://github.com/agentclientprotocol/claude-agent-acp/issues/1552))
([176ae06](176ae0634c))
* send the checklist as a plan operation and remove plans on reset
([#1554](https://github.com/agentclientprotocol/claude-agent-acp/issues/1554))
([766fd14](766fd142f6))


### Bug Fixes

* report v2 background tasks without a tool call as their own tool call
([#1556](https://github.com/agentclientprotocol/claude-agent-acp/issues/1556))
([d098fb0](d098fb08d5))
* update to @anthropic-ai/claude-agent-sdk 0.3.321
([#1557](https://github.com/agentclientprotocol/claude-agent-acp/issues/1557))
([855447c](855447c7cd))

---
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-11-09 20:52:05 +01:00
Ben Brandt
855447c7cd fix: update to @anthropic-ai/claude-agent-sdk 0.3.321 (#1557)
## Summary

Updates `@anthropic-ai/claude-agent-sdk` from 0.3.319 to 0.3.321. It
bundles Claude Code 2.1.321, which adds Fable 5.2 (`claude-fable-5-2`)
as the new default `fable` model. Also bumps these dev dependencies:
`@anthropic-ai/sdk` 1.6.0 → 1.7.0 and `@typescript-eslint/*` 8.72.1 →
8.72.2. `typescript` 7 is left for its own PR.

## Fable 5.2

Checked against the real CLI:

- A new session lists `fable` → `claude-fable-5-2`, and
`claude-fable-5-1` as its own row.
- A resumed Fable 5.2 session lists `claude-fable-5-2` and no `fable`
row, the same pattern 5.1 had.

`aliasResumedModelRow` already handles any version, so the code is
unchanged. Its doc comment and test fixture now describe the 2.1.321
lists. The fixture also covers a resumed Fable 5.1 session keeping
`claude-fable-5-1`, because a newer Fable is listed.

## Changelog review

- No SDK type changes that affect us. The only changes are doc text and
a new optional `endedLoopWakeup` field in `sdk-tools.d.ts`.
- Queued subagent reports in auto mode now share one model call, and all
but the last end in a `num_turns: 0` placeholder result. These results
carry `task-notification` or `peer` origins, which we already treat as
placeholders. That handling dates from CLI 2.1.274, which did the same
for background-task notifications.
- Fixes we pick up: one `undefined` entry in `hooks` no longer drops
every hook; a PreToolUse `ask` is no longer lost when another hook's
`defer` finishes first; project skills and commands no longer go missing
after entering a worktree; MCP OAuth no longer sends refresh tokens to
another host after a redirect.

## Testing

- `npm run build`, `npm run check`, `npm run test:run`
- Real CLI: listed the models for a new session and for a resumed Fable
5.2 session

Co-authored-by: Claude <noreply@anthropic.com>
2026-11-09 19:43:47 +00:00
Ben Brandt
d098fb08d5 fix: report v2 background tasks without a tool call as their own tool call (#1556)
## Summary

Background tasks that have no tool call or terminal on v2 now get a tool
call of their own. Before, they showed up as a `_claude/task` vendor
item. This follows the spec rule in
agentclientprotocol/agent-client-protocol#2786.

Examples: a workflow, or an MCP task that starts after its tool call
already returned, or a task a hook started.

- `BackgroundWork.adoptTasks` starts a tool call `task_<taskId>` for
each such task. It uses the task description as title, `kind: "other"`,
and `in_progress` or `paused`. A subagent's task goes on the subagent's
stream. The message loop sends these calls before `sync`, so a call is
always announced before the first `background` list names it. The same
path covers the `state_update` snapshot on `session/resume`.
- These calls are listed as `tool_call` items, so `tool_call/cancel`
stops the task. When the task ends they get a final status like any
other listed call (`completed`, `failed`, or `cancelled`).
- The `_claude/task` item type is gone. `BackgroundItem` is now
`terminal | tool_call | subagent`.
- Ambient tasks such as the monitor, and tasks whose own tool call is
still open, are unchanged.

`adoptTasks` is synchronous and returns the notifications, so the loop
only awaits when there is something to send. An extra await on every
pass broke the steering-cancel timing in `acp-agent.test.ts`.

## Compatibility

- ACP v1 clients, including JetBrains AIR's `_session/async_task/*`
extension, are unaffected. Background lists, and therefore this path,
only run when the v2 background sink exists and the session has
`perTaskStopAffordance`. On v1, `adoptTasks` returns `[]`.
- v2 clients that saw `_claude/task` items (v0.100.0 to v0.101.1) now
see `task_<id>` tool calls instead.

## Tests

`acp-v2.test.ts` replaces the `_claude/task` tests. New tests cover:

- starting and listing a call for a task that starts after its tool call
returned
- cancelling such a task with `tool_call/cancel`
- the call being sent before the `session/resume` snapshot
- a subagent's task appearing on the subagent's stream

`npm run check`, `build` and `test:run` pass.
2026-11-09 20:40:39 +01:00
Ben Brandt
766fd142f6 feat: send the checklist as a plan operation and remove plans on reset (#1554)
## Summary

Follow-up to the [plan operations
RFD](https://agentclientprotocol.com/rfds/plan-operations). The adapter
already sent approved ExitPlanMode plans as `markdown` plans to clients
with the `plan` capability, but two parts of the RFD were unused:

- **The checklist is now a plan operation on v1 too.** Clients with
`plan` (every v2 client, and v1 clients that opt in) get the
TodoWrite/Task checklist as `plan_update` with type `items` and the ID
`plan`. Before, v1 clients with the capability still got an ID-less
legacy `plan` next to the `plan-mode` plan. Clients without the
capability still get the legacy `plan`.
- **`plan_removed` is now sent.**
- An empty checklist sends `plan_removed` for `plan`. v2 clients used to
get a `plan_update` with no entries.
- A conversation reset (`/clear`) removes the checklist and the old
conversation's approved `plan-mode` plan. AIR keeps its approved plans,
which are joined to their approval cards by ID (#1469).

The new `src/plans.ts` builds the checklist updates. All three places
that send the checklist use it, and so does the v2 conversion of a
legacy `plan`, so v1 and v2 follow one rule.

The reset sends `plan_removed` for `plan-mode` even when the session
never published an approved plan. Tracking whether one was published
would be state that a restart loses, so the reset would behave
differently before and after one.

`file` plans are left out. A `file://` URI only works for a client on
the same machine, and v2 has no client filesystem.

## Tests

- `tools.test.ts`: TodoWrite with and without todos, and a TaskCreate
snapshot, for a v1 client with `plan`.
- `acp-agent.test.ts`: the reset test now runs for a client without plan
operations, a client with them, and AIR.
- `acp-v2.test.ts`: v2 end to end, TodoWrite then `/clear`, and an empty
legacy plan converting to `plan_removed`.
2026-11-09 14:58:36 +00:00
Ben Brandt
176ae0634c feat: list workflow phases and agents on the workflow card (#1552)
## Summary

Show the phases and agents of a running workflow on its ACP v2 tool
card, not only the current activity and totals.

Claude Code attaches a `workflow_progress` snapshot to some of a
workflow's `task_progress` events: every phase, and every agent with its
label, phase, state, tokens, tool calls, duration and last tool. #1550
deliberately left it out. This change renders it under the existing card
body:

```
**Count**
- two: done · 14,629 tokens · 0 tool calls · 1s
- three: running · Bash: echo 3

**Sum**
- not started
```

### Why an untyped field

The SDK (0.3.319) does not type `workflow_progress`. Workflow agents
also don't stream their messages through the root query, so this
snapshot is the only per-agent data the adapter gets.
`src/workflow-progress.ts` reads the shape the CLI sends and skips
entries that don't match it. If the CLI changes the format, the card
loses the list but still renders. The right long-term fix is for the SDK
to type this field, or to stream workflow agents with a parent id so
each one can become a child session.

### Behavior

- The snapshot is a full list, and the CLI leaves it out of some
reports. A report without one keeps the last list.
- A running agent shows its last tool (e.g. `Bash: echo 3`). A finished
one shows tokens, tool calls and duration.
- Agents with no known phase are listed first. A phase with no agent yet
shows `not started`. A retry shows its attempt. A state other than
`start`/`done` is shown as the CLI sends it.
- The existing duplicate suppression and the `session/resume` snapshot
cover the list too.
- AIR `async_task_progress` is unchanged. Like the rest of the card
progress from #1550, this only reaches v2 clients that get workflow
progress on the card.

Result previews, models and prompt previews are left out to keep the
card short.

## Validation

- Live probe on Claude Code 2.1.319 (SDK `query()`, two-phase
three-agent workflow, permissions limited to `Workflow` and one `echo
3`). It captured the snapshot shape used in the test doubles. Every
captured snapshot rendered through the built module as shown above. This
checked the protocol output, not the Delta UI.
- New v2 scenario built from the captured payload, plus unit cases in
`src/tests/workflow-progress.test.ts`.
- `npm run check`, `npm run build`, `npm run test:run` (3,691 passed, 30
skipped).
2026-11-09 14:49:48 +00:00
dependabot[bot]
fcb7e175ee ci(deps): Bump amannn/action-semantic-pull-request from 6.1.1 to 6.1.2 (#1551)
Bumps
[amannn/action-semantic-pull-request](https://github.com/amannn/action-semantic-pull-request)
from 6.1.1 to 6.1.2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/amannn/action-semantic-pull-request/releases">amannn/action-semantic-pull-request's
releases</a>.</em></p>
<blockquote>
<h2>v6.1.2</h2>
<h2><a
href="https://github.com/amannn/action-semantic-pull-request/compare/v6.1.1...v6.1.2">6.1.2</a>
(2026-11-05)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>Release accumulated dependency updates (closes <a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/321">#321</a>)
(<a
href="fc7da0cece">fc7da0c</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/amannn/action-semantic-pull-request/blob/main/CHANGELOG.md">amannn/action-semantic-pull-request's
changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<h2><a
href="https://github.com/amannn/action-semantic-pull-request/compare/v6.1.1...v6.1.2">6.1.2</a>
(2026-11-05)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>Release accumulated dependency updates (closes <a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/321">#321</a>)
(<a
href="fc7da0cece">fc7da0c</a>)</li>
</ul>
<h2><a
href="https://github.com/amannn/action-semantic-pull-request/compare/v6.1.0...v6.1.1">6.1.1</a>
(2025-08-22)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>Parse <code>headerPatternCorrespondence</code> properly (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/295">#295</a>)
(<a
href="800da4c97f">800da4c</a>)</li>
</ul>
<h2><a
href="https://github.com/amannn/action-semantic-pull-request/compare/v6.0.1...v6.1.0">6.1.0</a>
(2025-08-19)</h2>
<h3>Features</h3>
<ul>
<li>Support providing regexps for types (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/292">#292</a>)
(<a
href="a30288bf13">a30288b</a>)</li>
</ul>
<h3>Bug Fixes</h3>
<ul>
<li>Remove trailing whitespace from &quot;unknown release type&quot;
error message (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/291">#291</a>)
(<a
href="afa4edb1c4">afa4edb</a>)</li>
</ul>
<h2><a
href="https://github.com/amannn/action-semantic-pull-request/compare/v6.0.0...v6.0.1">6.0.1</a>
(2025-08-13)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>Actually execute action (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/289">#289</a>)
(<a
href="58e4ab40f5">58e4ab4</a>)</li>
</ul>
<h2><a
href="https://github.com/amannn/action-semantic-pull-request/compare/v5.5.3...v6.0.0">6.0.0</a>
(2025-08-13)</h2>
<h3>⚠ BREAKING CHANGES</h3>
<ul>
<li>Upgrade action to use Node.js 24 and ESM (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/287">#287</a>)</li>
</ul>
<h3>Features</h3>
<ul>
<li>Upgrade action to use Node.js 24 and ESM (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/287">#287</a>)
(<a
href="bc0c9a79ab">bc0c9a7</a>)</li>
</ul>
<h2><a
href="https://github.com/amannn/action-semantic-pull-request/compare/v5.5.2...v5.5.3">5.5.3</a>
(2024-06-28)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>Bump <code>braces</code> dependency (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/269">#269</a>.
by <a href="https://github.com/EelcoLos"><code>@​EelcoLos</code></a>)
(<a
href="2d952a1bf9">2d952a1</a>)</li>
</ul>
<h2><a
href="https://github.com/amannn/action-semantic-pull-request/compare/v5.5.1...v5.5.2">5.5.2</a>
(2024-04-24)</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="2252a14e81"><code>2252a14</code></a>
chore: Release 6.1.2 [skip ci]</li>
<li><a
href="af97545064"><code>af97545</code></a>
ci: pin conventional-changelog-conventionalcommits to v9 (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/323">#323</a>)</li>
<li><a
href="fc7da0cece"><code>fc7da0c</code></a>
fix: Release accumulated dependency updates (closes <a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/321">#321</a>)</li>
<li><a
href="4e8becf171"><code>4e8becf</code></a>
ci: bump the npm-packages group with 10 updates (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/319">#319</a>)</li>
<li><a
href="b3fa6edcb1"><code>b3fa6ed</code></a>
ci: bump actions/setup-node from 6 to 7 in the github-action-workflows
group ...</li>
<li><a
href="2c9480cf28"><code>2c9480c</code></a>
chore: bump the npm-packages group with 12 updates (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/315">#315</a>)</li>
<li><a
href="58e675f6ba"><code>58e675f</code></a>
ci: bump the github-action-workflows group with 2 updates (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/313">#313</a>)</li>
<li><a
href="45b9ed7cf2"><code>45b9ed7</code></a>
chore: bump the npm-packages group with 12 updates (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/308">#308</a>)</li>
<li><a
href="9d28649546"><code>9d28649</code></a>
ci: Bump the github-action-workflows group with 2 updates (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/309">#309</a>)</li>
<li><a
href="ac7e3fc035"><code>ac7e3fc</code></a>
docs: Fix snippet (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/306">#306</a>)</li>
<li>Additional commits viewable in <a
href="48f256284b...2252a14e81">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=amannn/action-semantic-pull-request&package-manager=github_actions&previous-version=6.1.1&new-version=6.1.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

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

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

---

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

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


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-09 15:39:29 +01:00
acp-release-bot[bot]
e9e0d414fe chore(main): release 0.101.1 (#1539)
🤖 I have created a release *beep* *boop*
---


##
[0.101.1](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.101.0...v0.101.1)
(2026-11-09)


### Bug Fixes

* keep autonomous turn tasks open across owed trailing idles
([#1455](https://github.com/agentclientprotocol/claude-agent-acp/issues/1455))
([1ee2422](1ee2422934))
* list a resumed Fable 5.1 session's model as `fable`
([#1548](https://github.com/agentclientprotocol/claude-agent-acp/issues/1548))
([4cceb06](4cceb06b31))
* list a subagent's background Bash commands
([#1540](https://github.com/agentclientprotocol/claude-agent-acp/issues/1540))
([6597626](6597626d86))
* list Monitor calls in the session's background work
([#1538](https://github.com/agentclientprotocol/claude-agent-acp/issues/1538))
([ba16234](ba162346c9))
* relay a rate_limit_event that arrives before the turn's first
assistant frame
([#1534](https://github.com/agentclientprotocol/claude-agent-acp/issues/1534))
([54392e1](54392e144d))
* render workflow tool cards and progress correctly in ACP v2
([#1550](https://github.com/agentclientprotocol/claude-agent-acp/issues/1550))
([30600ba](30600ba348))
* replay a compaction summary by its flag, not its framing
([#1536](https://github.com/agentclientprotocol/claude-agent-acp/issues/1536))
([aa0c123](aa0c1234b5))
* report setup-token logins as logged in
([#1376](https://github.com/agentclientprotocol/claude-agent-acp/issues/1376))
([7d1ef76](7d1ef76c1d))
* start transcript notices on their own line for clients without
session.notices
([#1362](https://github.com/agentclientprotocol/claude-agent-acp/issues/1362))
([47b64c0](47b64c04de))
* **test:** assert the blockquoted MCP startup transcript line
([#1543](https://github.com/agentclientprotocol/claude-agent-acp/issues/1543))
([3b22586](3b22586cf6))
* update to @agentclientprotocol/sdk 2.5.0
([#1549](https://github.com/agentclientprotocol/claude-agent-acp/issues/1549))
([ef65ab3](ef65ab31b4))
* update to @anthropic-ai/claude-agent-sdk 0.3.319
([#1545](https://github.com/agentclientprotocol/claude-agent-acp/issues/1545))
([a58293f](a58293f4c9))

---
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-11-09 14:03:18 +00:00
Ben Brandt
30600ba348 fix: render workflow tool cards and progress correctly in ACP v2 (#1550)
## Summary

Fix workflow tool cards that remain pending indefinitely, and surface
workflow progress through standard ACP v2 updates.

### Prevent orphan preview cards

Claude Code invokes Edit/Write preview hooks for workflow agents without
streaming those tools' uses and results through the root query.
Publishing a card from the hook alone leaves a pending call with no
completion path.

Stage early preview patches until the stream or a permission request
publishes the actual tool call. Keep patches pinned against later input
refinements, bound unclaimed previews, and clear them on teardown. A
call's own content takes precedence over a staged preview.

### Show workflow progress in place

Forward the SDK's typed task-progress fields to the existing workflow
card: current activity, summary, last activity, token count, tool-call
count, and elapsed time. Preserve progress arriving before the launch
result, suppress duplicate display updates, and include the latest
progress on session resume. Progress does not change paused/running
state or reopen completed calls.

This reads no workflow transcripts, journals, or metadata files and
introduces no ACP extension. The CLI also emits an untyped
`workflow_progress` snapshot; consuming that richer phase/agent data is
deliberately left out of this change. Existing v1 AIR progress uses the
same field normalization as before.

## Validation

- Regression reproduced before the orphan-preview fix.
- Nine relevant Vitest suites: **1,972 passed, 10 skipped**, including
v1/v2 scenarios, preview ordering, progress-before-launch, idle and
paused updates, duplicate suppression, resume snapshots, and late
progress after completion.
- `npm run check`
- `npm run build`
- `git diff --check`
- Live Claude Code **2.1.319** probes: captured SDK workflow events and
ran a real workflow through the updated ACP v2 adapter. One card
received two progress updates and then reached `completed`. This
verified protocol updates, not the Delta UI.

Rebased onto current `main` and reran the automated checks before
opening the PR.
2026-11-09 14:59:02 +01:00
Mark Tkachenko
dd9eed4e46 test: pin runtime exports of lib.js and acp-agent.js (#1546)
Adds a test that pins the runtime export names of `lib.js` and
`acp-agent.js`.

`acp-agent.ts` is about to be split into smaller modules in a series of
pure-move PRs. Each move keeps the old exports as re-exports, because
`"exports": { "./*": "./*" }` lets consumers deep-import
`dist/acp-agent.js`. tsc only catches a missing export that something in
the repo imports; this test catches the rest. The list should change
only on purpose.
2026-11-09 14:07:08 +01:00
Ben Brandt
4cceb06b31 fix: list a resumed Fable 5.1 session's model as fable (#1548)
## Problem

The Claude Code CLI gives Fable 5.1 a different option value depending
on how the session starts:

- **New session:** listed as `fable` (`resolvedModel:
"claude-fable-5-1"`).
- **Resumed session whose transcript last ran Fable 5.1:** the same row
is listed as `claude-fable-5-1`, and there is no `fable` row.

Reproduced with CLI 2.1.310. Other families don't do this: a session
resumed on `claude-opus-5-5` still lists `opus`.

Clients store the option value as the session's choice. A choice saved
in a new session (`fable`) is not among the options of a resumed one,
and the reverse. A client that checks its saved value against the
options then refuses to apply it, and the model choice keeps flipping
between the two ids.

## Fix

`aliasResumedModelRow` in `session-model.ts` gives the resumed model's
row its family alias back. It changes a row only when:

- the row's value is the model read from the resumed transcript, equals
its own `resolvedModel` and has no context hint,
- no row already has the alias, and
- no newer version of the family is listed, since the alias runs the
family's newest model.

`createSession` applies it before the allowlist and model selection, so
new, resumed and loaded sessions over ACP v1 and v2 all get the same
list.

This is a workaround for the CLI listing the row under a different
value. The CLI also lists rows this way when `ANTHROPIC_MODEL` or
`settings.model` pins `claude-fable-5-1`. Those rows keep the pinned id
on purpose: relabelling them would turn the user's pinned version into
the alias, which moves to a newer Fable when one ships.

## Tests

- `acp-agent-settings.test.ts`: a session resumed on `claude-fable-5-1`
offers `fable` as the current value and option, with no `setModel` call.
Fails without the fix.
- `model-resolution.test.ts`: unit cases for the relabel and for each
case that keeps the list as it is.
- Checked end to end with the real CLI: a Fable turn, then
`session/load`, reports `fable`.
2026-11-09 13:47:11 +01:00
Ben Brandt
ef65ab31b4 fix: update to @agentclientprotocol/sdk 2.5.0 (#1549)
Bumps `@agentclientprotocol/sdk` from 2.4.0 to 2.5.0 (exact pin).

2.5.0 updates the SDK to ACP schema v1.30.0 and v2.0.0-alpha.14
([release
notes](https://github.com/agentclientprotocol/typescript-sdk/releases/tag/v2.5.0)).
No source changes were needed; new schema fields and capabilities are
not adopted here.

## Testing

`npm run build`, `npm run check` and `npm run test:run` pass. `npm run
test:integration` was not run.
2026-11-09 12:43:12 +00:00
Ben Brandt
a58293f4c9 fix: update to @anthropic-ai/claude-agent-sdk 0.3.319 (#1545)
Updates the Claude Agent SDK from 0.3.318 to 0.3.319, which bundles
Claude Code 2.1.319. Also updates `@anthropic-ai/sdk` (1.4.0 → 1.6.0)
and the transitive dependencies in the lockfile (`@babel/runtime`,
`@eslint-community/regexpp`, `@rolldown/pluginutils`, `hono`, `jose`,
`vite`).

## Reviewed, no change needed

- **`mcp_task` background tasks:** SDK sessions get these only with
`CLAUDE_AUTO_BACKGROUND_TASKS=1` and streaming input. The adapter does
not set that variable, so they don't occur by default. If a user sets
it, the Async Tasks panel shows the raw `mcp_task` type, and the
`resource_links` on the task notification are not forwarded. Left as a
possible follow-up.
- **`deleteSession()` rejects with `ENOTEMPTY`** when the session folder
is only partly removed. `session/delete` already rethrows any error
other than "not found".
- **`session_writes_lost`** is now sent only when the CLI exits 0. The
adapter only logs it.
- **`permissions.denyInsteadOfPrompting`:** with it set, the CLI denies
instead of calling `canUseTool`. That is the setting's intent.
- **`mcp_call.check_permissions`, `set_has_subscribers`,
`disableWindowsBashCommandSplit`:** the adapter does not use these.
- The comments on the `getSettings` and `unanswered` type workarounds
now name 0.3.319; both are still missing from the SDK's types.

## Not updated

TypeScript stays on 6.0.3: typescript-eslint 8.72.1 requires `typescript
<6.1.0`, so 7.0.2 has to wait for a typescript-eslint release that
supports it.

## Testing

`npm run build`, `npm run check` and `npm run test:run` pass. `npm run
test:integration` was not run.
2026-11-09 08:56:37 +00:00
PerryLink
3b22586cf6 fix(test): assert the blockquoted MCP startup transcript line (#1543)
`#1362` moved every transcript fallback for clients without
`session.notices` into its own blockquote paragraph (`\n\n> …\n\n`) and
updated the existing assertions, but missed the MCP server startup one.
`main` has been red since that merge:

- The `Build` job on `47b64c0` fails with an annotation at
`src/tests/acp-agent.test.ts` line 13860:

  ```
AssertionError: expected '\n\n> **MCP server "client-fs" failed…' to be
'**MCP server "client-fs" failed to st…'
  ```

- The neighbouring fallback tests already assert the blockquote form,
e.g. `"\n\n> **Warning:** hook says no\n\n"` and the two-line `Stop
says:` message.

This updates the expected string to the blockquoted text; product code
is untouched.

## Validation

- `npx vitest run src/tests/acp-agent.test.ts -t "falls back to a
transcript line without the notices capability"` → 1 passed
- `npx vitest run src/tests/acp-agent.test.ts` → 759 passed | 9 skipped
- `npm run check` → passes
2026-11-08 07:57:15 +00:00
alexwan
47b64c04de fix: start transcript notices on their own line for clients without session.notices (#1362)
## What

When a client does not advertise `session.notices`, a notice falls back
to an `agent_message_chunk` like `**Notice:** …`. Clients render
consecutive chunks as one markdown stream, so the fallback is glued to
Claude's last sentence. In Zed 1.24 a Stop hook's `systemMessage` shows
up as:

```
…推上去。**Notice:** Stop says: token: 3 calls
Stop says: hand off now
```

The fallback is now its own blockquote paragraph, with blank lines on
both sides:

```
…推上去。

> **Notice:** Stop says: token: 3 calls
> hand off now
```

## Behavior

- `noticeOrTranscriptUpdate` wraps the transcript text with
`transcriptBlock`: a leading blank line so it starts on a new line, `> `
on every line, and a trailing blank line so Claude's next chunk does not
continue the quote. This covers every transcript fallback (hook notices,
"Task stopped by user", "Fast mode turned off", "Auto mode
unavailable").
- Claude Code prefixes every line of a hook's `systemMessage` with
`<HookEvent> says: `. The informational fallback keeps that prefix on
the first line only, and only when every line repeats the same prefix
(`collapseHookSpeaker`).
- Clients that advertise `session.notices` get the same `notice` update
as before.
- `Turn.noticeTexts` still records `message.content` unchanged, so the
hook-blocked result de-duplication keeps working.

## Tests

- New unit tests for `transcriptBlock` and `collapseHookSpeaker`, plus
an end-to-end test for a two-line `notice`-level hook message.
- Updated the existing transcript fallback assertions to the blockquote
form.
- `npm run check`, `npm run build` and `npm run test:run` pass (3111
tests).

Co-authored-by: alex <hongchang.wan@longbridge-inc.com>
2026-11-06 22:14:44 +00:00
Jonathan Ellis
1ee2422934 fix: keep autonomous turn tasks open across owed trailing idles (#1455)
When Claude Code starts a new autonomous cycle before the previous
cycle's trailing idle arrives, that late idle completes the new cycle's
AIR async task while Claude is still working. The next output then
creates a second task for the same cycle.

This change completes the autonomous task inside the existing cycle-end
branch, which already checks that `owedTrailingIdles` is zero. An owed
idle is now only consumed by the existing accounting and leaves the
current task open. The task still completes at its own terminal result
or at an unowed idle, so the task and the cycle end on the same
decision.

It adds a scripted SDK-stream regression for two consecutive autonomous
cycles with no running transition between them. The test checks that the
late idle leaves the second task open and that later output stays on the
same task, for a cycle that ends at a result and for one that ends at an
unowed idle. Both cases fail before the fix and pass after it.

Fixes #1453.

Validation, rebased on 0.98.0 (c90f856):
- `npm run build` and `npm run check` pass.
- `npm run test:run`: 3,378 passed, 32 skipped. One unrelated test,
`tools.test.ts` "picks up a rewrite that restored the modification time
of the file", failed once in the full run on a heavily loaded machine.
It passed 3 of 3 runs alone, both on this branch and on `main`, and this
change does not touch `tools.ts` or its tests.

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 22:11:36 +00:00
Kerekes Dániel
7d1ef76c1d fix: report setup-token logins as logged in (#1376)
Fixes #1295

With a `claude setup-token` login (`CLAUDE_CODE_OAUTH_TOKEN`), `claude
auth status --json` only returns `{"loggedIn": true, "authMethod":
"oauth_token", "apiProvider": "firstParty"}`. `fromCliStatus()` handed
that to `mapAuthFields()`, which has no plan or key fields to go on and
fell back to "Not logged in". Clients then showed a working session as
logged out.

This maps that exact state to `kind: "account"` with the label suggested
in the issue ("Claude subscription", detail "OAuth token"). Everything
else still goes through `mapAuthFields()`.

Added a case to `auth-status.test.ts` that fails without the change.
`npm run check`, `npm run build` and `npm run test:run` pass.
2026-11-06 23:10:04 +01:00
Hussain Hammad
aa0c1234b5 fix: replay a compaction summary by its flag, not its framing (#1536)
Fixes #1535

A compaction summary on replay was only sent as a `compaction_update` if
its text also parsed, so a change in Claude Code's framing turned it
into a `user_message_chunk`, shown as text the user typed. That is what
happens to summaries from Claude Code 2.1.312 and 2.1.316.

`isCompactSummary` now decides on its own. A summary whose framing isn't
recognized is sent as a completed `compaction_update` without summary
text. This reverses the choice to keep such text visible: the
model-facing framing isn't kept as a summary either way, and showing it
as the user's words is the worse failure.

`PERSISTED_PREAMBLE` also allows content before the `Summary:` line, so
current summaries keep their text. All 31 summaries in my own
transcripts now parse, and a real `session/load` on one from 2.1.316
sends a `compaction_update` with its text and no summary
`user_message_chunk`.
2026-11-06 22:09:30 +00:00
Alexis Rouillard
54392e144d fix: relay a rate_limit_event that arrives before the turn's first assistant frame (#1534)
## Problem

`rate_limit_event` is relayed to the client as
`usage_update._meta["_claude/rateLimit"]`, but only when
`lastAssistantTotalUsage !== null`. With partial messages on, the SDK
emits `rate_limit_event` before the turn's first `assistant` message,
while `lastAssistantTotalUsage` is still null (reset at each turn). The
SDK emits it once per turn, so in practice clients never receive
`_claude/rateLimit`.

Observed with a real subscription session (SDK 0.3.316): the SDK sends
`stream_event`s, then `rate_limit_event`, then the first `assistant`.
The adapter dropped the event and sent only `usage_update`s with
`_claude/model`.

## Fix

When the event arrives before the turn has any usage, keep its
`rate_limit_info` and attach it to the next `usage_update`. That happens
in `attachUsageModel`, which every `usage_update` already goes through.
A rate limit that the update already carries wins over the pending one.
When usage is already known, the event is sent right away, as before.

## Tests

- New test in `acp-agent.test.ts`: a `rate_limit_event` before the first
assistant frame ends up on the turn's first `usage_update`, and on no
later one. It fails without the fix.
- `npm run check`, `npm run build` and `npm run test:run` pass.
- Checked by hand over stdio with a real session: the turn's first
`usage_update` now carries `_claude/rateLimit`, including
`unifiedWindows.seven_day`.
2026-11-06 22:07:33 +00:00
Ben Brandt
6597626d86 fix: list a subagent's background Bash commands (#1540)
Claude Code sends no `tool_use_result` with a subagent's tool results.
The adapter recognizes a background Bash command only from the
`backgroundTaskId` in that structured result, so it never recognized one
that a subagent started. The command's terminal was reported exited
(code 0) as soon as the result came, while the command kept running, and
the command was missing from the subagent's `state_update.background`.

The real SDK stream (CLI 2.1.318) for a subagent running `sleep 30` in
the background:

```
task_started  task_id=beure6fer task_type=local_bash tool_use_id=toolu_018M… owned_by_subagent=true is_backgrounded=true
user          parent_tool_use_id=toolu_01HY… tool_result for toolu_018M…, no tool_use_result
```

The local transcripts agree: none of the 35 subagent background Bash
results across CLI 2.1.293 to 2.1.318 has a `toolUseResult`.

When a Bash result has no structured task id, the adapter now uses the
live background `local_bash` task whose `task_started` named that Bash
call. The existing subagent test now sends the stream the CLI sends. It
fails without the fix.
2026-11-06 19:12:42 +01:00
Ben Brandt
ba162346c9 fix: list Monitor calls in the session's background work (#1538)
Claude Code marks every live-update watcher `ambient`, "requested or
auto-started" per the SDK's `background_tasks_changed` docs, so the task
of a `Monitor` call the model made is ambient too. `background-work.ts`
left out every ambient task, so a running Monitor was never listed:

- its tool call went straight to `completed` instead of staying
`in_progress` until its task ends;
- a client couldn't stop it with `tool_call/cancel`;
- a client deciding whether a session is quiet, for example before
restarting or releasing it, saw no background work while the Monitor
kept starting turns.

Seen live in Delta against 0.101.0: a Monitor watching a loop kept
waking the agent every 20 seconds while every `state_update` reported
`background: []`.

## Change

- `toolResults` registers a task a tool call started even when it is
ambient, so the call stays `in_progress` and advertises `cancel`.
- The list builder lists a registered call's task before skipping
ambient tasks. Ambient tasks without a call of their own, such as
watchers Claude Code starts by itself, and ambient terminals stay out.

## Tests

- The Monitor fixture in `acp-v2.test.ts` now carries `ambient: true`,
as the CLI sends it. Without the fix, the four existing Monitor tests
fail with it.
- The `_claude/task` test is now an `it.each`: a non-ambient task
without a call is listed, and an ambient watcher without a call is not.
- `npm run check`, `npm run build`, `npm run test:run` pass. Not run:
`npm run test:integration` against the real CLI.
2026-11-06 12:12:58 +01:00
acp-release-bot[bot]
85aec26ef9 chore(main): release 0.101.0 (#1516)
🤖 I have created a release *beep* *boop*
---


##
[0.101.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.100.0...v0.101.0)
(2026-11-05)


### Features

* compact idle sessions before the prompt cache expires
([#1522](https://github.com/agentclientprotocol/claude-agent-acp/issues/1522))
([a79d073](a79d07324d)),
closes
[#1454](https://github.com/agentclientprotocol/claude-agent-acp/issues/1454)
[#1518](https://github.com/agentclientprotocol/claude-agent-acp/issues/1518)
* ship single-file executables built with bun
([#1524](https://github.com/agentclientprotocol/claude-agent-acp/issues/1524))
([e3f0b6c](e3f0b6c89c))
* start sessions in Auto mode by default
([#1533](https://github.com/agentclientprotocol/claude-agent-acp/issues/1533))
([7ea9eec](7ea9eec839))
* stream the output of v2 background commands
([#1512](https://github.com/agentclientprotocol/claude-agent-acp/issues/1512))
([53e3eec](53e3eec013))


### Bug Fixes

* honor configured fallback for unavailable restored modes
([#1520](https://github.com/agentclientprotocol/claude-agent-acp/issues/1520))
([8bbdea6](8bbdea6cc6))
* update to @anthropic-ai/claude-agent-sdk 0.3.318
([#1525](https://github.com/agentclientprotocol/claude-agent-acp/issues/1525))
([1d4418f](1d4418f80d))

---
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-11-05 22:07:42 +01:00
Ben Brandt
7ea9eec839 feat: start sessions in Auto mode by default (#1533)
Claude Code now starts interactive terminal and VS Code sessions in Auto
(v2.1.283+). Agent SDK runs with no `permissionMode` still start in
Manual ([docs](https://code.claude.com/docs/en/permission-modes)). Until
now the adapter followed the SDK default. ACP clients are interactive
editors, so this PR makes them start like the VS Code extension.

## How a session now starts

- **No mode configured:** with no `permissions.defaultMode`, or an
invalid one that settings resolution drops, a new session starts in
`auto`.
- **Model without Auto support:** that built-in Auto default starts in
Manual (`default`) with no warning, as Claude Code does. A user who
explicitly sets `"auto"` still gets the existing fallback to
`acceptEdits` with a warning.
- **`disableAutoMode: "disable"`:** the adapter ignored this setting
before. Now, set at the top level or under `permissions`, it removes
Auto from the mode list, the plan-exit options and mode switching, and
the session starts in `default`. The bundled CLI reads both spellings.
- **Unchanged:** an explicit `permissionMode`, a resumed transcript's
mode and a configured `defaultMode` behave as before.

## Limitations

- **HIPAA:** Claude Code starts organizations with the HIPAA
configuration in Manual. The SDK doesn't report that configuration (it
is not in `AccountInfo`, the `init` message or the model list), so those
sessions start in Auto here. This is noted in a code comment.
- **Mid-session setting changes:** like the bypass setting,
`disableAutoMode` is read when the session is created.

## Tests

- Updated the tests that expected the old default.
- Added cases for the built-in default, the Manual fallback on a model
without Auto, and both spellings of `disableAutoMode`.
- Regenerated the scenario snapshots for ACP v1 and v2. The 4
`origin-main` baseline files change only the mode value, from
`"default"` to `"auto"`.
2026-11-05 12:01:11 +00:00
Mark Tkachenko
a79d07324d feat: compact idle sessions before the prompt cache expires (#1522)
Claude Code compacts a large idle session shortly before its one-hour
prompt cache expires, but only in attended sessions. SDK sessions
(entrypoint `sdk-ts`) never arm that timer, so every return after an
hour re-writes the whole context to the cache.

The adapter now runs its own timer: 54 minutes after the last
main-thread request, if that request used the 1h cache, the context is
at least 200k tokens and the session is idle, it sends `/compact`
through the normal prompt path. It respects `idleCompaction: false`,
`autoCompactEnabled: false` and `DISABLE_COMPACT` /
`DISABLE_AUTO_COMPACT`. AIR gets `trigger: "idle"` in the compaction
`_meta`.

Closes #1454
Closes #1518
2026-11-04 20:04:11 +01:00
Ben Brandt
1d4418f80d fix: update to @anthropic-ai/claude-agent-sdk 0.3.318 (#1525)
Updates the Claude Agent SDK from 0.3.316 to 0.3.318, which bundles
Claude Code 2.1.318. Also updates `@anthropic-ai/sdk` (1.3.0 → 1.4.0),
`vitest` (5.0.5 → 5.1.0) and the transitive dependencies in the
lockfile.

## Changes from the changelogs

- **`session_writes_lost` (SDK 0.3.317):** a new system message that the
CLI sends as it exits, after the last result, when a full disk or
used-up quota kept it from saving the transcript or `~/.claude.json`. No
turn is open to carry a `session/update` then, so the adapter logs it,
as it does `mirror_error`. Without a case for it, the build fails on the
exhaustive switch.
- **TaskCreate `task.description` (SDK 0.3.317):** the plan now takes a
task's subject and description from the TaskCreate result, which holds
the task as saved. It differs from the input when Claude Code corrects a
malformed call or a PreToolUse hook changes it. This applies to the live
stream and to `session/load` replay.

## Reviewed, no change needed

- New `startup_failure_reason` values (`config_file_invalid`,
`initialize_not_first`) already reach the client as an internal error
with the CLI's own message.
- `onFailure: "block"` on hook matchers: none of the adapter's hooks
guard permissions, and a failed Edit/Write preview must not block the
edit.
- Removed `file_unchanged` Read output, optional `status` in
`PushNotificationInput`, and Grep/Glob `timedOut`: nothing in the
adapter reads these.
- `workingDirectoriesAsGiven`: it makes Claude Code not follow links at
the listed working directories. Listing the ACP `cwd` would put every
file of a symlinked workspace outside the working directories. Hosts
that want it can already pass it through
`_meta.claudeCode.options.settings`.
- The comments on the `getSettings` and `unanswered` type workarounds
now name 0.3.318; both are still missing from the SDK's types.

## Not updated

TypeScript stays on 6.0.3: typescript-eslint 8.72.1 requires `typescript
<6.1.0`, so 7.0.2 has to wait for a typescript-eslint release that
supports it.

## Testing

`npm run build`, `npm run check` and `npm run test:run` pass. New tests
cover both changes. `npm run test:integration` was not run.
2026-11-04 12:50:19 +00:00
Ben Brandt
21d24f9e8b ci: build the executables with the latest Bun (#1526)
Follow-up to #1524. The last commit on that branch didn't make it into
the merge.

- `binaries.yml` installs `bun-version: latest` instead of pinning
`1.4.4`. The compiled executable contains the Bun runtime, so the pin
also fixed the runtime we ship. But nothing checks for new Bun releases,
so the pin would just go stale. Each build is smoke-tested on its own
platform before it uploads, so a Bun release that breaks the executable
fails that job instead of shipping. npm is unaffected, and a re-run
fixes it once Bun does. If a Bun version ever regresses past the smoke
test, we pin it then.
- Each build job writes `Built <target> with Bun x.y.z` to its step
summary, so it's visible which Bun built each release's archives.
- Re-wraps an overlong line in the file's header comment.

`ci:` because nothing published changes until the next release builds
with it.
2026-11-04 12:50:03 +00:00
Ben Brandt
e3f0b6c89c feat: ship single-file executables built with bun (#1524)
Brings back the standalone executables that #525 and #572 turned off.
npm publishing is unchanged. Each stable release also attaches 8
archives (Linux glibc and musl, macOS, Windows, on x64 and arm64) to its
GitHub release.

## Why it works now

The old executables ran the SDK's JS CLI from inside Bun's `$bunfs`, and
it couldn't reach its `vendor/` files there. The SDK's CLI is now a
native binary shipped in a per-platform npm package. So each archive
ships that binary as `claude` next to `claude-agent-acp`, and the agent
runs it from its own directory. Nothing is embedded or extracted, and
the agent has no `node_modules` to resolve.

The first commit embedded the CLI and extracted it with the SDK's
`@anthropic-ai/claude-agent-sdk/extract` helper. That works, but the OS
can only run a real file, and Bun-embedded files only exist inside the
agent process. So it wrote a second ~300 MB copy to `/tmp` on first run,
once per version. Shipping the CLI next to the executable avoids that,
and the download size is the same (~180 MB `.tar.gz`).

Claude Code's helper tools work too. Native Claude Code has no Grep or
Glob tools: its Bash shell wraps `grep` and `find` in functions that run
the ugrep and bfs built into the binary at `$CLAUDE_CODE_EXECPATH`. That
variable points at the `claude` shipped next to the agent.

## Changes

- `src/claude-cli.ts` now owns `claudeCliPath()`, moved out of
`acp-agent.ts`. `useBundledClaudeCli(dir)` makes it return
`<dir>/claude[.exe]`. If that file is missing, it throws an error saying
to keep `claude` next to `claude-agent-acp` or set
`CLAUDE_CODE_EXECUTABLE`; before, the user got a bare ENOENT.
`CLAUDE_CODE_EXECUTABLE` still wins.
- `src/binary-entry.ts` is the executable's entry. It registers the
directory of `realpath(process.execPath)`, so a symlink in
`/usr/local/bin` works, then imports the agent. It's left out of the npm
tarball.
- `scripts/build-binary.mjs` (`npm run build:binary`) runs `bun build
--compile` for a target and copies that target's `claude` next to the
output.
- `scripts/smoke-binary.mjs` (`npm run smoke:binary`) runs:
  - `--version` and `--cli --version`
  - a symlinked executable
  - the missing-`claude` error
  - `initialize` over v1 and v2
- the `terminal-auth` login command, run with `--version` (checks that
no `$bunfs` path leaks into it)

  With `--full` it also drives a real session:
- Bash `grep`/`find`, checking that they ran Claude's embedded ugrep/bfs
  - Read on text and on a PNG
  - Edit
  - a subagent
  - `session/cancel` during a running command
  - `session/list`
  - `session/load` in a new process
  - `session/fork` and `session/delete`
- `src/agent-command.ts` builds the `terminal-auth` login command, which
reruns the agent. Under Node that's `node <script> ...flags`. In a
compiled executable, `argv[1]` is a path inside the executable
(`/$bunfs/root/claude-agent-acp`), so it's dropped. Passing it back, as
the old `argv.slice(1)` did, handed it to the Claude CLI as an argument
and broke `auth login`.
- `.github/workflows/binaries.yml` builds and smoke-tests each target on
its own platform's runner; the musl targets are tested in an Alpine
container. CI runs it on every PR, using the basic smoke test, since CI
has no Claude login. On a stable release, `publish.yml` attaches the
archives (same names as before). These jobs don't gate the npm publish,
and previews get no executables. On stable releases the registry
dispatch waits for `upload-binaries`, so the archives (and the digests
the registry reads) exist first. It only waits: a failed upload doesn't
stop the dispatch.

## Testing

I also ran the whole unit suite on Bun's runtime (`bun --bun vitest
run`). 3,573 of 3,575 tests pass. The two failures are test artifacts:
Bun's `os.homedir()` ignores a `HOME` changed at runtime, and one
steer-and-cancel test asserts before the mock delivers its last idle. An
idle-accounting trace is identical on both runtimes.


On macOS arm64 with Bun 1.4.4, `smoke:binary --full` passes all checks.
`CLAUDE_CODE_EXECPATH` pointed at the shipped `claude`, and nothing was
written to the temp directory. The basic smoke test also passes from an
unpacked archive outside the repo with `PATH=/usr/bin:/bin`, and `node
dist/index.js` passes the full run. On Linux, Windows and Intel macOS,
only the basic smoke test runs, in this PR's CI.
2026-11-04 11:00:25 +00:00
Slapoguzov Alexander
8bbdea6cc6 fix: honor configured fallback for unavailable restored modes (#1520)
A restored transcript mode such as `dontAsk` is recognized by the Claude
SDK but absent from the ACP mode catalog. It previously reached
initialization and was forced to Manual, skipping a supported
`permissions.defaultMode` from settings.

Use the same catalog for restored-mode selection and initialization.
Supported restored modes retain precedence; an unavailable restored mode
falls back to an available configured mode, then Manual. Explicit
initial modes, bypass restrictions, model-specific Auto handling, and
new-session configuration behavior remain unchanged.

Validation: TypeScript build, ESLint, Prettier, and 243 focused tests
pass. Added regressions cover helper selection plus v1 and v2 responses;
nine checks fail against unchanged production code.

The full local suite **FAILED**: 3,549 passed, 20 failed, 30 skipped.
Four selected failures also reproduce on unchanged base: three
session-index cases (watcher setup, live notification, SDK path-list
parity) and one background-work case. The remaining 16 failures were not
individually classified. No test timeouts or product behavior were
changed to suppress them. Live CLI and IDE permission-enforcement
acceptance remain unverified.

Co-authored-by: Codex <codex@openai.com>
2026-11-03 10:40:14 +01:00
Ben Brandt
1e986b526d refactor: give each session one owner of its background work (#1517)
Follow-up to #1510, #1511 and #1512. Clients receive exactly what they
did before.

## Why

The v2 background work grew as about 14 free functions taking `(session,
sessionId, …)` and reaching into six `Session` fields. Four of them it
kept itself: the task set, terminals, tool calls, and
`reportsBackground`. Three belong to other modules:
`liveBackgroundTasks`, the subagent map, and `toolUseCache`. The repo
guidelines ask for one module that owns the state.

## What changed

- **One owner per session.** `SessionBackgroundWork`, stored on
`Session.background`, owns the task set, terminals, tool calls, output
tails, and whether the client gets the lists. It has one method per
event: a Bash result, tool results, a task's end or pause, a new task
set, and a closed query.
- **The reporter only filters.** The object builds each session's
report, so `BackgroundWorkReporter` only drops unchanged lists and
forwards the rest.
- **No reference back to the session.** A recreated session (sign-out
respawn, provider change) gets a new `Session` under the same id, so an
old consumer must not reach the new one. Callers pass in the state other
modules own.
- **No `toolUseCache` dependency.** Whether a task's launching call has
returned used to be judged by the call still being in `toolUseCache`,
which Claude Code prunes at the tool's result. The object now records
returned calls itself, capped at 1024 like the other per-session sets.
- **Stricter typing on the output path.** Typing the terminals map
properly exposed that the parsed `outputFilePath` was loosely typed, so
it is now checked to be a string.
- **Test doubles.** The hand-built `Session` doubles in
`acp-agent.test.ts` and `session-doubles.ts` now carry the new required
field.

## Tests

- The existing suite passes unchanged.
- One new v2 test covers a task whose `task_started` arrives after its
call returned, so it is listed as `_claude/task`. A first version of
this refactor recorded "returned" per task and silently missed that
case. The test fails against that version, and against listing tasks
before their call returns.

`npm run check`, `npm run build` and `npm run test:run` pass (3556
passed, 30 skipped).
2026-11-02 19:52:25 +01:00
Ben Brandt
53e3eec013 feat: stream the output of v2 background commands (#1512)
Follow-up to #1510 and #1511, phase 2b of the v2 [Background Work
RFD](https://agentclientprotocol.com/rfds/v2/background-work).

Claude Code streams no output of a Bash command that went to the
background: it writes the output to the task's output file. So the
command's terminal showed only the Bash result's placeholder ("Command
running in background with ID …") until the command exited. The v2 spec
says the Agent **SHOULD** keep a listed terminal's output current
whenever it can read the process output.

## What a v2 client gets

- **Snapshot:** the file's first text replaces the placeholder as a
`terminal_output` snapshot.
- **Chunks:** later text is appended as `terminal_output_chunk`s.
- **Task end:** the rest of the file is read before the terminal's
`exitStatus`, as the spec asks.
- **Query close:** reading stops.

## Design

- New `src/background-output.ts` (`BackgroundOutputTail`) follows one
file:
- It polls every 500 ms, because watching a file is not reliable on
every platform and a missing file just means nothing was written yet.
- It reads at most 256 KiB per read, so a burst of output doesn't block
the event loop. Each read is sent before the next, including when
reading to the end at the task's end.
- Past 1 MiB of unread output (a command writing faster than the polls
read, about 512 KiB/s), it skips to the file's last 1 MiB and sends it
as a replacing snapshot, so the terminal shows the latest output without
a gap in it. A file that shrinks is read again from its start, also as a
snapshot.
- Its timer is unref'd, so a long-running command doesn't keep the agent
process alive.
- A `StringDecoder` holds a UTF-8 character split across reads until its
last byte is written.
  - Reads are serialised.
- `background-work.ts` starts the tail once the Bash result's own
updates are sent, so the first output replaces the placeholder rather
than coming before it. It finishes the tail before the terminal's exit
and stops it when the query closes. `acp-agent.ts` only gets wiring.
- The output path comes from the Bash result's text, the only place the
SDK gives it before the task ends. The existing
`asyncTaskOutputFilePath` parser only accepts an absolute path ending in
`tasks/<taskId>.output` for this task. Without a path, the terminal
keeps the placeholder as before.
- v2 only (`reportsBackground`), like the rest of the background work.
v1 clients that render terminals through the `_meta` extension could use
it too. That is a small follow-up, but it changes what v1 clients
receive.

## Tests

- Nine unit tests for `BackgroundOutputTail`:
  - the replacing first read, and appends
  - a file that appears later, and a file that never appears
  - a UTF-8 character split across reads
  - finishing, an empty output, and stopping
  - a backlog past 1 MiB, and a file that shrinks
- One v2 test over a real connection with a real output file. It checks
that the placeholder is replaced, later output arrives as chunks, and
the rest arrives before the exit.
- I checked by mutation that removing the read-before-exit, the
replacing snapshot, the UTF-8 decoder, the backlog cap or the shrink
restart each fails a test.

`npm run check`, `npm run build` and `npm run test:run` pass (3555
passed, 30 skipped).
2026-11-02 19:37:49 +01:00
acp-release-bot[bot]
981bf9a28a chore(main): release 0.100.0 (#1485)
🤖 I have created a release *beep* *boop*
---


##
[0.100.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.99.1...v0.100.0)
(2026-11-02)


### Features

* list v2 background tool calls
([#1511](https://github.com/agentclientprotocol/claude-agent-acp/issues/1511))
([4d79b9f](4d79b9ffbc))
* report subagent working directories
([#1496](https://github.com/agentclientprotocol/claude-agent-acp/issues/1496))
([eb78afa](eb78afa855))
* report v2 background work
([#1510](https://github.com/agentclientprotocol/claude-agent-acp/issues/1510))
([5565259](5565259771))
* report why Claude Code could not start a session
([#1508](https://github.com/agentclientprotocol/claude-agent-acp/issues/1508))
([38cf656](38cf656fc9))


### Bug Fixes

* align session/cancel, session/close, and v2 setup with the ACP v2 spec
([#1494](https://github.com/agentclientprotocol/claude-agent-acp/issues/1494))
([f50598a](f50598a39c))
* forward v2 elicitation values and read dismissed v2 permission prompts
as a refusal
([#1493](https://github.com/agentclientprotocol/claude-agent-acp/issues/1493))
([3bc6711](3bc671193c))
* keep native Windows paths in v2 git patches
([#1492](https://github.com/agentclientprotocol/claude-agent-acp/issues/1492))
([c3428de](c3428de1bf))
* preserve live session settings across provider refreshes
([#1474](https://github.com/agentclientprotocol/claude-agent-acp/issues/1474))
([e01a288](e01a288384))
* reject unreadable skill manifests
([#1482](https://github.com/agentclientprotocol/claude-agent-acp/issues/1482))
([99a6e76](99a6e76158))
* report the fresh context's usage when ExitPlanMode clears context
([#1484](https://github.com/agentclientprotocol/claude-agent-acp/issues/1484))
([2fd46c8](2fd46c866d))
* update to @agentclientprotocol/sdk 2.2.0
([#1491](https://github.com/agentclientprotocol/claude-agent-acp/issues/1491))
([1d2b92f](1d2b92feae))
* update to @agentclientprotocol/sdk 2.4.0
([#1509](https://github.com/agentclientprotocol/claude-agent-acp/issues/1509))
([720bbb3](720bbb3e4c))
* update to Claude Agent SDK 0.3.316 (Claude Code 2.1.316)
([#1507](https://github.com/agentclientprotocol/claude-agent-acp/issues/1507))
([1d03064](1d0306468b))
* validate session, prompt, config, and auth requests per the latest ACP
spec
([#1495](https://github.com/agentclientprotocol/claude-agent-acp/issues/1495))
([d2ef490](d2ef490a31))

---
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-11-02 15:33:56 +01:00
Ben Brandt
4d79b9ffbc feat: list v2 background tool calls (#1511)
Follow-up to #1510, phase 2a of the v2 [Background Work
RFD](https://agentclientprotocol.com/rfds/v2/background-work).

A Monitor, Workflow or backgrounded MCP call returns as soon as its task
starts, so its tool call completed at its result while the task ran on.
#1510 listed such tasks as `_claude/task` items, which clients don't
recognise. This PR lists them as the tool calls that started them, as
the RFD's adapter table describes.

## What a v2 client gets

| When | The tool call |
|---|---|
| Tool result, while the task is in Claude Code's live background task
set | Reports `in_progress` instead of `completed`, and is listed as a
`tool_call` item |
| While listed | Advertises `capabilities.cancel`. `tool_call/cancel`
stops the task with `stopTask`, or returns `-32602` if the call isn't
listed (also when its task ended mid-cancel) |
| `task_updated` with `paused` / `running` | `paused` / `in_progress` |
| The task ends | `completed`, `failed`, or `cancelled` for a stopped
(`killed`) task, with `capabilities: null`, before the list drops it.
The same happens when the live set drops the task without an end event
(`completed`, since nothing was reported), or the query closes
(`cancelled`) |
| `session/resume` | The call's current state, rebuilt from its tool
use, then its capability, before the snapshot |

The task is matched to its call through the structured `taskId` of a
Monitor or Workflow result, or through the live task whose
`task_started.tool_use_id` names the call (backgrounded MCP calls).

`session/cancel` no longer stops background workflows for sessions that
report their background work, because listed work must keep running.
Clients stop them with `tool_call/cancel` instead. v1 and AIR are
unchanged.

A task without such a call is still a `_claude/task` item. It is now
listed only once its `task_started` has arrived and the call that
started it, if any, has returned. Before that, the foreground work is
still waiting on that call, and the spec says foreground work is not
listed.

## Design

- `background-work.ts` holds the logic: registering background tool
calls, pause and end updates, the shared `cancelBackgroundWork` for
`terminal/cancel` and `tool_call/cancel`, and the resume states.
- `acp-agent.ts` only gets wiring.
- `paused` travels as a cast v1 status, alongside the existing
`V2_CANCELLED`.
- `V2Background` advertises and clears the cancel capability.

## Tests

- Six new v2 tests, using the real CLI's event order (tool use, task
joins the live set, `task_started`, tool result) and the `killed` +
`stopped` pair for a stopped task. They cover:
  - listing, pause and resume, and the final status
  - `tool_call/cancel`
  - a query that closes
  - a listed Workflow left running on `session/cancel`
  - resume
  - the `_claude/task` fallback
- I checked by mutation that each new guard fails a test when removed.

`npm run check`, `npm run build` and `npm run test:run` pass (3545
passed, 30 skipped).

## Still to come

- Tailing background Bash `output_file` into terminal output (phase 2b).
- `ScheduleWakeup` / `CronCreate`, which need a signal from the SDK.
- The SHOULD gap about follow-up work is being relaxed upstream in
agentclientprotocol/agent-client-protocol#2745.
2026-11-02 14:40:01 +01:00
Ben Brandt
5565259771 feat: report v2 background work (#1510)
Implements phase 1 of the ACP v2 [Background Work
RFD](https://agentclientprotocol.com/rfds/v2/background-work), now part
of schema v2.0.0-alpha.13. v2 sessions report the work Claude Code keeps
running independently of foreground work as `background` on every
`state_update`.

## What the client gets

The lists are built from Claude Code's live background task set
(`background_tasks_changed`). Tasks Claude Code marks `ambient` are
skipped, since they are housekeeping. The root session and each subagent
session (subagents RFD form) has its own list. Each task is listed once,
in the list of the session that runs it:

| Work | Listed as |
|---|---|
| A Bash command that went to the background | Its display terminal,
from the Bash result until its task ends. While listed, the terminal
advertises `capabilities.cancel`, and `terminal/cancel` stops its task
with `stopTask`. |
| A detached subagent | Its child session, while it works, and while it
is idle with background work of its own. The child reports its own list
on its `state_update` and on the parent's `subagent_update.state`
mirror. |
| Any other task (`Monitor`, `Workflow`, `mcp_task`, …) | A
`_claude/task` item with `taskId` and `description`. Its tool call ends
at its tool result, so it cannot be a `tool_call` item yet. |

The ordering rules the spec requires:
- **List changes:** the state is re-sent with the new list and no stop
reason, so a repeated `idle` is not a new completion.
- **Before a terminal is listed:** a `terminal_update` advertising
`cancel` goes out first.
- **Before a terminal leaves the list:** its `exitStatus` goes out, with
`capabilities: null`. This also holds when the level drops the task
without an end event, or when the query closes (process death, stream
error, sign-out recreation, `session/close`).
- **A child and its parent:** the child's own list goes out before the
parent's list drops it.
- **`session/resume`:** each listed subagent's current `subagent_update`
and each listed terminal's current `terminal_update` come before the
state snapshot.
- **`terminal/cancel`:** it returns `-32602` for a terminal that is not
listed, including one whose command ended while the stop was in flight.

## Gating

Only v2 sessions with `perTaskStopAffordance` report lists. That is the
default for a non-AIR v2 client. Without it, Claude Code's interrupt
kills background tasks, which would break the rule that `session/cancel`
leaves listed work running. v1 and AIR are unchanged.

## Design

- `src/background-work.ts` owns the lists. It builds them from session
state and publishes changes. `acp-agent.ts` only gets wiring calls: one
sync per message-loop iteration, the level update, terminal exits when a
query closes, and the `terminal/cancel` entry point.
- `src/v2/background.ts` adds the lists to v2 state updates and to
subagent state mirrors. The cancel capability lives in `V2Terminals`,
which already owns terminal state.
- `NativeSubagentRuntime.currentUpdate` builds a child's live
`subagent_update` for resume.

## Not in this PR

- **SHOULD gap:** when finished background work starts a follow-up
cycle, the spec wants `running` in the same snapshot that drops the
item. We send `idle []` and then `running`, because the adapter can't
know in advance that Claude Code will start a follow-up. The RFD
acknowledges this.
- **Phase 2:**
- List `Monitor`, `Workflow` and `mcp_task` as `tool_call` items that
stay `in_progress` (or `paused`), with `tool_call/cancel`.
  - Tail background Bash `output_file` into terminal output.
- **Phase 3:** `ScheduleWakeup` / `CronCreate`. The SDK gives no signal
for when one fires or is removed.

## Tests

- New v2 tests cover:
- terminal listing, cancel advertisement, exit ordering, ambient tasks,
and a level drop without an end event
- query death, `terminal/cancel` including the race, and resume for both
terminals and subagents
- a background subagent, an idle subagent kept listed by its own
command, `_claude/task` items, and sessions without
`perTaskStopAffordance`
- I checked by mutation that the guards for each required ordering fail
a test when removed.
- The v2 golden files gain `background: []`, and the subagent ones also
gain child lists.

`npm run check`, `npm run build` and `npm run test:run` pass (3540
passed, 30 skipped).
2026-11-02 14:16:10 +01:00
Ben Brandt
720bbb3e4c fix: update to @agentclientprotocol/sdk 2.4.0 (#1509)
Updates `@agentclientprotocol/sdk` from 2.2.0 to 2.4.0 (schema v1.29.1
and v2.0.0-alpha.13).

## What changed in the protocol

- **v1:** the stable schema and spec pages only changed wording. There
are no requirement changes.
- **v2 `session/cancel`:** the Client no longer answers pending
permission requests with `cancelled`. The Agent MUST stop waiting for
the cancelled work's permission requests and SHOULD cancel each one with
`$/cancel_request`.
- **v2 `session/close`:** the existing MUST to cancel pending
elicitations with `$/cancel_request` before responding now covers
permission requests too.

The adapter already did both: the tool call's signal and the session's
abort controller cancel the request, and a hand-written abort race
stopped the wait. Until now, no test checked that a v2 client actually
receives `$/cancel_request` for a permission request.

## Changes

- Permission requests and elicitations, on both v1 and v2, now use SDK
2.4.0's `stopWaitingOnCancel` option instead of `raceWithAbort`. The SDK
sends `$/cancel_request`, rejects at once with `-32800`, and quietly
drops any late response. Every caller already maps a rejection after an
abort to "Tool use aborted" or to `cancel`, so nothing else changes.
`raceWithAbort` is still used for the eager `tool_call` notification.
- **Tests:**
- **v2:** the existing tests where a `session/cancel` or `session/close`
withdraws a held request now also check that the client received
`$/cancel_request`. For `session/close`, it must arrive before the
response.
- **v1:** new tests over a real SDK connection with a client that holds
requests. These cover the v1 `ClientConnection` wiring, which the
mocked-client tests bypass.
- The v1 mock clients that "ignore cancellation" now model the SDK's
behaviour with a `heldRequest` helper.
- Removed one test that only covered `raceWithAbort` handling a
rejection from the client.
- I checked that the new assertions fail if the `stopWaitingOnCancel`
option is removed or v2 permission requests stop passing their
cancellation signal.

## Follow-ups (not in this PR)

- v2 background work: the `background` list on state updates, the
`paused` status, and `tool_call/cancel` / `terminal/cancel`. The AIR
`asyncTasks` extension stays as it is.
- The new unstable v1 methods (`session/set_title`,
`session/archive`/`unarchive`, `session/status`, `auth/status`).

`npm run check`, `npm run build` and `npm run test:run` pass (3520
passed, 30 skipped).
2026-11-02 11:41:32 +00:00
Ben Brandt
38cf656fc9 feat: report why Claude Code could not start a session (#1508)
When Claude Code refuses to start (invalid proxy URL, wrong organization
sign-in, a required sandbox that can't run, …), `session/new`,
`session/load`, `session/resume` and `session/fork` used to fail with a
generic `Claude Code process exited with code 1. stderr: …` error.

This sets `CLAUDE_CODE_STARTUP_FAILURE_RESULTS=1` on the query env, so
the CLI writes an error result with a `startup_failure_reason` before
exiting on a known startup failure. The SDK then rejects
`initializationResult()` with an error that carries
`startupFailureReason`. The new `src/startup-failure.ts` maps it to an
ACP error:

| `startupFailureReason` | ACP error |
|---|---|
| `gateway_signin_required`, `org_pin_mismatch`, `org_config_refused` |
`authRequired` (signing in again, or with another account, fixes it) |
| `cwd_unavailable` | `invalidParams` (matches the existing cwd
validation) |
| any other reason | `internalError` |

The message is the CLI's own text, which says what to fix (e.g. `Invalid
proxy URL in HTTPS_PROXY: … Fix or unset HTTPS_PROXY and restart Claude
Code.`), and `data.startupFailureReason` carries the reason. Errors
without a reason (older CLIs, unknown causes) pass through unchanged,
and the existing `sessionNotFound` mapping for resumes is untouched.

Checked against the real CLI (SDK 0.3.316): with `HTTPS_PROXY="not a
url"`, `initializationResult()` rejects with `startupFailureReason:
"proxy_invalid"` when the variable is set, and with a plain exit-code
error without it.

## Testing

- v1: `create-session-options.test.ts` covers the env var, each mapping,
and pass-through of errors without a reason.
- v2: `acp-v2.test.ts` covers the `session/new` error over the wire.
- `npm run build`, `npm run check`, `npm run test:run` pass. The
subprocess integration tests also passed locally with this change.

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

Co-authored-by: Claude <noreply@anthropic.com>
2026-11-02 11:23:33 +00:00
Ben Brandt
1d0306468b fix: update to Claude Agent SDK 0.3.316 (Claude Code 2.1.316) (#1507)
Updates dependencies to their latest versions.

- `@anthropic-ai/claude-agent-sdk` 0.3.312 → 0.3.316 (bundles Claude
Code 2.1.316)
- `@types/node` 26.6.5 → 26.6.6, `eslint` 10.13.0 → 10.14.0, `globals`
17.13.0 → 17.14.0
- `npm update` for transitive patch/minor bumps in the lockfile
- Bumps the "as of" version in two comments about SDK type gaps
(`Query.getSettings()` still missing from the declarations; `unanswered`
still only in the AskUserQuestion output schema). Both were re-checked
against 0.3.316.

TypeScript 7.0.2 is left out: `@typescript-eslint` 8.72.1 supports
TypeScript `<6.1.0`.

## Changelog review (SDK 0.3.313–0.3.316, Claude Code 2.1.313–2.1.316)

No required changes. The SDK type changes only add fields and union
members (`sandbox_required_unavailable` startup failure reason,
`blockWritesOutsideWorkingDirectories` / `blockReadsExemptPaths`
settings, `pending_elicitation_requests` on `initialize`, alpha
`prewarm()` in `/core`), none of which the adapter reads or needs. No
new `fast_mode_disabled_reason` values accompany the Opus 4.8 fast mode
deprecation.

Follow-up planned separately: use `CLAUDE_CODE_STARTUP_FAILURE_RESULTS`
/ `startup_failure_reason` to report typed startup failures to clients.

## Testing

`npm run build`, `npm run check`, `npm run test:run` pass.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-11-02 12:18:36 +01:00
Andrey Bragin
c6de68797b docs: add design-fit and test-duplication guidelines to AGENTS.md (#1480)
Adds guidelines to `AGENTS.md` that raise the baseline quality of new
PRs and features. Changes should fit the existing architecture instead
of working around it, respect the session's turn lifecycle, stay
compatible with existing clients, and come with focused, non-duplicated
tests. Authors and their agents can check these points before review.

- **Coding guidelines** replaces the **Code comments** section and
covers only the code itself. The comment rules are now one bullet, which
also requires comments and docs to be true on every code path they
describe. It adds rules for refusing requests with an actionable
`RequestError`, not reporting success for work started without awaiting
it, not letting results depend on cache state, and stating unenforced
ordering assumptions.
- **Architecture and design fit** is a new section. Build on the module
that owns the concept, put new features and components in dedicated
modules instead of `acp-agent.ts`, prefer extending existing mechanisms,
reuse sibling logic so paths that handle the same input (live stream and
`session/load` replay, command list and dispatch, `prompt` and `steer`)
apply the same rules, keep ACP v1 and v2 on one implementation, use
`startTurn`'s busy check and the turn queue for anything that sends
`session/update`, avoid branching on stale local copies of remote state,
don't shadow existing commands, keep backward compatibility, and explain
any unavoidable workaround.
- **Testing guidelines** is a new section. It adds rules against
duplicate or parametrizable tests, asks to reuse the existing test files
and helpers, to build test doubles from what the real Claude Code CLI
sends, to cover both protocol versions when client-visible output
changes, and to have a test for each behavior the docs promise.
2026-11-02 11:40:07 +01:00
Ben Brandt
eb78afa855 feat: report subagent working directories (#1496)
Schema v1.28.0 / v2.0.0-alpha.11 adds `cwd` and `additionalDirectories`
to `subagent_update` (agentclientprotocol/agent-client-protocol#2593,
#2660). When a child runs in a different directory from its parent, for
example an Agent call with `isolation: "worktree"`, the Agent must
report the child's `cwd` before sending child traffic that depends on
it.

**Signal**
- The agent reads the `cwd` of the child's own hooks, matched by
`agent_id`. The existing per-child configuration hook already runs on
SubagentStart, PreToolUse, PostToolUse and the others.
- Claude Code runs SubagentStart before the child's first model request
and waits for the hook, so the `cwd` reaches the client before the
child's first tool call.
- `worktreePath` in the Agent tool result arrives only when the child
finishes, which is too late.

**Behaviour (RFD form only; AIR is unchanged)**
- `cwd` is sent only when the child's directory differs from its
parent's, compared with `realpath`. A hook that runs before the
announcement puts the `cwd` in the announcing `subagent_update`.
Otherwise a separate `subagent_update { cwd }` follows.
- A child that moves back to its parent's directory gets `cwd: null`. A
child whose hook `cwd` changes later, for example after a `cd` that
Claude Code keeps, is reported again, since that is where it now runs.
- `additionalDirectories` is never sent. Claude Code gives subagents the
session's additional directories, so inheriting them is accurate.
- Replay uses the child's `.meta.json` (`cwd`, `worktreePath`,
`inheritedWorktreePath`) and the first `cwd` in its transcript. After
the load response, a child whose directory no longer exists gets `cwd:
null`.
- v2 passes the fields through unchanged.

**Not covered**
- `remote` isolation, which fires no local hooks.
- After a reconnect to a live session, the `cwd` the runtime holds is
not sent again.
2026-10-30 16:05:38 +01:00
Ben Brandt
d2ef490a31 fix: validate session, prompt, config, and auth requests per the latest ACP spec (#1495)
Request validation from the ACP schema v1.28.0 / v2.0.0-alpha.11 review.
Each change has a test.

**Paths**
- `session/list` rejects a relative `cwd` with -32602. Without a session
index it passes `includeWorktrees: false`, so the filter matches only
sessions of exactly that directory, not other git worktrees of the same
repo.
- Resume, load and fork check `cwd` and `additionalDirectories` before
anything else. Before, a resume with a bad `cwd` tore down the live
session and then failed, and a fork copied the transcript first.
- Relative `additionalDirectories` entries are rejected with -32602.

**Session setup (v2)**
- Uses the SDK's `assertSupportedMcpServers`, which also rejects
duplicate MCP server names with -32602.
- Rejects an empty `prompt` with -32602.
- The v2 initialize response no longer advertises the AIR `asyncTasks`
capability or the `goal` entry, since the v2 app has no handlers for
`_session/async_task/stop` or `_session/goal`. v1 AIR is unchanged.

**Errors**
- `session/fork` of an unknown session returns -32002 with
`data.sessionId` instead of -32603. This matches the Claude SDK's
"Session … not found" and "Invalid sessionId" messages.
- `authenticate` with an unknown `methodId` returns -32602 instead of
-32603.
- `set_config_option` rejects a value whose type doesn't match the
advertised option type. A `type: "boolean"` option takes `true`/`false`,
and a select option takes a string. Model aliases such as `"opus"` are
still accepted.
- An AIR `session/delete` of a session that has no transcript now
succeeds instead of failing with a generic error.

**Prompts and commands**
- v1 `session/prompt` rejects `audio` and unknown content blocks with
-32602 before queueing them. Before, they were silently dropped.
- Advertised command names that contain whitespace are dropped, and of
duplicate names only the first is kept.
- The `/mcp:server:prompt` rewrite now accepts arguments that span
lines, and applies only to the first text block.
2026-10-30 16:03:58 +01:00
Ben Brandt
f50598a39c fix: align session/cancel, session/close, and v2 setup with the ACP v2 spec (#1494)
**`session/cancel` cancels only earlier prompts**
(agentclientprotocol/agent-client-protocol#2685)
- `cancelTurns` used to read the turn queue again after two awaits: the
compaction interrupt and publishing the subagent state. A prompt that
arrived during those awaits was cancelled too, which is -32800 on v2.
- Each prompt now gets a sequence number when `startTurn` receives it.
The cancel records the count before its first await and cancels only
turns at or below it. That includes a prompt received earlier that
`startTurn` had not queued yet.
- Known gap: if the cancelled turn ends during those awaits and Claude
Code starts the newer prompt, the `query.interrupt()` that follows can
still stop that prompt. Fixing it means skipping the interrupt when
nothing the cancel covers is still running, which also affects
cancelling background work, so it is left for later.

**`session/close` cancels pending requests first**
(agentclientprotocol/agent-client-protocol#2683)
- MCP elicitations, AskUserQuestion, the refusal-fallback dialog and
permission requests now combine their callback signal with the session's
`abortController.signal`.
- `teardownSession` therefore sends `$/cancel_request` for each of them
before `session/close` responds, instead of relying on Claude Code to
abort its callback signals in time.

**v2 setup responses** (agentclientprotocol/agent-client-protocol#2688,
#2603)
- If reading the commands for the setup response fails, the agent now
sends them in an `available_commands_update` after the response. Before,
the client was never sent them.
- On v2, MCP OAuth starts after the `session/new`, resume or fork
response is sent, so its URL elicitation no longer reaches the client
before the response.

Not done: withdrawing a held prompt with `$/cancel_request`. The user
message is already in Claude Code's input once it is queued, and
removing it safely needs `cancel_async_message`, which not every CLI
version has. agentclientprotocol/agent-client-protocol#2636 allows
ignoring the cancel and sending the normal response.
2026-10-30 16:03:03 +01:00
Ben Brandt
3bc671193c fix: forward v2 elicitation values and read dismissed v2 permission prompts as a refusal (#1493)
**Elicitation content** (agentclientprotocol/agent-client-protocol#2595)
- v2 lets an elicitation answer hold any JSON value, such as an object.
The agent checked accepted content with the v1 guard, which allows only
strings, numbers, booleans and string arrays. A v2 accept with an object
therefore reached the MCP server as `{ action: "accept", content: {} }`.
- The agent now forwards the accepted content unchanged, and the MCP
server checks it against its own schema. If Claude Code's MCP client
still uses the older value types, it rejects such an answer with an
error instead of passing on empty content.
- AskUserQuestion reads only string, number and boolean picks, so it no
longer shows an object as `[object Object]`.
- The MCP OAuth url-mode check now tests `action !== "accept"` instead
of the v1 guard.

**Permission `cancelled` on v2**
(agentclientprotocol/agent-client-protocol#2582, #2658)
- In v2, `cancelled` also means the user dismissed the prompt or the
client declined it by policy.
- When no `session/cancel` is in progress, a foreground v2 `cancelled`
is now denied as "User refused permission to run tool", the same denial
background subagents already get, and the turn goes on. Before, Claude
Code reported it as "Tool permission request failed".
- During a `session/cancel` it is still aborted. v1 is unchanged.
- Unknown v2 outcomes map to `cancelled`, so they are also denied now
instead of aborted.
2026-10-30 14:44:34 +00:00
Ben Brandt
c3428de1bf fix: keep native Windows paths in v2 git patches (#1492)
ACP v2 now says each `git_patch` path, once unquoted, is the same native
absolute path string as the matching `changes` entry
(agentclientprotocol/agent-client-protocol#2650).

The v2 form converted `C:\work\App.ts` to `C:/work/App.ts`, so on
Windows the patch and `changes[].path` named different strings. The v2
form now keeps the native path, which Git quoting writes as
`"C:\\work\\App.ts"`. Git's own form, which the AIR extension uses,
still uses forward slashes and the `a/`/`b/` prefixes.
2026-10-30 14:39:21 +00:00
Ben Brandt
1d2b92feae fix: update to @agentclientprotocol/sdk 2.2.0 (#1491)
Moves to ACP schema v1.28.0 and v2.0.0-alpha.11.

- v2 dropped JSON-RPC batching, so `AnyWireMessage` is gone. The test
harnesses use `AnyMessage`, and a request id can now be `null` in the
types.
- The v2 elicitation schema now requires `properties`. A forwarded MCP
schema without them, such as `{ "type": "object" }`, gets an empty map,
so clients on SDK 2.2.0 don't reject it.

Runtime changes that come with the SDK and need no agent code:
- The SDK handles a repeated `initialize`, rejects batches, and rejects
messages sent before initialization. A failed `initialize` now leaves
the connection open.
- v2 `usage_update.cost` is now a patch field, where omitted means
unchanged. The agent sends `cost` only on the result update, so the
client keeps showing the last cost during a turn.

Follow-up PRs for the spec changes in this range are stacked on this
branch.
2026-10-30 14:39:09 +00:00
xiaofeilovecoding
99a6e76158 fix: reject unreadable skill manifests (#1482)
Fixes #1478.

Skip SKILL.md candidates when opening or reading the file fails, while
preserving readable unnamed manifests. Do not cache failed reads so
later resolutions can recover.

Adds nine resolver regressions for open/read errors, descriptor cleanup,
recovery, and valid alternatives.

Validation (Windows, Node 24.18.0):
- Nine new tests, TypeScript build, and ESLint pass.
- Full suite: 3,354 passed, 57 failed, 52 skipped. The same 57 failures
reproduce on unmodified 52c241e in the affected files.
- npm run check fails only on unchanged CLAUDE.md: Windows checked out
the symlink as a plain target file. The baseline has the same issue.

Co-authored-by: xiaofeilovecoding <fxy850328059@gmail.com>
2026-10-30 13:57:42 +01:00
Slapoguzov Alexander
e01a288384 fix: preserve live session settings across provider refreshes (#1474)
## Problem

When the provider changes on a running agent (`providers/set`,
`providers/disable`, or logout with a provider override), every open
session gets a new Claude Code process. For example, this happens after
a BYOK key edit or a proxy recovery. The new process started from the
original `session/new` params only. The session lost the model, effort,
mode, Fast mode, ultracode and agent the user had picked, while the
client still showed the old values (IJAI-4592).

## Fix

- After the new process starts, the session's previous choices are
applied again through the same code paths as client requests. The client
then gets a `config_option_update` with the actual configuration.
- If the new provider doesn't support a choice (for example, the new key
has no access to that model), the choice is logged and skipped. The
client sees the real value.
- Effort: only the user's current pin comes back. An effort the user
reset to Default, or that a model switch dropped, stays off.
- Mode, config changes and restarts run one at a time per session. A
choice sent during a restart lands after the restored ones. Other
sessions are not blocked.
- The respawn after a sign-out goes through the same path, so it keeps
the choices too.
- When a provider change overlaps that respawn, it replaces the current
recovered session and holds waiting prompts until the provider update
finishes. Those prompts then run once on the latest route with the
restored choices.
- Closing a session while it restarts no longer leaves a running process
behind.

## Tests

- `provider-session-configuration.test.ts`: the overlapping
sign-out/provider change and two waiting prompts held until the final
replacement initializes.

- `provider-session-configuration.test.ts`: restore order, effort edge
cases, races, and close or dispose during a restart.
- `session-hooks.test.ts`: real Claude Code CLI against a local fake
API. After a refresh, the next request goes out with the chosen model
and `effort: high`.

## Validation

[CI on the published
follow-up](https://github.com/agentclientprotocol/claude-agent-acp/actions/runs/40989963422)
passes formatting, lint, build and tests: 3,474 passed, 32 skipped,
including 37 provider, 107 authorization and five real CLI-hook tests
against a local fake API. Independent review passed; 151 scoped tests
also covered seven additional recovery/failure schedules.

<details><summary>Local baseline comparison and scope</summary>

The added overlap regression failed on the preceding draft and passes
with this repair. Complete local runs used identical locked dependencies
and two workers: baseline 3,431 passed / 19 failed / 30 skipped;
candidate 3,433 passed / the same 19 failed / 30 skipped. All local
failures are existing session-index cases; filesystem watching reported
EMFILE, with two SDK path-list assertions also failing. The complete
Linux CI run above passes.

This follow-up is a normal appended commit preserving the existing
implementation. The PR remains a draft. Released-package Air acceptance
follows merge and release.

</details>

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

Co-authored-by: Aleksandr Slapoguzov <aleksandr.slapoguzov@jetbrains.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 13:57:13 +01:00
Ben Brandt
2fd46c866d fix: report the fresh context's usage when ExitPlanMode clears context (#1484)
Choosing one of `ExitPlanMode`'s "clear context" options restarts
Claude's conversation with only the plan. The ACP session ID and the
turn stay the same, and the adapter resets its context count to 0, but
it didn't tell the client. Clients kept showing the old context's usage
until the fresh context first reported its own, so nothing on the client
side showed that the context had been cleared.

`publishSessionState` now sends a `usage_update` for the empty fresh
context (`used: 0`, the fresh session's context window as `size`) right
after the `current_mode_update` and `config_option_update` it already
sends. `used: 0` slightly understates the fresh context, which already
holds the system prompt and the plan, until Claude's next usage report.

Testing:
- `npm run build`, `npm run test:run`, and `npm run check` pass.
- `clear-context-coordinator.test.ts` checks the update's order and
values.
2026-10-29 22:09:59 +01:00
acp-release-bot[bot]
0f78dbbdd0 chore(main): release 0.99.1 (#1472)
🤖 I have created a release *beep* *boop*
---


##
[0.99.1](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.99.0...v0.99.1)
(2026-10-29)


### Bug Fixes

* add approval id to plan
([#1469](https://github.com/agentclientprotocol/claude-agent-acp/issues/1469))
([4b4b2c4](4b4b2c4de7))
* allow forking sessions into a different directory
([#1483](https://github.com/agentclientprotocol/claude-agent-acp/issues/1483))
([8bd0bc9](8bd0bc9b03))
* detect "Run /login" login-expired messages as authRequired
([#1481](https://github.com/agentclientprotocol/claude-agent-acp/issues/1481))
([fccaebc](fccaebcf87))
* mark plugin, bundled and nested Claude skills as skills for AIR
([#1473](https://github.com/agentclientprotocol/claude-agent-acp/issues/1473))
([52c241e](52c241e715))
* title subagent sessions by their task
([#1479](https://github.com/agentclientprotocol/claude-agent-acp/issues/1479))
([7dcb629](7dcb62923d))

---
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-10-29 20:34:22 +01:00
Ben Brandt
8bd0bc9b03 fix: allow forking sessions into a different directory (#1483)
Fix `session/fork` when the requested working directory differs from the
directory containing the source session's transcript, such as a
subthread in another worktree.

The Claude Agent SDK's `dir` option restricts **source transcript
lookup**; it does not select the fork's runtime directory. Passing ACP's
destination `cwd` therefore prevents the SDK from finding a source
session stored elsewhere.

- Omit `dir` from both SDK fork calls, active-history lookup, and
full-history import so every fork-resolution path searches all projects.
- Continue starting the forked query with the requested `cwd`, MCP
servers, and other session settings.
- Add 12 real-SDK tests using isolated temporary transcripts, covering
same- and cross-directory forks through latest history, active messages,
inactive branches, fingerprints, and live message maps, plus
missing-message errors. Check copied history, fresh UUIDs, and an
unchanged source transcript.
- Extend ACP v2 coverage to verify same- and cross-directory forks
remain immediately promptable and use the requested runtime directory.

## Validation

- Before the fix, all six cross-directory regression cases failed while
the six same-directory cases passed; after the fix, all 12 pass.
- `npm run check` — passed.
- `npm run build` — passed.
- `npm run test:run -- --reporter=dot --silent` — passed: 60 files,
3,437 tests; 4 files and 30 tests skipped.
- `git diff --check` — passed.

Final validation ran on macOS with Node 24. Earlier Windows validation
passed all fork-related tests but encountered unrelated path-separator
assertions and checkout line-ending formatting failures; those are
outside this change.

No authenticated Claude prompt or end-to-end Delta subthread test was
run. The new transcript tests use the real SDK without starting Claude
Code; ACP query execution is mocked in the protocol test.
2026-10-29 19:20:46 +00:00
Mark Tkachenko
52c241e715 fix: mark plugin, bundled and nested Claude skills as skills for AIR (#1473)
AIR lists a command as a skill only when the adapter finds its
`SKILL.md`, and the resolver only probed `<cwd|~>/.claude/skills`.
Plugin skills, CLI-bundled skills and nested `.claude/skills` therefore
reached AIR as plain commands.

The adapter now takes the paths from Claude Code's own skill list,
`getContextUsage().skills.skillFrontmatter`:
- a skill with a `path` gets `kind: "skill"` and `skillPath`; one
without (bundled `simplify`, remote connections) gets `kind` only;
- a skill not in the list falls back to the existing `SKILL.md` lookup;
builtin commands like `compact` stay unmarked;
- the read runs in the background (the first call takes about 1 s) on
the first `system/init` and on every `commands_changed`, and the command
list is re-sent when the result changes.
2026-10-29 14:42:08 +01:00
Ben Brandt
fccaebcf87 fix: detect "Run /login" login-expired messages as authRequired (#1481)
The CLI now reports an expired login (no refresh token) as:

Login expired · Run /login to sign in again, or re-authenticate your
Anthropic profile

and tags the synthetic assistant message `error: "invalid_request"`, not
`authentication_failed`. The adapter only recognised the exact phrase
"Please run /login", so this message streamed to the client as agent
text and the turn failed with a generic internal error instead of
`RequestError.authRequired()`. Clients never learned auth was required,
so they couldn't offer their terminal auth methods. "Not logged in · Run
/login" had the same gap.

Changes:
- Add `isLoginRequiredText()`, which matches "run /login"
case-insensitively, and use it in `isSyntheticLoginMessage()` (live loop
and replay) and the two result-text checks.
- Exclude synthetic usage-limit banners from the login match, since some
mention `/login` to switch accounts and have their own handling.

Tests cover both new wordings, the usage-limit exclusion, and an
end-to-end turn where the `invalid_request`-tagged "Login expired"
message must reject with `authRequired` and not leak "/login" text to
the client.
2026-10-29 13:17:53 +00:00
Ben Brandt
7dcb62923d fix: title subagent sessions by their task (#1479)
A subagent's `subagent_update.title` was its type (`general-purpose`),
or
`name (type)`, so a client showing the title couldn't tell siblings
apart;
the task was only in `description`. The RFD makes the title the parent's
display label and the description the child's role or purpose.

The title is now the agent's name when it has one, else the task's short
description, else its type. The description is the type, unless the type
is already the title.
2026-10-29 11:16:05 +00:00
Kirill Alekhnovich
4b4b2c4de7 fix: add approval id to plan (#1469)
Include the approval tool call ID in each Air plan ID so the client can
bind the plan to its review. Keep the legacy plan ID for other clients.
2026-10-28 16:34:01 +01:00
249 changed files with 33387 additions and 7764 deletions

125
.github/workflows/binaries.yml vendored Normal file
View File

@@ -0,0 +1,125 @@
# Builds the standalone executables (scripts/build-binary.mjs) and smoke-tests
# each one on a runner of its own platform. Each archive holds
# `claude-agent-acp` and the Claude CLI (`claude`) it runs from its directory.
# CI calls this to check that every target still builds and starts; the
# release calls it with `upload: true` and attaches the archives to the GitHub
# release.
name: Binaries
on:
workflow_call:
inputs:
upload:
description: Upload each archive as a workflow artifact for the release job.
type: boolean
default: false
permissions:
contents: read
jobs:
build:
name: Binary (${{ matrix.target }})
runs-on: ${{ matrix.runner }}
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
include:
- target: bun-linux-x64
runner: ubuntu-latest
archive: claude-agent-acp-linux-x64.tar.gz
- target: bun-linux-arm64
runner: ubuntu-24.04-arm
archive: claude-agent-acp-linux-arm64.tar.gz
# npm skips the musl package on a glibc host, so these are
# cross-built and smoke-tested in an Alpine container.
- target: bun-linux-x64-musl
runner: ubuntu-latest
archive: claude-agent-acp-linux-x64-musl.tar.gz
musl: true
- target: bun-linux-arm64-musl
runner: ubuntu-24.04-arm
archive: claude-agent-acp-linux-arm64-musl.tar.gz
musl: true
- target: bun-darwin-x64
runner: macos-15-intel
archive: claude-agent-acp-darwin-x64.zip
- target: bun-darwin-arm64
runner: macos-latest
archive: claude-agent-acp-darwin-arm64.zip
- target: bun-windows-x64
runner: windows-latest
archive: claude-agent-acp-windows-x64.zip
- target: bun-windows-arm64
runner: windows-11-arm
archive: claude-agent-acp-windows-arm64.zip
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1
with:
node-version: "lts/*"
check-latest: true
cache: "npm"
# Unpinned: each build is smoke-tested on its platform before it uploads,
# so a Bun release that breaks the executable fails here instead of
# shipping. Pin a version if one ever regresses past the smoke test.
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: latest
- run: npm ci
- name: Install the musl Claude CLI package
if: ${{ matrix.musl }}
run: |
version="$(node -p "require('./node_modules/@anthropic-ai/claude-agent-sdk/package.json').version")"
npm install --no-save --force "@anthropic-ai/claude-agent-sdk-linux-${RUNNER_ARCH,,}-musl@$version"
- name: Build
run: |
echo "Built \`${{ matrix.target }}\` with Bun $(bun --version)" >> "$GITHUB_STEP_SUMMARY"
node scripts/build-binary.mjs --target ${{ matrix.target }}
- name: Smoke test
if: ${{ !matrix.musl }}
run: node scripts/smoke-binary.mjs
# Claude Code's Alpine requirements: libgcc and libstdc++.
- name: Smoke test (Alpine)
if: ${{ matrix.musl }}
run: |
docker run --rm -v "$PWD:/work" -w /work node:lts-alpine \
sh -c 'apk add --no-cache libgcc libstdc++ >/dev/null && node scripts/smoke-binary.mjs'
- name: Archive
if: ${{ inputs.upload }}
env:
ARCHIVE: ${{ matrix.archive }}
run: |
cd dist-bin
if [[ "$ARCHIVE" == *.zip ]]; then
if [[ "$RUNNER_OS" == "Windows" ]]; then
pwsh -Command "Compress-Archive -Path claude-agent-acp.exe,claude.exe -DestinationPath ../$ARCHIVE"
else
zip "../$ARCHIVE" claude-agent-acp claude
fi
else
tar czf "../$ARCHIVE" claude-agent-acp claude
fi
- name: Upload
if: ${{ inputs.upload }}
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: ${{ matrix.archive }}
path: ${{ matrix.archive }}
retention-days: 7
if-no-files-found: error

View File

@@ -44,3 +44,7 @@ jobs:
- name: Run tests
run: npm run test:run
binaries:
name: Binaries
uses: ./.github/workflows/binaries.yml

View File

@@ -24,7 +24,7 @@ jobs:
steps:
# Config reference: https://github.com/amannn/action-semantic-pull-request#configuration
- uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1
- uses: amannn/action-semantic-pull-request@2252a14e810aa9b85c2d560bd7070c802206ecf6 # v6.1.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:

View File

@@ -88,6 +88,38 @@ jobs:
npm publish
echo "version=$version" >> "$GITHUB_OUTPUT"
# Standalone executables for the GitHub release, stable releases only. They
# don't gate the npm publish: a failed build leaves the release without
# binaries, and re-running the failed jobs attaches them.
build-binaries:
name: Build binaries
needs: [release-please]
if: ${{ needs.release-please.outputs.release_created == 'true' }}
# The called workflow only reads the repository; upload-binaries does the write.
permissions:
contents: read
uses: ./.github/workflows/binaries.yml
with:
upload: true
upload-binaries:
name: Attach binaries to the release
runs-on: ubuntu-latest
needs: [release-please, build-binaries]
permissions:
contents: write
timeout-minutes: 15
steps:
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
path: artifacts
merge-multiple: true
- name: Upload to the release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ needs.release-please.outputs.tag_name }}
run: gh release upload "$TAG" artifacts/* --clobber --repo "$GITHUB_REPOSITORY"
# Same shape as publish-npm. Tagging is a separate job, so nothing here needs
# write access to the repository.
publish-npm-preview:
@@ -199,7 +231,11 @@ jobs:
echo "Tagged \`v$VERSION\` at \`$SHA\`" >> "$GITHUB_STEP_SUMMARY"
trigger-registry-update:
needs: [publish-npm, publish-npm-preview]
# upload-binaries too, so a stable release's archives are attached before the
# registry reads the release: it takes each archive's sha256 from the release
# assets when it bumps the version. It only waits; a failed or skipped upload
# (every preview) doesn't stop the dispatch.
needs: [publish-npm, publish-npm-preview, upload-binaries]
# Both channels dispatch this: the registry has its own handling for preview
# versions. The two never fire together — a release merge publishes stable and
# skips the preview, and every other push does the reverse — so the registry sees

3
.gitignore vendored
View File

@@ -3,4 +3,5 @@ dist
.DS_Store
.claude/*.local.*
CLAUDE.local.md
.idea/
.idea/
dist-bin

View File

@@ -1,3 +1,3 @@
{
".": "0.99.0"
".": "0.104.1"
}

View File

@@ -1,7 +1,11 @@
# Agent guide
An ACP-compatible coding agent powered by the Claude Agent SDK. Source in `src/`,
TypeScript, ESM, Node >= 22.
TypeScript, ESM, Node >= 22. `src/v1/` holds the v1 agent (`ClaudeAcpAgent` in
`src/v1/acp-agent.ts`), `src/v2/` the v2 agent, and the rest of `src/` the modules
both share. The package exports only `src/lib.ts`. ESLint rejects imports between `src/v1/` and
`src/v2/`, and from shared modules into either agent. The entry points (`index.ts`, `lib.ts`,
`serve.ts`, `binary-entry.ts`) are exempt.
## Commands
@@ -16,13 +20,92 @@ npm run format # prettier --write
CI runs `format:check`, `lint`, `build` and `test:run`. Run `npm run check` before
opening a PR.
## Code comments
## Coding guidelines
Keep comments straightforward, concise, and concrete. Say what the code does and
why, in plain terms: name the actual state, call, or failure involved instead of
alluding to it. Prefer a short concrete example over an abstract description.
Don't use vague phrases like "keeps it honest" or "the residual case" that make
the reader reconstruct the meaning.
- Keep edits consistent with the surrounding code's formatting, naming and idioms.
- Keep comments concise and concrete. Name the actual state, call or failure
instead of alluding to it, prefer a short example over an abstract
description, and avoid vague phrases like "keeps it honest". A comment or doc
must be true on every code path it describes, and a doc that lists what a
result omits or rejects must name the actual cases.
- When a request can't be served, throw a `RequestError` whose message says what
happened and what to do (e.g. "Try again when the turn ends"). Don't drop the
request silently.
- Don't report success for work you start without awaiting it. If the detached
call can still refuse and its `.catch` only logs, check the refusal condition
first or report the failure to the client.
- A result must not depend on what an earlier call happened to cache. A request
right after a restart, with cold caches, must return the same thing as one
after a warm-up.
- If code relies on an ordering or call sequence that nothing enforces (e.g. an
echo arriving before the result), say so in a short comment.
## Architecture and design fit
- Before coding, read the code the change plugs into and find the module that
owns the state or concept. Build on that module, not beside it.
- Don't add new features or components to `src/v1/acp-agent.ts`. If no existing module fits
the new code cleanly, create a dedicated one that owns the feature's state and logic,
and keep the change in `src/v1/acp-agent.ts` to the calls that wire it in.
- Prefer extending an existing mechanism over adding a new one, even if it takes
a small refactor (moving a helper, generalizing a function). Add a parallel
path, special case or flag only when the existing structure can't support the
change.
- Before writing new logic, check whether it already exists in a sibling handler
of the same flow or in a helper that works with the same data, and reuse it.
Paths that handle the same input must apply the same rules through the same
code: the live stream and the `session/load` replay, the command list and
command dispatch, `prompt` and `steer`. Two copies of one check drift apart.
- Put guards, filters and cleanup in the module that owns the state, so every
caller gets them. If guards, flags or fallbacks pile up in one place,
restructure instead of adding another.
- If a result is meant to match another request's output, reuse that request's
logic.
- The adapter serves ACP v1 and v2. A change that is not meant for one protocol
version only must work correctly in both. `ClaudeAcpAgent` serves v1, and
`src/v2/` serves v2; they share modules (the SDK mapping, tool call
rendering, permissions, config options). Put a change meant for both versions
in the shared module, or make it in both cores.
- A session runs one turn at a time. On v1, "Claude is working" means
`startTurn`'s check (an unsettled turn in `turnQueue`, or `foregroundWork` for
the session); on v2, an open turn of the v2 session. Don't write a
narrower one. Anything that sends `session/update` must go
through the turn queue or be refused while the session is busy, or its output
lands inside another turn. Keep the session marked busy across awaited network
calls.
- Don't branch on a local copy of state that Claude Code or a remote service
owns: it goes stale when the other side changes. Have a command set the state
the user asks for instead of toggling it.
- A command or name the adapter adds must not shadow one that Claude Code or the
user's own commands and skills already provide.
- New optional request fields and `_meta` keys must not reject requests that
were valid before.
- If a workaround or new path is unavoidable (e.g. a Claude Code or SDK
limitation), say why in a short code comment and in the PR description.
## Testing guidelines
- Tests live in `src/tests/`: `v1/` holds the tests that drive the v1 agent,
`v2/` the tests of the v2 agent, and the top level the tests of shared
modules, with `helpers.ts`, the scenario harness (`acp-scenarios/`) and
`fixtures/`. Before adding a test, search `src/tests/` for existing tests of
the same API and scenario, and add to their file and `describe` block. Use
the helpers in `helpers.ts` and `v1/session-doubles.ts` instead of copying
setup.
- Don't add cases that take the same code path. Tests that differ only in input
and expected output belong in one `it.each`. Don't repeat another test's
assertions; assert only what this test is about.
- Reach a state the shortest way (e.g. set `session.tornDown = true`) instead of
driving a full flow with extra mocks, unless the flow is what you test.
- Build test doubles from what the bundled Claude Code CLI really sends,
including fields like `origin` it echoes back and fields it omits. A double
simpler than the real message hides bugs. If unsure, check against the real
CLI (`npm run test:integration`).
- Cover both ACP v1 and v2 when a change affects what a client receives
(updates, responses, replay). A change to internal logic only does not need
its tests duplicated per version.
- For each behavior the docs or comments promise, including what is omitted or
rejected, have a test that would fail if it broke.
## Pull requests

View File

@@ -1,5 +1,130 @@
# Changelog
## [0.104.1](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.104.0...v0.104.1) (2026-11-10)
### Bug Fixes
* carry the auth detail in the ACP account label ([#1573](https://github.com/agentclientprotocol/claude-agent-acp/issues/1573)) ([3d6eb7b](https://github.com/agentclientprotocol/claude-agent-acp/commit/3d6eb7b5b650a1f9e5d2e869e92e313c32dbdc86))
* distinguish Ultra from Max in auth status ([#1574](https://github.com/agentclientprotocol/claude-agent-acp/issues/1574)) ([7d95d30](https://github.com/agentclientprotocol/claude-agent-acp/commit/7d95d30ec3a4813d580b3bfdedb08f03d93a3571))
* update to @anthropic-ai/claude-agent-sdk 0.3.322 ([#1577](https://github.com/agentclientprotocol/claude-agent-acp/issues/1577)) ([b89e449](https://github.com/agentclientprotocol/claude-agent-acp/commit/b89e4497da9184072e7f3c4bb28de79801659222))
## [0.104.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.103.0...v0.104.0) (2026-11-10)
### Features
* implement the ACP auth/status RFD next to _auth/status_update ([#1570](https://github.com/agentclientprotocol/claude-agent-acp/issues/1570)) ([d202748](https://github.com/agentclientprotocol/claude-agent-acp/commit/d20274897684e8813477389b83a268f2b4f4ad11))
### Bug Fixes
* offer Anthropic Console login in remote environments ([#1572](https://github.com/agentclientprotocol/claude-agent-acp/issues/1572)) ([c09543d](https://github.com/agentclientprotocol/claude-agent-acp/commit/c09543d85ae1067e8df315f1cd3179527701ea16))
## [0.103.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.102.0...v0.103.0) (2026-11-10)
### ⚠ BREAKING CHANGES
* deep imports of `dist/` files fail with `ERR_PACKAGE_PATH_NOT_EXPORTED`, and `lib.ts` exports only `serveAcp`, `AUTH_STATUS_UPDATE_METHOD` and their types.
### Features
* serve ACP v2 with a direct v2 agent and remove the v1 bridge ([#1560](https://github.com/agentclientprotocol/claude-agent-acp/issues/1560)) ([cb285cd](https://github.com/agentclientprotocol/claude-agent-acp/commit/cb285cd2857924c07038c129cc836da007e2737f))
### Bug Fixes
* give subagent checklists their own plan ID ([#1558](https://github.com/agentclientprotocol/claude-agent-acp/issues/1558)) ([2f7e386](https://github.com/agentclientprotocol/claude-agent-acp/commit/2f7e386f3600658d905906f3b37301120b07605f))
### Code Refactoring
* export only lib.ts, with serveAcp and the authStatus wire names ([#1566](https://github.com/agentclientprotocol/claude-agent-acp/issues/1566)) ([566e265](https://github.com/agentclientprotocol/claude-agent-acp/commit/566e2652a9da5a31dab57a23ea1b06e9fff2ead8))
## [0.102.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.101.1...v0.102.0) (2026-11-09)
### Features
* list workflow phases and agents on the workflow card ([#1552](https://github.com/agentclientprotocol/claude-agent-acp/issues/1552)) ([176ae06](https://github.com/agentclientprotocol/claude-agent-acp/commit/176ae0634c2ef7cb0549339f4b9824884574b2f0))
* send the checklist as a plan operation and remove plans on reset ([#1554](https://github.com/agentclientprotocol/claude-agent-acp/issues/1554)) ([766fd14](https://github.com/agentclientprotocol/claude-agent-acp/commit/766fd142f60bda2f434d55baaa8e72939f215800))
### Bug Fixes
* report v2 background tasks without a tool call as their own tool call ([#1556](https://github.com/agentclientprotocol/claude-agent-acp/issues/1556)) ([d098fb0](https://github.com/agentclientprotocol/claude-agent-acp/commit/d098fb08d54f521d5056d920ebcc5a2656334083))
* update to @anthropic-ai/claude-agent-sdk 0.3.321 ([#1557](https://github.com/agentclientprotocol/claude-agent-acp/issues/1557)) ([855447c](https://github.com/agentclientprotocol/claude-agent-acp/commit/855447c7cd41aa566bdf361d6d59e6a5b038fe6e))
## [0.101.1](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.101.0...v0.101.1) (2026-11-09)
### Bug Fixes
* keep autonomous turn tasks open across owed trailing idles ([#1455](https://github.com/agentclientprotocol/claude-agent-acp/issues/1455)) ([1ee2422](https://github.com/agentclientprotocol/claude-agent-acp/commit/1ee242293401372aa71ea5765e8dfd0679146dc9))
* list a resumed Fable 5.1 session's model as `fable` ([#1548](https://github.com/agentclientprotocol/claude-agent-acp/issues/1548)) ([4cceb06](https://github.com/agentclientprotocol/claude-agent-acp/commit/4cceb06b31525ccaaecfd999ff9b21d401b2659a))
* list a subagent's background Bash commands ([#1540](https://github.com/agentclientprotocol/claude-agent-acp/issues/1540)) ([6597626](https://github.com/agentclientprotocol/claude-agent-acp/commit/6597626d86ddb8c10ea9f1091d921806752d045a))
* list Monitor calls in the session's background work ([#1538](https://github.com/agentclientprotocol/claude-agent-acp/issues/1538)) ([ba16234](https://github.com/agentclientprotocol/claude-agent-acp/commit/ba162346c9ccb4be51f13a3271198351d7822e78))
* relay a rate_limit_event that arrives before the turn's first assistant frame ([#1534](https://github.com/agentclientprotocol/claude-agent-acp/issues/1534)) ([54392e1](https://github.com/agentclientprotocol/claude-agent-acp/commit/54392e144d307d5dd5b7732ae14634d4b2678bd5))
* render workflow tool cards and progress correctly in ACP v2 ([#1550](https://github.com/agentclientprotocol/claude-agent-acp/issues/1550)) ([30600ba](https://github.com/agentclientprotocol/claude-agent-acp/commit/30600ba348e5f5c3755a50f5797fd24645e0ff48))
* replay a compaction summary by its flag, not its framing ([#1536](https://github.com/agentclientprotocol/claude-agent-acp/issues/1536)) ([aa0c123](https://github.com/agentclientprotocol/claude-agent-acp/commit/aa0c1234b5c52a63d96c46c9826631bd0f428062))
* report setup-token logins as logged in ([#1376](https://github.com/agentclientprotocol/claude-agent-acp/issues/1376)) ([7d1ef76](https://github.com/agentclientprotocol/claude-agent-acp/commit/7d1ef76c1d32e390f8ff8824087df3003825a170))
* start transcript notices on their own line for clients without session.notices ([#1362](https://github.com/agentclientprotocol/claude-agent-acp/issues/1362)) ([47b64c0](https://github.com/agentclientprotocol/claude-agent-acp/commit/47b64c04de83145c4d42033c4408c811736c9c40))
* **test:** assert the blockquoted MCP startup transcript line ([#1543](https://github.com/agentclientprotocol/claude-agent-acp/issues/1543)) ([3b22586](https://github.com/agentclientprotocol/claude-agent-acp/commit/3b22586cf63cef06f1029a8418a9abb3b2cc3236))
* update to @agentclientprotocol/sdk 2.5.0 ([#1549](https://github.com/agentclientprotocol/claude-agent-acp/issues/1549)) ([ef65ab3](https://github.com/agentclientprotocol/claude-agent-acp/commit/ef65ab31b405a5d339133c980eb606a70a2d3624))
* update to @anthropic-ai/claude-agent-sdk 0.3.319 ([#1545](https://github.com/agentclientprotocol/claude-agent-acp/issues/1545)) ([a58293f](https://github.com/agentclientprotocol/claude-agent-acp/commit/a58293f4c9aa559a500d61834ef97d2a23b3722b))
## [0.101.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.100.0...v0.101.0) (2026-11-05)
### Features
* compact idle sessions before the prompt cache expires ([#1522](https://github.com/agentclientprotocol/claude-agent-acp/issues/1522)) ([a79d073](https://github.com/agentclientprotocol/claude-agent-acp/commit/a79d07324dc15808c94a889a3b60615cca5a9fbd)), closes [#1454](https://github.com/agentclientprotocol/claude-agent-acp/issues/1454) [#1518](https://github.com/agentclientprotocol/claude-agent-acp/issues/1518)
* ship single-file executables built with bun ([#1524](https://github.com/agentclientprotocol/claude-agent-acp/issues/1524)) ([e3f0b6c](https://github.com/agentclientprotocol/claude-agent-acp/commit/e3f0b6c89c783939e6ca8c19c220c21b58263f46))
* start sessions in Auto mode by default ([#1533](https://github.com/agentclientprotocol/claude-agent-acp/issues/1533)) ([7ea9eec](https://github.com/agentclientprotocol/claude-agent-acp/commit/7ea9eec8392a43191b25a5c3636c181cc7eb42f3))
* stream the output of v2 background commands ([#1512](https://github.com/agentclientprotocol/claude-agent-acp/issues/1512)) ([53e3eec](https://github.com/agentclientprotocol/claude-agent-acp/commit/53e3eec0134209654f53c46f319aaac851b8fff0))
### Bug Fixes
* honor configured fallback for unavailable restored modes ([#1520](https://github.com/agentclientprotocol/claude-agent-acp/issues/1520)) ([8bbdea6](https://github.com/agentclientprotocol/claude-agent-acp/commit/8bbdea6cc62a7ae080cb73ff476ed28ad7574018))
* update to @anthropic-ai/claude-agent-sdk 0.3.318 ([#1525](https://github.com/agentclientprotocol/claude-agent-acp/issues/1525)) ([1d4418f](https://github.com/agentclientprotocol/claude-agent-acp/commit/1d4418f80daf14c29cecc5b23286f52c7f0402ec))
## [0.100.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.99.1...v0.100.0) (2026-11-02)
### Features
* list v2 background tool calls ([#1511](https://github.com/agentclientprotocol/claude-agent-acp/issues/1511)) ([4d79b9f](https://github.com/agentclientprotocol/claude-agent-acp/commit/4d79b9ffbce95c23928a5dd771d6c87476d37160))
* report subagent working directories ([#1496](https://github.com/agentclientprotocol/claude-agent-acp/issues/1496)) ([eb78afa](https://github.com/agentclientprotocol/claude-agent-acp/commit/eb78afa8556b8fc4082495e48e3fd1762fb6a50f))
* report v2 background work ([#1510](https://github.com/agentclientprotocol/claude-agent-acp/issues/1510)) ([5565259](https://github.com/agentclientprotocol/claude-agent-acp/commit/5565259771cb74e3a9bdbacfcbce43e061d81422))
* report why Claude Code could not start a session ([#1508](https://github.com/agentclientprotocol/claude-agent-acp/issues/1508)) ([38cf656](https://github.com/agentclientprotocol/claude-agent-acp/commit/38cf656fc9d80fec2d1a16c472779803e2c802d2))
### Bug Fixes
* align session/cancel, session/close, and v2 setup with the ACP v2 spec ([#1494](https://github.com/agentclientprotocol/claude-agent-acp/issues/1494)) ([f50598a](https://github.com/agentclientprotocol/claude-agent-acp/commit/f50598a39c5dc3908d4b98a7213e01d63ea867d5))
* forward v2 elicitation values and read dismissed v2 permission prompts as a refusal ([#1493](https://github.com/agentclientprotocol/claude-agent-acp/issues/1493)) ([3bc6711](https://github.com/agentclientprotocol/claude-agent-acp/commit/3bc671193c6456516fe05a0eaa53c17bdfb6a190))
* keep native Windows paths in v2 git patches ([#1492](https://github.com/agentclientprotocol/claude-agent-acp/issues/1492)) ([c3428de](https://github.com/agentclientprotocol/claude-agent-acp/commit/c3428de1bf411c513b0703ddd56bb6c5fdff583c))
* preserve live session settings across provider refreshes ([#1474](https://github.com/agentclientprotocol/claude-agent-acp/issues/1474)) ([e01a288](https://github.com/agentclientprotocol/claude-agent-acp/commit/e01a28838499dd6dd7c31e35708090b938e852c6))
* reject unreadable skill manifests ([#1482](https://github.com/agentclientprotocol/claude-agent-acp/issues/1482)) ([99a6e76](https://github.com/agentclientprotocol/claude-agent-acp/commit/99a6e76158b9164e51bd62e3b89113beadea5ce2))
* report the fresh context's usage when ExitPlanMode clears context ([#1484](https://github.com/agentclientprotocol/claude-agent-acp/issues/1484)) ([2fd46c8](https://github.com/agentclientprotocol/claude-agent-acp/commit/2fd46c866d4ff2939fe1b1245b15a536d6de7c0b))
* update to @agentclientprotocol/sdk 2.2.0 ([#1491](https://github.com/agentclientprotocol/claude-agent-acp/issues/1491)) ([1d2b92f](https://github.com/agentclientprotocol/claude-agent-acp/commit/1d2b92feae5eeccde3ac738edca1f1d71250762e))
* update to @agentclientprotocol/sdk 2.4.0 ([#1509](https://github.com/agentclientprotocol/claude-agent-acp/issues/1509)) ([720bbb3](https://github.com/agentclientprotocol/claude-agent-acp/commit/720bbb3e4c65874775a0bb9418799baf4b6e3680))
* update to Claude Agent SDK 0.3.316 (Claude Code 2.1.316) ([#1507](https://github.com/agentclientprotocol/claude-agent-acp/issues/1507)) ([1d03064](https://github.com/agentclientprotocol/claude-agent-acp/commit/1d0306468b632c6a56b4fb076ad7a19f23d9e7b5))
* validate session, prompt, config, and auth requests per the latest ACP spec ([#1495](https://github.com/agentclientprotocol/claude-agent-acp/issues/1495)) ([d2ef490](https://github.com/agentclientprotocol/claude-agent-acp/commit/d2ef490a318033049af00cb9164a39ac2dd1eb95))
## [0.99.1](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.99.0...v0.99.1) (2026-10-29)
### Bug Fixes
* add approval id to plan ([#1469](https://github.com/agentclientprotocol/claude-agent-acp/issues/1469)) ([4b4b2c4](https://github.com/agentclientprotocol/claude-agent-acp/commit/4b4b2c4de7c492070f6260cacea5680b2e045a5a))
* allow forking sessions into a different directory ([#1483](https://github.com/agentclientprotocol/claude-agent-acp/issues/1483)) ([8bd0bc9](https://github.com/agentclientprotocol/claude-agent-acp/commit/8bd0bc9b03fb4a30e0b1755128baad12a214a58d))
* detect "Run /login" login-expired messages as authRequired ([#1481](https://github.com/agentclientprotocol/claude-agent-acp/issues/1481)) ([fccaebc](https://github.com/agentclientprotocol/claude-agent-acp/commit/fccaebcf87e8f37144ec1a64920c0c53997edabe))
* mark plugin, bundled and nested Claude skills as skills for AIR ([#1473](https://github.com/agentclientprotocol/claude-agent-acp/issues/1473)) ([52c241e](https://github.com/agentclientprotocol/claude-agent-acp/commit/52c241e71512ec19c4dd79c95c51ce67f04d7aac))
* title subagent sessions by their task ([#1479](https://github.com/agentclientprotocol/claude-agent-acp/issues/1479)) ([7dcb629](https://github.com/agentclientprotocol/claude-agent-acp/commit/7dcb62923d38ac24e386b0626fe123334341c445))
## [0.99.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.98.0...v0.99.0) (2026-10-28)

View File

@@ -36,6 +36,23 @@ To try changes that have landed on `main` but are not released yet, install from
npm install @agentclientprotocol/claude-agent-acp@preview
```
### Standalone executables
Each release attaches archives for Linux (glibc and musl), macOS and Windows, on
x64 and arm64, to its [GitHub release](https://github.com/agentclientprotocol/claude-agent-acp/releases).
They need no Node.js install. Each holds two files: `claude-agent-acp`, and the
Claude CLI (`claude`) it runs. Keep them in the same directory; a symlink to
`claude-agent-acp` elsewhere works. To use a Claude CLI you already have
instead, set `CLAUDE_CODE_EXECUTABLE`. On Alpine, install `libgcc` and
`libstdc++` first.
To build one for your machine (needs [Bun](https://bun.sh)) and check it:
```sh
npm run build:binary
npm run smoke:binary # add `-- --full` to drive a real session; needs a Claude login
```
### Subagent sessions
A client that declares `clientCapabilities.subagents`, other than JetBrains AIR, gets each Agent or
@@ -48,6 +65,14 @@ Task subagent as its own child session, following the [subagents RFD](https://gi
`end_turn` or `cancelled` when the SDK gives the reason. A failure is `idle` with the `error`
stop reason and the SDK's error text as its `error`. The child keeps its session for every
delegation to it.
- A child that runs outside its parent's working directory, such as an Agent call with
`isolation: "worktree"`, gets its `cwd` on `subagent_update` before its first tool call. The
adapter takes it from the `cwd` of the child's own hooks: its SubagentStart hook runs before its
first model request. It sends `cwd: null` when the child is back in its parent's directory. A
child in its parent's directory gets no `cwd`, and no child gets `additionalDirectories`: Claude
Code gives a subagent the session's additional directories, so it inherits them. A replay
announces a child with the `cwd` that its transcript recorded, and clears it after the response
when that directory no longer exists.
- Session messages replace the Agent or Task call that launches the child, which its parent is
not shown:
- The launch's prompt is a `session_message` in both transcripts: the parent's outgoing view and
@@ -117,7 +142,11 @@ On v2 the adapter serves the session baseline (`session/new`, `session/list`, `s
with replay, `session/close`, `session/prompt`, `session/cancel`), `session/delete`,
`session/fork`, additional directories, config options (the mode is one), plans, notices,
compaction, display terminals for commands, permission requests, elicitation, providers, auth, and
[subagents](#subagent-sessions). Steering and the AIR extensions are v1 only for now.
[subagents](#subagent-sessions). Steering is v1 only for now.
v2 is served by the v2 agent in `src/v2/`, which runs straight on Claude
Code instead of translating to v1. A v2 client that declares JetBrains AIR
(`_meta.jetbrains.air`) also gets the AIR extensions that ACP v2 does not cover.
## Contribution Policy

View File

@@ -7,7 +7,9 @@ keeps a single release PR open, titled `chore(main): release X.Y.Z` and labelled
`autorelease: pending`.
Merging that PR is what releases. It tags `vX.Y.Z`, creates the GitHub release,
publishes to npm, and dispatches a version update to the agent registry.
publishes to npm, dispatches a version update to the agent registry, and
attaches the [standalone executables](../README.md#standalone-executables) to
the GitHub release. Previews get no executables.
There is no manual release button, and versions are never typed in by hand: the
version is an output of the commit history, not an input.
@@ -45,6 +47,9 @@ gh release view "v<version>"
npm view "@agentclientprotocol/claude-agent-acp@<version>"
```
The release should list 8 `claude-agent-acp-*` archives. They are attached a
few minutes after the release is created, once every platform has built.
## Preview releases
Every push to `main` that is not a release merge publishes a preview from the
@@ -203,6 +208,13 @@ npm versions are immutable. If the package already published and only the
registry update failed, pass `-f publish_npm=false` so the run skips publishing
and only re-dispatches the registry update.
### The release has no executables
`build-binaries` and `upload-binaries` don't gate npm, so a failed build only
leaves the release without archives. Re-run the failed jobs of that publish run
(`gh run rerun <run-id> --failed`); the upload uses `--clobber`, so archives
already attached are replaced rather than duplicated.
### A preview published but the commit was not tagged
Only the publish is irreversible, so re-run just the tag job:

View File

@@ -78,7 +78,7 @@ A page with more sessions after it has a `nextCursor`, as ACP defines.
A client that does not follow `nextCursor` sees only the newest 1000 sessions of the directory.
An unknown cursor gives an `invalidParams` error.
`src/tests/acp-scenarios.test.ts` compares the traffic of a plain client and of Zed with recordings of origin/main.
`src/tests/v1/acp-scenarios.test.ts` compares the traffic of a plain client and of Zed with recordings of origin/main.
## Negotiation
@@ -145,6 +145,15 @@ The agent list does not depend on the capabilities that AIR declares, except `se
An extension is active only when the client declared its capability. The client does not declare `sessionBatchGet`, `sessionArchive`, `sessionRename`, `sessionListSubscribe` and `sessionWorktrees`.
The response to a client that is not AIR has no `_meta.jetbrains` key.
### ACP v2
The v2 core (`src/v2/`) serves an AIR client only the extensions that ACP v2 does not cover.
Its `initialize` response lists `agentFileChangeReport` and `recommendedValue`.
It also serves the [permission presentation](#permission-presentation), the [question custom answers](#question-custom-answers), the [predicted follow-up prompt](#predicted-follow-up-prompt), and [load replay end](#load-replay-end).
The [agent file-change report](#agent-file-change-report) goes out at the end of the turn that serves the prompt, before the turn's idle `state_update`, because a v2 `session/prompt` is answered when Claude Code takes the prompt in.
ACP v2 covers subagents, background work, file plans, patches, compaction, the system prompt, and turn failures itself, so the v2 core serves no `nativeSubagentSessions`, `backgroundSubagents`, `asyncTasks`, `planFile`, `diffPatch`, `contextCompaction`, `customInstructions`, or `sessionFailure`.
It does not serve the [tool call contract](#tool-call-contract) yet.
### Capabilities
| Capability | Advertised | What the adapter does when the client declares it | Section |
@@ -219,7 +228,7 @@ The adapter keeps them where they are.
## Auth source display
The connection-scoped `_auth/status_update` continues to report agent-owned authentication, not client-owned `providers/set` routing. When the active key source is `apiKeyHelper`, the adapter reads the effective helper command through SDK `resolveSettings` / the session's `SettingsManager`, including programmatic settings overrides.
The connection-scoped `_auth/status_update` continues to report agent-owned authentication, not client-owned `providers/set` routing. The adapter also reports the same identity through the ACP `auth/status` method and `auth/status_update` notification (advertised as `auth.status`), with `kind`, `detail` and `plan` under the account's `_meta.claudeCode`. The account's `label` also carries a `detail` in parentheses, e.g. `Anthropic API key (ANTHROPIC_API_KEY)`. Each notification is sent when its own payload changed. `auth/status` answers with the identity last reported and starts a new read, whose change arrives as a notification. When the active key source is `apiKeyHelper`, the adapter reads the effective helper command through SDK `resolveSettings` / the session's `SettingsManager`, including programmatic settings overrides.
A command containing the whole word `central` and the word sequence `proxy start` is displayed as:
@@ -231,6 +240,8 @@ Matching is case-insensitive and accepts multiple whitespace characters between
Other active helpers use `label: "Anthropic API key"` and `detail: "API key helper"`; unknown key-source identifiers are not exposed as display text. CLI and SDK reads share this presentation, retain stale-probe protection, and suppress unchanged notifications.
For Max subscriptions, the existing `claude auth status --json` probe's optional `planTier` distinguishes `max_20x` (displayed as `Claude Ultra`) from `max_5x` (`Claude Max`). Missing, unrecognized, or malformed tiers fall back to the subscription's usual label. Other subscriptions and auth sources keep their existing classification. The raw `account.plan` and ACP `_meta.claudeCode.plan` remain unchanged. SDK account reads lack the tier, so they preserve the known label for the same account and plan; a fresh CLI read replaces it, including when the tier is absent. This is display metadata, not an entitlement check, and requires no additional probe or direct credential-file reads.
## Zed conventions
The adapter keeps these Zed and upstream conventions unchanged for every client.
@@ -515,6 +526,17 @@ While the adapter does not know the path, a report carries no `planFilePath` key
The adapter never sends a blank path, because AIR reads a present non-file value as a retraction.
Without `rawInputRendering`, a `planFile` client gets no copy of the plan in `content` either.
### Approved plan identity
For an AIR client with plan operations, the approved plan ID is `plan-mode:<tool-call ID>`.
Claude uses the SDK tool-call ID of ExitPlanMode.
Each review keeps its own plan. Repeated updates of that review keep the same plan ID.
AIR joins the plan to its review by ID, even when the text includes user edits.
This link needs no extra metadata.
The live result, history replay, and clear-context approval use the same identity.
The identity does not depend on `planFile`: it also applies when the request carries inline plan text.
Other clients keep the `plan-mode` ID.
### Fallback
The plan text goes out as before in these cases:
@@ -537,7 +559,7 @@ AIR puts the hooks into `_meta.claudeCode.options.settings.hooks` of `session/ne
```
- The settings go to the flag settings tier of that session only. Other sessions and the settings files do not change.
- Claude Code runs the hooks of every settings tier, so the user, project and IDE hooks of one event all run. `src/tests/session-hooks.test.ts` checks this with the Claude Code binary.
- Claude Code runs the hooks of every settings tier, so the user, project and IDE hooks of one event all run. `src/tests/v1/session-hooks.test.ts` checks this with the Claude Code binary.
- `CLAUDE_MODEL_CONFIG` stays in effect (see [Model configuration](model-configuration.md#precedence)).
- A `session/load` with different settings recreates the query, so it applies the new hooks.
- Claude asks for no hook trust step.
@@ -1051,7 +1073,8 @@ The adapter ignores a malformed request and a repeated request id.
### Report
The adapter sends one `session_info_update` before the `PromptResponse`:
The adapter sends one `session_info_update` before the `PromptResponse`.
On ACP v2 it comes before the idle `state_update` of the turn that serves the prompt (see [ACP v2](#acp-v2)):
```json
{
@@ -1558,7 +1581,7 @@ It is idempotent, and an unknown id also returns `{}`.
`session/delete`:
- a `sessionIndex` client: deletes every transcript of the session (empty ones too) and every `<sessionId>/` directory, also one left without a transcript by an earlier failed delete. Anything left behind fails the request. A session that runs here is closed first. As with the CLI, the session is deleted also when another Claude Code process has it open. An unknown session is `-32002`.
- an AIR client without `sessionIndex`: archives the session instead of deleting it (the title records of [Archive](#archive)), because AIR uses delete for "Done" and can reopen the session. A session that runs here is closed first. The list hides archived sessions from such a client too: those whose custom title, as the SDK list reports it, is archived. A session without a transcript fails with the error of the SDK delete, as before.
- an AIR client without `sessionIndex`: archives the session instead of deleting it (the title records of [Archive](#archive)), because AIR uses delete for "Done" and can reopen the session. A session that runs here is closed first. The list hides archived sessions from such a client too: those whose custom title, as the SDK list reports it, is archived. A session without a transcript has nothing to archive, so the delete succeeds, as ACP asks of a session that does not exist.
A `sessionIndex` client sees such a session as archived (`archived: true`), as AIR's native Claude path does.
- any other client: the SDK delete. A session without a transcript (unknown, deleted before, or
never prompted) succeeds, as ACP asks.
@@ -1627,7 +1650,8 @@ The hold protects a client that stops listening at the prompt response.
### Behavior with the capability
AIR declares `backgroundSubagents` in `initialize.clientCapabilities._meta.jetbrains.air.capabilities`.
The capability applies to ACP v1 only. ACP v2 keeps the hold, and the agent does not advertise the capability to ACP v2.
The capability applies to ACP v1 only, and the agent does not advertise it to ACP v2.
ACP v2 has no hold: the v2 agent answers `session/prompt` when Claude Code takes the prompt in, lists a background subagent in its parent session's `background` while it works, and reports each subagent's work in the state of its child session (see [ACP v2](#acp-v2)).
- The turn ends at its result with the stop reason of the result, usually `end_turn`.
- The subagent updates, permission requests and elicitations go out after the prompt response.
@@ -1668,14 +1692,14 @@ For AIR, both forms carry `_meta.jetbrains.air.contextCompaction`:
}
```
| Field | Meaning |
| ------------ | --------------------------------------------------------------------- |
| `version` | Must equal `1`. |
| `trigger` | `manual` or `automatic`. |
| `preTokens` | The context size before the compaction. |
| `postTokens` | The context size after the compaction. |
| `durationMs` | The duration of the compaction. |
| `error` | The error of a failed compaction. Only the tool call form carries it. |
| Field | Meaning |
| ------------ | -------------------------------------------------------------------------------------------------------- |
| `version` | Must equal `1`. |
| `trigger` | `manual`, `automatic`, or `idle`: the adapter compacted an idle session before its prompt cache expired. |
| `preTokens` | The context size before the compaction. |
| `postTokens` | The context size after the compaction. |
| `durationMs` | The duration of the compaction. |
| `error` | The error of a failed compaction. Only the tool call form carries it. |
The standard `toolCallId` or `compactionId` and the `status` own the identity and the phase.
The `compaction_update` form puts the error in the standard `error` field.

View File

@@ -4,6 +4,23 @@ import tsparser from "@typescript-eslint/parser";
import prettierConfig from "eslint-config-prettier";
import globals from "globals";
/**
* Rules that reject a module specifier matching `regex` in static imports and
* re-exports (type-only ones too), `import()` calls and `import("...")` types.
*/
function protocolBoundary(regex, message) {
// esquery regex literals end at the first `/`, so spell it `\x2F`.
const literal = `/${regex.replaceAll("/", "\\x2F")}/`;
return {
"no-restricted-imports": ["error", { patterns: [{ regex, message }] }],
"no-restricted-syntax": [
"error",
{ selector: `ImportExpression[source.value=${literal}]`, message },
{ selector: `TSImportType[argument.literal.value=${literal}]`, message },
],
};
}
export default [
{
ignores: ["node_modules/", "dist/", "coverage/", "*.min.js", "*.config.js", ".github/"],
@@ -44,5 +61,38 @@ export default [
curly: ["error", "all"],
},
},
// `src/v2/` must not import the v1 agent.
{
files: ["src/v2/**/*.ts"],
rules: protocolBoundary(
"^(\\.\\./)+v1/",
"src/v2/ must not import the v1 agent. Move what both versions need into a shared module in src/.",
),
},
// `src/v1/` must not import the v2 agent.
{
files: ["src/v1/**/*.ts"],
rules: protocolBoundary(
"^(\\.\\./)+v2/",
"src/v1/ must not import the v2 agent. Move what both versions need into a shared module in src/.",
),
},
// Shared modules must not import either agent. The entry points wire the agents up.
{
files: ["src/**/*.ts"],
ignores: [
"src/v1/**",
"src/v2/**",
"src/tests/**",
"src/index.ts",
"src/lib.ts",
"src/serve.ts",
"src/binary-entry.ts",
],
rules: protocolBoundary(
"^(\\./|(\\.\\./)+)(v1|v2)/",
"Shared modules must not import src/v1/ or src/v2/. Move the code into the shared module, or pass it in from the agent.",
),
},
prettierConfig,
];

492
package-lock.json generated
View File

@@ -1,16 +1,16 @@
{
"name": "@agentclientprotocol/claude-agent-acp",
"version": "0.99.0",
"version": "0.104.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@agentclientprotocol/claude-agent-acp",
"version": "0.99.0",
"version": "0.104.1",
"license": "Apache-2.0",
"dependencies": {
"@agentclientprotocol/sdk": "2.1.0",
"@anthropic-ai/claude-agent-sdk": "0.3.312",
"@agentclientprotocol/sdk": "2.5.0",
"@anthropic-ai/claude-agent-sdk": "0.3.322",
"diff": "9.0.0",
"zod": "4.6.7"
},
@@ -18,29 +18,29 @@
"claude-agent-acp": "dist/index.js"
},
"devDependencies": {
"@anthropic-ai/sdk": "1.3.0",
"@anthropic-ai/sdk": "1.7.0",
"@eslint/js": "10.0.1",
"@tsconfig/node22": "22.0.6",
"@types/node": "26.6.5",
"@typescript-eslint/eslint-plugin": "8.72.1",
"@typescript-eslint/parser": "8.72.1",
"@types/node": "26.6.6",
"@typescript-eslint/eslint-plugin": "8.72.2",
"@typescript-eslint/parser": "8.72.2",
"ajv": "8.20.0",
"ajv-formats": "3.0.1",
"eslint": "10.13.0",
"eslint": "10.14.0",
"eslint-config-prettier": "10.1.8",
"globals": "17.13.0",
"globals": "17.14.0",
"prettier": "3.9.10",
"typescript": "6.0.3",
"vitest": "5.0.5"
"vitest": "5.1.0"
},
"engines": {
"node": ">=22"
}
},
"node_modules/@agentclientprotocol/sdk": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-2.1.0.tgz",
"integrity": "sha512-IuKjJmHLPzbmbg26eK9Jopd8AVxgLuu8fX114YzYAU6tLYclCo2DUzLL4vbCpYGr9mzvs2T0kigE3ZCueL06tg==",
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-2.5.0.tgz",
"integrity": "sha512-MFlXeIuwN6nZSK45SAsSnVjbLgQ2r4fnthoF1lQoGJP3PdaPRpOZeCF2Dds8S4cN8yZP5QbxB1bkJccGskmcYw==",
"license": "Apache-2.0",
"peerDependencies": {
"@modelcontextprotocol/server": "^2.4.0",
@@ -53,22 +53,22 @@
}
},
"node_modules/@anthropic-ai/claude-agent-sdk": {
"version": "0.3.312",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.312.tgz",
"integrity": "sha512-Ilj5Agx3ak/Z0UPZNLS7+CTnYMa+Ijf8+FKelh9MKGRwHVYyfvmJgL0fsmooI78SdkZwqjI5a4CwOAsJurdjJA==",
"version": "0.3.322",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.322.tgz",
"integrity": "sha512-r88ij7R3SPV2j3BtFuavgrbWZNE8SVVS42AOQ1KxlS+QnSwNNnUhmVXV3Es2dniZySBYX8ENebL43ffhiB0azw==",
"license": "SEE LICENSE IN README.md",
"engines": {
"node": ">=18.14.0"
},
"optionalDependencies": {
"@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.312",
"@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.312",
"@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.312",
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.312",
"@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.312",
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.312",
"@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.312",
"@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.312"
"@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.322",
"@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.322",
"@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.322",
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.322",
"@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.322",
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.322",
"@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.322",
"@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.322"
},
"peerDependencies": {
"@anthropic-ai/sdk": ">=0.93.0",
@@ -77,9 +77,9 @@
}
},
"node_modules/@anthropic-ai/claude-agent-sdk-darwin-arm64": {
"version": "0.3.312",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.312.tgz",
"integrity": "sha512-AVmdMOuoeE3Y9f4wAIPcEAAS0OFaZw1L6l11YuyqfS0B/+DYxkWxS/ZYZ1NbUy57eaIW/vdEgz9Q5M99Pl6ZJw==",
"version": "0.3.322",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.322.tgz",
"integrity": "sha512-LoKkldW2zkLfaOLQD44mL1Ot4Aup8dBIo356LEeO3v2vF7JVNhMllQIXhhGpbYRz57QaYsGvOe0Kb4SbssHwcg==",
"cpu": [
"arm64"
],
@@ -90,9 +90,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-darwin-x64": {
"version": "0.3.312",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.312.tgz",
"integrity": "sha512-Dmxyil+GPzF1WENoFOi1Cx0roSdJA6HewGAfAFvsH5vYrzb6G0REGuoQQ0n3u8i8qFgtl1m/WSl4u5t+MdiHWA==",
"version": "0.3.322",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.322.tgz",
"integrity": "sha512-rTvLS9G1OzjMIaSXrJglQtmRtVcaRmMbJQC+ND02+atCA463ac7Ty/Wc6tyzBGk8zFFGKWnfAhKtLZTNg91eMg==",
"cpu": [
"x64"
],
@@ -103,9 +103,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64": {
"version": "0.3.312",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.312.tgz",
"integrity": "sha512-jebsXksKh7g7u1x+RxjYQXM50TCN0e7GthebXa+n5IebF3pPqtmyWGVIiEEjvpajn3P6Zh8BWUVuufUpXn8ccA==",
"version": "0.3.322",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.322.tgz",
"integrity": "sha512-H1v33+MxB5JMPlnJLZffiOQ+6muxyA7tgHPyYQ+So4TH7uRhaPcjv2X03CUo5U9gXleO6u+X8ngNNDRFtfUaSg==",
"cpu": [
"arm64"
],
@@ -119,9 +119,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64-musl": {
"version": "0.3.312",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.312.tgz",
"integrity": "sha512-9GT0JzbfhODwsCO3MCg79wPH47qARCuKECcryeAJnVczplAlXSIiBw1+6UcqL5+0wSYFu6D/tvw/AiVy+PkHsg==",
"version": "0.3.322",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.322.tgz",
"integrity": "sha512-zvgvVA/Mxpu1QttICZ1DGDlmgGo62HdVgKg6WbDGDQ3dgxtRC/kpCIepjtGEL8O1iO5NRxQ4mv8okLwCFcyrLQ==",
"cpu": [
"arm64"
],
@@ -135,9 +135,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-linux-x64": {
"version": "0.3.312",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.312.tgz",
"integrity": "sha512-UOsqza2uo5gqlX2OU72LhkQvIaNJKUQMJIi1poX2NtYqaWACqNo/TZPWfOBzxhyWmX8BLWaOlcuZGPLgcCJOTg==",
"version": "0.3.322",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.322.tgz",
"integrity": "sha512-M/i7uV5wQf1kvjiPJr2KF5PeA9BpftUpMTtwE+DpQ0Tj+scthQXSMsEhDvau/Rh/MfFOc+UHED4tltignmA5+Q==",
"cpu": [
"x64"
],
@@ -151,9 +151,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-linux-x64-musl": {
"version": "0.3.312",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.312.tgz",
"integrity": "sha512-zM+PB6qVybdwtlqElXQ4+wCwk2v/jjT3Xau4foydzy/g5pGRTuGT3PTcfDLeG0j9zF8IxLaCQ/+Bl9MABPQD8g==",
"version": "0.3.322",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.322.tgz",
"integrity": "sha512-jprUZMcQ3XcpoxwKRQ0fi3qAM1zYcytmNGhlD0gWDknbbSu8di5jLU7231ihJrCr4gbbtDu3FiFUd6w9EBEjfw==",
"cpu": [
"x64"
],
@@ -167,9 +167,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-win32-arm64": {
"version": "0.3.312",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.312.tgz",
"integrity": "sha512-Li9CyzsC2mzGmF1wtt/R59ChBLww6nc2dLn6tb+bgTDcd6kqvW44x5rIH+lRmBcpTNHR/8VO/k+GlmnkxqeaQQ==",
"version": "0.3.322",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.322.tgz",
"integrity": "sha512-o5sPmBKT4NOglODC45wSTFmjD1AQIboC9NdGFQFOTa8kMF3bok+YqpB0DF81RmMDf3iR5tieonZvOXdYm1TDjQ==",
"cpu": [
"arm64"
],
@@ -180,9 +180,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-win32-x64": {
"version": "0.3.312",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.312.tgz",
"integrity": "sha512-9GgGHHFEQnXx6EPhkPplOw1ndqmys8+Z+uOR68EIQlXGZm+wE+BmfypyHAoZhjs4151iWdQgNzjDT7wet3crdw==",
"version": "0.3.322",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.322.tgz",
"integrity": "sha512-gONOkYx76of3p73opgWAT9gqpV7PRL8+az3vKoWXLc4xaDW6vhZF7fixpCnLDeUmPWxjXhnQxf30WGpKneTzNw==",
"cpu": [
"x64"
],
@@ -193,9 +193,9 @@
]
},
"node_modules/@anthropic-ai/sdk": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-1.3.0.tgz",
"integrity": "sha512-k7z9BlLLPxfZH+ov7dS6EnaN/wiZeV8YNT1LZfrpAHIy7oY7pHr4UzirF9f2OlHtFEnRBOX+AZQ91Y5fK+TPTw==",
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-1.7.0.tgz",
"integrity": "sha512-Z+0c1iHQozlt4ymMHapPXEXfFnSbPEWGnSKbkavt+qeyaMQ3Sd6QNxfQj7vyhHYqiqYadC9shrXub8ISKf7xjQ==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/api": "^1.4.1",
@@ -215,9 +215,9 @@
}
},
"node_modules/@babel/runtime": {
"version": "7.29.10",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.10.tgz",
"integrity": "sha512-NpugTzbKgGrNe45Ui32cN8vCFFMxVtP/9rtfwv0nP5FRDAjvYBxJcLVpYA9mr1jxP6cnIT7x9LUzuDTbWYYNRA==",
"version": "7.29.13",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.13.tgz",
"integrity": "sha512-v+oN26xP4YIlQsbQ/CbTH1WG5m/l/DjKKZNy/Kp3f/AFvbgt7Em/D0FeMM3O2hX0YzeN2M16W8lOpyhIkfCBcQ==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -267,9 +267,9 @@
}
},
"node_modules/@eslint-community/regexpp": {
"version": "4.12.2",
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
"integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
"version": "4.12.3",
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.3.tgz",
"integrity": "sha512-R76UKbvsqI68DJJVTVyqFKqU98CN4+B+acxNfEA2IFarSzP0SjtKf/3+fFiXzTyv5Fj3PxLkGHe2wnVWWMxobQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -363,9 +363,9 @@
}
},
"node_modules/@hono/node-server": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.5.tgz",
"integrity": "sha512-MgP/I06GdvsilDspMk/qmRzpk8LOUStP6jFAbSRi40W09DhoyUn+Mj7eyKICPqeYKybOjVFLVIAQzvu1bfIa7Q==",
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.6.tgz",
"integrity": "sha512-p/KH9MnfNcsm3sR6x4lNgm5j+QN4Et8srKredl9DZfXiyZ1UQ3aQ2YVpFLbqktYN5JI+DB+VZFKM2PqE5hLKjQ==",
"license": "MIT",
"peer": true,
"engines": {
@@ -544,9 +544,9 @@
}
},
"node_modules/@oxc-project/types": {
"version": "0.155.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.155.0.tgz",
"integrity": "sha512-SeE8JPwW1xs1tHeNycjUtUBdhorRa8o4jlvjwk49R7/Z291i+4DQa7JJIgOm/TaYQZx+QrPQbVJ/9c8dMIZN8Q==",
"version": "0.156.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.156.0.tgz",
"integrity": "sha512-1Duyg48TdJNdyhEB8Xn1L36bT9Pv6GUtlKjHxOys8xkaF/rQq3XJE+bAurSK/AgIz5mGFsb77cn9Pb20+WCScw==",
"dev": true,
"license": "MIT",
"peer": true,
@@ -555,9 +555,9 @@
}
},
"node_modules/@rolldown/binding-android-arm-eabi": {
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.15.tgz",
"integrity": "sha512-aLcgK9gIJ3YRvrrtfSBbRa27F5WrpiTaPulnFpsHPxpUHgFlSgnU4854MHwoRGEBhfRJDWCS2WQ7lIjskE9Olw==",
"version": "1.2.18",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.18.tgz",
"integrity": "sha512-ubTG+4KlidvioxjOFdsdEa2F6JVGZb8m2ByJUqQ7fYzUeT1cEn194POcsQ10PKH4p//q0YQn4SB3YpSuzcA1Yg==",
"cpu": [
"arm"
],
@@ -573,9 +573,9 @@
}
},
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.15.tgz",
"integrity": "sha512-xD7DdZCrbThKuxNg7ooQmvp07ys4J/pnSEMTG3/OdOWl6daC67m/Q4fL78sRHMxTH/N4fGUbYhcPM0MF5WFHkg==",
"version": "1.2.18",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.18.tgz",
"integrity": "sha512-2Xa1t/JeFGTZn5TbzgPppcCftF8sCVnTNvrIvoSdcZhoFzAuVy1L3xxQUQ4sfWDtwVzg1u6Yi1R3SlJugRNV2Q==",
"cpu": [
"arm64"
],
@@ -591,9 +591,9 @@
}
},
"node_modules/@rolldown/binding-darwin-arm64": {
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.15.tgz",
"integrity": "sha512-m2WuQ6r0B4OTYxyPpNLtXaMRbRDUbG8YQV2Q3KTdSSK8Th6KHCnC8jVCJt8bvYeSB5WivkkNbc85mKadaSdOxA==",
"version": "1.2.18",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.18.tgz",
"integrity": "sha512-iUzw9HshVvrWci9gVXjxWS7/M/IcLvNo67vu2c/w9zKBz4hIvPRixdUpIW/iCfhldLaq9ER2hecsI9UY5z0/yA==",
"cpu": [
"arm64"
],
@@ -609,9 +609,9 @@
}
},
"node_modules/@rolldown/binding-darwin-x64": {
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.15.tgz",
"integrity": "sha512-5RtnsHM36nodM9WbMy+zip0LGS/G+y5wro5RHrpOlr4ac24+1zSi9kiV2MP1myZeZ/ifEHfulQcWjnAXzc2C2A==",
"version": "1.2.18",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.18.tgz",
"integrity": "sha512-XuDHFzbFEtZl0QGJi92+fmG67tfnp1iZ9VOeDkwasGyTq2VG3/5PnJnvAr+MTzFh/rNScJ3VhN+XpZyyGsN7Jw==",
"cpu": [
"x64"
],
@@ -627,9 +627,9 @@
}
},
"node_modules/@rolldown/binding-freebsd-x64": {
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.15.tgz",
"integrity": "sha512-g3l09pyFqhu+135JKkbkwiKGQmwU5hUMUPJ4VdBADvuqR8d33L6H578rh8m7rtxbdp+VmX/+nzI7nvhSZlzpUQ==",
"version": "1.2.18",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.18.tgz",
"integrity": "sha512-MM8opLfsLK15oEXDz6iBc0t3ahu+ew9jsAmha+ovZ33hSwYgqo7YmpRITqFTfW7xA71BLEQ3jJdTsaCmhB1c9w==",
"cpu": [
"x64"
],
@@ -645,9 +645,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.15.tgz",
"integrity": "sha512-nIe3VugI97nGQtspPxDqA4sXMUpeiocKIP/CqpY/u/vgbT+alX+cVxu1C/oVBPKg4feNTmYovE91v4DgXvXNwQ==",
"version": "1.2.18",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.18.tgz",
"integrity": "sha512-J6EoCWiy5RnFQzHrKyHXXevZ9lukQfGpnchX/zG/k/WC2nc9O9XyS1JgA8kI9VxMFggMbCPjpv7LcikriXfehw==",
"cpu": [
"arm"
],
@@ -663,9 +663,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-gnu": {
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.15.tgz",
"integrity": "sha512-iTBjXx+B5QSvgI89lJfmqh4wTh8NabR1YZIIN3hNx4zSG0EVfVqkOvLulNpG4251pbd6/IAmZ/LefVk1Y4rPhw==",
"version": "1.2.18",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.18.tgz",
"integrity": "sha512-HIkFuLF7CdDeki+1aCZLLbLq0hk/PHntb+z4/YCTo47JbksTD6jJEELIz92tYROmDmvMrNqUMqBi71BkJLDR+A==",
"cpu": [
"arm64"
],
@@ -684,9 +684,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-musl": {
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.15.tgz",
"integrity": "sha512-uN26XOnVR7rqQupG3V9oAG2EGGatWjcJHOeDOHUFCh4TBWmP2tHTFoVN13aYbwfm0aHqJAEmcwhAu6sooIjfqQ==",
"version": "1.2.18",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.18.tgz",
"integrity": "sha512-4oqWItLLYt7IceOnElCct8T2ek5p7PmD7LN04TRJRXGo5f3HtKTgGH6PhYRFK4C2ICkq/LMUU0BVPvvlAKhm8g==",
"cpu": [
"arm64"
],
@@ -705,9 +705,9 @@
}
},
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.15.tgz",
"integrity": "sha512-9nWkcdd+H+XhaA3fQIwpHnKIrfx/PQ6rO0pivr78Og07tXlUlctBBJoEYLPfhTmCR/QxEVdn6uPjHV5SymPhdw==",
"version": "1.2.18",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.18.tgz",
"integrity": "sha512-+AkRly5SrCEgnPhGRfub7pucASYEC2fTsxZg7VCcnozpA2+Wn/gCYy1t7LuJS67/xyJu/7noYBJncDUkbwgw2A==",
"cpu": [
"ppc64"
],
@@ -726,9 +726,9 @@
}
},
"node_modules/@rolldown/binding-linux-s390x-gnu": {
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.15.tgz",
"integrity": "sha512-Ggh0ciFjLwIusLSLz7LZYEaGdxOAUJXI+Qu5JCbhPJJHEt8vP+IjRATTJheiJsdzzTYgUiHUiw3X1Ea3ZpMBdA==",
"version": "1.2.18",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.18.tgz",
"integrity": "sha512-Ttg1r9vVykMnQrE/GwYO9icKtXiuiMfRe/w74xJkeoVuvhRG+WtT6LEMOf58Yl63yd77lhHzO8Y55R8CC3XALA==",
"cpu": [
"s390x"
],
@@ -747,9 +747,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-gnu": {
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.15.tgz",
"integrity": "sha512-FHg60rS6DBePAYZ9X9F67x+JObFdmgn6tupA1C/a3B+CufFmTD4mX+e8M75tCiAO3foR/6xwVBI0KQiI7sn/rw==",
"version": "1.2.18",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.18.tgz",
"integrity": "sha512-xX1Tt4o5jUMma0vJsXp5eRZA9nOuyigb2dKJMMzZRmzMxLYGE9PaKKSqHPzPvSQgM8ZOEacSTQwDQcwWfh0xmQ==",
"cpu": [
"x64"
],
@@ -768,9 +768,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-musl": {
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.15.tgz",
"integrity": "sha512-/cvV/7v5VGz/moiz0XKRj2dE8DzsKyhIu1KpOvJGPjRywg3yscON889UGvAe8zfiJLHup7VLjBduoGWi3HusWQ==",
"version": "1.2.18",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.18.tgz",
"integrity": "sha512-4VVlGI5K2kociZlW3lSN8VjG3TW5yQIlAcfCdBXMgGjJxZll15Fa4ocRHXHvIVVnwlibCw/NQajJrNDiqqpiiw==",
"cpu": [
"x64"
],
@@ -789,9 +789,9 @@
}
},
"node_modules/@rolldown/binding-openharmony-arm64": {
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.15.tgz",
"integrity": "sha512-MyzC5Hrv/sm+Z7TSGKP/j8001RO2xQm1zOpCNVL4j0cY/VhBZviludA2YFQ7i4lH6ZAXxQm0lcMdBzM+99mE7w==",
"version": "1.2.18",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.18.tgz",
"integrity": "sha512-PApTsgH1jLYuyy3tSCz9De82Gq5qXwi7vImVZhEzTgX1C4150Cp5M2ZfL3pLvbCvbEHL0oJeC6pd6podtv8Y5w==",
"cpu": [
"arm64"
],
@@ -807,9 +807,9 @@
}
},
"node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.15.tgz",
"integrity": "sha512-+PgYiS+AH5omy6L45ZLxybasauJaj3M8buoT83D+nzPmCayXlxUV9hydhciCKVyIg0B/v0BtXPDE1uGqNwCPpg==",
"version": "1.2.18",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.18.tgz",
"integrity": "sha512-J2XjWjx/SP+4x0H4r6dz3Vzb5WVkg7re40Do9K7HJ3sK2/xcmZrWwakXlSwaMvOkvruLJCD4CL/3wqsIR/thgg==",
"cpu": [
"arm64"
],
@@ -825,9 +825,9 @@
}
},
"node_modules/@rolldown/binding-win32-x64-msvc": {
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.15.tgz",
"integrity": "sha512-HFZd2QNQYn1JbIi+Hpd7H+r/lkWxcfIu28nirZ6eCn2LafuvmExMlb/P96Sfx5tf7pPArw39IdhmCF8vmV+NhQ==",
"version": "1.2.18",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.18.tgz",
"integrity": "sha512-XwRJxRQvkuEtS0hIIt29QrqPJ+JzMJSNYHJw4ae/mc6uEarfRG1ceyg4HmmWiBdL/YCb6g7N5BLbSvmRJRYZHw==",
"cpu": [
"x64"
],
@@ -843,9 +843,9 @@
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.1.0.tgz",
"integrity": "sha512-K+NU2HDCEem4sClgHH4ZZXwdJeNFkJo+hRk479DIwdIviAPN2hFJH1KHHwg8CxHQXvi0M5XY4deTXINfIiSG6w==",
"dev": true,
"license": "MIT",
"peer": true
@@ -889,9 +889,9 @@
"license": "MIT"
},
"node_modules/@types/estree": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.10.tgz",
"integrity": "sha512-lTyOMnOcmRBzdCv6/4zUZCrEhV0xrqQiUZBZtVcbmLHBXKLnN8q89v/wKRF/Q0HbwVsqbIG3o5HDoFl9sduM+g==",
"dev": true,
"license": "MIT"
},
@@ -903,9 +903,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "26.6.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.5.tgz",
"integrity": "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==",
"version": "26.6.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.6.tgz",
"integrity": "sha512-MzQj+8Kx3XI40OmUAE6Qoh58ZMNstNNMwn2nfd3qWsfe7nDNSqJNzes4njhz6AHwrSHOffSLwXU6yb474Q4aog==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -913,17 +913,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.72.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.72.1.tgz",
"integrity": "sha512-5CokTWXpmbylfFabsbvHC0CAw3XLaGnoa+nFJX5FR592h/LoZsENUE6p/rg5xvkKrFEXz6pYmgI29AgXc2N3BQ==",
"version": "8.72.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.72.2.tgz",
"integrity": "sha512-222PKFXKihmFpi/pS5njoBgdAqsPhj2/GBmFbrNw8Tq8gEG9Sv+kSlKFy4u2q+mJv9ZzJgM2Gkv/bUzRZwZw5g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.12.2",
"@typescript-eslint/scope-manager": "8.72.1",
"@typescript-eslint/type-utils": "8.72.1",
"@typescript-eslint/utils": "8.72.1",
"@typescript-eslint/visitor-keys": "8.72.1",
"@typescript-eslint/scope-manager": "8.72.2",
"@typescript-eslint/type-utils": "8.72.2",
"@typescript-eslint/utils": "8.72.2",
"@typescript-eslint/visitor-keys": "8.72.2",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.5.0"
@@ -936,22 +936,22 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.72.1",
"@typescript-eslint/parser": "^8.72.2",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.72.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.72.1.tgz",
"integrity": "sha512-ZGwZP4OCfyA0AQTpEAMWz+rtX01DRhQWW5hB0ucKDEIX5U7jdmPSaMx94rQfaPAQzIYz2BimOgKOJtjfpRFQ9g==",
"version": "8.72.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.72.2.tgz",
"integrity": "sha512-llP+T1YuJOLlxEciP6OyQGtz5SMxyn+9LA2BvZw5RmToGVa1y4beEDVQL6Gje7T5WFYh19WREjDCN9Ev4gm5AA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.72.1",
"@typescript-eslint/types": "8.72.1",
"@typescript-eslint/typescript-estree": "8.72.1",
"@typescript-eslint/visitor-keys": "8.72.1",
"@typescript-eslint/scope-manager": "8.72.2",
"@typescript-eslint/types": "8.72.2",
"@typescript-eslint/typescript-estree": "8.72.2",
"@typescript-eslint/visitor-keys": "8.72.2",
"debug": "^4.4.3"
},
"engines": {
@@ -967,14 +967,14 @@
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.72.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.72.1.tgz",
"integrity": "sha512-fAuVMkwdO/huI8nyo/WKnNKf6Gag93S56Rh3Ts5mGcavwSdaN8tRg1gCeIGX8PF+nzp1BjGfO4gHhGzW1J6jTg==",
"version": "8.72.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.72.2.tgz",
"integrity": "sha512-6y5dLTZdHhI8Eh3yIQ/bfQhRKwn5e+TpJj64o18Cngc0iW/BCrVvrr6zGYqXPnYH31u6YXAYiFh0+qxX5mybJw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.72.1",
"@typescript-eslint/types": "^8.72.1",
"@typescript-eslint/tsconfig-utils": "^8.72.2",
"@typescript-eslint/types": "^8.72.2",
"debug": "^4.4.3"
},
"engines": {
@@ -989,14 +989,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.72.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.72.1.tgz",
"integrity": "sha512-lC/t6eMCZZQAAKZEYGL6RDVSuFehpdmMT6h+nJIjIa6oylN/PwDgDVtEIbB7O9xFfZYtZAYLoLlDNWKJhtF+uQ==",
"version": "8.72.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.72.2.tgz",
"integrity": "sha512-iIoMJ6/8c1/QpFNyFjU0d1YzPco4MsZjQw0k3V+vfzNWAFmUmvhlyMoYEoYYDQZXc+J6Gi1X5XcxlzW2taIIGQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.72.1",
"@typescript-eslint/visitor-keys": "8.72.1"
"@typescript-eslint/types": "8.72.2",
"@typescript-eslint/visitor-keys": "8.72.2"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1007,9 +1007,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.72.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.72.1.tgz",
"integrity": "sha512-e4D5R5vpeAEPBxT2BAzsFQL3sbwyeeeo5+aczgNSRK1V734jE7pTqaGFU+fXapWEQwnZbpFaTn32+Z8DwS6OTg==",
"version": "8.72.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.72.2.tgz",
"integrity": "sha512-+h3iCgZXuTNtoLXcnnjP12ZUHLMqoUcIHPfQrFEXTUqvb9dF8QK9b5nv5VtSo5fLkdHuks8ehbTzZj3rImAm9w==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1024,15 +1024,15 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.72.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.72.1.tgz",
"integrity": "sha512-iY2wlmyrXJmx4yxUGP3aYCFF3cA/6ShddOKmLU2dx7LnEykbAM8WThlxZEfd1NddqceF6eJmZUayxx9oKIYNlg==",
"version": "8.72.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.72.2.tgz",
"integrity": "sha512-STfkR38qIdhfkLpgtXnUjAzsNy4xouqiBO0lrVc/Wpk9Kem5/aHXUx1J8dGC16zpdYN1+yc2ILQ28er1kXX7ag==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.72.1",
"@typescript-eslint/typescript-estree": "8.72.1",
"@typescript-eslint/utils": "8.72.1",
"@typescript-eslint/types": "8.72.2",
"@typescript-eslint/typescript-estree": "8.72.2",
"@typescript-eslint/utils": "8.72.2",
"debug": "^4.4.3",
"ts-api-utils": "^2.5.0"
},
@@ -1049,9 +1049,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.72.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.72.1.tgz",
"integrity": "sha512-LmE5IhQet1BWOWEXqBrVtBPsMAOQ6Nzk35d+WLh3mk4EUF33sqlfxjio1MdYeSvKSG5skactKKuixBIKXhL4Rw==",
"version": "8.72.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.72.2.tgz",
"integrity": "sha512-Xfn37OLZx03+PCU7qWx9DTv2FCRh1E6fk2+wONeNY15HXIwtcwuveaSwVpGAUptkNmPQegRxW5JyRel9T7wrog==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1063,16 +1063,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.72.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.72.1.tgz",
"integrity": "sha512-DAlUoUV93hIvrtIQLwCbmZQYVZwOMhiHJrOVUUIWicFZaIYjTRqlY3I/NZevfkLmSqkNHRu2qYDcq9lrn16oOQ==",
"version": "8.72.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.72.2.tgz",
"integrity": "sha512-pQEGqIK/cqEvKivVWaFLYpe7XYejFxfuAOi4mu1oAWFvIo36aRHI9esRFTUxcIy/z8N482RJyH+LpZB0ZIU7Ow==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.72.1",
"@typescript-eslint/tsconfig-utils": "8.72.1",
"@typescript-eslint/types": "8.72.1",
"@typescript-eslint/visitor-keys": "8.72.1",
"@typescript-eslint/project-service": "8.72.2",
"@typescript-eslint/tsconfig-utils": "8.72.2",
"@typescript-eslint/types": "8.72.2",
"@typescript-eslint/visitor-keys": "8.72.2",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
@@ -1091,16 +1091,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.72.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.72.1.tgz",
"integrity": "sha512-5v5liypurJBFHCBNqh/RjsymAD+VWcE0djuGY/BUqp5fC3Qn+luWDEksjuTtWjPn6i5zQlUtkZCRG/CYG0yCkA==",
"version": "8.72.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.72.2.tgz",
"integrity": "sha512-hv3eRYbpPbbpezuax1EfGrzpa3C4ySgNZkPUkLOWOAjU8HsOKowEDAr8C7nsNl1C+SBSKDpvno6VKHnlhvp9iA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.72.1",
"@typescript-eslint/types": "8.72.1",
"@typescript-eslint/typescript-estree": "8.72.1"
"@typescript-eslint/scope-manager": "8.72.2",
"@typescript-eslint/types": "8.72.2",
"@typescript-eslint/typescript-estree": "8.72.2"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1115,13 +1115,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.72.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.72.1.tgz",
"integrity": "sha512-SB8xqipSzZHUePCCLNeYxqnxq83X61/qzNhjQGbTfjxGZIeOEP7tBgWYX05Tcd6CgE02WdTJnh5b8kMZJ/+bnQ==",
"version": "8.72.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.72.2.tgz",
"integrity": "sha512-NAGrjn0Pitjqzq/GkgMC4kBYm233PQ8GffyQ9+kTA3AlNQYEzOf25EIM02BAHZv5FRJSpHpJk+OGUxVkOB93GQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.72.1",
"@typescript-eslint/types": "8.72.2",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
@@ -1146,14 +1146,14 @@
}
},
"node_modules/@vitest/mocker": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.5.tgz",
"integrity": "sha512-xLL3lkDegJZw+MGoGaCCsFayDZthGgpAtr3Vziyfc4p7xH89K7KRPcRBBh/kEYm0Aqkt5DtTmkYpBtRV/FrHZg==",
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.1.0.tgz",
"integrity": "sha512-5umB9/jlWcqVS6fsrFmv5Gdnpu6wAUio/lEOwAmvB7v+lFo/IW2B9FhAD0p19GCULTgMzLCdUmuCDCnAzWgppQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/trace-mapping": "0.3.31",
"@vitest/spy": "5.0.5",
"@vitest/spy": "5.1.0",
"estree-walker": "^3.0.3",
"magic-string": "^1.4.3"
},
@@ -1174,9 +1174,9 @@
}
},
"node_modules/@vitest/spy": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.5.tgz",
"integrity": "sha512-lwJRExuH417257Q5wv9QYugN9IMJkoxcrKrYxGPQkBzl7wlNpTUxD/5yNX4ESKVUIShKPgkGchYPFMWOOk0wzw==",
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.1.0.tgz",
"integrity": "sha512-2DPO5OMHWKrBngC+fCmTQqemNF0gidRhp3lxbBmRhW+M1XJrfgPpqNqWVevXtn2f2ufD5ZaR1B6B641aAlcX5A==",
"dev": true,
"license": "MIT",
"funding": {
@@ -1543,13 +1543,17 @@
"peer": true
},
"node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.1.tgz",
"integrity": "sha512-Ev/hIyePLZtpL98x562lvPtenrUfGr0lfaf44HFZ+hDEwso7ccV9QAZrEVLg0hxNAzygaAFxy2dYaAKKM0G06A==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 0.8"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/es-define-property": {
@@ -1613,9 +1617,9 @@
}
},
"node_modules/eslint": {
"version": "10.13.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.13.0.tgz",
"integrity": "sha512-UohDjD4zSzEwhf58M3O7vc/ycc8O1I7dNlhtD0WxE8Px8EHNJKtUwzBqrzeGhisRuv2ag4sAYswszCJRgKXYUg==",
"version": "10.14.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.14.0.tgz",
"integrity": "sha512-T0pNsxkb9IKmtQjhpK3d7nWOpFv+YHuMbTJnSVMoQblF0onvXfErrtp4V1jHtsQPan7dKaGVD0q1WCXnzMF31A==",
"dev": true,
"license": "MIT",
"workspaces": [
@@ -1628,7 +1632,7 @@
"@eslint/config-helpers": "^0.7.0",
"@eslint/core": "^1.2.1",
"@eslint/plugin-kit": "^0.7.3",
"@humanfs/node": "^0.16.6",
"@humanfs/node": "^0.16.8",
"@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.4.2",
"@types/estree": "^1.0.6",
@@ -1649,7 +1653,7 @@
"imurmurhash": "^0.1.4",
"is-glob": "^4.0.0",
"json-stable-stringify-without-jsonify": "^1.0.1",
"minimatch": "^10.2.5",
"minimatch": "^10.2.6",
"natural-compare": "^1.4.0",
"optionator": "^0.9.3"
},
@@ -1941,9 +1945,9 @@
}
},
"node_modules/express-rate-limit": {
"version": "8.7.1",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.7.1.tgz",
"integrity": "sha512-2FXbqkygil3mZiXzEtM0IjE/UBmBNot0J1GEVrJT4JbrvwOSoEtqtO+wHbiMQELWe1eNvHoYRg2dkjOid+6JkA==",
"version": "8.7.4",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.7.4.tgz",
"integrity": "sha512-LKBXzX700wz3zAGNNmVnjRI5mEbYkLv7UbMI4+9Xa+JPcg2UZh6AgY/2CZh9RrJjFM5ttC/N1lD+KzyFoZITCw==",
"license": "MIT",
"peer": true,
"dependencies": {
@@ -2201,9 +2205,9 @@
}
},
"node_modules/globals": {
"version": "17.13.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-17.13.0.tgz",
"integrity": "sha512-RwMTC61u7hrG3ZJMkZQOK4JLJrKS8QtoTii63EmWvFXHqycY9FObBJQCbsLxSO8kjKhWE5kV1GC+Vb1g3RI0Zg==",
"version": "17.14.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-17.14.0.tgz",
"integrity": "sha512-UtdkTVyAJePQv0J2M9Xe4L0ruNSDV314XiIJsADI2Zf40bcdAonE/D/OhAU8AT99oij9mhLByb+NvA3QWZYgCg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2266,9 +2270,9 @@
}
},
"node_modules/hono": {
"version": "4.13.14",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.13.14.tgz",
"integrity": "sha512-yhRLQ21U6qa9Hsu2N7Qcbh8tE+WKEcoErjIo56LeSGiGXGp9sA7UpE86Ui9uexb61KakiGwuHMn2QdV7w6i6kQ==",
"version": "4.13.17",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.13.17.tgz",
"integrity": "sha512-vZIcbkvjOR5hHRWERbGkr8fGfF0GGM1ooYkuH448iO3b2AdVEr3AXZSw4Z3Kn+1L5z0CVoVeyV29VGYmFh05kQ==",
"license": "MIT",
"peer": true,
"engines": {
@@ -2348,9 +2352,9 @@
"peer": true
},
"node_modules/ip-address": {
"version": "10.7.5",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.5.tgz",
"integrity": "sha512-t2v0rkILYv9WXmSL7TpAaU3HXgaFYOPUcH3QHgwQ++gpJuH3bqyMy4Fvt/DJCTKwzH3Cf+bAp5W/mFL39KPLzA==",
"version": "10.7.6",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.6.tgz",
"integrity": "sha512-00LYkM6byILa9eQIx6qoaXCM40xTLd56AZ4vyaPW3YTyb49VZYPo5xsiAq4tuQ54GH4R7gVdYScJ1B7B77PuTg==",
"license": "MIT",
"peer": true,
"engines": {
@@ -2404,9 +2408,9 @@
"license": "ISC"
},
"node_modules/jose": {
"version": "6.2.13",
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.13.tgz",
"integrity": "sha512-7TIg10ZE1x82YediN+lcrPNvGMKge9yFHQD+Qho/b8y2adfugU3AsvvHfxV0fzyFllT6l9xCjf4RWY9g/Z9PJQ==",
"version": "6.2.17",
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.17.tgz",
"integrity": "sha512-wOuK9V1yujQFH5S0ZWmLcDZN7+IKh9bUCC2ZPCf2HDlol3WWHy+PPyNjEI75FXRLMp3vDKQcupP59xhVU95hcg==",
"license": "MIT",
"peer": true,
"funding": {
@@ -3215,9 +3219,9 @@
"license": "MIT"
},
"node_modules/qs": {
"version": "6.17.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.17.0.tgz",
"integrity": "sha512-5LK5BZ2XDHMvcUJaf0CQ5HL3aVgKbpklvByC7qLSxYkfE8COEEbJg5xCHWCTqzqi0Gs4Jn5Mv5v3R38W6edscA==",
"version": "6.17.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.17.1.tgz",
"integrity": "sha512-jd3COkaKVgGUW+ZHyEHCG9bH7RoaaiWuMgzsIBjUMVjLAaWGWpoUAVwljVvBHxRu/2qEHnadLGZrkutYUBPQJA==",
"license": "BSD-3-Clause",
"peer": true,
"dependencies": {
@@ -3271,14 +3275,14 @@
}
},
"node_modules/rolldown": {
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.15.tgz",
"integrity": "sha512-6odND+fel5zVw3I4Yc2e69gvguDnX0CAAGiNDZSq4ik5WUcaLrkD2GLuog0XcODbor31hZ5ayEMgVDno+KPV5g==",
"version": "1.2.18",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.18.tgz",
"integrity": "sha512-C1uuT3UR60SShuCaPQFAAZyefJCWZlAKB02BV3BuPE2Rs4fevRQltEKoFO2tV5W9/eJhU5RieV6vJj3euulfNQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@oxc-project/types": "=0.155.0",
"@oxc-project/types": "=0.156.0",
"@rolldown/pluginutils": "^1.0.0"
},
"bin": {
@@ -3288,21 +3292,21 @@
"node": "^20.19.0 || >=22.12.0"
},
"optionalDependencies": {
"@rolldown/binding-android-arm-eabi": "1.2.15",
"@rolldown/binding-android-arm64": "1.2.15",
"@rolldown/binding-darwin-arm64": "1.2.15",
"@rolldown/binding-darwin-x64": "1.2.15",
"@rolldown/binding-freebsd-x64": "1.2.15",
"@rolldown/binding-linux-arm-gnueabihf": "1.2.15",
"@rolldown/binding-linux-arm64-gnu": "1.2.15",
"@rolldown/binding-linux-arm64-musl": "1.2.15",
"@rolldown/binding-linux-ppc64-gnu": "1.2.15",
"@rolldown/binding-linux-s390x-gnu": "1.2.15",
"@rolldown/binding-linux-x64-gnu": "1.2.15",
"@rolldown/binding-linux-x64-musl": "1.2.15",
"@rolldown/binding-openharmony-arm64": "1.2.15",
"@rolldown/binding-win32-arm64-msvc": "1.2.15",
"@rolldown/binding-win32-x64-msvc": "1.2.15"
"@rolldown/binding-android-arm-eabi": "1.2.18",
"@rolldown/binding-android-arm64": "1.2.18",
"@rolldown/binding-darwin-arm64": "1.2.18",
"@rolldown/binding-darwin-x64": "1.2.18",
"@rolldown/binding-freebsd-x64": "1.2.18",
"@rolldown/binding-linux-arm-gnueabihf": "1.2.18",
"@rolldown/binding-linux-arm64-gnu": "1.2.18",
"@rolldown/binding-linux-arm64-musl": "1.2.18",
"@rolldown/binding-linux-ppc64-gnu": "1.2.18",
"@rolldown/binding-linux-s390x-gnu": "1.2.18",
"@rolldown/binding-linux-x64-gnu": "1.2.18",
"@rolldown/binding-linux-x64-musl": "1.2.18",
"@rolldown/binding-openharmony-arm64": "1.2.18",
"@rolldown/binding-win32-arm64-msvc": "1.2.18",
"@rolldown/binding-win32-x64-msvc": "1.2.18"
}
},
"node_modules/router": {
@@ -3330,9 +3334,9 @@
"peer": true
},
"node_modules/semver": {
"version": "7.8.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
"version": "7.8.6",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.6.tgz",
"integrity": "sha512-9lpSSU4Mly3mrtruwLs1eLK6+gzbzqeBFwRNjIjE+9LuDpWdYctrQlQ3ej1Ilgi7gknpsTRnPoLP8DpCHqHIJw==",
"dev": true,
"license": "ISC",
"bin": {
@@ -3532,9 +3536,9 @@
"license": "MIT"
},
"node_modules/tinybench": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.2.tgz",
"integrity": "sha512-nIZ2N2dFpjBsg2hgitKu7mA+FvsIJuGJOwysRngmVRRmWgR/UyPjwJj8c/fT/dhm8EkHnF5EOogRf+0Z2qrimA==",
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.3.tgz",
"integrity": "sha512-Al01C2JIBnO2CXCAPbpAl6sUiOZHeThGtHYNTa51JRw9PyWOCdr+9Ong05R4qg/Sk3+H645E8ov1h2c58ityNw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3695,17 +3699,17 @@
}
},
"node_modules/vite": {
"version": "8.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.3.5.tgz",
"integrity": "sha512-z1YpjieJ9g0ebTmQpLBv/oq9E3F9EIlOe+7LahW8SrE3LTiG3udBRUXQOlYXP9b+IC2q2phHeVxj9xGlw34OEQ==",
"version": "8.3.7",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.3.7.tgz",
"integrity": "sha512-oxh0dMxrayBr33ScJvvBJ2HBQ2jc1TBrXKR2NyL/MJ9WPuX19mN2DXWUmywKdyw5RFVQBo8hmtb2FMC0LHrnng==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"lightningcss": "^1.33.0",
"picomatch": "^4.0.7",
"postcss": "^8.5.30",
"rolldown": "~1.2.14",
"postcss": "^8.5.32",
"rolldown": "~1.2.18",
"tinyglobby": "^0.2.17"
},
"bin": {
@@ -3774,14 +3778,14 @@
}
},
"node_modules/vitest": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.5.tgz",
"integrity": "sha512-cPODOMF8YnkiiYtDdHo6n4wWmBrOBBbOIYH2d33M4l3CdNn72h+qMmO6uXffUN6A5AZFXNND/d+krS9mgkXRfg==",
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-5.1.0.tgz",
"integrity": "sha512-MzmNFnrwa8v8lF1bLxw3UJ4EREwVOVb75VqJZmUDRjJ6EluK/x9th7qTbaeDk41WxE1GxWqmTXcdsfobfH5sOA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/chai": "^5.2.2",
"@vitest/mocker": "5.0.5",
"@vitest/mocker": "5.1.0",
"chai": "^6.3.0",
"es-module-lexer": "^2.3.2",
"expect-type": "^1.4.0",
@@ -3807,12 +3811,12 @@
"@edge-runtime/vm": "*",
"@opentelemetry/api": "^1.9.0",
"@types/node": "^22.0.0 || >=24.0.0",
"@vitest/browser-playwright": "5.0.5",
"@vitest/browser-preview": "5.0.5",
"@vitest/browser-playwright": "5.1.0",
"@vitest/browser-preview": "5.1.0",
"@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0",
"@vitest/coverage-istanbul": "5.0.5",
"@vitest/coverage-v8": "5.0.5",
"@vitest/ui": "5.0.5",
"@vitest/coverage-istanbul": "5.1.0",
"@vitest/coverage-v8": "5.1.0",
"@vitest/ui": "5.1.0",
"happy-dom": "*",
"jsdom": "*",
"vite": "^6.4.0 || ^7.0.0 || ^8.0.0"

View File

@@ -3,7 +3,7 @@
"publishConfig": {
"access": "public"
},
"version": "0.99.0",
"version": "0.104.1",
"description": "An ACP-compatible coding agent powered by the Claude Agent SDK (TypeScript)",
"main": "dist/lib.js",
"types": "dist/lib.d.ts",
@@ -16,17 +16,20 @@
"types": "./dist/lib.d.ts",
"import": "./dist/lib.js"
},
"./*": "./*"
"./package.json": "./package.json"
},
"files": [
"dist/",
"!dist/tests/",
"!dist/binary-entry.*",
"README.md",
"LICENSE",
"package.json"
],
"scripts": {
"build": "tsc",
"build:binary": "node scripts/build-binary.mjs",
"smoke:binary": "node scripts/smoke-binary.mjs",
"start": "node dist/index.js",
"dev": "npm run build && npm run start",
"example:simple-client": "node --experimental-strip-types --disable-warning=ExperimentalWarning examples/simple-client.ts",
@@ -65,25 +68,25 @@
"node": ">=22"
},
"dependencies": {
"@agentclientprotocol/sdk": "2.1.0",
"@anthropic-ai/claude-agent-sdk": "0.3.312",
"@agentclientprotocol/sdk": "2.5.0",
"@anthropic-ai/claude-agent-sdk": "0.3.322",
"diff": "9.0.0",
"zod": "4.6.7"
},
"devDependencies": {
"@anthropic-ai/sdk": "1.3.0",
"@anthropic-ai/sdk": "1.7.0",
"@eslint/js": "10.0.1",
"@tsconfig/node22": "22.0.6",
"@types/node": "26.6.5",
"@typescript-eslint/eslint-plugin": "8.72.1",
"@typescript-eslint/parser": "8.72.1",
"@types/node": "26.6.6",
"@typescript-eslint/eslint-plugin": "8.72.2",
"@typescript-eslint/parser": "8.72.2",
"ajv": "8.20.0",
"ajv-formats": "3.0.1",
"eslint": "10.13.0",
"eslint": "10.14.0",
"eslint-config-prettier": "10.1.8",
"globals": "17.13.0",
"globals": "17.14.0",
"prettier": "3.9.10",
"typescript": "6.0.3",
"vitest": "5.0.5"
"vitest": "5.1.0"
}
}

92
scripts/build-binary.mjs Normal file
View File

@@ -0,0 +1,92 @@
#!/usr/bin/env node
// Builds an executable of the agent with `bun build --compile` and copies the
// target platform's Claude CLI next to it. Both files go in the release
// archive: the executable runs the CLI from its own directory.
//
// node scripts/build-binary.mjs [--target bun-linux-x64-musl] [--outfile path]
//
// --target defaults to the host. Cross-compiling needs the target's SDK
// platform package installed; the script prints the install command when it
// is missing.
import { spawnSync } from "node:child_process";
import { chmodSync, copyFileSync, readFileSync } from "node:fs";
import { createRequire } from "node:module";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { parseArgs } from "node:util";
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
function hostTarget() {
const os = process.platform === "win32" ? "windows" : process.platform;
// Node linked against glibc reports its version; musl builds don't.
const musl =
process.platform === "linux" && !process.report.getReport().header.glibcVersionRuntime;
return `bun-${os}-${process.arch}${musl ? "-musl" : ""}`;
}
const { values } = parseArgs({
options: {
target: { type: "string", default: hostTarget() },
outfile: { type: "string" },
},
});
const target = values.target;
// Bun targets look like `bun-<os>-<arch>` with optional `-musl`, `-baseline` or
// `-modern` suffixes; the SDK names its packages `<platform>-<arch>[-musl]`.
const match = /^bun-(linux|darwin|windows)-(x64|arm64|aarch64)(?:-.+)?$/.exec(target);
if (!match) {
console.error(
`Unsupported target "${target}". Expected e.g. bun-linux-x64, bun-darwin-arm64, bun-windows-x64.`,
);
process.exit(1);
}
const [, os, rawArch] = match;
const platform = os === "windows" ? "win32" : os;
const arch = rawArch === "aarch64" ? "arm64" : rawArch;
const musl = target.split("-").includes("musl");
const exe = os === "windows" ? ".exe" : "";
const cliPackage = `@anthropic-ai/claude-agent-sdk-${platform}-${arch}${musl ? "-musl" : ""}`;
const cliSpecifier = `${cliPackage}/claude${exe}`;
const outfile = resolve(values.outfile ?? join(repoRoot, "dist-bin", `claude-agent-acp${exe}`));
// Resolve from the SDK, the way claudeCliPath() does at runtime, so a nested
// install is checked rather than only a hoisted one.
const sdkRequire = createRequire(
createRequire(join(repoRoot, "package.json")).resolve("@anthropic-ai/claude-agent-sdk"),
);
let cliPath;
try {
cliPath = sdkRequire.resolve(cliSpecifier);
} catch {
const sdkVersion = JSON.parse(
readFileSync(
join(dirname(sdkRequire.resolve("@anthropic-ai/claude-agent-sdk")), "package.json"),
"utf8",
),
).version;
console.error(
`${cliSpecifier} is not installed. npm only installs the host's platform package; for ${target} run:\n\n` +
` npm install --no-save --force ${cliPackage}@${sdkVersion}\n`,
);
process.exit(1);
}
const bun = spawnSync(
"bun",
["build", "src/binary-entry.ts", "--compile", "--target", target, "--outfile", outfile],
{ cwd: repoRoot, stdio: "inherit", shell: process.platform === "win32" },
);
if (bun.error) {
console.error(`Could not run bun: ${bun.error.message}. Install it from https://bun.sh.`);
process.exit(1);
}
if (bun.status !== 0) process.exit(bun.status ?? 1);
// The executable runs the CLI from its own directory (src/binary-entry.ts).
const cliOutfile = join(dirname(outfile), `claude${exe}`);
copyFileSync(cliPath, cliOutfile);
chmodSync(cliOutfile, 0o755);
console.log(`Copied ${cliSpecifier} to ${cliOutfile}`);

470
scripts/smoke-binary.mjs Normal file
View File

@@ -0,0 +1,470 @@
#!/usr/bin/env node
// Smoke-tests a built agent over ACP. Defaults to the executable from
// scripts/build-binary.mjs, with `claude` next to it. Pass another command
// after `--` to compare, e.g.
// `node scripts/smoke-binary.mjs --full -- node "$PWD/dist/index.js"` (the full
// run starts the agent in a scratch directory, so paths must be absolute).
//
// node scripts/smoke-binary.mjs # no login needed
// node scripts/smoke-binary.mjs --full # talks to the model; needs a logged-in Claude
//
// SMOKE_DEBUG=1 prints every tool call the full run saw.
//
// The basic run checks `--version`, `--cli --version`, `initialize` (v1 and
// v2) and the `terminal-auth` login command. The
// full run also has Claude use Bash, Read (text and image), Edit and a
// subagent in a scratch directory, then cancels a turn, lists the session,
// loads it in a fresh agent process, and forks and deletes it.
//
// Claude Code's native builds have no Grep or Glob tool: its Bash shell wraps
// `grep` and `find` in functions that run the ugrep and bfs built into the
// Claude binary at `$CLAUDE_CODE_EXECPATH`. The full run checks that those
// run, since they are the helpers a packaged build could fail to reach.
import { spawn, spawnSync } from "node:child_process";
import {
copyFileSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
symlinkSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import readline from "node:readline";
import { fileURLToPath } from "node:url";
import { parseArgs } from "node:util";
import { deflateSync } from "node:zlib";
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const { values, positionals } = parseArgs({
options: { full: { type: "boolean", default: false } },
allowPositionals: true,
});
const exe = process.platform === "win32" ? ".exe" : "";
const [command, ...commandArgs] =
positionals.length > 0 ? positionals : [join(repoRoot, "dist-bin", `claude-agent-acp${exe}`)];
let failures = 0;
function check(ok, label, detail) {
console.log(
`${ok ? "ok " : "FAIL"} ${label}${!ok && detail !== undefined ? `: ${detail}` : ""}`,
);
if (!ok) failures++;
return ok;
}
// ---------------------------------------------------------------- basic
function runSync(args, [cmd, ...cmdArgs] = [command, ...commandArgs]) {
const result = spawnSync(cmd, [...cmdArgs, ...args], {
encoding: "utf8",
timeout: 60_000,
});
return { stdout: (result.stdout ?? "").trim(), error: result.error ?? result.stderr };
}
const version = JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf8")).version;
const own = runSync(["--version"]);
check(own.stdout === version, `--version prints ${version}`, own.stdout || own.error);
const cli = runSync(["--cli", "--version"]);
check(
/\(Claude Code\)$/.test(cli.stdout),
"--cli --version runs the bundled Claude CLI",
cli.stdout || cli.error,
);
// The release archive's layout: the executable finds `claude` in the directory
// of its real path. Only for the default executable; a `--` command has no
// such layout.
if (positionals.length === 0) {
const scratch = mkdtempSync(join(tmpdir(), "acp-layout-"));
try {
if (process.platform !== "win32") {
const link = join(scratch, "claude-agent-acp");
symlinkSync(command, link);
const linked = runSync(["--cli", "--version"], [link]);
check(
/\(Claude Code\)$/.test(linked.stdout),
"a symlink to the executable finds the bundled CLI",
linked.stdout || linked.error,
);
rmSync(link);
}
const alone = join(scratch, `claude-agent-acp${exe}`);
copyFileSync(command, alone);
const missing = runSync(["--cli", "--version"], [alone]);
check(
String(missing.error).includes("The Claude CLI is missing"),
"without claude next to it, says the CLI is missing",
missing.stdout || missing.error,
);
} finally {
rmSync(scratch, { recursive: true, force: true });
}
}
/** Starts the agent and speaks newline-delimited JSON-RPC to it. */
function startAgent(cwd, onNotification) {
const child = spawn(command, commandArgs, { cwd, stdio: ["pipe", "pipe", "pipe"] });
const stderr = [];
child.stderr.on("data", (chunk) => stderr.push(chunk));
let nextId = 0;
const pending = new Map();
const write = (message) =>
child.stdin.write(JSON.stringify({ jsonrpc: "2.0", ...message }) + "\n");
readline.createInterface({ input: child.stdout }).on("line", (line) => {
let message;
try {
message = JSON.parse(line);
} catch {
check(false, "stdout carries only JSON-RPC", line.slice(0, 200));
return;
}
if (message.method === undefined && pending.has(message.id)) {
pending.get(message.id)(message);
pending.delete(message.id);
} else if (message.method === "session/request_permission") {
const allow =
message.params.options.find((o) => o.kind === "allow_once") ?? message.params.options[0];
write({
id: message.id,
result: { outcome: { outcome: "selected", optionId: allow.optionId } },
});
} else if (message.id !== undefined && message.method !== undefined) {
write({
id: message.id,
error: { code: -32601, message: `smoke client does not handle ${message.method}` },
});
} else {
onNotification?.(message);
}
});
const exited = new Promise((resolve) => child.on("exit", resolve));
// Answer anything still pending if the agent dies, instead of hanging.
let exitError;
exited.then((code) => {
exitError = { message: `agent exited with ${code}: ${Buffer.concat(stderr)}` };
for (const resolve of pending.values()) resolve({ error: exitError });
pending.clear();
});
return {
request(method, params) {
const id = ++nextId;
return new Promise((resolve) => {
if (exitError) return resolve({ error: exitError });
pending.set(id, resolve);
write({ id, method, params });
});
},
notify: (method, params) => write({ method, params }),
async stop() {
child.stdin.end();
const code = await Promise.race([
exited,
new Promise((r) => setTimeout(() => r("timeout"), 10_000)),
]);
if (code === "timeout") child.kill();
return { code, stderr: Buffer.concat(stderr).toString() };
},
};
}
const initializeParams = { protocolVersion: 1, clientCapabilities: {} };
{
const agent = startAgent(process.cwd());
const init = await agent.request("initialize", initializeParams);
check(
init.result?.agentInfo?.version === version,
"initialize answers",
JSON.stringify(init.error),
);
const { code } = await agent.stop();
check(code === 0, "exits 0 when stdin closes", code);
}
{
// The `terminal-auth` login command reruns the agent; run it with
// `--version` in place of `auth login --console` instead of logging in.
// `--version` wins over a stray argument, so also check that no path inside
// a compiled executable leaked into the arguments.
const agent = startAgent(process.cwd());
const init = await agent.request("initialize", {
protocolVersion: 1,
clientCapabilities: { _meta: { "terminal-auth": true } },
});
await agent.stop();
const login = init.result?.authMethods?.find((m) => m.id === "console-login")?._meta?.[
"terminal-auth"
];
const cliArgs = login?.args?.slice(0, login.args.indexOf("--cli") + 1);
const rerun = login
? runSync([...cliArgs, "--version"], [login.command])
: { stdout: "", error: "no terminal-auth console-login method" };
check(
/\(Claude Code\)$/.test(rerun.stdout) &&
!login.args.some((arg) => arg.includes("$bunfs") || arg.includes("~BUN")),
"the terminal-auth login command reruns the agent",
`${JSON.stringify(login)} -> ${rerun.stdout || rerun.error}`,
);
}
{
const agent = startAgent(process.cwd());
const init = await agent.request("initialize", {
protocolVersion: 2,
info: { name: "smoke-binary", version },
capabilities: {},
});
check(
init.result?.protocolVersion === 2,
"initialize answers ACP v2",
JSON.stringify(init.error),
);
await agent.stop();
}
// ---------------------------------------------------------------- full
if (values.full) {
const work = mkdtempSync(join(tmpdir(), "acp-smoke-"));
try {
await fullSmoke(work);
} finally {
rmSync(work, { recursive: true, force: true });
}
}
/** A solid red 16x16 PNG, so the model has something to name. */
function redPng() {
const crcTable = Array.from({ length: 256 }, (_, n) => {
let c = n;
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
return c >>> 0;
});
const crc = (buf) => {
let c = 0xffffffff;
for (const byte of buf) c = crcTable[(c ^ byte) & 0xff] ^ (c >>> 8);
return (c ^ 0xffffffff) >>> 0;
};
const chunk = (type, data) => {
const body = Buffer.concat([Buffer.from(type), data]);
const out = Buffer.alloc(body.length + 8);
out.writeUInt32BE(data.length, 0);
body.copy(out, 4);
out.writeUInt32BE(crc(body), body.length + 4);
return out;
};
const size = 16;
const header = Buffer.alloc(13);
header.writeUInt32BE(size, 0);
header.writeUInt32BE(size, 4);
header[8] = 8; // bit depth
header[9] = 2; // RGB
const row = Buffer.concat([Buffer.from([0]), Buffer.alloc(size * 3, Buffer.from([255, 0, 0]))]);
return Buffer.concat([
Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]),
chunk("IHDR", header),
chunk("IDAT", deflateSync(Buffer.concat(Array(size).fill(row)))),
chunk("IEND", Buffer.alloc(0)),
]);
}
/** Writes its results to files so the checks don't depend on the model's reply. */
function searchCommand(token) {
return [
`grep -rl "${token}" . > grep.out`,
'find . -name "*.md" > find.out',
'{ echo "$CLAUDE_CODE_EXECPATH"; grep --version | head -1; find --version | head -1; } > tools.out',
].join("; ");
}
function searchResults(work) {
const read = (name) => {
try {
return readFileSync(join(work, name), "utf8");
} catch (error) {
return String(error);
}
};
const [execPath = "", grepVersion = "", findVersion = ""] = read("tools.out").split("\n");
return { grep: read("grep.out"), find: read("find.out"), execPath, grepVersion, findVersion };
}
async function fullSmoke(work) {
const token = `SMOKE_TOKEN_${process.pid}`;
mkdirSync(join(work, "notes"));
mkdirSync(join(work, "src"));
writeFileSync(join(work, "notes", "alpha.md"), `# Alpha\n\nThe token is ${token}.\n`);
writeFileSync(join(work, "notes", "beta.md"), "# Beta\n\nNothing here.\n");
writeFileSync(join(work, "src", "gamma.ts"), `export const value = "${token}";\n`);
writeFileSync(join(work, "pixel.png"), redPng());
const toolCalls = new Map();
let text = "";
const onNotification = (message) => {
if (message.method !== "session/update") return;
const update = message.params.update;
if (update.sessionUpdate === "agent_message_chunk" && update.content.type === "text") {
text += update.content.text;
} else if (
update.sessionUpdate === "tool_call" ||
update.sessionUpdate === "tool_call_update"
) {
const call = toolCalls.get(update.toolCallId) ?? {};
toolCalls.set(update.toolCallId, {
...call,
name: update._meta?.claudeCode?.toolName ?? call.name,
status: update.status ?? call.status,
title: update.title ?? call.title,
});
}
};
const agent = startAgent(work, onNotification);
await agent.request("initialize", initializeParams);
const created = await agent.request("session/new", { cwd: work, mcpServers: [] });
if (!check(created.result?.sessionId, "session/new", JSON.stringify(created.error))) {
console.error((await agent.stop()).stderr);
return;
}
const sessionId = created.result.sessionId;
const started = Date.now();
const prompt = await agent.request("session/prompt", {
sessionId,
prompt: [
{
type: "text",
text: [
"This is an automated smoke test of your tools. Do each step with the named tool, in order, one tool call per step:",
`1. Bash: run exactly \`${searchCommand(token)}\``,
"2. Read: read notes/alpha.md.",
"3. Read: read pixel.png and note its color.",
`4. Edit: in notes/alpha.md replace "${token}" with "EDITED".`,
"5. Bash: run `cat notes/alpha.md`.",
'6. Agent: launch a subagent (subagent_type "Explore") that counts the .ts files under src/ and reports the number.',
"Then reply with one line: SMOKE-DONE color=<the color of pixel.png> tsfiles=<the subagent's count>.",
].join("\n"),
},
],
});
check(
prompt.result?.stopReason === "end_turn",
"prompt ends with end_turn",
JSON.stringify(prompt.result ?? prompt.error),
);
console.log(` (turn took ${Math.round((Date.now() - started) / 1000)}s)`);
const calls = [...toolCalls.values()];
if (process.env.SMOKE_DEBUG) console.log(JSON.stringify(calls, null, 1));
const searched = searchResults(work);
check(
searched.grep.includes("alpha.md") && searched.grep.includes("gamma.ts"),
"grep found the token",
searched.grep,
);
check(
searched.find.includes("alpha.md") && searched.find.includes("beta.md"),
"find listed the .md files",
searched.find,
);
check(
/^ugrep /.test(searched.grepVersion),
"grep ran Claude's embedded ugrep",
searched.grepVersion,
);
check(/^bfs /.test(searched.findVersion), "find ran Claude's embedded bfs", searched.findVersion);
console.log(` (CLAUDE_CODE_EXECPATH=${searched.execPath})`);
for (const name of ["Read", "Edit", "Bash"]) {
const used = calls.filter((c) => c.name === name);
check(
used.length > 0 && used.every((c) => c.status === "completed"),
`${name} ran and completed`,
JSON.stringify(used),
);
}
const subagent = calls.filter((c) => c.name === "Agent" || c.name === "Task");
check(
subagent.some((c) => c.status === "completed"),
"subagent ran and completed",
JSON.stringify(subagent),
);
const failed = calls.filter((c) => c.status === "failed");
check(failed.length === 0, "no tool call failed", JSON.stringify(failed));
check(
readFileSync(join(work, "notes", "alpha.md"), "utf8").includes("EDITED"),
"Edit changed the file on disk",
);
check(
/SMOKE-DONE/.test(text) && /color=\W*red/i.test(text),
"image read as red",
text.slice(-300),
);
check(/tsfiles=\W*1\b/.test(text), "subagent counted 1 .ts file", text.slice(-300));
// A cancel interrupts a running Bash command and ends the turn as cancelled.
const cancelled = agent.request("session/prompt", {
sessionId,
prompt: [
{ type: "text", text: "Use the Bash tool to run `sleep 60`. Do not run anything else." },
],
});
const deadline = Date.now() + 60_000;
const bashRunning = () =>
[...toolCalls.values()].some((c) => c.name === "Bash" && c.status !== "completed");
while (!bashRunning() && Date.now() < deadline) {
await new Promise((r) => setTimeout(r, 200));
}
agent.notify("session/cancel", { sessionId });
const cancelResult = await cancelled;
check(
cancelResult.result?.stopReason === "cancelled",
"session/cancel ends the turn as cancelled",
JSON.stringify(cancelResult.result ?? cancelResult.error),
);
const listed = await agent.request("session/list", { cwd: work });
check(
listed.result?.sessions?.some((s) => s.sessionId === sessionId),
"session/list finds the session",
JSON.stringify(listed.result ?? listed.error),
);
const { code, stderr } = await agent.stop();
check(code === 0, "exits 0 when stdin closes", code);
if (failures > 0) console.error(stderr);
// A fresh process, so the replay comes from the transcript on disk.
let replayed = "";
const reloaded = startAgent(work, (message) => {
const update = message.params?.update;
if (update?.sessionUpdate === "user_message_chunk" && update.content.type === "text")
replayed += update.content.text;
});
await reloaded.request("initialize", initializeParams);
const loaded = await reloaded.request("session/load", { sessionId, cwd: work, mcpServers: [] });
check(!loaded.error, "session/load in a new process", JSON.stringify(loaded.error));
check(replayed.includes(token), "session/load replays the first prompt", replayed.slice(0, 200));
// Fork and delete go through the SDK's transcript helpers, not the CLI.
const forked = await reloaded.request("session/fork", { sessionId, cwd: work, mcpServers: [] });
const forkId = forked.result?.sessionId;
check(
forkId && forkId !== sessionId,
"session/fork",
JSON.stringify(forked.result ?? forked.error),
);
if (forkId) {
const deleted = await reloaded.request("session/delete", { sessionId: forkId });
const after = await reloaded.request("session/list", { cwd: work });
check(
!deleted.error && !after.result?.sessions?.some((s) => s.sessionId === forkId),
"session/delete removes the fork",
JSON.stringify(deleted.error ?? after.result),
);
}
await reloaded.stop();
}
console.log(failures === 0 ? "\nsmoke test passed" : `\n${failures} check(s) failed`);
process.exit(failures === 0 ? 0 : 1);

46
src/acp-client.ts Normal file
View File

@@ -0,0 +1,46 @@
import type {
AuthStatusUpdateNotification,
CompleteElicitationNotification,
CreateElicitationRequest,
CreateElicitationResponse,
ReadTextFileRequest,
ReadTextFileResponse,
RequestPermissionResponse,
WriteTextFileRequest,
WriteTextFileResponse,
} from "@agentclientprotocol/sdk/unstable";
import type { AcpSessionNotification } from "./acp-subagents.js";
import type { AcpPermissionRequest } from "./permissions/presentation.js";
/**
* Client-facing surface the agent calls back into. This is the subset of ACP
* client methods the agent actually uses, expressed as a narrow interface so
* tests can supply lightweight mocks. In the v1 agent it is backed by
* `ClientConnection` (`v1/client-connection.ts`) over the SDK's typed `AgentContext`.
* The v2 session output passes one that only takes `sessionUpdate`
* (`v2/stream.ts`).
*/
export interface AcpClient {
sessionUpdate(params: AcpSessionNotification): Promise<void>;
/** `signal`, when aborted, sends `$/cancel_request` for the in-flight
* permission request so the client can dismiss its prompt, and rejects the
* call at once: a client that ignores the cancel never answers. */
requestPermission(
params: AcpPermissionRequest,
signal?: AbortSignal,
): Promise<RequestPermissionResponse>;
readTextFile(params: ReadTextFileRequest): Promise<ReadTextFileResponse>;
writeTextFile(params: WriteTextFileRequest): Promise<WriteTextFileResponse>;
/** `signal`, when aborted, sends `$/cancel_request` for the in-flight
* elicitation so the client can dismiss its prompt, and rejects the call
* at once, as {@link requestPermission} does. */
createElicitation(
params: CreateElicitationRequest,
signal?: AbortSignal,
): Promise<CreateElicitationResponse>;
completeElicitation(params: CompleteElicitationNotification): Promise<void>;
/** Sends `auth/status_update`: the connection's auth state changed. */
authStatusUpdate(params: AuthStatusUpdateNotification): Promise<void>;
/** Send a custom (extension) notification, e.g. `_claude/sdkMessage`. */
extNotification(method: string, params: Record<string, unknown>): Promise<void>;
}

View File

@@ -17,8 +17,7 @@ export { AIR_NATIVE_SUBAGENT_SESSIONS_CAPABILITY } from "./air-extension.js";
* - `rfd`: the subagents RFD (agentclientprotocol/agent-client-protocol#1992,
* as revised): `subagent_update` and `session_message`, which the SDK types.
* A client that declares `clientCapabilities.subagents` and is not AIR gets
* it. (An ACP v2 client gets it without declaring anything, since subagents
* are part of v2's baseline.)
* it.
* - `air`: the earlier draft of that RFD, which AIR implements:
* `subagent_spawned` and `subagent_state_update`, typed below. AIR gets it,
* whichever capability it declares (`docs/air-extensions.md`).

33
src/agent-command.ts Normal file
View File

@@ -0,0 +1,33 @@
import { HIDE_CLAUDE_AUTH_FLAG } from "./hide-claude-auth.js";
/**
* The command that starts this agent again with the flags it was started with,
* for a client to run in a terminal (e.g. `terminal-auth` login).
*
* Under Node, `process.argv` is `[node, script, ...flags]`, so the command is
* `node script ...flags`. In a `bun build --compile` executable it is
* `["bun", "/$bunfs/root/<name>", ...flags]` (`B:\~BUN\root\<name>` on
* Windows): `process.execPath` is the executable itself and `argv[1]` names a
* file inside it, which must not be passed back as an argument.
*/
export function agentCommand(argv: readonly string[] = process.argv): {
command: string;
args: string[];
} {
const [, script, ...flags] = argv;
if (script === undefined) return { command: process.execPath, args: [] };
const compiled = script.includes("$bunfs") || script.includes("~BUN");
return { command: process.execPath, args: compiled ? flags : [script, ...flags] };
}
/**
* The arguments for the Claude CLI under `--cli`: the flags this process was
* started with, minus the ones the adapter owns. The terminal-auth command
* reruns the agent with its own flags (`--hide-claude-auth --cli auth login`),
* and the CLI rejects the unknown option `--hide-claude-auth`. `argv.slice(2)`
* is the flags under both Node and a compiled executable (see
* {@link agentCommand}).
*/
export function cliArgs(argv: readonly string[] = process.argv): string[] {
return argv.slice(2).filter((arg) => arg !== "--cli" && arg !== HIDE_CLAUDE_AUTH_FLAG);
}

View File

@@ -105,6 +105,28 @@ type TaskProgress = TaskIdentity & {
output_file?: unknown;
};
/** The display fields of a task progress report, shared by AIR and v2 cards. */
export type TaskProgressFields = {
description?: string;
summary?: string;
lastToolName?: string;
usage?: { totalTokens: number; toolUses: number; durationMs: number };
};
/** Reads the display fields supplied by a task progress report. */
export function taskProgressFields(message: TaskProgress): TaskProgressFields {
const usage = taskUsage(message.usage);
const description = nonBlankString(message.description);
const summary = nonBlankString(message.summary);
const lastToolName = nonBlankString(field(message, "lastToolName", "last_tool_name"));
return {
...(description ? { description } : {}),
...(summary ? { summary } : {}),
...(lastToolName ? { lastToolName } : {}),
...(usage ? { usage } : {}),
};
}
type BackgroundTaskLevel = TaskIdentity & {
taskType?: unknown;
task_type?: unknown;
@@ -267,15 +289,8 @@ export class AsyncTaskRuntime {
return;
}
const usage = taskUsage(message.usage);
const description = nonBlankString(message.description);
const summary = nonBlankString(message.summary);
const lastToolName = nonBlankString(field(message, "lastToolName", "last_tool_name"));
const progress = {
...(description ? { description } : {}),
...(summary ? { summary } : {}),
...(lastToolName ? { lastToolName } : {}),
...(usage ? { usage } : {}),
...taskProgressFields(message),
...(outputChanged && task.outputFilePath ? { outputFilePath: task.outputFilePath } : {}),
...(task.toolCallId !== previousToolCallId && task.toolCallId
? { toolCallId: task.toolCallId }
@@ -863,18 +878,29 @@ function recordPublished(task: AsyncTask, fields: Record<string, unknown>): void
* Recovers a background Bash task from the structured `backgroundTaskId` of
* its tool result. The text of a tool result never gives a task id: a
* foreground command can print any text.
*
* Claude Code sends no `tool_use_result` with a subagent's tool results. For
* those, `startedTaskOf` gives the background `local_bash` task whose
* `task_started` named the Bash call, which comes before the result.
*/
export function backgroundBashTaskFromToolResult(
content: unknown,
toolUseResult: unknown,
toolUses: Record<string, { name: string; input: unknown }>,
startedTaskOf?: (toolUseId: string) => string | undefined,
): (AsyncTaskStarted & { taskId: string; toolCallId: string }) | undefined {
if (!Array.isArray(content)) return undefined;
const toolResults = content.flatMap(toolResultBlock);
const bashResults = toolResults.filter((result) => toolUses[result.toolUseId]?.name === "Bash");
if (bashResults.length === 0) return undefined;
const backgroundResults = structuredBackgroundResults(toolUseResult, toolUses);
let backgroundResults = structuredBackgroundResults(toolUseResult, toolUses);
if (backgroundResults.length === 0 && startedTaskOf) {
backgroundResults = bashResults.flatMap((result) => {
const taskId = result.isError ? undefined : startedTaskOf(result.toolUseId);
return taskId ? [{ taskId, toolUseId: result.toolUseId }] : [];
});
}
const distinctTaskIds = new Set(backgroundResults.map((result) => result.taskId));
if (distinctTaskIds.size !== 1) return undefined;
const taskId = [...distinctTaskIds][0];

83
src/auth-methods.ts Normal file
View File

@@ -0,0 +1,83 @@
import type { AuthMethod } from "@agentclientprotocol/sdk/unstable";
import { agentCommand } from "./agent-command.js";
import { shouldHideClaudeAuth } from "./hide-claude-auth.js";
/**
* The terminal login methods the agent advertises, in v1 form.
*
* - `terminal`: the client runs `args` against the agent's own command
* (`auth.terminal` in v1, an `auth.terminal` object in v2).
* - `metaTerminal`: the client reads `_meta["terminal-auth"]`, which names
* the command too.
*
* Without either, the client cannot run a login, and the list is empty. On a
* remote machine (SSH, `NO_BROWSER`, `CLAUDE_CODE_REMOTE`, as the SDK's own
* isRemote check) the subscription login runs the TUI's `claude /login`:
* clients such as Tidewave set `NO_BROWSER` to get it. The Anthropic Console
* login is offered there too, because since Claude Code 2.1.126 `auth login`
* accepts the OAuth code pasted into the terminal when the localhost callback
* can't be reached. The Claude subscription method is left out when
* `shouldHideClaudeAuth()` says so.
*/
export function terminalAuthMethods({
terminal,
metaTerminal,
}: {
terminal: boolean;
metaTerminal: boolean;
}): AuthMethod[] {
if (!terminal && !metaTerminal) return [];
const withMeta = (method: AuthMethod & { args: string[] }, label: string): AuthMethod => {
if (!metaTerminal) return method;
const agent = agentCommand();
return {
...method,
_meta: {
"terminal-auth": { command: agent.command, args: [...agent.args, ...method.args], label },
},
};
};
const isRemote = !!(
process.env.NO_BROWSER ||
process.env.SSH_CONNECTION ||
process.env.SSH_CLIENT ||
process.env.SSH_TTY ||
process.env.CLAUDE_CODE_REMOTE
);
const subscriptionLogin = isRemote
? withMeta(
{
description: "Run `claude /login` in the terminal",
name: "Log in with Claude",
id: "claude-login",
type: "terminal",
args: ["--cli"],
},
"Claude Login",
)
: withMeta(
{
description: "Use Claude subscription ",
name: "Claude Subscription",
id: "claude-ai-login",
type: "terminal",
args: ["--cli", "auth", "login", "--claudeai"],
},
"Claude Login",
);
const methods: AuthMethod[] = [];
if (!shouldHideClaudeAuth()) methods.push(subscriptionLogin);
methods.push(
withMeta(
{
description: "Use Anthropic Console (API usage billing)",
name: "Anthropic Console",
id: "console-login",
type: "terminal",
args: ["--cli", "auth", "login", "--console"],
},
"Anthropic Console Login",
),
);
return methods;
}

367
src/auth-status-reporter.ts Normal file
View File

@@ -0,0 +1,367 @@
/**
* The connection's `authStatus` state, shared by the v1 core and the v2
* core: the last identity it reported, the `claude auth status --json` probe,
* the `_auth/status_update` and `auth/status_update` pushes, and the
* `auth/status` answer (see `auth-status.ts` for when each core reads the
* identity).
*/
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { RequestError } from "@agentclientprotocol/sdk/unstable";
import {
type AccountInfo,
filterEscalatingDefaultMode,
type Options,
resolveSettings,
type Settings,
} from "@anthropic-ai/claude-agent-sdk";
import {
type AcpAuthStatus,
AUTH_STATUS_PROBE_TIMEOUT_MS,
type AuthStatus,
type AuthStatusKind,
fromAccountInfo,
fromCliStatus,
mergeAuthStatus,
sameAuthStatus,
toAcpAuthStatus,
} from "./auth-status.js";
import { claudeCliPath } from "./claude-cli.js";
import type { SettingsManager } from "./settings.js";
const execFileAsync = promisify(execFile);
type ReporterLogger = { log: (...args: any[]) => void; error: (...args: any[]) => void };
/** The settings a probe runs under: a session's cwd, its key helper, and its launch env. */
export type AuthStatusSettings = Pick<SettingsManager, "getCwd" | "getApiKeyHelper">;
export interface AuthStatusReporterOptions {
/** Sends `_auth/status_update`. */
push: (status: AuthStatus) => Promise<void>;
/** Sends the ACP `auth/status_update`. */
pushAcp: (status: AcpAuthStatus) => Promise<void>;
logger: ReporterLogger;
/** An ACP gateway login is active: the gateway, not the CLI store, is the identity. */
gatewayLogin: () => boolean;
/** A `providers/set` override is active: a session account describes its route. */
providerOverride: () => boolean;
/** A completed probe read `kind`. */
onProbe?: (kind: AuthStatusKind) => void;
}
export class AuthStatusReporter {
/** Last identity reported to the client. Undefined means "not determined yet". */
current?: AuthStatus;
/**
* Counts auth-affecting events (a login, a logout, a new settings context).
* A probe records it at start and publishes only if it has not moved, so a
* slow read can never overwrite a newer login or logout.
*/
epoch = 0;
private settings?: {
manager: AuthStatusSettings;
override?: Settings;
env?: Options["env"];
};
private helper?: string;
/** In-flight `claude auth status --json` probe, shared by every caller so
* a concurrent `initialize` and start-of-prompt read never spawn two CLI
* processes. */
private probing: Promise<AuthStatus | undefined> | null = null;
/** Keeps the "session account told us nothing" note to one line per process
* instead of one per session. */
private loggedUninformativeAccount = false;
/** Same, for the "client provider override active" note. */
private loggedOverriddenAccount = false;
/** Same, for the "the CLI probe timed out" warning: a wedged CLI stays wedged
* and would otherwise warn once per probe, i.e. once per user prompt. */
private loggedProbeTimeout = false;
/** Numbers the probe runs in start order. */
private probeRuns = 0;
/** The latest-started probe run that published its read. */
private publishedRun = 0;
/** The last `auth/status_update` payload sent, as JSON. */
private sentAcp?: string;
constructor(private readonly options: AuthStatusReporterOptions) {}
/**
* Stores `next` and pushes it to the client, as `_auth/status_update` and
* as the ACP `auth/status_update`.
*
* A push goes out only when the payload changed. The identity is read on many
* occasions — each session create, each guarded turn, the start of each user
* prompt — and almost all of them see the same login. Clients replace their
* whole state on each update and tolerate duplicates, so a repeat is
* harmless, but it is also pure noise; {@link sameAuthStatus} drops it.
*
* What the agent owes is truth — a stale or uninformative source must not
* reach here at all (see the epoch check in the probe and the guards at
* session create).
*/
set(next: AuthStatus | undefined): void {
if (!next) {
return;
}
if (sameAuthStatus(this.current, next)) {
return;
}
this.current = next;
// ACP notifications get no reply, and clients that do not know the method
// drop it silently — send unconditionally and never fail a caller on it.
void Promise.resolve()
.then(() => this.options.push(next))
.catch((error) => {
this.options.logger.error("Failed to send _auth/status_update:", error);
});
// Compared on its own payload: two identities can map to the same one.
const acp = toAcpAuthStatus(next);
const sent = JSON.stringify(acp);
if (sent === this.sentAcp) {
return;
}
this.sentAcp = sent;
void Promise.resolve()
.then(() => this.options.pushAcp(acp))
.catch((error) => {
this.options.logger.error("Failed to send auth/status_update:", error);
});
}
/**
* The `auth/status` answer: the identity the connection holds, which is
* also what the last `auth/status_update` said.
*
* - No identity yet: it waits for a probe, joining the one `initialize`
* started, so a query right after `initialize` gets the identity. The
* probe is bounded by {@link AUTH_STATUS_PROBE_TIMEOUT_MS}. The answer is
* `unknown` when that probe publishes nothing: the CLI failed, printed
* no JSON, or timed out, or a login or logout overtook the read.
* - An identity is known: it answers at once and starts a new probe that
* nothing waits for. A login or logout made outside the client then
* arrives as `auth/status_update`.
*
* The probe is an ordinary one: it runs under the settings of the session
* that last started or prompted, pushes a change, and reports its kind to
* `onProbe`. Under an ACP gateway login it reads nothing.
*/
async status(): Promise<AcpAuthStatus> {
if (this.current) {
void this.probe({ fresh: true });
} else {
await this.probe();
}
return toAcpAuthStatus(this.current);
}
/** A login or a logout: any probe already running read the world before it. */
changed(): void {
this.epoch += 1;
}
/**
* Probes run under these settings from now on. A new context (cwd, key
* helper, or launch env) starts a fresh probe in it.
*/
useSettings(
manager: AuthStatusSettings,
settingsOverride?: Settings,
launchEnv?: Options["env"],
) {
const helper = manager.getApiKeyHelper(settingsOverride);
const changed =
manager.getCwd() !== (this.settings?.manager.getCwd() ?? process.cwd()) ||
helper !== this.helper ||
JSON.stringify(launchEnv) !== JSON.stringify(this.settings?.env);
this.settings = { manager, override: settingsOverride, env: launchEnv };
this.helper = helper;
if (changed) {
// The epoch bump discards any probe that read the old context, and the
// session account may carry no identity signal (an apiKeyHelper), so
// only a probe in the new context can classify it.
this.epoch += 1;
void this.probe({ fresh: true });
}
}
/**
* Report the identity behind a session's `AccountInfo`.
*
* `AccountInfo` describes what the live query authenticates with, but lacks
* the CLI's plan tier. The v1 core calls it before the
* `--hide-claude-auth` guard may refuse the session or the turn: that flag
* blocks the *use* of a subscription, it does not make the state secret, and
* a refusal is when the client most needs to know the account it was
* refused for.
*
* Two cases skip it:
*
* - ACP gateway *authentication*: the gateway, not this account, is the
* agent-owned identity — already reported as `kind: "gateway"` by the login.
* - A client-driven provider override (`providers/set`): the session then
* routes through the client's endpoint and `AccountInfo` describes that
* route (e.g. `apiProvider: "gateway"`), which is NOT agent-owned state.
* `authStatus` reports the agent's own login only, so the CLI probe —
* which reads the credential store the override never touches — stays
* authoritative.
*
* An account with no identity signal (e.g. `{apiProvider: "firstParty"}`
* under an apiKeyHelper) means "nothing to add", not "logged out" — keep
* what the CLI probe already established instead of overwriting it.
*/
publishSessionAccount(account: AccountInfo | undefined, apiKeyHelper?: string): void {
if (this.options.providerOverride()) {
if (!this.loggedOverriddenAccount) {
this.loggedOverriddenAccount = true;
this.options.logger.log(
"[authStatus] client provider override active; keeping agent-owned probe state",
);
}
return;
}
if (this.options.gatewayLogin()) {
return;
}
const fromSession = fromAccountInfo(account, apiKeyHelper);
if (fromSession) {
this.set(mergeAuthStatus(this.current, fromSession, "session"));
} else if (!this.loggedUninformativeAccount) {
this.loggedUninformativeAccount = true;
this.options.logger.log(
"[authStatus] session account carries no identity signal; keeping probe",
);
}
}
/** Shares one in-flight `claude auth status --json` run between callers. The
* promise is released once settled so a later call re-probes instead of
* replaying a stale verdict. `fresh` forces a new run even when one is in
* flight — `logout` needs a read that started after the credentials were
* cleared. */
probe(options?: { fresh?: boolean }): Promise<AuthStatus | undefined> {
if (!options?.fresh && this.probing) {
return this.probing;
}
const probe = this.runProbe();
this.probing = probe;
void probe.finally(() => {
if (this.probing === probe) {
this.probing = null;
}
});
return probe;
}
/** Never rejects: an unavailable CLI means "not reported", not an error. */
private async runProbe(): Promise<AuthStatus | undefined> {
const { logger } = this.options;
// Monotonicity: a read that started before the connection's latest
// auth-affecting event describes a world that no longer exists. Remember
// which one this read belongs to and drop the answer if it moved on.
const epoch = this.epoch;
const run = ++this.probeRuns;
const context = this.settings;
const apiKeyHelper = context?.manager.getApiKeyHelper(context.override);
// ACP gateway auth bypasses the CLI credential store entirely, so the
// probe would report an identity that is not the one being used. A mere
// client provider override (`providers/set`) does NOT skip the probe: it
// reroutes traffic without touching the credential store, and the store is
// exactly the agent-owned login `authStatus` reports.
if (this.options.gatewayLogin()) {
return this.current;
}
let stdout: string;
try {
const cliPath = await claudeCliPath();
const args = ["auth", "status", "--json"];
// `--settings` is a global CLI option: after the `auth status` subcommand the CLI rejects
// it with "unknown option '--settings'", so it has to come first.
if (context?.override) args.unshift("--settings", JSON.stringify(context.override));
({ stdout } = await execFileAsync(cliPath, args, {
timeout: AUTH_STATUS_PROBE_TIMEOUT_MS,
cwd: context?.manager.getCwd() ?? process.cwd(),
env: context?.env ? { ...process.env, ...context.env } : process.env,
}));
} catch (error) {
const failed = error as { stdout?: unknown; killed?: boolean; signal?: unknown } | null;
// Node killed the child on the timeout. Whatever it printed so far is a
// truncated fragment, never valid JSON: report nothing and keep the last
// known state, so nothing regresses and no push goes out.
if (failed?.killed === true && failed.signal) {
if (!this.loggedProbeTimeout) {
this.loggedProbeTimeout = true;
logger.error(
"claude auth status did not answer within 5 s; the identity is not refreshed",
);
}
return this.current;
}
// The logged-out case exits 1 while still printing valid JSON, so the
// stdout of a failed exec is parsed just like a successful one.
if (typeof failed?.stdout !== "string" || failed.stdout.trim().length === 0) {
logger.error(
"claude auth status failed:",
error instanceof Error ? error.message : String(error),
);
return undefined;
}
stdout = failed.stdout;
}
let status = fromCliStatus(stdout, apiKeyHelper);
if (!context && status?.kind === "api_key" && status.detail === "API key helper") {
try {
const resolved = await resolveSettings({ cwd: process.cwd() });
status = fromCliStatus(stdout, filterEscalatingDefaultMode(resolved).apiKeyHelper);
} catch {
logger.error("Could not resolve auth source settings");
}
}
if (!status) {
logger.error("claude auth status returned unparseable output");
return undefined;
}
if (epoch !== this.epoch) {
// A login or logout landed while this probe was running; the newer
// state wins and this answer is discarded, never published.
logger.log("[authStatus] discarding a probe that predates the latest auth change");
return this.current;
}
if (run < this.publishedRun) {
// Probes can overlap (`fresh`) and land out of order. A read that
// started before one already published is the older of the two.
logger.log("[authStatus] discarding a probe that a newer one overtook");
return this.current;
}
this.publishedRun = run;
// The CLI probe can be poorer than the session `AccountInfo` for the very
// same login (no organization, say). Keep those extra fields rather than
// regressing the payload; a different identity replaces it wholesale.
const merged = mergeAuthStatus(this.current, status, "cli");
this.set(merged);
this.options.onProbe?.(status.kind);
return merged;
}
}
/**
* Runs `claude auth logout`. The Claude and Console logins keep their
* credentials in the CLI's store (keychain or config dir), which only the
* binary can clear. The command is non-interactive and idempotent. Throws an
* `internal_error` with the CLI's stderr when it fails.
*/
export async function cliLogout(): Promise<void> {
try {
const cliPath = await claudeCliPath();
await execFileAsync(cliPath, ["auth", "logout"]);
} catch (error) {
const stderr =
typeof error === "object" && error && "stderr" in error
? String((error as { stderr: unknown }).stderr).trim()
: undefined;
throw RequestError.internalError(
{ stderr: stderr || undefined },
`claude auth logout failed: ${error instanceof Error ? error.message : String(error)}`,
);
}
}

View File

@@ -3,10 +3,14 @@
* identity the agent process itself uses (Claude subscription, API key,
* gateway, external cloud credentials, or nothing).
*
* Push only. One carrier, connection-scoped (never per session): the
* notification `_auth/status_update` → `{authStatus}`. The client never asks;
* there is no request method. The empty `agentCapabilities._meta.authStatus`
* marker means "this agent pushes its identity", nothing more.
* Two carriers of the one identity, both connection-scoped (never per
* session):
* - the extension: the notification `_auth/status_update` → `{authStatus}`,
* advertised by the empty `agentCapabilities._meta.authStatus` marker. It
* has no request method;
* - the ACP `auth/status` RFD: the `auth/status` request and the
* `auth/status_update` notification, advertised as `auth.status`.
* {@link toAcpAuthStatus} maps the identity to their payload.
*
* The agent reads the identity at these moments:
* - `initialize`, with an asynchronous CLI probe. Nothing waits for it, and its
@@ -20,7 +24,10 @@
* login or a logout done in another terminal, including the sign-in a client
* performs after a refusal, because the retried prompt is a new prompt.
* Nothing waits for it and the push goes out mid-turn if that is when the
* read lands.
* read lands;
* - each `auth/status` request, with an asynchronous CLI probe. The request
* waits for it only while no identity is known yet (see
* `AuthStatusReporter.status`).
*
* A probe whose kind differs from a live session's account marks that session
* for recreation under `--hide-claude-auth`. The mark is a flag, consumed at
@@ -29,18 +36,18 @@
* guard decides on the account it reports.
*
* A read does not always cause a push. The agent sends `_auth/status_update`
* only when the payload is different from the last one it sent.
* only when its payload is different from the last one it sent, and
* `auth/status_update` likewise.
*
* "Cannot determine" is expressed by silence: an agent that cannot read its own
* identity pushes nothing, and the client shows "not reported". A known
* logged-out state is a payload of its own (`kind: "none"`), which is what
* distinguishes the two.
* identity pushes nothing, and `auth/status` answers `unknown`. A known
* logged-out state is a payload of its own (`kind: "none"`, `unauthenticated`),
* which is what distinguishes the two.
*
* Reporting only — there is no write path in v1. When the upstream RFD lands,
* the same payload moves from `_meta` to first-class fields and this module
* is retired.
* Reporting only — there is no write path in v1.
*/
import type { AuthAccount, AuthStatusResponse } from "@agentclientprotocol/sdk/unstable";
import type { AccountInfo } from "@anthropic-ai/claude-agent-sdk";
/** Change notification, agent → client. Fire and forget: clients that do not
@@ -85,6 +92,47 @@ export function authStatusCapability(): AuthStatusCapability {
return {};
}
/** The fields of an `auth/status` response and of an `auth/status_update`
* notification, which are the same on ACP v1 and v2. */
export type AcpAuthStatus = Pick<AuthStatusResponse, "state" | "accounts">;
/**
* Maps the identity to the ACP `auth/status` payload. The identity is the
* agent's own login, so `state` does not count a route that the client set
* with `providers/set`: a logged-out CLI is `unauthenticated` under one too.
*
* - No identity read yet (`undefined`): `unknown`, no accounts.
* - `kind: "none"`: `unauthenticated`, no accounts.
* - Any other kind: `authenticated`, with the identity as the one account,
* marked `active`. The account has no field for `detail`, so its label
* carries it in parentheses, e.g. "Anthropic API key (ANTHROPIC_API_KEY)",
* for clients that show only the standard fields. `_meta.claudeCode`
* carries the `kind`, `detail` and `plan` on their own.
*
* `--hide-claude-auth` does not change the payload: it reports the login the
* CLI holds, also when the subscription guard refuses to use it.
*/
export function toAcpAuthStatus(status: AuthStatus | undefined): AcpAuthStatus {
if (!status) {
return { state: "unknown" };
}
if (status.kind === "none") {
return { state: "unauthenticated" };
}
const label = status.detail ? `${status.label} (${status.detail})` : status.label;
const account: AuthAccount = { label, active: true };
if (status.account?.email) account.email = status.account.email;
if (status.account?.organization) account.organization = status.account.organization;
account._meta = {
claudeCode: {
kind: status.kind,
...(status.detail !== undefined ? { detail: status.detail } : {}),
...(status.account?.plan !== undefined ? { plan: status.account.plan } : {}),
},
};
return { state: "authenticated", accounts: [account] };
}
/** Shape of `claude auth status --json`. Absent fields are omitted, not null,
* and the logged-out case exits 1 while still printing valid JSON. */
export type CliAuthStatus = {
@@ -96,6 +144,8 @@ export type CliAuthStatus = {
orgId?: string;
orgName?: string;
subscriptionType?: string;
/** CLI-only tier, e.g. "max_5x" or "max_20x"; not in SDK AccountInfo. */
planTier?: string;
};
const NOT_LOGGED_IN: AuthStatus = {
@@ -210,9 +260,19 @@ export function fromCliStatus(stdout: string, apiKeyHelper?: string): AuthStatus
if (!externalBackend && !status.loggedIn && !status.apiKeySource) {
return notLoggedInAuthStatus();
}
if (
status.loggedIn &&
status.authMethod === "oauth_token" &&
status.apiProvider === "firstParty" &&
!status.apiKeySource &&
!status.subscriptionType
) {
return { kind: "account", label: "Claude subscription", detail: "OAuth token" };
}
return mapAuthFields({
apiProvider: status.apiProvider,
subscriptionType: status.subscriptionType,
planTier: status.planTier,
apiKeySource: status.apiKeySource,
email: status.email,
organization: status.orgName,
@@ -233,6 +293,7 @@ export function fromCliStatus(stdout: string, apiKeyHelper?: string): AuthStatus
function mapAuthFields(fields: {
apiProvider?: string;
subscriptionType?: string;
planTier?: string;
apiKeySource?: string;
email?: string;
organization?: string;
@@ -272,7 +333,7 @@ function mapAuthFields(fields: {
if (organization) account.organization = organization;
return {
kind: "account",
label: planLabel(subscriptionType),
label: planLabel(subscriptionType, fields.planTier),
// No detail: the client falls back to `account.email` for line 2.
account,
};
@@ -280,12 +341,14 @@ function mapAuthFields(fields: {
return notLoggedInAuthStatus();
}
/** Names the plan with exactly one "Claude". A newer CLI already reports
* "Claude Max", an older one reports "max", and both must read "Claude Max".
* Each word is title-cased; a word that already carries capitals is kept. */
function planLabel(plan: string): string {
/** Names the plan with exactly one "Claude". Without a tier, "Claude Max"
* from a newer CLI and "max" from an older one both read "Claude Max".
* Each word is title-cased; a word that already carries capitals is kept.
* The CLI's max_20x tier identifies Ultra; other tiers keep the plan label. */
function planLabel(plan: string, planTier?: string): string {
const titled = plan.replace(/\S+/g, (word) => word.charAt(0).toUpperCase() + word.slice(1));
return /^claude(\s|$)/i.test(plan) ? titled : `Claude ${titled}`;
const label = /^claude(\s|$)/i.test(plan) ? titled : `Claude ${titled}`;
return label.toLowerCase() === "claude max" && planTier === "max_20x" ? "Claude Ultra" : label;
}
/**
@@ -313,8 +376,14 @@ export function sameIdentity(a: AuthStatus, b: AuthStatus): boolean {
* organization the CLI probe omits. So when the read describes the same
* identity, its values win field by field but the fields it lacks are kept;
* when it describes a different identity, it replaces the old one wholesale.
* Session reads lack the CLI's plan tier, so keep the label for the same plan.
* A CLI read always replaces the label, including when its tier is absent.
*/
export function mergeAuthStatus(previous: AuthStatus | undefined, next: AuthStatus): AuthStatus {
export function mergeAuthStatus(
previous: AuthStatus | undefined,
next: AuthStatus,
source: "cli" | "session",
): AuthStatus {
if (!previous || !sameIdentity(previous, next)) {
return next;
}
@@ -325,6 +394,14 @@ export function mergeAuthStatus(previous: AuthStatus | undefined, next: AuthStat
if (previous.account || next.account) {
merged.account = { ...previous.account, ...next.account };
}
if (
source === "session" &&
previous.account?.plan &&
next.account?.plan &&
planLabel(previous.account.plan).toLowerCase() === planLabel(next.account.plan).toLowerCase()
) {
merged.label = previous.label;
}
if (merged.vendor === undefined && previous.vendor !== undefined) {
merged.vendor = previous.vendor;
}

131
src/background-output.ts Normal file
View File

@@ -0,0 +1,131 @@
/**
* The output of a command that went to the background, read from the file
* that Claude Code writes it to. Claude Code streams no output of such a
* command, so its display terminal would show only the placeholder of the
* Bash result ("Command running in background with ID …") until the command
* exits. ACP v2 asks an Agent to keep a listed terminal's output current
* whenever it can read it (the v2 prompt lifecycle, "Background work").
*
* The file is polled: watching a file is not reliable on every platform, and
* a missing file only means that nothing was written yet.
*/
import { open } from "node:fs/promises";
import { StringDecoder } from "node:string_decoder";
/** How often a running command's output file is read. */
export const BACKGROUND_OUTPUT_POLL_MS = 500;
/** The most bytes one read takes, so a burst of output does not block the loop. */
export const BACKGROUND_OUTPUT_READ_BYTES = 256 * 1024;
/**
* The most unread bytes that are still sent. A command that writes faster
* than the polls read falls behind. Past this, the reads skip to the file's
* last bytes, as a snapshot: a terminal shows the latest output, and a
* client keeps a bounded amount of it anyway.
*/
export const BACKGROUND_OUTPUT_MAX_BACKLOG_BYTES = 1024 * 1024;
/**
* Text that the file adds. A snapshot replaces the terminal's output so far:
* the first one replaces the placeholder of the Bash result, and a later one
* follows a skip or a file that shrank. Other text appends to it.
*/
export type BackgroundOutput = { text: string; replaces: boolean };
/**
* Follows the output file `path` of one background command and hands each
* piece of new text to `publish`, in order. Reads never overlap. A read that
* fails, for example because the file does not exist yet, is tried again at
* the next poll. `publish` must not reject: the next read waits for it.
*/
export class BackgroundOutputTail {
private offset = 0;
private decoder = new StringDecoder("utf8");
/** Whether the next text replaces the terminal's output. */
private replaces = true;
private stopped = false;
private reading: Promise<void> = Promise.resolve();
private readonly timer: ReturnType<typeof setInterval>;
constructor(
private readonly path: string,
private readonly publish: (output: BackgroundOutput) => Promise<void>,
) {
this.timer = setInterval(() => void this.read(false), BACKGROUND_OUTPUT_POLL_MS);
// A command that runs on must not keep the agent process alive.
this.timer.unref();
}
/**
* Reads the rest of the file and stops. The command ended, so what the file
* holds is its whole output, and it goes out before the terminal's exit.
*/
async finish(): Promise<void> {
clearInterval(this.timer);
await this.read(true);
this.stopped = true;
}
/** Stops without reading more: the session ended. */
stop(): void {
clearInterval(this.timer);
this.stopped = true;
}
/**
* Reads what the file adds, one read of at most
* {@link BACKGROUND_OUTPUT_READ_BYTES} at a time, and publishes each read's
* text before the next. `toEnd` reads until the end of the file, and ends a
* character that the command cut off with a replacement character.
*/
private read(toEnd: boolean): Promise<void> {
this.reading = this.reading.then(async () => {
if (this.stopped) return;
let file;
try {
file = await open(this.path, "r");
} catch {
// Nothing written yet, or the file is gone: try again at the next
// poll. At the end, the terminal keeps the output it has.
return;
}
try {
const { size } = await file.stat();
// A file that shrank is a new one: its output starts over. Past the
// backlog, the reads skip to the file's last bytes.
const start = size < this.offset ? 0 : this.offset;
const skipTo = Math.max(start, size - BACKGROUND_OUTPUT_MAX_BACKLOG_BYTES);
if (skipTo !== this.offset) {
this.offset = skipTo;
this.decoder = new StringDecoder("utf8");
this.replaces = true;
}
const buffer = Buffer.alloc(BACKGROUND_OUTPUT_READ_BYTES);
let bytesRead: number;
do {
({ bytesRead } = await file.read(buffer, 0, buffer.length, this.offset));
this.offset += bytesRead;
const text = this.decoder.write(buffer.subarray(0, bytesRead));
if (text !== "") await this.send(text);
} while (toEnd && bytesRead > 0);
if (toEnd) {
const rest = this.decoder.end();
// A command that wrote nothing still replaces the placeholder.
if (rest !== "" || this.replaces) await this.send(rest);
}
} catch {
// The read failed: try again at the next poll.
} finally {
await file.close();
}
});
return this.reading;
}
private async send(text: string): Promise<void> {
const replaces = this.replaces;
this.replaces = false;
await this.publish({ text, replaces });
}
}

12
src/binary-entry.ts Normal file
View File

@@ -0,0 +1,12 @@
// Entry of the release executable (`scripts/build-binary.mjs`). It registers the
// Claude CLI shipped next to the executable before importing the agent:
// index.ts starts serving (or, with --cli, spawns the CLI) while it evaluates,
// and a static import would evaluate it before this module's body runs.
import { realpathSync } from "node:fs";
import { dirname } from "node:path";
import { useBundledClaudeCli } from "./claude-cli.js";
// realpath, so a symlink to the executable (e.g. in /usr/local/bin) still finds
// the CLI next to the real file.
useBundledClaudeCli(dirname(realpathSync(process.execPath)));
await import("./index.js");

88
src/claude-cli.ts Normal file
View File

@@ -0,0 +1,88 @@
import { existsSync } from "node:fs";
import { join } from "node:path";
const CLI_FILE_NAME = process.platform === "win32" ? "claude.exe" : "claude";
let bundledCliDirectory: string | undefined;
/**
* Called by the executable's entry (`src/binary-entry.ts`) before the agent
* starts. A `bun build --compile` executable has no `node_modules` to resolve
* the SDK's platform package from, so the release archive ships the Claude CLI
* next to it. The CLI isn't embedded: a child process can't run a file inside
* Bun's `$bunfs`, so an embedded copy would have to be extracted to disk first.
*/
export function useBundledClaudeCli(directory: string): void {
bundledCliDirectory = directory;
}
/**
* The Claude CLI binary to spawn: `CLAUDE_CODE_EXECUTABLE` if set, then the CLI
* shipped next to the executable, then the SDK's platform-specific package.
*/
export async function claudeCliPath(): Promise<string> {
if (process.env.CLAUDE_CODE_EXECUTABLE) {
return process.env.CLAUDE_CODE_EXECUTABLE;
}
if (bundledCliDirectory !== undefined) {
return bundledClaudeCliPath(bundledCliDirectory);
}
return installedClaudeCliPath();
}
function bundledClaudeCliPath(directory: string): string {
const path = join(directory, CLI_FILE_NAME);
// Without this check, spawning a missing CLI fails with a bare ENOENT.
if (!existsSync(path)) {
throw new Error(
`The Claude CLI is missing: expected ${path}. Keep ${CLI_FILE_NAME} from the release archive ` +
"in the same directory as claude-agent-acp, or set CLAUDE_CODE_EXECUTABLE to a Claude CLI binary.",
);
}
return path;
}
async function installedClaudeCliPath(): Promise<string> {
// The SDK's CLI is a native binary shipped as a platform-specific optional
// dependency of @anthropic-ai/claude-agent-sdk. Resolve via a require bound
// to the SDK so nested installs are found even when npm doesn't hoist.
const { createRequire } = await import("node:module");
const req = createRequire(import.meta.resolve("@anthropic-ai/claude-agent-sdk"));
const ext = process.platform === "win32" ? ".exe" : "";
// On linux, both glibc and musl variants may be installed side-by-side
// (e.g. bunx hydrates every optional dep), so picking one by trial is
// unreliable: the wrong binary segfaults at runtime instead of failing to
// spawn. Detect the runtime libc and prefer the matching variant, falling
// back to the other only if the preferred one isn't installed.
const candidates =
process.platform === "linux"
? isMuslLibc()
? [
`@anthropic-ai/claude-agent-sdk-linux-${process.arch}-musl/claude${ext}`,
`@anthropic-ai/claude-agent-sdk-linux-${process.arch}/claude${ext}`,
]
: [
`@anthropic-ai/claude-agent-sdk-linux-${process.arch}/claude${ext}`,
`@anthropic-ai/claude-agent-sdk-linux-${process.arch}-musl/claude${ext}`,
]
: [`@anthropic-ai/claude-agent-sdk-${process.platform}-${process.arch}/claude${ext}`];
for (const candidate of candidates) {
try {
return req.resolve(candidate);
} catch {
// try next candidate
}
}
throw new Error(
`Claude native binary not found for ${process.platform}-${process.arch}. ` +
`Reinstall @anthropic-ai/claude-agent-sdk without --omit=optional, or set CLAUDE_CODE_EXECUTABLE.`,
);
}
function isMuslLibc(): boolean {
// process.report.getReport().header.glibcVersionRuntime is populated when
// Node is dynamically linked against glibc, and absent on musl.
const report = process.report?.getReport() as
{ header?: { glibcVersionRuntime?: string } } | undefined;
return !report?.header?.glibcVersionRuntime;
}

View File

@@ -45,6 +45,7 @@ export type ClearContextSession<Turn extends ClearContextTurn = ClearContextTurn
turnQueue?: Turn[];
pendingExitPlanContextReset?: ClearContextReset;
contextUsedTokens?: number;
contextWindowSize: number;
sentUserUuids?: Set<string>;
input: { push(message: SDKUserMessage): unknown };
};
@@ -61,10 +62,13 @@ export type ClearContextCoordinatorHost<
): Promise<Session>;
applyFastMode(session: Session, enabled: boolean): Promise<void>;
applyUltracode(session: Session, enabled: boolean): Promise<void>;
/** Publishes the fresh context's mode, config options, and usage: the
* context starts empty in a window of `contextWindowSize` tokens. */
publishSessionState(
sessionId: string,
mode: PermissionMode,
configOptions: SessionConfigOption[],
contextWindowSize: number,
): Promise<void>;
continuationMessage(sessionId: string, plan: string, promptUuid: string): SDKUserMessage;
ensureConsumer(session: Session, sessionId: string): void;
@@ -165,7 +169,12 @@ export async function continuePlanInFreshContext<
freshSession.contextUsedTokens = 0;
try {
await host.publishSessionState(sessionId, reset.mode, freshSession.configOptions);
await host.publishSessionState(
sessionId,
reset.mode,
freshSession.configOptions,
freshSession.contextWindowSize,
);
} catch (error) {
host.logError("Failed to publish clear-context session state:", error);
}

View File

@@ -72,3 +72,30 @@ export function sdkSystemPrompt(
? { type: "preset", preset: "claude_code", append: text }
: { type: "preset", preset: "claude_code" };
}
/**
* The SDK `systemPrompt` of a session: the spec's `systemPrompt`
* (`clientPrompt`), else the `_meta.systemPrompt` extension (`metaPrompt`), a
* string that replaces the preset or an object of preset options, else the
* Claude Code preset. `customInstructions` are appended to the preset.
*/
export function sessionSystemPrompt(
clientPrompt: ClientSystemPrompt | undefined,
metaPrompt: unknown,
customInstructions?: string,
): NonNullable<Options["systemPrompt"]> {
if (clientPrompt) return sdkSystemPrompt(clientPrompt, customInstructions);
if (typeof metaPrompt === "string" && metaPrompt) return metaPrompt;
if (typeof metaPrompt === "object" && metaPrompt !== null && !Array.isArray(metaPrompt)) {
// Forward all preset options (append, excludeDynamicSections, and
// anything the SDK adds later) while locking type/preset.
return {
...(metaPrompt as object),
type: "preset",
preset: "claude_code",
} as NonNullable<Options["systemPrompt"]>;
}
return customInstructions === undefined
? { type: "preset", preset: "claude_code" }
: { type: "preset", preset: "claude_code", append: customInstructions };
}

View File

@@ -2,7 +2,7 @@ import { AIR_CONTEXT_COMPACTION_KEY, withAirMeta } from "./air-extension.js";
export const CONTEXT_COMPACTION_META_VERSION = 1;
export type ContextCompactionTrigger = "manual" | "automatic";
export type ContextCompactionTrigger = "manual" | "automatic" | "idle";
export interface ContextCompactionMetadata {
version: typeof CONTEXT_COMPACTION_META_VERSION;

View File

@@ -1,8 +1,10 @@
import { ClientCapabilities, SessionNotification } from "@agentclientprotocol/sdk/unstable";
import {
ContextCompactionMetadata,
ContextCompactionTrigger,
createContextCompactionMeta,
} from "./context-compaction-meta.js";
import { assistantMessageText } from "./session-failure-extension.js";
import { compactionToolCall } from "./tool-calls/reporters/compaction.js";
type CompactionStatus = "completed" | "failed";
@@ -66,7 +68,7 @@ const ANALYSIS_BLOCKS = [
* it as the post-compaction user message. */
const CONTINUATION_PREAMBLE = /^\s*This session is being continued from a previous conversation/i;
const PERSISTED_PREAMBLE =
/^\s*This session is being continued from a previous conversation[^\n]*\n+Summary:[ \t]*\r?\n/;
/^\s*This session is being continued from a previous conversation[^\n]*\n(?:[\s\S]*?\n)?Summary:[ \t]*\r?\n/;
const PERSISTED_TRAILERS = [
/\n+If you need specific details from before compaction[\s\S]*$/,
/\n+(?:Please )?[Cc]ontinue the conversation from where it left (?:it )?off[\s\S]*$/,
@@ -131,6 +133,36 @@ export function isCompactSummaryMessage(message: unknown): boolean {
);
}
/**
* Replays the compact summary message `message` (see
* {@link isCompactSummaryMessage}) of the root conversation as one completed
* `compaction_update` at its transcript position, keyed by the message's
* uuid. Returns false, and sends nothing, for any other message.
*
* The boundary's own system record comes back from `getSessionMessages`
* without its metadata, so the summary message is the one durable marker.
* Its uuid differs from the live id (the `compacting` status uuid): replay
* is terminal-first by the RFD, and no client merges a replay into a live
* entity store. A summary whose framing is unrecognized is still this
* compaction, without summary text.
*/
export async function replayCompactSummary(
message: { uuid?: unknown; message?: unknown },
sendUpdate: SendUpdate,
options: { sessionId: string; airClient?: boolean },
): Promise<boolean> {
if (!isCompactSummaryMessage(message) || typeof message.uuid !== "string" || !message.uuid) {
return false;
}
const replayed = new ContextCompactionLifecycle(sendUpdate, {
...options,
presentation: "compaction_update",
});
replayed.recordSummary(assistantMessageText(message.message));
await replayed.finish(message.uuid, "completed");
return true;
}
/**
* Translates Claude's compaction signals into one idempotent ACP lifecycle.
*
@@ -408,14 +440,29 @@ function withoutError({
return facts;
}
export function contextCompactionMetadataFromBoundary(compactMetadata: {
trigger: "manual" | "auto";
pre_tokens: number;
post_tokens?: number;
duration_ms?: number;
}): Omit<ContextCompactionMetadata, "version"> {
function compactionTrigger(
cliTrigger: "manual" | "auto",
sentByAdapter: boolean,
): ContextCompactionTrigger {
// The CLI's own automatic compaction stays automatic, even inside the adapter's /compact turn.
if (cliTrigger === "auto") return "automatic";
// The CLI reports every /compact as manual; only the adapter knows it sent this one itself.
if (sentByAdapter) return "idle";
return "manual";
}
/** `idleCompaction` marks a `/compact` that the adapter sent itself (see `IdleCompaction`). */
export function contextCompactionMetadataFromBoundary(
compactMetadata: {
trigger: "manual" | "auto";
pre_tokens: number;
post_tokens?: number;
duration_ms?: number;
},
{ idleCompaction = false }: { idleCompaction?: boolean } = {},
): Omit<ContextCompactionMetadata, "version"> {
return {
trigger: compactMetadata.trigger === "auto" ? "automatic" : "manual",
trigger: compactionTrigger(compactMetadata.trigger, idleCompaction),
preTokens: compactMetadata.pre_tokens,
...(compactMetadata.post_tokens !== undefined
? { postTokens: compactMetadata.post_tokens }

166
src/context-window.ts Normal file
View File

@@ -0,0 +1,166 @@
/**
* The context window of a session's model, as both the v1 core and the
* v2 core report it in `usage_update.size`: seeded without IPC when the
* session starts or switches models, refined by a background
* `getContextUsage`, and learned from each result's `modelUsage`.
*/
import type { ModelInfo, Query } from "@anthropic-ai/claude-agent-sdk";
import { DEFAULT_CONTEXT_WINDOW, inferContextWindowFromModel } from "./session-usage.js";
/** Cross-session cache of authoritative context windows, keyed by
* `${providerCacheKey}\0${modelId}` (see {@link contextWindowCacheKey}).
* The window is a property of (model id, backend): the same resolved model id
* (e.g. "claude-sonnet-5[1m]", the spelling of the `result.modelUsage` keys)
* can name different context lanes behind different base URLs, routing
* headers, or credentials, so the key carries both. Caching it module-level
* lets a later session/new or switch that resolves to the same (backend,
* model) — in this session or any other, within the adapter's lifetime — seed
* the correct window synchronously with no IPC. Keying on the resolved id
* (rather than the picker value) means aliases that resolve to the same
* concrete model share one entry; the result handler additionally writes the
* bare assistant-message spelling so seed-time reads that fall back to a
* verbatim live id (rows without `resolvedModel`) can hit too.
*
* Populated authoritatively by each `result.modelUsage` a turn confirms (see
* the consumer's result handler). We deliberately never populate it from
* `getContextUsage`: its `model` spelling and `rawMaxTokens` can differ from
* the `result.modelUsage` key and window (e.g. 967000 vs 1000000 for sonnet),
* so it only refines the live session (`refreshContextWindowInBackground`).
* Cleared on `logout`: 1M-context entitlement can differ per account/tier, so
* windows learned under one login must not seed sessions under the next. */
const contextWindowCache = new Map<string, number>();
/** The env vars that determine which LLM backend — and which context lane on
* it — a query's API traffic reaches: endpoint selection (base URLs and the
* Bedrock/Vertex switches with their project/region), routing/beta headers
* (an `anthropic-beta: context-1m-…` header flips the same model id at the
* same endpoint between context lanes), and credential identity (extended
* context is entitlement-gated per account). Used to derive the
* provider-cache key from the exact env a query is created with, so
* `providers/set` config, per-session `_meta` env overrides, and ambient
* process env are all distinguished exactly as the CLI will see them. */
const PROVIDER_ROUTING_ENV_VARS = [
"ANTHROPIC_BASE_URL",
"ANTHROPIC_BEDROCK_BASE_URL",
"ANTHROPIC_VERTEX_BASE_URL",
"CLAUDE_CODE_USE_BEDROCK",
"CLAUDE_CODE_USE_VERTEX",
"ANTHROPIC_VERTEX_PROJECT_ID",
"CLOUD_ML_REGION",
"AWS_REGION",
"ANTHROPIC_CUSTOM_HEADERS",
"ANTHROPIC_API_KEY",
"ANTHROPIC_AUTH_TOKEN",
"CLAUDE_CODE_OAUTH_TOKEN",
] as const;
/** Stable identifier for the LLM backend a session's query is created against,
* used to scope {@link contextWindowCache} per backend. Positional `\0`-join
* of {@link PROVIDER_ROUTING_ENV_VARS} values, so no segment can masquerade
* as another and unset vars everywhere yield one stable "default" bucket.
* Header/credential values can be secrets; the key only ever lives as an
* in-memory Map key and is never logged or surfaced. Over-keying is the safe
* side: a var change that didn't really change the backend costs one cache
* miss (heuristic seed until the next result), while under-keying would serve
* one backend's window for another's. */
export function providerCacheKeyFor(env: Record<string, string | undefined>): string {
return PROVIDER_ROUTING_ENV_VARS.map((name) => env[name] ?? "").join("\0");
}
/** Compose the `contextWindowCache` key from a session's provider key and a
* model id. `\0`-joined so the model segment can't collide with a provider
* segment. */
export function contextWindowCacheKey(providerCacheKey: string, modelId: string): string {
return `${providerCacheKey}\0${modelId}`;
}
function cacheContextWindow(modelKey: string, window: number): void {
if (window > 0) {
contextWindowCache.set(modelKey, window);
}
}
/** Caches the window that a result's `modelUsage` reported, keyed on (the
* session's provider, the resolved model id — the `modelUsage` key, e.g.
* "claude-sonnet-5[1m]") so a later session/new or switch on the same
* provider that resolves to this model seeds the correct window
* synchronously, with no getContextUsage IPC.
*
* Also caches it under the assistant message's own (bare) spelling. Seed-time
* reads fall back to a picker value / verbatim live id when a row carries no
* resolvedModel (the synthesized out-of-allowlist resume row sets it
* undefined on purpose), and those spellings match `.model` from the
* assistant message, not the decorated modelUsage key — without this entry
* such rows could never hit the cache. A non-positive window is not cached. */
export function cacheResultContextWindow(
providerCacheKey: string,
modelUsageKey: string,
assistantModel: string | null | undefined,
window: number,
): void {
cacheContextWindow(contextWindowCacheKey(providerCacheKey, modelUsageKey), window);
if (assistantModel && assistantModel !== modelUsageKey) {
cacheContextWindow(contextWindowCacheKey(providerCacheKey, assistantModel), window);
}
}
/** The context window to report *right now* for a model, with NO IPC on the
* critical path: the cached authoritative value if we've learned it (from a
* prior turn's `result.modelUsage`, this or any session on the same backend),
* else the text heuristic over the model row's identity strings, else the
* default. Derives the cache key itself — `modelInfo?.resolvedModel ?? modelId`,
* the same rule at every seed site — so read keys can't drift from the write
* site's spelling. `authoritative` reports whether the value came from the
* cache: an authoritative window can legitimately equal
* DEFAULT_CONTEXT_WINDOW, so the value alone can't tell the caller. */
export function immediateContextWindow(
providerCacheKey: string,
modelId: string,
modelInfo?: Pick<ModelInfo, "resolvedModel" | "displayName" | "description">,
): { size: number; authoritative: boolean } {
const cached = contextWindowCache.get(
contextWindowCacheKey(providerCacheKey, modelInfo?.resolvedModel ?? modelId),
);
if (cached !== undefined) return { size: cached, authoritative: true };
return {
size:
inferContextWindowFromModel(
modelId,
modelInfo?.resolvedModel,
modelInfo?.displayName,
modelInfo?.description,
) ?? DEFAULT_CONTEXT_WINDOW,
authoritative: false,
};
}
/** Forgets the learned windows, as a logout asks: 1M-context entitlement is
* gated per org/tier, and an OAuth re-login is invisible to the env-derived
* provider cache key, so windows learned under the old login must not seed
* sessions under the next. */
export function clearContextWindowCache(): void {
contextWindowCache.clear();
}
/**
* Reads the window of the query's current model with
* `getContextUsage().rawMaxTokens`. Undefined when Claude Code reports none.
*
* Requests the `summary` detail instead of the default `full` breakdown,
* which issues one `messages/count_tokens` call per category (~18 for a bare
* session) and hit rate limits on every model switch; `summary` answers
* locally with the same `rawMaxTokens`. Callers never await it on the setup
* or switch path: SDK control requests are serialized, so awaiting would
* delay session/new or a model switch (~15s on older CLIs, issues
* #886/#880; ~0.5s on 2.1.283).
*
* The result does not go into the cross-session cache: its `rawMaxTokens` can
* differ from the `result.modelUsage` window (e.g. 967000 vs 1000000 for
* sonnet), so it only refines the live session.
*/
export async function readContextWindow(
query: Pick<Query, "getContextUsage">,
): Promise<number | undefined> {
const usage = await query.getContextUsage({ detail: "summary" });
return usage.rawMaxTokens > 0 ? usage.rawMaxTokens : undefined;
}

View File

@@ -89,7 +89,7 @@ export type PatchForm = "air" | "v2";
/**
* How a git patch names its file. `git` is git's own form: the leading slash
* dropped and the `a/` and `b/` prefixes added. `absolute` is the form of an
* ACP v2 `git_patch`, whose paths must be absolute: the path itself.
* ACP v2 `git_patch`, whose paths must be absolute: the native path itself.
*/
type GitPatchPaths = "git" | "absolute";
@@ -438,11 +438,11 @@ export function gitPatchText(
}
function gitPatchHeader(filePath: string, change: FileChange, paths: GitPatchPaths): string[] {
// A Windows path gets forward slashes so the header names the same file on
// every platform.
const slashed = filePath.replaceAll("\\", "/");
// git drops one leading slash of an absolute path.
const name = paths === "git" ? slashed.replace(/^\/+/u, "") : slashed;
// git's form uses forward slashes, so the header names the same file on
// every platform, and drops one leading slash of an absolute path. An ACP v2
// patch keeps the native path so that, once unquoted, it equals the `changes`
// entry: `C:\work\App.ts` is written `"C:\\work\\App.ts"`.
const name = paths === "git" ? filePath.replaceAll("\\", "/").replace(/^\/+/u, "") : filePath;
const oldName = quoteGitPath(paths === "git" ? "a/" : "", name);
const newName = quoteGitPath(paths === "git" ? "b/" : "", name);
// git ends a ---/+++ name that contains a space with a tab, for GNU patch.
@@ -717,7 +717,7 @@ function airPatchContent(filePath: string, { change, hunks }: FilePatch): ToolCa
*
* The agent builds tool call content in v1 types, and v1 cannot express a v2
* diff, so the diff travels as v1 content. Nothing in the agent reads inside a
* diff, and only a v2 client gets one: the v2 surface sends it as it is
* diff, and only a v2 client gets one: the v2 core sends it as it is
* (`src/v2/tool-call.ts`).
*/
export function v2DiffContent(

View File

@@ -1,13 +1,18 @@
import { randomUUID } from "node:crypto";
import { CreateElicitationResponse } from "@agentclientprotocol/sdk/unstable";
import type {
CreateElicitationRequest,
ElicitationAcceptAction,
CreateElicitationResponse,
ElicitationPropertySchema,
ElicitationSchema,
EnumOption,
} from "@agentclientprotocol/sdk/unstable";
import type { ElicitationRequest, ElicitationResult } from "@anthropic-ai/claude-agent-sdk";
import type {
ElicitationRequest,
ElicitationResult,
PermissionResult,
UserDialogRequest,
UserDialogResult,
} from "@anthropic-ai/claude-agent-sdk";
import type {
AskUserQuestionInput,
AskUserQuestionOutput,
@@ -75,17 +80,22 @@ export function mcpElicitationToCreateRequest(
}
/**
* Content of an accepted elicitation response.
* Content of an accepted elicitation response, or `{}` when the response is
* no accept or its `content` is no JSON object.
*
* Uses the SDK's validating guard rather than an `action === "accept"` check:
* the guard both narrows past the union's custom/future variant and validates
* the payload, so a malformed accept (right tag, ill-typed content) yields
* empty content — the same classification the SDK's wire validators apply.
* The values are not checked. v1 allows a string, number, boolean, or string
* array; v2 also allows any other JSON value, such as an object. An MCP
* server checks the content against its own `requestedSchema`, so it gets the
* content as the client sent it: before, an accept whose content held an
* object reached the server as `{ action: "accept", content: {} }`. Callers
* that read a field must check its type.
*/
function acceptedElicitationContent(
response: CreateElicitationResponse,
): NonNullable<ElicitationAcceptAction["content"]> {
return CreateElicitationResponse.isAccept(response) ? (response.content ?? {}) : {};
function acceptedElicitationContent(response: CreateElicitationResponse): Record<string, unknown> {
if (response.action !== "accept") return {};
const content: unknown = (response as { content?: unknown }).content;
return content !== null && typeof content === "object" && !Array.isArray(content)
? (content as Record<string, unknown>)
: {};
}
/**
@@ -97,7 +107,11 @@ export function createElicitationResponseToElicitResult(
): ElicitationResult {
switch (response.action) {
case "accept":
return { action: "accept", content: acceptedElicitationContent(response) };
// `ElicitResult` types the values as v1 does. The MCP server checks them.
return {
action: "accept",
content: acceptedElicitationContent(response) as ElicitationResult["content"],
};
case "decline":
return { action: "decline" };
case "cancel":
@@ -295,7 +309,7 @@ export function applyAskElicitationResponse(
questions: AskUserQuestion[],
options: { reportUnanswered?: boolean } = {},
): AskUserQuestionOutcome {
// Typed against the output schema: as of SDK 0.3.312 only it lists
// Typed against the output schema: as of SDK 0.3.319 only it lists
// `unanswered`, though the tool reads the same annotations from its input.
const annotations: NonNullable<AskUserQuestionOutput["annotations"]> = {};
const withAnnotations = (answers: AskUserQuestionOutput["answers"]) => ({
@@ -325,13 +339,13 @@ export function applyAskElicitationResponse(
const custom = content[questionCustomFieldKey(index)];
const customText = typeof custom === "string" ? custom.trim() : "";
// A pick is a string, number, or boolean. Other values, such as an object
// that a v2 client may send, are dropped instead of read as "[object Object]".
const value = content[questionFieldKey(index)];
const picks: string[] =
value === undefined || value === null
? []
: Array.isArray(value)
? value.filter((item) => item !== undefined && item !== null && item !== "").map(String)
: [String(value)];
const picks: string[] = (Array.isArray(value) ? value : [value])
.filter(isElicitationScalar)
.map(String)
.filter((item) => item !== "");
// A multi-select is additive, and the form offers the selection and the
// custom box as independent fields — a user who fills both means both, so
@@ -374,10 +388,16 @@ export function applyAskElicitationResponse(
return { action: "answered", updatedInput: withAnnotations(answers) };
}
function isElicitationScalar(value: unknown): value is string | number | boolean {
return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
}
/**
* Coerce an arbitrary MCP `requestedSchema` into an ACP `ElicitationSchema`.
* The two are structurally compatible JSON Schemas; we just guarantee the
* `type: "object"` discriminator is present.
* `type: "object"` discriminator and a `properties` map are present. The v2
* schema requires both, so a client on SDK 2.2.0 or later rejects an MCP
* schema such as `{ type: "object" }` that has no `properties`.
*/
function normalizeElicitationSchema(
schema: Record<string, unknown> | undefined,
@@ -385,7 +405,8 @@ function normalizeElicitationSchema(
if (!schema || typeof schema !== "object") {
return { type: "object", properties: {} };
}
return { ...(schema as ElicitationSchema), type: "object" };
const { properties } = schema as Partial<ElicitationSchema>;
return { ...(schema as ElicitationSchema), type: "object", properties: properties ?? {} };
}
/**
@@ -497,3 +518,156 @@ export function refusalFallbackResultFromResponse(response: CreateElicitationRes
const choice = acceptedElicitationContent(response)[REFUSAL_FALLBACK_CHOICE_KEY];
return choice === RETRY_FALLBACK_RESULT ? RETRY_FALLBACK_RESULT : KEEP_REFUSAL_RESULT;
}
/**
* What became of an elicitation: the client's response, or `withdrawn` when
* its signal aborted first (Claude Code withdrew the request, or a cancel or
* close of the session did). A response that comes after the abort counts as
* withdrawn too.
*/
export type ElicitationOutcome = { response: CreateElicitationResponse } | { withdrawn: true };
/**
* Sends an elicitation with `send` and reads what became of it. A rejection
* while `signal` is not aborted, such as the client's -32602 for a form it
* cannot render, is rethrown.
*/
export async function elicitUnlessWithdrawn(
signal: AbortSignal,
send: (signal: AbortSignal) => Promise<CreateElicitationResponse>,
): Promise<ElicitationOutcome> {
try {
const response = await send(signal);
return signal.aborted ? { withdrawn: true } : { response };
} catch (error) {
if (signal.aborted) return { withdrawn: true };
throw error;
}
}
/** Sends a built elicitation to the client (see {@link elicitUnlessWithdrawn}). */
export type Elicit = (request: CreateElicitationRequest) => Promise<ElicitationOutcome>;
type ElicitationLogger = { error: (...args: unknown[]) => void };
/**
* Answers an MCP server's elicitation (the SDK's `onElicitation`) through
* `elicit`. Only the user's own decline reaches the MCP server as `decline`.
* A request the user never saw, because the client did not advertise its
* mode, it cannot be represented, the client answered with an error, or it
* was withdrawn, is a `cancel`.
*/
export async function answerMcpElicitation(
request: ElicitationRequest,
{
sessionId,
support,
elicit,
logger,
}: { sessionId: string; support: ElicitationSupport; elicit: Elicit; logger: ElicitationLogger },
): Promise<ElicitationResult> {
const isUrl = request.mode === "url";
if ((isUrl && !support.url) || (!isUrl && !support.form)) {
return { action: "cancel" };
}
const createRequest = mcpElicitationToCreateRequest(request, sessionId);
if (!createRequest) {
return { action: "cancel" };
}
try {
const outcome = await elicit(createRequest);
if ("withdrawn" in outcome) return { action: "cancel" };
return createElicitationResponseToElicitResult(outcome.response);
} catch (error) {
// The client answered with an error, e.g. -32602 for a form it cannot
// render, so the user never saw the request. MCP's `decline` means the
// user refused; `cancel` means they made no explicit choice.
logger.error(`Failed to forward MCP elicitation: ${error}`);
return { action: "cancel" };
}
}
/**
* Presents the AskUserQuestion tool's questions as a form elicitation of
* `sessionId` through `elicit`, and returns the answers as the tool's
* `updatedInput`. A withdrawn question, or one the user cancelled, aborts the
* tool use. `reportUnanswered` is read once the answer is in (see
* {@link applyAskElicitationResponse}).
*/
export async function answerAskUserQuestion(
toolInput: Record<string, unknown>,
{
sessionId,
toolUseID,
airClient = false,
reportUnanswered,
elicit,
logger,
}: {
sessionId: string;
toolUseID: string;
airClient?: boolean;
reportUnanswered: () => boolean;
elicit: Elicit;
logger: ElicitationLogger;
},
): Promise<PermissionResult> {
const questions = extractAskUserQuestions(toolInput);
if (!questions) {
return { behavior: "deny", message: "AskUserQuestion called with no valid questions." };
}
const createRequest = askUserQuestionsToCreateRequest(questions, sessionId, toolUseID, airClient);
let outcome: ElicitationOutcome;
try {
outcome = await elicit(createRequest);
} catch (error) {
logger.error(`Failed to present AskUserQuestion elicitation: ${error}`);
return { behavior: "deny", message: "Could not present the question to the user." };
}
if ("withdrawn" in outcome) {
throw new Error("Tool use aborted");
}
const answer = applyAskElicitationResponse(outcome.response, toolInput, questions, {
reportUnanswered: reportUnanswered(),
});
if (answer.action === "cancel") {
throw new Error("Tool use aborted");
}
return { behavior: "allow", updatedInput: answer.updatedInput };
}
/**
* Answers a `request_user_dialog` of Claude Code (the SDK's `onUserDialog`).
* Only kinds declared in `supportedDialogKinds` are ever emitted; everything
* unexpected is answered `cancelled` (the required answer for unrecognized
* kinds), which applies the dialog's default behavior CLI-side. The only
* declared kind is the refusal-fallback consent prompt, rendered as a form
* elicitation. A failed or withdrawn elicitation is `cancelled` too: the CLI
* then keeps the refusal rather than switching models without consent.
*/
export async function answerUserDialog(
request: UserDialogRequest,
{ sessionId, elicit, logger }: { sessionId: string; elicit: Elicit; logger: ElicitationLogger },
): Promise<UserDialogResult> {
if (request.dialogKind !== REFUSAL_FALLBACK_DIALOG_KIND) {
return { behavior: "cancelled" };
}
const prompt = extractRefusalFallbackPrompt(request.payload);
if (!prompt) {
logger.error(
`refusal_fallback_prompt payload had an unexpected shape; cancelling the dialog: ${JSON.stringify(request.payload)}`,
);
return { behavior: "cancelled" };
}
let outcome: ElicitationOutcome;
try {
outcome = await elicit(refusalFallbackToCreateRequest(prompt, sessionId));
} catch (error) {
logger.error(`Failed to present refusal fallback elicitation: ${error}`);
return { behavior: "cancelled" };
}
if ("withdrawn" in outcome) {
return { behavior: "cancelled" };
}
return { behavior: "completed", result: refusalFallbackResultFromResponse(outcome.response) };
}

View File

@@ -126,10 +126,9 @@ export function isExitPlanInterruptionResult(
);
}
/** The id of the plan that an approved ExitPlanMode publishes. Claude Code
* keeps one plan file per session, which a later plan mode rewrites, so each
* approval replaces the plan before it. The TodoWrite checklist is a
* separate plan. */
/** The base ID of an approved plan. AIR appends the review ID to keep each
* approval in the transcript. Other clients replace one plan per session.
* The TodoWrite checklist is a separate plan. */
export const PLAN_MODE_PLAN_ID = "plan-mode";
/** The plan, trimmed, that a successful ExitPlanMode result approved: the plan
@@ -150,11 +149,19 @@ export function approvedPlan(input: unknown, output: unknown): string | undefine
}
/** The update that shows an approved plan to a client that supports plan
* operations (the `plan` client capability, which every v2 client has). */
export function planModeUpdate(plan: string): SessionNotification["update"] {
* operations (the `plan` client capability, which every v2 client has).
* For AIR, pass the review ID to join the plan to its approval card. */
export function planModeUpdate(
plan: string,
reviewToolCallId?: string,
): SessionNotification["update"] {
return {
sessionUpdate: "plan_update",
plan: { type: "markdown", planId: PLAN_MODE_PLAN_ID, content: plan },
plan: {
type: "markdown",
planId: reviewToolCallId ? `${PLAN_MODE_PLAN_ID}:${reviewToolCallId}` : PLAN_MODE_PLAN_ID,
content: plan,
},
};
}
@@ -178,6 +185,7 @@ export type ExitPlanRestartHost<
sessionUpdate(notification: SessionNotification): Promise<void>;
/** Whether the client takes plan operations (see planModeUpdate). */
supportsPlanOperations(): boolean;
isAirClient(): boolean;
destroyReplacement(sessionId: string, session: Session): void;
settleCancelledTurn(oldSession: Session, turnSession: Session, turn: Turn): void;
settleFailedTurn(turnSession: Session, turn: Turn, error: unknown): void;
@@ -205,7 +213,7 @@ export class ExitPlanCoordinator<
try {
const clearContextHost: ClearContextCoordinatorHost<Session, Turn> = {
...this.host,
publishSessionState: async (id, mode, configOptions) => {
publishSessionState: async (id, mode, configOptions, contextWindowSize) => {
await this.host.sessionUpdate({
sessionId: id,
update: { sessionUpdate: "current_mode_update", currentModeId: mode },
@@ -214,11 +222,23 @@ export class ExitPlanCoordinator<
sessionId: id,
update: { sessionUpdate: "config_option_update", configOptions },
});
// Otherwise the client shows the old context's usage until the fresh
// context first reports its own.
await this.host.sessionUpdate({
sessionId: id,
update: { sessionUpdate: "usage_update", used: 0, size: contextWindowSize },
});
// The approved plan becomes the session's plan once the fresh
// context that carries it out exists. Its ExitPlanMode ended with
// an error result, so toAcpNotifications did not publish it.
if (this.host.supportsPlanOperations()) {
await this.host.sessionUpdate({ sessionId: id, update: planModeUpdate(reset.plan) });
await this.host.sessionUpdate({
sessionId: id,
update: planModeUpdate(
reset.plan,
this.host.isAirClient() ? reset.toolUseId : undefined,
),
});
}
},
continuationMessage: (id, plan, promptUuid) => ({

View File

@@ -104,11 +104,12 @@ const FILE_EDIT_TOOLS = new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
* Adds to `turn.editedPaths` the `file_path` (`notebook_path` for NotebookEdit)
* of each successful file-tool edit among the tool results of a subagent
* message. Root-agent messages (`parentToolUseId === null`) are skipped: the
* checkpoint already tracks their files. Must run before `toAcpNotifications`
* drops the tool use cache entries.
* checkpoint already tracks their files. A turn that v1 marked `settled`
* records nothing. Must run before `toAcpNotifications` drops the tool use
* cache entries.
*/
export function recordSubagentFileEdits(
turn: (NativeFileChangeReportTurn & { settled: boolean }) | null | undefined,
turn: (NativeFileChangeReportTurn & { settled?: boolean }) | null | undefined,
parentToolUseId: string | null,
content: unknown,
toolUses: Record<string, { name: string; input: unknown } | undefined>,

View File

@@ -69,10 +69,7 @@ function forkPointMessageIdCandidates(messageId: string): string[] {
return protocolMessageId === messageId ? [messageId] : [messageId, protocolMessageId];
}
async function loadFullSessionHistory(
sessionId: string,
cwd: string,
): Promise<SessionStoreEntry[]> {
async function loadFullSessionHistory(sessionId: string): Promise<SessionStoreEntry[]> {
// getSessionMessages returns only the active parentUuid chain. The import keeps inactive branches that AIR can still reference.
const entries: SessionStoreEntry[] = [];
const store: SessionStore = {
@@ -81,7 +78,7 @@ async function loadFullSessionHistory(
},
load: async () => null,
};
await importSessionToStore(sessionId, store, { dir: cwd, includeSubagents: false });
await importSessionToStore(sessionId, store, { includeSubagents: false });
return entries;
}
@@ -179,14 +176,44 @@ function resolveFromFullHistory(
return occurrenceMatches.length === 1 ? occurrenceMatches[0]?.uuid : undefined;
}
/**
* Whether `error` is the Claude SDK's report that `sessionId` has no
* transcript: `forkSession` and `importSessionToStore` throw
* `Error("Session <id> not found")`, and both throw
* `"Invalid sessionId: <id>"` for an id that is not a UUID.
*/
function isUnknownSessionError(error: unknown, sessionId: string): boolean {
if (!(error instanceof Error)) return false;
return (
error.message === `Session ${sessionId} not found` ||
error.message === `Invalid sessionId: ${sessionId}`
);
}
export async function forkSession(
params: ForkSessionRequest,
dependencies: ForkSessionDependencies,
): Promise<ForkSessionResponse> {
try {
return await forkTranscript(params, dependencies);
} catch (error) {
if (isUnknownSessionError(error, params.sessionId)) {
throw RequestError.sessionNotFound(params.sessionId);
}
throw error;
}
}
async function forkTranscript(
params: ForkSessionRequest,
dependencies: ForkSessionDependencies,
): Promise<ForkSessionResponse> {
const timing = new SessionTiming(dependencies.logger, "fork", params.sessionId);
const point = forkPoint(params._meta);
// SDK `dir` scopes source lookup, not the fork's working directory. Search
// all projects: params.cwd can name a different workspace for the new session.
if (!point) {
const forked = await forkClaudeSession(params.sessionId, { dir: params.cwd });
const forked = await forkClaudeSession(params.sessionId);
timing.phase("sdk-fork", " resolution=latest");
return { sessionId: forked.sessionId };
}
@@ -195,9 +222,7 @@ export async function forkSession(
const liveUuid = candidateIds
.map((candidateId) => dependencies.liveMessageIdToUuid?.get(candidateId))
.find(Boolean);
const history = liveUuid
? undefined
: await getSessionMessages(params.sessionId, { dir: params.cwd });
const history = liveUuid ? undefined : await getSessionMessages(params.sessionId);
timing.phase("active-history", ` resolution=${liveUuid ? "live" : "pending"}`);
const messageUuid =
liveUuid ??
@@ -213,7 +238,7 @@ export async function forkSession(
const fullHistoryUuid = messageUuid
? undefined
: resolveFromFullHistory(
await loadFullSessionHistory(params.sessionId, params.cwd),
await loadFullSessionHistory(params.sessionId),
candidateIds,
point,
dependencies.messageIdForGrouping,
@@ -231,7 +256,6 @@ export async function forkSession(
}
const forked = await forkClaudeSession(params.sessionId, {
dir: params.cwd,
upToMessageId: messageUuid ?? fullHistoryUuid,
});
timing.phase("sdk-fork");

View File

@@ -2,7 +2,7 @@
* `--hide-claude-auth`: this integration must never bill a claude.ai
* subscription. Under the flag the agent hides the claude.ai login method from
* `initialize`, and refuses with `authRequired` every session and every turn
* that a subscription would pay for. `acp-agent.ts` decides when the guard
* that a subscription would pay for. `v1/acp-agent.ts` decides when the guard
* applies (flag on, no provider override); this module decides what it refuses.
*
* The guard judges the `AccountInfo` that the SDK caches at `initialize`. That
@@ -35,8 +35,10 @@
import { RequestError } from "@agentclientprotocol/sdk/unstable";
import type { AccountInfo, Query } from "@anthropic-ai/claude-agent-sdk";
export const HIDE_CLAUDE_AUTH_FLAG = "--hide-claude-auth";
export function shouldHideClaudeAuth(): boolean {
return process.argv.includes("--hide-claude-auth");
return process.argv.includes(HIDE_CLAUDE_AUTH_FLAG);
}
/**
@@ -208,14 +210,14 @@ type GuardOptions = {
* covers a session created under a provider override that `providers/disable`
* later removed, so the create-time check never ran for the routing in force
* now. A sign-out ends the query (see `markSessionForSignOutRespawn` in
* `acp-agent.ts`), so the next turn recreates it and `initialize` reports the
* `v1/acp-agent.ts`), so the next turn recreates it and `initialize` reports the
* real account. This guard does not see the residual case the module header
* names either.
*
* A turn is refused when the account bills a subscription, and also when it
* holds nothing this integration accepts, which is the user signing out in
* mid-session. The refusal mirrors the signed-out path exactly (see
* `failActiveWithSessionFailure` for `auth_required` in `acp-agent.ts`): every
* `failActiveWithSessionFailure` for `auth_required` in `v1/acp-agent.ts`): every
* client gets the `authRequired` JSON-RPC rejection that starts its own auth
* flow. The guard sends no separate access message.
*

240
src/idle-compaction.ts Normal file
View File

@@ -0,0 +1,240 @@
export type PromptCacheTtl = "5m" | "1h";
export type IdleCompactionHost = {
/** Claude is working (`startTurn`'s check) or a permission request / user input is pending. */
isBusy(): boolean;
/** Context size after the last main-thread request, or undefined if unknown. */
contextTokens(): number | undefined;
/** Current merged Claude Code settings. */
settings(): {
idleCompaction?: boolean;
autoCompactEnabled?: boolean;
env?: Record<string, string | undefined>;
};
/** Runs `/compact` as an adapter-originated turn; settles with that turn. */
compact(): Promise<unknown>;
/** Debug-level log. */
log(message: string): void;
};
export type IdleCompactionOptions = {
env?: Record<string, string | undefined>;
setTimeout?: (callback: () => void, ms: number) => unknown;
};
/** The longest delay `setTimeout` takes; a longer one fires at once. */
const MAX_TIMER_MS = 2_147_483_647;
const HOUR_MS = 3_600_000;
const DEFAULT_MIN_TOKENS = 200_000;
const MIN_TOKENS_FLOOR = 100_000;
// 54 min for a 1h cache: leaves a margin for the gap between the provider
// writing the cache and the adapter receiving the response.
const DEFAULT_DELAY_MS = 0.9 * HOUR_MS;
// Node timers do not run while the machine sleeps, so one can fire hours after
// its deadline, on a cache that expired meanwhile. Same threshold as Claude Code.
const LAPSED_AFTER_MS = 60_000;
// The values Claude Code reads as "on" for env flags such as DISABLE_COMPACT
// (case-insensitive, trimmed).
const TRUTHY_ENV = new Set(["1", "true", "yes", "on"]);
function isTruthyEnv(value: string | undefined): boolean {
return value !== undefined && TRUTHY_ENV.has(value.trim().toLowerCase());
}
function parseCacheTtl(usage: unknown): PromptCacheTtl | undefined {
const creation = (usage as { cache_creation?: unknown } | null | undefined)?.cache_creation;
if (typeof creation !== "object" || creation === null) return undefined;
const { ephemeral_1h_input_tokens: hour, ephemeral_5m_input_tokens: fiveMin } = creation as {
ephemeral_1h_input_tokens?: unknown;
ephemeral_5m_input_tokens?: unknown;
};
if (typeof hour === "number" && hour > 0) return "1h";
if (typeof fiveMin === "number" && fiveMin > 0) return "5m";
return undefined;
}
/**
* Compacts a session shortly before its one-hour prompt cache expires, so the
* next prompt writes a short summary instead of the whole context. Claude Code
* has this feature but arms it only for "attended" sessions, which SDK sessions
* are not. One instance per session.
*/
export class IdleCompaction {
private readonly env: Record<string, string | undefined>;
private readonly setTimer: (callback: () => void, ms: number) => unknown;
private ttl: PromptCacheTtl | undefined;
private lastRequestAt: number | undefined;
private lastMessageId: string | undefined;
/** Set when `/compact` was sent, cleared by the next request: a compaction that failed,
* or left the context large, would otherwise re-arm at once and loop until the user prompts. */
private compactedSinceLastRequest = false;
private timer: unknown;
private compacting = false;
private disposed = false;
private warnedInvalidDelay = false;
/** `status` of the last `rate_limit_event`; unknown until one arrives. */
private rateLimitStatus: string | undefined;
constructor(
private readonly host: IdleCompactionHost,
options: IdleCompactionOptions = {},
) {
this.env = options.env ?? process.env;
this.setTimer = options.setTimeout ?? ((cb, ms) => setTimeout(cb, ms));
}
/** Whether an idle compaction it started is still running. `v1/acp-agent.ts` reports
* `trigger: "idle"` while it is true. A client's `/compact` queued behind is read
* only after the adapter's turn's `result`, which clears `compacting`; this relies
* on the CLI emitting that compaction's `compact_boundary` after the previous
* result, which nothing here enforces. */
get isCompacting(): boolean {
return this.compacting;
}
/** A main-thread response arrived. Records its time and cache TTL, and drops any armed timer.
* The provider writes the cache when the request starts, so the deadline is measured from the
* response's first sighting: the `message_start` stream event, or its first `assistant` message
* when it was not streamed. The CLI sends one `assistant` message per content block, and a
* 7-minute response would otherwise put the deadline 7 minutes after the cache was written.
* A later call with the same `messageId` only updates the TTL. */
noteRequest(usage: unknown, messageId?: string): void {
if (this.disposed) return;
this.ttl = parseCacheTtl(usage) ?? this.ttl;
if (messageId !== undefined && messageId === this.lastMessageId) return;
this.lastMessageId = messageId;
this.lastRequestAt = Date.now();
this.compactedSinceLastRequest = false;
this.disarm("new request");
}
/** The session became idle. Arms the timer if every condition holds, else logs why not. */
onIdle(): void {
if (this.disposed) return;
const skip = this.skipReason();
if (skip) {
this.host.log(`[idle-compaction] skip: ${skip}`);
return;
}
this.disarm("rearm");
const deadline = this.lastRequestAt! + this.delayMs();
this.timer = this.setTimer(() => void this.fire(), Math.max(0, deadline - Date.now()));
(this.timer as { unref?: () => void } | undefined)?.unref?.();
this.host.log(`[idle-compaction] armed deadline=${new Date(deadline).toISOString()}`);
}
/** A `rate_limit_event` arrived (`SDKRateLimitInfo`). Anything but `allowed` skips compaction,
* as in Claude Code. This is account state, so `reset()` keeps it. */
noteRateLimit(info: unknown): void {
const status = (info as { status?: unknown } | null | undefined)?.status;
if (typeof status === "string") this.rateLimitStatus = status;
}
/** Claude Code reported its compaction as failed. Its turn still ends normally, so `compact()` does not reject. */
noteCompactionFailed(error: string | undefined): void {
if (!this.compacting) return;
this.host.log(`[idle-compaction] compaction failed: ${error ?? "no reason given"}`);
}
/** The conversation was replaced (`/clear`): forgets the recorded request and TTL, and drops any armed timer. */
reset(): void {
this.ttl = undefined;
this.lastRequestAt = undefined;
this.lastMessageId = undefined;
this.compactedSinceLastRequest = false;
this.disarm("conversation reset");
}
/** Drops the armed timer, if any. `reason` is only logged. */
disarm(reason: string): void {
if (this.timer === undefined) return;
clearTimeout(this.timer as ReturnType<typeof setTimeout>);
this.timer = undefined;
this.host.log(`[idle-compaction] disarmed: ${reason}`);
}
/** Disarms and ignores every later call. */
dispose(): void {
this.disarm("disposed");
this.disposed = true;
}
private async fire(): Promise<void> {
this.timer = undefined;
if (this.disposed) return;
const skip = this.skipReason();
if (skip) {
this.host.log(`[idle-compaction] skip at fire: ${skip}`);
return;
}
this.host.log("[idle-compaction] firing /compact");
this.compacting = true;
this.compactedSinceLastRequest = true;
try {
await this.host.compact();
} catch (error) {
this.host.log(`[idle-compaction] compaction failed: ${error}`);
} finally {
this.compacting = false;
}
}
/** Why no compaction should be scheduled right now, or undefined if all conditions hold. */
private skipReason(): string | undefined {
if (this.compacting) return "compaction already running";
const disabled = this.disabledBy();
if (disabled) return `disabled by ${disabled}`;
if (this.lastRequestAt === undefined) return "no request recorded in this process";
if (Date.now() > this.lastRequestAt + this.delayMs() + LAPSED_AFTER_MS) {
return "cache lapsed: more than 60 s past the deadline";
}
if (this.rateLimitStatus !== undefined && this.rateLimitStatus !== "allowed") {
return `rate limit status is ${this.rateLimitStatus}`;
}
if (this.compactedSinceLastRequest) return "already compacted for this request";
if (this.ttl !== "1h") return `cache TTL is ${this.ttl ?? "unknown"}, not 1h`;
const tokens = this.host.contextTokens();
const min = this.minTokens();
if (tokens === undefined || tokens < min) {
return `context ${tokens ?? "unknown"} tokens is below the ${min} minimum`;
}
if (this.host.isBusy()) return "session is busy";
return undefined;
}
private disabledBy(): string | undefined {
const settings = this.host.settings();
if (settings.idleCompaction === false) return "idleCompaction setting";
if (settings.autoCompactEnabled === false) return "autoCompactEnabled setting";
for (const name of ["DISABLE_COMPACT", "DISABLE_AUTO_COMPACT"]) {
if (isTruthyEnv(this.claudeCodeEnv(name))) return name;
}
return undefined;
}
/** The CLI applies the settings `env` block after the launch env, so it wins. */
private claudeCodeEnv(name: string): string | undefined {
return this.host.settings().env?.[name] ?? this.env[name];
}
/** `0` and invalid values fall back to 200,000, as in the CLI, which declares the variable
* with `min: 1` and computes `Math.max(100000, env ?? 200000)`. */
private minTokens(): number {
const raw = this.claudeCodeEnv("CLAUDE_CODE_IDLE_COMPACT_MIN_TOKENS");
const parsed = raw !== undefined && /^\d+$/.test(raw.trim()) ? Number(raw.trim()) : 0;
return Math.max(MIN_TOKENS_FLOOR, parsed > 0 ? parsed : DEFAULT_MIN_TOKENS);
}
private delayMs(): number {
const raw = this.env.CLAUDE_AGENT_ACP_IDLE_COMPACT_DELAY_MS;
if (raw === undefined) return DEFAULT_DELAY_MS;
if (/^\d+$/.test(raw.trim()) && Number(raw.trim()) <= MAX_TIMER_MS) return Number(raw.trim());
if (!this.warnedInvalidDelay) {
this.warnedInvalidDelay = true;
this.host.log(
`[idle-compaction] ignoring invalid CLAUDE_AGENT_ACP_IDLE_COMPACT_DELAY_MS=${raw}`,
);
}
return DEFAULT_DELAY_MS;
}
}

View File

@@ -1,6 +1,7 @@
#!/usr/bin/env node
import { claudeCliPath } from "./acp-agent.js";
import { cliArgs } from "./agent-command.js";
import { claudeCliPath } from "./claude-cli.js";
import { applyManagedPolicyEnv } from "./managed-policy.js";
import { acpProtocolsFromEnv, serveAcp } from "./serve.js";
import packageJson from "../package.json" with { type: "json" };
@@ -12,7 +13,7 @@ import { join } from "node:path";
// handler below. Our version flag only applies when not delegating.
if (process.argv.includes("--cli")) {
const { spawn } = await import("node:child_process");
const args = process.argv.slice(2).filter((arg) => arg !== "--cli");
const args = cliArgs();
const child = spawn(await claudeCliPath(), args, { stdio: "inherit" });
const signals =

View File

@@ -1,25 +1,7 @@
// Export the main agent class and utilities for library usage
export {
ClaudeAcpAgent,
isLocalCommandMetadata,
stripLocalCommandMetadata,
runAcp,
toAcpNotifications,
streamEventToAcpNotifications,
type ToolUpdateMeta,
type NewSessionMeta,
type SDKMessageFilter,
} from "./acp-agent.js";
export { nodeToWebReadable, nodeToWebWritable, Pushable, unreachable } from "./utils.js";
export {
toolInfoFromToolUse,
toDisplayPath,
planEntries,
toolUpdateFromToolResult,
} from "./tools.js";
export { SettingsManager, type SettingsManagerOptions } from "./settings.js";
// The `authStatus` extension's wire surface: the notification's method name and
// the payload types needed to read the traffic. The mappers stay internal.
// The package's public API: serve ACP (v1 and v2) on stdio, and the wire
// names of the `authStatus` extension that a client needs to read its traffic.
export { serveAcp, type AcpProtocols } from "./serve.js";
export type { Logger } from "./logger.js";
export {
AUTH_STATUS_UPDATE_METHOD,
type AuthStatus,
@@ -27,6 +9,3 @@ export {
type AuthStatusKind,
type AuthStatusUpdateNotification,
} from "./auth-status.js";
// Export types
export type { ClaudePlanEntry } from "./tools.js";

9
src/logger.ts Normal file
View File

@@ -0,0 +1,9 @@
/**
* Logger interface for customizing logging output
*/
export interface Logger {
log: (...args: any[]) => void;
error: (...args: any[]) => void;
/** Optional: a caller that supplies no `warn` gets its warnings on `error`. */
warn?: (...args: any[]) => void;
}

View File

@@ -111,8 +111,11 @@ function needsReconnect(status: McpServerStatus): boolean {
function codeSpan(text: string): string {
const longestRun = Math.max(0, ...(text.match(/`+/g) ?? []).map((run) => run.length));
const fence = "`".repeat(longestRun + 1);
// CommonMark strips one space from each end of a span, so a pad keeps the text unchanged.
const pad = text.startsWith("`") || text.endsWith("`") || /^ .*[^ ].* $/.test(text) ? " " : "";
// CommonMark strips one space from each end of a span that is not all spaces
// (line endings count as spaces), so a pad keeps the text unchanged. Plain
// string checks, not one regex: `^ .*[^ ].* $` backtracks quadratically.
const strippedByCommonMark = text.startsWith(" ") && text.endsWith(" ") && /[^ \r\n]/.test(text);
const pad = text.startsWith("`") || text.endsWith("`") || strippedByCommonMark ? " " : "";
return `${fence}${pad}${text}${pad}${fence}`;
}

214
src/mcp-oauth.ts Normal file
View File

@@ -0,0 +1,214 @@
/**
* OAuth for the MCP servers that Claude Code reports as `needs-auth`, as an
* ACP URL elicitation. Protocol-neutral: the v1 core and the v2 core
* both start it once a session's setup response is sent.
*/
import { randomUUID } from "node:crypto";
import type {
CompleteElicitationNotification,
CreateElicitationRequest,
CreateElicitationResponse,
} from "@agentclientprotocol/sdk/unstable";
import type { McpServerStatus, Query } from "@anthropic-ai/claude-agent-sdk";
/** Claude Code keeps the OAuth callback listener open in the background after
* `mcpAuthenticate` returns the authorization URL. The SDK does not expose
* that listener's completion promise, so watch the server status while the
* ACP client has the URL elicitation open. */
const MCP_OAUTH_STATUS_POLL_MS = 1_000;
const MCP_OAUTH_TIMEOUT_MS = 10 * 60_000;
/** Runtime MCP OAuth control exposed by the pinned Agent SDK. It is not yet in
* the public `Query` declaration, even though the method is present on the
* SDK query object and backed by Claude Code's `mcp_authenticate` control. */
type McpOAuthQuery = Query & {
mcpAuthenticate(
serverName: string,
redirectUri?: string,
): Promise<{
authUrl?: string;
requiresUserAction: boolean;
callbackExpected: boolean;
redirectScheme?: "localhost" | "custom";
callbackPort?: number;
state?: string;
}>;
};
export function supportsMcpOAuth(query: Query): query is McpOAuthQuery {
return typeof (query as Partial<McpOAuthQuery>).mcpAuthenticate === "function";
}
/** What an OAuth run needs of the client connection. */
export type McpOAuthClient = {
/** Whether the client advertised URL elicitation. Without it, OAuth does not start. */
readonly urlElicitation: boolean;
createElicitation(
params: CreateElicitationRequest,
signal: AbortSignal,
): Promise<CreateElicitationResponse>;
completeElicitation(params: CompleteElicitationNotification): Promise<void>;
readonly logger: { error: (...args: unknown[]) => void };
};
/** What an OAuth run needs of the session. */
export type McpOAuthSession = {
/** One OAuth run at a time per key: the session object. */
readonly key: object;
readonly sessionId: string;
/** The session's query now, or `undefined` once the session is gone. */
currentQuery(): Query | undefined;
/** Aborts when the session closes. */
readonly closed: AbortSignal;
};
const mcpAuthentications = new WeakMap<object, Promise<void>>();
/** Start OAuth in the background for the servers in `requestedServers` that
* Claude reports as needing authentication. It does nothing while another
* OAuth run of the session is in progress. */
export function startMcpServerAuthentication(
client: McpOAuthClient,
session: McpOAuthSession,
requestedServers: Set<string>,
): void {
if (!client.urlElicitation || requestedServers.size === 0) return;
const query = session.currentQuery();
if (!query || mcpAuthentications.has(session.key)) return;
const authentication = authenticateMcpServers(client, session, query, requestedServers)
.catch((error) => {
if (!session.closed.aborted) {
client.logger.error(`Failed to inspect MCP servers for OAuth: ${error}`);
}
})
.finally(() => {
if (mcpAuthentications.get(session.key) === authentication) {
mcpAuthentications.delete(session.key);
}
});
mcpAuthentications.set(session.key, authentication);
}
async function authenticateMcpServers(
client: McpOAuthClient,
session: McpOAuthSession,
query: Query,
requestedServers: Set<string>,
): Promise<void> {
if (!supportsMcpOAuth(query)) {
client.logger.error("The Claude Agent SDK does not expose MCP OAuth authentication.");
return;
}
const statuses = await query.mcpServerStatus();
for (const status of statuses) {
if (status.status !== "needs-auth" || !requestedServers.has(status.name)) continue;
try {
await authenticateMcpServer(client, session, query, status.name);
} catch (error) {
if (session.currentQuery() && !session.closed.aborted) {
client.logger.error(`Failed to authenticate MCP server ${status.name}: ${error}`);
}
}
}
}
/** Bridge Claude Code's startup MCP OAuth control to ACP URL elicitation.
* Claude opens and owns the localhost callback listener; the ACP client only
* needs to present the returned authorization URL. */
async function authenticateMcpServer(
client: McpOAuthClient,
session: McpOAuthSession,
query: McpOAuthQuery,
serverName: string,
): Promise<void> {
if (!session.currentQuery()) return;
const login = await query.mcpAuthenticate(serverName);
if (!login.requiresUserAction) return;
if (!login.authUrl) {
throw new Error("Claude Code requested user action without returning an authorization URL");
}
const elicitationId = `mcp-oauth-${randomUUID()}`;
const flowAbort = new AbortController();
const abortFlow = () => flowAbort.abort(session.closed.reason);
session.closed.addEventListener("abort", abortFlow, { once: true });
if (session.closed.aborted) abortFlow();
try {
const completed = waitForMcpAuthentication(session, query, serverName, flowAbort.signal);
const elicitation = client.createElicitation(
{
mode: "url",
sessionId: session.sessionId,
message: `Authenticate with MCP server ${serverName}`,
url: login.authUrl,
elicitationId,
},
flowAbort.signal,
);
const first = await Promise.race([
completed.then((authenticated) => ({ type: "completed" as const, authenticated })),
elicitation.then((response) => ({ type: "elicitation" as const, response })),
]);
// A url-mode accept carries no content to check. The guard
// `CreateElicitationResponse.isAccept` would also reject an accept whose
// `content` holds a v2-only value, such as an object.
if (first.type === "elicitation" && first.response.action !== "accept") {
return;
}
if (first.type === "elicitation") {
await completed;
}
try {
await client.completeElicitation({ elicitationId });
} catch (error) {
if (!flowAbort.signal.aborted) {
client.logger.error(`Failed to complete MCP OAuth elicitation: ${error}`);
}
}
} finally {
flowAbort.abort();
session.closed.removeEventListener("abort", abortFlow);
}
}
async function waitForMcpAuthentication(
session: McpOAuthSession,
query: Query,
serverName: string,
signal: AbortSignal,
): Promise<boolean> {
const deadline = Date.now() + MCP_OAUTH_TIMEOUT_MS;
while (!signal.aborted && Date.now() < deadline) {
if (session.currentQuery() !== query) return false;
const status: McpServerStatus | undefined = (await query.mcpServerStatus()).find(
(server) => server.name === serverName,
);
if (!status || status.status === "failed" || status.status === "disabled") return false;
if (status.status === "connected") return true;
if (!(await waitUnlessAborted(MCP_OAUTH_STATUS_POLL_MS, signal))) return false;
}
return false;
}
/** Wait for a polling interval, resolving false when the session is aborted. */
export function waitUnlessAborted(milliseconds: number, signal: AbortSignal): Promise<boolean> {
if (signal.aborted) return Promise.resolve(false);
return new Promise((resolve) => {
const timer = setTimeout(() => {
signal.removeEventListener("abort", onAbort);
resolve(true);
}, milliseconds);
const onAbort = () => {
clearTimeout(timer);
resolve(false);
};
signal.addEventListener("abort", onAbort, { once: true });
});
}

33
src/mcp-servers.ts Normal file
View File

@@ -0,0 +1,33 @@
import type { McpServer } from "@agentclientprotocol/sdk/unstable";
import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
/**
* The SDK configs of a session's ACP MCP servers, by name. HTTP and SSE
* servers keep their URL and headers; a server without `type` is stdio. A
* server of any other transport is left out: the session's `initialize`
* advertises none.
*/
export function sdkMcpServers(
servers: readonly McpServer[] | null | undefined,
): Record<string, McpServerConfig> {
const configs: Record<string, McpServerConfig> = {};
for (const server of servers ?? []) {
if ("type" in server && (server.type === "http" || server.type === "sse")) {
configs[server.name] = {
type: server.type,
url: server.url,
headers: server.headers
? Object.fromEntries(server.headers.map((e) => [e.name, e.value]))
: undefined,
};
} else if (!("type" in server)) {
configs[server.name] = {
type: "stdio",
command: server.command,
args: server.args,
env: server.env ? Object.fromEntries(server.env.map((e) => [e.name, e.value])) : undefined,
};
}
}
return configs;
}

213
src/message-filters.ts Normal file
View File

@@ -0,0 +1,213 @@
/**
* The Claude Code messages and content that a client never sees as the user's
* or the agent's text: local slash command markers, context the CLI injects,
* the synthetic `/login` message and meta messages from other agents. The live
* stream and the `session/load` replay both filter with these.
*/
import { isLoginRequiredText } from "./result-end.js";
import { isSyntheticUsageLimitMessage } from "./session-failure-extension.js";
import { markerTagText, TASK_NOTIFICATION_TAG } from "./task-notifications.js";
// Slash commands that the SDK handles locally without replaying the user
// message and without invoking the model.
export const LOCAL_ONLY_COMMANDS = new Set(["/context", "/heapdump", "/extra-usage"]);
// Commands whose marker-only transcript records are ACP/client-local UI noise,
// not model-visible user prompts. Marker-only custom slash skills are not in
// this list, so replay can reconstruct them from `<command-name>`/`<command-args>`.
const REPLAY_HIDDEN_COMMANDS = new Set([
...LOCAL_ONLY_COMMANDS,
"/compact",
"/model",
"/mcp",
"/status",
"/usage",
]);
// The Claude SDK persists local slash command invocations (e.g. `/model`) and
// their output as user messages in the session transcript, wrapping the
// payload in these XML-like markers that the CLI uses for its own display.
// The live prompt loop drops them; replay must strip them too or they leak
// into the UI on session/load.
const LOCAL_COMMAND_MARKERS = [
"command-name",
"command-message",
"command-args",
"local-command-stdout",
"local-command-stderr",
].map((tag) => ({ open: `<${tag}>`, close: `</${tag}>` }));
// Context the CLI injects into a user turn to steer the model, appended to
// whatever the user typed. Nobody wrote it and no client sees it live — the
// prompt loop's user-message skip covers the whole message — but it is
// persisted alongside that prose, so replay would hand the client a prompt
// with instructions in it the user never gave. A `task-notification` tells the
// model that a background task stopped; live, the SDK `task_notification`
// frame reports the same stop, so replay restores the task state from it (see
// taskNotificationsOf) instead of showing it.
const INJECTED_CONTEXT_MARKERS = ["system-reminder", TASK_NOTIFICATION_TAG].map((tag) => ({
open: `<${tag}>`,
close: `</${tag}>`,
}));
// Everything a replayed user message may be wrapped in that is not speech.
const TRANSCRIPT_MARKERS = [...LOCAL_COMMAND_MARKERS, ...INJECTED_CONTEXT_MARKERS];
// Single-pass scanner that removes each `<tag>…</tag>` marker (matching the
// nearest closing tag of the same name, like a lazy regex would).
function stripMarkerTags(text: string): string {
const dead = new Set<string>();
let result = "";
let copiedUpTo = 0;
let i = 0;
while (i < text.length) {
if (text[i] === "<") {
const marker = TRANSCRIPT_MARKERS.find(
(m) => !dead.has(m.open) && text.startsWith(m.open, i),
);
if (marker) {
const end = text.indexOf(marker.close, i + marker.open.length);
if (end !== -1) {
result += text.slice(copiedUpTo, i);
i = copiedUpTo = end + marker.close.length;
continue;
}
// No closing marker remains anywhere ahead, and `indexOf` only ever
// searches forward from here on, so stop treating this tag as an
// opener — that avoids rescanning the tail for it on every match.
dead.add(marker.open);
}
}
i++;
}
return result + text.slice(copiedUpTo);
}
function hasMarkerTag(text: string, tag: string): boolean {
return markerTagText(text, tag) !== undefined;
}
function commandInvocationFromMarkerOnlyText(text: string): string | null {
if (hasMarkerTag(text, "local-command-stdout") || hasMarkerTag(text, "local-command-stderr")) {
return null;
}
const commandName = markerTagText(text, "command-name")?.trim();
if (!commandName?.startsWith("/")) return null;
if (REPLAY_HIDDEN_COMMANDS.has(commandName.split(" ", 1)[0])) return null;
const commandArgs = markerTagText(text, "command-args")?.trim();
return commandArgs ? `${commandName} ${commandArgs}` : commandName;
}
function stripLocalCommandMetadataText(text: string): string | null {
const stripped = stripMarkerTags(text);
if (stripped.trim() !== "") return stripped;
return commandInvocationFromMarkerOnlyText(text);
}
/**
* Return user-message content with local-command and injected-context marker
* tags removed, or `null` if nothing meaningful remains (caller should skip
* the message). Preserves real prose that's mixed in alongside the markers —
* e.g. a message like `<command-name>…</command-name>hi` becomes `hi`, and
* `hi<system-reminder>…</system-reminder>` becomes `hi`.
*/
export function stripLocalCommandMetadata(content: unknown): unknown | null {
if (typeof content === "string") {
return stripLocalCommandMetadataText(content);
}
if (!Array.isArray(content)) return content;
const kept: unknown[] = [];
for (const block of content) {
if (
block &&
typeof block === "object" &&
"type" in block &&
(block as { type: unknown }).type === "text" &&
"text" in block &&
typeof (block as { text: unknown }).text === "string"
) {
const stripped = stripLocalCommandMetadataText((block as { text: string }).text);
if (stripped === null) continue;
kept.push({ ...(block as object), text: stripped });
} else {
kept.push(block);
}
}
if (kept.length === 0) return null;
return kept;
}
/**
* True for the synthetic assistant message the CLI injects into the transcript
* when a turn fails authentication (e.g. "Not logged in · Please run /login",
* "Session expired. Please run /login to sign in again."). The `/login`
* instruction is Claude Code TUI-specific and meaningless to ACP clients
* (issue #863). The live prompt loop suppresses the text and fails the turn
* with `authRequired` so the client can run its own auth flow; replay must
* skip it too — both for parity with what the client saw live and because the
* message stays in the transcript forever, so it would resurface on every
* session/load even after the user has logged back in.
*
* Takes the API message (`message.message`), which replay only knows as
* `unknown`. The persisted record's structured `error` marker is stripped by
* `getSessionMessages`, and the CLI does not always set it to
* `authentication_failed` anyway: an expired login with no refresh token
* ("Login expired · Run /login to sign in again, …") arrives as
* `invalid_request`. So the synthetic model + text is what both paths match on.
* Usage-limit banners are excluded because some of them also mention `/login`
* (to switch accounts) and have their own handling.
*/
export function isSyntheticLoginMessage(apiMessage: unknown): boolean {
if (!apiMessage || typeof apiMessage !== "object") {
return false;
}
const { model, content } = apiMessage as { model?: unknown; content?: unknown };
if (model !== "<synthetic>" || !Array.isArray(content) || content.length !== 1) {
return false;
}
const block = content[0] as { type?: unknown; text?: unknown } | null;
return (
!!block &&
block.type === "text" &&
typeof block.text === "string" &&
isLoginRequiredText(block.text) &&
!isSyntheticUsageLimitMessage(apiMessage)
);
}
/** Origin kinds of the meta user messages `getSessionMessages` returns since
* SDK 0.3.284 (messages from other agents, sessions and channels). */
const REPLAY_HIDDEN_META_ORIGIN_KINDS = new Set([
"peer",
"channel",
"observer",
"observer-activity",
"slack-ping",
]);
/**
* True for a transcript message delivered to the model from another agent,
* session or channel. Since SDK 0.3.284 `getSessionMessages` returns these as
* `is_meta` user messages whose content is the full harness framing (envelope
* XML plus the "not typed by your user" preamble). The live prompt loop never
* renders them, so replay skips them too rather than presenting them as text
* the user typed. Other `is_meta` messages (compact summaries) are kept.
*
* `is_meta` and `origin` are runtime fields not declared on `SessionMessage`.
*/
export function isReplayHiddenMetaMessage(message: unknown): boolean {
if (!message || typeof message !== "object") return false;
const { type, is_meta, origin } = message as {
type?: unknown;
is_meta?: unknown;
origin?: { kind?: unknown } | null;
};
return (
type === "user" &&
is_meta === true &&
typeof origin?.kind === "string" &&
REPLAY_HIDDEN_META_ORIGIN_KINDS.has(origin.kind)
);
}

View File

@@ -3,9 +3,12 @@ import {
type SessionConfigOption,
type SessionNotification,
} from "@agentclientprotocol/sdk/unstable";
import { realpath } from "node:fs/promises";
import path from "node:path";
import type { AcpSessionNotification, SubagentForm, SubagentState } from "./acp-subagents.js";
import { AIR_SUBAGENT_KEY, airExtensionMeta } from "./air-extension.js";
import { MODEL_CONFIG_ID } from "./session-model.js";
import { orderedSubagentConfiguration } from "./subagent-configuration.js";
import { isFinalToolCallStatus } from "./tool-calls/renderer.js";
/** The work state of a child, as the RFD's `subagent_update.state` carries it. */
@@ -25,11 +28,14 @@ export type NativeSubagent = {
* form. It is absent when the adapter has no prompt.
*/
prompt?: string;
/** The RFD form: the parent's label for the child, `subagent_update.title`. */
/**
* The RFD form: the parent's label for the child, `subagent_update.title`:
* its name, else its task.
*/
title?: string;
/** The RFD form: the child's role, its `subagent_type`, for the title. */
/** The RFD form: the child's role, its `subagent_type`. */
role?: string;
/** The RFD form: the child's purpose, `subagent_update.description`. */
/** The RFD form: `subagent_update.description`, the child's role. */
description?: string;
/**
* The RFD form: the work state the client holds. The child is reusable, so
@@ -53,14 +59,23 @@ export type NativeSubagent = {
/** The RFD form: the calls that delegated to the child, whose runs handed back a report. */
handedBack?: Set<string>;
announced?: boolean;
/**
* The RFD form: the `cwd` that the client holds for the child. Undefined or
* null while the child inherits its parent's working directory.
*/
reportedCwd?: string | null;
terminalState?: SubagentState;
/** Connection-local single-flight state; never serialized on the wire. */
announcePromise?: Promise<void>;
/** Connection-local: serializes the child's `cwd` reports; never serialized on the wire. */
cwdPublishing?: Promise<void>;
/** Connection-local single-flight state; never serialized on the wire. */
terminalPromise?: Promise<void>;
};
export type NativeSubagentSession = {
/** The working directory of the root session, which a child inherits unless it reports its own. */
cwd?: string;
nativeSubagentsByTaskId?: Map<string, NativeSubagent>;
nativeSubagentTaskIdByToolUseId?: Map<string, string>;
nativeSubagentParentByToolUseId?: Map<string, string>;
@@ -116,7 +131,7 @@ type TaskStarted = {
prompt?: unknown;
};
type SubagentIdentity = {
export type SubagentIdentity = {
name?: string;
description?: string;
prompt?: string;
@@ -130,6 +145,8 @@ export type HistoricalSubagent = {
launchId: string;
/** The agent that made that call. None means the root session. */
parentAgentId?: string;
/** Its working directory, when Claude Code placed it elsewhere, e.g. in a worktree. */
cwd?: string;
name?: string;
agentType?: string;
description?: string;
@@ -169,6 +186,11 @@ export class NativeSubagentRuntime {
private readonly identityByToolUse = new Map<string, SubagentIdentity>();
private readonly controlByToolUse = new Map<string, AcpSessionNotification>();
private readonly configBySession = new Map<string, ChildConfiguration>();
/**
* The RFD form: the working directory of each child, by session id, as its
* hooks last reported it. A hook can come before the child is announced.
*/
private readonly cwdBySession = new Map<string, string>();
private readonly childByParentToolUse = new Map<string, NativeSubagent>();
/**
* The child session of each tool call that went to a child session. A later
@@ -367,14 +389,7 @@ export class NativeSubagentRuntime {
// so a delayed publication cannot replace newer settings with older ones.
const publishing = config.publishing.then(async () => {
if (this.configBySession.get(child.sessionId) !== config || ended()) return;
const priority = ["mode", "model", "thought_level", "model_config"];
const rank = (option: SessionConfigOption) => {
const index = priority.indexOf(option.category ?? "");
return index < 0 ? priority.length : index;
};
const configOptions = [...config.options.values()].sort(
(left, right) => rank(left) - rank(right),
);
const configOptions = orderedSubagentConfiguration(config.options.values());
const snapshot = JSON.stringify(configOptions);
if (config.published === snapshot) return;
await this.publish({
@@ -397,6 +412,83 @@ export class NativeSubagentRuntime {
await publishing;
}
/**
* The RFD form: takes the working directory of the child `agentId`, the
* `cwd` of one of its hooks. A child that Claude Code runs in its own
* worktree (Agent `isolation: "worktree"`) reports the worktree. The client
* gets the `cwd` only when it differs from the parent's, in the
* announcement or in a `subagent_update` right after it. Claude Code runs a
* child's SubagentStart hook before its first model request, so the `cwd`
* reaches the client before the child's first tool call.
*/
async reportCwd(agentId: string, cwd: string): Promise<void> {
if (this.form !== "rfd" || agentId === this.rootSessionId || !path.isAbsolute(cwd)) return;
const known = this.children.get(agentId) ?? this.childOfSession(agentId);
const sessionId = known?.sessionId ?? agentId;
if (!known && !this.cwdBySession.has(sessionId)) {
const pending = [...this.cwdBySession.keys()].filter((id) => !this.childOfSession(id)).length;
if (pending >= MAX_PENDING_PARENTS) {
this.logger.log(
`Session ${this.rootSessionId}: dropping cwd for unannounced subagent ${agentId}; pending buffer limit reached`,
);
return;
}
}
this.cwdBySession.set(sessionId, cwd);
if (!known) return;
// An announcement in flight may have been built without this `cwd`.
if (known.announcePromise) await known.announcePromise.catch(() => {});
if (known.announced) await this.publishCwd(known);
}
/**
* The `cwd` of `child` that the client does not hold yet: its working
* directory when that differs from its parent's, `null` when it is back in
* the parent's after it reported another, else undefined.
*/
private async unreportedCwd(child: NativeSubagent): Promise<string | null | undefined> {
const cwd = this.cwdBySession.get(child.sessionId);
if (cwd === undefined) return undefined;
const inherited = this.effectiveCwd(child.parentSessionId);
if (inherited !== undefined && (await sameDirectory(cwd, inherited))) {
return child.reportedCwd ? null : undefined;
}
return cwd === child.reportedCwd ? undefined : cwd;
}
/**
* The working directory of the session `sessionId` as the client holds it:
* the root's from session setup, a child's reported `cwd`, else its
* parent's.
*/
private effectiveCwd(sessionId: string): string | undefined {
const seen = new Set<string>();
let child = this.childOfSession(sessionId);
while (child && !seen.has(child.sessionId)) {
if (child.reportedCwd) return child.reportedCwd;
seen.add(child.sessionId);
child = this.childOfSession(child.parentSessionId);
}
return this.session.cwd;
}
/** The RFD form: sends the `cwd` of the announced `child` that the client does not hold. */
private async publishCwd(child: NativeSubagent): Promise<void> {
if (this.form !== "rfd") return;
// Concurrent hooks of one child report the same `cwd` once.
const publishing = (child.cwdPublishing ?? Promise.resolve()).then(async () => {
const cwd = await this.unreportedCwd(child);
if (cwd === undefined) return;
await this.publish({
sessionId: child.parentSessionId,
update: { sessionUpdate: "subagent_update", sessionId: child.sessionId, cwd },
});
child.reportedCwd = cwd;
});
child.cwdPublishing = publishing.catch(() => {});
await publishing;
}
private async routeUpdate(
notification: AcpSessionNotification,
deliver: Publish,
@@ -920,9 +1012,14 @@ export class NativeSubagentRuntime {
this.childByParentToolUse.set(entry.launchId, child);
}
if (child.announced) continue;
// A hook of the resumed agent may have reported a newer `cwd`.
if (entry.cwd && !this.cwdBySession.has(child.sessionId)) {
this.cwdBySession.set(child.sessionId, entry.cwd);
}
if (entry.agentId === agentId) {
await this.announce(child);
} else {
const cwd = await this.unreportedCwd(child);
await this.publish({
sessionId: child.parentSessionId,
update: {
@@ -930,8 +1027,10 @@ export class NativeSubagentRuntime {
sessionId: child.sessionId,
...(child.title ? { title: child.title } : {}),
...(child.description ? { description: child.description } : {}),
...(cwd ? { cwd } : {}),
},
});
if (cwd) child.reportedCwd = cwd;
child.announced = true;
}
for (const pending of this.takePending(entry.launchId)) await deliver(pending);
@@ -1100,6 +1199,7 @@ export class NativeSubagentRuntime {
private withdraw(taskId: string, child: NativeSubagent): void {
this.children.delete(taskId);
this.configBySession.delete(child.sessionId);
this.cwdBySession.delete(child.sessionId);
if (child.parentToolUseId) {
this.takePending(child.parentToolUseId);
this.cleanupControl(child.parentToolUseId);
@@ -1122,12 +1222,16 @@ export class NativeSubagentRuntime {
}
/**
* Announces `child` in the client's form, then its configuration. A runtime
* without a form never does.
* Announces `child` in the client's form, with its `cwd` in the RFD form,
* then its configuration. A runtime without a form never does.
*/
private async announce(child: NativeSubagent): Promise<void> {
if (!this.form) return;
await announceNativeSubagent(child, this.publish, this.form);
const cwd =
this.form === "rfd" && !child.announced ? await this.unreportedCwd(child) : undefined;
await announceNativeSubagent(child, this.publish, this.form, cwd ?? undefined);
// A hook that reported a `cwd` while the announcement was being built.
await this.publishCwd(child);
await this.publishConfiguration(child);
}
@@ -1393,6 +1497,7 @@ export class NativeSubagentRuntime {
this.identityByToolUse.clear();
this.controlByToolUse.clear();
this.configBySession.clear();
this.cwdBySession.clear();
this.childByParentToolUse.clear();
this.taskFinishPromises.clear();
this.senderByToolUse.clear();
@@ -1598,6 +1703,7 @@ export async function announceNativeSubagent(
child: NativeSubagent,
publish: Publish,
form: SubagentForm,
cwd?: string,
): Promise<void> {
if (child.announced) return;
if (child.announcePromise) return child.announcePromise;
@@ -1611,10 +1717,12 @@ export async function announceNativeSubagent(
sessionId: child.sessionId,
...(child.title ? { title: child.title } : {}),
...(child.description ? { description: child.description } : {}),
...(cwd ? { cwd } : {}),
capabilities: { cancel: {} },
state: { state: "running" },
},
});
if (cwd) child.reportedCwd = cwd;
child.announced = true;
// The immediate parent made the Agent/Task call that launched the child.
if (child.prompt) {
@@ -1840,7 +1948,7 @@ function isWorking(child: NativeSubagent): boolean {
* A failure is idle with the `error` stop reason and, when the SDK gives its
* text, the JSON-RPC `error`, as the RFD asks: a v1 child has no prompt
* response, so this is how a client learns that its work failed. The error is
* an internal error, as for a failed turn on the v2 surface.
* an internal error, as for a failed turn on v2.
*
* A task that a worker restart orphaned, or that was still running when the
* stream ended, is idle with no stop reason: its work ended, but the adapter
@@ -1868,26 +1976,22 @@ export function rfdEndState(state: SubagentState, end: TaskEnd | undefined): Wor
}
/**
* The RFD form: the parent's labels for a child. The title is the agent's
* name, with its role after it when both are known, else the role. The
* description is the task's short description, never the prompt.
* The RFD form: the parent's labels for a child. The title is what tells
* siblings apart: the agent's name when it has one, else the task's short
* description (never the prompt), else its role. The description is the
* role, its `subagent_type`, unless the role is already the title.
*/
export function rfdLabels(
name: unknown,
role: unknown,
description: unknown,
): { title?: string; role?: string; description?: string } {
const agentName = nonBlankString(name);
const agentRole = nonBlankString(role);
const title =
agentName && agentRole && agentName !== agentRole
? `${agentName} (${agentRole})`
: (agentName ?? agentRole);
const text = nonBlankString(description);
const title = nonBlankString(name) ?? nonBlankString(description) ?? agentRole;
return {
...(title ? { title } : {}),
...(agentRole ? { role: agentRole } : {}),
...(text ? { description: text } : {}),
...(agentRole && agentRole !== title ? { description: agentRole } : {}),
};
}
@@ -1999,7 +2103,7 @@ function isFailedToolCallUpdate(update: AcpSessionNotification["update"]): boole
* one), a SendMessage call whose message reached no announced child, or a
* failed SubagentHandback call.
*/
function unexposedControlFallback(
export function unexposedControlFallback(
initial: AcpSessionNotification | undefined,
terminal: AcpSessionNotification,
sessionId: string,
@@ -2093,7 +2197,8 @@ function subagentDescription(prompt: unknown, description: unknown): string {
return "Delegated task";
}
function subagentIdentity(input: unknown): SubagentIdentity | undefined {
/** The labels and prompt of a child, from the input of the Agent or Task call that launched it. */
export function subagentIdentity(input: unknown): SubagentIdentity | undefined {
if (typeof input !== "object" || input === null || Array.isArray(input)) return undefined;
const value = input as Record<string, unknown>;
const identity: SubagentIdentity = {
@@ -2105,7 +2210,7 @@ function subagentIdentity(input: unknown): SubagentIdentity | undefined {
return Object.values(identity).some(Boolean) ? identity : undefined;
}
function mergeSubagentIdentity(
export function mergeSubagentIdentity(
previous: SubagentIdentity | undefined,
next: SubagentIdentity,
): SubagentIdentity {
@@ -2156,3 +2261,15 @@ function promptField(prompt: string | undefined): { prompt?: string } {
function nonBlankString(value: unknown): string | undefined {
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
}
/**
* Whether `left` and `right` are the same directory, also when one is
* spelled through a symlink, e.g. `/tmp/project` and `/private/tmp/project`
* on macOS. A path that does not exist is compared as written.
*/
export async function sameDirectory(left: string, right: string): Promise<boolean> {
if (path.resolve(left) === path.resolve(right)) return true;
const real = (dir: string) => realpath(dir).catch(() => path.resolve(dir));
const [realLeft, realRight] = await Promise.all([real(left), real(right)]);
return realLeft === realRight;
}

41
src/new-session-meta.ts Normal file
View File

@@ -0,0 +1,41 @@
import type { Options, SDKMessageOrigin } from "@anthropic-ai/claude-agent-sdk";
export type SDKMessageFilter = {
type: string;
subtype?: string;
origin?: SDKMessageOrigin["kind"];
};
/**
* Extra metadata that can be given when creating a new session.
*/
export type NewSessionMeta = {
claudeCode?: {
/**
* Options forwarded to Claude Code when starting a new session.
* Those parameters will be ignored and managed by ACP:
* - cwd
* - includePartialMessages
* - permissionMode
* - canUseTool
* - executable
* Those parameters will be used and updated to work with ACP:
* - hooks (merged with ACP's hooks)
* - mcpServers (merged with ACP's mcpServers)
* - disallowedTools (merged with ACP's disallowedTools)
* - tools (passed through; defaults to claude_code preset if not provided)
* - allowDangerouslySkipPermissions (set to `false` to remove bypassPermissions
* from this session; repeat it on session/load. `true` cannot override root)
*/
options?: Options;
/**
* When set, raw SDK messages are emitted as extNotification("_claude/sdkMessage", message)
* in addition to normal processing.
* - true: emit all messages
* - false/undefined: emit nothing (default)
* - SDKMessageFilter[]: emit only messages matching at least one filter
*/
emitRawSDKMessages?: boolean | SDKMessageFilter[];
};
additionalRoots?: string[];
};

View File

@@ -1,4 +1,4 @@
import type { PermissionMode } from "@anthropic-ai/claude-agent-sdk";
import type { PermissionMode, Settings } from "@anthropic-ai/claude-agent-sdk";
interface PermissionModeLogger {
error: (...args: unknown[]) => void;
@@ -58,3 +58,10 @@ export function resolvePermissionMode(
return mapped;
}
/** Claude Code reads `disableAutoMode` both at the top level and under `permissions`. */
export function autoModeDisabled(settings: Settings): boolean {
return (
settings.disableAutoMode === "disable" || settings.permissions?.disableAutoMode === "disable"
);
}

View File

@@ -120,18 +120,64 @@ export function buildClaudePermissionPresentation(
}),
// Only AIR gets the permission presentation.
...(title && capabilities.air.client
? {
_meta: withAirMeta(undefined, AIR_PERMISSION_KEY, {
version: 1,
title,
...(description ? { description } : {}),
// The CLI's own hint, forwarded so a client that can pre-select
// an option keeps the decline focused; the option order already
// leads with the reject options when this is set.
...(value.defaultToNo === true ? { defaultToNo: true } : {}),
}),
}
? { _meta: airPermissionMeta({ title, description, defaultToNo: value.defaultToNo }) }
: {}),
...(capabilities.v2 ? { title, ...(description ? { description } : {}) } : {}),
};
}
/**
* The `_meta.jetbrains.air.permission` of a permission request, from the
* heading and explanation that {@link buildClaudePermissionPresentation}
* built. The v2 core adds it for an AIR client, whose tool calls it
* renders without the AIR tool call contract.
*/
export function airPermissionMeta({
title,
description,
defaultToNo,
}: {
title: string;
description?: string;
defaultToNo?: boolean;
}): Record<string, unknown> {
return withAirMeta(undefined, AIR_PERMISSION_KEY, {
version: 1,
title,
...(description ? { description } : {}),
// The CLI's own hint, forwarded so a client that can pre-select an
// option keeps the decline focused; the option order already leads with
// the reject options when this is set.
...(defaultToNo === true ? { defaultToNo: true } : {}),
});
}
/**
* The `_meta.claudeCode` of a permission request's tool call: which MCP
* server serves an `mcp__*` tool and where its definition came from
* (`mcpServer`, SDK 0.3.274+), so a client can key trust on `source`, and,
* for clients other than AIR, the tool name and the Agent or Task call of a
* subagent's request. Undefined when there is nothing to add.
*/
export function permissionToolCallMeta({
toolName,
parentToolUseId,
mcpServer,
airClient,
}: {
toolName: string;
parentToolUseId: string | undefined;
mcpServer: { name: string; source: string } | undefined;
airClient: boolean;
}): { claudeCode: Record<string, unknown> } | undefined {
if (!mcpServer && !(parentToolUseId && !airClient)) return undefined;
// The name is the config key as authored: untrusted text, so it rides
// `_meta` rather than the title.
return {
claudeCode: {
...(airClient ? {} : { toolName }),
...(parentToolUseId && !airClient ? { parentToolUseId } : {}),
...(mcpServer ? { mcpServer: { name: mcpServer.name, source: mcpServer.source } } : {}),
},
};
}

110
src/plans.ts Normal file
View File

@@ -0,0 +1,110 @@
/**
* The session updates that show the session's plans. A client with the `plan`
* capability (every v2 client, and a v1 client that asks for it) tracks plans
* by ID and gets `plan_update` and `plan_removed`; any other client gets the
* legacy `plan` update, which holds one ID-less plan per session.
*/
import type {
PlanEntry,
PlanItems,
PlanRemoved,
SessionNotification,
} from "@agentclientprotocol/sdk/unstable";
import { PLAN_MODE_PLAN_ID } from "./exit-plan.js";
type PlanRemovalUpdate = PlanRemoved & { sessionUpdate: "plan_removed" };
/** The ID of the session's checklist, from TodoWrite and the Task tools. Every
* update replaces it whole. An approved ExitPlanMode plan is a separate plan
* ({@link PLAN_MODE_PLAN_ID}), and so is a subagent's TodoWrite checklist in
* its parent's session ({@link subagentChecklistPlanId}). */
export const CHECKLIST_PLAN_ID = "plan";
/** The ID of the TodoWrite checklist of the subagent that the tool call
* `parentToolUseId` launched, when the checklist goes to its parent's session.
* Claude Code keeps a TodoWrite list per agent, so it is a separate plan from
* the main conversation's checklist. The Task tools share one task list
* between all agents of a session, so their checklist is always
* {@link CHECKLIST_PLAN_ID}. */
export function subagentChecklistPlanId(parentToolUseId: string): string {
return `${CHECKLIST_PLAN_ID}:${parentToolUseId}`;
}
/** The plan operation that shows the checklist `entries` as the plan `planId`:
* `plan_update`, or `plan_removed` for an empty checklist. */
export function checklistPlanOperation(
entries: PlanEntry[],
planId: string = CHECKLIST_PLAN_ID,
): { sessionUpdate: "plan_update"; plan: PlanItems & { type: "items" } } | PlanRemovalUpdate {
if (entries.length === 0) return { sessionUpdate: "plan_removed", planId };
return { sessionUpdate: "plan_update", plan: { type: "items", planId, entries } };
}
/** The update that shows the checklist `entries`: a plan operation (see
* {@link checklistPlanOperation}), else a legacy `plan`, which with no entries
* clears the client's plan. */
export function checklistPlanUpdate(
entries: PlanEntry[],
planOperations: boolean,
): SessionNotification["update"] {
return planOperations ? checklistPlanOperation(entries) : { sessionUpdate: "plan", entries };
}
/** The plan IDs of the subagent checklists that the client holds in a session
* (see {@link subagentChecklistPlanId}). */
export type SubagentChecklists = Set<string>;
/**
* The update that shows the TodoWrite checklist `entries` of the agent that
* made the call: the main conversation, or the subagent that the tool call
* `parentToolUseId` launched.
*
* A subagent's checklist gets its own plan ID, recorded in `shown` while the
* client holds it, when it goes to the parent's session. It does not with
* `childSessions` (native subagent sessions), where it goes to the subagent's
* own session, which holds no other checklist. A client without plan
* operations holds one plan per session, so there a subagent's checklist
* replaces the main one.
*/
export function todoWritePlanUpdate(
entries: PlanEntry[],
options: {
planOperations: boolean;
parentToolUseId?: string | null;
childSessions: boolean;
shown?: SubagentChecklists;
},
): SessionNotification["update"] {
const { planOperations, parentToolUseId, childSessions, shown } = options;
if (!planOperations || !parentToolUseId || childSessions) {
return checklistPlanUpdate(entries, planOperations);
}
const planId = subagentChecklistPlanId(parentToolUseId);
if (entries.length === 0) shown?.delete(planId);
else shown?.add(planId);
return checklistPlanOperation(entries, planId);
}
/** The updates that dismiss the plans of a conversation that a reset (`/clear`)
* replaced: the checklist, the subagent checklists in `subagentChecklists`
* (which it then empties), and the approved ExitPlanMode plan. AIR keeps its
* approved plans, each joined to its approval card in the transcript (see
* planModeUpdate). Without plan operations, a legacy empty `plan` clears the
* one plan the client holds. */
export function resetPlanUpdates(
planOperations: boolean,
airClient: boolean,
subagentChecklists?: SubagentChecklists,
): SessionNotification["update"][] {
const updates = [checklistPlanUpdate([], planOperations)];
if (planOperations) {
for (const planId of subagentChecklists ?? []) {
updates.push({ sessionUpdate: "plan_removed", planId });
}
}
subagentChecklists?.clear();
if (planOperations && !airClient) {
updates.push({ sessionUpdate: "plan_removed", planId: PLAN_MODE_PLAN_ID });
}
return updates;
}

View File

@@ -42,3 +42,51 @@ export function promptSuggestionsMeta(text: string | null): Record<string, unkno
const value: PromptSuggestionsMetaValue = { suggestions: text === null ? [] : [{ text }] };
return { [PROMPT_SUGGESTIONS_META_KEY]: value };
}
/**
* The prediction a session holds on the client, and whether an arriving
* `prompt_suggestion` still answers the turn the user waits on. Both cores
* keep one per session and send the `_meta` that its methods return as a
* `session_info_update`, to an AIR client only. Undefined: nothing to send.
*/
export class PromptSuggestions {
/** The last text sent (`null` after a clear). Undefined until the first send. */
private published: string | null | undefined;
/**
* Set at a turn's result, cleared when the next prompt arrives. A
* prediction that the next prompt outran (sent before the prediction was
* read off the stream) follows a turn that is answered already, so it is
* dropped instead of published mid-turn.
*/
private expected = false;
/** A result came: the CLI predicts the next prompt right after it. */
resultArrived(): void {
this.expected = true;
}
/** The `_meta` of the CLI's `prompt_suggestion`, if it answers the last result and changes what the client holds. */
suggested(raw: string | null | undefined): Record<string, unknown> | undefined {
if (!this.expected) return undefined;
this.expected = false;
return this.publish(promptSuggestionText(raw));
}
/**
* The `_meta` that clears the prediction: a client prompt spent it, or a
* `conversation_reset` dropped the transcript it came from. It also drops
* a prediction still in flight for the last result. Undefined when the
* client holds none.
*/
clear(): Record<string, unknown> | undefined {
this.expected = false;
return this.publish(null);
}
private publish(text: string | null): Record<string, unknown> | undefined {
// An unchanged prediction is not sent again.
if ((this.published ?? null) === text) return undefined;
this.published = text;
return promptSuggestionsMeta(text);
}
}

129
src/prompt-to-claude.ts Normal file
View File

@@ -0,0 +1,129 @@
/**
* The conversion of an ACP prompt into the Claude Code user message.
*/
import type { PromptRequest } from "@agentclientprotocol/sdk/unstable";
import type { SDKUserMessage } from "@anthropic-ai/claude-agent-sdk";
import { blobResourceToClaude } from "./blob-resources.js";
function formatUriAsLink(uri: string): string {
try {
if (uri.startsWith("file://")) {
const path = uri.slice(7); // Remove "file://"
const name = path.split("/").pop() || path;
return `[@${name}](${uri})`;
} else if (uri.startsWith("zed://")) {
const parts = uri.split("/");
const name = parts[parts.length - 1] || uri;
return `[@${name}](${uri})`;
}
return uri;
} catch {
return uri;
}
}
/** The prompt content block types that `promptToClaude` converts. The agent
* advertises no `audio` prompt capability. */
export const SUPPORTED_PROMPT_CONTENT_TYPES: readonly string[] = [
"text",
"image",
"resource_link",
"resource",
];
export function promptToClaude(prompt: PromptRequest): SDKUserMessage {
const content: any[] = [];
const context: any[] = [];
const firstText = prompt.prompt.find((chunk) => chunk.type === "text");
for (const chunk of prompt.prompt) {
switch (chunk.type) {
case "text": {
let text = chunk.text;
// A slash command is the first text block. Change
// `/mcp:server:command args` to `/server:command (MCP) args`; the
// arguments can span lines.
const mcpMatch =
chunk === firstText ? text.match(/^\/mcp:([^:\s]+):(\S+)(?:\s+([\s\S]*))?$/) : null;
if (mcpMatch) {
const [, server, command, args] = mcpMatch;
text = `/${server}:${command} (MCP)${args ? ` ${args}` : ""}`;
}
content.push({ type: "text", text });
break;
}
case "resource_link": {
const formattedUri = formatUriAsLink(chunk.uri);
content.push({
type: "text",
text: formattedUri,
});
break;
}
case "resource": {
const { uri } = chunk.resource;
// A blob resource becomes an image or document block, or its decoded
// text. A v1 prompt skips a blob of any other type, e.g.
// `application/zip`; a v2 prompt rejects it (see `v1PromptRequest`).
const blob = "blob" in chunk.resource ? blobResourceToClaude(chunk.resource) : undefined;
const text =
"text" in chunk.resource
? chunk.resource.text
: blob?.kind === "text"
? blob.text
: undefined;
if (blob?.kind === "image" || blob?.kind === "document") {
content.push({ type: "text", text: formatUriAsLink(uri) }, blob.block);
} else if (text !== undefined) {
content.push({ type: "text", text: formatUriAsLink(uri) });
context.push({
type: "text",
text: `\n<context ref="${uri}">\n${text}\n</context>`,
});
}
break;
}
case "image":
if (chunk.data) {
content.push({
type: "image",
source: {
type: "base64",
data: chunk.data,
media_type: chunk.mimeType,
},
});
} else if (chunk.uri && chunk.uri.startsWith("http")) {
content.push({
type: "image",
source: {
type: "url",
url: chunk.uri,
},
});
}
break;
// Ignore audio and other unsupported types
default:
break;
}
}
content.push(...context);
return {
type: "user",
message: {
role: "user",
content: content,
},
session_id: prompt.sessionId,
parent_tool_use_id: null,
// ACP prompts are the user's own input relayed by the client. Stamp the
// provenance explicitly: per the SDK, a host wrapping keyboard input must
// send `{kind: "human"}` — an absent `origin` is treated as unattributed
// and fails closed at the CLI's strict isHuman() trust gates (e.g. the
// ultracode keyword opt-in honors only human-originated turns).
origin: { kind: "human" },
};
}

421
src/providers.ts Normal file
View File

@@ -0,0 +1,421 @@
/**
* Client-managed LLM routing, shared by the v1 core and the v2 core:
* the `main` provider of `providers/list`, `providers/set` and
* `providers/disable`, and the legacy gateway logins (`authenticate` or
* `auth/login` with `gateway` or `gateway-bedrock`). Each core holds its own
* {@link ProviderConfig} and gateway login, and builds the query env of its
* sessions with {@link createEnvForProvider}.
*/
import {
RequestError,
type AuthenticateRequest,
type AuthMethod,
type ListProvidersResponse,
type LlmProtocol,
type ProviderInfo,
type SetProviderRequest,
} from "@agentclientprotocol/sdk/unstable";
import {
GatewayListingError,
listBedrockModels,
listGatewayModels,
PROVIDER_AUTH_TOKEN_PLACEHOLDER,
selectBedrockRegion,
} from "./gateway-models.js";
type ProviderLogger = { log: (...args: any[]) => void; error: (...args: any[]) => void };
/**
* Extra metadata for 'gateway' authentication requests.
*/
export type GatewayAuthMeta = {
/**
* These parameters are mapped to environment variables to:
* - Redirect API calls via baseUrl
* - Inject custom headers
* - Bypass the default Claude login requirement
*
* Both members are optional in the type because the payload arrives
* unvalidated from the client. `authenticate` rejects a request that lacks a
* usable `baseUrl`.
*/
gateway?: {
baseUrl?: string;
headers?: Record<string, string>;
};
};
export type GatewayAuthRequest = AuthenticateRequest & { _meta?: GatewayAuthMeta };
export const SUPPORTED_PROTOCOLS: LlmProtocol[] = ["anthropic", "bedrock", "vertex"];
export const PROVIDER_ID = "main";
export const DEFAULT_ANTHROPIC_BASE_URL = "https://api.anthropic.com";
export const DEFAULT_VERTEX_BASE_URL = "https://aiplatform.googleapis.com";
/**
* Vertex needs project + region that the standard `providers/set` payload
* (`apiType`/`baseUrl`/`headers`) does not model, so clients pass them through
* `_meta.claudeCode.vertex`. Required only when `apiType === "vertex"`.
*/
export type SetProviderMeta = {
claudeCode?: {
vertex?: {
projectId: string;
region: string;
};
};
};
/**
* Resolved, non-secret + secret routing config for the `main` provider. This is
* the shared shape produced by both `providers/set` and the legacy gateway auth
* path, and consumed by {@link createEnvForProvider}. `null` means the provider
* is unconfigured (no client-managed routing in effect).
*/
export type ProviderConfig = {
apiType: LlmProtocol;
baseUrl: string;
headers: Record<string, string>;
/** Present only for `apiType === "vertex"`. */
vertex?: {
projectId: string;
region: string;
};
/**
* Model ids an `anthropic` or `bedrock` gateway listed when it was set via
* `providers/set`; the model picker offers only these. Absent when the
* listing failed or the config came from elsewhere (gateway `authenticate`),
* so nothing is narrowed.
*/
listedModels?: string[];
/**
* The cross-region prefix of the model ids a `bedrock` gateway
* listed (`eu` for `eu.anthropic.claude-sonnet-5`), passed to the CLI as
* `ANTHROPIC_BEDROCK_REGION_PREFIX` so it calls the models under that prefix.
*/
bedrockRegionPrefix?: string;
};
/**
* Adapt a legacy gateway `authenticate` request into the shared
* {@link ProviderConfig} shape. Returns `null` when no gateway request is
* present. `methodId` selects the protocol: `gateway-bedrock` → bedrock,
* otherwise anthropic.
*/
export function gatewayRequestToProviderConfig(
request?: GatewayAuthRequest,
): ProviderConfig | null {
// `authenticate` validates the payload before it stores one, so a stored
// request always carries a usable gateway. Re-check the shape here anyway:
// this function decides whether a provider override is active, and the
// `--hide-claude-auth` guard is off while one is.
const gateway = request?._meta?.gateway;
if (!gateway || !isValidBaseUrl(gateway.baseUrl)) {
return null;
}
return {
apiType: request?.methodId === "gateway-bedrock" ? "bedrock" : "anthropic",
baseUrl: gateway.baseUrl,
headers: gateway.headers ?? {},
};
}
/**
* Map a resolved provider config into the Claude Code env vars that redirect API
* traffic and inject headers. Returns an empty object when routing is
* unconfigured. The token/bypass placeholders (`" "`) are required so the CLI
* skips its normal login/credential checks when a gateway is in use.
*/
export function createEnvForProvider(config: ProviderConfig | null): Record<string, string> {
if (!config) {
return {};
}
const resetRouting = {
ANTHROPIC_BASE_URL: "",
ANTHROPIC_BEDROCK_BASE_URL: "",
ANTHROPIC_VERTEX_BASE_URL: "",
CLAUDE_CODE_USE_BEDROCK: "0",
CLAUDE_CODE_USE_VERTEX: "0",
ANTHROPIC_VERTEX_PROJECT_ID: "",
CLOUD_ML_REGION: "",
AWS_REGION: "",
ANTHROPIC_API_KEY: "",
ANTHROPIC_AUTH_TOKEN: "",
CLAUDE_CODE_OAUTH_TOKEN: "",
};
const customHeaders = Object.entries(config.headers)
.map(([key, value]) => `${key}: ${value}`)
.join("\n");
if (config.apiType === "bedrock") {
return {
...resetRouting,
CLAUDE_CODE_USE_BEDROCK: "1",
AWS_BEARER_TOKEN_BEDROCK: PROVIDER_AUTH_TOKEN_PLACEHOLDER, // Bypass local AWS credential checks
ANTHROPIC_BEDROCK_BASE_URL: config.baseUrl,
ANTHROPIC_CUSTOM_HEADERS: customHeaders,
...(config.bedrockRegionPrefix
? { ANTHROPIC_BEDROCK_REGION_PREFIX: config.bedrockRegionPrefix }
: {}),
};
}
if (config.apiType === "vertex") {
// `config.vertex` is guaranteed present for vertex by `unstable_setProvider`
// validation; fall back to empty strings defensively.
// Leaving ANTHROPIC_VERTEX_BASE_URL empty for the default endpoint lets
// Claude Code use its native Vertex endpoint/model resolution. Supplying
// even the default host marks the session as a custom Vertex deployment and
// can hide otherwise-valid models.
return {
...resetRouting,
CLAUDE_CODE_USE_VERTEX: "1",
...(config.baseUrl !== DEFAULT_VERTEX_BASE_URL
? { ANTHROPIC_VERTEX_BASE_URL: config.baseUrl }
: {}),
ANTHROPIC_VERTEX_PROJECT_ID: config.vertex?.projectId ?? "",
CLOUD_ML_REGION: config.vertex?.region ?? "",
ANTHROPIC_CUSTOM_HEADERS: customHeaders,
};
}
return {
...resetRouting,
ANTHROPIC_BASE_URL: config.baseUrl,
ANTHROPIC_CUSTOM_HEADERS: customHeaders,
ANTHROPIC_AUTH_TOKEN: PROVIDER_AUTH_TOKEN_PLACEHOLDER, // Bypass local Claude login checks
};
}
function sameHeaders(a: Record<string, string>, b: Record<string, string>): boolean {
const aKeys = Object.keys(a);
return aKeys.length === Object.keys(b).length && aKeys.every((key) => b[key] === a[key]);
}
/**
* A provider base URL as logs may show it: its origin only. The URL can come
* from the env or the client and can carry credentials (`https://user:key@host`)
* or a key in its path or query, so a log line must not print it whole.
*/
export function loggableBaseUrl(baseUrl: string | undefined): string {
if (baseUrl === undefined) return "native";
try {
return new URL(baseUrl).origin;
} catch {
return "invalid";
}
}
/**
* Validate a provider base URL: must be a non-empty absolute http(s) URL.
*/
export function isValidBaseUrl(baseUrl: string | undefined): baseUrl is string {
if (typeof baseUrl !== "string" || baseUrl.trim() === "") {
return false;
}
let parsed: URL;
try {
parsed = new URL(baseUrl);
} catch {
return false;
}
return parsed.protocol === "http:" || parsed.protocol === "https:";
}
/** The gateway login methods, advertised to a client with `auth._meta.gateway`. */
export function gatewayAuthMethods(): AuthMethod[] {
return (["anthropic", "bedrock"] as const).map((protocol) => ({
id: protocol === "anthropic" ? "gateway" : "gateway-bedrock",
name: "Custom model gateway",
description: "Use a custom gateway to authenticate and access models",
_meta: { gateway: { protocol } },
}));
}
export function isGatewayAuthMethod(methodId: string): boolean {
return methodId === "gateway" || methodId === "gateway-bedrock";
}
/**
* The gateway login `params` asks for, once its payload is usable: the same
* base URL rule as `providers/set`. An unchecked payload either throws a
* `TypeError` deep in session creation, or installs an empty base URL that
* silently turns the `--hide-claude-auth` subscription guard off.
*
* A request that carries no gateway payload at all keeps its historical
* meaning: it installs no override and succeeds. A client that probes the
* method this way must keep working. Only a payload that IS present has to be
* usable.
*/
export function validatedGatewayAuthRequest(params: AuthenticateRequest): GatewayAuthRequest {
const gateway = (params as GatewayAuthRequest)._meta?.gateway;
if (gateway !== undefined && gateway !== null) {
if (typeof gateway !== "object" || !isValidBaseUrl(gateway.baseUrl)) {
throw RequestError.invalidParams(
{ baseUrl: (gateway as { baseUrl?: unknown }).baseUrl },
"`_meta.gateway.baseUrl` must be a non-empty absolute http(s) URL.",
);
}
if (gateway.headers !== undefined && typeof gateway.headers !== "object") {
throw RequestError.invalidParams(
undefined,
"`_meta.gateway.headers` must be an object of header names to values.",
);
}
}
return params as GatewayAuthRequest;
}
/** The native routing of the agent: a gateway login, else the process env. */
export function defaultProviderConfig(gatewayAuthRequest?: GatewayAuthRequest): ProviderConfig {
const gatewayConfig = gatewayRequestToProviderConfig(gatewayAuthRequest);
if (gatewayConfig) {
return gatewayConfig;
}
if (process.env.CLAUDE_CODE_USE_BEDROCK) {
return {
apiType: "bedrock",
baseUrl: process.env.ANTHROPIC_BEDROCK_BASE_URL ?? "https://bedrock-runtime.amazonaws.com",
headers: {},
};
}
if (process.env.CLAUDE_CODE_USE_VERTEX) {
return {
apiType: "vertex",
baseUrl: process.env.ANTHROPIC_VERTEX_BASE_URL ?? DEFAULT_VERTEX_BASE_URL,
headers: {},
};
}
return {
apiType: "anthropic",
baseUrl: process.env.ANTHROPIC_BASE_URL ?? DEFAULT_ANTHROPIC_BASE_URL,
headers: {},
};
}
/** The `providers/list` response for the `main` provider routed by `config`. */
export function listProvidersResponse(config: ProviderConfig): ListProvidersResponse {
const provider: ProviderInfo = {
providerId: PROVIDER_ID,
supported: SUPPORTED_PROTOCOLS,
required: false,
current: { apiType: config.apiType, baseUrl: config.baseUrl },
};
return { providers: [provider] };
}
/**
* The configuration a `providers/set` request asks for. Rejects unknown IDs,
* unsupported protocols, empty or invalid base URLs, and a `vertex` request
* without its project and region, with `invalid_params`.
*/
export function providerConfigFromSetRequest(params: SetProviderRequest): ProviderConfig {
if (params.providerId !== PROVIDER_ID) {
throw RequestError.invalidParams(
{ providerId: params.providerId },
`Unknown provider ID "${params.providerId}"; expected "${PROVIDER_ID}".`,
);
}
if (!SUPPORTED_PROTOCOLS.includes(params.apiType)) {
throw RequestError.invalidParams(
{ apiType: params.apiType, supported: SUPPORTED_PROTOCOLS },
`Unsupported apiType "${params.apiType}" for provider "${PROVIDER_ID}".`,
);
}
if (!isValidBaseUrl(params.baseUrl)) {
throw RequestError.invalidParams(
{ baseUrl: params.baseUrl },
"baseUrl must be a non-empty absolute http(s) URL.",
);
}
const config: ProviderConfig = {
apiType: params.apiType,
baseUrl: params.baseUrl,
headers: params.headers ?? {},
};
// Vertex requires project + region, which the standard payload cannot
// carry, so they arrive via `_meta.claudeCode.vertex`.
if (params.apiType === "vertex") {
const vertex = (params._meta as SetProviderMeta | undefined)?.claudeCode?.vertex;
if (
!vertex ||
typeof vertex.projectId !== "string" ||
vertex.projectId.trim() === "" ||
typeof vertex.region !== "string" ||
vertex.region.trim() === ""
) {
throw RequestError.invalidParams(
undefined,
"vertex apiType requires non-empty `_meta.claudeCode.vertex.projectId` and `_meta.claudeCode.vertex.region`.",
);
}
config.vertex = { projectId: vertex.projectId, region: vertex.region };
}
return config;
}
/**
* Adds to `config` the model ids its gateway lists, and for `bedrock` the
* region prefix picked from them, so the sessions it routes offer only those.
* Setting the same gateway as `current` again reuses its listing. `vertex`
* gateways are not listed. A failed listing throws an `internal_error`.
*/
export async function withListedModels(
config: ProviderConfig,
current: ProviderConfig | undefined,
logger: ProviderLogger,
): Promise<ProviderConfig> {
try {
Object.assign(config, await listProviderModels(config, current, logger));
} catch (error) {
if (error instanceof GatewayListingError) {
throw RequestError.internalError(
{ providerId: PROVIDER_ID, apiType: config.apiType, baseUrl: config.baseUrl },
`Model discovery for provider "${PROVIDER_ID}" failed: ${error.message}`,
);
}
throw error;
}
logger.log(
`[providers/set] listedModels=${config.listedModels?.length ?? "none"} bedrockRegionPrefix=${config.bedrockRegionPrefix ?? "none"}`,
);
return config;
}
async function listProviderModels(
config: ProviderConfig,
current: ProviderConfig | undefined,
logger: ProviderLogger,
): Promise<Pick<ProviderConfig, "listedModels" | "bedrockRegionPrefix">> {
// Reuse needs the very same headers, so a rotated token lists again, and a
// failed listing then fails that `providers/set`. Deliberate: AIR keeps its
// proxy key for the IDE's lifetime and does not rotate it.
if (
current?.listedModels &&
current.apiType === config.apiType &&
current.baseUrl === config.baseUrl &&
sameHeaders(current.headers, config.headers)
) {
return {
listedModels: current.listedModels,
bedrockRegionPrefix: current.bedrockRegionPrefix,
};
}
if (config.apiType === "anthropic") {
return { listedModels: await listGatewayModels(config.baseUrl, config.headers, logger) };
}
if (config.apiType === "bedrock") {
const modelIds = await listBedrockModels(config.baseUrl, config.headers, logger);
if (!modelIds) return {};
const region = selectBedrockRegion(modelIds);
if (region.modelIds.length < modelIds.length) {
logger.log(
`[providers/set] Bedrock models span several region prefixes; using ${region.regionPrefix} and dropping ${modelIds.length - region.modelIds.length} id(s) of other prefixes`,
);
}
return { listedModels: region.modelIds, bedrockRegionPrefix: region.regionPrefix };
}
return {};
}

129
src/remote-control.ts Normal file
View File

@@ -0,0 +1,129 @@
/**
* `/remote-control`, which the adapter serves itself, and the messages that a
* person types in another client of the session (claude.ai or the Claude
* mobile app, over Remote Control). Shared by the v1 core and the v2
* core, which run the command each in their own turn model.
*/
/** The content blocks of a prompt, in v1 or v2 form: the command is one text block. */
type PromptContent = ReadonlyArray<{ type: string; text?: unknown }>;
import type {
Query,
SDKUserMessage,
SDKUserMessageReplay,
SlashCommand,
} from "@anthropic-ai/claude-agent-sdk";
/** Runtime Remote Control control exposed by the pinned Agent SDK. Like
* `mcpAuthenticate`, it is not in the public `Query` declaration yet. The CLI's
* own `/remote-control` is terminal-bound and not offered to SDK hosts, so the
* adapter handles the command itself and answers with the session URL. */
export type RemoteControlQuery = Query & {
enableRemoteControl(
enabled: boolean,
name?: string,
): Promise<{ session_url?: string; connect_url?: string }>;
};
export function supportsRemoteControl(query: Query): query is RemoteControlQuery {
return typeof (query as Partial<RemoteControlQuery>).enableRemoteControl === "function";
}
export const REMOTE_CONTROL_COMMAND_NAME = "remote-control";
export type RemoteControlCommand = { action: "connect"; name?: string } | { action: "disconnect" };
/** Parses `/remote-control [name]` and `/remote-control off` from a single-text
* prompt. Connecting is idempotent, so the command never needs to know whether
* Remote Control is already on. */
export function parseRemoteControlCommand(prompt: PromptContent): RemoteControlCommand | null {
const [first] = prompt;
if (prompt.length !== 1 || first?.type !== "text" || typeof first.text !== "string") return null;
const match = /^\/remote-control(?:\s+([\s\S]*))?$/.exec(first.text.trim());
if (!match) return null;
const argument = match[1]?.trim();
if (argument?.toLowerCase() === "off") return { action: "disconnect" };
return argument ? { action: "connect", name: argument } : { action: "connect" };
}
/** Whether Claude Code offers a slash command of this name to an ACP client.
* This is the rule `getAvailableSlashCommands` applies when it lists the
* commands, so the adapter stands in for the command exactly when the client
* sees the adapter's entry: Claude Code's terminal-bound commands are not
* offered, and a user's own command or skill of the name is never shadowed. */
export function claudeCodeOffersCommand(
commands: readonly SlashCommand[],
terminalCommands: readonly string[] | undefined,
name: string,
): boolean {
return commands.some(
(command) => command.name === name && !terminalCommands?.includes(command.name),
);
}
/**
* The `/remote-control` command of `prompt` that the adapter runs: the query
* serves Remote Control and Claude Code does not offer a command of that name.
* `null` for any other prompt, which goes to Claude Code.
*/
export async function adapterRemoteControlCommand(
prompt: PromptContent,
query: Query,
terminalCommands: readonly string[] | undefined,
): Promise<{ query: RemoteControlQuery; command: RemoteControlCommand } | null> {
const command = parseRemoteControlCommand(prompt);
if (!command || !supportsRemoteControl(query)) return null;
if (
claudeCodeOffersCommand(
await query.supportedCommands(),
terminalCommands,
REMOTE_CONTROL_COMMAND_NAME,
)
) {
return null;
}
return { query, command };
}
/**
* Connects Remote Control (again, if it is already on) or disconnects it, and
* returns the text that tells the user how it went. A failure is in the text:
* this never throws.
*/
export async function runRemoteControlCommand(
query: RemoteControlQuery,
command: RemoteControlCommand,
logError: (message: string) => void,
): Promise<string> {
if (command.action === "disconnect") {
try {
await query.enableRemoteControl(false);
return "Remote Control is off. Run /remote-control to reconnect.";
} catch (error) {
logError(`Failed to disable Remote Control: ${error}`);
return `Could not stop Remote Control: ${error instanceof Error ? error.message : error}`;
}
}
try {
const result = await query.enableRemoteControl(true, command.name);
return result.session_url
? `Remote Control is active. Continue this session from claude.ai/code or the Claude mobile app:\n\n${result.session_url}\n\nRun /remote-control off to disconnect.`
: "Remote Control is active, but Claude Code did not return a session URL.";
} catch (error) {
logError(`Failed to enable Remote Control: ${error}`);
return `Could not start Remote Control: ${error instanceof Error ? error.message : error}`;
}
}
/** A top-level message a person typed in another client of the session, such
* as claude.ai or the Claude mobile app over Remote Control. Tool results and
* the CLI's own synthetic messages are user-role too, but carry no human
* origin. */
export function isRemoteHumanMessage(message: SDKUserMessage | SDKUserMessageReplay): boolean {
return (
message.type === "user" &&
message.origin?.kind === "human" &&
message.parent_tool_use_id === null &&
!message.isSynthetic
);
}

105
src/result-end.ts Normal file
View File

@@ -0,0 +1,105 @@
/**
* How Claude Code's `result` ends the work that it reports, and whether that
* work was the user's turn or a cycle Claude Code started on its own: the
* rules that both cores read a turn's result with (the v1 prompt loop in
* `v1/acp-agent.ts` and `v2/`). Each core adds its own side effects, such as v1's
* session failure category.
*/
import type {
SDKMessage,
SDKMessageOrigin,
SDKResultMessage,
} from "@anthropic-ai/claude-agent-sdk";
export type ResultEnd =
| { stopReason: "refusal" | "max_tokens" | "end_turn" | "max_turn_requests" }
/**
* The result failed the work, with Claude Code's error text. `loginRequired`:
* the text of a `success` result tells the user to run `/login`.
*/
| { error: string; loginRequired: boolean };
/**
* True when CLI error text tells the user to sign in with `/login`. The CLI
* words this several ways — "Not logged in · Please run /login", "Not logged
* in · Run /login", "Login expired · Run /login to sign in again, or
* re-authenticate your Anthropic profile" — so match the "run /login"
* instruction rather than one sentence.
*/
export function isLoginRequiredText(text: string): boolean {
return /\brun \/login\b/i.test(text);
}
/**
* - Any subtype with `stop_reason: "refusal"`: `refusal`, also on an error.
* - `success`: an error with the `/login` text fails the work, then
* `stop_reason: "max_tokens"` is `max_tokens`, then any other error fails
* the work. Otherwise `end_turn`.
* - `error_during_execution`: `max_tokens` as for `success`, then an error
* fails the work. Otherwise `end_turn`.
* - `error_max_turns`, `error_max_budget_usd` and
* `error_max_structured_output_retries`: an error fails the work, whatever
* the stop reason. Otherwise `max_turn_requests`.
*/
export function resultEnd(result: SDKResultMessage): ResultEnd {
if (result.stop_reason === "refusal") return { stopReason: "refusal" };
switch (result.subtype) {
case "success":
if (result.is_error && isLoginRequiredText(result.result)) {
return { error: result.result, loginRequired: true };
}
if (result.stop_reason === "max_tokens") return { stopReason: "max_tokens" };
if (result.is_error) return { error: result.result, loginRequired: false };
return { stopReason: "end_turn" };
case "error_during_execution":
if (result.stop_reason === "max_tokens") return { stopReason: "max_tokens" };
if (result.is_error) return failed(result);
return { stopReason: "end_turn" };
default:
// `error_max_turns`, `error_max_budget_usd`, `error_max_structured_output_retries`.
if (result.is_error) return failed(result);
return { stopReason: "max_turn_requests" };
}
}
function failed(result: Exclude<SDKResultMessage, { subtype: "success" }>): ResultEnd {
return { error: result.errors.join(", ") || result.subtype, loginRequired: false };
}
/** Result-message origin kinds that mark an AUTONOMOUS cycle — work the
* model did on its own (a task-notification followup, a peer/coordinator/
* observer message it handled) rather than the user's prompt. Absent,
* `human`, and `channel` origins are the user's own turn (this adapter's
* prompts arrive as the ACP channel on some CLI configurations — ALL
* channel servers are treated as user, so a foreign channel integration's
* autonomously-handled result is misclassified as the user's; accepted,
* see below), and `auto-continuation` continues the user's turn, so its
* result is the turn's real terminal.
*
* Deliberately fail-OPEN: an unknown future kind defaults to the user
* lane — including `unclassified` (SDK 0.3.232+), the CLI's own "couldn't
* attribute this" marker, which gets the same safe default. Misrouting a
* USER result into the autonomous lane hangs the prompt un-detectably
* (the result is skipped, its trailing idle absorbed as owed, so the
* #825 detector can't fire); misrouting an autonomous result into the
* user lane is the bounded misattribution class this set exists to
* reduce.
*
* Exception: a result from one of these origins that names a pending prompt
* in `user_message_uuids` consumed that folded prompt and takes the user
* lane (see the result handler of each core). */
export const AUTONOMOUS_RESULT_ORIGINS: ReadonlySet<SDKMessageOrigin["kind"]> = new Set([
"task-notification",
"peer",
"coordinator",
"observer",
"observer-activity",
]);
/** Whether Claude Code ended the work of `result` because an interrupt
* stopped it, while the model streamed or while a tool ran. */
export function isAbortedResult(result: Extract<SDKMessage, { type: "result" }>): boolean {
return (
result.terminal_reason === "aborted_streaming" || result.terminal_reason === "aborted_tools"
);
}

853
src/sdk-to-acp.ts Normal file
View File

@@ -0,0 +1,853 @@
/**
* The mapping of Claude Code output to ACP session updates: content blocks
* (`toAcpNotifications`) and partial stream events
* (`streamEventToAcpNotifications`) become message chunks, thoughts, tool
* calls and plans. The live stream and the `session/load` replay of both
* protocol versions use it; v2 converts the v1 updates it returns.
*/
import type { ClientCapabilities, SessionNotification } from "@agentclientprotocol/sdk/unstable";
import type { SDKPartialAssistantMessage } from "@anthropic-ai/claude-agent-sdk";
import type { ContentBlockParam } from "@anthropic-ai/sdk/resources";
import type {
BetaContentBlock,
BetaRawContentBlockDelta,
} from "@anthropic-ai/sdk/resources/beta.mjs";
import type { AcpClient } from "./acp-client.js";
import { clientSupportsSubagents } from "./acp-subagents.js";
import { approvedPlan, planModeUpdate } from "./exit-plan.js";
import type { Logger } from "./logger.js";
import { checklistPlanUpdate, todoWritePlanUpdate, type SubagentChecklists } from "./plans.js";
import { ClientCapabilities as ToolCallClientCapabilities } from "./tool-calls/client-capabilities.js";
import type { ToolCallFieldTracker } from "./tool-calls/field-tracker.js";
import { AcpToolCallRenderer, type ToolUpdateMeta } from "./tool-calls/renderer.js";
import { parseToolResultMeta } from "./tool-result-meta.js";
import {
applyTaskCreate,
applyTaskList,
applyTaskUpdate,
changedTaskPlanEntries,
type ClaudePlanEntry,
completeHookCallback,
hasHookCallback,
parseTaskCreateOutput,
parseTaskListOutput,
parseTaskUpdateOutput,
planEntries,
registerHookCallback,
type TaskState,
unregisterHookCallback,
} from "./tools.js";
import { unreachable } from "./utils.js";
/** Attributes a report to the Agent/Task tool call of the subagent that made it. */
export function stampParentToolUseId(
update: SessionNotification["update"],
parentToolUseId: string,
) {
update._meta = {
...update._meta,
claudeCode: {
...((update._meta?.claudeCode as Record<string, unknown> | undefined) ?? {}),
parentToolUseId,
},
};
}
export type ToolUseCache = {
[key: string]: {
type: "tool_use" | "server_tool_use" | "mcp_tool_use";
id: string;
name: string;
input: unknown;
};
};
type StreamedToolInput = {
id: string;
name: string;
partialJson: string;
inString: boolean;
escaped: boolean;
objectDepth: number;
arrayDepth: number;
/** Offset of the most recent comma at the top level of the input object
* (-1 before the first). Everything before it is a complete field. */
lastTopLevelComma: number;
/** The comma offset the last emitted refinement was sliced at (-1 before the
* first), so a field boundary only triggers one recovery attempt. */
emittedThroughComma: number;
};
export type StreamedToolInputCache = Map<string, Map<number, StreamedToolInput>>;
/**
* Advance the lexer state across the fragment appended since the last delta:
* just enough JSON awareness (string/escape, nesting depth) to spot commas
* that sit at the top level of the input object — everything before such a
* comma is a set of complete fields. Returns true once the input object's
* closing brace arrives.
*
* The lexer reads the fragment itself, never `partialJson` by index. V8 keeps a
* string built with `+=` as a chain of parts, and the first index access
* copies the whole chain into one flat string. An index scan of `partialJson`
* would copy the whole input on each delta, so a large Write would cost
* quadratic time. A test in `acp-agent.test.ts` checks this.
*/
function scanStreamedToolInput(state: StreamedToolInput, fragment: string): boolean {
const offset = state.partialJson.length;
state.partialJson += fragment;
let complete = false;
for (let index = 0; index < fragment.length; index++) {
const character = fragment[index];
if (state.inString) {
if (state.escaped) {
state.escaped = false;
} else if (character === "\\") {
state.escaped = true;
} else if (character === '"') {
state.inString = false;
}
continue;
}
if (character === '"') {
state.inString = true;
} else if (character === "{") {
state.objectDepth++;
} else if (character === "}") {
state.objectDepth--;
if (state.objectDepth === 0) {
complete = true;
}
} else if (character === "[") {
state.arrayDepth++;
} else if (character === "]") {
state.arrayDepth--;
} else if (character === "," && state.objectDepth === 1 && state.arrayDepth === 0) {
state.lastTopLevelComma = offset + index;
}
}
return complete;
}
/** Parse the complete top-level fields before a top-level comma by closing the
* object at that boundary. */
function recoveredToolInput(prefix: string): Record<string, unknown> | undefined {
try {
const value: unknown = JSON.parse(prefix + "}");
return value !== null && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: undefined;
} catch {
return undefined;
}
}
/**
* Resolves the ACP `messageId` for a Claude SDK message (live) or a persisted
* transcript message (replay) so chunk grouping is identical in both views.
*
* Assistant turns are keyed by the Anthropic API message id (`message.id`),
* which is identical at `message_start`, on the consolidated assistant message,
* and in the persisted transcript — unlike the per-`stream_event` uuid, which is
* unique per event and never persisted. User messages have no API id, but they
* are never streamed, so their (stable) SDK uuid is used instead. ACP message
* ids are opaque strings, so no particular format is required.
*/
export function messageIdForGrouping(message: {
type?: string;
uuid?: string | null;
message?: unknown;
}): string | undefined {
if (message.type === "assistant") {
const inner = message.message;
const apiId =
inner && typeof inner === "object" && "id" in inner
? (inner as { id?: unknown }).id
: undefined;
if (typeof apiId === "string" && apiId.length > 0) {
return apiId;
}
}
return typeof message.uuid === "string" && message.uuid.length > 0 ? message.uuid : undefined;
}
/**
* Stamps an ACP `messageId` onto a session update, but only on the message/
* thought chunk variants that carry one — tool_call/plan/etc. updates never do.
* No-op when `messageId` is falsy, so callers can pass it through unconditionally.
*/
function applyMessageId(
update: SessionNotification["update"],
messageId: string | undefined,
): void {
if (
messageId &&
(update.sessionUpdate === "agent_message_chunk" ||
update.sessionUpdate === "user_message_chunk" ||
update.sessionUpdate === "agent_thought_chunk")
) {
update.messageId = messageId;
}
}
/** Built-in tools that drive the task list (headless/SDK sessions use these
* instead of TodoWrite). Their tool_use/tool_result are surfaced as `plan`
* snapshots rather than as tool_calls. */
function isTaskTool(toolName: string): boolean {
return (
toolName === "TaskCreate" ||
toolName === "TaskUpdate" ||
toolName === "TaskList" ||
toolName === "TaskGet"
);
}
/** Whether the streamed tool_use path surfaces this tool as a standalone
* `tool_call`. TodoWrite is rendered as a `plan` and Task* tools are
* suppressed (their plan snapshot is emitted at tool_result time), so neither
* produces a streamed tool_call/tool_call_update — which means a
* permission-surfaced tool_call for them (see `ensureToolCallEmitted`) must be
* resolved explicitly at tool_result time. */
function shouldEmitToolCall(toolName: string): boolean {
return toolName !== "TodoWrite" && !isTaskTool(toolName);
}
/**
* The renderer of the notification functions: the agent's tool call
* capabilities when they are passed, else those of the ACP capabilities.
*/
function toolCallRenderer(options?: {
clientCapabilities?: ClientCapabilities;
toolCallCapabilities?: ToolCallClientCapabilities;
replay?: boolean;
}): AcpToolCallRenderer {
return options?.toolCallCapabilities
? new AcpToolCallRenderer(options.toolCallCapabilities, options.replay)
: AcpToolCallRenderer.for(options?.clientCapabilities, options?.replay);
}
/**
* Convert an SDKAssistantMessage (Claude) to a SessionNotification (ACP).
* Only handles text, image, and thinking chunks for now.
*/
export function toAcpNotifications(
content: string | ContentBlockParam[] | BetaContentBlock[] | BetaRawContentBlockDelta[],
role: "assistant" | "user",
sessionId: string,
toolUseCache: ToolUseCache,
client: AcpClient,
logger: Logger,
options?: {
registerHooks?: boolean;
clientCapabilities?: ClientCapabilities;
// The agent's tool call capabilities. They win over clientCapabilities,
// and carry what ACP capabilities cannot (ToolCallClientCapabilities.v2).
toolCallCapabilities?: ToolCallClientCapabilities;
parentToolUseId?: string | null;
cwd?: string;
taskState?: TaskState;
// The subagent TodoWrite checklists that the client holds in the session.
// Mutated in place.
subagentChecklists?: SubagentChecklists;
// Tracks tool_use ids already emitted as a `tool_call` so a permission
// request (which emits the tool_call eagerly) and the streamed tool_use
// chunk don't both emit one — whichever arrives second emits a
// `tool_call_update` instead. Mutated in place. When omitted, the
// tool_call/update decision falls back to `toolUseCache` presence (the
// historical single-source behavior).
emittedToolCalls?: Set<string>;
// False while the input of a streamed tool_use still streams: the first
// tool_call then leaves `rawInput` out, and the consolidated message
// sends it once it is complete.
inputComplete?: boolean;
// Remembers the fields sent for each open tool call. When present, a
// tool_call_update carries only the fields that changed, and an update
// with nothing new is dropped. Mutated in place.
toolCallFields?: ToolCallFieldTracker;
// Opaque id identifying the message these chunks belong to (ACP message ids
// are opaque strings — no particular format is required). Attached to
// user/agent message and thought chunks so clients can group streamed chunks
// into a single message. Omit it (leave undefined) when unknown — never send
// an explicit `null`.
messageId?: string;
// The SDK user message's `tool_use_result`: the structured Output object of
// the tool_result this message carries (shape is per-tool). Used to render
// Agent/Task results from the structured subagent report instead of the raw
// text (which ends in a model-directed agentId/usage trailer).
toolUseResult?: unknown;
// The SDK user message's `tool_result_meta` sidecar, passed raw (it's
// untyped in sdk.d.ts) and validated by `parseToolResultMeta`. Stamps
// denied/interrupted tool_call_updates with why the tool never ran.
toolResultMeta?: unknown;
// True when the content is a result of the work that a `session/cancel`
// stopped. An error result then ends a run that the cancel aborted,
// whichever kind the SDK stamped on it (see `resultStatus`).
cancelled?: boolean;
// True when the content comes from the history of a loaded session.
replay?: boolean;
},
): SessionNotification[] {
const taskState = options?.taskState ?? new Map();
const registerHooks = options?.registerHooks !== false;
const renderer = toolCallRenderer(options);
if (typeof content === "string") {
if (content.length === 0) {
return [];
}
const update: SessionNotification["update"] = {
sessionUpdate: role === "assistant" ? "agent_message_chunk" : "user_message_chunk",
content: {
type: "text",
text: content,
},
};
applyMessageId(update, options?.messageId);
if (options?.parentToolUseId) stampParentToolUseId(update, options.parentToolUseId);
return [{ sessionId, update }];
}
// `tool_use_result` is message-level and carries no tool_use_id of its own:
// it describes "the" tool_result block of the message it rode in on. If
// several tool_result blocks were ever batched into one message it couldn't
// be attributed, so it is only honored when the message carries exactly one.
const toolUseResult =
options?.toolUseResult !== undefined &&
content.filter((c) => typeof c === "object" && c !== null && c.type === "tool_result")
.length === 1
? options.toolUseResult
: undefined;
// Unlike `tool_use_result`, entries carry their own tool_use_id, so batched
// messages need no single-block guard.
const toolResultMeta = parseToolResultMeta(options?.toolResultMeta);
const output = [];
// Only handle the first chunk for streaming; extend as needed for batching
for (const chunk of content) {
let update: SessionNotification["update"] | null = null;
// The tool call that this chunk finishes, if it is a tool result.
let finishedToolCallId: string | undefined;
// The plan that this chunk approves, if it is an ExitPlanMode result.
let approvedPlanUpdate: SessionNotification["update"] | undefined;
switch (chunk.type) {
case "text":
case "text_delta": {
if (chunk.text) {
update = {
sessionUpdate: role === "assistant" ? "agent_message_chunk" : "user_message_chunk",
content: {
type: "text",
text: chunk.text,
},
};
}
break;
}
case "image":
update = {
sessionUpdate: role === "assistant" ? "agent_message_chunk" : "user_message_chunk",
content: {
type: "image",
data: chunk.source.type === "base64" ? chunk.source.data : "",
mimeType: chunk.source.type === "base64" ? chunk.source.media_type : "",
uri: chunk.source.type === "url" ? chunk.source.url : undefined,
},
};
break;
case "thinking":
case "thinking_delta": {
// Recent models default `thinking.display` to "omitted", which streams
// signature-only thinking blocks whose text is empty.
if (chunk.thinking) {
update = {
sessionUpdate: "agent_thought_chunk",
content: {
type: "text",
text: chunk.thinking,
},
};
}
break;
}
case "tool_use":
case "server_tool_use":
case "mcp_tool_use": {
const alreadyCached = chunk.id in toolUseCache;
toolUseCache[chunk.id] = chunk;
if (chunk.name === "TodoWrite") {
// @ts-expect-error - sometimes input is empty object or undefined
if (Array.isArray(chunk.input?.todos)) {
update = todoWritePlanUpdate(planEntries(chunk.input as { todos: ClaudePlanEntry[] }), {
planOperations: options?.clientCapabilities?.plan != null,
parentToolUseId: options?.parentToolUseId,
// With native subagent sessions, an update of a subagent goes
// to the subagent's session (see NativeSubagentRuntime).
childSessions: clientSupportsSubagents(options?.clientCapabilities),
shown: options?.subagentChecklists,
});
}
} else if (isTaskTool(chunk.name)) {
// Task* tool_use is suppressed; the plan update is emitted at
// tool_result time once we have the task ID (for TaskCreate) and
// confirmation that the change took effect.
} else {
// Only register hooks on first encounter to avoid double-firing
if (registerHooks && !alreadyCached) {
// Capture the tool name in the closure rather than re-reading the
// cache when the hook fires. The cache entry is pruned at
// tool_result time, and a PostToolUse hook can fire after that, so
// closing over the name keeps the diff working without depending on
// (or pinning) the cache entry's lifetime.
const toolName = chunk.name;
const hookToolCallId = chunk.id;
registerHookCallback(
chunk.id,
{
onPostToolUseHook: async (toolUseId, _toolInput, toolResponse) => {
// The final diff of an Edit or a Write replaces the
// optimistic content built from the input. Only the marker
// fields of the tool_response travel: the rest repeats
// output that the content already carries.
const update = await renderer.hookResult(
{ id: toolUseId, name: toolName },
toolResponse,
options?.cwd,
);
if (!update) return;
if (options?.parentToolUseId) {
stampParentToolUseId(update, options.parentToolUseId);
}
// The final result may replace a pinned approval patch.
if (
options?.toolCallFields &&
!options.toolCallFields.apply(update, { replacePinnedContent: true })
) {
return;
}
await client.sessionUpdate({ sessionId, update });
},
onRelease: () => options?.toolCallFields?.finishHook(hookToolCallId),
},
sessionId,
);
}
let rawInput;
try {
rawInput = JSON.parse(JSON.stringify(chunk.input));
} catch {
// ignore if we can't turn it to JSON
}
const toolUse = { id: chunk.id, name: chunk.name, input: rawInput };
// Emit a `tool_call` only the first time this id surfaces to the
// client; afterwards refine it with a `tool_call_update`. The first
// surface may be this stream chunk OR an earlier permission request
// (see `ensureToolCallEmitted`), so emission is tracked separately
// from `toolUseCache`. Without an `emittedToolCalls` set we fall back
// to cache presence — the historical streaming-only behavior.
const emittedToolCalls = options?.emittedToolCalls;
const alreadyEmitted = emittedToolCalls ? emittedToolCalls.has(chunk.id) : alreadyCached;
emittedToolCalls?.add(chunk.id);
// A permission request surfaced the tool call with its complete
// input. The empty input at the stream start has nothing to add.
if (alreadyEmitted && options?.inputComplete === false) break;
update = alreadyEmitted
? // Already surfaced (full assistant message after streaming, or a
// permission request emitted it first): refine it with the
// complete input.
renderer.refinement(toolUse, options?.cwd)
: // First surface (streaming content_block_start or replay).
renderer.toolCall(toolUse, {
cwd: options?.cwd,
inputComplete: options?.inputComplete,
});
}
break;
}
case "advisor_tool_result": {
// Advisor results arrive in assistant content, including encrypted advice.
// Resolve the server tool through the normal result path without rendering
// opaque ciphertext. Server tools have no later PostToolUse hook to await.
const result = chunk.content;
unregisterHookCallback(chunk.tool_use_id);
output.push(
...toAcpNotifications(
[
{
type: "tool_result",
tool_use_id: chunk.tool_use_id,
is_error: result.type === "advisor_tool_result_error",
content:
result.type === "advisor_result"
? result.text
: result.type === "advisor_redacted_result"
? "Advisor guidance applied server-side."
: `Advisor error: ${result.error_code}`,
},
],
role,
sessionId,
toolUseCache,
client,
logger,
options,
),
);
break;
}
case "tool_result":
case "tool_search_tool_result":
case "web_fetch_tool_result":
case "web_search_tool_result":
case "code_execution_tool_result":
case "bash_code_execution_tool_result":
case "text_editor_code_execution_tool_result":
case "mcp_tool_result": {
const wasEmitted = options?.emittedToolCalls?.has(chunk.tool_use_id) === true;
options?.emittedToolCalls?.delete(chunk.tool_use_id);
completeHookCallback(chunk.tool_use_id);
finishedToolCallId = chunk.tool_use_id;
// Why this is_error result carries harness prose instead of tool
// output (user-rejected / interrupted / …), when the SDK said so.
// Spread into the claudeCode meta of every update emitted below; the
// untracked-tool fallback can't carry it (claudeCode metas always
// carry `toolName`, which is unknown there).
const nonExecution = toolResultMeta?.get(chunk.tool_use_id);
const toolUse = toolUseCache[chunk.tool_use_id];
if (!toolUse) {
// The permission flow may have surfaced this tool_call even though
// its tool_use never reached the cache (e.g. the assistant message
// carrying it was dropped by the cancelled-turn guard and a straggler
// result landed later). Resolve the surfaced call anyway so it can't
// stay pending in the client forever; without the cache entry the
// tool name is unknown, so no claudeCode meta is attached.
if (wasEmitted) {
output.push({
sessionId,
update: {
toolCallId: chunk.tool_use_id,
sessionUpdate: "tool_call_update" as const,
status: renderer.resultStatus(
"is_error" in chunk && chunk.is_error === true,
nonExecution,
{ cancelled: options?.cancelled },
),
rawOutput: chunk.content,
},
});
}
logger.error(
`[claude-agent-acp] Got a tool result for tool use that wasn't tracked: ${chunk.tool_use_id}`,
);
break;
}
// A permission request may have surfaced a plan-rendered (TodoWrite) or
// suppressed (Task*) tool as a real tool_call so the request referenced
// a tool call the client knows about (see `ensureToolCallEmitted`,
// issue #851). The branches below never emit a tool_call_update for
// those tools, which would leave the surfaced call pending in the
// client forever — resolve it here. `wasEmitted` is only ever true for
// these tools via the permission flow: the streamed plan/suppressed
// branches don't record emissions.
if (wasEmitted && !shouldEmitToolCall(toolUse.name)) {
output.push({
sessionId,
update: {
_meta: {
claudeCode: {
toolName: toolUse.name,
...(nonExecution ?? {}),
...(options?.parentToolUseId ? { parentToolUseId: options.parentToolUseId } : {}),
},
} satisfies ToolUpdateMeta,
toolCallId: chunk.tool_use_id,
sessionUpdate: "tool_call_update" as const,
status: renderer.resultStatus(
"is_error" in chunk && chunk.is_error === true,
nonExecution,
{ cancelled: options?.cancelled },
),
rawOutput: chunk.content,
},
});
}
if (isTaskTool(toolUse.name)) {
// Headless/SDK sessions emit Task* tools instead of TodoWrite.
// TaskCreate / TaskUpdate mutate the accumulated task list. TaskList
// reconciles it from the SDK's authoritative snapshot, which repairs
// resumed or compacted sessions whose creating calls are no longer in
// replay history. TaskGet is read-only and remains suppressed. Plan
// updates always carry the full accumulated snapshot, mirroring the
// legacy TodoWrite behavior.
const isError = "is_error" in chunk && chunk.is_error;
let shouldEmitTaskPlan = false;
if (!isError) {
if (toolUse.name === "TaskCreate") {
applyTaskCreate(
taskState,
toolUse.input as Parameters<typeof applyTaskCreate>[1],
parseTaskCreateOutput(toolUseResult) ?? parseTaskCreateOutput(chunk.content),
);
shouldEmitTaskPlan = true;
} else if (toolUse.name === "TaskUpdate") {
const input = toolUse.input as Parameters<typeof applyTaskUpdate>[1];
const output =
parseTaskUpdateOutput(toolUseResult, input?.taskId) ??
parseTaskUpdateOutput(chunk.content, input?.taskId);
// Older CLI transcripts have no structured output, so retain the
// input-based fallback. When an output is available, only apply a
// confirmed update for the same task.
if (!output || (output.success && output.taskId === input?.taskId)) {
applyTaskUpdate(taskState, input);
shouldEmitTaskPlan = true;
}
} else if (toolUse.name === "TaskList") {
const output =
parseTaskListOutput(toolUseResult) ?? parseTaskListOutput(chunk.content);
if (output) {
applyTaskList(taskState, output);
shouldEmitTaskPlan = true;
}
}
}
const entries = shouldEmitTaskPlan
? changedTaskPlanEntries(taskState, renderer.capabilities.air.client)
: undefined;
if (entries) {
update = checklistPlanUpdate(entries, options?.clientCapabilities?.plan != null);
}
} else if (toolUse.name !== "TodoWrite") {
// A command sends its output first, then the exit and the status.
const [finalUpdate, ...rest] = renderer
.result(toolUse, chunk as Parameters<AcpToolCallRenderer["result"]>[1], {
structured: toolUseResult,
nonExecution,
cancelled: options?.cancelled,
})
.reverse();
for (const outputUpdate of rest.reverse()) {
if (options?.parentToolUseId) {
stampParentToolUseId(outputUpdate, options.parentToolUseId);
}
output.push({ sessionId, update: outputUpdate });
}
// An approved plan of the main conversation becomes the session's
// markdown plan, after the tool call's result. An approval that
// clears the context ends the tool with an error result instead, and
// the restart into the fresh context publishes that plan (see
// ExitPlanCoordinator.restart).
if (
toolUse.name === "ExitPlanMode" &&
options?.clientCapabilities?.plan &&
!options.parentToolUseId &&
!("is_error" in chunk && chunk.is_error)
) {
const plan = approvedPlan(toolUse.input, toolUseResult);
if (plan) {
approvedPlanUpdate = planModeUpdate(
plan,
renderer.capabilities.air.client ? toolUse.id : undefined,
);
}
}
update = finalUpdate;
}
// The tool_use is fully resolved now — drop it so a long session doesn't
// retain every tool call. The PostToolUse hook (Edit/Write diffs) closes
// over the tool name and no longer reads the cache, so pruning here is
// safe regardless of hook/result ordering.
delete toolUseCache[chunk.tool_use_id];
break;
}
case "document":
case "search_result":
case "redacted_thinking":
case "input_json_delta":
case "citations_delta":
case "signature_delta":
case "container_upload":
case "compaction":
case "compaction_delta":
case "fallback":
case "mcp_tool_listing":
break;
default:
unreachable(chunk, logger);
break;
}
if (update) {
if (options?.parentToolUseId) stampParentToolUseId(update, options.parentToolUseId);
applyMessageId(update, options?.messageId);
// A tool result is final, so it may replace a pinned approval patch,
// for example with the error text of a rejected Edit.
if (
!options?.toolCallFields ||
options.toolCallFields.apply(update, {
replacePinnedContent: finishedToolCallId !== undefined,
})
) {
output.push({ sessionId, update });
}
}
if (approvedPlanUpdate) output.push({ sessionId, update: approvedPlanUpdate });
if (finishedToolCallId !== undefined) {
// The PostToolUse hook can still send the final diff, so the fields
// stay tracked until its callback leaves the registry.
options?.toolCallFields?.finishResult(
finishedToolCallId,
hasHookCallback(finishedToolCallId),
);
}
}
return output;
}
export function streamEventToAcpNotifications(
message: SDKPartialAssistantMessage,
sessionId: string,
toolUseCache: ToolUseCache,
client: AcpClient,
logger: Logger,
options?: {
clientCapabilities?: ClientCapabilities;
// See toAcpNotifications.
toolCallCapabilities?: ToolCallClientCapabilities;
cwd?: string;
taskState?: TaskState;
subagentChecklists?: SubagentChecklists;
emittedToolCalls?: Set<string>;
toolCallFields?: ToolCallFieldTracker;
messageId?: string;
streamedToolInputs?: StreamedToolInputCache;
},
): SessionNotification[] {
const event = message.event;
const streamKey = message.parent_tool_use_id ?? "";
const streamedToolInputs = options?.streamedToolInputs;
const forwardedOptions = {
clientCapabilities: options?.clientCapabilities,
toolCallCapabilities: options?.toolCallCapabilities,
parentToolUseId: message.parent_tool_use_id,
cwd: options?.cwd,
taskState: options?.taskState,
subagentChecklists: options?.subagentChecklists,
emittedToolCalls: options?.emittedToolCalls,
toolCallFields: options?.toolCallFields,
messageId: options?.messageId,
};
switch (event.type) {
case "content_block_start": {
const block = event.content_block;
if (
streamedToolInputs &&
(block.type === "tool_use" ||
block.type === "server_tool_use" ||
block.type === "mcp_tool_use")
) {
let inputsForMessage = streamedToolInputs.get(streamKey);
if (!inputsForMessage) {
inputsForMessage = new Map();
streamedToolInputs.set(streamKey, inputsForMessage);
}
inputsForMessage.set(event.index, {
id: block.id,
name: block.name,
partialJson: "",
inString: false,
escaped: false,
objectDepth: 0,
arrayDepth: 0,
lastTopLevelComma: -1,
emittedThroughComma: -1,
});
}
// The input of a streamed tool_use starts empty and streams after this
// event, so the tool_call waits for the complete input.
return toAcpNotifications([block], "assistant", sessionId, toolUseCache, client, logger, {
...forwardedOptions,
inputComplete: false,
});
}
case "content_block_delta": {
if (event.delta.type === "input_json_delta") {
const streamedInput = streamedToolInputs?.get(streamKey)?.get(event.index);
if (!streamedInput) return [];
if (scanStreamedToolInput(streamedInput, event.delta.partial_json)) {
// Input complete: the consolidated assistant message replays the
// block with its full input and refines the call there; emitting
// here too would send a duplicate identical update.
const inputsForMessage = streamedToolInputs?.get(streamKey);
inputsForMessage?.delete(event.index);
if (inputsForMessage?.size === 0) streamedToolInputs?.delete(streamKey);
return [];
}
if (streamedInput.lastTopLevelComma <= streamedInput.emittedThroughComma) {
return [];
}
streamedInput.emittedThroughComma = streamedInput.lastTopLevelComma;
const input = recoveredToolInput(
streamedInput.partialJson.slice(0, streamedInput.lastTopLevelComma),
);
if (!input) return [];
// TodoWrite and the Task* tools never surfaced a tool_call to refine.
if (!shouldEmitToolCall(streamedInput.name)) return [];
const update: SessionNotification["update"] = toolCallRenderer(options).partialRefinement(
streamedInput,
input,
options?.cwd,
);
if (message.parent_tool_use_id) stampParentToolUseId(update, message.parent_tool_use_id);
applyMessageId(update, options?.messageId);
// A refinement resends only what changed: rawInput grows with every
// field, and title, kind, and locations usually stay the same.
if (options?.toolCallFields && !options.toolCallFields.apply(update)) return [];
return [{ sessionId, update }];
}
return toAcpNotifications(
[event.delta],
"assistant",
sessionId,
toolUseCache,
client,
logger,
forwardedOptions,
);
}
// No content. `ping` is a Messages-API keep-alive event that the SDK's
// `BetaRawMessageStreamEvent` union doesn't include even though the
// wire format emits it; the `as never` cast lets us no-op it here
// instead of letting it fall through to `unreachable`.
case "ping" as never:
case "message_delta":
return [];
// A message boundary ends every input stream on this lane: message_stop is
// the normal end, and a message_start clears anything a prior message on
// the lane left behind (e.g. a stream cut short mid-block).
case "message_start":
case "message_stop":
streamedToolInputs?.delete(streamKey);
return [];
case "content_block_stop": {
const inputsForMessage = streamedToolInputs?.get(streamKey);
inputsForMessage?.delete(event.index);
if (inputsForMessage?.size === 0) streamedToolInputs?.delete(streamKey);
return [];
}
default:
unreachable(event, logger);
return [];
}
}

View File

@@ -1,7 +1,8 @@
import * as v2 from "@agentclientprotocol/sdk/experimental/v2";
import { v1AgentApp, type ClaudeAcpAgent, type Logger } from "./acp-agent.js";
import { v1AgentApp, type ClaudeAcpAgent } from "./v1/acp-agent.js";
import type { Logger } from "./logger.js";
import { nodeToWebReadable, nodeToWebWritable } from "./utils.js";
import { v2AgentApp } from "./v2/agent.js";
import { v2AgentApp, type AgentHandle } from "./v2/agent.js";
/**
* The ACP versions that the adapter serves: `v1` or `v2` alone, or both with
@@ -32,13 +33,13 @@ export function serveAcp(
const input = nodeToWebReadable(process.stdin);
// The router opens the connection of the negotiated version only when it
// reads `initialize`, so a client that leaves before then leaves no agent.
let agent: ClaudeAcpAgent | undefined;
const onAgent = (created: ClaudeAcpAgent) => {
let agent: AgentHandle | undefined;
const onAgent = (created: AgentHandle) => {
agent = created;
};
const { closed } = acpProtocolRouter(logger, onAgent, options.protocols).connect(
v2.ndJsonStream(output, input),
);
const { closed } = acpProtocolRouter(logger, onAgent, options.protocols, {
onV2Agent: onAgent,
}).connect(v2.ndJsonStream(output, input));
return {
closed,
dispose: async () => {
@@ -53,15 +54,20 @@ export function serveAcp(
* the draft ACP v2, and every other client gets ACP v1. With `v1`, a v2
* client is negotiated down to v1. With `v2`, the router rejects the
* `initialize` of a v1 client as an unsupported protocol version. `onAgent`
* receives the agent of the connection once the router has opened it.
* receives the agent of a v1 connection (`ClaudeAcpAgent`) once the router
* has opened it, and `onV2Agent` the agent of a v2 connection, which the
* v2 core (`src/v2/`) serves.
*/
export function acpProtocolRouter(
logger: Logger | undefined,
onAgent: (agent: ClaudeAcpAgent) => void,
protocols: AcpProtocols = "auto",
{ onV2Agent = () => {} }: { onV2Agent?: (agent: AgentHandle) => void } = {},
): v2.AgentProtocolRouter {
const router = v2.agentProtocolRouter();
if (protocols !== "v2") router.withV1(v1AgentApp(logger, onAgent));
if (protocols !== "v1") router.withV2(v2AgentApp(logger, onAgent));
if (protocols !== "v1") {
router.withV2(v2AgentApp(logger, onV2Agent));
}
return router;
}

View File

@@ -1,5 +1,6 @@
/** Shared sentinels and ids for session preferences managed by the adapter. */
export const DEFAULT_MODEL_ID = "default";
export const DEFAULT_AGENT_ID = "default";
export const AGENT_CONFIG_ID = "agent";
export const EFFORT_CONFIG_ID = "effort";
export const ULTRACODE_CONFIG_ID = "ultracode";

View File

@@ -0,0 +1,244 @@
/**
* The config options of a session, as both the v1 core and the v2 core
* build them: mode, model, effort, Fast mode, ultracode and the main-thread
* agent.
*/
import {
RequestError,
type ClientCapabilities,
type SessionConfigOption,
type SessionModeState,
type SetSessionConfigOptionRequest,
} from "@agentclientprotocol/sdk/unstable";
import type { AgentInfo, ModelInfo, Query } from "@anthropic-ai/claude-agent-sdk";
import {
AIR_RECOMMENDED_CONFIG_VALUE_CAPABILITY,
clientSupportsAirCapability,
} from "./air-extension.js";
import { AGENT_CONFIG_ID, DEFAULT_AGENT_ID } from "./session-config-ids.js";
import { buildEffortConfigOption } from "./session-effort.js";
import {
createFastModeConfigOption,
FAST_MODE_CONFIG_ID,
resolveFastModeEnabled,
type FastModeOptionState,
} from "./session-fast-mode.js";
import { MODE_CONFIG_ID, SessionModeManager } from "./session-mode.js";
import {
buildModelConfigOption,
MODEL_CONFIG_ID,
resolveModelPreference,
type SessionModelState,
} from "./session-model.js";
import {
createUltracodeConfigOption,
resolveUltracodeEnabled,
ULTRACODE_CONFIG_ID,
type UltracodeOptionState,
} from "./session-ultracode.js";
// `supportedAgents()` always returns Claude Code's built-in subagents — the
// ones used for Task-tool delegation (Explore, Plan, etc.) — even when the user
// has configured none of their own. Those aren't meaningful *main-thread*
// personas, so we filter them out and only surface the Agent picker when the
// user (or a plugin/project) has configured custom agents. Update this set if
// the SDK's built-in roster changes.
export const BUILTIN_AGENT_NAMES = new Set([
"claude",
"general-purpose",
"Explore",
"Plan",
"statusline-setup",
]);
// Value of the synthetic "Default" entry in the agent picker, which maps to the
// standard Claude Code agent (`applyFlagSettings({ agent: null })`). It is a
// reserved sentinel: a custom agent named exactly this would collide with it
// (two options sharing the value, selection silently routing to `null`), so we
// exclude that name from discovery.
/** Discover user/plugin/project-configured main-thread agents, excluding the
* built-in subagents and the reserved "default" sentinel. Returns an empty
* list if discovery fails so a flaky control request never blocks session
* creation. */
export async function discoverCustomAgents(q: Query): Promise<AgentInfo[]> {
try {
return customAgents(await q.supportedAgents());
} catch {
return [];
}
}
/** The configured agents of `agents`: neither a built-in subagent nor the "default" sentinel. */
export function customAgents(agents: readonly AgentInfo[]): AgentInfo[] {
return agents.filter((a) => !BUILTIN_AGENT_NAMES.has(a.name) && a.name !== DEFAULT_AGENT_ID);
}
/** Stable ids for the session config options surfaced via `configOptions`.
* Centralized so the option declarations in `buildConfigOptions` and the
* handlers in `setSessionConfigOption`/`applyConfigOptionValue` reference the
* same identifiers and can't drift apart. */
export { MODE_CONFIG_ID };
export { AGENT_CONFIG_ID };
/** Whether the Client advertised support for boolean session config options
* (`session.configOptions.boolean`). Agents MUST only send `type: "boolean"`
* config options to Clients that opt in; otherwise we fall back to a `select`.
* See https://agentclientprotocol.com/rfds/boolean-config-option. */
export function clientSupportsBooleanConfigOptions(
clientCapabilities?: ClientCapabilities | null,
): boolean {
return clientCapabilities?.session?.configOptions?.boolean != null;
}
/** Whether the Client advertised the AIR `recommendedValue` capability for
* select-style model and effort options. Missing or malformed AIR metadata
* retains the legacy `default` entries. */
export function clientSupportsRecommendedConfigValue(
clientCapabilities?: ClientCapabilities | null,
): boolean {
return clientSupportsAirCapability(clientCapabilities, AIR_RECOMMENDED_CONFIG_VALUE_CAPABILITY);
}
export type ConfigOptionPresentation = {
/** Replace ambiguous `default` rows with concrete values and advertise the
* SDK/adapter recommendation as `_meta.jetbrains.air.recommendedValue`. */
useRecommendedValue: boolean;
};
export function buildConfigOptions(
modes: SessionModeState,
models: SessionModelState,
modelInfos: ModelInfo[],
currentEffortLevel?: string,
agents: AgentInfo[] = [],
currentAgent: string = DEFAULT_AGENT_ID,
fastMode?: FastModeOptionState,
presentation?: ConfigOptionPresentation,
ultracode?: UltracodeOptionState,
): SessionConfigOption[] {
const options: SessionConfigOption[] = [SessionModeManager.configOption(modes)];
// A gateway that lists none of the SDK's models leaves no model to offer, so
// there is no model option at all.
if (models.availableModels.length > 0) {
options.push(
buildModelConfigOption(models, modelInfos, presentation?.useRecommendedValue === true),
);
}
const effort = buildEffortConfigOption(
modelInfos,
models.currentModelId,
currentEffortLevel,
presentation?.useRecommendedValue === true,
);
if (effort) options.push(effort);
// Surface the Fast mode toggle only when the current model supports it. The
// option renders as a native boolean toggle for Clients that opted in, and a
// two-value select otherwise.
if (fastMode?.supported) {
options.push(
createFastModeConfigOption(
fastMode.enabled,
fastMode.useBooleanOption,
fastMode.disabledReason,
),
);
}
// Surface the ultracode toggle only while the CLI can run it: dynamic
// workflows are enabled and the current model supports it.
if (ultracode?.supported) {
options.push(createUltracodeConfigOption(ultracode.enabled, ultracode.useBooleanOption));
}
// Only surface the Agent picker when there's a real choice — i.e. the user
// has configured at least one custom agent (built-ins are filtered out in
// discoverCustomAgents). With none configured, "Default" would be the only
// entry, so we omit the option entirely.
if (agents.length > 0) {
options.push({
id: AGENT_CONFIG_ID,
name: "Agent",
description: "Main-thread agent persona",
type: "select",
currentValue: currentAgent,
options: [
{ value: DEFAULT_AGENT_ID, name: "Default", description: "Standard Claude Code agent" },
...agents.map((a) => ({
value: a.name,
name: a.name,
description: a.description || undefined,
})),
],
});
}
return options;
}
/**
* The value that a `session/set_config_option` request sets on `option`, the
* option as the session advertised it. The value must match the option's
* type: a `type: "boolean"` option takes `true`/`false`, not "on"/"off", and a
* `type: "select"` option takes one of its values.
*
* Fast mode and ultracode give `enabled`: a boolean, or the "on"/"off" select
* fallback for clients without boolean config options. A model takes an alias
* too, such as "opus" or "sonnet" for a full model id. It is resolved against
* `modelInfos` first: those entries carry `resolvedModel`, so a full model id
* (in either hint spelling) lands on the right row via the exact tier instead
* of a fuzzier one picking a same-family sibling from a different context
* lane. The option's own entries (which never carry `resolvedModel`) remain as
* a fallback for resolutions that don't map back onto a selectable option
* (e.g. a fuzzy hit on an out-of-picker verbatim entry). `value` is then the
* canonical option value, the model id rather than the alias.
*/
export function configOptionValue(
option: SessionConfigOption,
params: SetSessionConfigOptionRequest,
modelInfos: ModelInfo[],
): { enabled: boolean } | { value: string } {
const expectedType = option.type === "boolean" ? "boolean" : "string";
if (typeof params.value !== expectedType) {
throw RequestError.invalidParams(
{ configId: params.configId, value: params.value },
`Config option ${params.configId} takes a ${expectedType} value, but received: ${JSON.stringify(params.value)}`,
);
}
if (params.configId === FAST_MODE_CONFIG_ID) return { enabled: resolveFastModeEnabled(params) };
if (params.configId === ULTRACODE_CONFIG_ID) {
return { enabled: resolveUltracodeEnabled(params) };
}
if (typeof params.value !== "string") {
throw RequestError.invalidParams(
undefined,
`Invalid value for config option ${params.configId}: ${params.value}`,
);
}
const requested = params.value;
const allValues =
"options" in option && Array.isArray(option.options)
? option.options.flatMap((o) => ("options" in o ? o.options : [o]))
: [];
let validValue = allValues.find((o) => o.value === requested);
if (!validValue && params.configId === MODEL_CONFIG_ID) {
const toOptionValue = (resolved: ModelInfo | null) =>
resolved ? allValues.find((o) => o.value === resolved.value) : undefined;
validValue = toOptionValue(resolveModelPreference(modelInfos, requested));
if (!validValue) {
const optionInfos: ModelInfo[] = allValues.map((o) => ({
value: o.value,
displayName: o.name,
description: o.description ?? "",
}));
validValue = toOptionValue(resolveModelPreference(optionInfos, requested));
}
}
if (!validValue) {
throw RequestError.invalidParams(
undefined,
`Invalid value for config option ${params.configId}: ${requested}`,
);
}
return { value: validValue.value };
}

View File

@@ -25,6 +25,22 @@ export function effortFlagSettings(
return ultracode ? { effortLevel, ultracode: true } : { effortLevel };
}
/**
* `params` without `_meta.claudeCode.options.effort`, for a restart of the
* session's query: the restart restores the current effort pin instead of
* the one the session was created with.
*/
export function withoutEffort<T extends { _meta?: unknown }>(params: T): T {
const meta = params._meta as
{ claudeCode?: { options?: { effort?: unknown } } } | null | undefined;
if (meta?.claudeCode?.options?.effort === undefined) {
return params;
}
const options = { ...meta.claudeCode.options };
delete options.effort;
return { ...params, _meta: { ...meta, claudeCode: { ...meta.claudeCode, options } } };
}
function canonicalizeModelSettingsKey(value: string): string {
return value
.trim()
@@ -133,3 +149,130 @@ export function buildEffortConfigOption(
: {}),
};
}
/** A session's effort bookkeeping: the level the user pinned through the
* picker, and the level last applied to the SDK's flag layer. */
export type EffortState = {
pinned: string | undefined;
applied: string | undefined;
};
/** The effort after a switch to `newModelInfo`, before the option is rebuilt
* (see {@link settleEffortAfterModelSwitch}). */
export type EffortSwitch = {
/** The effort to build the new model's option with. */
seed: string | undefined;
/** Whether the user's pin is one of the new model's levels. */
pinnedForNewModel: boolean;
before: EffortState;
/** The level applied before the switch, restored if the sync fails. */
appliedBeforeSwitch: string | undefined;
};
/** The effort seed of a model switch. It depends on who owns the choice: a
* user pin made through the ACP picker this session lives at the SDK's flag
* layer and follows the session across switches, so it carries forward.
* Otherwise the CLI resolves effort itself from the persisted settings — the
* NEW model's `modelSettings` entry first (the CLI persists /effort per
* model), then the legacy top-level value — so the option shows what it
* will actually run instead of dragging the old model's value along. */
export function effortForModelSwitch(input: {
state: EffortState;
/** The current value of the effort option, if it is shown. */
displayed: string | undefined;
newModelInfo: ModelInfo | undefined;
modelId: string;
/** The merged effort settings (see {@link mergeEffortSettings}). */
settings: Settings;
useRecommendedValue: boolean;
}): EffortSwitch {
const { state, displayed, newModelInfo, useRecommendedValue } = input;
const pinned = state.pinned;
const pinnedForNewModel =
pinned !== undefined &&
newModelInfo?.supportsEffort === true &&
newModelInfo.supportedEffortLevels?.some((level) => level === pinned) === true;
return {
seed: pinnedForNewModel
? pinned
: settingsEffortForModel(input.settings, newModelInfo, input.modelId),
pinnedForNewModel,
before: state,
appliedBeforeSwitch:
state.applied ??
(useRecommendedValue && typeof displayed === "string" && displayed !== "default"
? displayed
: pinned),
};
}
/**
* Applies the effort of a model switch to the SDK's flag layer, once the new
* model's effort `option` is built from `change.seed`. Returns the new state
* and the option to show (undefined to hide it).
*
* Opted-in clients (`useRecommendedValue`) apply the concrete displayed effort
* on every switch, including settings-derived values, so a previous automatic
* flag cannot shadow the new model's settings. This does not create a user
* pin. For other clients, the flag is synced only when a user pin is lost
* across the switch — the new model clamped it away, where the flag must be
* cleared too or the SDK would keep running the old pin invisibly.
* Settings-derived seeds are display-only: the CLI resolves persisted effort
* itself, and pinning it at the flag layer would shadow the per-model values
* on every later switch.
*
* The model switch already succeeded, so a failed sync is returned as
* `error`, not thrown. The old bookkeeping stays, and the unapplied value is
* never shown: the option shows the last applied value when the new model can
* select it, else it is hidden until a later switch rebuilds it.
*/
export async function settleEffortAfterModelSwitch(
change: EffortSwitch,
option: SessionConfigOption | undefined,
ultracode: boolean,
useRecommendedValue: boolean,
applyFlagSettings: (settings: ReturnType<typeof effortFlagSettings>) => Promise<void>,
): Promise<{ state: EffortState; option: SessionConfigOption | undefined; error?: unknown }> {
const { pinnedForNewModel, before } = change;
const sync = useRecommendedValue || (before.pinned !== undefined && !pinnedForNewModel);
if (!sync) {
return {
state: pinnedForNewModel ? { pinned: before.pinned, applied: before.pinned } : before,
option,
};
}
const newEffort = typeof option?.currentValue === "string" ? option.currentValue : undefined;
try {
// A legacy client's unpinned effort is display-only: the CLI resolves the
// persisted value for the new model. When an old user pin is no longer
// supported, clear the flag layer instead of replacing it with that
// displayed value. Opted-in clients deliberately apply their concrete
// displayed effort.
await applyFlagSettings(
effortFlagSettings(useRecommendedValue ? newEffort : undefined, ultracode),
);
return {
state: {
pinned: pinnedForNewModel ? before.pinned : undefined,
applied: useRecommendedValue && newEffort !== "default" ? newEffort : undefined,
},
option,
};
} catch (error) {
const applied = change.appliedBeforeSwitch;
const selectable =
applied !== undefined &&
option?.type === "select" &&
option.options.some((entry) =>
"value" in entry
? entry.value === applied
: entry.options.some((nested) => nested.value === applied),
);
return {
state: { pinned: before.pinned, applied },
option:
selectable && option?.type === "select" ? { ...option, currentValue: applied } : undefined,
error,
};
}
}

81
src/session-env.ts Normal file
View File

@@ -0,0 +1,81 @@
/**
* Session options that come from the agent's environment, shared by the v1
* core and the v2 core.
*/
import * as fs from "node:fs/promises";
import * as path from "node:path";
import type { Options, Settings, ThinkingConfig } from "@anthropic-ai/claude-agent-sdk";
/** Translate the legacy `MAX_THINKING_TOKENS` env var, plus the optional
* `CLAUDE_CODE_THINKING_DISPLAY` env var, into the SDK's `thinking` option.
* The `maxThinkingTokens` option it used to feed is deprecated and reduced
* to on/off on current models, so map the value to explicit thinking config
* instead: unset → `undefined` (SDK default, adaptive on models that support
* it) unless a display mode is requested, in which case it becomes explicit
* `adaptive`; `0` → disabled; a positive integer → a fixed token budget.
* Anything else is ignored with a warning. `CLAUDE_CODE_THINKING_DISPLAY`
* must be `summarized` or `omitted`; it has no effect when thinking is
* disabled. */
export function resolveThinkingConfig(
rawTokens: string | undefined,
rawDisplay: string | undefined,
logger: { error: (...args: unknown[]) => void },
): ThinkingConfig | undefined {
let display: "summarized" | "omitted" | undefined;
if (rawDisplay !== undefined) {
if (rawDisplay === "summarized" || rawDisplay === "omitted") {
display = rawDisplay;
} else {
logger.error(
`Ignoring CLAUDE_CODE_THINKING_DISPLAY: expected 'summarized' or 'omitted', got '${rawDisplay}'.`,
);
}
}
if (rawTokens === undefined) {
return display ? { type: "adaptive", display } : undefined;
}
const parsed = Number.parseInt(rawTokens, 10);
if (Number.isNaN(parsed) || parsed < 0) {
logger.error(
`Ignoring MAX_THINKING_TOKENS: expected a non-negative integer, got '${rawTokens}'.`,
);
return display ? { type: "adaptive", display } : undefined;
}
return parsed === 0
? { type: "disabled" }
: { type: "enabled", budgetTokens: parsed, ...(display && { display }) };
}
/** The `CLAUDE_MODEL_CONFIG` JSON: Bedrock model overrides and an allowlist, say. */
function parseModelConfig(
raw: string | undefined,
): { modelOverrides?: Record<string, string>; availableModels?: string[] } | undefined {
if (!raw) return undefined;
const parsed = JSON.parse(raw);
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
throw new Error("CLAUDE_MODEL_CONFIG must be a JSON object");
}
const result: { modelOverrides?: Record<string, string>; availableModels?: string[] } = {};
if (parsed.modelOverrides !== undefined) result.modelOverrides = parsed.modelOverrides;
if (parsed.availableModels !== undefined) result.availableModels = parsed.availableModels;
return Object.keys(result).length > 0 ? result : undefined;
}
/**
* The programmatic settings of a session, before the provider routing: the
* models of `CLAUDE_MODEL_CONFIG`, then the client's `settings` option, an
* object or a file path relative to `cwd`. Throws for a `CLAUDE_MODEL_CONFIG`
* that is no JSON object, before it reads the file.
*/
export async function configuredSettings(
provided: Options["settings"],
cwd: string,
): Promise<Settings | undefined> {
const modelConfig = parseModelConfig(process.env.CLAUDE_MODEL_CONFIG);
const object =
typeof provided === "string"
? (JSON.parse(await fs.readFile(path.resolve(cwd, provided), "utf8")) as Settings)
: (provided as Settings | undefined);
return modelConfig ? { ...modelConfig, ...object } : object;
}

190
src/session-fast-mode.ts Normal file
View File

@@ -0,0 +1,190 @@
/**
* The Fast mode toggle of a session: its config option, the value a
* `session/set_config_option` request sets, and the state Claude Code reports
* (`fast_mode_state` and `fast_mode_disabled_reason`, on `system`/init and on
* results). Shared by the v1 core and the v2 core.
*/
import { RequestError } from "@agentclientprotocol/sdk/unstable";
import type {
SessionConfigOption,
SetSessionConfigOptionRequest,
} from "@agentclientprotocol/sdk/unstable";
import type { FastModeDisabledReason, FastModeState } from "@anthropic-ai/claude-agent-sdk";
import { sentenceCase } from "./session-notices.js";
export const FAST_MODE_CONFIG_ID = "fast";
/** Select-fallback values used when the client has not opted into boolean
* config options (see {@link createFastModeConfigOption}). */
export const FAST_MODE_ON = "on";
export const FAST_MODE_OFF = "off";
const FAST_MODE_DESCRIPTION = "Faster responses on supported models";
/** Map the SDK's tri-state `fast_mode_state` onto the boolean config toggle.
* `cooldown` (fast mode temporarily suspended after a rate limit, per the SDK
* docs) keeps the toggle on so it reflects the user's intent — only an
* explicit `off` clears it. */
export function fastModeStateEnabled(state: FastModeState): boolean {
return state !== "off";
}
/** User-facing explanations for the SDK's `fast_mode_disabled_reason` values
* that a user can act on (or at least wants to know about). Deliberately
* partial — the omitted reasons are not worth surfacing:
* - `sdk_opt_in_required`: every SDK session starts here (the toggle IS the
* opt-in), so it describes the default, not a problem.
* - `preference`: the user turned Fast mode off themselves.
* - `pending`: eligibility is still resolving; the next report supersedes it.
* - `unknown`: nothing meaningful to say.
* Unknown future reasons fall through the same way (open set — the SDK's docs
* say to ignore values you don't handle). */
const FAST_MODE_UNAVAILABLE_EXPLANATIONS: Partial<Record<FastModeDisabledReason, string>> = {
free: "not available on the free plan",
extra_usage_disabled: "requires extra usage to be enabled for this account",
model_not_allowed: "not available for the selected model",
not_first_party: "not available on this API provider",
disabled_by_env: "disabled by environment configuration",
network_error: "eligibility could not be verified (network error)",
};
/** Normalize an SDK-reported `fast_mode_disabled_reason` to the one we retain:
* a reason we have an explanation for, else `undefined`. Keeping only
* explainable reasons means state comparisons (see `fastModeStateChange`) track
* exactly what the user can see, so routine reports like
* `sdk_opt_in_required` never churn the config option. */
export function normalizeFastModeDisabledReason(
reason: FastModeDisabledReason | undefined,
): FastModeDisabledReason | undefined {
return reason && FAST_MODE_UNAVAILABLE_EXPLANATIONS[reason] ? reason : undefined;
}
/** Build the Fast mode config option. When the Client supports boolean config
* options we expose a native `type: "boolean"` toggle; otherwise we degrade to
* a two-value `select` ("on"/"off") so older Clients still get a usable
* control.
*
* `disabledReason` (the SDK's `fast_mode_disabled_reason`) is folded into the
* description while the toggle reads off, so a user whose account or provider
* can't serve Fast mode sees why instead of a switch that silently refuses to
* stay on. Ignored while enabled: a reason reported alongside an `on`/`cooldown`
* state isn't blocking anything right now. */
export function createFastModeConfigOption(
enabled: boolean,
useBooleanOption: boolean,
disabledReason?: FastModeDisabledReason,
): SessionConfigOption {
const explanation = enabled
? undefined
: disabledReason && FAST_MODE_UNAVAILABLE_EXPLANATIONS[disabledReason];
const base = {
id: FAST_MODE_CONFIG_ID,
name: "Fast mode",
description: explanation ? `${FAST_MODE_DESCRIPTION} — ${explanation}` : FAST_MODE_DESCRIPTION,
category: "model_config",
} as const;
if (useBooleanOption) {
return { ...base, type: "boolean", currentValue: enabled };
}
return {
...base,
type: "select",
currentValue: enabled ? FAST_MODE_ON : FAST_MODE_OFF,
options: [
{ value: FAST_MODE_ON, name: "On" },
{ value: FAST_MODE_OFF, name: "Off" },
],
};
}
/** Resolve the requested Fast mode value from a `session/set_config_option`
* request. Accepts a native boolean (boolean-capable Clients) or the
* "on"/"off" select-fallback strings. */
export function resolveFastModeEnabled(params: SetSessionConfigOptionRequest): boolean {
const value = params.value;
if (typeof value === "boolean") {
return value;
}
if (value === FAST_MODE_ON) {
return true;
}
if (value === FAST_MODE_OFF) {
return false;
}
throw RequestError.invalidParams(
undefined,
`Invalid value for config option ${FAST_MODE_CONFIG_ID}: ${value}`,
);
}
/** Per-model Fast mode state threaded into `buildConfigOptions`. The
* option is only surfaced when the current model `supported`s fast mode. */
export type FastModeOptionState = {
supported: boolean;
enabled: boolean;
/** Whether the Client opted into boolean config options. */
useBooleanOption: boolean;
/** Latest explainable `fast_mode_disabled_reason`, folded into the option's
* description while the toggle reads off. */
disabledReason?: FastModeDisabledReason;
};
/** What a reported `fast_mode_state` changes in a Fast mode toggle that the
* current model shows. The SDK can flip fast mode independently of the
* user — e.g. back to `on` once a rate-limit `cooldown` clears — so
* definitive on/off changes are mirrored into the option.
*
* Undefined when nothing changes: an absent state, a `cooldown` (a transient
* suspension of an already-enabled fast mode: the toggle stays as-is rather
* than flapping, and a stray cooldown never enables a toggle the user has
* off), or the same state with the same explainable reason.
*
* `reason` is the SDK's `fast_mode_disabled_reason`, reported alongside the
* state. Only explainable reasons are retained (see
* {@link normalizeFastModeDisabledReason}), so a routine
* `sdk_opt_in_required` report on every turn's result can't churn the
* option, while a real blocker updates the description even when the
* toggle's own value is unchanged.
*
* `notice` is set when the user had Fast mode on and the SDK turned it off
* for an explainable reason: the description carries the same explanation,
* but a toggle silently snapping back is worth saying out loud once. */
export function fastModeStateChange(
current: { enabled: boolean; disabledReason?: FastModeDisabledReason },
state: FastModeState | undefined,
reason: FastModeDisabledReason | undefined,
):
| { enabled: boolean; disabledReason?: FastModeDisabledReason; notice?: FastModeOffNotice }
| undefined {
if (state === undefined || state === "cooldown") return undefined;
const enabled = state === "on";
// A reason only describes an off state; drop any that rides an `on` report
// so it can't decorate the option the next time fast mode goes off.
const disabledReason = enabled ? undefined : normalizeFastModeDisabledReason(reason);
if (enabled === current.enabled && disabledReason === current.disabledReason) return undefined;
const explanation =
disabledReason !== undefined ? FAST_MODE_UNAVAILABLE_EXPLANATIONS[disabledReason] : undefined;
if (!current.enabled || enabled || explanation === undefined) {
return { enabled, disabledReason };
}
return {
enabled,
disabledReason,
notice: {
notice: {
severity: "warning",
title: "Fast mode turned off",
description: `${sentenceCase(explanation)}.`,
},
transcript: `**Fast mode turned off:** ${explanation}.`,
},
};
}
/** The notice of Fast mode that the SDK turned off, and its transcript line
* for a client without `notice` updates. */
export type FastModeOffNotice = {
notice: { severity: "warning"; title: string; description: string };
transcript: string;
};

View File

@@ -66,29 +66,50 @@ type ModeConfigSession = SessionMode & {
configOptions: SessionConfigOption[];
};
type InitializeSessionModeParams = {
query: Pick<Query, "setPermissionMode">;
requestedMode: PermissionMode;
currentModelInfo?: ModelInfo;
currentModelId: string;
/** Which optional modes the session's mode catalog offers. */
export type ModeAvailability = {
/** When false, bypassPermissions is omitted from the mode catalog. */
allowBypass: boolean;
/** When false (`disableAutoMode: "disable"` in settings), auto is omitted. */
allowAuto: boolean;
};
type InitializeSessionModeParams = ModeAvailability & {
query: Pick<Query, "setPermissionMode">;
requestedMode: PermissionMode;
/** The mode is the built-in default: no request, transcript or setting chose it. */
builtInDefault?: boolean;
currentModelInfo?: ModelInfo;
currentModelId: string;
};
/**
* The permission mode a new Claude query starts in.
*
* An explicit mode wins. A resume or a load continues in the mode of the last
* user record of the transcript. If that mode is unknown or not available now,
* for example bypass, the session starts in `defaultMode` of the settings.
* user record of the transcript. If that mode is unknown or absent from the
* session's mode catalog, use an available `defaultMode` from settings, or
* `default` when that setting is unavailable too.
*
* With no `defaultMode` set, the session starts in `auto` when the catalog
* offers it and in `default` otherwise (`builtInDefault: true`), like an
* interactive Claude Code session. Claude Code also starts organizations with
* the HIPAA configuration in `default`; the SDK does not report that
* configuration, so those sessions start in `auto` here.
*/
export function resolveInitialPermissionMode(
sources: { explicit?: PermissionMode; resumed?: string; defaultMode?: unknown },
logger: { error: (...args: unknown[]) => void },
allowBypass: boolean,
): PermissionMode {
availability: ModeAvailability,
): { mode: PermissionMode; builtInDefault: boolean } {
const { allowBypass } = availability;
const available = (mode: PermissionMode) =>
buildAvailableModes(availability).some((entry) => entry.id === mode);
if (sources.explicit !== undefined) {
return resolvePermissionMode(sources.explicit, logger, allowBypass);
return {
mode: resolvePermissionMode(sources.explicit, logger, allowBypass),
builtInDefault: false,
};
}
if (sources.resumed !== undefined) {
let rejected = false;
@@ -97,13 +118,23 @@ export function resolveInitialPermissionMode(
{ error: () => (rejected = true) },
allowBypass,
);
if (!rejected) return mode;
if (!rejected && available(mode)) {
return { mode, builtInDefault: false };
}
logger.error(
`Ignoring permission mode "${sources.resumed}" of the resumed transcript: ` +
`not available in this session.`,
);
}
return resolvePermissionMode(sources.defaultMode, logger, allowBypass);
if (sources.defaultMode === undefined) {
return { mode: available("auto") ? "auto" : "default", builtInDefault: true };
}
const configuredMode = resolvePermissionMode(sources.defaultMode, logger, allowBypass);
// A rejected transcript mode falls back only to a mode this session can offer.
if (sources.resumed !== undefined && !available(configuredMode)) {
return { mode: "default", builtInDefault: false };
}
return { mode: configuredMode, builtInDefault: false };
}
/** Owns session-mode policy and the ACP/SDK synchronization it requires. */
@@ -113,22 +144,40 @@ export class SessionModeManager<S extends SessionMode> {
async initialize({
query,
requestedMode,
builtInDefault = false,
currentModelInfo,
currentModelId,
allowBypass,
allowAuto,
}: InitializeSessionModeParams): Promise<{
modes: SessionModeState;
autoModeFallbackWarningPending: boolean;
}> {
const availableModes = this.buildAvailableModes(allowBypass);
const availableModes = buildAvailableModes(
{ allowBypass, allowAuto },
this.options.airClient?.() ?? false,
);
let effectiveMode = requestedMode;
let autoModeFallbackWarningPending = false;
if (
const autoUnsupported =
effectiveMode === "auto" &&
currentModelInfo !== undefined &&
currentModelInfo.supportsAutoMode !== true
) {
currentModelInfo.supportsAutoMode !== true;
if (!this.isAvailable(availableModes, effectiveMode)) {
this.options.logError(
`permissions.defaultMode "${effectiveMode}" is not available in ` +
`this session; falling back to "default".`,
);
effectiveMode = "default";
await this.trySyncMode(query, effectiveMode, "Failed to sync clamped permissionMode to SDK:");
} else if (autoUnsupported && builtInDefault) {
// Like Claude Code: a built-in Auto default the model can't serve starts
// in Manual, without a warning. Nobody chose Auto, so don't escalate to
// acceptEdits.
effectiveMode = "default";
await this.trySyncMode(query, effectiveMode, "Failed to sync clamped permissionMode to SDK:");
} else if (autoUnsupported) {
this.options.logError(
`permissions.defaultMode "auto" is not available for model ` +
`"${currentModelId}"; falling back to "${AUTO_MODE_FALLBACK}".`,
@@ -136,13 +185,6 @@ export class SessionModeManager<S extends SessionMode> {
effectiveMode = AUTO_MODE_FALLBACK;
autoModeFallbackWarningPending = true;
await this.trySyncMode(query, effectiveMode, "Failed to sync clamped permissionMode to SDK:");
} else if (!this.isAvailable(availableModes, effectiveMode)) {
this.options.logError(
`permissions.defaultMode "${effectiveMode}" is not available in ` +
`this session; falling back to "default".`,
);
effectiveMode = "default";
await this.trySyncMode(query, effectiveMode, "Failed to sync clamped permissionMode to SDK:");
}
return {
@@ -350,50 +392,6 @@ export class SessionModeManager<S extends SessionMode> {
}
}
private buildAvailableModes(allowBypass: boolean): SessionModeState["availableModes"] {
// Only AIR gets the mode kind.
const air = this.options.airClient?.() ?? false;
const kind = (value: string) => {
const meta = airOnlyMeta(air, AIR_KIND_KEY, value);
return meta ? { _meta: meta } : {};
};
const modes: SessionModeState["availableModes"] = [
{
id: "default",
name: "Manual",
description: "Always ask before making changes",
...kind("standard"),
},
{
id: "acceptEdits",
name: "Accept edits",
description: "Automatically accept all file edits",
...kind("standard"),
},
{
id: "plan",
name: "Plan",
description: "Create a plan before making changes",
...kind("plan"),
},
{
id: "auto",
name: "Auto",
description: "Claude handles permission decisions",
...kind("auto_review"),
},
];
if (allowBypass) {
modes.push({
id: "bypassPermissions",
name: "Bypass permissions",
description: "Accepts all permissions",
...kind("full_access"),
});
}
return modes;
}
private async trySyncMode(
query: Pick<Query, "setPermissionMode">,
mode: PermissionMode,
@@ -406,3 +404,51 @@ export class SessionModeManager<S extends SessionMode> {
}
}
}
export function buildAvailableModes(
{ allowBypass, allowAuto }: ModeAvailability,
air = false,
): SessionModeState["availableModes"] {
// Only AIR gets the mode kind.
const kind = (value: string) => {
const meta = airOnlyMeta(air, AIR_KIND_KEY, value);
return meta ? { _meta: meta } : {};
};
const modes: SessionModeState["availableModes"] = [
{
id: "default",
name: "Manual",
description: "Always ask before making changes",
...kind("standard"),
},
{
id: "acceptEdits",
name: "Accept edits",
description: "Automatically accept all file edits",
...kind("standard"),
},
{
id: "plan",
name: "Plan",
description: "Create a plan before making changes",
...kind("plan"),
},
];
if (allowAuto) {
modes.push({
id: "auto",
name: "Auto",
description: "Claude handles permission decisions",
...kind("auto_review"),
});
}
if (allowBypass) {
modes.push({
id: "bypassPermissions",
name: "Bypass permissions",
description: "Accepts all permissions",
...kind("full_access"),
});
}
return modes;
}

View File

@@ -84,39 +84,73 @@ function versionedModelDisplayName(model: ModelInfo): string {
return `${withoutContext} ${version}${contextSuffix[0]}`;
}
function tokenizeModelPreference(model: string): { tokens: string[]; contextHint?: string } {
const lower = model.trim().toLowerCase();
const contextHint = lower.match(MODEL_CONTEXT_HINT_PATTERN)?.[1]?.toLowerCase();
const tokens = lower
.replace(MODEL_CONTEXT_HINT_PATTERN, " $1 ")
function modelWords(value: string): string[] {
return value
.toLowerCase()
.split(/[^a-z0-9]+/)
.filter(Boolean)
.map((token) => {
if (token === "opusplan") return "opus";
if (token === "best" || token === "default") return "";
return token;
})
.filter((token) => token && token !== "claude")
.filter((token) => /[a-z]/.test(token) || token.endsWith("m"));
return { tokens, contextHint };
.filter(Boolean);
}
function scoreModelMatch(model: ModelInfo, tokens: string[], contextHint?: string): number {
const haystack = `${model.value} ${model.displayName}`.toLowerCase();
let score = 0;
let nonHintMatched = false;
for (const token of tokens) {
if (!haystack.includes(token)) continue;
if (token !== contextHint) nonHintMatched = true;
score += token === contextHint ? 3 : 1;
/** Whether `needle`'s words appear in `haystack`'s words, adjacent and in
* order: "opus" in "claude-opus-4-5", but not "son" in "sonnet" or "a" in
* "Default (recommended)". */
function containsWords(haystack: string, needle: string): boolean {
const words = modelWords(haystack);
const wanted = modelWords(needle);
if (wanted.length === 0) return false;
for (let start = 0; start + wanted.length <= words.length; start++) {
if (wanted.every((word, offset) => words[start + offset] === word)) return true;
}
return contextHint && !nonHintMatched ? 0 : score;
return false;
}
/** `id` without the gateway spellings that `normalizeModelId` keeps for
* `deniedModels` and gateway listings: path segments before `claude-`
* (`vertex_ai/`, `openrouter/anthropic/`, `publishers/anthropic/models/`)
* and the Anthropic API's `-latest` suffix (`claude-3-5-haiku-latest`). */
function withoutGatewaySpelling(id: string): string {
return id.replace(/^(?:[^/]+\/)+(?=claude-)/, "").replace(/-latest$/, "");
}
/** The words of `preference` that name a model: without what
* `normalizeModelId` drops (provider prefixes, dates, `-v1:0`, `-fast`,
* context hints), version numbers, "claude", and "best"/"default".
* "us.anthropic.claude-sonnet-4-5-v1:0" gives ["sonnet"], "opusplan" gives
* ["opus"]. */
function modelPreferenceTokens(preference: string): string[] {
return modelWords(withoutGatewaySpelling(normalizeModelId(preference)))
.map((token) => (token === "opusplan" ? "opus" : token))
.filter((token) => /[a-z]/.test(token) && !["claude", "best", "default"].includes(token));
}
/** Whether every one of `tokens` is a word of `model`'s value or display name. */
function modelCoversTokens(model: ModelInfo, tokens: string[]): boolean {
const words = new Set(modelWords(`${model.value} ${model.displayName}`));
return tokens.length > 0 && tokens.every((token) => words.has(token));
}
/**
* The entry of `models` that `preference` names, or null when it names none.
* `best` names the entry `opus` resolves to, unless an entry's value is `best`.
* In order: an entry whose value, display name or `resolvedModel` equals it
* (in either context-hint spelling); an entry whose value or display name
* contains its words ("opus" for "claude-opus-4-5"), or whose value it
* contains while every model word of the preference is the entry's
* ("sonnet" for "us.anthropic.claude-sonnet-5-v1:0"); then any entry with all
* of its model words, preferring one with the same context hint ("sonnet[1m]"
* for "claude-sonnet-5-1m"). The fuzzy tiers need a compatible version, and
* words match whole: "not-a-model", "son" and "foo-sonnet" name no entry.
*/
export function resolveModelPreference(models: ModelInfo[], preference: string): ModelInfo | null {
const trimmed = preference.trim();
if (!trimmed) return null;
const lower = trimmed.toLowerCase();
// The CLI resolves `best` to its flagship: Opus, unless a server-side flag
// names another family that this adapter cannot read. The CLI ignores a
// `[1m]` hint on `best`. A listed entry named `best` still matches directly.
if (stripContextHints(lower) === "best" && !models.some((model) => model.value === trimmed)) {
return resolveModelPreference(models, "opus");
}
const canonicalPreference = canonicalizeModelId(trimmed);
const directMatch = models.find(
(model) =>
@@ -134,28 +168,26 @@ export function resolveModelPreference(models: ModelInfo[], preference: string):
if (resolvedMatch) return resolvedMatch;
const preferenceHint = contextHintOf(trimmed);
const tokens = modelPreferenceTokens(trimmed);
const includesMatch = models.find((model) => {
if (!modelVersionsCompatible(trimmed, model)) return false;
if (contextHintOf(model.value) !== preferenceHint) return false;
const value = model.value.toLowerCase();
const display = model.displayName.toLowerCase();
return value.includes(lower) || display.includes(lower) || lower.includes(value);
return (
containsWords(model.value, trimmed) ||
containsWords(model.displayName, trimmed) ||
(containsWords(trimmed, model.value) && modelCoversTokens(model, tokens))
);
});
if (includesMatch) return includesMatch;
const { tokens, contextHint } = tokenizeModelPreference(trimmed);
if (tokens.length === 0) return null;
let bestMatch: ModelInfo | null = null;
let bestScore = 0;
for (const model of models) {
if (!modelVersionsCompatible(trimmed, model)) continue;
const score = scoreModelMatch(model, tokens, contextHint);
if (score > 0 && (!bestMatch || score > bestScore)) {
bestMatch = model;
bestScore = score;
}
}
return bestMatch;
const candidates = models.filter(
(model) => modelVersionsCompatible(trimmed, model) && modelCoversTokens(model, tokens),
);
return (
candidates.find((model) => contextHintOf(model.value) === preferenceHint) ??
candidates[0] ??
null
);
}
export function matchResumedModel(models: ModelInfo[], liveModel: string): ModelInfo {
@@ -281,6 +313,67 @@ function resolveSettingsModel(
const MODEL_FAMILY_ALIASES = new Set(["opus", "sonnet", "haiku", "fable"]);
/** The family and version of a `claude-<family>-<major>[-<minor>]` id:
* `claude-fable-5-1` is fable 5.1, `claude-haiku-4-5-20251001` haiku 4.5. */
function familyVersionOf(id: string): { family: string; major: number; minor: number } | null {
const match = id.toLowerCase().match(/^claude-([a-z]+)-(\d+)(?:-(\d{1,2}))?(?![\d.])/);
if (!match) return null;
return { family: match[1], major: Number(match[2]), minor: Number(match[3] ?? 0) };
}
/**
* Give back the family alias value to the row the CLI lists under a resumed
* session's model id.
*
* The CLI lists Fable 5.2 as `fable`, but when it resumes a session that ran
* Fable 5.2 it lists the same row as `claude-fable-5-2` and has no `fable`
* row. Clients store the option value as the session's choice, so a value
* from a new session would not be offered by a resumed one, and the reverse.
*
* A row gets its family's alias as its value when its `value` is
* `resumedModel`, equals its own `resolvedModel` and has no context hint, no
* row has that alias, and no row of the family has a newer version (an alias
* runs its family's newest model). Every other row is returned as it is.
*
* The CLI lists a row the same way when `ANTHROPIC_MODEL` or `settings.model`
* pins `claude-fable-5-2`. Those rows keep the pinned id, so a client's choice
* stays on that version after a newer Fable becomes `fable`. A resumed Fable
* 5.1 session keeps `claude-fable-5-1` too, because Fable 5.2 is newer.
*/
export function aliasResumedModelRow(
models: ModelInfo[],
resumedModel: string | undefined,
): ModelInfo[] {
if (resumedModel === undefined) return models;
const resumed = canonicalizeModelId(resumedModel);
const values = new Set(models.map((model) => model.value));
const versions = models.flatMap((model) => {
const version = familyVersionOf(model.resolvedModel ?? model.value);
return version ? [version] : [];
});
return models.map((model) => {
const version = familyVersionOf(model.value);
if (
version === null ||
!MODEL_FAMILY_ALIASES.has(version.family) ||
values.has(version.family) ||
model.resolvedModel !== model.value ||
contextHintOf(model.value) !== null ||
canonicalizeModelId(model.value) !== resumed ||
versions.some(
(other) =>
other.family === version.family &&
(other.major > version.major ||
(other.major === version.major && other.minor > version.minor)),
)
) {
return model;
}
values.add(version.family);
return { ...model, value: version.family };
});
}
/** A model id without the spellings that name the same model: provider
* prefixes, dates, version suffixes, `:variant` suffixes, dotted versions,
* `-fast` and context hints. `deniedModels` and gateway listings both match on
@@ -456,6 +549,61 @@ export function narrowToGatewayModels(
: [];
}
/**
* The capabilities of the session's current model, and the model infos that
* every later lookup reads.
*
* A resumed session can be running a model outside the `availableModels`
* allowlist (currentModelId is then the verbatim live id, see
* `matchResumedModel`); its capabilities are still known to the SDK's
* unfiltered list, so they are resolved from that before the model is treated
* as unknown — otherwise auto mode would be spuriously clamped and the
* Fast-mode/Effort options hidden for a model that supports them.
*
* The fallback-resolved capabilities are registered under the verbatim live
* id, so every modelInfos consumer (the effort option, later rebuilds) agrees
* with that gating. `buildModelConfigOption` lists this model as its own
* entry while it is current and drops it once the user switches to a listed
* model; it is never added to `availableModels`. The spread keeps every
* capability flag (current and future); the identity fields are overridden
* because the fuzzy-matched sibling's resolvedModel/displayName/description
* can describe a different context lane and would poison later resolvedModel
* matching (a model switch that the adapter did not make) and context-window
* inference if they traveled under this id. `allowlistedModelInfo` is the
* allowlisted entry alone, the only one whose identity strings are a
* trustworthy window signal.
*/
export function withCurrentModelInfo(
allowedModels: ModelInfo[],
sdkModels: ModelInfo[],
currentModelId: string,
): {
allowlistedModelInfo: ModelInfo | undefined;
currentModelInfo: ModelInfo | undefined;
modelInfos: ModelInfo[];
} {
const allowlistedModelInfo = allowedModels.find((m) => m.value === currentModelId);
const fallbackModelInfo = allowlistedModelInfo
? undefined
: (resolveModelPreference(sdkModels, currentModelId) ?? undefined);
return {
allowlistedModelInfo,
currentModelInfo: allowlistedModelInfo ?? fallbackModelInfo,
modelInfos: fallbackModelInfo
? [
...allowedModels,
{
...fallbackModelInfo,
value: currentModelId,
displayName: currentModelId,
description: "",
resolvedModel: undefined,
},
]
: allowedModels,
};
}
/** Whether a rejected `setModel` was vetoed by a user-configured
* PreModelSwitch hook. The CLI's rejection message is the stable,
* distinguishable marker ("Model switch blocked by a PreModelSwitch hook:

View File

@@ -50,11 +50,35 @@ export function noticeOrTranscriptUpdate(
if (supportsNotices) return noticeUpdate(notice);
return {
sessionUpdate: "agent_message_chunk",
content: { type: "text", text: transcriptText },
content: { type: "text", text: transcriptBlock(transcriptText) },
...(transcriptMeta ? { _meta: transcriptMeta } : {}),
};
}
/** Clients render consecutive agent_message_chunks as one markdown stream, so
* a bare `**Notice:** …` chunk lands mid-sentence: "…all done.**Notice:** Stop
* says: …". Put it in its own blockquote paragraph, blank lines on both sides,
* so it starts on a new line and Claude's next chunk does not continue the
* quote. */
export function transcriptBlock(text: string): string {
const lines = text.trim().split(/\r?\n/);
return `\n\n${lines.map((line) => (line ? `> ${line}` : ">")).join("\n")}\n\n`;
}
const HOOK_SPEAKER = /^[^\n:]{1,80} says: /;
/** Claude Code prefixes every line of a hook's `systemMessage` with
* "<HookEvent> says: ", so a two-line message reads "Stop says: a\nStop says:
* b". Keep the prefix on the first line only when every line repeats it. */
export function collapseHookSpeaker(text: string): string {
const lines = text.split(/\r?\n/);
const prefix = lines[0].match(HOOK_SPEAKER)?.[0];
if (!prefix || lines.length < 2 || !lines.every((line) => line.startsWith(prefix))) {
return text;
}
return [lines[0], ...lines.slice(1).map((line) => line.slice(prefix.length))].join("\n");
}
/** Fragments that read on after a bold label stand alone as a notice
* description, so start them with a capital. */
export function sentenceCase(text: string): string {
@@ -94,3 +118,98 @@ export function splitNoticeText(
}
return rest ? { title: firstLine, description: rest } : { title: firstLine };
}
/** The notice of Claude Code's `informational` message (e.g. why a Stop hook
* blocked continuation): a warning at level `warning`, else info. */
export function informationalNotice(message: { level: string; content: string }): SessionNotice {
const severity = message.level === "warning" ? "warning" : "info";
return {
severity,
...splitNoticeText(
message.content,
severity === "warning" ? "Claude reported a warning" : "Claude reported a notice",
),
};
}
/** The notice of a client MCP server that the `system/init` lists as not
* started: `failed`, or `needs-auth` when the client cannot run the OAuth
* flow (`oauthAvailable`). Undefined for any other status. */
export function mcpServerStartupNotice(
server: { name: string; status: string },
oauthAvailable: boolean,
): SessionNotice | undefined {
if (server.status === "failed") {
return {
severity: "warning",
title: `MCP server "${server.name}" failed to start`,
description: "Its tools are unavailable in this session. Run /mcp for the error.",
};
}
if (server.status === "needs-auth" && !oauthAvailable) {
return {
severity: "info",
title: `MCP server "${server.name}" needs authentication`,
description: "Its tools are unavailable until the server is authenticated.",
};
}
return undefined;
}
/**
* What Claude Code's `model_refusal_fallback` tells the user: the SDK retried
* a refused turn on the fallback model.
*
* Current CLIs only emit direction "retry", a swap that lasts for the
* session. "revert"/"sticky" are retained in the SDK enum for older CLIs,
* where "revert" marked a turn-only fallback: the session stays on the
* original model. `scope` (CLI 2.1.232+) "local" means a subagent,
* side-question or background fork response fell back and the session model
* is unchanged. Absent scope means an older CLI, where every retry was a
* session-level swap. `persistent` is whether the session now runs
* `fallback_model`, so its model option must follow.
*/
export function modelRefusalFallback(message: {
original_model: string;
fallback_model: string;
direction?: string;
scope?: string;
api_refusal_category?: string | null;
api_refusal_explanation?: string | null;
}): {
persistent: boolean;
/** The one-line summary. */
summary: string;
explanation: string | undefined;
/** The notice: the summary as its title when it is short enough, else a
* generic title with everything in the description. */
notice: SessionNotice;
/** The transcript line for a client without `notice` updates. */
transcript: string;
} {
const local = message.scope === "local";
const persistent = message.direction !== "revert" && !local;
const category = message.api_refusal_category ? ` (${message.api_refusal_category})` : "";
const outcome = persistent
? `The session will continue on ${message.fallback_model}.`
: local
? `Only that response came from ${message.fallback_model}; the session stays on ${message.original_model}.`
: `The session stays on ${message.original_model}.`;
const summary =
`${message.original_model} declined this request${category}; ` +
`retried with ${message.fallback_model}. ${outcome}`;
const explanation = message.api_refusal_explanation || undefined;
const full = explanation ? `${summary}\n\n${explanation}` : summary;
return {
persistent,
summary,
explanation,
notice: {
severity: "warning",
...(summary.length > MAX_NOTICE_TITLE_LENGTH
? { title: "Model fallback", description: full }
: { title: summary, ...(explanation ? { description: explanation } : {}) }),
},
transcript: `**Model fallback:** ${full}`,
};
}

View File

@@ -11,19 +11,42 @@
* `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` or
* `CLAUDE_CODE_DISABLE_TERMINAL_TITLE` set), the same request generates it.
*
* {@link SessionTitles} holds the per-session state and is owned by `Session`.
* `acp-agent.ts` drives it from five places: {@link SessionTitles.onPrompt} and
* {@link SessionTitles.onAssistantText} to collect text,
* {@link SessionTitles.onPromptQueued} once a prompt is sent to the CLI,
* {@link SessionTitles.onTurnEnd} at `session_state_changed: idle`, and
* {@link SessionTitles.reset} on `conversation_reset`.
* {@link SessionTitles} holds the per-session state and is owned by the
* session, in the v1 core (`v1/acp-agent.ts`) and the v2 core
* (`v2/session.ts`). Each drives it from the same places:
* {@link SessionTitles.onPrompt} and {@link SessionTitles.onAssistantText} to
* collect text, {@link SessionTitles.onPromptQueued} once a prompt is sent to
* the CLI, {@link SessionTitles.onTurnEnd} at `session_state_changed: idle`,
* and, in v1, {@link SessionTitles.reset} on `conversation_reset`.
*/
import type { ContentBlock, PromptRequest } from "@agentclientprotocol/sdk/unstable";
import type {
ContentBlock,
PromptRequest,
SessionNotification,
} from "@agentclientprotocol/sdk/unstable";
import { getSessionInfo, type Query, type SDKSessionInfo } from "@anthropic-ai/claude-agent-sdk";
import type { ClaudeAcpAgent, Session } from "./acp-agent.js";
import { ExplicitTitle } from "./session-index/explicit-title.js";
/** What titles read of their session. */
export interface TitledSession {
readonly query: Query;
readonly queryClosed?: boolean;
/** Whether a cancel stopped the session's work: no title is generated then. */
readonly cancelled?: boolean;
readonly cwd: string;
}
/** What titles need of the core that serves the session (`ClaudeAcpAgent` in v1). */
export interface TitleOwner {
readonly client: { sessionUpdate(notification: SessionNotification): Promise<void> };
readonly logger: { error: (...args: any[]) => void };
/** The live sessions by id: a session replaced or closed meanwhile adopts no title. */
readonly sessions: Readonly<Record<string, TitledSession | undefined>>;
/** The title as the client shows it, such as with an archive prefix removed. */
clientTitle?(title: string): string;
}
const MAX_TITLE_LENGTH = 256;
/** How much session text the title is generated from. The CLI caps its own
@@ -137,7 +160,7 @@ export class SessionTitles {
}
constructor(
private readonly agent: ClaudeAcpAgent,
private readonly agent: TitleOwner,
private readonly sessionId: string,
) {}
@@ -158,7 +181,7 @@ export class SessionTitles {
* prompt alone is the description, because the CLI titles from the prompt
* alone. When this yields no title, the stored summary is published and
* the next prompt tries again. */
onPromptQueued(session: Session, prompt: PromptRequest["prompt"]): void {
onPromptQueued(session: TitledSession, prompt: PromptRequest["prompt"]): void {
this.askedAtPrompt = false;
// `session.cancelled` is not checked: after a cancelled turn it stays set
// until the next turn starts, which is after this prompt is queued.
@@ -209,7 +232,7 @@ export class SessionTitles {
* turn-end must not wait on it. That covers a prompt too short to title
* from, such as "hi". Only when that is not possible do we fall back to
* `summary`, which for an SDK-driven session is just the raw first prompt. */
async onTurnEnd(session: Session): Promise<void> {
async onTurnEnd(session: TitledSession): Promise<void> {
if (this.explicit.settledByClient) {
// No title is generated over the one the client set, but a custom title
// stored since by someone else (a `/rename`) is adopted, as before.
@@ -301,7 +324,7 @@ export class SessionTitles {
* have it. Both come from the session file, as `session/list` reports
* them; a session with no file yet sends the last title published, if any,
* without `updatedAt`. */
async publishCurrent(session: Session): Promise<void> {
async publishCurrent(session: TitledSession): Promise<void> {
const info = await this.readSessionInfo(session);
// A title published but not yet stored (a generated title still being
// written) wins over `summary`, which is then only the raw first prompt.
@@ -329,7 +352,7 @@ export class SessionTitles {
/** Read the SDK's stored info for this session. A missing session file or read
* error is non-fatal: the title is best-effort and another turn will retry. */
private async readSessionInfo(session: Session): Promise<SDKSessionInfo | undefined> {
private async readSessionInfo(session: TitledSession): Promise<SDKSessionInfo | undefined> {
try {
return await getSessionInfo(this.sessionId, { dir: session.cwd });
} catch (error) {
@@ -357,7 +380,7 @@ export class SessionTitles {
/** Whether to ask the SDK for a generated title now: once per session, on a
* live query, with enough collected text for the generator to work with. */
private canRequest(session: Session): boolean {
private canRequest(session: TitledSession): boolean {
if (this.settled || session.queryClosed || session.cancelled) {
return false;
}
@@ -383,7 +406,7 @@ export class SessionTitles {
* session's title, or a `/rename`), else one generated from `description`.
* The file is read first because the CLI checks only the titles it holds in
* memory before generating one. */
private async titleFromPrompt(session: Session, description: string): Promise<void> {
private async titleFromPrompt(session: TitledSession, description: string): Promise<void> {
const info = await this.readSessionInfo(session);
if (this.agent.sessions[this.sessionId] !== session || this.explicit.active) {
return;
@@ -405,7 +428,7 @@ export class SessionTitles {
* prompt or turn end retries, and publishes `fallback` (the stored summary)
* so a backend that can't generate titles is no worse off than before. */
private async requestGenerateTitle(
session: Session,
session: TitledSession,
description: string,
fallback: TitleFallback | undefined,
): Promise<void> {

View File

@@ -25,7 +25,7 @@ export type UltracodeSettingsReport = {
};
// `Query.getSettings()` exists at runtime (sdk.mjs) but is missing from the
// SDK's type declarations as of 0.3.312.
// SDK's type declarations as of 0.3.319.
type GetSettingsQuery = Query & {
getSettings?: () => Promise<{
applied?: { ultracodeAvailable?: unknown; ultracodeRequested?: unknown };

259
src/session-usage.ts Normal file
View File

@@ -0,0 +1,259 @@
/**
* Token usage as Claude Code reports it: the context window figures of
* `usage_update` and the `_meta.quota` of a turn. Shared by the v1 core and
* the v2 core.
*/
import type { ModelUsage } from "@anthropic-ai/claude-agent-sdk";
export type AccumulatedUsage = {
inputTokens: number;
outputTokens: number;
cachedReadTokens: number;
cachedWriteTokens: number;
};
/** Per-model token tallies keyed by the model id the SDK reported them under
* (its resolved spelling, e.g. "claude-opus-5[1m]"). */
export type ModelTokenTally = Record<string, AccumulatedUsage>;
export type UsageSnapshot = {
input_tokens: number;
output_tokens: number;
cache_read_input_tokens: number;
cache_creation_input_tokens: number;
};
export const ZERO_USAGE = Object.freeze({
input_tokens: 0,
output_tokens: 0,
cache_read_input_tokens: 0,
cache_creation_input_tokens: 0,
});
export const DEFAULT_CONTEXT_WINDOW = 200000;
/** `_meta.quota` for work that spent `usage` in its main agent loop and
* `modelUsage` by model (see `turnQuotaMeta` in `v1/acp-agent.ts`). */
export function quotaMeta(usage: AccumulatedUsage, modelUsage: ModelTokenTally | undefined) {
return {
quota: {
token_count: quotaTokenCount(usage),
model_usage: Object.entries(modelUsage ?? {}).map(([model, usage]) => ({
model,
token_count: quotaTokenCount(usage),
})),
},
};
}
/** One `token_count` object. `cachedInputTokens` is cache reads, matching
* codex's field; Claude also reports cache writes, which codex has no slot
* for, so those ride along in an extra sibling (the same name the ACP `usage`
* field already uses) and are counted in `totalTokens`. `reasoningOutputTokens`
* is always 0: Claude bills thinking inside its output tokens and never breaks
* it out — the key is kept so the shape stays uniform across agents. */
function quotaTokenCount(usage: AccumulatedUsage) {
return {
totalTokens: tallyTotal(usage),
inputTokens: usage.inputTokens,
cachedInputTokens: usage.cachedReadTokens,
cachedWriteTokens: usage.cachedWriteTokens,
outputTokens: usage.outputTokens,
reasoningOutputTokens: 0,
};
}
/** Sum a tally's four counters. Per the Anthropic API `inputTokens` excludes
* the cache counters, so this is not double-counting (see `totalTokens`). */
export function tallyTotal(usage: AccumulatedUsage): number {
return usage.inputTokens + usage.outputTokens + usage.cachedReadTokens + usage.cachedWriteTokens;
}
/** Project `result.modelUsage` into our tally shape. Counters are coerced the
* way `snapshotFromUsage` coerces the stream's: third-party backends have been
* observed omitting fields, and a missing or NaN counter must not reach the
* wire as NaN (which `JSON.stringify` writes as `null`). */
export function normalizeModelUsage(
modelUsage: Record<string, ModelUsage> | undefined,
): ModelTokenTally {
const tally: ModelTokenTally = {};
for (const [model, usage] of Object.entries(modelUsage ?? {})) {
tally[model] = {
inputTokens: finiteCount(usage?.inputTokens),
outputTokens: finiteCount(usage?.outputTokens),
cachedReadTokens: finiteCount(usage?.cacheReadInputTokens),
cachedWriteTokens: finiteCount(usage?.cacheCreationInputTokens),
};
}
return tally;
}
function finiteCount(value: number | null | undefined): number {
return typeof value === "number" && Number.isFinite(value) ? value : 0;
}
/** `current - previous` per model, dropping models with nothing to report so a
* turn only lists the models it actually ran on. A reading that fell BELOW the
* previous one means the running total restarted under us (a mid-session
* /clear, a zeroed crash result): there is no usable reference left to
* subtract, so the reading itself is the increment. */
export function modelUsageIncrement(
current: ModelTokenTally,
previous: ModelTokenTally,
): ModelTokenTally {
const increment: ModelTokenTally = {};
for (const [model, usage] of Object.entries(current)) {
const base = previous[model];
const subtracted: AccumulatedUsage = base
? {
inputTokens: usage.inputTokens - base.inputTokens,
outputTokens: usage.outputTokens - base.outputTokens,
cachedReadTokens: usage.cachedReadTokens - base.cachedReadTokens,
cachedWriteTokens: usage.cachedWriteTokens - base.cachedWriteTokens,
}
: usage;
const rewound = Object.values(subtracted).some((count) => count < 0);
const resolved = rewound ? usage : subtracted;
if (tallyTotal(resolved) > 0) {
increment[model] = resolved;
}
}
return increment;
}
/** The per-model rows for the first result of a resumed session. Its
* `modelUsage` reading continues from the totals the transcript saved (SDK
* 0.3.277+; older transcripts may hold none), so there is no earlier reading
* to subtract from, and taking the reading itself would charge the whole
* pre-resume history to this one turn. The result's own `usage` is per-turn
* (main agent loop only, like the response's `usage`), so it stands in under
* the turn's top-level model — `key` is that model's `modelUsage` spelling
* when the reading has one, else the assistant message's own. Any subagent
* spend on this one turn goes unlisted rather than over-listed. */
export function resumedFirstResultModelUsage(
usage: {
input_tokens?: number | null;
output_tokens?: number | null;
cache_read_input_tokens?: number | null;
cache_creation_input_tokens?: number | null;
},
key: string | null,
): ModelTokenTally {
if (key === null) {
return {};
}
const row: AccumulatedUsage = {
inputTokens: finiteCount(usage.input_tokens),
outputTokens: finiteCount(usage.output_tokens),
cachedReadTokens: finiteCount(usage.cache_read_input_tokens),
cachedWriteTokens: finiteCount(usage.cache_creation_input_tokens),
};
return tallyTotal(row) > 0 ? { [key]: row } : {};
}
/** Fold `increment` into `base` per model — the per-model counterpart of the
* `+=` the turn's flat accumulator uses. */
export function addModelUsage(base: ModelTokenTally, increment: ModelTokenTally): ModelTokenTally {
const merged: ModelTokenTally = { ...base };
for (const [model, usage] of Object.entries(increment)) {
const existing = merged[model];
merged[model] = existing
? {
inputTokens: existing.inputTokens + usage.inputTokens,
outputTokens: existing.outputTokens + usage.outputTokens,
cachedReadTokens: existing.cachedReadTokens + usage.cachedReadTokens,
cachedWriteTokens: existing.cachedWriteTokens + usage.cachedWriteTokens,
}
: usage;
}
return merged;
}
/** Sum all four fields as a proxy for post-turn context occupancy: the current
* turn's output becomes next turn's input. Per the Anthropic API, input_tokens
* excludes cache tokens — cache_read and cache_creation are reported
* separately — so summing all four is not double-counting. */
export function totalTokens(usage: UsageSnapshot): number {
return (
usage.input_tokens +
usage.output_tokens +
usage.cache_read_input_tokens +
usage.cache_creation_input_tokens
);
}
/** Project a nullable API usage object into our non-null snapshot shape.
* Both SDK message_start and assistant message `usage` have `number | null`
* cache fields; we coerce absent values to 0 so `totalTokens` never hits
* NaN. `input_tokens`/`output_tokens` are typed `number` by the SDK but
* synthetic or third-party-backend stream events have been observed emitting
* them as null/undefined — coerce those too so a malformed upstream event
* can't leak NaN into the wire `used` field. Delta events have different
* semantics (cumulative + prev fallback) and are handled inline. */
export function snapshotFromUsage(usage: {
input_tokens?: number | null;
output_tokens?: number | null;
cache_read_input_tokens?: number | null;
cache_creation_input_tokens?: number | null;
}): UsageSnapshot {
return {
input_tokens: usage.input_tokens ?? 0,
output_tokens: usage.output_tokens ?? 0,
cache_read_input_tokens: usage.cache_read_input_tokens ?? 0,
cache_creation_input_tokens: usage.cache_creation_input_tokens ?? 0,
};
}
function commonPrefixLength(a: string, b: string) {
let i = 0;
while (i < a.length && i < b.length && a[i] === b[i]) {
i++;
}
return i;
}
/** Best-effort first guess of a model's context window, used to seed the
* window synchronously (via `immediateContextWindow`) until a `result` message
* arrives with the authoritative `modelUsage` value.
*
* Anthropic 1M-context variants encode "1m" as a distinct token in the SDK
* model ID (e.g., "claude-opus-4-6-1m"), which `\b1m\b` catches without also
* matching things like "10m" or embedded substrings. Semantic aliases like
* `default` carry no such token in the ID, but their `resolvedModel` and the
* SDK's human-facing `displayName`/`description` can (e.g.
* "claude-opus-4-8[1m]", "Opus 4.7 (1M context)"), so callers pass those too.
* This text scan can't catch every model — some resolve to extended-context
* models with no "1m" anywhere (e.g. `sonnet` → claude-sonnet-5, natively
* ~1M). Such a miss falls back to the default window, is refined by the
* background `getContextUsage` (`refreshContextWindowInBackground`), and is
* corrected by `result.modelUsage` (and cached) within one turn. */
export function inferContextWindowFromModel(...texts: Array<string | undefined>): number | null {
if (texts.some((text) => text != null && /\b1m\b/i.test(text))) return 1_000_000;
return null;
}
export function getMatchingModelUsage(
modelUsage: Record<string, ModelUsage>,
currentModel: string,
) {
let bestKey: string | null = null;
let bestLen = 0;
for (const key of Object.keys(modelUsage)) {
const len = commonPrefixLength(key, currentModel);
if (len > bestLen) {
bestLen = len;
bestKey = key;
}
}
if (bestKey) {
// `bestKey` is the SDK's resolved model id (e.g. "claude-sonnet-5[1m]"),
// the same spelling as ModelInfo.resolvedModel — the primary key the
// window is cached under. `currentModel` (the assistant message's
// `.model`) can be the bare form (e.g. "claude-sonnet-5"); the result
// handler caches under that spelling too, for seed-time reads that fall
// back to a bare id (rows without `resolvedModel`).
return { key: bestKey, usage: modelUsage[bestKey] };
}
}

124
src/slash-commands.ts Normal file
View File

@@ -0,0 +1,124 @@
/**
* The `available_commands_update` list: the slash commands Claude Code reports,
* minus the ones ACP clients can't run, plus the commands the adapter adds.
*/
import type { AvailableCommand } from "@agentclientprotocol/sdk/unstable";
import type { Query, SlashCommand } from "@anthropic-ai/claude-agent-sdk";
import { AIR_KIND_KEY, AIR_SKILL_PATH_KEY, withAirMeta } from "./air-extension.js";
import { MCP_AVAILABLE_COMMAND } from "./mcp-command.js";
import {
claudeCodeOffersCommand,
REMOTE_CONTROL_COMMAND_NAME,
supportsRemoteControl,
} from "./remote-control.js";
import { resolveSkillPath, type SkillScanCache } from "./tool-calls/reporters/interaction.js";
export function getAvailableSlashCommands(
commands: SlashCommand[],
// Names the CLI tagged terminal-bound on `system`/init (their UX lives in
// the CLI's own terminal, which ACP clients aren't) — filtered alongside
// the static list. Raw CLI names, matched before the MCP rename.
terminalCommands?: readonly string[],
// The session cwd of an AIR client, or undefined for every other client.
// AIR gets the kind and the SKILL.md path of each skill command.
airSkillCwd?: string,
// The resolved SKILL.md paths of the session, keyed by the cwd and the name.
skillPaths?: Map<string, string | undefined>,
query?: Query,
// The skill files Claude Code reports (`getContextUsage().skills.skillFrontmatter`).
// A listed skill gets its path from here; one that is not listed, or has no
// path (remote connections), is looked up on disk.
skillFiles?: ReadonlyMap<string, { path?: string }>,
): AvailableCommand[] {
// The container listings of this one pass, so a list of a hundred commands lists each container once.
const skillScans: SkillScanCache = new Map();
const skillPath = (name: string, cwd: string): string | undefined => {
const key = `${cwd}\0${name}`;
if (skillPaths?.has(key)) return skillPaths.get(key);
const resolved = resolveSkillPath(name, cwd, skillScans);
skillPaths?.set(key, resolved);
return resolved;
};
const UNSUPPORTED_COMMANDS = [
"clear",
"cost",
"keybindings-help",
"login",
"logout",
"output-style:new",
"release-notes",
"todos",
];
const remoteControlCommands: AvailableCommand[] =
query && supportsRemoteControl(query)
? [
{
name: REMOTE_CONTROL_COMMAND_NAME,
description:
"Continue this session from claude.ai/code or the Claude mobile app; `off` disconnects",
input: { hint: "[session name | off]" },
},
]
: [];
// The adapter replaces the terminal text of `/mcp` and runs its actions,
// even when Claude Code tags the command terminal-bound.
const advertised = commands
.filter((command) => command.name !== "mcp" && !terminalCommands?.includes(command.name))
.map((command) => {
const input = command.argumentHint
? {
hint: Array.isArray(command.argumentHint)
? command.argumentHint.join(" ")
: command.argumentHint,
}
: null;
let name = command.name;
const mcpPrompt = command.name.endsWith(" (MCP)");
if (mcpPrompt) {
name = `mcp:${name.replace(" (MCP)", "")}`;
}
const skillFile = airSkillCwd && !mcpPrompt ? skillFiles?.get(command.name) : undefined;
const skillMdPath =
skillFile?.path ??
(airSkillCwd && !command.builtin && !mcpPrompt
? skillPath(command.name, airSkillCwd)
: undefined);
return {
name,
description: command.description || "",
input,
...(skillMdPath
? {
_meta: withAirMeta(
withAirMeta(undefined, AIR_KIND_KEY, "skill"),
AIR_SKILL_PATH_KEY,
skillMdPath,
),
}
: skillFile
? // A skill without a file (bundled with the CLI, like `simplify`, or a remote connection): kind only.
{ _meta: withAirMeta(undefined, AIR_KIND_KEY, "skill") }
: {}),
};
})
.filter((command: AvailableCommand) => !UNSUPPORTED_COMMANDS.includes(command.name));
// Command names must not contain whitespace and must be unique. A name
// like "my command" is dropped, and of two commands named "review" only the
// first is kept.
const names = new Set<string>();
return [
...advertised,
MCP_AVAILABLE_COMMAND,
...remoteControlCommands.filter(
(command) => !claudeCodeOffersCommand(commands, terminalCommands, command.name),
),
].filter((command) => {
if (/\s/.test(command.name) || names.has(command.name)) return false;
names.add(command.name);
return true;
});
}

42
src/startup-failure.ts Normal file
View File

@@ -0,0 +1,42 @@
import { RequestError } from "@agentclientprotocol/sdk/unstable";
import type { SDKStartupFailureReason } from "@anthropic-ai/claude-agent-sdk";
/** Makes the CLI write an `error_during_execution` result with a
* `startup_failure_reason` before it exits on a known startup failure. Without
* it, some failures (e.g. `proxy_invalid`) end with stderr and an exit code
* alone. The SDK then rejects `initializationResult()` with an error that
* carries the reason as `startupFailureReason`. */
export const STARTUP_FAILURE_RESULTS_ENV = { CLAUDE_CODE_STARTUP_FAILURE_RESULTS: "1" };
/** The SDK's prefix on the message of an error built from an error result. */
const ERROR_RESULT_PREFIX = "Claude Code returned an error result: ";
/** Reasons that signing in again, or with another account, can fix. */
const AUTH_REQUIRED_REASONS: ReadonlySet<SDKStartupFailureReason> = new Set([
"gateway_signin_required",
"org_pin_mismatch",
"org_config_refused",
]);
/** The ACP error for a CLI that refused to start, or `undefined` when `error`
* carries no `startupFailureReason` (older CLIs, failures without a known
* cause). The message is the CLI's own, which says what to fix, e.g.
* `Invalid proxy URL in HTTPS_PROXY: … Fix or unset HTTPS_PROXY and restart
* Claude Code.`; `data.startupFailureReason` carries the reason. */
export function startupFailureError(error: unknown): RequestError | undefined {
if (!(error instanceof Error)) return undefined;
const reason = (error as { startupFailureReason?: unknown }).startupFailureReason;
if (typeof reason !== "string" || reason === "") return undefined;
const message = error.message.startsWith(ERROR_RESULT_PREFIX)
? error.message.slice(ERROR_RESULT_PREFIX.length)
: error.message;
const data = { startupFailureReason: reason };
if (AUTH_REQUIRED_REASONS.has(reason as SDKStartupFailureReason)) {
return RequestError.authRequired(data, message);
}
if (reason === "cwd_unavailable") {
return RequestError.invalidParams(data, message);
}
return RequestError.internalError(data, message);
}

86
src/streamed-text.ts Normal file
View File

@@ -0,0 +1,86 @@
/**
* The text and thinking of an assistant message that streamed live as
* deltas, and what its consolidated message adds to them. Shared by the v1
* core and the v2 core.
*/
import type { SDKPartialAssistantMessage } from "@anthropic-ai/claude-agent-sdk";
/** Text or thinking that streamed live as deltas, accumulated per block. */
export type StreamedBlock = { index: number; type: "text" | "thinking"; text: string };
/**
* Records the text or thinking that `event` streamed in `streamedBlocks`.
* Contiguous deltas of the same block (same index and type) extend the
* current entry; anything else opens a new one. Other events change nothing.
*/
export function recordStreamedDelta(
streamedBlocks: StreamedBlock[],
event: SDKPartialAssistantMessage["event"],
): void {
if (event.type !== "content_block_delta") return;
const delta = event.delta;
const chunk =
delta.type === "text_delta"
? { type: "text" as const, text: delta.text }
: delta.type === "thinking_delta"
? { type: "thinking" as const, text: delta.thinking }
: undefined;
// Skip empty deltas (some gateways emit empty thinking chunks — #793):
// appending "" is a no-op, but pushing a "" entry would create a block the
// `text.length > 0` guard of `unstreamedRemainder` can never consume,
// stalling the diff cursor and re-emitting the next block as a duplicate.
if (!chunk?.text) return;
const last = streamedBlocks[streamedBlocks.length - 1];
if (last && last.index === event.index && last.type === chunk.type) {
last.text += chunk.text;
} else {
streamedBlocks.push({ index: event.index, type: chunk.type, text: chunk.text });
}
}
/**
* The blocks of a consolidated assistant message without the text that
* already streamed as deltas.
*
* Each assembled text/thinking block is diffed against the streamed blocks in
* document order: nothing is left if it streamed in full (the common case),
* the whole block if it never streamed (a non-streaming gateway), and just the
* tail if the stream was cut short mid-block. Matching on content rather than
* the message id keeps the dedupe robust for gateways without a stable id.
* Tool-use and other blocks pass through untouched.
*/
export function unstreamedRemainder<Block extends { type: string }>(
blocks: Block[],
streamedBlocks: StreamedBlock[],
): Block[] {
const kept: Block[] = [];
let streamPos = 0;
for (const item of blocks) {
if (item.type !== "text" && item.type !== "thinking") {
kept.push(item);
continue;
}
const block = item as Block & { text?: string; thinking?: string };
const full = (item.type === "text" ? block.text : block.thinking) ?? "";
// Empty assembled blocks carry nothing: drop them.
if (full.length === 0) continue;
// A streamed block of the same type whose text is a prefix of this one
// was already delivered, at least partly. A non-empty streamed text is
// required so an empty or aborted streamed block does not swallow it.
const streamed = streamedBlocks[streamPos];
if (
streamed &&
streamed.type === item.type &&
streamed.text.length > 0 &&
full.startsWith(streamed.text)
) {
streamPos++;
const remainder = full.slice(streamed.text.length);
if (remainder.length === 0) continue;
kept.push({ ...item, [item.type === "text" ? "text" : "thinking"]: remainder });
continue;
}
kept.push(item);
}
return kept;
}

View File

@@ -0,0 +1,96 @@
/**
* The configuration of a subagent's session, as its hooks report it. Both
* the v1 core (`ClaudeAcpAgent.subagentConfigurationHook`, for both forms of
* subagent sessions) and the v2 core (`v2/child-config.ts`)
* build a child's config options with these.
*/
import type { SessionConfigOption, SessionModeState } from "@agentclientprotocol/sdk/unstable";
import type { AgentInfo, HookInput } from "@anthropic-ai/claude-agent-sdk";
import { childModelOption, responseModelEntry, type SubagentModels } from "./native-subagents.js";
import { AGENT_CONFIG_ID, EFFORT_CONFIG_ID } from "./session-config-ids.js";
import { effortLevelName } from "./session-effort.js";
import { SessionModeManager } from "./session-mode.js";
/** What the root session knows that a child's options name. */
export type SubagentConfigurationContext = {
/** The root session's modes, for the name of the child's mode. */
availableModes: SessionModeState["availableModes"];
/** The root session's `model` entries, for the child's model. */
models: SubagentModels | undefined;
/** The custom agents of the session, for the description of the child's agent type. */
agents: readonly AgentInfo[];
};
/**
* The config options that a hook of a subagent (`agent_id`) reports of it:
* its permission mode, its model after a `PostModelSwitch`, its effort, and
* its agent type. Each option lists only the child's current value. Empty
* for a hook of the root session, and for one that reports none of these.
*/
export function subagentHookOptions(
input: HookInput,
context: SubagentConfigurationContext,
): SessionConfigOption[] {
if (!input.agent_id) return [];
const options: SessionConfigOption[] = [];
if (input.permission_mode) {
const mode = context.availableModes.find((mode) => mode.id === input.permission_mode);
options.push(
SessionModeManager.configOption({
currentModeId: input.permission_mode,
availableModes: [mode ?? { id: input.permission_mode, name: input.permission_mode }],
}),
);
}
const switchedModel =
input.hook_event_name === "PostModelSwitch"
? responseModelEntry(input.to_model, context.models)
: undefined;
// The entry of the root session's `model` option, as for a child response.
if (switchedModel) options.push(childModelOption(switchedModel));
if (input.effort?.level) {
options.push({
id: EFFORT_CONFIG_ID,
name: "Effort",
description: "Current reasoning effort",
category: "thought_level",
type: "select",
currentValue: input.effort.level,
options: [{ value: input.effort.level, name: effortLevelName(input.effort.level) }],
});
}
if (input.agent_type) {
const agent = context.agents.find((agent) => agent.name === input.agent_type);
options.push({
id: AGENT_CONFIG_ID,
name: "Agent",
description: "Subagent type",
type: "select",
currentValue: input.agent_type,
options: [
{
value: input.agent_type,
name: input.agent_type,
...(agent?.description ? { description: agent.description } : {}),
},
],
});
}
return options;
}
/**
* A child's configuration, as it goes to the client: every option it holds,
* the mode first, then the model, the effort and the model config, then the
* rest in the order they came.
*/
export function orderedSubagentConfiguration(
options: Iterable<SessionConfigOption>,
): SessionConfigOption[] {
const priority = ["mode", "model", "thought_level", "model_config"];
const rank = (option: SessionConfigOption) => {
const index = priority.indexOf(option.category ?? "");
return index < 0 ? priority.length : index;
};
return [...options].sort((left, right) => rank(left) - rank(right));
}

View File

@@ -24,6 +24,11 @@ export interface SubagentRecord {
background: boolean;
/** Whether the user stopped it. */
stoppedByUser: boolean;
/**
* Its working directory, when Claude Code placed it outside its parent's:
* in a worktree of its own or of an ancestor, or in a `cwd` of its own.
*/
cwd?: string;
}
/** Where the subagent transcripts of a session are. */
@@ -43,7 +48,9 @@ export interface SubagentHistory {
* Claude keeps the history of a subagent in its own transcript,
* `<session>/subagents/agent-<id>.jsonl`, next to the session transcript.
* The `agent-<id>.meta.json` file beside it names the launching tool use.
* The ids are empty for a session without a local transcript.
* For a child that Claude Code placed elsewhere, it has `worktreePath`,
* `inheritedWorktreePath` or `cwd`; then the child's working directory is the
* `cwd` of its transcript's records. The ids are empty for a session without a local transcript.
*/
export async function subagentHistory(sessionId: string): Promise<SubagentHistory> {
const agents = new Map<string, SubagentRecord>();
@@ -67,6 +74,12 @@ export async function subagentHistory(sessionId: string): Promise<SubagentHistor
> | null;
if (typeof meta?.toolUseId !== "string") return;
const text = (value: unknown) => (typeof value === "string" ? value : undefined);
const placement = [meta.cwd, meta.worktreePath, meta.inheritedWorktreePath]
.map(text)
.find((value) => value && path.isAbsolute(value));
const cwd = placement
? ((await recordedCwd(path.join(directory, `agent-${match[1]!}.jsonl`))) ?? placement)
: undefined;
agents.set(meta.toolUseId, {
agentId: match[1]!,
name: text(meta.name),
@@ -74,6 +87,7 @@ export async function subagentHistory(sessionId: string): Promise<SubagentHistor
description: text(meta.description),
background: meta.requestShape === "background",
stoppedByUser: meta.stoppedByUser === true,
...(cwd ? { cwd } : {}),
});
} catch {
// A damaged meta file leaves its subagent without history.
@@ -87,6 +101,31 @@ export async function subagentHistory(sessionId: string): Promise<SubagentHistor
return dir ? { agents, dir } : { agents };
}
/**
* The `cwd` of the first record of the transcript `file` that has one, or
* undefined when the file cannot be read. Claude Code writes the working
* directory of the session or subagent on each record.
*/
async function recordedCwd(file: string): Promise<string | undefined> {
const stream = createReadStream(file, { encoding: "utf8" });
try {
for await (const line of readline.createInterface({ input: stream, crlfDelay: Infinity })) {
if (!line.includes('"cwd"')) continue;
try {
const cwd = (JSON.parse(line) as { cwd?: unknown } | null)?.cwd;
if (typeof cwd === "string" && path.isAbsolute(cwd)) return cwd;
} catch {
// A damaged record; a later one can have the `cwd`.
}
}
} catch {
// An unreadable transcript; the caller falls back to the metadata's path.
} finally {
stream.destroy();
}
return undefined;
}
/** The folder of the subagent transcripts, next to the session transcript `transcript`. */
function subagentsDirectory(transcript: string, sessionId: string): string {
return path.join(path.dirname(transcript), sessionId, "subagents");
@@ -162,6 +201,7 @@ export async function subagentLineage(
agentId: current,
launchId: launch.launchId,
...(launcher !== null ? { parentAgentId: launcher } : {}),
...(launch.record.cwd ? { cwd: launch.record.cwd } : {}),
...(launch.record.name ? { name: launch.record.name } : {}),
...(launch.record.agentType ? { agentType: launch.record.agentType } : {}),
...(launch.record.description ? { description: launch.record.description } : {}),

View File

@@ -34,6 +34,7 @@
*/
import type { SessionConfigOption } from "@agentclientprotocol/sdk/unstable";
import type { SessionMessage } from "@anthropic-ai/claude-agent-sdk";
import { statSync } from "node:fs";
import type { AcpSessionNotification, SubagentState } from "./acp-subagents.js";
import {
childModelOption,
@@ -47,6 +48,7 @@ import {
responseModelEntry,
rfdEndState,
rfdLabels,
sameDirectory,
type SubagentModels,
type WorkState,
} from "./native-subagents.js";
@@ -71,6 +73,8 @@ export type ReplayChild = {
/** The state of the child's last persisted task notification. */
notified?: SubagentState;
announced: boolean;
/** The `cwd` that the replay reported for it; undefined while it inherits its parent's. */
cwd?: string;
promptSent: boolean;
/** Whether it handed a report to its parent with a SubagentHandback call. */
handedBack?: boolean;
@@ -80,7 +84,7 @@ export type ReplayChild = {
responseModel?: string;
};
type SendMessageCall = {
export type SendMessageCall = {
toolUseId: string;
to: string;
message: string;
@@ -93,32 +97,27 @@ type SendMessageCall = {
outgoingSent: boolean;
};
export class RfdSubagentReplay {
/** The children by the id of the tool use that launched each. */
private readonly children = new Map<string, ReplayChild>();
private readonly byAgentId = new Map<string, ReplayChild>();
private readonly sendMessages: SendMessageCall[] = [];
/** Launches without metadata, which the replay leaves out. */
private readonly omitted = new Set<string>();
/** Children whose transcript could not be read: only their association is replayed. */
private withoutHistory = 0;
/** The SubagentHandback calls, with their report and whether their result failed. */
private readonly handbacks = new Map<
string,
{ message: string; failed: boolean; sent: boolean }
>();
/** A SubagentHandback call: its report, whether its result failed, and whether the replay sent it. */
type ReplayHandback = { message: string; failed: boolean; sent: boolean };
constructor(
private readonly history: SubagentHistory,
private readonly publish: Publish,
private readonly models?: SubagentModels,
/**
* Whether the replay maps a model to the session's model list as it goes.
* A load lacks the list until the CLI started, so it maps only in
* {@link currentStates}, after the load response.
*/
private readonly mapsDuringReplay = true,
) {}
/**
* What both replays read from the transcripts before they send anything: the
* children that launches with metadata started, the launches without metadata
* (left out), the SendMessage calls, and the SubagentHandback calls. It sends
* nothing; `RfdSubagentReplay` (v1) and `src/v2/subagent-replay.ts` send the
* replay from it.
*/
export class ReplayTranscript<Child extends ReplayChild = ReplayChild> {
/** The children by the id of the tool use that launched each. */
readonly children = new Map<string, Child>();
readonly byAgentId = new Map<string, Child>();
readonly sendMessages: SendMessageCall[] = [];
/** Launches without metadata, which the replay leaves out. */
readonly omitted = new Set<string>();
/** The SubagentHandback calls, by tool use id. */
readonly handbacks = new Map<string, ReplayHandback>();
constructor(private readonly history: SubagentHistory) {}
/**
* Registers the launches, the launch results, and the SendMessage calls in
@@ -159,15 +158,71 @@ export class RfdSubagentReplay {
}
}
private registerLaunch(
toolUseId: string,
input: Record<string, unknown>,
parentSessionId: string,
): void {
if (this.children.has(toolUseId)) return;
const record = this.history.agents.get(toolUseId);
if (!record) {
this.omitted.add(toolUseId);
return;
}
// A child type adds only optional fields, which a new child leaves unset.
const child = {
launchId: toolUseId,
sessionId: record.agentId,
record,
parentSessionId,
input,
announced: false,
promptSent: false,
} as Child;
this.children.set(toolUseId, child);
this.byAgentId.set(record.agentId, child);
}
}
export class RfdSubagentReplay {
private readonly transcript: ReplayTranscript;
/** Children whose transcript could not be read: only their association is replayed. */
private withoutHistory = 0;
constructor(
private readonly history: SubagentHistory,
private readonly publish: Publish,
private readonly models?: SubagentModels,
/**
* Whether the replay maps a model to the session's model list as it goes.
* A load lacks the list until the CLI started, so it maps only in
* {@link currentStates}, after the load response.
*/
private readonly mapsDuringReplay = true,
/** The working directory of the root session, which a child inherits unless it reports its own. */
private readonly rootCwd?: string,
) {
this.transcript = new ReplayTranscript(history);
}
/**
* Registers the launches, the launch results, and the SendMessage calls in
* `messages`, the transcript of the session `ownerSessionId`.
*/
scan(messages: readonly SessionMessage[], ownerSessionId: string): void {
this.transcript.scan(messages, ownerSessionId);
}
/** The child that the tool use `toolUseId` launched, when the replay shows it. */
childOfLaunch(toolUseId: string): ReplayChild | undefined {
return this.children.get(toolUseId);
return this.transcript.children.get(toolUseId);
}
/**
* Announces `child` on its parent, as live: running, with the parent's
* labels. Then it sends `firstModel`, the model of the child's first
* response, when the transcript has one.
* labels, and with its recorded `cwd` when that differs from its parent's.
* Then it sends `firstModel`, the model of the child's first response, when
* the transcript has one.
*/
async announce(child: ReplayChild, firstModel?: string): Promise<void> {
if (child.announced) return;
@@ -176,6 +231,12 @@ export class RfdSubagentReplay {
child.record.agentType ?? child.input.subagent_type,
child.record.description ?? child.input.description,
);
const inherited = this.effectiveCwd(child.parentSessionId);
const cwd =
child.record.cwd &&
(inherited === undefined || !(await sameDirectory(child.record.cwd, inherited)))
? child.record.cwd
: undefined;
await this.publish({
sessionId: child.parentSessionId,
update: {
@@ -183,13 +244,30 @@ export class RfdSubagentReplay {
sessionId: child.sessionId,
...(labels.title ? { title: labels.title } : {}),
...(labels.description ? { description: labels.description } : {}),
...(cwd ? { cwd } : {}),
state: { state: "running" },
},
});
child.cwd = cwd;
child.announced = true;
await this.responseModel(child, firstModel);
}
/**
* The working directory of the session `sessionId` as the client holds it:
* a child's reported `cwd`, else its parent's, up to the root's.
*/
private effectiveCwd(sessionId: string): string | undefined {
const seen = new Set<string>();
let child = this.transcript.byAgentId.get(sessionId);
while (child && !seen.has(child.sessionId)) {
if (child.cwd) return child.cwd;
seen.add(child.sessionId);
child = this.transcript.byAgentId.get(child.parentSessionId);
}
return this.rootCwd;
}
/**
* Takes the model of a replayed response of the child `owner`. A child
* whose model changes gets its `model` option again.
@@ -269,7 +347,9 @@ export class RfdSubagentReplay {
if (update.sessionUpdate !== "tool_call" && update.sessionUpdate !== "tool_call_update") {
return false;
}
const sendMessage = this.sendMessages.find((call) => call.toolUseId === update.toolCallId);
const sendMessage = this.transcript.sendMessages.find(
(call) => call.toolUseId === update.toolCallId,
);
if (sendMessage) {
if (sendMessage.recipientSessionId === undefined) {
sendMessage.shownAsToolCall = true;
@@ -287,7 +367,7 @@ export class RfdSubagentReplay {
}
return true;
}
const handback = owner ? this.handbacks.get(update.toolCallId) : undefined;
const handback = owner ? this.transcript.handbacks.get(update.toolCallId) : undefined;
if (owner && handback && !handback.failed) {
if (!handback.sent) {
handback.sent = true;
@@ -303,7 +383,7 @@ export class RfdSubagentReplay {
}
return true;
}
const child = this.children.get(update.toolCallId);
const child = this.transcript.children.get(update.toolCallId);
if (!child) return false;
if (
update.status === "completed" &&
@@ -322,41 +402,31 @@ export class RfdSubagentReplay {
* its task id is the agent id.
*/
taskNotification(taskId: string, status: string): boolean {
const child = this.byAgentId.get(taskId);
const child = this.transcript.byAgentId.get(taskId);
if (!child) return false;
child.notified = nativeSubagentState(status) ?? child.notified;
return true;
}
/**
* Sends the end of every announced child (see `endState`), and a notice when
* Sends the end of every announced child (see `replayEndState`), and a notice when
* subagent history is missing.
*/
async finish(supportsNotices: boolean, rootSessionId: string): Promise<void> {
for (const child of [...this.children.values()].reverse()) {
for (const child of [...this.transcript.children.values()].reverse()) {
if (!child.announced) continue;
await this.publish({
sessionId: child.parentSessionId,
update: {
sessionUpdate: "subagent_update",
sessionId: child.sessionId,
state: endState(child),
state: replayEndState(child),
},
});
}
const missing = this.omitted.size + this.withoutHistory;
const missing = this.transcript.omitted.size + this.withoutHistory;
if (missing > 0 && supportsNotices) {
await this.publish({
sessionId: rootSessionId,
update: noticeUpdate({
severity: "info",
title: "Some subagent history is not available",
description:
missing === 1
? "The history of 1 subagent of this session could not be restored."
: `The history of ${missing} subagents of this session could not be restored.`,
}),
});
await this.publish({ sessionId: rootSessionId, update: missingHistoryNotice(missing) });
}
}
@@ -370,17 +440,23 @@ export class RfdSubagentReplay {
* when a live session is loaded again. Such a child reports that state and
* can be cancelled, as live. Any other child cannot be running, because the
* process that ran it is gone, so its replayed end is its current state.
*
* A replayed `cwd` whose directory is gone, such as a worktree that Claude
* Code removed when the child finished, is cleared with `cwd: null`, as the
* RFD requires after a reconnection.
*/
currentStates(
live: (sessionId: string) => WorkState["state"] | undefined,
liveConfiguration?: (sessionId: string) => SessionConfigOption[] | undefined,
): AcpSessionNotification[] {
return [...this.children.values()]
return [...this.transcript.children.values()]
.filter((child) => child.announced)
.flatMap((child): AcpSessionNotification[] => {
const liveState = live(child.sessionId);
const state: WorkState =
liveState === undefined || liveState === "idle" ? endState(child) : { state: liveState };
liveState === undefined || liveState === "idle"
? replayEndState(child)
: { state: liveState };
// The session's model list may be known only now, after the load response.
const entry = responseModelEntry(child.responseModel, this.models);
const replayed = child.model ? JSON.stringify([childModelOption(child.model)]) : undefined;
@@ -395,6 +471,7 @@ export class RfdSubagentReplay {
sessionUpdate: "subagent_update",
sessionId: child.sessionId,
...(liveState !== undefined ? { capabilities: { cancel: {} } } : {}),
...(child.cwd && !isDirectory(child.cwd) ? { cwd: null } : {}),
state,
},
},
@@ -410,30 +487,6 @@ export class RfdSubagentReplay {
});
}
private registerLaunch(
toolUseId: string,
input: Record<string, unknown>,
parentSessionId: string,
): void {
if (this.children.has(toolUseId)) return;
const record = this.history.agents.get(toolUseId);
if (!record) {
this.omitted.add(toolUseId);
return;
}
const child: ReplayChild = {
launchId: toolUseId,
sessionId: record.agentId,
record,
parentSessionId,
input,
announced: false,
promptSent: false,
};
this.children.set(toolUseId, child);
this.byAgentId.set(record.agentId, child);
}
/** The prompt of the launch, with the launch's id, to the parent and the child, as live. */
private async sendPrompt(child: ReplayChild, text: string): Promise<void> {
child.promptSent = true;
@@ -463,12 +516,10 @@ export class RfdSubagentReplay {
message: SessionMessage,
text: string,
): Promise<void> {
const names = [child.sessionId, child.record.name, child.input.name];
const call = this.sendMessages.find(
(candidate) =>
candidate.recipientSessionId === undefined &&
names.includes(candidate.to) &&
text.includes(candidate.message.trim()),
const call = deliveredCall(
this.transcript.sendMessages,
[child.sessionId, child.record.name, child.input.name],
text,
);
if (call) {
call.recipientSessionId = child.sessionId;
@@ -530,7 +581,7 @@ export function replaySubagentTerminalState(
* `cancelled` for a child the user stopped, else the result of its foreground
* launch, else `idle` with no stop reason.
*/
function endState(child: ReplayChild): WorkState {
export function replayEndState(child: ReplayChild): WorkState {
const end = child.notified ?? (child.record.stoppedByUser ? "cancelled" : child.launchEnd);
return end ? rfdEndState(end, undefined) : { state: "idle" };
}
@@ -563,7 +614,7 @@ function inputOf(block: Record<string, unknown>): Record<string, unknown> {
}
/** The text of user message content: a string, or its text blocks. */
function textOf(content: unknown): string {
export function textOf(content: unknown): string {
if (typeof content === "string") return content;
if (!Array.isArray(content)) return "";
return content
@@ -574,7 +625,48 @@ function textOf(content: unknown): string {
.join("\n");
}
/** Whether `dir` is an existing directory. */
export function isDirectory(dir: string): boolean {
try {
return statSync(dir).isDirectory();
} catch {
return false;
}
}
/** A message that a SendMessage call delivered to the child. */
function isDelivery(message: SessionMessage): boolean {
export function isDelivery(message: SessionMessage): boolean {
return (message as { origin?: { kind?: unknown } | null }).origin?.kind === "coordinator";
}
/**
* The SendMessage call of `calls` that a delivered message `text` came from,
* for the child that `names` names (its agent id, its name). Claude Code
* frames the message for the child's model ("The coordinator sent a message
* while you were working:", then the message, then an instruction), so it is
* the first undelivered call to the child whose message the text contains.
*/
export function deliveredCall<
T extends { to: string; message: string; recipientSessionId?: string },
>(calls: readonly T[], names: readonly unknown[], text: string): T | undefined {
return calls.find(
(call) =>
call.recipientSessionId === undefined &&
names.includes(call.to) &&
text.includes(call.message.trim()),
);
}
/** The notice that the history of `missing` subagents of a replayed session is not available. */
export function missingHistoryNotice(
missing: number,
): Extract<AcpSessionNotification["update"], { sessionUpdate: "notice" }> {
return noticeUpdate({
severity: "info",
title: "Some subagent history is not available",
description:
missing === 1
? "The history of 1 subagent of this session could not be restored."
: `The history of ${missing} subagents of this session could not be restored.`,
}) as Extract<AcpSessionNotification["update"], { sessionUpdate: "notice" }>;
}

View File

@@ -1,7 +1,7 @@
/**
* The `<task-notification>` inputs that Claude Code writes to a transcript
* when a background task stops. The replay of a session (`acp-agent.ts`) and
* the session index (`session-index/background-tasks.ts`) read them here, so
* when a background task stops. The replay of a session (`v1/acp-agent.ts`) and
* the session index (`v1/session-index/background-tasks.ts`) read them here, so
* both read the same fields.
*/

View File

@@ -1,4 +1,4 @@
{"kind":"initialize","payload":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","agentFileChangeReport","nativeSubagentSessions","asyncTasks","customInstructions","recommendedValue","diffPatch","planFile","backgroundSubagents"],"goal":{"actions":["set","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"http":true,"sse":true},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{},"systemPrompt":{"mode":{"override":{}}}}},"agentInfo":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"authMethods":[],"protocolVersion":1}}
{"kind":"initialize","payload":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","agentFileChangeReport","nativeSubagentSessions","asyncTasks","customInstructions","recommendedValue","diffPatch","planFile","backgroundSubagents"],"goal":{"actions":["set","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"logout":{},"status":{}},"loadSession":true,"mcpCapabilities":{"http":true,"sse":true},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{},"systemPrompt":{"mode":{"override":{}}}}},"agentInfo":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"authMethods":[],"protocolVersion":1}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"content":{"text":"Fix the bug","type":"text"},"messageId":"00000000-0000-4000-8000-00000000c001","sessionUpdate":"user_message_chunk"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"content":{"text":"Check the file.","type":"text"},"messageId":"msg_r1","sessionUpdate":"agent_thought_chunk"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"content":{"text":"I will edit it.","type":"text"},"messageId":"msg_r1","sessionUpdate":"agent_message_chunk"}}}
@@ -12,6 +12,6 @@
{"kind":"sessionUpdate","payload":{"sessionId":"<session>:replay-subagent:toolu_r_task","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_r_task"}},"content":{"text":"Subagent text.","type":"text"},"messageId":"msg_r_sub","sessionUpdate":"agent_message_chunk"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"content":{"text":"Fixed.","type":"text"},"messageId":"msg_r2","sessionUpdate":"agent_message_chunk"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"sessionUpdate":"subagent_state_update","state":"completed","subagentSessionId":"<session>:replay-subagent:toolu_r_task"}}}
{"kind":"loadSession","payload":{"configOptions":[{"category":"mode","currentValue":"default","description":"Session permission mode","id":"mode","name":"Mode","options":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Always ask before making changes","name":"Manual","value":"default"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"_meta":{"jetbrains":{"air":{"kind":"plan","version":1}}},"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","id":"model","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"modes":{"availableModes":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Always ask before making changes","id":"default","name":"Manual"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Automatically accept all file edits","id":"acceptEdits","name":"Accept edits"},{"_meta":{"jetbrains":{"air":{"kind":"plan","version":1}}},"description":"Create a plan before making changes","id":"plan","name":"Plan"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Claude handles permission decisions","id":"auto","name":"Auto"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Accepts all permissions","id":"bypassPermissions","name":"Bypass permissions"}],"currentModeId":"default"},"sessionId":"<session>"}}
{"kind":"loadSession","payload":{"configOptions":[{"category":"mode","currentValue":"auto","description":"Session permission mode","id":"mode","name":"Mode","options":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Always ask before making changes","name":"Manual","value":"default"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"_meta":{"jetbrains":{"air":{"kind":"plan","version":1}}},"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","id":"model","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"modes":{"availableModes":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Always ask before making changes","id":"default","name":"Manual"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Automatically accept all file edits","id":"acceptEdits","name":"Accept edits"},{"_meta":{"jetbrains":{"air":{"kind":"plan","version":1}}},"description":"Create a plan before making changes","id":"plan","name":"Plan"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Claude handles permission decisions","id":"auto","name":"Auto"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Accepts all permissions","id":"bypassPermissions","name":"Bypass permissions"}],"currentModeId":"auto"},"sessionId":"<session>"}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>:replay-subagent:toolu_r_task","update":{"configOptions":[{"category":"model","currentValue":"claude-sonnet-4-6","id":"model","name":"Model","options":[{"name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionUpdate":"config_option_update"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]"},"name":"mcp"}],"sessionUpdate":"available_commands_update"}}}

View File

@@ -1,3 +1,3 @@
{"kind":"initialize","payload":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","agentFileChangeReport","nativeSubagentSessions","asyncTasks","customInstructions","recommendedValue","diffPatch","planFile","backgroundSubagents"],"goal":{"actions":["set","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"http":true,"sse":true},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{},"systemPrompt":{"mode":{"override":{}}}}},"agentInfo":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"authMethods":[],"protocolVersion":1}}
{"kind":"newSession","payload":{"configOptions":[{"category":"mode","currentValue":"default","description":"Session permission mode","id":"mode","name":"Mode","options":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Always ask before making changes","name":"Manual","value":"default"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"_meta":{"jetbrains":{"air":{"kind":"plan","version":1}}},"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","id":"model","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"modes":{"availableModes":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Always ask before making changes","id":"default","name":"Manual"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Automatically accept all file edits","id":"acceptEdits","name":"Accept edits"},{"_meta":{"jetbrains":{"air":{"kind":"plan","version":1}}},"description":"Create a plan before making changes","id":"plan","name":"Plan"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Claude handles permission decisions","id":"auto","name":"Auto"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Accepts all permissions","id":"bypassPermissions","name":"Bypass permissions"}],"currentModeId":"default"},"sessionId":"<session>"}}
{"kind":"initialize","payload":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","agentFileChangeReport","nativeSubagentSessions","asyncTasks","customInstructions","recommendedValue","diffPatch","planFile","backgroundSubagents"],"goal":{"actions":["set","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"logout":{},"status":{}},"loadSession":true,"mcpCapabilities":{"http":true,"sse":true},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{},"systemPrompt":{"mode":{"override":{}}}}},"agentInfo":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"authMethods":[],"protocolVersion":1}}
{"kind":"newSession","payload":{"configOptions":[{"category":"mode","currentValue":"auto","description":"Session permission mode","id":"mode","name":"Mode","options":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Always ask before making changes","name":"Manual","value":"default"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"_meta":{"jetbrains":{"air":{"kind":"plan","version":1}}},"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","id":"model","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"modes":{"availableModes":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Always ask before making changes","id":"default","name":"Manual"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Automatically accept all file edits","id":"acceptEdits","name":"Accept edits"},{"_meta":{"jetbrains":{"air":{"kind":"plan","version":1}}},"description":"Create a plan before making changes","id":"plan","name":"Plan"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Claude handles permission decisions","id":"auto","name":"Auto"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Accepts all permissions","id":"bypassPermissions","name":"Bypass permissions"}],"currentModeId":"auto"},"sessionId":"<session>"}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]"},"name":"mcp"}],"sessionUpdate":"available_commands_update"}}}

View File

@@ -1,15 +1,15 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[],"kind":"other","name":"AskUserQuestion","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Asking for your input","toolCallId":"toolu_ask"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"content":{"text":"Which database?","type":"text"},"type":"content"}],"rawInput":{"questions":[{"header":"Database","multiSelect":false,"options":[{"description":"Relational","label":"Postgres","preview":"CREATE TABLE t ();"},{"description":"Key-value","label":"Redis"}],"question":"Which database?"}]},"sessionUpdate":"tool_call_update","title":"Which database?","toolCallId":"toolu_ask"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"requires_action"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"requires_action"}}}
{"kind":"request","method":"elicitation/create","payload":{"message":"Which database?","mode":"form","requestedSchema":{"properties":{"question_0":{"oneOf":[{"_meta":{"_claude/askUserQuestionOption":{"preview":"CREATE TABLE t ();"}},"const":"Postgres","description":"Relational","title":"Postgres"},{"const":"Redis","description":"Key-value","title":"Redis"}],"title":"Database","type":"string"},"question_0_custom":{"description":"Type your own answer, or add a note to the option you chose above (optional).","title":"Other","type":"string"}},"type":"object"},"sessionId":"<session>","toolCallId":"toolu_ask"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"content":{"text":"User has answered your questions: \"Which database?\"=\"Postgres\".","type":"text"},"type":"content"}],"rawOutput":"User has answered your questions: \"Which database?\"=\"Postgres\".","sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_ask"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"cost":{"amount":0,"currency":"USD"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"background":[],"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}

View File

@@ -1,6 +1,6 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"background":[],"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}

View File

@@ -1,6 +1,6 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"background":[],"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}

View File

@@ -1,6 +1,6 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"background":[],"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}

View File

@@ -1,8 +1,8 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"terminal_update","terminalId":"toolu_bg"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"terminalId":"toolu_bg","type":"terminal"}],"kind":"execute","name":"Bash","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Terminal","toolCallId":"toolu_bg"}}}
@@ -15,4 +15,4 @@
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"exitStatus":{},"sessionUpdate":"terminal_update","terminalId":"toolu_bg"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_bg"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"cost":{"amount":0,"currency":"USD"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"background":[],"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}

View File

@@ -1,8 +1,8 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"terminal_update","terminalId":"toolu_bash"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"terminalId":"toolu_bash","type":"terminal"}],"kind":"execute","name":"Bash","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Terminal","toolCallId":"toolu_bash"}}}
@@ -14,4 +14,4 @@
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"exitStatus":{"exitCode":1},"sessionUpdate":"terminal_update","terminalId":"toolu_bash"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"tool_call_update","status":"failed","toolCallId":"toolu_bash"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"cost":{"amount":0,"currency":"USD"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"background":[],"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}

View File

@@ -1,8 +1,8 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"terminal_update","terminalId":"toolu_bash"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"terminalId":"toolu_bash","type":"terminal"}],"kind":"execute","name":"Bash","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Terminal","toolCallId":"toolu_bash"}}}
@@ -10,9 +10,9 @@
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"command":"ls -la"},"sessionUpdate":"tool_call_update","title":"ls -la","toolCallId":"toolu_bash"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"command":"ls -la","description":"List files"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_bash"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"requires_action"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"requires_action"}}}
{"kind":"request","method":"session/request_permission","payload":{"options":[{"kind":"allow_once","name":"Yes","optionId":"allow-once"},{"kind":"reject_once","name":"No","optionId":"reject"}],"sessionId":"<session>","subject":{"toolCall":{"content":[{"terminalId":"toolu_bash","type":"terminal"}],"kind":"execute","name":"Bash","rawInput":{"command":"ls -la","description":"List files"},"title":"ls -la","toolCallId":"toolu_bash"},"type":"tool_call"},"title":"ls -la"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"output":{"data":"YS50cwpiLnRz"},"sessionUpdate":"terminal_update","terminalId":"toolu_bash"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"exitStatus":{"exitCode":0},"sessionUpdate":"terminal_update","terminalId":"toolu_bash"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_bash"}}}
@@ -20,4 +20,4 @@
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":{"text":"ne.","type":"text"},"messageId":"msg_done","sessionUpdate":"agent_message_chunk"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"cost":{"amount":0,"currency":"USD"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"background":[],"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}

View File

@@ -1,8 +1,8 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"compactionId":"00000000-0000-4000-8000-000000000001","sessionUpdate":"compaction_update","status":"in_progress"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"compactionId":"00000000-0000-4000-8000-000000000001","error":"Context too small","sessionUpdate":"compaction_update","status":"failed"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"background":[],"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}

View File

@@ -1,11 +1,11 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"compactionId":"00000000-0000-4000-8000-000000000001","sessionUpdate":"compaction_update","status":"in_progress"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"compactionId":"00000000-0000-4000-8000-000000000001","content":{"text":"The summary.","type":"text"},"sessionUpdate":"compaction_summary_chunk"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"compactionId":"00000000-0000-4000-8000-000000000001","sessionUpdate":"compaction_update","status":"completed"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"usage_update","size":200000,"used":100}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"cost":{"amount":0,"currency":"USD"},"sessionUpdate":"usage_update","size":200000,"used":100}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"background":[],"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}

View File

@@ -1,11 +1,11 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"compactionId":"00000000-0000-4000-8000-000000000001","sessionUpdate":"compaction_update","status":"in_progress"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"compactionId":"00000000-0000-4000-8000-000000000001","content":{"text":"The summary.","type":"text"},"sessionUpdate":"compaction_summary_chunk"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"compactionId":"00000000-0000-4000-8000-000000000001","sessionUpdate":"compaction_update","status":"completed"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"usage_update","size":200000,"used":100}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"cost":{"amount":0,"currency":"USD"},"sessionUpdate":"usage_update","size":200000,"used":100}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"background":[],"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}

View File

@@ -1,8 +1,8 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[],"kind":"edit","locations":[],"name":"Edit","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Edit","toolCallId":"toolu_edit"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"locations":[{"path":"<cwd>/src/app.ts"}],"rawInput":{"file_path":"<cwd>/src/app.ts"},"sessionUpdate":"tool_call_update","title":"Edit src/app.ts","toolCallId":"toolu_edit"}}}
@@ -10,9 +10,9 @@
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"file_path":"<cwd>/src/app.ts","new_string":"value = 2","old_string":"value = 1"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_edit"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"changes":[{"fileType":"text","operation":"modify","path":"<cwd>/src/app.ts"}],"patch":{"format":"git_patch","text":"diff --git <cwd>/src/app.ts <cwd>/src/app.ts\n--- <cwd>/src/app.ts\n+++ <cwd>/src/app.ts\n@@ -1 +1 @@\n-const value = 1;\n+const value = 2;\n"},"type":"diff"}],"sessionUpdate":"tool_call_update","toolCallId":"toolu_edit"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"requires_action"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"requires_action"}}}
{"kind":"request","method":"session/request_permission","payload":{"options":[{"kind":"allow_once","name":"Yes","optionId":"allow-once"},{"kind":"reject_once","name":"No","optionId":"reject"}],"sessionId":"<session>","subject":{"toolCall":{"content":[{"changes":[{"fileType":"text","operation":"modify","path":"<cwd>/src/app.ts"}],"patch":{"format":"git_patch","text":"diff --git <cwd>/src/app.ts <cwd>/src/app.ts\n--- <cwd>/src/app.ts\n+++ <cwd>/src/app.ts\n@@ -1 +1 @@\n-const value = 1;\n+const value = 2;\n"},"type":"diff"}],"kind":"edit","locations":[{"path":"<cwd>/src/app.ts"}],"name":"Edit","rawInput":{"file_path":"<cwd>/src/app.ts","new_string":"value = 2","old_string":"value = 1"},"title":"Edit src/app.ts","toolCallId":"toolu_edit"},"type":"tool_call"},"title":"Edit src/app.ts"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"content":{"text":"```\nThe user doesn't want to proceed with this tool use. The tool use was rejected.\n```","type":"text"},"type":"content"}],"rawOutput":"The user doesn't want to proceed with this tool use. The tool use was rejected.","sessionUpdate":"tool_call_update","status":"failed","toolCallId":"toolu_edit"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"cost":{"amount":0,"currency":"USD"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"background":[],"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}

View File

@@ -1,8 +1,8 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[],"kind":"edit","locations":[],"name":"Edit","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Edit","toolCallId":"toolu_edit"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"locations":[{"path":"<cwd>/src/app.ts"}],"rawInput":{"file_path":"<cwd>/src/app.ts"},"sessionUpdate":"tool_call_update","title":"Edit src/app.ts","toolCallId":"toolu_edit"}}}
@@ -10,10 +10,10 @@
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"file_path":"<cwd>/src/app.ts","new_string":"const value = 2;","old_string":"const value = 1;"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_edit"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"changes":[{"fileType":"text","operation":"modify","path":"<cwd>/src/app.ts"}],"patch":{"format":"git_patch","text":"diff --git <cwd>/src/app.ts <cwd>/src/app.ts\n--- <cwd>/src/app.ts\n+++ <cwd>/src/app.ts\n@@ -1,3 +1,3 @@\n line 1\n-const value = 1;\n+const value = 2;\n line 3\n"},"type":"diff"}],"sessionUpdate":"tool_call_update","toolCallId":"toolu_edit"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"requires_action"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"requires_action"}}}
{"kind":"request","method":"session/request_permission","payload":{"options":[{"kind":"allow_once","name":"Yes","optionId":"allow-once"},{"kind":"reject_once","name":"No","optionId":"reject"}],"sessionId":"<session>","subject":{"toolCall":{"content":[{"changes":[{"fileType":"text","operation":"modify","path":"<cwd>/src/app.ts"}],"patch":{"format":"git_patch","text":"diff --git <cwd>/src/app.ts <cwd>/src/app.ts\n--- <cwd>/src/app.ts\n+++ <cwd>/src/app.ts\n@@ -1,3 +1,3 @@\n line 1\n-const value = 1;\n+const value = 2;\n line 3\n"},"type":"diff"}],"kind":"edit","locations":[{"path":"<cwd>/src/app.ts"}],"name":"Edit","rawInput":{"file_path":"<cwd>/src/app.ts","new_string":"const value = 2;","old_string":"const value = 1;"},"title":"Edit src/app.ts","toolCallId":"toolu_edit"},"type":"tool_call"},"title":"Edit src/app.ts"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawOutput":"The file <cwd>/src/app.ts has been updated successfully.","sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_edit"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"locations":[{"line":1,"path":"<cwd>/src/app.ts"}],"sessionUpdate":"tool_call_update","toolCallId":"toolu_edit"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"cost":{"amount":0,"currency":"USD"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"background":[],"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}

View File

@@ -1,17 +1,17 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[],"kind":"switch_mode","name":"ExitPlanMode","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Approve Plan","toolCallId":"toolu_plan"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"content":{"text":"# Plan\n1. Do it","type":"text"},"type":"content"}],"rawInput":{"plan":"# Plan\n1. Do it"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_plan"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"requires_action"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"requires_action"}}}
{"kind":"request","method":"session/request_permission","payload":{"options":[{"kind":"allow_once","name":"Yes, manually approve edits","optionId":"exit-plan-default"},{"kind":"allow_always","name":"Yes, clear context (0% used) and use auto mode","optionId":"exit-plan-clear-auto"},{"kind":"allow_always","name":"Yes, and use auto mode","optionId":"exit-plan-auto"},{"kind":"allow_always","name":"Yes, and bypass permissions","optionId":"exit-plan-bypass"},{"kind":"reject_once","name":"No, keep planning","optionId":"reject"}],"sessionId":"<session>","subject":{"toolCall":{"content":[{"content":{"text":"# Plan\n1. Do it","type":"text"},"type":"content"}],"kind":"switch_mode","name":"ExitPlanMode","rawInput":{"plan":"# Plan\n1. Do it"},"title":"Approve Plan","toolCallId":"toolu_plan"},"type":"tool_call"},"title":"Ready to code?"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionUpdate":"config_option_update"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawOutput":"User has approved your plan. You can now start coding.\n\n## Approved Plan:\n# Plan\n1. Do it","sessionUpdate":"tool_call_update","status":"completed","title":"Exited Plan Mode","toolCallId":"toolu_plan"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"plan":{"content":"# Plan\n1. Do it","planId":"plan-mode","type":"markdown"},"sessionUpdate":"plan_update"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"cost":{"amount":0,"currency":"USD"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"background":[],"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}

View File

@@ -1,15 +1,15 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[],"kind":"switch_mode","name":"ExitPlanMode","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Approve Plan","toolCallId":"toolu_plan"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"content":{"text":"# Plan\n1. Do it","type":"text"},"type":"content"}],"rawInput":{"plan":"# Plan\n1. Do it"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_plan"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"requires_action"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"requires_action"}}}
{"kind":"request","method":"session/request_permission","payload":{"options":[{"kind":"allow_once","name":"Yes, manually approve edits","optionId":"exit-plan-default"},{"kind":"allow_always","name":"Yes, clear context (0% used) and use auto mode","optionId":"exit-plan-clear-auto"},{"kind":"allow_always","name":"Yes, and use auto mode","optionId":"exit-plan-auto"},{"kind":"allow_always","name":"Yes, and bypass permissions","optionId":"exit-plan-bypass"},{"kind":"reject_once","name":"No, keep planning","optionId":"reject"}],"sessionId":"<session>","subject":{"toolCall":{"content":[{"content":{"text":"# Plan\n1. Do it","type":"text"},"type":"content"}],"kind":"switch_mode","name":"ExitPlanMode","rawInput":{"plan":"# Plan\n1. Do it"},"title":"Approve Plan","toolCallId":"toolu_plan"},"type":"tool_call"},"title":"Ready to code?"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"content":{"text":"```\n```\nThe user doesn't want to proceed with this tool use.\n```\n```","type":"text"},"type":"content"}],"rawOutput":"The user doesn't want to proceed with this tool use.","sessionUpdate":"tool_call_update","status":"failed","toolCallId":"toolu_plan"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"cost":{"amount":0,"currency":"USD"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"background":[],"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}

View File

@@ -1,6 +1,6 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"/goal Ship the feature","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"background":[],"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}

View File

@@ -1,8 +1,8 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[],"kind":"search","name":"Grep","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"grep","toolCallId":"toolu_grep"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"pattern":"value"},"sessionUpdate":"tool_call_update","title":"grep \"value\"","toolCallId":"toolu_grep"}}}
@@ -16,4 +16,4 @@
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"pattern":"**/*.ts"},"sessionUpdate":"tool_call_update","title":"Find `**/*.ts`","toolCallId":"toolu_glob"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"content":{"text":"src/app.ts\nsrc/b.ts","type":"text"},"type":"content"}],"rawOutput":"src/app.ts\nsrc/b.ts","sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_glob"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"cost":{"amount":0,"currency":"USD"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"background":[],"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}

View File

@@ -1,15 +1,15 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[],"kind":"other","name":"mcp__docs__search","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"mcp__docs__search","toolCallId":"toolu_mcp"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"q":"acp"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_mcp"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"requires_action"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"requires_action"}}}
{"kind":"request","method":"session/request_permission","payload":{"options":[{"kind":"allow_once","name":"Yes","optionId":"allow-once"},{"kind":"reject_once","name":"No","optionId":"reject"}],"sessionId":"<session>","subject":{"toolCall":{"_meta":{"claudeCode":{"mcpServer":{"name":"docs","source":"project"}}},"content":[],"kind":"other","name":"mcp__docs__search","rawInput":{"q":"acp"},"title":"mcp__docs__search","toolCallId":"toolu_mcp"},"type":"tool_call"},"title":"mcp__docs__search"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"content":{"text":"Found 2 documents.","type":"text"},"type":"content"}],"rawOutput":[{"text":"Found 2 documents.","type":"text"}],"sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_mcp"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"cost":{"amount":0,"currency":"USD"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"background":[],"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}

View File

@@ -1,8 +1,8 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"toolResponse":{"mode":"select"}}},"kind":"read","locations":[{"path":"/memory/a.md"},{"path":"/memory/b.md"}],"name":"memory_recall","sessionUpdate":"tool_call_update","status":"completed","title":"Recalled 2 memories","toolCallId":"00000000-0000-4000-8000-000000000001"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"toolResponse":{"mode":"synthesize"}}},"content":[{"content":{"text":"The user prefers tabs.","type":"text"},"type":"content"}],"kind":"read","name":"memory_recall","sessionUpdate":"tool_call_update","status":"completed","title":"Recalled synthesized memory","toolCallId":"00000000-0000-4000-8000-000000000002"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"background":[],"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}

View File

@@ -1,11 +1,11 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[],"kind":"other","name":"SandboxNetworkAccess","rawInput":{"host":"example.com"},"sessionUpdate":"tool_call_update","status":"pending","title":"SandboxNetworkAccess","toolCallId":"toolu_net"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"requires_action"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"requires_action"}}}
{"kind":"request","method":"session/request_permission","payload":{"description":"Reason: Sandbox blocks network","options":[{"kind":"allow_once","name":"Yes","optionId":"allow-once"},{"kind":"reject_once","name":"No","optionId":"reject"}],"sessionId":"<session>","subject":{"toolCall":{"content":[{"content":{"text":"```json\n{\n \"host\": \"example.com\"\n}\n```","type":"text"},"type":"content"}],"kind":"other","name":"SandboxNetworkAccess","rawInput":{"host":"example.com"},"title":"example.com","toolCallId":"toolu_net"},"type":"tool_call"},"title":"example.com"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_net"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"background":[],"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}

View File

@@ -1,8 +1,8 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[],"kind":"other","name":"NotebookEdit","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"NotebookEdit","toolCallId":"toolu_nb"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"notebook_path":"<cwd>/n.ipynb"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_nb"}}}
@@ -10,9 +10,9 @@
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"cell_id":"cell-1","new_source":"print('hi')","notebook_path":"<cwd>/n.ipynb"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_nb"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"cell_id":"cell-1","edit_mode":"replace","new_source":"print('hi')","notebook_path":"<cwd>/n.ipynb"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_nb"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"requires_action"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"requires_action"}}}
{"kind":"request","method":"session/request_permission","payload":{"options":[{"kind":"allow_once","name":"Yes","optionId":"allow-once"},{"kind":"reject_once","name":"No","optionId":"reject"}],"sessionId":"<session>","subject":{"toolCall":{"content":[],"kind":"other","name":"NotebookEdit","rawInput":{"cell_id":"cell-1","edit_mode":"replace","new_source":"print('hi')","notebook_path":"<cwd>/n.ipynb"},"title":"NotebookEdit","toolCallId":"toolu_nb"},"type":"tool_call"},"title":"NotebookEdit"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"content":{"text":"Updated cell cell-1 with print('hi')","type":"text"},"type":"content"}],"rawOutput":"Updated cell cell-1 with print('hi')","sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_nb"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"cost":{"amount":0,"currency":"USD"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"background":[],"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}

View File

@@ -1,8 +1,8 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"terminal_update","terminalId":"toolu_denied"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"terminalId":"toolu_denied","type":"terminal"}],"kind":"execute","name":"Bash","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Terminal","toolCallId":"toolu_denied"}}}
@@ -14,4 +14,4 @@
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"exitStatus":{},"sessionUpdate":"terminal_update","terminalId":"toolu_denied"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"terminalId":"toolu_denied","type":"terminal"}],"sessionUpdate":"tool_call_update","toolCallId":"toolu_denied"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"cost":{"amount":0,"currency":"USD"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"background":[],"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}

View File

@@ -1,12 +1,12 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":{"text":"Work","type":"text"},"messageId":"msg_rate","sessionUpdate":"agent_message_chunk"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":{"text":"ing.","type":"text"},"messageId":"msg_rate","sessionUpdate":"agent_message_chunk"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6","_claude/rateLimit":{"resetsAt":1700000000,"status":"allowed_warning","utilization":0.9}},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6","_claude/origin":{"kind":"human"}},"cost":{"amount":0,"currency":"USD"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"background":[],"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}

View File

@@ -1,12 +1,12 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[],"kind":"read","locations":[],"name":"Read","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Read File","toolCallId":"toolu_read"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"locations":[{"line":1,"path":"<cwd>/a.ts"}],"rawInput":{"file_path":"<cwd>/a.ts"},"sessionUpdate":"tool_call_update","title":"Read a.ts","toolCallId":"toolu_read"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"content":{"text":"```\n1\tconst a = 1;\n```","type":"text"},"type":"content"}],"rawOutput":"1\tconst a = 1;\n<system-reminder>x</system-reminder>","sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_read"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"cost":{"amount":0,"currency":"USD"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"background":[],"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}

Some files were not shown because too many files have changed in this diff Show More