mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-11-05 17:01:44 +00:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c19bddcf79 | ||
|
|
edf3af043b | ||
|
|
980ac8dc56 | ||
|
|
d9bd36d8b0 | ||
|
|
0a88de2a83 | ||
|
|
ff9b96d462 | ||
|
|
1887ada215 | ||
|
|
06c3d7bdbd | ||
|
|
92548f0435 | ||
|
|
cc5d5e8b80 | ||
|
|
82cd692e50 |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
cache: "npm"
|
||||
|
||||
27
.github/workflows/publish.yml
vendored
27
.github/workflows/publish.yml
vendored
@@ -46,10 +46,35 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
# 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"
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- run: npm publish
|
||||
|
||||
trigger-registry-update:
|
||||
needs: publish-npm
|
||||
runs-on: ubuntu-latest
|
||||
environment: release
|
||||
permissions: {}
|
||||
steps:
|
||||
- name: Generate token scoped to the registry repo
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
|
||||
id: registry-token
|
||||
with:
|
||||
app-id: ${{ secrets.REGISTRY_UPDATER_APP_ID }}
|
||||
private-key: ${{ secrets.REGISTRY_UPDATER_APP_PRIVATE_KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
repositories: registry
|
||||
|
||||
- name: Dispatch registry version update
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.registry-token.outputs.token }}
|
||||
run: |
|
||||
gh workflow run update-versions.yml \
|
||||
--repo ${{ github.repository_owner }}/registry \
|
||||
--ref main \
|
||||
-f apply=true \
|
||||
-f agents=claude-acp
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,3 +3,4 @@ dist
|
||||
.DS_Store
|
||||
.claude/*.local.*
|
||||
CLAUDE.local.md
|
||||
.idea/
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "0.59.0"
|
||||
".": "0.61.0"
|
||||
}
|
||||
|
||||
23
CHANGELOG.md
23
CHANGELOG.md
@@ -1,5 +1,28 @@
|
||||
# Changelog
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **deps:** Update to claude-agent-sdk 0.3.215 ([#890](https://github.com/agentclientprotocol/claude-agent-acp/issues/890)) ([92548f0](https://github.com/agentclientprotocol/claude-agent-acp/commit/92548f043547b0ddac95921ece020e69f7c12c5f))
|
||||
* implement configurable LLM providers ([#853](https://github.com/agentclientprotocol/claude-agent-acp/issues/853)) ([82cd692](https://github.com/agentclientprotocol/claude-agent-acp/commit/82cd692e500eedec182f817b18cebb005c4b98ce))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* parse Agent/Task trailers without regex ([#879](https://github.com/agentclientprotocol/claude-agent-acp/issues/879)) ([06c3d7b](https://github.com/agentclientprotocol/claude-agent-acp/commit/06c3d7bdbd8cc9415c8cabac060a50e0951c758b))
|
||||
* remove ~15s stall on session/new and model switch by seeding the context window synchronously ([#894](https://github.com/agentclientprotocol/claude-agent-acp/issues/894)) ([ff9b96d](https://github.com/agentclientprotocol/claude-agent-acp/commit/ff9b96d462831b1c3b96722ea20215ff6e529cb1))
|
||||
* Silence missing PostToolUse callbacks ([#895](https://github.com/agentclientprotocol/claude-agent-acp/issues/895)) ([1887ada](https://github.com/agentclientprotocol/claude-agent-acp/commit/1887ada215b27bb1025d9b7696a46ae7a4ac0f7a)), closes [#889](https://github.com/agentclientprotocol/claude-agent-acp/issues/889)
|
||||
|
||||
## [0.59.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.58.1...v0.59.0) (2026-07-13)
|
||||
|
||||
|
||||
|
||||
191
examples/steering.ts
Normal file
191
examples/steering.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* 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 (see ../steering_protocol.md) has three moving parts:
|
||||
*
|
||||
* 1. The agent advertises support in its `initialize` response, at the
|
||||
* top-level `_meta.steering.supported` (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 (an unavoidable race),
|
||||
* so the message began a fresh turn instead.
|
||||
* Both are success outcomes — the message is never dropped and the race is
|
||||
* never surfaced as an error.
|
||||
*
|
||||
* 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 } 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 }>;
|
||||
};
|
||||
|
||||
/** Result of a `_session/steering` request. Both values are successes: they
|
||||
* tell the client where the message landed, not whether it succeeded. */
|
||||
type SteeringResponse = {
|
||||
outcome: "injected" | "startedNewTurn";
|
||||
};
|
||||
|
||||
// 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}`);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
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 meta = init._meta as { steering?: { supported?: boolean } } | null | undefined;
|
||||
const steeringSupported = meta?.steering?.supported === true;
|
||||
log(`agent advertises steering: ${steeringSupported}`);
|
||||
if (!steeringSupported) {
|
||||
log("agent does not advertise steering; the steering request may be rejected.");
|
||||
}
|
||||
|
||||
// 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 }],
|
||||
};
|
||||
try {
|
||||
const result = await agent.request<SteeringResponse>(STEERING_METHOD, steerRequest);
|
||||
log(`steer outcome: ${result.outcome}`);
|
||||
} catch (err) {
|
||||
log(`steer rejected: ${err}`);
|
||||
}
|
||||
|
||||
// 5. Await the turn. With outcome "injected" the steer already reshaped the
|
||||
// output above; with "startedNewTurn" the follow-up runs as a fresh turn
|
||||
// that may still be streaming, so linger briefly to capture it.
|
||||
const response = await turn.catch((err: unknown) => {
|
||||
log(`turn error: ${err}`);
|
||||
return undefined;
|
||||
});
|
||||
if (response) log(`turn stopReason: ${response.stopReason}`);
|
||||
await delay(3000);
|
||||
process.stdout.write("\n----- end of agent output -----\n");
|
||||
|
||||
connection.close();
|
||||
child.kill();
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
log(`fatal: ${err?.stack ?? err}`);
|
||||
process.exit(1);
|
||||
});
|
||||
418
package-lock.json
generated
418
package-lock.json
generated
@@ -1,32 +1,32 @@
|
||||
{
|
||||
"name": "@agentclientprotocol/claude-agent-acp",
|
||||
"version": "0.59.0",
|
||||
"version": "0.61.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@agentclientprotocol/claude-agent-acp",
|
||||
"version": "0.59.0",
|
||||
"version": "0.61.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "1.2.1",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.3.207",
|
||||
"@agentclientprotocol/sdk": "1.3.0",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.3.217",
|
||||
"zod": "^3.25.0 || ^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"claude-agent-acp": "dist/index.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@anthropic-ai/sdk": "0.111.0",
|
||||
"@anthropic-ai/sdk": "0.112.5",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@tsconfig/node22": "22.0.5",
|
||||
"@types/node": "26.1.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.63.0",
|
||||
"@typescript-eslint/parser": "8.63.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.65.0",
|
||||
"@typescript-eslint/parser": "8.65.0",
|
||||
"eslint": "10.7.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"globals": "17.7.0",
|
||||
"prettier": "3.9.5",
|
||||
"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.207",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.207.tgz",
|
||||
"integrity": "sha512-y0PkQRmQBi96MHiN5Xzfq+GaddxCZCqI/cXEQBLYBLXGa4i1nDSlulQqkMBj2RorrrSGQJ6Wdw+uhu6OfHNPzA==",
|
||||
"version": "0.3.217",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.217.tgz",
|
||||
"integrity": "sha512-juszT3itL8R6OQ6nb/8IZE34UjKps8Jf7N8vjCXLx+vbJc+k3EojZOs93tJwT5iTRvfV1a0N53zJbKn/iJpKrQ==",
|
||||
"license": "SEE LICENSE IN README.md",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.207",
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.207",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.207",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.207",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.207",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.207",
|
||||
"@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.207",
|
||||
"@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.207"
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.217",
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.217",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.217",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.217",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.217",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.217",
|
||||
"@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.217",
|
||||
"@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.217"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@anthropic-ai/sdk": ">=0.93.0",
|
||||
@@ -69,9 +69,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-darwin-arm64": {
|
||||
"version": "0.3.207",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.207.tgz",
|
||||
"integrity": "sha512-08xSo1FDx8h0aLhL5tvcRxa2SMmcUV3aDWeZiEJVTclyiDAs61BgTjAxCg+SZcu1CndjJO8cfO0yM5dhamxz3g==",
|
||||
"version": "0.3.217",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.217.tgz",
|
||||
"integrity": "sha512-dl119zmL1Ssyd8Fx0xfVMpss2scrGCZwf+rhZwl2lHa2dYuXVluLgqi4DUIWDj3rRYdrAvaMpjCAv6a5w07ddw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -82,9 +82,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-darwin-x64": {
|
||||
"version": "0.3.207",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.207.tgz",
|
||||
"integrity": "sha512-1o7K4EYqyCixZ/oeOZSh7AzSy6TM86xoOuf4VuORjPSS31hBnoqY0NGZd27+2VDs9LGtsdksmsTqcNGx9xd1hA==",
|
||||
"version": "0.3.217",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.217.tgz",
|
||||
"integrity": "sha512-IeKL1HN8fEcRQ4uw5d02by1ThpjhRtOgfHcCTBQ2KS4JfEIHvc1VGWt6Exb2a7VHhT8uRcfjPk9urbmYayZmaw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -95,9 +95,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64": {
|
||||
"version": "0.3.207",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.207.tgz",
|
||||
"integrity": "sha512-X4uezYOifDiNTTmmugfRCdg3nNamrr1LFRY9hg30vWYTShL+bbN+nfC3KaFfSYCl4GTtsEEUbYdOTC2F3bBpcA==",
|
||||
"version": "0.3.217",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.217.tgz",
|
||||
"integrity": "sha512-KtrnfEwUSCdq2cc4Pgysl+U66vqw3h7u04N5/OLHmYZ4AZYy8JcqdOaSJZ27iL2bgbAxyKwu5/9YmEk9A4IswA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -111,9 +111,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64-musl": {
|
||||
"version": "0.3.207",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.207.tgz",
|
||||
"integrity": "sha512-oPj+g2DslhH4Y9nCTs7al4t9wZv78FZwLFQwOCg99BXuz1o0ZOpKmxyvR7J9eBR+GPszeMMS8gYplQTiZC9o2w==",
|
||||
"version": "0.3.217",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.217.tgz",
|
||||
"integrity": "sha512-Bb4AJxqrVPouM4sYIdvX3/AO5womhe70u3Euv+6B5J2OoqcRaWarVvYevX3KRruC5TvlV2Josw14dsL5qVNL+A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -127,9 +127,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-linux-x64": {
|
||||
"version": "0.3.207",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.207.tgz",
|
||||
"integrity": "sha512-Kg6BPH8Ee0ny/oEUWJmvT1jCRBne4jVpRSOMsJcYp1Fav1rMEgpU219oJJs+LWwx4ifuuLtNWedqJNnVw7mnKg==",
|
||||
"version": "0.3.217",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.217.tgz",
|
||||
"integrity": "sha512-JsAQyfl4n0PR4LX0h1SxMo0raERGb8B8dvbaoNQRRSpb9A2vvcwPEjyKu0eRKHRhTvspvuD6TfNxzxrmnouX9A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -143,9 +143,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-linux-x64-musl": {
|
||||
"version": "0.3.207",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.207.tgz",
|
||||
"integrity": "sha512-uRv+D5oG/7EYr41FAJ9IPo2pZYBe2ZMaA6nSHCeizsgPxCSMtl5bNppmU21+jZJvo4hivObEgkGFERAhdGqygg==",
|
||||
"version": "0.3.217",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.217.tgz",
|
||||
"integrity": "sha512-qhugNZd77vAoPMIGM8vFHlbwTltFyI1POmfyl0ZJSpc6v7RE9+5+nqL2aGbGSDsDQkEHrJasXURxIeTMn9ut2w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -159,9 +159,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-win32-arm64": {
|
||||
"version": "0.3.207",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.207.tgz",
|
||||
"integrity": "sha512-9fWpUzfkXlPAg2tf8JpQe7w9avFaomAUbfAwyAmykQgSIf66LwaJjvI5hNqhNqczRKyfsXPn3ei2S5HKlmFP+Q==",
|
||||
"version": "0.3.217",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.217.tgz",
|
||||
"integrity": "sha512-LuaQ+PXZvIToAR81JoiGa6Me9HDma2WH2oiYlAWh43IWaXHyOqgaI1aqSM0BjDhy2UiYWTvGzAopnqPnk+jSBw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -172,9 +172,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk-win32-x64": {
|
||||
"version": "0.3.207",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.207.tgz",
|
||||
"integrity": "sha512-YPjVT0q6aXEM2MgN4CI6/9fqiTXwETji+4NoPOzCYuqAkhXZqp30Jsk7/NHqYGNNSfURKrsuAoliKB0rsbpbjg==",
|
||||
"version": "0.3.217",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.217.tgz",
|
||||
"integrity": "sha512-4r/T+ze/S/CLZ58tP4Mw52XPmsc/LOrCOd8jZOqM13FCPWdCMU2osWmszEIKGVMRG2cGsaLVDYcks5cWFqjCjw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -185,9 +185,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@anthropic-ai/sdk": {
|
||||
"version": "0.111.0",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.111.0.tgz",
|
||||
"integrity": "sha512-1hUqKi+uJQoS5X90+InwHbFAXMvgq0DnsC5hVLEeSRaODiU5WvmqDAcVCmGS2wC0pN9Z8jtWCbWw7JLzeDdm/Q==",
|
||||
"version": "0.112.5",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.112.5.tgz",
|
||||
"integrity": "sha512-FaaGkyS4/zW4n+8Pr9jQkyo5zWcBNw+R+heCLXdoKDUEuALbPALBk6zLAlmsU+veREiaATxVG0TwkK/cQ3NZsQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"json-schema-to-ts": "^3.1.1",
|
||||
@@ -262,16 +262,16 @@
|
||||
}
|
||||
},
|
||||
"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.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.0.tgz",
|
||||
"integrity": "sha512-rd6n9Sefya1R1Vd594TRwzeHq2VnmICbvh3jebyt1Hs/6OoCSYNVpjGRRsKSTwAq3SKl09w0AfRCU57hoDLmxg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eslint-visitor-keys": "^3.4.3"
|
||||
"eslint-visitor-keys": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
@@ -944,17 +944,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.63.0.tgz",
|
||||
"integrity": "sha512-rvwSgqT+DHpWdzfSzPatRLm02a0GlESt++9iy3hLCDY4BgkaLcl8LBi9Yh7XGFBpwcBE/K3024QuXWTpbz4FfQ==",
|
||||
"version": "8.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.63.0",
|
||||
"@typescript-eslint/type-utils": "8.63.0",
|
||||
"@typescript-eslint/utils": "8.63.0",
|
||||
"@typescript-eslint/visitor-keys": "8.63.0",
|
||||
"@typescript-eslint/scope-manager": "8.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 +967,22 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.63.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.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.63.0.tgz",
|
||||
"integrity": "sha512-gwh4gvvlaVDKKxyfxMG+Gnu1u9X0OQBwyGLkbwB65dIzBKnxeRiJlNFqlI3zwVhNXJIs6qV7mlFCn/BIajlVig==",
|
||||
"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.63.0",
|
||||
"@typescript-eslint/types": "8.63.0",
|
||||
"@typescript-eslint/typescript-estree": "8.63.0",
|
||||
"@typescript-eslint/visitor-keys": "8.63.0",
|
||||
"@typescript-eslint/scope-manager": "8.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 +998,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.63.0.tgz",
|
||||
"integrity": "sha512-e5dh0/UI0ok53AlZ5wRkXCB32z/f2jUZqPR/ygAw5WYaSw8j9EoJWlS7wQjr/dmOaqWjnPIn2m+HhVPCMWGZVQ==",
|
||||
"version": "8.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.63.0",
|
||||
"@typescript-eslint/types": "^8.63.0",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.65.0",
|
||||
"@typescript-eslint/types": "^8.65.0",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1020,14 +1020,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.63.0.tgz",
|
||||
"integrity": "sha512-uUyfMWCnDSN8bCpcrY8nGP2BLkQ9Xn0GsipcONcpIDWhwhO4ZSyHvyS14U3X75mzxWxL3I2UZIrenTzdzcJO8A==",
|
||||
"version": "8.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.63.0",
|
||||
"@typescript-eslint/visitor-keys": "8.63.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 +1038,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.63.0.tgz",
|
||||
"integrity": "sha512-sUAbkulqBAsncKnbRP3+7CtQFRKicexnj7ZwNC6ddCR7EmrXvjvdCYMJbUIqMd6lwoEriZjwLo08aS5tSjVMHg==",
|
||||
"version": "8.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 +1055,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.63.0.tgz",
|
||||
"integrity": "sha512-Nzzh/OGxVCOjObjaj1CQF2RUasyYy2Jfuh+zZ3PjLzG2fYRriAiZLib9UKtO+CpQAS3YHiAS+ckZDclwqI1TPA==",
|
||||
"version": "8.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.63.0",
|
||||
"@typescript-eslint/typescript-estree": "8.63.0",
|
||||
"@typescript-eslint/utils": "8.63.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 +1080,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.63.0.tgz",
|
||||
"integrity": "sha512-xyLtl9DUBBFrcJS4x2pIqGLH68/tC2uOa4Z7pUteW09D3bXnnXUom4dyPikzWgB7llmIc1zoeI3aoUdC4rPK/Q==",
|
||||
"version": "8.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 +1094,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.63.0.tgz",
|
||||
"integrity": "sha512-ygBkU+B7ex5UI/gKhaqexWev79uISfIv7XQCRNYO/jmD8rGLPyWLAb3KMRT6nd8Gt9bmUBi9+iX6tBdYfOY81Q==",
|
||||
"version": "8.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.63.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.63.0",
|
||||
"@typescript-eslint/types": "8.63.0",
|
||||
"@typescript-eslint/visitor-keys": "8.63.0",
|
||||
"@typescript-eslint/project-service": "8.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 +1122,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.63.0.tgz",
|
||||
"integrity": "sha512-fUKaeAvrTuQg/Tgt3nliAUSZHJM6DlCcfyEmxCvlX8kieWSStBX+5O5Fnidtc3i2JrH+9c/GL4RY2iasd/GPTA==",
|
||||
"version": "8.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.63.0",
|
||||
"@typescript-eslint/types": "8.63.0",
|
||||
"@typescript-eslint/typescript-estree": "8.63.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 +1146,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.63.0.tgz",
|
||||
"integrity": "sha512-UexrHGnGTpbuQHct2ExOc2ZcFbGUS9FOesCxxqdBGcpI1BxYu/LZ6U8Aq6/72XtF/qRBk9nhuGHFJIXXMhPMdw==",
|
||||
"version": "8.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.63.0",
|
||||
"@typescript-eslint/types": "8.65.0",
|
||||
"eslint-visitor-keys": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1163,19 +1163,6 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
|
||||
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "4.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz",
|
||||
@@ -1835,13 +1822,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-visitor-keys": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
|
||||
"integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
|
||||
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
@@ -1864,19 +1851,6 @@
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/eslint-visitor-keys": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
|
||||
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
@@ -1912,19 +1886,6 @@
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/espree/node_modules/eslint-visitor-keys": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
|
||||
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/esquery": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
|
||||
@@ -2069,12 +2030,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/express-rate-limit": {
|
||||
"version": "8.5.2",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz",
|
||||
"integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==",
|
||||
"version": "8.6.0",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.0.tgz",
|
||||
"integrity": "sha512-XKJXDsASUOo0LLtFwW5hCcQGH0N4WQc/Rn8/Pvoia+TJFOkkFPvrtW9lZOeeNcxQJspvOIERMwiRLsVFlhHEkA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"debug": "^4.4.3",
|
||||
"ip-address": "^10.2.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2114,9 +2076,9 @@
|
||||
"license": "Unlicense"
|
||||
},
|
||||
"node_modules/fast-uri": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz",
|
||||
"integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==",
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz",
|
||||
"integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -2371,9 +2333,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/hono": {
|
||||
"version": "4.12.30",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.30.tgz",
|
||||
"integrity": "sha512-emn+JoJjrN9YTpRDS5it/UI2SO9BAE37T6I3d963RxcZ81G9A4pr2SZTEiiaiKbzx+NKRg5BZ89fCL7gCJCUog==",
|
||||
"version": "4.12.31",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.31.tgz",
|
||||
"integrity": "sha512-zJIHFrl6bq3RDd2YusFNCDlM8qUprxKswyi/OPzPyzKDdyBXDqWx8bZlZ7R+saTdSTatUmb3O7K4SspGPaEOQg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
@@ -2419,9 +2381,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "7.0.5",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
|
||||
"integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz",
|
||||
"integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -2502,9 +2464,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": {
|
||||
@@ -2577,9 +2539,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
||||
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
|
||||
"integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==",
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
@@ -2593,23 +2555,23 @@
|
||||
"url": "https://opencollective.com/parcel"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"lightningcss-android-arm64": "1.32.0",
|
||||
"lightningcss-darwin-arm64": "1.32.0",
|
||||
"lightningcss-darwin-x64": "1.32.0",
|
||||
"lightningcss-freebsd-x64": "1.32.0",
|
||||
"lightningcss-linux-arm-gnueabihf": "1.32.0",
|
||||
"lightningcss-linux-arm64-gnu": "1.32.0",
|
||||
"lightningcss-linux-arm64-musl": "1.32.0",
|
||||
"lightningcss-linux-x64-gnu": "1.32.0",
|
||||
"lightningcss-linux-x64-musl": "1.32.0",
|
||||
"lightningcss-win32-arm64-msvc": "1.32.0",
|
||||
"lightningcss-win32-x64-msvc": "1.32.0"
|
||||
"lightningcss-android-arm64": "1.33.0",
|
||||
"lightningcss-darwin-arm64": "1.33.0",
|
||||
"lightningcss-darwin-x64": "1.33.0",
|
||||
"lightningcss-freebsd-x64": "1.33.0",
|
||||
"lightningcss-linux-arm-gnueabihf": "1.33.0",
|
||||
"lightningcss-linux-arm64-gnu": "1.33.0",
|
||||
"lightningcss-linux-arm64-musl": "1.33.0",
|
||||
"lightningcss-linux-x64-gnu": "1.33.0",
|
||||
"lightningcss-linux-x64-musl": "1.33.0",
|
||||
"lightningcss-win32-arm64-msvc": "1.33.0",
|
||||
"lightningcss-win32-x64-msvc": "1.33.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-android-arm64": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
|
||||
"integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz",
|
||||
"integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2628,9 +2590,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-darwin-arm64": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
|
||||
"integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz",
|
||||
"integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2649,9 +2611,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-darwin-x64": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
|
||||
"integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz",
|
||||
"integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2670,9 +2632,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-freebsd-x64": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
|
||||
"integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz",
|
||||
"integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2691,9 +2653,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm-gnueabihf": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
|
||||
"integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz",
|
||||
"integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -2712,9 +2674,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm64-gnu": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
|
||||
"integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz",
|
||||
"integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2736,9 +2698,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm64-musl": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
|
||||
"integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz",
|
||||
"integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2760,9 +2722,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-x64-gnu": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
|
||||
"integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz",
|
||||
"integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2784,9 +2746,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-x64-musl": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
|
||||
"integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz",
|
||||
"integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2808,9 +2770,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-win32-arm64-msvc": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
|
||||
"integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz",
|
||||
"integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2829,9 +2791,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-win32-x64-msvc": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
|
||||
"integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz",
|
||||
"integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -3024,9 +2986,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/obug": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz",
|
||||
"integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==",
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz",
|
||||
"integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
"https://github.com/sponsors/sxzz",
|
||||
@@ -3188,9 +3150,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.19",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz",
|
||||
"integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==",
|
||||
"version": "8.5.22",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.22.tgz",
|
||||
"integrity": "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -3208,7 +3170,7 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.12",
|
||||
"nanoid": "^3.3.16",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
@@ -3227,9 +3189,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": {
|
||||
@@ -3826,16 +3788,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "8.1.4",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.4.tgz",
|
||||
"integrity": "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ==",
|
||||
"version": "8.1.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz",
|
||||
"integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lightningcss": "^1.32.0",
|
||||
"picomatch": "^4.0.5",
|
||||
"postcss": "^8.5.16",
|
||||
"rolldown": "~1.1.4",
|
||||
"postcss": "^8.5.17",
|
||||
"rolldown": "~1.1.5",
|
||||
"tinyglobby": "^0.2.17"
|
||||
},
|
||||
"bin": {
|
||||
|
||||
14
package.json
14
package.json
@@ -3,7 +3,7 @@
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"version": "0.59.0",
|
||||
"version": "0.61.0",
|
||||
"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.207",
|
||||
"@agentclientprotocol/sdk": "1.3.0",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.3.217",
|
||||
"zod": "^3.25.0 || ^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@anthropic-ai/sdk": "0.111.0",
|
||||
"@anthropic-ai/sdk": "0.112.5",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@tsconfig/node22": "22.0.5",
|
||||
"@types/node": "26.1.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.63.0",
|
||||
"@typescript-eslint/parser": "8.63.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.65.0",
|
||||
"@typescript-eslint/parser": "8.65.0",
|
||||
"eslint": "10.7.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"globals": "17.7.0",
|
||||
"prettier": "3.9.5",
|
||||
"prettier": "3.9.6",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "6.0.3",
|
||||
"vitest": "4.1.10"
|
||||
|
||||
890
src/acp-agent.ts
890
src/acp-agent.ts
File diff suppressed because it is too large
Load Diff
@@ -157,6 +157,8 @@ function mockSessionState(overrides: Record<string, any> = {}) {
|
||||
abortController: new AbortController(),
|
||||
emitRawSDKMessages: false,
|
||||
contextWindowSize: 200000,
|
||||
contextWindowAuthoritative: false,
|
||||
providerCacheKey: "default",
|
||||
taskState: new Map(),
|
||||
toolUseCache: {},
|
||||
emittedToolCalls: new Set(),
|
||||
@@ -1947,6 +1949,8 @@ describe("permission request cancellation", () => {
|
||||
abortController: new AbortController(),
|
||||
emitRawSDKMessages: false,
|
||||
contextWindowSize: 200000,
|
||||
contextWindowAuthoritative: false,
|
||||
providerCacheKey: "default",
|
||||
taskState: new Map(),
|
||||
toolUseCache: {},
|
||||
emittedToolCalls: new Set(),
|
||||
@@ -2300,6 +2304,63 @@ describe("tool_call emitted before permission request", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("canUseTool in bypassPermissions mode", () => {
|
||||
function setup() {
|
||||
const events: string[] = [];
|
||||
const mockClient = {
|
||||
sessionUpdate: async () => {},
|
||||
requestPermission: async () => {
|
||||
events.push("permission");
|
||||
return { outcome: { outcome: "selected", optionId: "allow" } };
|
||||
},
|
||||
} as unknown as AcpClient;
|
||||
const agent = new ClaudeAcpAgent(mockClient, { log: () => {}, error: () => {} });
|
||||
agent.sessions["session-1"] = mockSessionState({
|
||||
modes: { currentModeId: "bypassPermissions", availableModes: [] },
|
||||
});
|
||||
// The tool_call was already surfaced by the streamed tool_use chunk, so
|
||||
// the permission flow (when taken) goes straight to requestPermission.
|
||||
agent.sessions["session-1"]!.emittedToolCalls.add("tool-1");
|
||||
return { agent, events };
|
||||
}
|
||||
|
||||
it("auto-allows asks that carry no matchedAskRule", async () => {
|
||||
const { agent, events } = setup();
|
||||
|
||||
const result = await agent.canUseTool("session-1")("Bash", { command: "ls" }, {
|
||||
signal: new AbortController().signal,
|
||||
suggestions: [],
|
||||
toolUseID: "tool-1",
|
||||
} as any);
|
||||
|
||||
expect(events).toEqual([]);
|
||||
expect(result).toMatchObject({ behavior: "allow" });
|
||||
});
|
||||
|
||||
// The asks that still reach canUseTool in bypass mode are the ones the CLI
|
||||
// insists on prompting for even under --dangerously-skip-permissions. When
|
||||
// one was forced by the user's own permissions.ask rule (matchedAskRule),
|
||||
// honoring that rule beats bypass: it must go to the client instead of
|
||||
// being silently auto-allowed.
|
||||
it("prompts the client when the ask was forced by a permissions.ask rule", async () => {
|
||||
const { agent, events } = setup();
|
||||
|
||||
const result = await agent.canUseTool("session-1")("Bash", { command: "terraform destroy" }, {
|
||||
signal: new AbortController().signal,
|
||||
suggestions: [],
|
||||
toolUseID: "tool-1",
|
||||
matchedAskRule: {
|
||||
source: "projectSettings",
|
||||
toolName: "Bash",
|
||||
ruleContent: "Bash(terraform:*)",
|
||||
},
|
||||
} as any);
|
||||
|
||||
expect(events).toEqual(["permission"]);
|
||||
expect(result).toMatchObject({ behavior: "allow" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("subagent permission attribution (issue #851)", () => {
|
||||
// A background subagent's permission requests reach canUseTool with only an
|
||||
// `agentID`; the streamed subagent messages carry `parent_tool_use_id`
|
||||
@@ -3730,6 +3791,8 @@ describe("session/close", () => {
|
||||
abortController: new AbortController(),
|
||||
emitRawSDKMessages: false,
|
||||
contextWindowSize: 200000,
|
||||
contextWindowAuthoritative: false,
|
||||
providerCacheKey: "default",
|
||||
taskState: new Map(),
|
||||
toolUseCache: {},
|
||||
emittedToolCalls: new Set(),
|
||||
@@ -3820,6 +3883,8 @@ describe("session/delete", () => {
|
||||
abortController: new AbortController(),
|
||||
emitRawSDKMessages: false,
|
||||
contextWindowSize: 200000,
|
||||
contextWindowAuthoritative: false,
|
||||
providerCacheKey: "default",
|
||||
taskState: new Map(),
|
||||
toolUseCache: {},
|
||||
emittedToolCalls: new Set(),
|
||||
@@ -3927,6 +3992,8 @@ describe("getOrCreateSession param change detection", () => {
|
||||
abortController: new AbortController(),
|
||||
emitRawSDKMessages: false,
|
||||
contextWindowSize: 200000,
|
||||
contextWindowAuthoritative: false,
|
||||
providerCacheKey: "default",
|
||||
taskState: new Map(),
|
||||
toolUseCache: {},
|
||||
emittedToolCalls: new Set(),
|
||||
@@ -5045,6 +5112,359 @@ describe("usage_update computation", () => {
|
||||
expect(usageUpdate.update.size).toBe(200000);
|
||||
expect(session.contextWindowSize).toBe(200000);
|
||||
});
|
||||
|
||||
it("caches the turn's authoritative window under the resolved id and serves it on a later switch, with no getContextUsage IPC", async () => {
|
||||
// End-to-end for the cross-session context-window cache:
|
||||
// - WRITE: a turn's result.modelUsage is the only authoritative window. The
|
||||
// assistant message reports the BARE model id ("…-9") while modelUsage is
|
||||
// keyed by the RESOLVED id ("…-9[1m]"); the cache must be written under the
|
||||
// resolved key (matched by getMatchingModelUsage), the same spelling as
|
||||
// ModelInfo.resolvedModel — otherwise a later read never hits.
|
||||
// - READ: switching to a picker value whose resolvedModel is that key seeds
|
||||
// the window synchronously from the cache, with NO getContextUsage.
|
||||
// 777_000 is chosen so it can only come from the cache: text inference on the
|
||||
// resolved id "…-9[1m]" would yield 1_000_000 (the "1m" token), the default
|
||||
// is 200_000, and the pre-switch sentinel is 123_456.
|
||||
//
|
||||
// The `contextWindowCache` is module-global and this file does not
|
||||
// vi.resetModules() per test, so it persists across tests. This test stays
|
||||
// isolated by using a unique resolved id ("claude-cachehit-probe-9[1m]") that
|
||||
// no other test writes or reads — the convention here, since a statically
|
||||
// imported module's cache can't be cleared from a test.
|
||||
const RESOLVED_ID = "claude-cachehit-probe-9[1m]";
|
||||
const { agent } = createMockAgentWithCapture();
|
||||
injectSession(agent, [
|
||||
createAssistantMessage({ model: "claude-cachehit-probe-9" }),
|
||||
createResultMessageWithModel({
|
||||
modelUsage: {
|
||||
[RESOLVED_ID]: {
|
||||
inputTokens: 1,
|
||||
outputTokens: 1,
|
||||
cacheReadInputTokens: 0,
|
||||
cacheCreationInputTokens: 0,
|
||||
webSearchRequests: 0,
|
||||
costUSD: 0,
|
||||
contextWindow: 777_000,
|
||||
maxOutputTokens: 16384,
|
||||
},
|
||||
},
|
||||
}),
|
||||
{ type: "system", subtype: "session_state_changed", state: "idle" },
|
||||
]);
|
||||
|
||||
await agent.prompt({ sessionId: "test-session", prompt: [{ type: "text", text: "go" }] });
|
||||
|
||||
const session = agent.sessions["test-session"]!;
|
||||
// The turn learned the window from modelUsage even though the assistant
|
||||
// message carried the bare id — confirms the write matched on the resolved key.
|
||||
expect(session.contextWindowSize).toBe(777_000);
|
||||
|
||||
// Now switch to a picker value that resolves to the cached id. Seed a
|
||||
// sentinel and a getContextUsage spy first: a cache miss would surface as
|
||||
// 1_000_000 (inference on the "1m" id), and any IPC as a spy call.
|
||||
const getContextUsage = vi.fn(async () => ({ rawMaxTokens: 200000 }));
|
||||
(session.query as any).getContextUsage = getContextUsage;
|
||||
session.contextWindowSize = 123_456;
|
||||
session.models = { currentModelId: "default", availableModels: [] };
|
||||
session.modelInfos = [
|
||||
{
|
||||
value: "probe-alias",
|
||||
displayName: "Probe",
|
||||
description: "probe model",
|
||||
resolvedModel: RESOLVED_ID,
|
||||
},
|
||||
] as any;
|
||||
session.configOptions = [
|
||||
{
|
||||
id: "model",
|
||||
name: "Model",
|
||||
type: "select",
|
||||
category: "model",
|
||||
currentValue: "default",
|
||||
options: [{ value: "probe-alias", name: "Probe" }],
|
||||
},
|
||||
] as any;
|
||||
|
||||
await agent.setSessionConfigOption({
|
||||
sessionId: "test-session",
|
||||
configId: "model",
|
||||
value: "probe-alias",
|
||||
});
|
||||
|
||||
expect(getContextUsage).not.toHaveBeenCalled();
|
||||
expect(session.contextWindowSize).toBe(777_000);
|
||||
});
|
||||
|
||||
it("scopes the window cache per provider: a switch on a different provider does not read another provider's window", async () => {
|
||||
// The window is a property of (model id, provider). A turn on provider-A
|
||||
// learns 500_000 for RESOLVED_ID; a switch to the SAME resolved id on
|
||||
// provider-B must NOT read it (falls to inference → 1_000_000 for the "1m"
|
||||
// id), while a switch on provider-A DOES read it (500_000). All three
|
||||
// outcomes are distinct: cached 500_000 vs inference 1_000_000 vs default.
|
||||
// Uses a unique resolved id ("claude-provkey-probe[1m]") so the module-global
|
||||
// cache (not reset per test in this file) can't cross this test with others.
|
||||
const RESOLVED_ID = "claude-provkey-probe[1m]";
|
||||
const { agent } = createMockAgentWithCapture();
|
||||
|
||||
// Provider-A session learns the authoritative window on a turn.
|
||||
injectSession(agent, [
|
||||
createAssistantMessage({ model: "claude-provkey-probe" }),
|
||||
createResultMessageWithModel({
|
||||
modelUsage: {
|
||||
[RESOLVED_ID]: {
|
||||
inputTokens: 1,
|
||||
outputTokens: 1,
|
||||
cacheReadInputTokens: 0,
|
||||
cacheCreationInputTokens: 0,
|
||||
webSearchRequests: 0,
|
||||
costUSD: 0,
|
||||
contextWindow: 500_000,
|
||||
maxOutputTokens: 16384,
|
||||
},
|
||||
},
|
||||
}),
|
||||
{ type: "system", subtype: "session_state_changed", state: "idle" },
|
||||
]);
|
||||
const sessionA = agent.sessions["test-session"]!;
|
||||
sessionA.providerCacheKey = "apiType-A https://a.example";
|
||||
await agent.prompt({ sessionId: "test-session", prompt: [{ type: "text", text: "go" }] });
|
||||
expect(sessionA.contextWindowSize).toBe(500_000);
|
||||
|
||||
const modelInfos = [
|
||||
{
|
||||
value: "alias",
|
||||
displayName: "Alias",
|
||||
description: "probe model",
|
||||
resolvedModel: RESOLVED_ID,
|
||||
},
|
||||
];
|
||||
const configOptions = [
|
||||
{
|
||||
id: "model",
|
||||
name: "Model",
|
||||
type: "select",
|
||||
category: "model",
|
||||
currentValue: "default",
|
||||
options: [{ value: "alias", name: "Alias" }],
|
||||
},
|
||||
];
|
||||
|
||||
// A DIFFERENT provider seeing the same resolved id must not inherit A's
|
||||
// window — it falls to inference (1_000_000), not the cached 500_000.
|
||||
agent.sessions["session-B"] = mockSessionState({
|
||||
providerCacheKey: "apiType-B https://b.example",
|
||||
models: { currentModelId: "default", availableModels: [] },
|
||||
modelInfos,
|
||||
configOptions,
|
||||
query: {
|
||||
setModel: vi.fn(async () => {}),
|
||||
setPermissionMode: vi.fn(async () => {}),
|
||||
applyFlagSettings: vi.fn(async () => {}),
|
||||
getContextUsage: vi.fn(async () => ({ rawMaxTokens: 200000 })),
|
||||
supportedCommands: vi.fn(async () => []),
|
||||
},
|
||||
});
|
||||
await agent.setSessionConfigOption({
|
||||
sessionId: "session-B",
|
||||
configId: "model",
|
||||
value: "alias",
|
||||
});
|
||||
expect(agent.sessions["session-B"]!.contextWindowSize).toBe(1_000_000);
|
||||
|
||||
// The SAME provider (A) switching to that id DOES read the cached window.
|
||||
sessionA.contextWindowSize = 123_456; // sentinel
|
||||
sessionA.models = { currentModelId: "default", availableModels: [] };
|
||||
sessionA.modelInfos = modelInfos as any;
|
||||
sessionA.configOptions = configOptions as any;
|
||||
await agent.setSessionConfigOption({
|
||||
sessionId: "test-session",
|
||||
configId: "model",
|
||||
value: "alias",
|
||||
});
|
||||
expect(sessionA.contextWindowSize).toBe(500_000);
|
||||
});
|
||||
|
||||
it("ignores a nonsensical (non-positive) reported window: keeps the prior window and doesn't poison the cache", async () => {
|
||||
// A result.modelUsage that reports a non-positive contextWindow (observed
|
||||
// from third-party backends) must not overwrite the window learned earlier,
|
||||
// nor be written to the cross-session cache. Unique id keeps this isolated
|
||||
// from other tests sharing the module-global cache (no resetModules here).
|
||||
const RESOLVED_ID = "claude-nonpositive-probe[1m]";
|
||||
const { agent } = createMockAgentWithCapture();
|
||||
injectSession(agent, [
|
||||
createAssistantMessage({ model: "claude-nonpositive-probe" }),
|
||||
createResultMessageWithModel({
|
||||
modelUsage: {
|
||||
[RESOLVED_ID]: {
|
||||
inputTokens: 1,
|
||||
outputTokens: 1,
|
||||
cacheReadInputTokens: 0,
|
||||
cacheCreationInputTokens: 0,
|
||||
webSearchRequests: 0,
|
||||
costUSD: 0,
|
||||
contextWindow: 0, // nonsensical
|
||||
maxOutputTokens: 16384,
|
||||
},
|
||||
},
|
||||
}),
|
||||
{ type: "system", subtype: "session_state_changed", state: "idle" },
|
||||
]);
|
||||
const session = agent.sessions["test-session"]!;
|
||||
session.contextWindowSize = 900_000; // a window learned on a prior turn
|
||||
|
||||
await agent.prompt({ sessionId: "test-session", prompt: [{ type: "text", text: "go" }] });
|
||||
|
||||
// The bad window was ignored — the prior window is preserved.
|
||||
expect(session.contextWindowSize).toBe(900_000);
|
||||
|
||||
// And it never reached the cache: a switch to that id falls to inference
|
||||
// (1_000_000 for the "1m" id), not the bad 0.
|
||||
session.contextWindowSize = 123_456; // sentinel
|
||||
session.models = { currentModelId: "default", availableModels: [] };
|
||||
session.modelInfos = [
|
||||
{ value: "alias", displayName: "Alias", description: "probe", resolvedModel: RESOLVED_ID },
|
||||
] as any;
|
||||
session.configOptions = [
|
||||
{
|
||||
id: "model",
|
||||
name: "Model",
|
||||
type: "select",
|
||||
category: "model",
|
||||
currentValue: "default",
|
||||
options: [{ value: "alias", name: "Alias" }],
|
||||
},
|
||||
] as any;
|
||||
await agent.setSessionConfigOption({
|
||||
sessionId: "test-session",
|
||||
configId: "model",
|
||||
value: "alias",
|
||||
});
|
||||
expect(session.contextWindowSize).toBe(1_000_000);
|
||||
});
|
||||
|
||||
it("does not let the message_start heuristic clobber an authoritative 200k window", async () => {
|
||||
// An authoritative window can legitimately equal DEFAULT_CONTEXT_WINDOW
|
||||
// (e.g. a third-party backend serving a 200k lane under a "[1m]"-spelled
|
||||
// id). The message_start upgrade must key off the authoritative flag, not
|
||||
// the value — otherwise the "1m" text match overwrites the cache-seeded
|
||||
// 200k with 1M mid-stream on every turn.
|
||||
const { agent, updates } = createMockAgentWithCapture();
|
||||
injectSession(agent, [
|
||||
createStreamEvent("message_start", {
|
||||
model: "claude-authprobe-1[1m]",
|
||||
usage: {
|
||||
input_tokens: 100,
|
||||
output_tokens: 10,
|
||||
cache_read_input_tokens: 0,
|
||||
cache_creation_input_tokens: 0,
|
||||
},
|
||||
}),
|
||||
// Empty modelUsage: the result settles the turn without supplying a
|
||||
// window of its own, so the assertion isolates the message_start path.
|
||||
createResultMessageWithModel({ modelUsage: {} }),
|
||||
{ type: "system", subtype: "session_state_changed", state: "idle" },
|
||||
]);
|
||||
const session = agent.sessions["test-session"]!;
|
||||
// Simulate a cache-seeded authoritative window that equals the default.
|
||||
session.contextWindowSize = 200000;
|
||||
session.contextWindowAuthoritative = true;
|
||||
|
||||
await agent.prompt({ sessionId: "test-session", prompt: [{ type: "text", text: "go" }] });
|
||||
|
||||
expect(session.contextWindowSize).toBe(200000);
|
||||
const usageUpdates = updates.filter((u: any) => u.update?.sessionUpdate === "usage_update");
|
||||
for (const u of usageUpdates) {
|
||||
expect(u.update.size).toBe(200000);
|
||||
}
|
||||
});
|
||||
|
||||
it("still upgrades a heuristic default window from the message_start model id", async () => {
|
||||
// Companion to the authoritative-200k test: with no authoritative seed the
|
||||
// old behavior stands — a "1m"-carrying live model id upgrades the default
|
||||
// mid-stream so usage_update reports the right size before the result.
|
||||
const { agent } = createMockAgentWithCapture();
|
||||
injectSession(agent, [
|
||||
createStreamEvent("message_start", {
|
||||
model: "claude-authprobe-2[1m]",
|
||||
usage: {
|
||||
input_tokens: 100,
|
||||
output_tokens: 10,
|
||||
cache_read_input_tokens: 0,
|
||||
cache_creation_input_tokens: 0,
|
||||
},
|
||||
}),
|
||||
// Empty modelUsage: settles the turn without a window of its own.
|
||||
createResultMessageWithModel({ modelUsage: {} }),
|
||||
{ type: "system", subtype: "session_state_changed", state: "idle" },
|
||||
]);
|
||||
const session = agent.sessions["test-session"]!;
|
||||
expect(session.contextWindowAuthoritative).toBe(false);
|
||||
|
||||
await agent.prompt({ sessionId: "test-session", prompt: [{ type: "text", text: "go" }] });
|
||||
|
||||
expect(session.contextWindowSize).toBe(1_000_000);
|
||||
});
|
||||
|
||||
it("caches the turn's window under the bare assistant-message id too, so resolvedModel-less rows can hit", async () => {
|
||||
// Seed-time reads fall back to the picker value / verbatim live id when a
|
||||
// model row carries no resolvedModel (the synthesized out-of-allowlist
|
||||
// resume row sets it undefined on purpose). Those spellings match the
|
||||
// assistant message's bare `.model`, not the "[1m]"-decorated modelUsage
|
||||
// key, so the result handler must write both spellings — otherwise such
|
||||
// rows silently never hit the cache. 555_000 can only come from the cache:
|
||||
// inference on the bare id (no "1m" token) yields the 200_000 default, and
|
||||
// the pre-switch sentinel is 123_456.
|
||||
const BARE_ID = "claude-bareprobe-7";
|
||||
const { agent } = createMockAgentWithCapture();
|
||||
injectSession(agent, [
|
||||
createAssistantMessage({ model: BARE_ID }),
|
||||
createResultMessageWithModel({
|
||||
modelUsage: {
|
||||
[`${BARE_ID}[1m]`]: {
|
||||
inputTokens: 1,
|
||||
outputTokens: 1,
|
||||
cacheReadInputTokens: 0,
|
||||
cacheCreationInputTokens: 0,
|
||||
webSearchRequests: 0,
|
||||
costUSD: 0,
|
||||
contextWindow: 555_000,
|
||||
maxOutputTokens: 16384,
|
||||
},
|
||||
},
|
||||
}),
|
||||
{ type: "system", subtype: "session_state_changed", state: "idle" },
|
||||
]);
|
||||
const session = agent.sessions["test-session"]!;
|
||||
|
||||
await agent.prompt({ sessionId: "test-session", prompt: [{ type: "text", text: "go" }] });
|
||||
expect(session.contextWindowSize).toBe(555_000);
|
||||
|
||||
// Switch to a row registered under the bare id with NO resolvedModel —
|
||||
// the shape of the out-of-allowlist resume row.
|
||||
session.contextWindowSize = 123_456; // sentinel
|
||||
session.contextWindowAuthoritative = false;
|
||||
session.models = { currentModelId: "default", availableModels: [] };
|
||||
session.modelInfos = [{ value: BARE_ID, displayName: "Bare", description: "probe" }] as any;
|
||||
session.configOptions = [
|
||||
{
|
||||
id: "model",
|
||||
name: "Model",
|
||||
type: "select",
|
||||
category: "model",
|
||||
currentValue: "default",
|
||||
options: [{ value: BARE_ID, name: "Bare" }],
|
||||
},
|
||||
] as any;
|
||||
|
||||
await agent.setSessionConfigOption({
|
||||
sessionId: "test-session",
|
||||
configId: "model",
|
||||
value: BARE_ID,
|
||||
});
|
||||
|
||||
expect(session.contextWindowSize).toBe(555_000);
|
||||
expect(session.contextWindowAuthoritative).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("assembled assistant text fallback", () => {
|
||||
@@ -6396,6 +6816,8 @@ describe("post-error recovery", () => {
|
||||
abortController: new AbortController(),
|
||||
emitRawSDKMessages: false,
|
||||
contextWindowSize: 200000,
|
||||
contextWindowAuthoritative: false,
|
||||
providerCacheKey: "default",
|
||||
taskState: new Map(),
|
||||
toolUseCache: {},
|
||||
emittedToolCalls: new Set(),
|
||||
@@ -9036,6 +9458,204 @@ describe("deferred settlement for live background subagents (issues #864/#866)",
|
||||
});
|
||||
});
|
||||
|
||||
describe("turn steering (_session/steering)", () => {
|
||||
function createMockAgent() {
|
||||
const mockClient = {
|
||||
sessionUpdate: async () => {},
|
||||
} as unknown as AcpClient;
|
||||
return new ClaudeAcpAgent(mockClient, { log: () => {}, error: () => {} });
|
||||
}
|
||||
|
||||
function createResultMessage() {
|
||||
return {
|
||||
type: "result" as const,
|
||||
subtype: "success" as const,
|
||||
stop_reason: "end_turn",
|
||||
is_error: false,
|
||||
result: "",
|
||||
errors: [],
|
||||
duration_ms: 0,
|
||||
duration_api_ms: 0,
|
||||
num_turns: 1,
|
||||
total_cost_usd: 0,
|
||||
usage: {
|
||||
input_tokens: 10,
|
||||
output_tokens: 5,
|
||||
cache_read_input_tokens: 0,
|
||||
cache_creation_input_tokens: 0,
|
||||
},
|
||||
modelUsage: {},
|
||||
permission_denials: [],
|
||||
uuid: randomUUID(),
|
||||
session_id: "test-session",
|
||||
};
|
||||
}
|
||||
|
||||
const waitFor = async (cond: () => boolean) => {
|
||||
for (let i = 0; i < 200; i++) {
|
||||
if (cond()) return;
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
}
|
||||
throw new Error("waitFor timed out");
|
||||
};
|
||||
|
||||
it("rejects when the session is unknown", async () => {
|
||||
const agent = createMockAgent();
|
||||
await expect(
|
||||
agent.steer({ sessionId: "missing", prompt: [{ type: "text", text: "hi" }] }),
|
||||
).rejects.toThrow("Session not found");
|
||||
});
|
||||
|
||||
it("rejects when the query stream has already closed", async () => {
|
||||
const agent = createMockAgent();
|
||||
agent.sessions["test-session"] = mockSessionState({
|
||||
input: new Pushable<any>(),
|
||||
queryClosed: true,
|
||||
turnQueue: [
|
||||
{
|
||||
promptUuid: "x",
|
||||
isLocalOnlyCommand: false,
|
||||
settled: false,
|
||||
resolve: () => {},
|
||||
reject: () => {},
|
||||
},
|
||||
],
|
||||
});
|
||||
await expect(
|
||||
agent.steer({ sessionId: "test-session", prompt: [{ type: "text", text: "hi" }] }),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("advertises steering support at _meta.steering.supported", async () => {
|
||||
const agent = createMockAgent();
|
||||
const response = await agent.initialize({
|
||||
protocolVersion: 1,
|
||||
clientCapabilities: {},
|
||||
});
|
||||
// Top-level _meta (sibling of agentCapabilities), per the wire protocol.
|
||||
expect((response._meta as any)?.steering).toEqual({ supported: true });
|
||||
});
|
||||
|
||||
it("starts a new turn (outcome 'startedNewTurn') when no turn is in flight (race)", async () => {
|
||||
const agent = createMockAgent();
|
||||
const captured: any[] = [];
|
||||
// Idle session: turnQueue starts empty, so the turn we meant to steer has
|
||||
// (from the agent's view) already finished. Steering must not error — it
|
||||
// starts a fresh turn with the message.
|
||||
injectGeneratorSession(
|
||||
agent,
|
||||
(input) => {
|
||||
async function* messageGenerator() {
|
||||
const iter = input[Symbol.asyncIterator]();
|
||||
const u1 = await iter.next();
|
||||
captured.push(u1.value);
|
||||
yield userEcho(u1.value); // activates the new turn
|
||||
yield createResultMessage();
|
||||
yield { type: "system", subtype: "session_state_changed", state: "idle" };
|
||||
}
|
||||
return messageGenerator();
|
||||
},
|
||||
{ turnQueue: [] },
|
||||
);
|
||||
|
||||
const res = await agent.steer({
|
||||
sessionId: "test-session",
|
||||
prompt: [{ type: "text", text: "late follow-up" }],
|
||||
});
|
||||
expect(res.outcome).toBe("startedNewTurn");
|
||||
|
||||
// A real turn was enqueued and drains like any normal prompt.
|
||||
await waitFor(() => (agent.sessions["test-session"].turnQueue ?? []).length === 0);
|
||||
expect(captured).toHaveLength(1);
|
||||
// It went through the normal prompt() path — no steering delivery priority.
|
||||
expect(captured[0].priority).toBeUndefined();
|
||||
expect(JSON.stringify(captured[0].message.content)).toContain("late follow-up");
|
||||
});
|
||||
|
||||
it("injects (outcome 'injected') a priority:'now' message into the running turn without spawning a new turn", async () => {
|
||||
const agent = createMockAgent();
|
||||
const captured: any[] = [];
|
||||
injectGeneratorSession(agent, (input) => {
|
||||
async function* messageGenerator() {
|
||||
const iter = input[Symbol.asyncIterator]();
|
||||
const u1 = await iter.next();
|
||||
yield userEcho(u1.value); // turn becomes active
|
||||
// The steered message is pushed next; capture it, replay its echo
|
||||
// (which matches no queued turn and must be dropped), then finish.
|
||||
const steered = await iter.next();
|
||||
captured.push(steered.value);
|
||||
yield userEcho(steered.value); // unrelated replay — must NOT settle a turn
|
||||
yield createResultMessage();
|
||||
yield { type: "system", subtype: "session_state_changed", state: "idle" };
|
||||
}
|
||||
return messageGenerator();
|
||||
});
|
||||
|
||||
const turn = agent.prompt({
|
||||
sessionId: "test-session",
|
||||
prompt: [{ type: "text", text: "start" }],
|
||||
});
|
||||
await waitFor(() => !!agent.sessions["test-session"]?.activeTurn);
|
||||
|
||||
const steerRes = await agent.steer({
|
||||
sessionId: "test-session",
|
||||
prompt: [{ type: "text", text: "also handle X" }],
|
||||
});
|
||||
|
||||
// Steering into a live turn reports the 'injected' outcome.
|
||||
expect(steerRes.outcome).toBe("injected");
|
||||
|
||||
// The original turn resolves as a single, normal end_turn — the injected
|
||||
// message steered it rather than producing a second PromptResponse.
|
||||
await expect(turn).resolves.toEqual(expect.objectContaining({ stopReason: "end_turn" }));
|
||||
expect(agent.sessions["test-session"].turnQueue).toHaveLength(0);
|
||||
|
||||
// The pushed message carried priority:'now' and a fresh uuid (matching no
|
||||
// queued turn, so its echo is dropped rather than settling anything).
|
||||
expect(captured).toHaveLength(1);
|
||||
const injected = captured[0];
|
||||
expect(injected.priority).toBe("now");
|
||||
expect(typeof injected.uuid).toBe("string");
|
||||
expect(JSON.stringify(injected.message.content)).toContain("also handle X");
|
||||
});
|
||||
|
||||
it("always injects at 'now' priority, ignoring any client-supplied priority", async () => {
|
||||
const agent = createMockAgent();
|
||||
const captured: any[] = [];
|
||||
injectGeneratorSession(agent, (input) => {
|
||||
async function* messageGenerator() {
|
||||
const iter = input[Symbol.asyncIterator]();
|
||||
const u1 = await iter.next();
|
||||
yield userEcho(u1.value);
|
||||
const steered = await iter.next();
|
||||
captured.push(steered.value);
|
||||
yield createResultMessage();
|
||||
yield { type: "system", subtype: "session_state_changed", state: "idle" };
|
||||
}
|
||||
return messageGenerator();
|
||||
});
|
||||
|
||||
const turn = agent.prompt({
|
||||
sessionId: "test-session",
|
||||
prompt: [{ type: "text", text: "start" }],
|
||||
});
|
||||
await waitFor(() => !!agent.sessions["test-session"]?.activeTurn);
|
||||
|
||||
// Delivery priority is an internal detail, not part of the wire contract.
|
||||
// Even if a client sneaks a `priority` field into the params, it's ignored
|
||||
// and the message is always delivered at 'now'.
|
||||
const res = await agent.steer({
|
||||
sessionId: "test-session",
|
||||
prompt: [{ type: "text", text: "queue this after" }],
|
||||
priority: "next",
|
||||
} as any);
|
||||
await turn;
|
||||
|
||||
expect(res.outcome).toBe("injected");
|
||||
expect(captured[0]?.priority).toBe("now");
|
||||
});
|
||||
});
|
||||
|
||||
describe("session/cancel wedge recovery (issue #680)", () => {
|
||||
function createMockAgent() {
|
||||
const mockClient = {
|
||||
@@ -9107,6 +9727,8 @@ describe("session/cancel wedge recovery (issue #680)", () => {
|
||||
abortController: new AbortController(),
|
||||
emitRawSDKMessages: false,
|
||||
contextWindowSize: 200000,
|
||||
contextWindowAuthoritative: false,
|
||||
providerCacheKey: "default",
|
||||
taskState: new Map(),
|
||||
toolUseCache: {},
|
||||
emittedToolCalls: new Set(),
|
||||
@@ -10376,6 +10998,8 @@ describe("agent selection config option", () => {
|
||||
abortController: new AbortController(),
|
||||
emitRawSDKMessages: false,
|
||||
contextWindowSize: 200000,
|
||||
contextWindowAuthoritative: false,
|
||||
providerCacheKey: "default",
|
||||
taskState: new Map(),
|
||||
toolUseCache: {},
|
||||
emittedToolCalls: new Set(),
|
||||
|
||||
@@ -7,7 +7,7 @@ import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
|
||||
let capturedOptions: Options | undefined;
|
||||
let contextUsageResult: (() => Promise<{ rawMaxTokens: number }>) | undefined;
|
||||
let contextUsageResult: (() => Promise<{ rawMaxTokens: number; model?: string }>) | undefined;
|
||||
vi.mock("@anthropic-ai/claude-agent-sdk", async () => {
|
||||
const actual = await vi.importActual<typeof import("@anthropic-ai/claude-agent-sdk")>(
|
||||
"@anthropic-ai/claude-agent-sdk",
|
||||
@@ -625,38 +625,101 @@ describe("createSession options merging", () => {
|
||||
return (agent as unknown as { sessions: Record<string, any> }).sessions[sessionId];
|
||||
}
|
||||
|
||||
it("seeds contextWindowSize from the SDK's getContextUsage report", async () => {
|
||||
// Aliases like `sonnet` can resolve to an extended-context model with no
|
||||
// "1m" token anywhere in id/displayName/description, so the authoritative
|
||||
// report must win over text inference (issue #596).
|
||||
it("does not call getContextUsage during session creation", async () => {
|
||||
// getContextUsage stalls until the session's first prompt turn has run
|
||||
// (it is not serviced pre-turn), so session/new must never call it —
|
||||
// awaiting it inline is what regressed session/new latency in 0.59.0.
|
||||
const ctxSpy = vi.fn(async () => ({ rawMaxTokens: 967000 }));
|
||||
contextUsageResult = ctxSpy;
|
||||
|
||||
await agent.newSession({ cwd: process.cwd(), mcpServers: [] });
|
||||
|
||||
expect(ctxSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("seeds contextWindowSize from text inference, falling back to the default when it misses", async () => {
|
||||
// The mock model ("claude-sonnet-4-6" / "Claude Sonnet" / "Fast") carries
|
||||
// no "1m" token anywhere, so inference misses and the window falls back to
|
||||
// the default; the authoritative value arrives later via result.modelUsage.
|
||||
contextUsageResult = async () => ({ rawMaxTokens: 967000 });
|
||||
|
||||
const response = await agent.newSession({ cwd: process.cwd(), mcpServers: [] });
|
||||
|
||||
expect(sessionFor(response.sessionId).contextWindowSize).toBe(967000);
|
||||
expect(sessionFor(response.sessionId).contextWindowSize).toBe(200000);
|
||||
expect(sessionFor(response.sessionId).contextWindowAuthoritative).toBe(false);
|
||||
});
|
||||
|
||||
it("falls back to the default window when getContextUsage fails and inference misses", async () => {
|
||||
// getContextUsage rejects, and the mock model ("claude-sonnet-4-6" /
|
||||
// "Claude Sonnet" / "Fast") has no "1m" hint.
|
||||
contextUsageResult = () => Promise.reject(new Error("no context usage mocked"));
|
||||
// The rejection is deliberate — capture the agent's warning instead of
|
||||
// letting it hit the console.
|
||||
const errorSpy = vi.fn();
|
||||
(agent as any).logger = { log: () => {}, error: errorSpy };
|
||||
it("session/load seeds the window from the resumed session's getContextUsage report", async () => {
|
||||
// Resumed sessions get getContextUsage serviced pre-turn (issue #845 uses
|
||||
// it to restore the live model), and the same response carries the
|
||||
// authoritative window (`rawMaxTokens`). After a process restart the
|
||||
// module cache is empty and text inference misses natively-1M aliases, so
|
||||
// discarding this in-hand value would replay the issue-#596 flicker on
|
||||
// every reload — the flagship scenario. 888_000 can only come from the
|
||||
// report: inference on the mock model yields null → 200_000 default.
|
||||
contextUsageResult = async () => ({ rawMaxTokens: 888_000, model: "claude-sonnet-4-6" });
|
||||
|
||||
const response = await agent.newSession({ cwd: process.cwd(), mcpServers: [] });
|
||||
await (
|
||||
agent as unknown as {
|
||||
createSession: (params: object, opts: { resume?: string }) => Promise<unknown>;
|
||||
}
|
||||
).createSession({ cwd: process.cwd(), mcpServers: [] }, { resume: "resumed-window-probe" });
|
||||
|
||||
expect(sessionFor(response.sessionId).contextWindowSize).toBe(200000);
|
||||
expect(errorSpy).toHaveBeenCalled();
|
||||
const session = sessionFor("resumed-window-probe");
|
||||
expect(session.contextWindowSize).toBe(888_000);
|
||||
expect(session.contextWindowAuthoritative).toBe(true);
|
||||
});
|
||||
|
||||
it("ignores a nonsensical (non-positive) reported window", async () => {
|
||||
contextUsageResult = async () => ({ rawMaxTokens: 0 });
|
||||
it("scopes providerCacheKey by per-session env routing", async () => {
|
||||
// The context-window cache key must distinguish backends exactly as the
|
||||
// CLI will see them: a session routed to a proxy via _meta env shares a
|
||||
// model id spelling with default-routed sessions but not a context lane,
|
||||
// so it must land in its own cache bucket (and two default-routed
|
||||
// sessions must share one).
|
||||
const r1 = await agent.newSession({ cwd: process.cwd(), mcpServers: [] });
|
||||
const r2 = await agent.newSession({
|
||||
cwd: process.cwd(),
|
||||
mcpServers: [],
|
||||
_meta: {
|
||||
claudeCode: {
|
||||
options: { env: { ANTHROPIC_BASE_URL: "https://window-probe-proxy.example" } },
|
||||
},
|
||||
},
|
||||
});
|
||||
const r3 = await agent.newSession({ cwd: process.cwd(), mcpServers: [] });
|
||||
|
||||
const response = await agent.newSession({ cwd: process.cwd(), mcpServers: [] });
|
||||
expect(sessionFor(r2.sessionId).providerCacheKey).not.toBe(
|
||||
sessionFor(r1.sessionId).providerCacheKey,
|
||||
);
|
||||
expect(sessionFor(r3.sessionId).providerCacheKey).toBe(
|
||||
sessionFor(r1.sessionId).providerCacheKey,
|
||||
);
|
||||
});
|
||||
|
||||
expect(sessionFor(response.sessionId).contextWindowSize).toBe(200000);
|
||||
it("scopes providerCacheKey by provider headers: same endpoint, different headers → different buckets", async () => {
|
||||
// Two providers/set configs sharing apiType+baseUrl but differing in
|
||||
// headers (e.g. an `anthropic-beta: context-1m-…` routing header) can
|
||||
// serve different context lanes for the same model id, so they must not
|
||||
// share a window-cache bucket.
|
||||
await agent.unstable_setProvider({
|
||||
providerId: "main",
|
||||
apiType: "anthropic",
|
||||
baseUrl: "https://gw.example",
|
||||
headers: {},
|
||||
});
|
||||
const plain = await agent.newSession({ cwd: process.cwd(), mcpServers: [] });
|
||||
|
||||
await agent.unstable_setProvider({
|
||||
providerId: "main",
|
||||
apiType: "anthropic",
|
||||
baseUrl: "https://gw.example",
|
||||
headers: { "anthropic-beta": "context-1m-2025-08-07" },
|
||||
});
|
||||
const beta = await agent.newSession({ cwd: process.cwd(), mcpServers: [] });
|
||||
|
||||
expect(sessionFor(beta.sessionId).providerCacheKey).not.toBe(
|
||||
sessionFor(plain.sessionId).providerCacheKey,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
303
src/tests/providers.test.ts
Normal file
303
src/tests/providers.test.ts
Normal file
@@ -0,0 +1,303 @@
|
||||
import { describe, expect, it, Mock, vi, afterEach, beforeEach } from "vitest";
|
||||
import { AcpClient, ClaudeAcpAgent } from "../acp-agent.js";
|
||||
|
||||
const mockQuery = vi.hoisted(() =>
|
||||
vi.fn(() => ({
|
||||
initializationResult: vi.fn().mockResolvedValue({
|
||||
models: [
|
||||
{ value: "id", displayName: "name", description: "description", supportsAutoMode: true },
|
||||
],
|
||||
}),
|
||||
setModel: vi.fn(),
|
||||
setPermissionMode: vi.fn(),
|
||||
supportedCommands: vi.fn().mockResolvedValue([]),
|
||||
})),
|
||||
);
|
||||
|
||||
vi.mock("@anthropic-ai/claude-agent-sdk", async () => ({
|
||||
...(await vi.importActual<typeof import("@anthropic-ai/claude-agent-sdk")>(
|
||||
"@anthropic-ai/claude-agent-sdk",
|
||||
)),
|
||||
query: mockQuery,
|
||||
}));
|
||||
|
||||
// `logout` shells out to `claude auth logout`; make execFile succeed so the
|
||||
// real logout body runs (and clears provider config) without a live CLI.
|
||||
vi.mock("node:child_process", async () => {
|
||||
const actual = await vi.importActual<typeof import("node:child_process")>("node:child_process");
|
||||
return {
|
||||
...actual,
|
||||
execFile: vi.fn((_file: string, _args: string[], cb: (...a: unknown[]) => void) =>
|
||||
cb(null, { stdout: "", stderr: "" }),
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
describe("providers", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.runAllTimers();
|
||||
vi.useRealTimers();
|
||||
vi.unstubAllGlobals();
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
async function createAgentMock(): Promise<[ClaudeAcpAgent, Mock]> {
|
||||
const connectionMock = {
|
||||
sessionUpdate: async (_: any) => {},
|
||||
} as AcpClient;
|
||||
const agent = new ClaudeAcpAgent(connectionMock);
|
||||
return [agent, mockQuery];
|
||||
}
|
||||
|
||||
it("advertises the providers capability in initialize", async () => {
|
||||
const [agent] = await createAgentMock();
|
||||
const response = await agent.initialize({ protocolVersion: 1, clientCapabilities: {} });
|
||||
expect(response.agentCapabilities?.providers).toEqual({});
|
||||
});
|
||||
|
||||
it("lists a single optional 'main' provider, unconfigured by default", async () => {
|
||||
const [agent] = await createAgentMock();
|
||||
const response = await agent.unstable_listProviders({});
|
||||
expect(response.providers).toEqual([
|
||||
{
|
||||
providerId: "main",
|
||||
supported: ["anthropic", "bedrock", "vertex"],
|
||||
required: false,
|
||||
current: null,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("reflects apiType/baseUrl after set, and never echoes headers", async () => {
|
||||
const [agent] = await createAgentMock();
|
||||
await agent.unstable_setProvider({
|
||||
providerId: "main",
|
||||
apiType: "anthropic",
|
||||
baseUrl: "https://gateway.example/v1",
|
||||
headers: { authorization: "Bearer secret" },
|
||||
});
|
||||
|
||||
const response = await agent.unstable_listProviders({});
|
||||
expect(response.providers[0].current).toEqual({
|
||||
apiType: "anthropic",
|
||||
baseUrl: "https://gateway.example/v1",
|
||||
});
|
||||
// headers must not leak through list
|
||||
expect(JSON.stringify(response.providers)).not.toContain("secret");
|
||||
});
|
||||
|
||||
it("rejects set for an unknown providerId", async () => {
|
||||
const [agent] = await createAgentMock();
|
||||
await expect(
|
||||
agent.unstable_setProvider({
|
||||
providerId: "openai",
|
||||
apiType: "anthropic",
|
||||
baseUrl: "https://gateway.example",
|
||||
}),
|
||||
).rejects.toMatchObject({ code: -32602 });
|
||||
});
|
||||
|
||||
it("rejects set for an unsupported apiType", async () => {
|
||||
const [agent] = await createAgentMock();
|
||||
await expect(
|
||||
agent.unstable_setProvider({
|
||||
providerId: "main",
|
||||
apiType: "openai",
|
||||
baseUrl: "https://gateway.example",
|
||||
}),
|
||||
).rejects.toMatchObject({ code: -32602 });
|
||||
});
|
||||
|
||||
it("rejects set for an empty or non-http baseUrl", async () => {
|
||||
const [agent] = await createAgentMock();
|
||||
await expect(
|
||||
agent.unstable_setProvider({ providerId: "main", apiType: "anthropic", baseUrl: "" }),
|
||||
).rejects.toMatchObject({ code: -32602 });
|
||||
await expect(
|
||||
agent.unstable_setProvider({
|
||||
providerId: "main",
|
||||
apiType: "anthropic",
|
||||
baseUrl: "ftp://nope.example",
|
||||
}),
|
||||
).rejects.toMatchObject({ code: -32602 });
|
||||
});
|
||||
|
||||
it("disables the 'main' provider by clearing config and reporting current: null", async () => {
|
||||
const [agent] = await createAgentMock();
|
||||
await agent.unstable_setProvider({
|
||||
providerId: "main",
|
||||
apiType: "anthropic",
|
||||
baseUrl: "https://gateway.example/v1",
|
||||
});
|
||||
expect((await agent.unstable_listProviders({})).providers[0].current).not.toBeNull();
|
||||
|
||||
await expect(agent.unstable_disableProvider({ providerId: "main" })).resolves.toEqual({});
|
||||
|
||||
expect((await agent.unstable_listProviders({})).providers[0].current).toBeNull();
|
||||
});
|
||||
|
||||
it("treats disabling an unknown provider as an idempotent no-op", async () => {
|
||||
const [agent] = await createAgentMock();
|
||||
await expect(agent.unstable_disableProvider({ providerId: "openai" })).resolves.toEqual({});
|
||||
});
|
||||
|
||||
it("routes anthropic provider config into session env", async () => {
|
||||
const [agent, mockQuery] = await createAgentMock();
|
||||
await agent.initialize({ protocolVersion: 1, clientCapabilities: {} });
|
||||
await agent.unstable_setProvider({
|
||||
providerId: "main",
|
||||
apiType: "anthropic",
|
||||
baseUrl: "https://gateway.example",
|
||||
headers: { "x-api-key": "test" },
|
||||
});
|
||||
|
||||
await agent.newSession({ cwd: process.cwd(), mcpServers: [] });
|
||||
|
||||
expect(mockQuery).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
options: expect.objectContaining({
|
||||
env: expect.objectContaining({
|
||||
ANTHROPIC_AUTH_TOKEN: " ",
|
||||
ANTHROPIC_BASE_URL: "https://gateway.example",
|
||||
ANTHROPIC_CUSTOM_HEADERS: "x-api-key: test",
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("routes bedrock provider config into session env", async () => {
|
||||
const [agent, mockQuery] = await createAgentMock();
|
||||
await agent.initialize({ protocolVersion: 1, clientCapabilities: {} });
|
||||
await agent.unstable_setProvider({
|
||||
providerId: "main",
|
||||
apiType: "bedrock",
|
||||
baseUrl: "https://gateway.example",
|
||||
headers: { "custom-header": "test" },
|
||||
});
|
||||
|
||||
await agent.newSession({ cwd: process.cwd(), mcpServers: [] });
|
||||
|
||||
expect(mockQuery).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
options: expect.objectContaining({
|
||||
env: expect.objectContaining({
|
||||
CLAUDE_CODE_USE_BEDROCK: "1",
|
||||
AWS_BEARER_TOKEN_BEDROCK: " ",
|
||||
ANTHROPIC_BEDROCK_BASE_URL: "https://gateway.example",
|
||||
ANTHROPIC_CUSTOM_HEADERS: "custom-header: test",
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts vertex config via _meta and routes it into session env", async () => {
|
||||
const [agent, mockQuery] = await createAgentMock();
|
||||
await agent.initialize({ protocolVersion: 1, clientCapabilities: {} });
|
||||
await agent.unstable_setProvider({
|
||||
providerId: "main",
|
||||
apiType: "vertex",
|
||||
baseUrl: "https://vertex.example",
|
||||
headers: { "custom-header": "test" },
|
||||
_meta: { claudeCode: { vertex: { projectId: "my-project", region: "us-east5" } } },
|
||||
});
|
||||
|
||||
// list surfaces apiType/baseUrl but not the _meta extras
|
||||
const listed = await agent.unstable_listProviders({});
|
||||
expect(listed.providers[0].current).toEqual({
|
||||
apiType: "vertex",
|
||||
baseUrl: "https://vertex.example",
|
||||
});
|
||||
|
||||
await agent.newSession({ cwd: process.cwd(), mcpServers: [] });
|
||||
|
||||
expect(mockQuery).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
options: expect.objectContaining({
|
||||
env: expect.objectContaining({
|
||||
CLAUDE_CODE_USE_VERTEX: "1",
|
||||
ANTHROPIC_VERTEX_BASE_URL: "https://vertex.example",
|
||||
ANTHROPIC_VERTEX_PROJECT_ID: "my-project",
|
||||
CLOUD_ML_REGION: "us-east5",
|
||||
ANTHROPIC_CUSTOM_HEADERS: "custom-header: test",
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects vertex set without _meta project/region", async () => {
|
||||
const [agent] = await createAgentMock();
|
||||
await expect(
|
||||
agent.unstable_setProvider({
|
||||
providerId: "main",
|
||||
apiType: "vertex",
|
||||
baseUrl: "https://vertex.example",
|
||||
}),
|
||||
).rejects.toMatchObject({ code: -32602 });
|
||||
|
||||
// partial _meta (missing region) is also rejected
|
||||
await expect(
|
||||
agent.unstable_setProvider({
|
||||
providerId: "main",
|
||||
apiType: "vertex",
|
||||
baseUrl: "https://vertex.example",
|
||||
_meta: { claudeCode: { vertex: { projectId: "my-project" } } } as any,
|
||||
}),
|
||||
).rejects.toMatchObject({ code: -32602 });
|
||||
});
|
||||
|
||||
it("providers/set takes precedence over gateway auth", async () => {
|
||||
const [agent, mockQuery] = await createAgentMock();
|
||||
await agent.initialize({
|
||||
protocolVersion: 1,
|
||||
clientCapabilities: { auth: { terminal: true, _meta: { gateway: true } } } as any,
|
||||
});
|
||||
|
||||
// Gateway auth first...
|
||||
await agent.authenticate({
|
||||
methodId: "gateway",
|
||||
_meta: { gateway: { baseUrl: "https://gateway.example", headers: {} } },
|
||||
});
|
||||
// ...then providers/set wins.
|
||||
await agent.unstable_setProvider({
|
||||
providerId: "main",
|
||||
apiType: "anthropic",
|
||||
baseUrl: "https://provider.example",
|
||||
});
|
||||
|
||||
await agent.newSession({ cwd: process.cwd(), mcpServers: [] });
|
||||
|
||||
expect(mockQuery).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
options: expect.objectContaining({
|
||||
env: expect.objectContaining({
|
||||
ANTHROPIC_BASE_URL: "https://provider.example",
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("clears provider config on logout", async () => {
|
||||
const [agent] = await createAgentMock();
|
||||
// Avoids resolving the native CLI binary in claudeCliPath().
|
||||
vi.stubEnv("CLAUDE_CODE_EXECUTABLE", "/bin/true");
|
||||
|
||||
await agent.unstable_setProvider({
|
||||
providerId: "main",
|
||||
apiType: "anthropic",
|
||||
baseUrl: "https://provider.example",
|
||||
});
|
||||
expect((await agent.unstable_listProviders({})).providers[0].current).not.toBeNull();
|
||||
|
||||
await agent.logout({});
|
||||
|
||||
expect((await agent.unstable_listProviders({})).providers[0].current).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -936,30 +936,16 @@ describe("session config options", () => {
|
||||
return (agent as unknown as { sessions: Record<string, any> }).sessions[SESSION_ID];
|
||||
}
|
||||
|
||||
it("refreshes contextWindowSize from getContextUsage when the model changes", async () => {
|
||||
it("sets the window from text inference on model switch, without any getContextUsage IPC", async () => {
|
||||
// getContextUsage stalls until the session's first prompt turn, so the
|
||||
// switch path must never call it; the window is seeded from the text
|
||||
// heuristic (here via the new model's resolvedModel) and later confirmed
|
||||
// by result.modelUsage.
|
||||
const session = getSession();
|
||||
session.query.getContextUsage = vi.fn(async () => ({ rawMaxTokens: 967000 }));
|
||||
|
||||
await agent.setSessionConfigOption({
|
||||
sessionId: SESSION_ID,
|
||||
configId: "model",
|
||||
value: "claude-sonnet-4-6",
|
||||
});
|
||||
|
||||
expect(session.query.getContextUsage).toHaveBeenCalled();
|
||||
expect(session.contextWindowSize).toBe(967000);
|
||||
});
|
||||
|
||||
it("falls back to resolvedModel text inference when getContextUsage fails", async () => {
|
||||
const session = getSession();
|
||||
session.query.getContextUsage = vi.fn().mockRejectedValue(new Error("boom"));
|
||||
session.modelInfos = session.modelInfos.map((m: ModelInfo) =>
|
||||
m.value === "claude-sonnet-4-6" ? { ...m, resolvedModel: "claude-sonnet-5[1m]" } : m,
|
||||
);
|
||||
// The rejection is deliberate — capture the agent's warning instead of
|
||||
// letting it hit the console.
|
||||
const errorSpy = vi.fn();
|
||||
(agent as any).logger = { log: () => {}, error: errorSpy };
|
||||
|
||||
await agent.setSessionConfigOption({
|
||||
sessionId: SESSION_ID,
|
||||
@@ -967,18 +953,17 @@ describe("session config options", () => {
|
||||
value: "claude-sonnet-4-6",
|
||||
});
|
||||
|
||||
expect(session.query.getContextUsage).not.toHaveBeenCalled();
|
||||
expect(session.contextWindowSize).toBe(1_000_000);
|
||||
expect(errorSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls back to the default window when the SDK and inference both miss", async () => {
|
||||
it("falls back to the default window when inference misses, without any getContextUsage IPC", async () => {
|
||||
const session = getSession();
|
||||
session.contextWindowSize = 1_000_000;
|
||||
session.query.getContextUsage = vi.fn().mockRejectedValue(new Error("boom"));
|
||||
// The rejection is deliberate — capture the agent's warning instead of
|
||||
// letting it hit the console.
|
||||
const errorSpy = vi.fn();
|
||||
(agent as any).logger = { log: () => {}, error: errorSpy };
|
||||
// Present but must NOT be called; the switch never consults it.
|
||||
session.query.getContextUsage = vi.fn(async () => ({ rawMaxTokens: 967000 }));
|
||||
// claude-sonnet-4-6 carries no "1m" token in its id, resolvedModel,
|
||||
// displayName, or description, so inference misses → default window.
|
||||
|
||||
await agent.setSessionConfigOption({
|
||||
sessionId: SESSION_ID,
|
||||
@@ -986,8 +971,22 @@ describe("session config options", () => {
|
||||
value: "claude-sonnet-4-6",
|
||||
});
|
||||
|
||||
expect(session.query.getContextUsage).not.toHaveBeenCalled();
|
||||
expect(session.contextWindowSize).toBe(200000);
|
||||
expect(errorSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not call getContextUsage even when switching to a fresh model", async () => {
|
||||
const session = getSession();
|
||||
const spy = vi.fn(async () => ({ rawMaxTokens: 967000 }));
|
||||
session.query.getContextUsage = spy;
|
||||
|
||||
await agent.setSessionConfigOption({
|
||||
sessionId: SESSION_ID,
|
||||
configId: "model",
|
||||
value: "claude-sonnet-4-6",
|
||||
});
|
||||
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps the learned window when re-asserting the current model", async () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect, beforeEach } from "vitest";
|
||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
import { ClientCapabilities } from "@agentclientprotocol/sdk";
|
||||
import { ImageBlockParam, ToolResultBlockParam } from "@anthropic-ai/sdk/resources";
|
||||
import {
|
||||
@@ -1024,7 +1024,7 @@ describe("Bash terminal output", () => {
|
||||
);
|
||||
|
||||
// Fire PostToolUse hook with a structuredPatch in tool_response
|
||||
const hook = createPostToolUseHook(mockLogger);
|
||||
const hook = createPostToolUseHook();
|
||||
await hook(
|
||||
{
|
||||
hook_event_name: "PostToolUse",
|
||||
@@ -1102,7 +1102,7 @@ describe("Bash terminal output", () => {
|
||||
mockLogger,
|
||||
);
|
||||
|
||||
const hook = createPostToolUseHook(mockLogger);
|
||||
const hook = createPostToolUseHook();
|
||||
await hook(
|
||||
{
|
||||
hook_event_name: "PostToolUse",
|
||||
@@ -1182,7 +1182,7 @@ describe("Bash terminal output", () => {
|
||||
mockLogger,
|
||||
);
|
||||
|
||||
const hook = createPostToolUseHook(mockLogger);
|
||||
const hook = createPostToolUseHook();
|
||||
await hook(
|
||||
{
|
||||
hook_event_name: "PostToolUse",
|
||||
@@ -1203,6 +1203,36 @@ describe("Bash terminal output", () => {
|
||||
expect(hookUpdate.content).toBeUndefined();
|
||||
expect(hookUpdate.locations).toBeUndefined();
|
||||
});
|
||||
|
||||
// Regression for issue #889: tool uses that never register a callback
|
||||
// (TodoWrite/Task* are rendered as plan updates, not tool_calls) fire the
|
||||
// PostToolUse hook too. That's expected — the hook must stay silent
|
||||
// instead of spamming "No onPostToolUseHook found" to stderr.
|
||||
it("should not log an error when no callback is registered for the tool use", async () => {
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
try {
|
||||
const hook = createPostToolUseHook();
|
||||
const result = await hook(
|
||||
{
|
||||
hook_event_name: "PostToolUse",
|
||||
tool_name: "TodoWrite",
|
||||
tool_input: { todos: [] },
|
||||
tool_response: { success: true },
|
||||
tool_use_id: "toolu_todo_no_callback",
|
||||
session_id: "test-session",
|
||||
transcript_path: "/tmp/test",
|
||||
cwd: "/tmp",
|
||||
},
|
||||
"toolu_todo_no_callback",
|
||||
{ signal: AbortSignal.abort() },
|
||||
);
|
||||
|
||||
expect(result).toEqual({ continue: true });
|
||||
expect(errorSpy).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
errorSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("post-tool-use hook sends diff content for Write tool", () => {
|
||||
@@ -1239,7 +1269,7 @@ describe("Bash terminal output", () => {
|
||||
mockLogger,
|
||||
);
|
||||
|
||||
const hook = createPostToolUseHook(mockLogger);
|
||||
const hook = createPostToolUseHook();
|
||||
await hook(
|
||||
{
|
||||
hook_event_name: "PostToolUse",
|
||||
@@ -1315,7 +1345,7 @@ describe("Bash terminal output", () => {
|
||||
mockLogger,
|
||||
);
|
||||
|
||||
const hook = createPostToolUseHook(mockLogger);
|
||||
const hook = createPostToolUseHook();
|
||||
await hook(
|
||||
{
|
||||
hook_event_name: "PostToolUse",
|
||||
@@ -1438,7 +1468,7 @@ describe("Bash terminal output", () => {
|
||||
expect((resultNotifications[1].update as any).status).toBe("completed");
|
||||
|
||||
// Step 3: Fire the PostToolUse hook (simulates what Claude Code SDK does)
|
||||
const hook = createPostToolUseHook(mockLogger);
|
||||
const hook = createPostToolUseHook();
|
||||
await hook(
|
||||
{
|
||||
hook_event_name: "PostToolUse",
|
||||
@@ -1519,7 +1549,7 @@ describe("Bash terminal output", () => {
|
||||
);
|
||||
|
||||
// Fire hook
|
||||
const hook = createPostToolUseHook(mockLogger);
|
||||
const hook = createPostToolUseHook();
|
||||
await hook(
|
||||
{
|
||||
hook_event_name: "PostToolUse",
|
||||
@@ -2275,6 +2305,62 @@ describe("Agent/Task tool_result rendering from tool_use_result", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("leaves malformed trailers alone", () => {
|
||||
// Incomplete trailers are ordinary report text, not metadata to strip.
|
||||
const malformedResult: ToolResultBlockParam = {
|
||||
type: "tool_result",
|
||||
tool_use_id: "toolu_agent",
|
||||
content: [
|
||||
{ type: "text", text: "Report.\n<usage>missing closing tag" },
|
||||
{ type: "text", text: "Report.\nagentId: abc-123 (missing closing paren" },
|
||||
],
|
||||
};
|
||||
|
||||
const update = toolUpdateFromToolResult(malformedResult, agentToolUse, false);
|
||||
|
||||
expect(update.content).toEqual([
|
||||
{ type: "content", content: { type: "text", text: "Report.\n<usage>missing closing tag" } },
|
||||
{
|
||||
type: "content",
|
||||
content: { type: "text", text: "Report.\nagentId: abc-123 (missing closing paren" },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("strips only the trailer when the report itself mentions <usage>", () => {
|
||||
const result: ToolResultBlockParam = {
|
||||
type: "tool_result",
|
||||
tool_use_id: "toolu_agent",
|
||||
content: "Grep for <usage> found 3 hits.\n<usage>subagent_tokens: 5</usage>",
|
||||
};
|
||||
const update = toolUpdateFromToolResult(result, agentToolUse, false);
|
||||
|
||||
expect(update.content).toEqual([
|
||||
{ type: "content", content: { type: "text", text: "Grep for <usage> found 3 hits." } },
|
||||
]);
|
||||
});
|
||||
|
||||
it("handles adversarial trailer-shaped input in linear time", () => {
|
||||
// Regression: the old regex-based strip backtracked quadratically on
|
||||
// these shapes (CodeQL js/polynomial-redos) — at this size it would
|
||||
// blow the test timeout rather than merely run slow.
|
||||
const result: ToolResultBlockParam = {
|
||||
type: "tool_result",
|
||||
tool_use_id: "toolu_agent",
|
||||
content: [
|
||||
{ type: "text", text: "agentId: - (".repeat(20000) },
|
||||
{ type: "text", text: "<usage>".repeat(30000) },
|
||||
],
|
||||
};
|
||||
const update = toolUpdateFromToolResult(result, agentToolUse, false);
|
||||
|
||||
// Neither is a real trailer, so both come through unchanged.
|
||||
expect(update.content).toEqual([
|
||||
{ type: "content", content: { type: "text", text: "agentId: - (".repeat(20000) } },
|
||||
{ type: "content", content: { type: "text", text: "<usage>".repeat(30000) } },
|
||||
]);
|
||||
});
|
||||
|
||||
it("falls back (trailer-stripped) when tool_use_result is the async_launched variant", () => {
|
||||
const update = toolUpdateFromToolResult(rawResult, agentToolUse, false, {
|
||||
status: "async_launched",
|
||||
@@ -2349,6 +2435,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 = {
|
||||
|
||||
52
src/tools.ts
52
src/tools.ts
@@ -60,7 +60,6 @@ import {
|
||||
BetaWebSearchToolResultBlockParam,
|
||||
} from "@anthropic-ai/sdk/resources/beta.mjs";
|
||||
import path from "node:path";
|
||||
import { Logger } from "./acp-agent.js";
|
||||
|
||||
/**
|
||||
* Union of all possible content types that can appear in tool results from the Anthropic SDK.
|
||||
@@ -515,9 +514,42 @@ function structuredResult<T extends object>(toolUseResult: unknown): T | undefin
|
||||
* matching rather than mangle the report.
|
||||
*/
|
||||
function stripAgentTrailer(text: string): string {
|
||||
return text
|
||||
.replace(/\n?<usage>[\s\S]*?<\/usage>\s*$/, "")
|
||||
.replace(/\n?agentId: [\w-]+ \([^)]*\)\s*$/, "");
|
||||
return stripAgentIdLine(stripUsageBlock(text));
|
||||
}
|
||||
|
||||
const USAGE_OPEN = "<usage>";
|
||||
const USAGE_CLOSE = "</usage>";
|
||||
|
||||
/** Remove a trailing `<usage>…</usage>` block, plus trailing whitespace and
|
||||
* one preceding newline. Matches from the *last* `<usage>` so a report that
|
||||
* merely mentions the marker earlier isn't truncated at the mention. */
|
||||
function stripUsageBlock(text: string): string {
|
||||
const body = text.trimEnd();
|
||||
if (!body.endsWith(USAGE_CLOSE)) {
|
||||
return text;
|
||||
}
|
||||
const open = body.lastIndexOf(USAGE_OPEN, body.length - USAGE_CLOSE.length - USAGE_OPEN.length);
|
||||
if (open === -1) {
|
||||
return text;
|
||||
}
|
||||
return body.slice(0, open > 0 && body[open - 1] === "\n" ? open - 1 : open);
|
||||
}
|
||||
|
||||
/** The continuation line, anchored to a whole line so the regex has a single
|
||||
* start position and no ambiguous repetition (`[\w-]+` can't consume the
|
||||
* following space, `[^)]*` can't consume the closing paren) — it runs in
|
||||
* linear time on any input. */
|
||||
const AGENT_ID_LINE = /^agentId: [\w-]+ \([^)]*\)$/;
|
||||
|
||||
/** Remove a final `agentId: <id> (…)` line, plus trailing whitespace and the
|
||||
* newline that preceded the line. */
|
||||
function stripAgentIdLine(text: string): string {
|
||||
const body = text.trimEnd();
|
||||
const lineStart = body.lastIndexOf("\n") + 1;
|
||||
if (!AGENT_ID_LINE.test(body.slice(lineStart))) {
|
||||
return text;
|
||||
}
|
||||
return body.slice(0, Math.max(lineStart - 1, 0));
|
||||
}
|
||||
|
||||
/** Apply {@link stripAgentTrailer} across a raw tool_result `content` (plain
|
||||
@@ -1184,12 +1216,7 @@ export const registerHookCallback = (
|
||||
|
||||
/* A callback for Claude Code that is called when receiving a PostToolUse hook */
|
||||
export const createPostToolUseHook =
|
||||
(
|
||||
logger: Logger = console,
|
||||
options?: {
|
||||
onEnterPlanMode?: () => Promise<void>;
|
||||
},
|
||||
): HookCallback =>
|
||||
(options?: { onEnterPlanMode?: () => Promise<void> }): HookCallback =>
|
||||
async (input: any, toolUseID: string | undefined): Promise<{ continue: boolean }> => {
|
||||
if (input.hook_event_name === "PostToolUse") {
|
||||
// Handle EnterPlanMode tool - notify client of mode change after successful execution
|
||||
@@ -1201,11 +1228,8 @@ export const createPostToolUseHook =
|
||||
const onPostToolUseHook = toolUseCallbacks[toolUseID]?.onPostToolUseHook;
|
||||
if (onPostToolUseHook) {
|
||||
await onPostToolUseHook(toolUseID, input.tool_input, input.tool_response);
|
||||
delete toolUseCallbacks[toolUseID]; // Cleanup after execution
|
||||
} else {
|
||||
logger.error(`No onPostToolUseHook found for tool use ID: ${toolUseID}`);
|
||||
delete toolUseCallbacks[toolUseID];
|
||||
}
|
||||
delete toolUseCallbacks[toolUseID]; // Cleanup after execution
|
||||
}
|
||||
}
|
||||
return { continue: true };
|
||||
|
||||
Reference in New Issue
Block a user