mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-11-04 08:22:03 +00:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c98141201e | ||
|
|
76009f1ede | ||
|
|
fd60fc2b31 | ||
|
|
4302a4b0b6 | ||
|
|
faa04bd0ca | ||
|
|
0936ec281e | ||
|
|
9e50dfadee | ||
|
|
08a62ed537 | ||
|
|
e56f344691 | ||
|
|
43af4ec29e | ||
|
|
d7a65ce1d0 | ||
|
|
3f60921959 | ||
|
|
8e099e8442 | ||
|
|
15979bba79 | ||
|
|
8f67b6a92b | ||
|
|
5559ba890c | ||
|
|
d0604140f9 | ||
|
|
4c7b897183 | ||
|
|
8663170f18 | ||
|
|
53a0c36ce3 | ||
|
|
c91f943f86 | ||
|
|
5d35001563 | ||
|
|
51936049e1 | ||
|
|
14d06273c0 | ||
|
|
8cbaf97254 | ||
|
|
809d41c6b7 | ||
|
|
c19bddcf79 | ||
|
|
edf3af043b | ||
|
|
980ac8dc56 | ||
|
|
d9bd36d8b0 |
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -21,10 +21,10 @@ jobs:
|
||||
timeout-minutes: 5
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
cache: "npm"
|
||||
|
||||
15
.github/workflows/publish.yml
vendored
15
.github/workflows/publish.yml
vendored
@@ -3,9 +3,16 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
description: Tag or commit to publish
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
release-please:
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
runs-on: ubuntu-latest
|
||||
environment: release # Optional: for enhanced security
|
||||
permissions:
|
||||
@@ -38,15 +45,17 @@ jobs:
|
||||
name: Publish to npm
|
||||
runs-on: ubuntu-latest
|
||||
needs: [release-please]
|
||||
if: ${{ needs.release-please.outputs.release_created }}
|
||||
if: ${{ always() && (github.event_name == 'workflow_dispatch' || needs.release-please.outputs.release_created == 'true') }}
|
||||
environment: release # Optional: for enhanced security
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
with:
|
||||
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.ref }}
|
||||
# Setup .npmrc file to publish to npm
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,3 +3,4 @@ dist
|
||||
.DS_Store
|
||||
.claude/*.local.*
|
||||
CLAUDE.local.md
|
||||
.idea/
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "0.60.0"
|
||||
".": "0.64.2"
|
||||
}
|
||||
|
||||
60
CHANGELOG.md
60
CHANGELOG.md
@@ -1,5 +1,65 @@
|
||||
# Changelog
|
||||
|
||||
## [0.64.2](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.64.1...v0.64.2) (2026-08-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* restore the single-tool representation for ExitPlanMode ([#942](https://github.com/agentclientprotocol/claude-agent-acp/issues/942)) ([4302a4b](https://github.com/agentclientprotocol/claude-agent-acp/commit/4302a4b0b6df821b164cbe4857f26cf5b44b532c))
|
||||
|
||||
## [0.64.1](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.64.0...v0.64.1) (2026-08-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* release 0.65.0 ([#939](https://github.com/agentclientprotocol/claude-agent-acp/issues/939)) ([0936ec2](https://github.com/agentclientprotocol/claude-agent-acp/commit/0936ec281ec730714c605e3da732069ff47d8969))
|
||||
|
||||
## [0.64.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.63.0...v0.64.0) (2026-07-30)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **deps:** Bump actions/checkout from 7.0.0 to 7.0.1 ([#925](https://github.com/agentclientprotocol/claude-agent-acp/issues/925)) ([8e099e8](https://github.com/agentclientprotocol/claude-agent-acp/commit/8e099e844254c3e91508c79a02e3e7dc2239fcbb))
|
||||
* **deps:** Bump the minor group with 7 updates ([#928](https://github.com/agentclientprotocol/claude-agent-acp/issues/928)) ([3f60921](https://github.com/agentclientprotocol/claude-agent-acp/commit/3f609219592e63b947539f79c696b3cedb421060))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **steering:** add opt-in host-owned fallback ([#919](https://github.com/agentclientprotocol/claude-agent-acp/issues/919)) ([43af4ec](https://github.com/agentclientprotocol/claude-agent-acp/commit/43af4ec29ea5396c2614813af05967bfb0b1bac8)), closes [#903](https://github.com/agentclientprotocol/claude-agent-acp/issues/903)
|
||||
|
||||
## [0.63.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.62.0...v0.63.0) (2026-07-27)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Update to claude agent sdk v0.3.220 ([#921](https://github.com/agentclientprotocol/claude-agent-acp/issues/921)) ([4c7b897](https://github.com/agentclientprotocol/claude-agent-acp/commit/4c7b89718306229254879e5045a183233b5ed073))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Only resolve a denied tool call the client was told about ([#923](https://github.com/agentclientprotocol/claude-agent-acp/issues/923)) ([8f67b6a](https://github.com/agentclientprotocol/claude-agent-acp/commit/8f67b6a92bec24ae43b3dfbd087fe35df0531857)), closes [#918](https://github.com/agentclientprotocol/claude-agent-acp/issues/918)
|
||||
* Report tool_progress heartbeats against the tool call they describe ([#916](https://github.com/agentclientprotocol/claude-agent-acp/issues/916)) ([5559ba8](https://github.com/agentclientprotocol/claude-agent-acp/commit/5559ba890ca614cdaa189500aba65d81cc4cd51a))
|
||||
* **tools:** key Bash terminal metas off the announced tool_use id ([#917](https://github.com/agentclientprotocol/claude-agent-acp/issues/917)) ([d060414](https://github.com/agentclientprotocol/claude-agent-acp/commit/d0604140f907adbf9747f26a070690926e1de82d))
|
||||
|
||||
## [0.62.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.61.0...v0.62.0) (2026-07-24)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **deps:** Bump @hono/node-server from 1.19.14 to 1.19.15 ([#908](https://github.com/agentclientprotocol/claude-agent-acp/issues/908)) ([5193604](https://github.com/agentclientprotocol/claude-agent-acp/commit/51936049e175274de8e0fd90bc1be2af988c3ca6))
|
||||
* **deps:** Bump media-typer from 1.1.0 to 1.1.1 ([#909](https://github.com/agentclientprotocol/claude-agent-acp/issues/909)) ([5d35001](https://github.com/agentclientprotocol/claude-agent-acp/commit/5d35001563ffd702a29c3bac3b7ca134e40c77f8))
|
||||
* **deps:** Bump the minor group with 2 updates ([#900](https://github.com/agentclientprotocol/claude-agent-acp/issues/900)) ([809d41c](https://github.com/agentclientprotocol/claude-agent-acp/commit/809d41c6b7c9e7ba3cb5b206d00793a70edba64a))
|
||||
* **deps:** Bump the minor group with 2 updates ([#907](https://github.com/agentclientprotocol/claude-agent-acp/issues/907)) ([14d0627](https://github.com/agentclientprotocol/claude-agent-acp/commit/14d06273c01ad4ae944912e36700f6b5599c4c4d))
|
||||
* Update to claude-agent-sdk 0.3.218 ([#904](https://github.com/agentclientprotocol/claude-agent-acp/issues/904)) ([8cbaf97](https://github.com/agentclientprotocol/claude-agent-acp/commit/8cbaf97254576089a3b5ee6ae222fb763003c01d))
|
||||
|
||||
## [0.61.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.60.0...v0.61.0) (2026-07-22)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **deps:** Bump actions/setup-node from 6.4.0 to 7.0.0 ([#897](https://github.com/agentclientprotocol/claude-agent-acp/issues/897)) ([d9bd36d](https://github.com/agentclientprotocol/claude-agent-acp/commit/d9bd36d8b06764d63656d0387ad9430ec9fcb27d))
|
||||
* **deps:** Update to @anthropic-ai/claude-agent-sdk 0.3.217 ([#899](https://github.com/agentclientprotocol/claude-agent-acp/issues/899)) ([edf3af0](https://github.com/agentclientprotocol/claude-agent-acp/commit/edf3af043b6d00e5caca2cc81a2285c477c8b2ab))
|
||||
|
||||
## [0.60.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.59.0...v0.60.0) (2026-07-20)
|
||||
|
||||
|
||||
|
||||
12
README.md
12
README.md
@@ -12,12 +12,24 @@ This tool implements an ACP agent by using the official [Claude Agent SDK](https
|
||||
- Following
|
||||
- Edit review
|
||||
- TODO lists
|
||||
- Nested subagent transcripts
|
||||
- Interactive (and background) terminals
|
||||
- Custom [Slash commands](https://docs.anthropic.com/en/docs/claude-code/slash-commands)
|
||||
- Client MCP servers
|
||||
|
||||
Learn more about the [Agent Client Protocol](https://agentclientprotocol.com/).
|
||||
|
||||
### Nested subagent transcripts
|
||||
|
||||
ACP 1.2 has no standard subagent tool kind or nested-message relationship. Clients that can render
|
||||
nested transcripts can opt in with `clientCapabilities._meta["subagent-transcript"] = true`.
|
||||
The agent then forwards subagent text, thinking, and tool calls, relating nested updates to the
|
||||
launching Agent/Task call through `_meta.claudeCode.parentToolUseId`. Agent/Task calls are marked
|
||||
with `_meta.claudeCode.subagent = true`.
|
||||
|
||||
Clients that do not advertise the capability retain the legacy flattened behavior. In both modes,
|
||||
the normal Agent/Task tool result is preserved as the protocol-compatible fallback.
|
||||
|
||||
## Contribution Policy
|
||||
|
||||
This project does not require a Contributor License Agreement (CLA). Instead, contributions are accepted under the following terms:
|
||||
|
||||
221
examples/steering.ts
Normal file
221
examples/steering.ts
Normal file
@@ -0,0 +1,221 @@
|
||||
/**
|
||||
* Example: mid-turn steering over ACP with the `_session/steering` extension.
|
||||
*
|
||||
* "Steering" lets a client deliver a follow-up message to a turn that is still
|
||||
* running, instead of waiting for it to finish and sending a fresh
|
||||
* `session/prompt`. This is what powers "the user typed something while the
|
||||
* agent was still working" — the new message joins the in-flight turn so the
|
||||
* agent can adapt immediately (it shines in multi-step / tool-using turns,
|
||||
* where the message slots in between tool calls).
|
||||
*
|
||||
* The wire protocol has three moving parts:
|
||||
*
|
||||
* 1. The agent advertises support in its `initialize` response, at the
|
||||
* top-level `_meta.steering` (a sibling of `agentCapabilities`).
|
||||
* 2. The client calls the `_session/steering` request with `{ sessionId,
|
||||
* prompt }` while a turn is running.
|
||||
* 3. The agent replies with an `outcome`:
|
||||
* - "injected" the message joined the running turn;
|
||||
* - "startedNewTurn" the turn had already finished and the legacy
|
||||
* detached fallback was used;
|
||||
* - "promptRequired" the turn had already finished, but this request
|
||||
* explicitly opted into Host-owned prompt delivery.
|
||||
*
|
||||
* This example launches the agent as a subprocess, starts a deliberately
|
||||
* long-running prompt, and — as soon as the agent begins streaming — injects a
|
||||
* steering message and prints the outcome. All agent output is streamed to
|
||||
* stdout so you can watch the turn change course.
|
||||
*
|
||||
* Run (build the agent first so `dist/index.js` exists):
|
||||
*
|
||||
* npm run build
|
||||
* node examples/steering.ts
|
||||
*
|
||||
* (Node < 22.18 needs `node --experimental-strip-types examples/steering.ts`.)
|
||||
*
|
||||
* Override the prompts with the PROMPT / STEER env vars. Requires the agent to
|
||||
* be authenticated, since it talks to the real model.
|
||||
*/
|
||||
import { spawn } from "node:child_process";
|
||||
import { Readable, Writable } from "node:stream";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import path from "node:path";
|
||||
import {
|
||||
client as acpClient,
|
||||
methods,
|
||||
ndJsonStream,
|
||||
type PromptRequest,
|
||||
type PromptResponse,
|
||||
} from "@agentclientprotocol/sdk";
|
||||
|
||||
/** The steering extension method, per the ACP steering wire protocol. */
|
||||
const STEERING_METHOD = "_session/steering";
|
||||
|
||||
/** Params for a `_session/steering` request — the same shape as the relevant
|
||||
* subset of a `session/prompt`. */
|
||||
type SteeringRequest = {
|
||||
sessionId: string;
|
||||
prompt: Array<{ type: "text"; text: string }>;
|
||||
_meta?: { steering?: { idleBehavior?: "promptRequired" } };
|
||||
};
|
||||
|
||||
/** Result of a `_session/steering` request. `injected` means the message joined
|
||||
* the running turn; `promptRequired` means the turn had already settled, so the
|
||||
* message was NOT consumed and must be (re)sent through a normal
|
||||
* `session/prompt`. Both are successes. */
|
||||
type SteeringResponse =
|
||||
| { outcome: "injected" }
|
||||
| { outcome: "startedNewTurn" }
|
||||
| { outcome: "promptRequired"; reason: "noRunningTurn" };
|
||||
|
||||
/** The existing steering capability advertised at the top-level `_meta.steering`
|
||||
* of the `initialize` result. The idle behavior is selected per request. */
|
||||
type SteeringCapability = {
|
||||
supported?: boolean;
|
||||
};
|
||||
|
||||
// The built agent entry. Run `npm run build` first so this exists.
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const AGENT_ENTRY = process.env.AGENT_ENTRY ?? path.join(repoRoot, "dist", "index.js");
|
||||
const CWD = process.env.CWD ?? process.cwd();
|
||||
|
||||
// A deliberately long-running first prompt, and the follow-up injected while it
|
||||
// is still streaming. Override either via env vars to experiment.
|
||||
const PROMPT =
|
||||
process.env.PROMPT ??
|
||||
"Count slowly from 1 to 30, one number per line, with a short sentence of " +
|
||||
"commentary after each. Do not stop early.";
|
||||
const STEER =
|
||||
process.env.STEER ?? "Actually stop counting and instead reply with exactly one line: STEERED-OK";
|
||||
|
||||
function log(msg: string) {
|
||||
process.stderr.write(`\x1b[2m[client]\x1b[0m ${msg}\n`);
|
||||
}
|
||||
|
||||
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
async function main() {
|
||||
// An ACP client launches the agent as a subprocess and speaks JSON-RPC over
|
||||
// its stdin/stdout. stderr is inherited so the agent's own logs stay visible.
|
||||
const child = spawn(process.execPath, [AGENT_ENTRY], {
|
||||
stdio: ["pipe", "pipe", "inherit"],
|
||||
env: process.env,
|
||||
});
|
||||
child.on("error", (err) => {
|
||||
log(`failed to spawn agent (${AGENT_ENTRY}): ${err}`);
|
||||
});
|
||||
|
||||
try {
|
||||
const stream = ndJsonStream(
|
||||
Writable.toWeb(child.stdin!) as WritableStream<Uint8Array>,
|
||||
Readable.toWeb(child.stdout!) as unknown as ReadableStream<Uint8Array>,
|
||||
);
|
||||
|
||||
// Resolves the first time the agent streams assistant text — our signal that
|
||||
// the turn is genuinely underway and therefore steerable.
|
||||
let signalFirstOutput = () => {};
|
||||
const firstOutput = new Promise<void>((resolve) => (signalFirstOutput = resolve));
|
||||
|
||||
const connection = acpClient({ name: "steering-example" })
|
||||
.onNotification(methods.client.session.update, (ctx) => {
|
||||
const update = ctx.params.update;
|
||||
if (update.sessionUpdate === "agent_message_chunk" && update.content.type === "text") {
|
||||
process.stdout.write(update.content.text);
|
||||
signalFirstOutput();
|
||||
}
|
||||
})
|
||||
// Auto-approve permission prompts so the turn is never blocked on us.
|
||||
.onRequest(methods.client.session.requestPermission, (ctx) => {
|
||||
const options = ctx.params.options;
|
||||
const option = options.find((o) => o.kind === "allow_once") ?? options[0];
|
||||
return { outcome: { outcome: "selected", optionId: option.optionId } };
|
||||
})
|
||||
// Minimal file-system stubs; the example prompts don't touch files.
|
||||
.onRequest(methods.client.fs.readTextFile, () => ({ content: "" }))
|
||||
.onRequest(methods.client.fs.writeTextFile, () => ({}))
|
||||
.connect(stream);
|
||||
|
||||
try {
|
||||
const agent = connection.agent;
|
||||
|
||||
// 1. Initialize and confirm the agent advertises steering. Per the wire
|
||||
// protocol the capability lives at the TOP-LEVEL `_meta` of the initialize
|
||||
// result — a sibling of `agentCapabilities`, not nested inside it.
|
||||
const init = await agent.request(methods.agent.initialize, {
|
||||
protocolVersion: 1,
|
||||
clientCapabilities: { fs: { readTextFile: true, writeTextFile: true } },
|
||||
});
|
||||
const steering = (init._meta as { steering?: SteeringCapability } | null | undefined)
|
||||
?.steering;
|
||||
if (steering?.supported !== true) {
|
||||
throw new Error("agent does not advertise steering support");
|
||||
}
|
||||
log("agent advertises steering support");
|
||||
|
||||
// 2. Open a session.
|
||||
const { sessionId } = await agent.request(methods.agent.session.new, {
|
||||
cwd: CWD,
|
||||
mcpServers: [],
|
||||
});
|
||||
log(`session: ${sessionId}`);
|
||||
|
||||
// 3. Start a long turn, but DON'T await it yet — we need it in flight so we
|
||||
// can steer it. Its output streams through the notification handler above.
|
||||
log(`prompt: ${PROMPT}`);
|
||||
process.stdout.write("\n----- agent output -----\n");
|
||||
const turn = agent.request(methods.agent.session.prompt, {
|
||||
sessionId,
|
||||
prompt: [{ type: "text", text: PROMPT }],
|
||||
});
|
||||
|
||||
// 4. Once the turn is producing output, inject the follow-up. Wait for the
|
||||
// first streamed chunk (with a fallback) plus a beat, so the steer clearly
|
||||
// lands mid-turn.
|
||||
await Promise.race([firstOutput, delay(5000)]);
|
||||
await delay(1000);
|
||||
|
||||
process.stdout.write("\n");
|
||||
log(`steer: ${STEER}`);
|
||||
const steerRequest: SteeringRequest = {
|
||||
sessionId,
|
||||
prompt: [{ type: "text", text: STEER }],
|
||||
// Opt into the host-owned idle fallback. Without this request metadata,
|
||||
// the Adapter preserves its legacy `startedNewTurn` behavior.
|
||||
_meta: { steering: { idleBehavior: "promptRequired" } },
|
||||
};
|
||||
const result = await agent.request<SteeringResponse>(STEERING_METHOD, steerRequest);
|
||||
log(`steer outcome: ${result.outcome}`);
|
||||
|
||||
if (result.outcome === "promptRequired") {
|
||||
// The target turn already settled, so steering did not consume the message.
|
||||
// Start a normal session/prompt on the same session to deliver it — that
|
||||
// request owns the continuation's updates and terminal response.
|
||||
log(`steer fallback: ${result.reason}; starting a normal session/prompt`);
|
||||
const continuationRequest: PromptRequest = {
|
||||
sessionId: steerRequest.sessionId,
|
||||
prompt: steerRequest.prompt,
|
||||
};
|
||||
const continuation = await agent.request<PromptResponse, PromptRequest>(
|
||||
methods.agent.session.prompt,
|
||||
continuationRequest,
|
||||
);
|
||||
log(`continuation stopReason: ${continuation.stopReason}`);
|
||||
}
|
||||
|
||||
// 5. Await the original turn. With outcome "injected" the steer already
|
||||
// reshaped the output above; the promptRequired branch owns its own turn.
|
||||
const response = await turn;
|
||||
log(`original turn stopReason: ${response.stopReason}`);
|
||||
process.stdout.write("\n----- end of agent output -----\n");
|
||||
} finally {
|
||||
connection.close();
|
||||
}
|
||||
} finally {
|
||||
child.kill();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
log(`fatal: ${err?.stack ?? err}`);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
410
package-lock.json
generated
410
package-lock.json
generated
@@ -1,32 +1,32 @@
|
||||
{
|
||||
"name": "@agentclientprotocol/claude-agent-acp",
|
||||
"version": "0.60.0",
|
||||
"version": "0.64.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@agentclientprotocol/claude-agent-acp",
|
||||
"version": "0.60.0",
|
||||
"version": "0.64.2",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "1.2.1",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.3.215",
|
||||
"@agentclientprotocol/sdk": "1.3.0",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.3.220",
|
||||
"zod": "^3.25.0 || ^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"claude-agent-acp": "dist/index.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@anthropic-ai/sdk": "0.112.3",
|
||||
"@anthropic-ai/sdk": "0.115.0",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@tsconfig/node22": "22.0.5",
|
||||
"@types/node": "26.1.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.64.0",
|
||||
"@typescript-eslint/parser": "8.64.0",
|
||||
"eslint": "10.7.0",
|
||||
"@types/node": "26.1.2",
|
||||
"@typescript-eslint/eslint-plugin": "8.65.0",
|
||||
"@typescript-eslint/parser": "8.65.0",
|
||||
"eslint": "10.8.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"globals": "17.7.0",
|
||||
"prettier": "3.9.5",
|
||||
"globals": "17.8.0",
|
||||
"prettier": "3.9.6",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "6.0.3",
|
||||
"vitest": "4.1.10"
|
||||
@@ -36,31 +36,31 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@agentclientprotocol/sdk": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-1.2.1.tgz",
|
||||
"integrity": "sha512-jwYUdOQR7tc+Zfch53VL4JJyUNK/46q03uUTYb+PjECsmnNl94XFXOfYLJ8RBpMNidXd1rpOAVgb0vqD98xImA==",
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-1.3.0.tgz",
|
||||
"integrity": "sha512-i3h/efaeuMUFAO1HSfo97QZQnnvMd7wWBYtBsdL6UMZg3a78sk3Ffya5Xu7C7tYsXomXoDXJBAzQF2PcFKAhIQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"zod": "^3.25.0 || ^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk": {
|
||||
"version": "0.3.215",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.215.tgz",
|
||||
"integrity": "sha512-fBktJCwfu8ZeOSnnSLcWVkIHyp/pjouJsGVGtRnQ0HkcRuOReIbRcB/6n2O5dYV331Ok+MfdGHiPaTORj7pCtQ==",
|
||||
"version": "0.3.220",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.220.tgz",
|
||||
"integrity": "sha512-glc7SdwPkOkLw8oxwLo9PKTdLJGqW/PIR4urWXFoRtX9YllwozsEVc5Tc1+EvLSkfrsxPJqQWqOgpjUOQXf1oA==",
|
||||
"license": "SEE LICENSE IN README.md",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.215",
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.215",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.215",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.215",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.215",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.215",
|
||||
"@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.215",
|
||||
"@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.215"
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.220",
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.220",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.220",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.220",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.220",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.220",
|
||||
"@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.220",
|
||||
"@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.220"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@anthropic-ai/sdk": ">=0.93.0",
|
||||
@@ -69,9 +69,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-darwin-arm64": {
|
||||
"version": "0.3.215",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.215.tgz",
|
||||
"integrity": "sha512-KIOe3N/ypVIdsI7fnJUHT0Djei1RH01TbxK6LI2mgoHKZ6NVDt/Q9kQ1+On3b/l899RhE6C1SMAiQ0iB4sbkjA==",
|
||||
"version": "0.3.220",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.220.tgz",
|
||||
"integrity": "sha512-7VxlbEosK7DODiOnsjoVd0DSJzbnaPrM2jelMHI0y8zx1UnLS3WC6EFUXbvy74F2sXqEznh2tzn7EKWInaRN6Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -82,9 +82,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-darwin-x64": {
|
||||
"version": "0.3.215",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.215.tgz",
|
||||
"integrity": "sha512-cyjfQgkgF/zZbSJP6uJpPXoIJE/gYFOgz+u/SjklvakcO56BpEnsbdl/oHVu5G8GCw69V11hKFZ8T8YsVOiv2w==",
|
||||
"version": "0.3.220",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.220.tgz",
|
||||
"integrity": "sha512-X9RwDsSmbF6ultKZroaip+DL8WRgC64gHbrAwrRlAFSPNZV7zmJyP2ur8rW7KrxqmtuehdMMkw8+SAC/6hD2PA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -95,9 +95,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64": {
|
||||
"version": "0.3.215",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.215.tgz",
|
||||
"integrity": "sha512-lb7ayxZeWLiEhlOjzF8oX+DdJHrVgR1hvwkwRdYo+LgTT3hSxv6h43sheS3hZ1I9LYvOTKWa2E0O1EnffOzyCg==",
|
||||
"version": "0.3.220",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.220.tgz",
|
||||
"integrity": "sha512-WkROPwWskqhKR9XgnmseHQ6rLi9zM9qt57IWoToIjL/eXOqDWipp7JXZ1L5ud+LrA42dunHPZfBwD/vXZ+A7LA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -111,9 +111,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64-musl": {
|
||||
"version": "0.3.215",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.215.tgz",
|
||||
"integrity": "sha512-dXMR8afbZCFWUKQGyvn9swDYYvMtZWGqqbZ994ahS7HvGN4AgF63uvojHVErW1Xxn2601Bm+eCYQt+BSLk6iZQ==",
|
||||
"version": "0.3.220",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.220.tgz",
|
||||
"integrity": "sha512-OHoZOZ8Cf2TBr6oXIXPwyvUxj9jrq2w8E4poA8dMpacXszcPSPiCQCMuuOh4aWJzfeJE1+TtWxhKMVb2csXyZQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -127,9 +127,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-linux-x64": {
|
||||
"version": "0.3.215",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.215.tgz",
|
||||
"integrity": "sha512-Iivu3oq7hIEc81dpTu1W0GJ/kCE8TRtj9tb+wdZCp1grsuGEJiS3Bh1tLxCrhLOTDxPggK6xZGVN/RPJh84ywA==",
|
||||
"version": "0.3.220",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.220.tgz",
|
||||
"integrity": "sha512-tkTJFnpR9VifvWX2fmkCAPkT6+8Wk/gVu8B5jsVekKZPiZoWRHmMXO30BnZn+f0TZhgYP+82PSX3S8crH1kn+w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -143,9 +143,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-linux-x64-musl": {
|
||||
"version": "0.3.215",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.215.tgz",
|
||||
"integrity": "sha512-jSmrjSupnWtw8/I1Wmr32J6lB02gFXytmNbse4kcLEgH6UGtdZckKnDVnh/ejHma4QE9WGSn54xLsSFnjHVrRg==",
|
||||
"version": "0.3.220",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.220.tgz",
|
||||
"integrity": "sha512-K+FWj+LcGhC1Z7wqeWoLxm1iemcba5xKpLLFVwYm4V6HyMx3ruYd/2r2TiQtjT+JWeNFWIys0ScHiItR6vWAiA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -159,9 +159,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-win32-arm64": {
|
||||
"version": "0.3.215",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.215.tgz",
|
||||
"integrity": "sha512-9Xtpwd4DzfObOycDkfYfQ7clhWoFu2fmRqmMZWyQRyk2mEHWR3gLBwUgpfjYb6m4Fke9eTmIx2lXDG2i1OaBzA==",
|
||||
"version": "0.3.220",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.220.tgz",
|
||||
"integrity": "sha512-rIwgq0UwQExWl6KrHUyC4w5KwpL9l6nd95aUTx6RitexaAuEw//xtfTVLnuE4hDDQZFkzEwpdKc3nxDWoGcUbA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -172,9 +172,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-win32-x64": {
|
||||
"version": "0.3.215",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.215.tgz",
|
||||
"integrity": "sha512-DkPwdc1zS6KIf6YNKXhlqAw95wnjG8Uh5bSXRrFA+MxsaWedlVFeja6zFM5uZ84ouGrcHEeU3PGUcGaKR1U4cQ==",
|
||||
"version": "0.3.220",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.220.tgz",
|
||||
"integrity": "sha512-MuOuXhbr66HlGaWXD2f3w0k2PsvmnbkwcUZ0dAe2poFLdl72GC2dapwwOBefxm9QmoNqk9+jmv/dSKGOVWyvLw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -185,9 +185,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/sdk": {
|
||||
"version": "0.112.3",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.112.3.tgz",
|
||||
"integrity": "sha512-wjcozJlitVIuBEw9cj/xBuRznwkhcLmXmNzlFoeHbh4AvrDG3HGZrdvEOTTmobcbhjGkfOpKbmDTCQ4s9LQvCg==",
|
||||
"version": "0.115.0",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.115.0.tgz",
|
||||
"integrity": "sha512-BJrFIVyjNuU8lfDyIJTvlRYzgQg+zEl78BxE7fq8esULsGz9IRQvGtW5spq3tydmtjQb/GFdooKGdGsetpx+lQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"json-schema-to-ts": "^3.1.1",
|
||||
@@ -228,43 +228,46 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
|
||||
"integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
|
||||
"version": "2.0.0-alpha.3",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-2.0.0-alpha.3.tgz",
|
||||
"integrity": "sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.2.2",
|
||||
"@emnapi/wasi-threads": "2.0.1",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
|
||||
"integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
|
||||
"version": "2.0.0-alpha.3",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-2.0.0-alpha.3.tgz",
|
||||
"integrity": "sha512-hFPAhMUjJD9BSyCANEISPOogeXC9Zo9ZQl7L6vKnaVsMkCtzznaW/naYypeyl0Gv5rYfWYsZbpixTMpjDJzQeA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
|
||||
"integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-2.0.1.tgz",
|
||||
"integrity": "sha512-9DsSk+o5NBX0CCJT8s0EROGSGxjR/tKu6aBTaVyq+SjAEQH4XcdcRxPBRzsBLizTTJ49MJjF+jgu3qnO9GLQcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/eslint-utils": {
|
||||
"version": "4.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
|
||||
"integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
|
||||
"version": "4.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz",
|
||||
"integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -306,9 +309,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/config-helpers": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz",
|
||||
"integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==",
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz",
|
||||
"integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -377,9 +380,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@hono/node-server": {
|
||||
"version": "1.19.14",
|
||||
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz",
|
||||
"integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==",
|
||||
"version": "1.19.17",
|
||||
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.17.tgz",
|
||||
"integrity": "sha512-dSneS5qhiauZWGDCeK4o695Xd9nUNjviSZCMQrj10eetr8Uln1ucn6bbphOM6UynAMMtNIzZNSpL9vnASJwrPQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
@@ -484,13 +487,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/sdk": {
|
||||
"version": "1.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz",
|
||||
"integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==",
|
||||
"version": "1.30.0",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz",
|
||||
"integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^1.19.9",
|
||||
"@hono/node-server": "^1.19.9 || ^2.0.5",
|
||||
"ajv": "^8.17.1",
|
||||
"ajv-formats": "^3.0.1",
|
||||
"content-type": "^1.0.5",
|
||||
@@ -525,22 +528,25 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz",
|
||||
"integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==",
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.0.tgz",
|
||||
"integrity": "sha512-kDoONqMa+VnZ4vvvu/ZUurpJ4gkZU57e7g69qpNgWhYcZFPUHZM2CEMKm+cG6ufDVALbjMvfmMjFVqaK7uEMnA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@tybys/wasm-util": "^0.10.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || >=23.5.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emnapi/core": "^1.7.1",
|
||||
"@emnapi/runtime": "^1.7.1"
|
||||
"@emnapi/core": "^2.0.0-alpha.3",
|
||||
"@emnapi/runtime": "^2.0.0-alpha.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-project/types": {
|
||||
@@ -794,6 +800,40 @@
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
|
||||
"integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.2.2",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
|
||||
"integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
|
||||
"integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz",
|
||||
@@ -934,9 +974,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "26.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz",
|
||||
"integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==",
|
||||
"version": "26.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.2.tgz",
|
||||
"integrity": "sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -944,17 +984,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.64.0.tgz",
|
||||
"integrity": "sha512-CGvQPBxN3wZLu6Rz2kFUpZeoCm78xUic92ck39KPePkO1NPOwjCqdQnm5Q87tpWw9vcBvW8XLrDXjH9PWYtJ3Q==",
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz",
|
||||
"integrity": "sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.12.2",
|
||||
"@typescript-eslint/scope-manager": "8.64.0",
|
||||
"@typescript-eslint/type-utils": "8.64.0",
|
||||
"@typescript-eslint/utils": "8.64.0",
|
||||
"@typescript-eslint/visitor-keys": "8.64.0",
|
||||
"@typescript-eslint/scope-manager": "8.65.0",
|
||||
"@typescript-eslint/type-utils": "8.65.0",
|
||||
"@typescript-eslint/utils": "8.65.0",
|
||||
"@typescript-eslint/visitor-keys": "8.65.0",
|
||||
"ignore": "^7.0.5",
|
||||
"natural-compare": "^1.4.0",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
@@ -967,22 +1007,22 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.64.0",
|
||||
"@typescript-eslint/parser": "^8.65.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.64.0.tgz",
|
||||
"integrity": "sha512-KA0OshtlcCCXmbfqyZkM5pV3/WNraJf7DkJRLpyrmwPtud57H5BDX7C3k0LPSPxpprfRL+cJDGabF10mvNCoCw==",
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.65.0.tgz",
|
||||
"integrity": "sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.64.0",
|
||||
"@typescript-eslint/types": "8.64.0",
|
||||
"@typescript-eslint/typescript-estree": "8.64.0",
|
||||
"@typescript-eslint/visitor-keys": "8.64.0",
|
||||
"@typescript-eslint/scope-manager": "8.65.0",
|
||||
"@typescript-eslint/types": "8.65.0",
|
||||
"@typescript-eslint/typescript-estree": "8.65.0",
|
||||
"@typescript-eslint/visitor-keys": "8.65.0",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -998,14 +1038,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.64.0.tgz",
|
||||
"integrity": "sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg==",
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz",
|
||||
"integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.64.0",
|
||||
"@typescript-eslint/types": "^8.64.0",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.65.0",
|
||||
"@typescript-eslint/types": "^8.65.0",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1020,14 +1060,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.64.0.tgz",
|
||||
"integrity": "sha512-CXEaFdYXjSTgKhisNkwCcJwTP8Pl+fmRrEQrri4nm3vU743bALrxzLmq7fHG/7e6a5xO0lDYeURpZmBuhHk54w==",
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz",
|
||||
"integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.64.0",
|
||||
"@typescript-eslint/visitor-keys": "8.64.0"
|
||||
"@typescript-eslint/types": "8.65.0",
|
||||
"@typescript-eslint/visitor-keys": "8.65.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1038,9 +1078,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.64.0.tgz",
|
||||
"integrity": "sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw==",
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz",
|
||||
"integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1055,15 +1095,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.64.0.tgz",
|
||||
"integrity": "sha512-XWG4Fmmv/6SvyS9nH8jWrKs6terwJvE8cyRt1CzYYqzp9OrPhCT4cMc/f7C6RZCwG+qMmiffJS1/qJP8G1URtg==",
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.65.0.tgz",
|
||||
"integrity": "sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.64.0",
|
||||
"@typescript-eslint/typescript-estree": "8.64.0",
|
||||
"@typescript-eslint/utils": "8.64.0",
|
||||
"@typescript-eslint/types": "8.65.0",
|
||||
"@typescript-eslint/typescript-estree": "8.65.0",
|
||||
"@typescript-eslint/utils": "8.65.0",
|
||||
"debug": "^4.4.3",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
},
|
||||
@@ -1080,9 +1120,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.64.0.tgz",
|
||||
"integrity": "sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==",
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz",
|
||||
"integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1094,16 +1134,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.64.0.tgz",
|
||||
"integrity": "sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA==",
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz",
|
||||
"integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.64.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.64.0",
|
||||
"@typescript-eslint/types": "8.64.0",
|
||||
"@typescript-eslint/visitor-keys": "8.64.0",
|
||||
"@typescript-eslint/project-service": "8.65.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.65.0",
|
||||
"@typescript-eslint/types": "8.65.0",
|
||||
"@typescript-eslint/visitor-keys": "8.65.0",
|
||||
"debug": "^4.4.3",
|
||||
"minimatch": "^10.2.2",
|
||||
"semver": "^7.7.3",
|
||||
@@ -1122,16 +1162,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.64.0.tgz",
|
||||
"integrity": "sha512-aJUGVB3+U0htrrCjoA8qukw8cm8fNCGAxK/tVoS70k8aeb7DETKeFozRiVFIwEeN9WJLsjaP3ph8I60tY2XZoQ==",
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.65.0.tgz",
|
||||
"integrity": "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.9.1",
|
||||
"@typescript-eslint/scope-manager": "8.64.0",
|
||||
"@typescript-eslint/types": "8.64.0",
|
||||
"@typescript-eslint/typescript-estree": "8.64.0"
|
||||
"@typescript-eslint/scope-manager": "8.65.0",
|
||||
"@typescript-eslint/types": "8.65.0",
|
||||
"@typescript-eslint/typescript-estree": "8.65.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1146,13 +1186,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.64.0.tgz",
|
||||
"integrity": "sha512-mrtuL8Nsn6gi2H4mo5KMTp823M+3Q19Ew/i+Zlikq20tIMm99C3Ez0dCmkWWnxut20esQvTg8aUSEhMcAOXhEw==",
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz",
|
||||
"integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.64.0",
|
||||
"@typescript-eslint/types": "8.65.0",
|
||||
"eslint-visitor-keys": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1304,9 +1344,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.17.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
|
||||
"integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
|
||||
"version": "8.18.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz",
|
||||
"integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@@ -1441,16 +1481,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "5.0.7",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
|
||||
"integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==",
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
|
||||
"integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
@@ -1741,9 +1781,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "10.7.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.7.0.tgz",
|
||||
"integrity": "sha512-GVTD7s1vdIl6UYvAfriOPeY1Df8LIZjfofLvHwde+erDHGGuHyuM6xoxRxmHiebhYuD2p1vN4wWh0XzPARSGDQ==",
|
||||
"version": "10.8.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.0.tgz",
|
||||
"integrity": "sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
@@ -1753,7 +1793,7 @@
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.2",
|
||||
"@eslint/config-array": "^0.23.5",
|
||||
"@eslint/config-helpers": "^0.6.0",
|
||||
"@eslint/config-helpers": "^0.7.0",
|
||||
"@eslint/core": "^1.2.1",
|
||||
"@eslint/plugin-kit": "^0.7.2",
|
||||
"@humanfs/node": "^0.16.6",
|
||||
@@ -1777,7 +1817,7 @@
|
||||
"imurmurhash": "^0.1.4",
|
||||
"is-glob": "^4.0.0",
|
||||
"json-stable-stringify-without-jsonify": "^1.0.1",
|
||||
"minimatch": "^10.2.4",
|
||||
"minimatch": "^10.2.5",
|
||||
"natural-compare": "^1.4.0",
|
||||
"optionator": "^0.9.3"
|
||||
},
|
||||
@@ -2069,9 +2109,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/express-rate-limit": {
|
||||
"version": "8.6.0",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.0.tgz",
|
||||
"integrity": "sha512-XKJXDsASUOo0LLtFwW5hCcQGH0N4WQc/Rn8/Pvoia+TJFOkkFPvrtW9lZOeeNcxQJspvOIERMwiRLsVFlhHEkA==",
|
||||
"version": "8.6.1",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.1.tgz",
|
||||
"integrity": "sha512-0D493aP61w0TJ2A0wy27riRsO7FMQ7FK+KUHOKCSfPvYo0R55aiC6emCVgFUeShH0fq0ICPVzNcgoS+BsbXQCA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
@@ -2216,9 +2256,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/flatted": {
|
||||
"version": "3.4.2",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
|
||||
"integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.3.tgz",
|
||||
"integrity": "sha512-/zipXxyO6rGvuNGDiULY9MvEGSkb2gaG4GGH4ygMi0ZZzyMHdUZBmntJmx5x1G2VuPytCwGN4xsJP6cw+sK+vQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
@@ -2320,9 +2360,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/globals": {
|
||||
"version": "17.7.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-17.7.0.tgz",
|
||||
"integrity": "sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==",
|
||||
"version": "17.8.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-17.8.0.tgz",
|
||||
"integrity": "sha512-Zz/LMDZScFmkakeL2cTHzf+PbWKdpU3uclqkZT7TjDG58j5WPt0PpA+n9uPI24fZtlw07q0OtEi84K+umsRzqQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -2372,9 +2412,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/hono": {
|
||||
"version": "4.12.31",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.31.tgz",
|
||||
"integrity": "sha512-zJIHFrl6bq3RDd2YusFNCDlM8qUprxKswyi/OPzPyzKDdyBXDqWx8bZlZ7R+saTdSTatUmb3O7K4SspGPaEOQg==",
|
||||
"version": "4.12.32",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.32.tgz",
|
||||
"integrity": "sha512-XcuyW9qE2kJn07PkecMOBd5Vq/hMy7mmGw+idz1yblbg9N17ijJODrvPkn7/dwL3Kulj8LcRJ69DLOWf91dRUg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
@@ -2447,9 +2487,9 @@
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "10.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
|
||||
"integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==",
|
||||
"version": "10.3.1",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.3.1.tgz",
|
||||
"integrity": "sha512-1e9d3kb97NHJTIJDZW9rKqW2h6+dFa50Dy0fpPSMQp2ADje5gvKsXmdiK6dwY5t76TaTt5+P5N1Y/LoToIxP6g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
@@ -2503,9 +2543,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/jose": {
|
||||
"version": "6.2.3",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz",
|
||||
"integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==",
|
||||
"version": "6.2.4",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.4.tgz",
|
||||
"integrity": "sha512-N8acGzVsQy6M/fjFcxtysNc4Q379TcM5dM/qKkNtsHFji88yANnXTr7BLeP75iPnFwBfQzM/jg2BZ9+HZrHCZA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
@@ -2894,13 +2934,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/media-typer": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
|
||||
"integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz",
|
||||
"integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-descriptors": {
|
||||
@@ -2944,13 +2988,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "10.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
|
||||
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
|
||||
"version": "10.2.6",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz",
|
||||
"integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^5.0.5"
|
||||
"brace-expansion": "^5.0.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
@@ -3189,9 +3233,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.20",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.20.tgz",
|
||||
"integrity": "sha512-lW616l85ucIQL+FocMmL7pQFPqBmwejrCMg+iPxyImlrANNJG9NHq/RkyCZopDhd8C3LA03PHRJDjkbGu8vvug==",
|
||||
"version": "8.5.24",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.24.tgz",
|
||||
"integrity": "sha512-8RyVklq0owXUTa4xlpzu4l9AaVKIdQvAcOHZWaMh98HgySsUtxRVf/chRe3dsSLqb6i40BzGRzEUddRaI+9TSw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -3228,9 +3272,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.9.5",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.5.tgz",
|
||||
"integrity": "sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg==",
|
||||
"version": "3.9.6",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz",
|
||||
"integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@@ -3632,9 +3676,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tinyrainbow": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
|
||||
"integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz",
|
||||
"integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
||||
20
package.json
20
package.json
@@ -3,7 +3,7 @@
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"version": "0.60.0",
|
||||
"version": "0.64.2",
|
||||
"description": "An ACP-compatible coding agent powered by the Claude Agent SDK (TypeScript)",
|
||||
"main": "dist/lib.js",
|
||||
"types": "dist/lib.d.ts",
|
||||
@@ -63,21 +63,21 @@
|
||||
"node": ">=22"
|
||||
},
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "1.2.1",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.3.215",
|
||||
"@agentclientprotocol/sdk": "1.3.0",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.3.220",
|
||||
"zod": "^3.25.0 || ^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@anthropic-ai/sdk": "0.112.3",
|
||||
"@anthropic-ai/sdk": "0.115.0",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@tsconfig/node22": "22.0.5",
|
||||
"@types/node": "26.1.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.64.0",
|
||||
"@typescript-eslint/parser": "8.64.0",
|
||||
"eslint": "10.7.0",
|
||||
"@types/node": "26.1.2",
|
||||
"@typescript-eslint/eslint-plugin": "8.65.0",
|
||||
"@typescript-eslint/parser": "8.65.0",
|
||||
"eslint": "10.8.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"globals": "17.7.0",
|
||||
"prettier": "3.9.5",
|
||||
"globals": "17.8.0",
|
||||
"prettier": "3.9.6",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "6.0.3",
|
||||
"vitest": "4.1.10"
|
||||
|
||||
635
src/acp-agent.ts
635
src/acp-agent.ts
@@ -60,6 +60,7 @@ import {
|
||||
CanUseTool,
|
||||
deleteSession,
|
||||
EffortLevel,
|
||||
FastModeDisabledReason,
|
||||
FastModeState,
|
||||
getSessionInfo,
|
||||
getSessionMessages,
|
||||
@@ -192,6 +193,78 @@ const TURN_NO_RESULT_MESSAGE =
|
||||
"The turn ended without a result: the agent went idle while this prompt was still in flight " +
|
||||
"(e.g. the model stream dropped mid-turn). Any partial output may be incomplete; please retry.";
|
||||
|
||||
/** Custom (extension) request method a client uses to steer the turn that is
|
||||
* currently running: the message is injected into the in-flight turn rather
|
||||
* than queued as a separate `session/prompt`. Named `_session/steering` per the
|
||||
* agreed ACP steering wire protocol; advertised to clients via the top-level
|
||||
* `InitializeResponse._meta.steering.supported`. */
|
||||
const STEER_METHOD = "_session/steering";
|
||||
|
||||
/** How urgently the SDK delivers a steered message relative to the running
|
||||
* turn — an internal Claude implementation detail, not part of the wire
|
||||
* contract. `now` pre-empts the current generation and handles the message
|
||||
* immediately (interrupting a single-shot response, or slotting in between a
|
||||
* multi-step turn's tool calls). Maps to `SDKUserMessage.priority`; injected
|
||||
* steering always uses `now` so the running turn adapts as soon as possible. */
|
||||
const STEER_PRIORITY = "now" as const;
|
||||
|
||||
/** Request-level steering options. `promptRequired` is opt-in so existing Hosts
|
||||
* keep the established idle fallback behavior. */
|
||||
type SteerMeta = {
|
||||
[key: string]: unknown;
|
||||
steering?: {
|
||||
idleBehavior?: "promptRequired";
|
||||
};
|
||||
};
|
||||
|
||||
/** Params of a {@link STEER_METHOD} request. Shaped like the relevant subset of
|
||||
* a `PromptRequest` so the same `promptToClaude` conversion applies. Delivery
|
||||
* priority is deliberately NOT exposed here — it's an internal detail the agent
|
||||
* chooses (see {@link STEER_PRIORITY}). */
|
||||
export type SteerRequest = {
|
||||
sessionId: string;
|
||||
prompt: PromptRequest["prompt"];
|
||||
_meta?: SteerMeta | null;
|
||||
};
|
||||
|
||||
/** Result of a {@link STEER_METHOD} request. The legacy `startedNewTurn` result
|
||||
* remains the default idle behavior; `promptRequired` is returned only when the
|
||||
* Host explicitly opts into the host-owned fallback in request `_meta`. */
|
||||
export type SteerResponse =
|
||||
| { outcome: "injected" }
|
||||
| { outcome: "startedNewTurn" }
|
||||
| { outcome: "promptRequired"; reason: "noRunningTurn" };
|
||||
|
||||
/** Validate raw JSON-RPC params into a {@link SteerRequest}. Kept minimal — the
|
||||
* content blocks are handed to `promptToClaude`, which tolerates unknown block
|
||||
* types — but `sessionId` and a non-empty `prompt` array are required. */
|
||||
function parseSteerRequest(params: unknown): SteerRequest {
|
||||
if (!params || typeof params !== "object") {
|
||||
throw RequestError.invalidParams(undefined, "steer params must be an object");
|
||||
}
|
||||
const { sessionId, prompt, _meta } = params as Record<string, unknown>;
|
||||
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
||||
throw RequestError.invalidParams(undefined, "steer params require a non-empty sessionId");
|
||||
}
|
||||
if (!Array.isArray(prompt) || prompt.length === 0) {
|
||||
throw RequestError.invalidParams(undefined, "steer params require a non-empty prompt array");
|
||||
}
|
||||
const steering =
|
||||
_meta && typeof _meta === "object" ? (_meta as Record<string, unknown>).steering : undefined;
|
||||
const idleBehavior =
|
||||
steering && typeof steering === "object"
|
||||
? (steering as Record<string, unknown>).idleBehavior
|
||||
: undefined;
|
||||
if (idleBehavior !== undefined && idleBehavior !== "promptRequired") {
|
||||
throw RequestError.invalidParams(undefined, "unsupported steering idleBehavior");
|
||||
}
|
||||
return {
|
||||
sessionId,
|
||||
prompt: prompt as PromptRequest["prompt"],
|
||||
_meta: _meta as SteerMeta | null | undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/** Internal model-selection state. Mirrors the shape the ACP SDK exposed as
|
||||
* `SessionModelState` before model selection moved entirely into
|
||||
* `SessionConfigOption` (category "model"). Retained internally to track the
|
||||
@@ -378,6 +451,13 @@ type Session = {
|
||||
* user's intent so it persists across model switches; the Fast mode config
|
||||
* option is only surfaced while the selected model supports it. */
|
||||
fastModeEnabled: boolean;
|
||||
/** Why the SDK currently can't serve Fast mode, when the reason is one worth
|
||||
* telling the user about (see {@link FAST_MODE_UNAVAILABLE_EXPLANATIONS} —
|
||||
* routine states like the SDK's own opt-in requirement normalize to
|
||||
* `undefined`). Refreshed from every `fast_mode_disabled_reason` the SDK
|
||||
* reports on `system`/init and user-turn `result`s; surfaced in the Fast mode
|
||||
* option's description so a toggle that snaps back off explains itself. */
|
||||
fastModeDisabledReason?: FastModeDisabledReason;
|
||||
abortController: AbortController;
|
||||
/** Signal the consumer races `query.next()` against. Aborted by cancel()
|
||||
* (after a grace period) to force the active turn to settle "cancelled" when
|
||||
@@ -391,6 +471,9 @@ type Session = {
|
||||
* cancel. */
|
||||
forceCancelTimer?: ReturnType<typeof setTimeout>;
|
||||
emitRawSDKMessages: boolean | SDKMessageFilter[];
|
||||
/** Whether nested subagent text/thinking is forwarded to the ACP client.
|
||||
* Enabled by either the ACP capability or the pre-existing SDK option. */
|
||||
forwardSubagentText: boolean;
|
||||
/** Context window size of the session's current model, carried across
|
||||
* prompts so mid-stream usage_update notifications report a correct `size`
|
||||
* before the turn's first result message arrives. Seeded synchronously at
|
||||
@@ -738,12 +821,28 @@ export type ToolUpdateMeta = {
|
||||
claudeCode?: {
|
||||
/* The name of the tool that was used in Claude Code. */
|
||||
toolName: string;
|
||||
/* A human-readable title supplied by Claude Code for the tool call. */
|
||||
title?: string;
|
||||
/* The structured output provided by Claude Code. */
|
||||
toolResponse?: unknown;
|
||||
/* For a tool call made inside a subagent: the tool_use id of the
|
||||
Agent/Task call that spawned the subagent. Mirrors the SDK's
|
||||
`parent_tool_use_id` on streamed subagent messages. */
|
||||
parentToolUseId?: string;
|
||||
/* On a "failed" tool_call_update: why the tool never actually ran, so a
|
||||
client can render the denial/cancellation distinctly from a real tool
|
||||
failure. From the SDK's `tool_result_meta` non_execution_kind:
|
||||
"user-rejected", "permission-rule", "interrupted", "cancelled", …
|
||||
(open set). Absent when the tool executed — including real failures. */
|
||||
nonExecutionKind?: string;
|
||||
/* Free-text the user supplied when rejecting the tool call, when the
|
||||
harness collected any. Only ever present alongside nonExecutionKind. */
|
||||
userFeedback?: string;
|
||||
/* Marks Agent/Task tool calls as subagent launches. ACP 1.2 has no
|
||||
standard subagent ToolKind yet, so clients that support nested
|
||||
transcripts need a namespaced marker instead of inferring from
|
||||
`toolName` or the generic `think` kind. */
|
||||
subagent?: true;
|
||||
};
|
||||
/* Terminal metadata for Bash tool execution, matching codex-acp's _meta protocol. */
|
||||
terminal_info?: {
|
||||
@@ -760,6 +859,28 @@ export type ToolUpdateMeta = {
|
||||
};
|
||||
};
|
||||
|
||||
const SUBAGENT_TRANSCRIPT_CAPABILITY = "subagent-transcript";
|
||||
|
||||
function supportsSubagentTranscript(capabilities?: ClientCapabilities | null): boolean {
|
||||
return capabilities?._meta?.[SUBAGENT_TRANSCRIPT_CAPABILITY] === true;
|
||||
}
|
||||
|
||||
function parentToolUseIdOf(message: { parent_tool_use_id?: unknown }): string | null {
|
||||
if (!("parent_tool_use_id" in message)) return null;
|
||||
return typeof message.parent_tool_use_id === "string" ? message.parent_tool_use_id : null;
|
||||
}
|
||||
|
||||
function stripSubagentTextAndThinking(content: unknown): unknown {
|
||||
if (!Array.isArray(content)) return content;
|
||||
return content.filter(
|
||||
(item) =>
|
||||
!item ||
|
||||
typeof item !== "object" ||
|
||||
!("type" in item) ||
|
||||
(item.type !== "text" && item.type !== "thinking"),
|
||||
);
|
||||
}
|
||||
|
||||
export type ToolUseCache = {
|
||||
[key: string]: {
|
||||
type: "tool_use" | "server_tool_use" | "mcp_tool_use";
|
||||
@@ -1073,48 +1194,122 @@ export function resolvePermissionMode(
|
||||
return mapped;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the label for the "Always Allow" permission option so the user can see
|
||||
* the exact scope they are committing to. Uses the SDK-provided suggestions
|
||||
* when available (e.g. `Bash(npm test:*)`) and falls back to naming the whole
|
||||
* tool so "Always Allow" is never a blank check without disclosure.
|
||||
*/
|
||||
export function describeAlwaysAllow(
|
||||
function permissionLifetime(destination: PermissionUpdate["destination"]): Record<string, string> {
|
||||
switch (destination) {
|
||||
case "session":
|
||||
return { scope: "session" };
|
||||
case "cliArg":
|
||||
return { scope: "process", storage: "cli_argument" };
|
||||
case "userSettings":
|
||||
return { scope: "persistent", storage: "user" };
|
||||
case "projectSettings":
|
||||
return { scope: "persistent", storage: "project" };
|
||||
case "localSettings":
|
||||
return { scope: "persistent", storage: "project_local" };
|
||||
default:
|
||||
return { scope: "unknown" };
|
||||
}
|
||||
}
|
||||
|
||||
function permissionMetadataForAlwaysAllow(
|
||||
suggestions: PermissionUpdate[] | undefined,
|
||||
toolName: string,
|
||||
): string {
|
||||
if (!suggestions || suggestions.length === 0) {
|
||||
return `Always Allow all ${toolName}`;
|
||||
}
|
||||
): Record<string, unknown> {
|
||||
const effectiveSuggestions =
|
||||
suggestions && suggestions.length > 0
|
||||
? suggestions
|
||||
: [
|
||||
{
|
||||
type: "addRules" as const,
|
||||
rules: [{ toolName }],
|
||||
behavior: "allow" as const,
|
||||
destination: "session" as const,
|
||||
},
|
||||
];
|
||||
const changes: Array<Record<string, unknown>> = [];
|
||||
|
||||
const ruleLabels: string[] = [];
|
||||
const directories: string[] = [];
|
||||
|
||||
for (const update of suggestions) {
|
||||
if (update.type === "addRules" && update.behavior === "allow") {
|
||||
for (const rule of update.rules) {
|
||||
ruleLabels.push(
|
||||
rule.ruleContent ? `${rule.toolName}(${rule.ruleContent})` : `all ${rule.toolName}`,
|
||||
);
|
||||
for (const update of effectiveSuggestions) {
|
||||
switch (update.type) {
|
||||
case "addRules":
|
||||
case "removeRules":
|
||||
case "replaceRules": {
|
||||
const operation =
|
||||
update.type === "addRules" ? "add" : update.type === "removeRules" ? "remove" : "replace";
|
||||
const targets = update.rules.map((rule) => ({
|
||||
type: "tool",
|
||||
toolName: rule.toolName,
|
||||
...(rule.ruleContent
|
||||
? {
|
||||
matcher: {
|
||||
type: "provider_rule",
|
||||
provider: "claudeCode",
|
||||
value: rule.ruleContent,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
}));
|
||||
const renderedRules = update.rules
|
||||
.map((rule) =>
|
||||
rule.ruleContent
|
||||
? `${rule.toolName} calls matching ${rule.ruleContent}`
|
||||
: `all ${rule.toolName} calls`,
|
||||
)
|
||||
.join(", ");
|
||||
const verb =
|
||||
operation === "add"
|
||||
? update.behavior === "allow"
|
||||
? "Allow"
|
||||
: update.behavior === "deny"
|
||||
? "Deny"
|
||||
: "Ask before"
|
||||
: operation === "remove"
|
||||
? `Remove ${update.behavior} rules for`
|
||||
: `Replace ${update.behavior} rules with`;
|
||||
changes.push({
|
||||
type: "policy_rule",
|
||||
operation,
|
||||
ruleBehavior: update.behavior,
|
||||
description: `${verb} ${renderedRules}`,
|
||||
lifetime: permissionLifetime(update.destination),
|
||||
targets,
|
||||
});
|
||||
break;
|
||||
}
|
||||
} else if (update.type === "addDirectories") {
|
||||
directories.push(...update.directories);
|
||||
case "addDirectories":
|
||||
case "removeDirectories": {
|
||||
const operation = update.type === "addDirectories" ? "add" : "remove";
|
||||
changes.push({
|
||||
type: "policy_rule",
|
||||
operation,
|
||||
ruleBehavior: "allow",
|
||||
description:
|
||||
operation === "add"
|
||||
? `Allow filesystem access under ${update.directories.join(", ")}`
|
||||
: `Remove additional filesystem access under ${update.directories.join(", ")}`,
|
||||
lifetime: permissionLifetime(update.destination),
|
||||
targets: update.directories.map((path) => ({
|
||||
type: "filesystem",
|
||||
matcher: { type: "directory", path },
|
||||
})),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "setMode":
|
||||
changes.push({
|
||||
type: "permission_mode",
|
||||
operation: "set",
|
||||
provider: "claudeCode",
|
||||
mode: update.mode,
|
||||
description: `Set Claude Code permission mode to ${update.mode}`,
|
||||
lifetime: permissionLifetime(update.destination),
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const parts: string[] = [];
|
||||
if (ruleLabels.length > 0) {
|
||||
parts.push(ruleLabels.join(", "));
|
||||
}
|
||||
if (directories.length > 0) {
|
||||
parts.push(`access to ${directories.join(", ")}`);
|
||||
}
|
||||
|
||||
if (parts.length === 0) {
|
||||
return `Always Allow all ${toolName}`;
|
||||
}
|
||||
|
||||
return `Always Allow ${parts.join(" and ")}`;
|
||||
return { version: 1, changes };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1368,6 +1563,14 @@ export class ClaudeAcpAgent {
|
||||
...terminalAuthMethods,
|
||||
...(supportsGatewayAuth ? [gatewayAuthMethod, gatewayBedrockAuthMethod] : []),
|
||||
],
|
||||
// Top-level `_meta` (sibling of `agentCapabilities`), per the existing ACP
|
||||
// steering extension contract: advertises the `_session/steering` request
|
||||
// so clients know they may inject a follow-up into a running turn.
|
||||
_meta: {
|
||||
steering: {
|
||||
supported: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1665,6 +1868,75 @@ export class ClaudeAcpAgent {
|
||||
return response;
|
||||
}
|
||||
|
||||
/** Steer the session per the ACP steering wire protocol: inject a follow-up
|
||||
* message into the turn that is currently running. If that turn already
|
||||
* settled, the established default starts a new detached turn; Hosts may opt
|
||||
* into the host-owned `promptRequired` fallback through request `_meta`.
|
||||
*
|
||||
* When a turn is in flight this injects (returns `injected`): unlike
|
||||
* `prompt()`, it does NOT create a Turn or enqueue on `turnQueue`; it pushes
|
||||
* an `SDKUserMessage` onto the same streaming input, which the SDK routes
|
||||
* into the in-flight turn. The injected message's echo carries a uuid that
|
||||
* matches no queued turn, so the consumer drops it as an unrelated replay
|
||||
* without promoting/settling anything. It is delivered at {@link
|
||||
* STEER_PRIORITY} (`now`) so it pre-empts the current generation (interrupting
|
||||
* a single-shot response, or slotting in between a multi-step turn's tool
|
||||
* calls). The steered message's own output streams via `session/update`, not
|
||||
* this response.
|
||||
*
|
||||
* When the session is idle, the opt-in path returns `promptRequired` WITHOUT
|
||||
* calling `prompt()`, pushing SDK input, or mutating `turnQueue`: the content
|
||||
* stays Host-owned so the Host can submit it through a standard
|
||||
* `session/prompt`. Without the opt-in, the existing detached `prompt()` and
|
||||
* `startedNewTurn` result are preserved for compatibility. */
|
||||
async steer(params: SteerRequest): Promise<SteerResponse> {
|
||||
const sessionId = params.sessionId;
|
||||
const session = this.sessions[sessionId];
|
||||
if (!session) {
|
||||
throw new Error("Session not found");
|
||||
}
|
||||
if (session.queryClosed) {
|
||||
throw RequestError.internalError(undefined, SESSION_ENDED_MESSAGE);
|
||||
}
|
||||
|
||||
// "A turn is running" = the queue holds an unsettled turn. This covers both
|
||||
// the activated turn and one just submitted but not yet echoed/activated,
|
||||
// which is exactly the window in which steering is meaningful. This check
|
||||
// and the active-path push below stay in one synchronous section so the
|
||||
// turn cannot settle in the gap between deciding to inject and enqueueing.
|
||||
const turnInFlight = (session.turnQueue ?? []).some((turn) => !turn.settled);
|
||||
if (!turnInFlight) {
|
||||
const promptRequest: PromptRequest = {
|
||||
sessionId,
|
||||
prompt: params.prompt,
|
||||
};
|
||||
if (params._meta?.steering?.idleBehavior === "promptRequired") {
|
||||
// The opt-in path leaves the content untouched so the Host can retry via
|
||||
// a normal session/prompt whose lifecycle owns the continuation result.
|
||||
return { outcome: "promptRequired", reason: "noRunningTurn" };
|
||||
}
|
||||
|
||||
// Preserve the established default for Hosts that do not opt in. This is
|
||||
// intentionally detached for compatibility with the existing contract.
|
||||
this.prompt(promptRequest).catch((error) => {
|
||||
this.logger.error(`Session ${sessionId}: steered new turn failed: ${error}`);
|
||||
});
|
||||
return { outcome: "startedNewTurn" };
|
||||
}
|
||||
|
||||
const promptRequest: PromptRequest = {
|
||||
sessionId,
|
||||
prompt: params.prompt,
|
||||
};
|
||||
const userMessage = promptToClaude(promptRequest);
|
||||
userMessage.uuid = randomUUID();
|
||||
// Deliver into the running turn rather than queuing behind it as a fresh
|
||||
// prompt would.
|
||||
userMessage.priority = STEER_PRIORITY;
|
||||
session.input.push(userMessage);
|
||||
return { outcome: "injected" };
|
||||
}
|
||||
|
||||
/** Lazily start the per-session consumer that drains the SDK query stream for
|
||||
* the session's whole life. Idempotent: only the first `prompt()` starts it. */
|
||||
private ensureConsumer(session: Session, sessionId: string): void {
|
||||
@@ -2326,7 +2598,12 @@ export class ClaudeAcpAgent {
|
||||
// A fresh `system`/init (e.g. after reinitialize) can carry an
|
||||
// updated Fast mode state; reconcile it with what we seeded at
|
||||
// session creation.
|
||||
await this.syncFastModeState(message.session_id, session, message.fast_mode_state);
|
||||
await this.syncFastModeState(
|
||||
message.session_id,
|
||||
session,
|
||||
message.fast_mode_state,
|
||||
message.fast_mode_disabled_reason,
|
||||
);
|
||||
break;
|
||||
case "status": {
|
||||
// These banners count as delivered text (via sendUpdate), so
|
||||
@@ -2552,9 +2829,9 @@ export class ClaudeAcpAgent {
|
||||
// Push the full slash-command list after a mid-session change
|
||||
// (e.g. skills discovered dynamically as the agent works in a
|
||||
// subdirectory). The client should REPLACE its cached command
|
||||
// list with this payload: supportedCommands() is captured once
|
||||
// at initialize and never reflects mid-session changes, so we
|
||||
// forward message.commands directly rather than re-querying.
|
||||
// list with this payload. Forward message.commands directly —
|
||||
// it's authoritative, and re-querying supportedCommands()
|
||||
// would just return the same list with an extra round-trip.
|
||||
await sendUpdate({
|
||||
sessionId: message.session_id,
|
||||
update: {
|
||||
@@ -2580,6 +2857,28 @@ export class ClaudeAcpAgent {
|
||||
// already emitted as a `tool_call`, so mark it failed with the
|
||||
// rejection reason — otherwise the client shows a tool call
|
||||
// that silently never resolves.
|
||||
//
|
||||
// The id is the executing call's own, and the frame lands
|
||||
// between its `tool_use` and its `tool_result` (the SDK enqueues
|
||||
// it from inside canUseTool), so the call is normally in flight
|
||||
// here. Not always: the assistant message carrying the tool_use
|
||||
// is dropped by the cancelled-turn guard below, and a denial for
|
||||
// it can still arrive afterwards — the case the `tool_result`
|
||||
// fallback in `toAcpNotifications` gates on `wasEmitted` for.
|
||||
// Drop the update rather than reference a tool call the client
|
||||
// was never given (see `ensureToolCallEmitted`, issue #851).
|
||||
if (!session.emittedToolCalls.has(message.tool_use_id)) {
|
||||
break;
|
||||
}
|
||||
// A denial inside a subagent identifies the subagent by
|
||||
// `agent_id` (as canUseTool does with `agentID`), never by the
|
||||
// Agent/Task call that spawned it. Resolve it the same way so
|
||||
// the update lands in the subagent's transcript alongside the
|
||||
// `tool_call` it resolves, which carries the parent stamped from
|
||||
// `parent_tool_use_id` (see `liveBackgroundTasks`).
|
||||
const parentToolUseId = message.agent_id
|
||||
? session.liveBackgroundTasks.get(message.agent_id)?.parentToolUseId
|
||||
: undefined;
|
||||
const reason = message.decision_reason ?? message.message;
|
||||
await sendUpdate({
|
||||
sessionId: message.session_id,
|
||||
@@ -2596,6 +2895,7 @@ export class ClaudeAcpAgent {
|
||||
_meta: {
|
||||
claudeCode: {
|
||||
toolName: message.tool_name,
|
||||
...(parentToolUseId ? { parentToolUseId } : {}),
|
||||
toolResponse: {
|
||||
decisionReasonType: message.decision_reason_type,
|
||||
decisionReason: message.decision_reason,
|
||||
@@ -2836,7 +3136,12 @@ export class ClaudeAcpAgent {
|
||||
// an autonomous cycle's state lands on the next user turn's
|
||||
// result. Runs even when the turn errors or was cancelled.
|
||||
if (!isAutonomousResult) {
|
||||
await this.syncFastModeState(params.sessionId, session, message.fast_mode_state);
|
||||
await this.syncFastModeState(
|
||||
params.sessionId,
|
||||
session,
|
||||
message.fast_mode_state,
|
||||
message.fast_mode_disabled_reason,
|
||||
);
|
||||
}
|
||||
|
||||
// A user-turn result needs an active turn so its stop reason is
|
||||
@@ -3521,11 +3826,15 @@ export class ClaudeAcpAgent {
|
||||
// Consumed: reset so the next message's blocks accumulate fresh and
|
||||
// the record stays bounded to the in-flight message.
|
||||
streamedBlocks.length = 0;
|
||||
} else if (message.type === "assistant") {
|
||||
// Subagent assistant message (`parent_tool_use_id !== null`). It is
|
||||
// never streamed live and its text/thinking is internal to the tool
|
||||
// call — keep dropping it so subagent prose doesn't leak into the
|
||||
// top-level feed.
|
||||
} else if (
|
||||
message.type === "assistant" &&
|
||||
!(session.forwardSubagentText || supportsSubagentTranscript(this.clientCapabilities))
|
||||
) {
|
||||
// Legacy clients don't understand nested transcripts. Keep the
|
||||
// historical behavior for them: subagent text/thinking remains
|
||||
// internal to the tool call instead of leaking into the top-level
|
||||
// feed. Capable clients opt into the branch above unchanged, with
|
||||
// `parentToolUseId` stamped by toAcpNotifications.
|
||||
content = message.message.content.filter(
|
||||
(item) => item.type !== "text" && item.type !== "thinking",
|
||||
);
|
||||
@@ -3548,6 +3857,12 @@ export class ClaudeAcpAgent {
|
||||
emittedToolCalls: session.emittedToolCalls,
|
||||
messageId: messageIdForGrouping(message),
|
||||
toolUseResult: message.type === "user" ? message.tool_use_result : undefined,
|
||||
// On the wire since CLI 2.1.216 but not in SDKUserMessage's
|
||||
// type, hence the cast. Validated by parseToolResultMeta.
|
||||
toolResultMeta:
|
||||
message.type === "user"
|
||||
? (message as { tool_result_meta?: unknown }).tool_result_meta
|
||||
: undefined,
|
||||
},
|
||||
)) {
|
||||
// sendUpdate records delivery. Subagent text/thinking is
|
||||
@@ -3559,11 +3874,30 @@ export class ClaudeAcpAgent {
|
||||
break;
|
||||
}
|
||||
case "tool_progress": {
|
||||
// Not every beat reports under the id of a tool call the client has
|
||||
// seen: heartbeats derive `<tool_use_id>-heartbeat-<n>`, and the
|
||||
// `agent_api_retry` beats behind `subagentRetry` report under
|
||||
// `agent_<assistant_message_id>`. Forwarding those verbatim leaves the
|
||||
// client resolving an id it has never been told about (the same trap
|
||||
// `ensureToolCallEmitted` documents for #851). The SDK stamps
|
||||
// `parent_tool_use_id` with the executing tool's real id whenever the
|
||||
// beat doesn't carry one of its own, so fall back to it rather than
|
||||
// pattern-matching each synthetic id shape. Beats that do report a real
|
||||
// id (a subagent's `bash_progress`, whose parent is the spawning Agent
|
||||
// call) keep resolving to that id.
|
||||
const toolCallId = session.emittedToolCalls.has(message.tool_use_id)
|
||||
? message.tool_use_id
|
||||
: message.parent_tool_use_id;
|
||||
// Ids leave `emittedToolCalls` at `tool_result`, so this also stops a
|
||||
// beat that races past completion from reopening a finished call.
|
||||
if (toolCallId === null || !session.emittedToolCalls.has(toolCallId)) {
|
||||
break;
|
||||
}
|
||||
await sendUpdate({
|
||||
sessionId: message.session_id,
|
||||
update: {
|
||||
sessionUpdate: "tool_call_update",
|
||||
toolCallId: message.tool_use_id,
|
||||
toolCallId,
|
||||
status: "in_progress",
|
||||
_meta: {
|
||||
claudeCode: {
|
||||
@@ -4121,6 +4455,9 @@ export class ClaudeAcpAgent {
|
||||
private async replaySessionHistory(sessionId: string): Promise<void> {
|
||||
const toolUseCache: ToolUseCache = {};
|
||||
const messages = await getSessionMessages(sessionId);
|
||||
const forwardSubagentText =
|
||||
this.sessions[sessionId]?.forwardSubagentText ??
|
||||
supportsSubagentTranscript(this.clientCapabilities);
|
||||
|
||||
for (const message of messages) {
|
||||
// Backfill the ACP messageId -> SDK uuid mapping for messages we didn't
|
||||
@@ -4142,6 +4479,10 @@ export class ClaudeAcpAgent {
|
||||
|
||||
// @ts-expect-error - untyped in SDK but we handle all of these
|
||||
let content: unknown = message.message.content;
|
||||
const parentToolUseId = parentToolUseIdOf(message);
|
||||
if (message.type === "assistant" && parentToolUseId && !forwardSubagentText) {
|
||||
content = stripSubagentTextAndThinking(content);
|
||||
}
|
||||
// @ts-expect-error - untyped in SDK but we handle all of these
|
||||
if (message.message.role === "user") {
|
||||
content = stripLocalCommandMetadata(content);
|
||||
@@ -4163,6 +4504,7 @@ export class ClaudeAcpAgent {
|
||||
cwd: this.sessions[sessionId]?.cwd,
|
||||
taskState: this.sessions[sessionId]?.taskState,
|
||||
messageId: replayMessageId,
|
||||
parentToolUseId,
|
||||
},
|
||||
)) {
|
||||
await this.client.sessionUpdate(notification);
|
||||
@@ -4268,7 +4610,6 @@ export class ClaudeAcpAgent {
|
||||
toolInput,
|
||||
{ signal, suggestions, toolUseID, agentID, matchedAskRule },
|
||||
) => {
|
||||
const alwaysAllowLabel = describeAlwaysAllow(suggestions, toolName);
|
||||
const supportsTerminalOutput = this.clientCapabilities?._meta?.["terminal_output"] === true;
|
||||
const session = this.sessions[sessionId];
|
||||
if (!session) {
|
||||
@@ -4423,13 +4764,16 @@ export class ClaudeAcpAgent {
|
||||
const response = await this.requestPermissionFromClient(
|
||||
{
|
||||
options: [
|
||||
{ kind: "reject_once", name: "Deny", optionId: "reject" },
|
||||
{ kind: "allow_once", name: "Allow Once", optionId: "allow" },
|
||||
{
|
||||
kind: "allow_always",
|
||||
name: alwaysAllowLabel,
|
||||
name: "Always Allow",
|
||||
optionId: "allow_always",
|
||||
_meta: {
|
||||
permission: permissionMetadataForAlwaysAllow(suggestions, toolName),
|
||||
},
|
||||
},
|
||||
{ kind: "allow_once", name: "Allow", optionId: "allow" },
|
||||
{ kind: "reject_once", name: "Reject", optionId: "reject" },
|
||||
],
|
||||
sessionId,
|
||||
toolCall: {
|
||||
@@ -4709,6 +5053,14 @@ export class ClaudeAcpAgent {
|
||||
session.modes = { ...session.modes, availableModes: newAvailableModes };
|
||||
}
|
||||
|
||||
// `model_not_allowed` described the model we just left, so it must not
|
||||
// follow us onto the new one; the remaining reasons are account- or
|
||||
// environment-scoped and stay true across a switch. Either way the next
|
||||
// init/result report refreshes this.
|
||||
if (session.fastModeDisabledReason === "model_not_allowed") {
|
||||
session.fastModeDisabledReason = undefined;
|
||||
}
|
||||
|
||||
// Rebuild config options since effort levels depend on the selected model
|
||||
const effortOpt = session.configOptions.find((o) => o.id === EFFORT_CONFIG_ID);
|
||||
const currentEffort =
|
||||
@@ -4727,6 +5079,7 @@ export class ClaudeAcpAgent {
|
||||
supported: newModelInfo?.supportsFastMode ?? false,
|
||||
enabled: session.fastModeEnabled,
|
||||
useBooleanOption: clientSupportsBooleanConfigOptions(this.clientCapabilities),
|
||||
disabledReason: session.fastModeDisabledReason,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -4825,6 +5178,7 @@ export class ClaudeAcpAgent {
|
||||
const refreshed = createFastModeConfigOption(
|
||||
enabled,
|
||||
clientSupportsBooleanConfigOptions(this.clientCapabilities),
|
||||
session.fastModeDisabledReason,
|
||||
);
|
||||
session.configOptions = session.configOptions.map((o) =>
|
||||
o.id === FAST_MODE_CONFIG_ID ? refreshed : o,
|
||||
@@ -4858,11 +5212,19 @@ export class ClaudeAcpAgent {
|
||||
* here).
|
||||
* - `cooldown`: a transient suspension of an already-enabled fast mode.
|
||||
* Leave the toggle as-is rather than flapping it — and never let a stray
|
||||
* cooldown spuriously enable a toggle the user has off. */
|
||||
* cooldown spuriously enable a toggle the user has off.
|
||||
*
|
||||
* `reason` is the SDK's `fast_mode_disabled_reason`, reported alongside the
|
||||
* state. Only explainable reasons are retained (see
|
||||
* {@link normalizeFastModeDisabledReason}), so the comparison below tracks
|
||||
* exactly what the user can see: 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. */
|
||||
private async syncFastModeState(
|
||||
sessionId: string,
|
||||
session: Session,
|
||||
state: FastModeState | undefined,
|
||||
reason?: FastModeDisabledReason,
|
||||
): Promise<void> {
|
||||
if (state === undefined) {
|
||||
return;
|
||||
@@ -4874,11 +5236,31 @@ export class ClaudeAcpAgent {
|
||||
return;
|
||||
}
|
||||
const enabled = state === "on";
|
||||
if (enabled === session.fastModeEnabled) {
|
||||
// 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 nextReason = enabled ? undefined : normalizeFastModeDisabledReason(reason);
|
||||
if (enabled === session.fastModeEnabled && nextReason === session.fastModeDisabledReason) {
|
||||
return;
|
||||
}
|
||||
// The user asked for Fast mode and the SDK is telling us it can't serve it.
|
||||
// The description carries the same explanation, but a toggle silently
|
||||
// snapping back is the case worth saying out loud once, at the flip.
|
||||
const explain = session.fastModeEnabled && !enabled && nextReason !== undefined;
|
||||
session.fastModeEnabled = enabled;
|
||||
session.fastModeDisabledReason = nextReason;
|
||||
this.refreshFastModeOption(session, enabled);
|
||||
if (explain) {
|
||||
await this.client.sessionUpdate({
|
||||
sessionId,
|
||||
update: {
|
||||
sessionUpdate: "agent_message_chunk",
|
||||
content: {
|
||||
type: "text",
|
||||
text: `**Fast mode turned off:** ${FAST_MODE_UNAVAILABLE_EXPLANATIONS[nextReason]}.`,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
await this.client.sessionUpdate({
|
||||
sessionId,
|
||||
update: {
|
||||
@@ -5043,6 +5425,9 @@ export class ClaudeAcpAgent {
|
||||
// Extract options from _meta if provided
|
||||
const sessionMeta = params._meta as NewSessionMeta | undefined;
|
||||
const userProvidedOptions = sessionMeta?.claudeCode?.options;
|
||||
const forwardSubagentText =
|
||||
supportsSubagentTranscript(this.clientCapabilities) ||
|
||||
userProvidedOptions?.forwardSubagentText === true;
|
||||
|
||||
// Configure thinking behavior from environment variable
|
||||
const thinking = resolveThinkingConfig(process.env.MAX_THINKING_TOKENS, this.logger);
|
||||
@@ -5121,6 +5506,7 @@ export class ClaudeAcpAgent {
|
||||
// Override certain fields that must be controlled by ACP
|
||||
cwd: params.cwd,
|
||||
includePartialMessages: true,
|
||||
forwardSubagentText,
|
||||
mcpServers: { ...(userProvidedOptions?.mcpServers || {}), ...mcpServers },
|
||||
// If we want bypassPermissions to be an option, we have to allow it here.
|
||||
// But it doesn't work in root mode, so we only activate it if it will work.
|
||||
@@ -5384,10 +5770,18 @@ export class ClaudeAcpAgent {
|
||||
const fastModeEnabled =
|
||||
initializationResult.fast_mode_state !== undefined &&
|
||||
fastModeStateEnabled(initializationResult.fast_mode_state);
|
||||
// `fast_mode_disabled_reason` reflects the post-switch model since SDK
|
||||
// 0.3.219 (the initialize response used to answer from the spawn-time
|
||||
// model). A fresh SDK session reports `sdk_opt_in_required` — the toggle IS
|
||||
// the opt-in — which normalizes away, so only real blockers are retained.
|
||||
const fastModeDisabledReason = fastModeEnabled
|
||||
? undefined
|
||||
: normalizeFastModeDisabledReason(initializationResult.fast_mode_disabled_reason);
|
||||
const fastMode: FastModeOptionState = {
|
||||
supported: currentModelInfo?.supportsFastMode ?? false,
|
||||
enabled: fastModeEnabled,
|
||||
useBooleanOption: clientSupportsBooleanConfigOptions(this.clientCapabilities),
|
||||
disabledReason: fastModeDisabledReason,
|
||||
};
|
||||
|
||||
const configOptions = buildConfigOptions(
|
||||
@@ -5463,8 +5857,10 @@ export class ClaudeAcpAgent {
|
||||
agents,
|
||||
currentAgent,
|
||||
fastModeEnabled,
|
||||
fastModeDisabledReason,
|
||||
abortController,
|
||||
emitRawSDKMessages: sessionMeta?.claudeCode?.emitRawSDKMessages ?? false,
|
||||
forwardSubagentText,
|
||||
contextWindowSize: seededWindow.size,
|
||||
contextWindowAuthoritative: seededWindow.authoritative,
|
||||
providerCacheKey,
|
||||
@@ -5769,6 +6165,36 @@ 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 `syncFastModeState`) 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;
|
||||
}
|
||||
|
||||
/** 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`.
|
||||
@@ -5782,15 +6208,25 @@ export function clientSupportsBooleanConfigOptions(
|
||||
/** 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. */
|
||||
* 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: FAST_MODE_DESCRIPTION,
|
||||
description: explanation ? `${FAST_MODE_DESCRIPTION} — ${explanation}` : FAST_MODE_DESCRIPTION,
|
||||
category: "model_config",
|
||||
} as const;
|
||||
|
||||
@@ -5833,6 +6269,9 @@ export type FastModeOptionState = {
|
||||
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;
|
||||
};
|
||||
|
||||
export function buildConfigOptions(
|
||||
@@ -5910,7 +6349,13 @@ export function buildConfigOptions(
|
||||
// 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));
|
||||
options.push(
|
||||
createFastModeConfigOption(
|
||||
fastMode.enabled,
|
||||
fastMode.useBooleanOption,
|
||||
fastMode.disabledReason,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Only surface the Agent picker when there's a real choice — i.e. the user
|
||||
@@ -6565,6 +7010,29 @@ function shouldEmitToolCall(toolName: string): boolean {
|
||||
return toolName !== "TodoWrite" && !isTaskTool(toolName);
|
||||
}
|
||||
|
||||
/** Build the Claude Code-specific metadata for a tool call. Bash descriptions
|
||||
* are kept out of ACP's standard `title`, which clients may use as the shell
|
||||
* command preview, while still giving clients access to Claude's concise
|
||||
* human-readable title. */
|
||||
function claudeCodeMetaFromToolUse(toolUse: {
|
||||
name: string;
|
||||
input?: unknown;
|
||||
}): NonNullable<ToolUpdateMeta["claudeCode"]> {
|
||||
const description =
|
||||
toolUse.name === "Bash" &&
|
||||
toolUse.input !== null &&
|
||||
typeof toolUse.input === "object" &&
|
||||
"description" in toolUse.input &&
|
||||
typeof toolUse.input.description === "string"
|
||||
? toolUse.input.description
|
||||
: undefined;
|
||||
return {
|
||||
toolName: toolUse.name,
|
||||
...(description ? { title: description } : {}),
|
||||
...((toolUse.name === "Agent" || toolUse.name === "Task") && { subagent: true as const }),
|
||||
};
|
||||
}
|
||||
|
||||
/** Build the `tool_call` (or, with `refine`, the `tool_call_update`)
|
||||
* notification for a tool_use. Shared by every site that surfaces a tool call:
|
||||
* the streamed tool_use path (first encounter → tool_call, later encounter →
|
||||
@@ -6581,7 +7049,7 @@ function toolCallNotification(
|
||||
): SessionNotification["update"] {
|
||||
if (refine) {
|
||||
return {
|
||||
_meta: { claudeCode: { toolName: toolUse.name } } satisfies ToolUpdateMeta,
|
||||
_meta: { claudeCode: claudeCodeMetaFromToolUse(toolUse) } satisfies ToolUpdateMeta,
|
||||
toolCallId: toolUse.id,
|
||||
sessionUpdate: "tool_call_update",
|
||||
rawInput,
|
||||
@@ -6590,7 +7058,7 @@ function toolCallNotification(
|
||||
}
|
||||
return {
|
||||
_meta: {
|
||||
claudeCode: { toolName: toolUse.name },
|
||||
claudeCode: claudeCodeMetaFromToolUse(toolUse),
|
||||
...(toolUse.name === "Bash" && supportsTerminalOutput
|
||||
? { terminal_info: { terminal_id: toolUse.id } }
|
||||
: {}),
|
||||
@@ -6625,7 +7093,9 @@ function streamedInputRefinement(
|
||||
cwd,
|
||||
);
|
||||
return {
|
||||
_meta: { claudeCode: { toolName: toolUse.name } } satisfies ToolUpdateMeta,
|
||||
_meta: {
|
||||
claudeCode: claudeCodeMetaFromToolUse({ ...toolUse, input }),
|
||||
} satisfies ToolUpdateMeta,
|
||||
toolCallId: toolUse.id,
|
||||
sessionUpdate: "tool_call_update",
|
||||
rawInput: input,
|
||||
@@ -6635,6 +7105,36 @@ function streamedInputRefinement(
|
||||
};
|
||||
}
|
||||
|
||||
/** Validates the SDK user message's `tool_result_meta` sidecar (emitted on the
|
||||
* wire by CLI ≥ 2.1.216 but absent from sdk.d.ts, hence unknown-typed) into a
|
||||
* by-tool_use_id lookup. Each entry explains why an is_error tool_result
|
||||
* carries harness prose instead of the tool's own output — "user-rejected",
|
||||
* "permission-rule", "interrupted", "cancelled", … (open set: new kinds ship
|
||||
* on the wire ahead of schema updates, so no enum check). Malformed entries
|
||||
* are skipped rather than failing the message. */
|
||||
function parseToolResultMeta(
|
||||
raw: unknown,
|
||||
): Map<string, { nonExecutionKind: string; userFeedback?: string }> | undefined {
|
||||
if (!Array.isArray(raw)) {
|
||||
return undefined;
|
||||
}
|
||||
let byToolUseId: Map<string, { nonExecutionKind: string; userFeedback?: string }> | undefined;
|
||||
for (const entry of raw) {
|
||||
if (typeof entry !== "object" || entry === null) {
|
||||
continue;
|
||||
}
|
||||
const { id, non_execution_kind, user_feedback } = entry as Record<string, unknown>;
|
||||
if (typeof id !== "string" || typeof non_execution_kind !== "string") {
|
||||
continue;
|
||||
}
|
||||
(byToolUseId ??= new Map()).set(id, {
|
||||
nonExecutionKind: non_execution_kind,
|
||||
...(typeof user_feedback === "string" ? { userFeedback: user_feedback } : {}),
|
||||
});
|
||||
}
|
||||
return byToolUseId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an SDKAssistantMessage (Claude) to a SessionNotification (ACP).
|
||||
* Only handles text, image, and thinking chunks for now.
|
||||
@@ -6670,6 +7170,10 @@ export function toAcpNotifications(
|
||||
// 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;
|
||||
},
|
||||
): SessionNotification[] {
|
||||
const taskState = options?.taskState ?? new Map();
|
||||
@@ -6712,6 +7216,10 @@ export function toAcpNotifications(
|
||||
? 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) {
|
||||
@@ -6862,6 +7370,12 @@ export function toAcpNotifications(
|
||||
case "mcp_tool_result": {
|
||||
const wasEmitted = options?.emittedToolCalls?.has(chunk.tool_use_id) === true;
|
||||
options?.emittedToolCalls?.delete(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
|
||||
@@ -6905,6 +7419,7 @@ export function toAcpNotifications(
|
||||
_meta: {
|
||||
claudeCode: {
|
||||
toolName: toolUse.name,
|
||||
...(nonExecution ?? {}),
|
||||
...(options?.parentToolUseId ? { parentToolUseId: options.parentToolUseId } : {}),
|
||||
},
|
||||
} satisfies ToolUpdateMeta,
|
||||
@@ -6976,6 +7491,7 @@ export function toAcpNotifications(
|
||||
_meta: {
|
||||
claudeCode: {
|
||||
toolName: toolUse.name,
|
||||
...(nonExecution ?? {}),
|
||||
},
|
||||
...(toolMeta?.terminal_exit ? { terminal_exit: toolMeta.terminal_exit } : {}),
|
||||
} satisfies ToolUpdateMeta,
|
||||
@@ -7242,6 +7758,9 @@ export function runAcp() {
|
||||
runPromptWithCancellation(agent, ctx.params, ctx.signal),
|
||||
)
|
||||
.onNotification(methods.agent.session.cancel, (ctx) => agent.cancel(ctx.params))
|
||||
.onRequest<SteerRequest, SteerResponse>(STEER_METHOD, { parse: parseSteerRequest }, (ctx) =>
|
||||
agent.steer(ctx.params),
|
||||
)
|
||||
.connect(stream);
|
||||
|
||||
agent = new ClaudeAcpAgent(new ClientConnection(connection.client));
|
||||
|
||||
@@ -148,6 +148,14 @@ function questionCustomFieldKey(index: number): string {
|
||||
*/
|
||||
const OPTION_META_KEY = "_claude/askUserQuestionOption";
|
||||
|
||||
/**
|
||||
* Shared `_meta` key for marking a per-question free-text field as the custom
|
||||
* answer companion for a select question. This intentionally has no
|
||||
* agent-specific namespace so ACP clients can recognize the same marker across
|
||||
* Codex, Claude, and other AskUserQuestion bridges.
|
||||
*/
|
||||
const CUSTOM_ANSWER_META_KEY = "_askUserQuestionCustomAnswer";
|
||||
|
||||
/**
|
||||
* Render the AskUserQuestion tool's questions as an ACP form elicitation.
|
||||
*
|
||||
@@ -204,6 +212,12 @@ export function askUserQuestionsToCreateRequest(
|
||||
type: "string",
|
||||
title: "Other",
|
||||
description: "Type your own answer instead of choosing an option above (optional).",
|
||||
_meta: {
|
||||
[CUSTOM_ANSWER_META_KEY]: {
|
||||
questionId: questionFieldKey(index),
|
||||
isCustomAnswer: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -263,6 +263,38 @@ describe("createSession options merging", () => {
|
||||
expect(capturedOptions!.tools).toEqual([]);
|
||||
});
|
||||
|
||||
describe("subagent transcript forwarding", () => {
|
||||
it("keeps the legacy default when neither the client nor caller opts in", async () => {
|
||||
await agent.newSession({ cwd: process.cwd(), mcpServers: [] });
|
||||
|
||||
expect(capturedOptions!.forwardSubagentText).toBe(false);
|
||||
});
|
||||
|
||||
it("preserves the pre-existing caller-provided SDK option", async () => {
|
||||
await agent.newSession({
|
||||
cwd: process.cwd(),
|
||||
mcpServers: [],
|
||||
_meta: { claudeCode: { options: { forwardSubagentText: true } } },
|
||||
});
|
||||
|
||||
expect(capturedOptions!.forwardSubagentText).toBe(true);
|
||||
});
|
||||
|
||||
it("enables SDK forwarding when the ACP client advertises support", async () => {
|
||||
await agent.initialize({
|
||||
protocolVersion: 1,
|
||||
clientCapabilities: { _meta: { "subagent-transcript": true } },
|
||||
});
|
||||
await agent.newSession({
|
||||
cwd: process.cwd(),
|
||||
mcpServers: [],
|
||||
_meta: { claudeCode: { options: { forwardSubagentText: false } } },
|
||||
});
|
||||
|
||||
expect(capturedOptions!.forwardSubagentText).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("systemPrompt via _meta", () => {
|
||||
it("defaults to the claude_code preset when not provided", async () => {
|
||||
await agent.newSession({ cwd: process.cwd(), mcpServers: [] });
|
||||
|
||||
@@ -264,10 +264,22 @@ describe("askUserQuestionsToCreateRequest", () => {
|
||||
expect(schema.properties?.["question_0_custom"]).toMatchObject({
|
||||
type: "string",
|
||||
title: "Other",
|
||||
_meta: {
|
||||
_askUserQuestionCustomAnswer: {
|
||||
questionId: "question_0",
|
||||
isCustomAnswer: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(schema.properties?.["question_1_custom"]).toMatchObject({
|
||||
type: "string",
|
||||
title: "Other",
|
||||
_meta: {
|
||||
_askUserQuestionCustomAnswer: {
|
||||
questionId: "question_1",
|
||||
isCustomAnswer: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import type { ClientCapabilities, SessionNotification } from "@agentclientprotocol/sdk";
|
||||
import type { ModelInfo } from "@anthropic-ai/claude-agent-sdk";
|
||||
import type { FastModeDisabledReason, ModelInfo } from "@anthropic-ai/claude-agent-sdk";
|
||||
import {
|
||||
buildConfigOptions,
|
||||
clientSupportsBooleanConfigOptions,
|
||||
createFastModeConfigOption,
|
||||
fastModeStateEnabled,
|
||||
normalizeFastModeDisabledReason,
|
||||
resolveFastModeEnabled,
|
||||
FAST_MODE_CONFIG_ID,
|
||||
FAST_MODE_ON,
|
||||
@@ -42,6 +43,28 @@ describe("createFastModeConfigOption", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("explains a blocking disabled reason while the toggle reads off", () => {
|
||||
const option = createFastModeConfigOption(false, true, "free");
|
||||
expect(option.description).toBe(
|
||||
"Faster responses on supported models — not available on the free plan",
|
||||
);
|
||||
});
|
||||
|
||||
it("ignores routine and on-state reasons", () => {
|
||||
// Every SDK session starts at `sdk_opt_in_required` — the toggle IS the
|
||||
// opt-in, so it must not read as a blocker.
|
||||
expect(createFastModeConfigOption(false, true, "sdk_opt_in_required").description).toBe(
|
||||
"Faster responses on supported models",
|
||||
);
|
||||
expect(createFastModeConfigOption(false, true, "preference").description).toBe(
|
||||
"Faster responses on supported models",
|
||||
);
|
||||
// A reason riding an `on` state isn't blocking anything right now.
|
||||
expect(createFastModeConfigOption(true, true, "free").description).toBe(
|
||||
"Faster responses on supported models",
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to an on/off select when the client did not opt in", () => {
|
||||
const option = createFastModeConfigOption(false, false);
|
||||
expect(option).toMatchObject({
|
||||
@@ -104,6 +127,28 @@ describe("fastModeStateEnabled", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizeFastModeDisabledReason", () => {
|
||||
it("retains only reasons we have an explanation for", () => {
|
||||
expect(normalizeFastModeDisabledReason("free")).toBe("free");
|
||||
expect(normalizeFastModeDisabledReason("extra_usage_disabled")).toBe("extra_usage_disabled");
|
||||
expect(normalizeFastModeDisabledReason("model_not_allowed")).toBe("model_not_allowed");
|
||||
expect(normalizeFastModeDisabledReason("not_first_party")).toBe("not_first_party");
|
||||
expect(normalizeFastModeDisabledReason("disabled_by_env")).toBe("disabled_by_env");
|
||||
expect(normalizeFastModeDisabledReason("network_error")).toBe("network_error");
|
||||
});
|
||||
|
||||
it("drops routine states and unknown values", () => {
|
||||
expect(normalizeFastModeDisabledReason("sdk_opt_in_required")).toBeUndefined();
|
||||
expect(normalizeFastModeDisabledReason("preference")).toBeUndefined();
|
||||
expect(normalizeFastModeDisabledReason("pending")).toBeUndefined();
|
||||
expect(normalizeFastModeDisabledReason("unknown")).toBeUndefined();
|
||||
expect(normalizeFastModeDisabledReason(undefined)).toBeUndefined();
|
||||
expect(
|
||||
normalizeFastModeDisabledReason("some_future_reason" as FastModeDisabledReason),
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildConfigOptions Fast mode", () => {
|
||||
it("omits the Fast mode option when the model does not support it", () => {
|
||||
const options = buildConfigOptions(MODES, MODELS, MODEL_INFOS, undefined, [], "default", {
|
||||
@@ -249,6 +294,7 @@ describe("syncFastModeState (SDK-driven state changes)", () => {
|
||||
const session = {
|
||||
query: {},
|
||||
fastModeEnabled: opts.fastModeEnabled,
|
||||
fastModeDisabledReason: undefined as FastModeDisabledReason | undefined,
|
||||
configOptions: opts.withOption
|
||||
? [createFastModeConfigOption(opts.fastModeEnabled, true)]
|
||||
: [],
|
||||
@@ -261,6 +307,7 @@ describe("syncFastModeState (SDK-driven state changes)", () => {
|
||||
sessionId: string,
|
||||
session: unknown,
|
||||
state: string | undefined,
|
||||
reason?: FastModeDisabledReason,
|
||||
) => Promise<void>;
|
||||
}
|
||||
).syncFastModeState.bind(agent);
|
||||
@@ -319,10 +366,65 @@ describe("syncFastModeState (SDK-driven state changes)", () => {
|
||||
|
||||
await sync(SESSION_ID, session, undefined);
|
||||
await sync(SESSION_ID, session, "off");
|
||||
// A routine reason normalizes away, so it's still "unchanged" — an
|
||||
// `sdk_opt_in_required` on every turn's result must not churn the option.
|
||||
await sync(SESSION_ID, session, "off", "sdk_opt_in_required");
|
||||
|
||||
expect(sessionUpdates).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("explains a blocking reason once when the SDK refuses a toggle the user turned on", async () => {
|
||||
const { sync, session, sessionUpdates } = setup({ fastModeEnabled: true, withOption: true });
|
||||
|
||||
await sync(SESSION_ID, session, "off", "extra_usage_disabled");
|
||||
|
||||
expect(session.fastModeEnabled).toBe(false);
|
||||
expect(session.fastModeDisabledReason).toBe("extra_usage_disabled");
|
||||
expect(sessionUpdates).toHaveLength(2);
|
||||
expect(sessionUpdates[0].update).toEqual({
|
||||
sessionUpdate: "agent_message_chunk",
|
||||
content: {
|
||||
type: "text",
|
||||
text: "**Fast mode turned off:** requires extra usage to be enabled for this account.",
|
||||
},
|
||||
});
|
||||
expect(sessionUpdates[1].update).toMatchObject({ sessionUpdate: "config_option_update" });
|
||||
expect(session.configOptions).toContainEqual(
|
||||
createFastModeConfigOption(false, true, "extra_usage_disabled"),
|
||||
);
|
||||
|
||||
// The same report again changes nothing the user can see: no repeat notice.
|
||||
await sync(SESSION_ID, session, "off", "extra_usage_disabled");
|
||||
expect(sessionUpdates).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("updates the description when only the reason changes", async () => {
|
||||
const { sync, session, sessionUpdates } = setup({ fastModeEnabled: false, withOption: true });
|
||||
|
||||
await sync(SESSION_ID, session, "off", "not_first_party");
|
||||
|
||||
expect(session.fastModeEnabled).toBe(false);
|
||||
// The toggle was already off, so the flip-time notice doesn't fire — only
|
||||
// the description carries the explanation.
|
||||
expect(sessionUpdates).toHaveLength(1);
|
||||
expect(sessionUpdates[0].update).toMatchObject({ sessionUpdate: "config_option_update" });
|
||||
expect(session.configOptions).toContainEqual(
|
||||
createFastModeConfigOption(false, true, "not_first_party"),
|
||||
);
|
||||
});
|
||||
|
||||
it("drops a retained reason once fast mode comes back on", async () => {
|
||||
const { sync, session } = setup({ fastModeEnabled: false, withOption: true });
|
||||
|
||||
await sync(SESSION_ID, session, "off", "network_error");
|
||||
expect(session.fastModeDisabledReason).toBe("network_error");
|
||||
|
||||
await sync(SESSION_ID, session, "on");
|
||||
expect(session.fastModeEnabled).toBe(true);
|
||||
expect(session.fastModeDisabledReason).toBeUndefined();
|
||||
expect(session.configOptions).toContainEqual(createFastModeConfigOption(true, true));
|
||||
});
|
||||
|
||||
it("preserves the retained setting (no clobber) when the model has no Fast mode option", async () => {
|
||||
// Model without fast support: the SDK reports a capability-driven state, not
|
||||
// the user's intent. We must leave session.fastModeEnabled untouched so it's
|
||||
|
||||
@@ -491,6 +491,94 @@ describe("Bash terminal output", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keys the terminal metas off the tool_use id, which is what was announced", () => {
|
||||
// `toolInfoFromToolUse` announces the terminal as `toolUse.id`, so the
|
||||
// result's metas have to use the same value for the client to match them
|
||||
// up. A result block that disagrees (or omits `tool_use_id`) must not be
|
||||
// allowed to retarget them.
|
||||
const toolResult = {
|
||||
...makeBashResult("out", "", 0),
|
||||
tool_use_id: "toolu_something_else",
|
||||
};
|
||||
const update = toolUpdateFromToolResult(toolResult, bashToolUse, true);
|
||||
|
||||
expect(update.content).toEqual([{ type: "terminal", terminalId: "toolu_bash" }]);
|
||||
expect(update._meta).toEqual({
|
||||
terminal_info: { terminal_id: "toolu_bash" },
|
||||
terminal_output: { terminal_id: "toolu_bash", data: "out" },
|
||||
terminal_exit: { terminal_id: "toolu_bash", exit_code: 0, signal: null },
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to the result's tool_use_id when the tool_use is unavailable", () => {
|
||||
const toolResult = makeBashResult("out", "", 0);
|
||||
const update = toolUpdateFromToolResult(toolResult, { name: "Bash" }, true);
|
||||
|
||||
expect(update.content).toEqual([{ type: "terminal", terminalId: "toolu_bash" }]);
|
||||
expect(update._meta).toEqual({
|
||||
terminal_info: { terminal_id: "toolu_bash" },
|
||||
terminal_output: { terminal_id: "toolu_bash", data: "out" },
|
||||
terminal_exit: { terminal_id: "toolu_bash", exit_code: 0, signal: null },
|
||||
});
|
||||
});
|
||||
|
||||
it("renders a code block instead of a dangling terminal when no id is available", () => {
|
||||
// Previously this emitted `terminal_id: ""` for all three metas. Nothing
|
||||
// on the client has a terminal under that id, so the output was stranded
|
||||
// (Zed buffers output/exit for unknown terminals indefinitely) and the
|
||||
// user saw an empty terminal. Degrade to the non-terminal rendering.
|
||||
// `tool_use_id` is required on every result-block type, so a block without
|
||||
// it can only arrive at runtime (an older or non-conforming emitter). The
|
||||
// source guards for it with `"tool_use_id" in toolResult`, so exercise that
|
||||
// path with a cast rather than pretending the type allows it.
|
||||
const { content, type } = makeBashResult("out", "", 0);
|
||||
const update = toolUpdateFromToolResult(
|
||||
{ content, type } as unknown as Parameters<typeof toolUpdateFromToolResult>[0],
|
||||
{ name: "Bash" },
|
||||
true,
|
||||
);
|
||||
|
||||
expect(update._meta).toBeUndefined();
|
||||
expect(update.content).toEqual([
|
||||
{
|
||||
type: "content",
|
||||
content: { type: "text", text: "```console\nout\n```" },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("treats an empty tool_use id as no id and falls back to the result block", () => {
|
||||
const toolResult = makeBashResult("out", "", 0);
|
||||
const update = toolUpdateFromToolResult(toolResult, { id: "", name: "Bash" }, true);
|
||||
|
||||
expect(update.content).toEqual([{ type: "terminal", terminalId: "toolu_bash" }]);
|
||||
expect(update._meta).toEqual({
|
||||
terminal_info: { terminal_id: "toolu_bash" },
|
||||
terminal_output: { terminal_id: "toolu_bash", data: "out" },
|
||||
terminal_exit: { terminal_id: "toolu_bash", exit_code: 0, signal: null },
|
||||
});
|
||||
});
|
||||
|
||||
it("renders a code block when neither id is a usable string", () => {
|
||||
// A present-but-undefined `tool_use_id` still satisfies an `in` check, and
|
||||
// stringifying it would key all three metas on the literal "undefined" — an
|
||||
// id no client ever created a terminal for, so the output strands exactly as
|
||||
// it did under the old empty-string id.
|
||||
const toolResult = {
|
||||
...makeBashResult("out", "", 0),
|
||||
tool_use_id: undefined,
|
||||
} as unknown as Parameters<typeof toolUpdateFromToolResult>[0];
|
||||
const update = toolUpdateFromToolResult(toolResult, { id: "", name: "Bash" }, true);
|
||||
|
||||
expect(update._meta).toBeUndefined();
|
||||
expect(update.content).toEqual([
|
||||
{
|
||||
type: "content",
|
||||
content: { type: "text", text: "```console\nout\n```" },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("should include exit_code from return_code in terminal_exit", () => {
|
||||
const toolResult = makeBashResult("", "command not found", 127);
|
||||
const update = toolUpdateFromToolResult(toolResult, bashToolUse, true);
|
||||
@@ -2435,6 +2523,154 @@ describe("Agent/Task tool_result rendering from tool_use_result", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("tool_result_meta non-execution stamping", () => {
|
||||
const mockClient = {} as AcpClient;
|
||||
const mockLogger: Logger = { log: () => {}, error: () => {} };
|
||||
|
||||
const bashToolUse = {
|
||||
type: "tool_use" as const,
|
||||
id: "toolu_bash",
|
||||
name: "Bash",
|
||||
input: { command: "rm -rf build" },
|
||||
};
|
||||
|
||||
const deniedResult: ToolResultBlockParam = {
|
||||
type: "tool_result",
|
||||
tool_use_id: "toolu_bash",
|
||||
is_error: true,
|
||||
content: "The user doesn't want to proceed with this tool use.",
|
||||
};
|
||||
|
||||
it("stamps nonExecutionKind and userFeedback on the failed tool_call_update", () => {
|
||||
const toolUseCache: ToolUseCache = { toolu_bash: bashToolUse };
|
||||
|
||||
const notifications = toAcpNotifications(
|
||||
[deniedResult] as any,
|
||||
"user",
|
||||
"test-session",
|
||||
toolUseCache,
|
||||
mockClient,
|
||||
mockLogger,
|
||||
{
|
||||
toolResultMeta: [
|
||||
{ id: "toolu_bash", non_execution_kind: "user-rejected", user_feedback: "use npm" },
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
expect(notifications).toHaveLength(1);
|
||||
expect(notifications[0].update).toMatchObject({
|
||||
sessionUpdate: "tool_call_update",
|
||||
toolCallId: "toolu_bash",
|
||||
status: "failed",
|
||||
_meta: {
|
||||
claudeCode: {
|
||||
toolName: "Bash",
|
||||
nonExecutionKind: "user-rejected",
|
||||
userFeedback: "use npm",
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("attributes entries by tool_use_id, so only the flagged result in a batch is stamped", () => {
|
||||
const toolUseCache: ToolUseCache = {
|
||||
toolu_bash: bashToolUse,
|
||||
toolu_bash2: { ...bashToolUse, id: "toolu_bash2" },
|
||||
};
|
||||
|
||||
const notifications = toAcpNotifications(
|
||||
[
|
||||
deniedResult,
|
||||
{
|
||||
type: "tool_result",
|
||||
tool_use_id: "toolu_bash2",
|
||||
content: "ok",
|
||||
},
|
||||
] as any,
|
||||
"user",
|
||||
"test-session",
|
||||
toolUseCache,
|
||||
mockClient,
|
||||
mockLogger,
|
||||
{ toolResultMeta: [{ id: "toolu_bash", non_execution_kind: "user-rejected" }] },
|
||||
);
|
||||
|
||||
expect(notifications).toHaveLength(2);
|
||||
const [denied, ran] = notifications.map((n) => n.update) as any[];
|
||||
expect(denied._meta.claudeCode).toMatchObject({ nonExecutionKind: "user-rejected" });
|
||||
// No user_feedback on the wire entry → no userFeedback key at all.
|
||||
expect(denied._meta.claudeCode).not.toHaveProperty("userFeedback");
|
||||
expect(ran._meta.claudeCode).not.toHaveProperty("nonExecutionKind");
|
||||
});
|
||||
|
||||
it("ignores a malformed sidecar and malformed entries", () => {
|
||||
for (const malformed of [
|
||||
"user-rejected", // not an array
|
||||
[{ non_execution_kind: "user-rejected" }], // entry missing id
|
||||
[{ id: "toolu_bash", non_execution_kind: 7 }], // kind not a string
|
||||
[null, 42], // entries not objects
|
||||
]) {
|
||||
const toolUseCache: ToolUseCache = { toolu_bash: bashToolUse };
|
||||
const notifications = toAcpNotifications(
|
||||
[deniedResult] as any,
|
||||
"user",
|
||||
"test-session",
|
||||
toolUseCache,
|
||||
mockClient,
|
||||
mockLogger,
|
||||
{ toolResultMeta: malformed },
|
||||
);
|
||||
|
||||
expect(notifications).toHaveLength(1);
|
||||
expect((notifications[0].update as any)._meta.claudeCode).not.toHaveProperty(
|
||||
"nonExecutionKind",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("stamps the resolve of a permission-surfaced suppressed tool (Task*)", () => {
|
||||
// A TaskGet surfaced as a real tool_call by the permission flow never gets
|
||||
// a tool_call_update from the suppressed Task* branch; the wasEmitted
|
||||
// resolve must carry the denial kind too.
|
||||
const taskGetToolUse = {
|
||||
type: "tool_use" as const,
|
||||
id: "toolu_taskget",
|
||||
name: "TaskGet",
|
||||
input: { taskId: "1" },
|
||||
};
|
||||
const toolUseCache: ToolUseCache = { toolu_taskget: taskGetToolUse };
|
||||
|
||||
const notifications = toAcpNotifications(
|
||||
[
|
||||
{
|
||||
type: "tool_result",
|
||||
tool_use_id: "toolu_taskget",
|
||||
is_error: true,
|
||||
content: "The user doesn't want to proceed with this tool use.",
|
||||
},
|
||||
] as any,
|
||||
"user",
|
||||
"test-session",
|
||||
toolUseCache,
|
||||
mockClient,
|
||||
mockLogger,
|
||||
{
|
||||
emittedToolCalls: new Set(["toolu_taskget"]),
|
||||
toolResultMeta: [{ id: "toolu_taskget", non_execution_kind: "user-rejected" }],
|
||||
},
|
||||
);
|
||||
|
||||
expect(notifications).toHaveLength(1);
|
||||
expect(notifications[0].update).toMatchObject({
|
||||
sessionUpdate: "tool_call_update",
|
||||
toolCallId: "toolu_taskget",
|
||||
status: "failed",
|
||||
_meta: { claudeCode: { toolName: "TaskGet", nonExecutionKind: "user-rejected" } },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("structured tool_use_result rendering (Read/Bash/WebSearch)", () => {
|
||||
describe("Read", () => {
|
||||
const readToolUse = {
|
||||
|
||||
23
src/tools.ts
23
src/tools.ts
@@ -686,7 +686,19 @@ export function toolUpdateFromToolResult(
|
||||
|
||||
case "Bash": {
|
||||
const result = toolResult.content;
|
||||
const terminalId = "tool_use_id" in toolResult ? String(toolResult.tool_use_id) : "";
|
||||
// The terminal was announced under the tool_use's own id (see
|
||||
// `toolInfoFromToolUse`), so key the output/exit metas off that: it is the
|
||||
// id the client actually created a terminal for. `toolResult.tool_use_id`
|
||||
// is the same value whenever present — the caller looks the tool_use up by
|
||||
// it — so preferring `toolUse.id` only adds a source for the case where the
|
||||
// result block carries no id at all. Anything that isn't a non-empty
|
||||
// string is no id at all: `""` matches no terminal, and stringifying a
|
||||
// present-but-undefined field would invent the literal `"undefined"`.
|
||||
const terminalIdOf = (id: unknown): string | undefined =>
|
||||
typeof id === "string" && id.length > 0 ? id : undefined;
|
||||
const terminalId: string | undefined =
|
||||
terminalIdOf(toolUse?.id) ??
|
||||
terminalIdOf("tool_use_id" in toolResult ? toolResult.tool_use_id : undefined);
|
||||
const isError = "is_error" in toolResult && toolResult.is_error;
|
||||
|
||||
// Extract output and exit code from either format:
|
||||
@@ -767,7 +779,14 @@ export function toolUpdateFromToolResult(
|
||||
}
|
||||
}
|
||||
|
||||
if (supportsTerminalOutput) {
|
||||
// Without a terminal id there is nothing the client can reconcile these
|
||||
// metas against, and emitting them anyway strands the output: a client that
|
||||
// buffers output/exit for terminals it has not been told about (Zed keeps
|
||||
// them in `pending_terminal_output`/`pending_terminal_exit`, drained only on
|
||||
// a matching create) would hold them forever behind an id that never
|
||||
// arrives, showing an empty terminal. Fall through to the code-block
|
||||
// rendering below instead.
|
||||
if (supportsTerminalOutput && terminalId !== undefined) {
|
||||
return {
|
||||
content: [{ type: "terminal" as const, terminalId }],
|
||||
_meta: {
|
||||
|
||||
Reference in New Issue
Block a user