Compare commits

...

14 Commits

Author SHA1 Message Date
Bennet Bo Fenner
3b4cd4b2d5 Fix duplicate tool use IDs error (#198)
<img width="595" height="107" alt="image"
src="https://github.com/user-attachments/assets/3b9ae5e9-cbc8-425a-8cb6-1d201df3a812"
/>

---------

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2025-12-09 22:00:38 +01:00
Ben Brandt
9320e05891 Update to claude-agent-sdk 0.1.61 (#195) 2025-12-08 12:17:49 +01:00
Ben Brandt
3f44543019 Unify ACP and Claude Code session ids (#193)
There have been some issues for folks because the session id we returned
for ACP wasn't the same as the session id for Claude Code.

Now, we pass it through to the `--session-id` flag of the Claude Code
CLI, which means both ACP and Claude Code are always referring to the
same UUID. Yay!
2025-12-05 15:14:16 +00:00
Ben Brandt
4db3d855bf Update to claude-agent-sdk v0.1.59 (#192) 2025-12-05 16:06:31 +01:00
Ben Brandt
e57893f6d0 Update to claude-agent-sdk 0.1.57 (#191) 2025-12-03 11:27:01 +00:00
Ben Brandt
6a93d7e786 Update to ACP SDK v0.7.0 (#188) 2025-12-01 17:45:24 +01:00
RAIT-09
c3c0f4da36 Prevent invalid locations when Read tool call has no file_path (#186)
## Summary

Follow-up to #121. The same issue (invalid locations when `file_path` is
undefined) existed in the `Read` tool case as well.

## Problem

The ACP spec requires `path` as a required field in `ToolCallLocation`.
PR #121 fixed this for `mcp__acp__Read`, but the same issue remained for
the `Read` tool case.

When Claude generates a Read tool call without `file_path`, the current
code produces an invalid notification:

```json
{
  "locations": [
    {
      "line": 0
    }
  ]
}
```

This causes the following error in the client:

```
Error handling notification
{code: -32602, message: 'Invalid params', data: {…}}
```

## Solution

Return an empty `locations` array when `input.file_path` is undefined,
consistent with the fix in #121.
2025-12-01 13:22:06 +01:00
Ben Brandt
73d308e4ad Update to claude-agent-sdk 0.1.55 (#182) 2025-11-27 14:35:53 +01:00
Ben Brandt
49b28a25eb Add CLAUDECODE environment to terminal invocations (#181) 2025-11-27 13:10:11 +00:00
Ben Brandt
8516df7188 fix: Access to undefined meta field (#180) 2025-11-27 14:06:47 +01:00
Guillaume Sabran
e31a16228a Allow user to pass Options to Claude Code in new session (#176)
When using Claude Code ACP as a direct dependency, this will allow the
user to configure Claude Code using the `Options` provided by the SDK.
2025-11-26 11:36:01 +01:00
Guillaume Sabran
c00782c103 Logging configuration (#177)
Allow to provide a logger to replace `console` (`console` remains the
default value)
2025-11-25 11:29:16 +01:00
Ben Brandt
4211f8cf05 Update to claude-agent-sdk 0.1.51 (#178)
Adds support for Opus 4.5
2025-11-24 23:24:34 +01:00
Ben Brandt
ac2119a23a More forgiving unreachable (#173)
Currently, if we get an unexpected message, we crash. While this is fine
in the world where we vendor CC (which we do by default) we also allow
users to override the executable for cases where their work needs a
wrapper.

This makes it so that we just log the event that we get, but don't crash
right away in case we can continue on.
2025-11-24 12:17:11 +00:00
10 changed files with 522 additions and 739 deletions

View File

@@ -1,5 +1,41 @@
# Changelog
## 0.12.2
- Fix duplicate tool use IDs error
## 0.12.1
- Update to @anthropic-ai/claude-agent-sdk@v0.1.61
- Update to @agentclientprotocol/sdk@0.8.0
## 0.12.0
- Update to @anthropic-ai/claude-agent-sdk@v0.1.59
- Brings Opus to Claude Pro plans
- Support "Don't Ask" profile
- Unify ACP + Claude Code session ids
## 0.11.0
- Update to @anthropic-ai/claude-agent-sdk@v0.1.57
- Removed dependency on @anthropic-ai/claude-code since this is no longer needed
## 0.10.10
- Update to @agentclientprotocol/sdk@0.7.0
## 0.10.9
- Update to @anthropic-ai/claude-agent-sdk@v0.1.55
- Allow defining a custom logger when used as a library
- Allow specifying custom options when used as a library
- Add `CLAUDECODE=1` to terminal invocations to match default Claude Code behavior
## 0.10.8
- Update to @anthropic-ai/claude-agent-sdk@v0.1.51 (adds support for Opus 4.5)
## 0.10.7
- Fix read/edit tool error handling so upstream errors surface

878
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@
"publishConfig": {
"access": "public"
},
"version": "0.10.7",
"version": "0.12.2",
"description": "An ACP-compatible coding agent powered by the Claude Code SDK (TypeScript)",
"main": "dist/lib.js",
"bin": {
@@ -51,26 +51,22 @@
"author": "Zed Industries",
"license": "Apache-2.0",
"dependencies": {
"@agentclientprotocol/sdk": "0.5.1",
"@anthropic-ai/claude-agent-sdk": "0.1.50",
"@anthropic-ai/claude-code": "2.0.50",
"@modelcontextprotocol/sdk": "1.22.0",
"diff": "8.0.2",
"uuid": "13.0.0"
"@agentclientprotocol/sdk": "0.8.0",
"@anthropic-ai/claude-agent-sdk": "0.1.61",
"@modelcontextprotocol/sdk": "1.24.3",
"diff": "8.0.2"
},
"devDependencies": {
"@anthropic-ai/sdk": "0.70.1",
"@anthropic-ai/sdk": "0.71.2",
"@types/node": "24.10.1",
"@types/uuid": "10.0.0",
"@typescript-eslint/eslint-plugin": "8.47.0",
"@typescript-eslint/parser": "8.47.0",
"@vitest/ui": "4.0.13",
"@typescript-eslint/eslint-plugin": "8.48.1",
"@typescript-eslint/parser": "8.48.1",
"eslint": "9.39.1",
"eslint-config-prettier": "10.1.8",
"globals": "16.5.0",
"prettier": "3.6.2",
"prettier": "3.7.4",
"ts-node": "10.9.2",
"typescript": "5.9.3",
"vitest": "4.0.13"
"vitest": "4.0.15"
}
}

View File

@@ -16,6 +16,7 @@ import {
ReadTextFileResponse,
RequestError,
SessionModelState,
SessionNotification,
SetSessionModelRequest,
SetSessionModelResponse,
SetSessionModeRequest,
@@ -38,9 +39,7 @@ import {
import * as fs from "node:fs";
import * as path from "node:path";
import * as os from "node:os";
import { v7 as uuidv7 } from "uuid";
import { nodeToWebReadable, nodeToWebWritable, Pushable, unreachable } from "./utils.js";
import { SessionNotification } from "@agentclientprotocol/sdk";
import { createMcpServer, EDIT_TOOL_NAMES, toolNames } from "./mcp-server.js";
import {
toolInfoFromToolUse,
@@ -48,11 +47,20 @@ import {
toolUpdateFromToolResult,
ClaudePlanEntry,
registerHookCallback,
postToolUseHook,
createPostToolUseHook,
} from "./tools.js";
import { ContentBlockParam } from "@anthropic-ai/sdk/resources";
import { BetaContentBlock, BetaRawContentBlockDelta } from "@anthropic-ai/sdk/resources/beta.mjs";
import packageJson from "../package.json" with { type: "json" };
import { randomUUID } from "node:crypto";
/**
* Logger interface for customizing logging output
*/
export interface Logger {
log: (...args: any[]) => void;
error: (...args: any[]) => void;
}
type Session = {
query: Query;
@@ -73,13 +81,37 @@ type BackgroundTerminal =
};
/**
* Extra metadata that the agent provides for each tool_call / tool_update update, under the `_meta.claudeCode` key.
* Extra metadata that can be given to Claude Code when creating a new session.
*/
export type NewSessionMeta = {
claudeCode?: {
/**
* Options forwarded to Claude Code when starting a new session.
* Those parameters will be ignored and managed by ACP:
* - cwd
* - includePartialMessages
* - allowDangerouslySkipPermissions
* - permissionMode
* - canUseTool
* - executable
* Those parameters will be used and updated to work with ACP:
* - hooks (merged with ACP's hooks)
* - mcpServers (merged with ACP's mcpServers)
*/
options?: Options;
};
};
/**
* Extra metadata that the agent provides for each tool_call / tool_update update.
*/
export type ToolUpdateMeta = {
/* The name of the tool that was used in Claude Code. */
toolName: string;
/* The structured output provided by Claude Code. */
toolResponse?: unknown;
claudeCode?: {
/* The name of the tool that was used in Claude Code. */
toolName: string;
/* The structured output provided by Claude Code. */
toolResponse?: unknown;
};
};
type ToolUseCache = {
@@ -104,12 +136,14 @@ export class ClaudeAcpAgent implements Agent {
fileContentCache: { [key: string]: string };
backgroundTerminals: { [key: string]: BackgroundTerminal } = {};
clientCapabilities?: ClientCapabilities;
logger: Logger;
constructor(client: AgentSideConnection) {
constructor(client: AgentSideConnection, logger?: Logger) {
this.sessions = {};
this.client = client;
this.toolUseCache = {};
this.fileContentCache = {};
this.logger = logger ?? console;
}
async initialize(request: InitializeRequest): Promise<InitializeResponse> {
@@ -163,7 +197,7 @@ export class ClaudeAcpAgent implements Agent {
throw RequestError.authRequired();
}
const sessionId = uuidv7();
const sessionId = randomUUID();
const input = new Pushable<SDKUserMessage>();
const mcpServers: Record<string, McpServerConfig> = {};
@@ -216,18 +250,25 @@ export class ClaudeAcpAgent implements Agent {
const permissionMode = "default";
// Extract options from _meta if provided
const userProvidedOptions = (params._meta as NewSessionMeta | undefined)?.claudeCode?.options;
const options: Options = {
cwd: params.cwd,
includePartialMessages: true,
mcpServers,
systemPrompt,
settingSources: ["user", "project", "local"],
stderr: (err) => this.logger.error(err),
...userProvidedOptions,
// Override certain fields that must be controlled by ACP
cwd: params.cwd,
includePartialMessages: true,
mcpServers: { ...(userProvidedOptions?.mcpServers || {}), ...mcpServers },
// Set our own session id
extraArgs: { ...userProvidedOptions?.extraArgs, "session-id": sessionId },
// If we want bypassPermissions to be an option, we have to allow it here.
// But it doesn't work in root mode, so we only activate it if it will work.
allowDangerouslySkipPermissions: !IS_ROOT,
permissionMode,
canUseTool: this.canUseTool(sessionId),
stderr: (err) => console.error(err),
// note: although not documented by the types, passing an absolute path
// here works to find zed's managed node version.
executable: process.execPath as any,
@@ -235,9 +276,11 @@ export class ClaudeAcpAgent implements Agent {
pathToClaudeCodeExecutable: process.env.CLAUDE_CODE_EXECUTABLE,
}),
hooks: {
...userProvidedOptions?.hooks,
PostToolUse: [
...(userProvidedOptions?.hooks?.PostToolUse || []),
{
hooks: [postToolUseHook],
hooks: [createPostToolUseHook(this.logger)],
},
],
},
@@ -297,6 +340,12 @@ export class ClaudeAcpAgent implements Agent {
options.disallowedTools = disallowedTools;
}
// Handle abort controller from meta options
const abortController = userProvidedOptions?.abortController;
if (abortController?.signal.aborted) {
throw new Error("Cancelled");
}
const q = query({
prompt: input,
options,
@@ -326,18 +375,23 @@ export class ClaudeAcpAgent implements Agent {
const availableModes = [
{
id: "default",
name: "Always Ask",
description: "Prompts for permission on first use of each tool",
name: "Default",
description: "Standard behavior, prompts for dangerous operations",
},
{
id: "acceptEdits",
name: "Accept Edits",
description: "Automatically accepts file edit permissions for the session",
description: "Auto-accept file edit operations",
},
{
id: "plan",
name: "Plan Mode",
description: "Claude can analyze but not modify files or execute commands",
description: "Planning mode, no actual tool execution",
},
{
id: "dontAsk",
name: "Don't Ask",
description: "Don't prompt for permissions, deny if not pre-approved",
},
];
// Only works in non-root mode
@@ -345,7 +399,7 @@ export class ClaudeAcpAgent implements Agent {
availableModes.push({
id: "bypassPermissions",
name: "Bypass Permissions",
description: "Skips all permission prompts",
description: "Bypass all permission checks",
});
}
@@ -392,7 +446,8 @@ export class ClaudeAcpAgent implements Agent {
// Todo: process via status api: https://docs.claude.com/en/docs/claude-code/hooks#hook-output
break;
default:
unreachable(message);
unreachable(message, this.logger);
break;
}
break;
case "result": {
@@ -429,7 +484,8 @@ export class ClaudeAcpAgent implements Agent {
}
return { stopReason: "max_turn_requests" };
default:
unreachable(message);
unreachable(message, this.logger);
break;
}
break;
}
@@ -440,6 +496,7 @@ export class ClaudeAcpAgent implements Agent {
this.toolUseCache,
this.fileContentCache,
this.client,
this.logger,
)) {
await this.client.sessionUpdate(notification);
}
@@ -457,7 +514,7 @@ export class ClaudeAcpAgent implements Agent {
typeof message.message.content === "string" &&
message.message.content.includes("<local-command-stdout>")
) {
console.log(message.message.content);
this.logger.log(message.message.content);
break;
}
@@ -465,7 +522,7 @@ export class ClaudeAcpAgent implements Agent {
typeof message.message.content === "string" &&
message.message.content.includes("<local-command-stderr>")
) {
console.error(message.message.content);
this.logger.error(message.message.content);
break;
}
// Skip these user messages for now, since they seem to just be messages we don't want in the feed
@@ -503,6 +560,7 @@ export class ClaudeAcpAgent implements Agent {
this.toolUseCache,
this.fileContentCache,
this.client,
this.logger,
)) {
await this.client.sessionUpdate(notification);
}
@@ -514,6 +572,7 @@ export class ClaudeAcpAgent implements Agent {
break;
default:
unreachable(message);
break;
}
}
throw new Error("Session did not end in result");
@@ -543,6 +602,7 @@ export class ClaudeAcpAgent implements Agent {
case "default":
case "acceptEdits":
case "bypassPermissions":
case "dontAsk":
case "plan":
this.sessions[params.sessionId].permissionMode = params.modeId;
try {
@@ -574,7 +634,7 @@ export class ClaudeAcpAgent implements Agent {
}
canUseTool(sessionId: string): CanUseTool {
return async (toolName, toolInput, { suggestions, toolUseID }) => {
return async (toolName, toolInput, { signal, suggestions, toolUseID }) => {
const session = this.sessions[sessionId];
if (!session) {
return {
@@ -599,11 +659,17 @@ export class ClaudeAcpAgent implements Agent {
toolCall: {
toolCallId: toolUseID,
rawInput: toolInput,
title: toolInfoFromToolUse({ name: toolName, input: toolInput }, this.fileContentCache)
.title,
title: toolInfoFromToolUse(
{ name: toolName, input: toolInput },
this.fileContentCache,
this.logger,
).title,
},
});
if (signal.aborted || response.outcome?.outcome === "cancelled") {
throw new Error("Tool use aborted");
}
if (
response.outcome?.outcome === "selected" &&
(response.outcome.optionId === "default" || response.outcome.optionId === "acceptEdits")
@@ -660,10 +726,16 @@ export class ClaudeAcpAgent implements Agent {
toolCall: {
toolCallId: toolUseID,
rawInput: toolInput,
title: toolInfoFromToolUse({ name: toolName, input: toolInput }, this.fileContentCache)
.title,
title: toolInfoFromToolUse(
{ name: toolName, input: toolInput },
this.fileContentCache,
this.logger,
).title,
},
});
if (signal.aborted || response.outcome?.outcome === "cancelled") {
throw new Error("Tool use aborted");
}
if (
response.outcome?.outcome === "selected" &&
(response.outcome.optionId === "allow" || response.outcome.optionId === "allow_always")
@@ -852,6 +924,7 @@ export function toAcpNotifications(
toolUseCache: ToolUseCache,
fileContentCache: { [key: string]: string },
client: AgentSideConnection,
logger: Logger,
): SessionNotification[] {
if (typeof content === "string") {
return [
@@ -927,8 +1000,8 @@ export function toAcpNotifications(
claudeCode: {
toolResponse,
toolName: toolUse.name,
} satisfies ToolUpdateMeta,
},
},
} satisfies ToolUpdateMeta,
toolCallId: toolUseId,
sessionUpdate: "tool_call_update",
};
@@ -937,7 +1010,7 @@ export function toAcpNotifications(
update,
});
} else {
console.error(
logger.error(
`[claude-code-acp] Got a tool response for tool use that wasn't tracked: ${toolUseId}`,
);
}
@@ -954,19 +1027,20 @@ export function toAcpNotifications(
_meta: {
claudeCode: {
toolName: chunk.name,
} satisfies ToolUpdateMeta,
},
},
} satisfies ToolUpdateMeta,
toolCallId: chunk.id,
sessionUpdate: "tool_call",
rawInput,
status: "pending",
...toolInfoFromToolUse(chunk, fileContentCache),
...toolInfoFromToolUse(chunk, fileContentCache, logger),
};
}
break;
}
case "tool_result":
case "tool_search_tool_result":
case "web_fetch_tool_result":
case "web_search_tool_result":
case "code_execution_tool_result":
@@ -975,7 +1049,7 @@ export function toAcpNotifications(
case "mcp_tool_result": {
const toolUse = toolUseCache[chunk.tool_use_id];
if (!toolUse) {
console.error(
logger.error(
`[claude-code-acp] Got a tool result for tool use that wasn't tracked: ${chunk.tool_use_id}`,
);
break;
@@ -986,8 +1060,8 @@ export function toAcpNotifications(
_meta: {
claudeCode: {
toolName: toolUse.name,
} satisfies ToolUpdateMeta,
},
},
} satisfies ToolUpdateMeta,
toolCallId: chunk.tool_use_id,
sessionUpdate: "tool_call_update",
status: "is_error" in chunk && chunk.is_error ? "failed" : "completed",
@@ -1007,7 +1081,8 @@ export function toAcpNotifications(
break;
default:
unreachable(chunk);
unreachable(chunk, logger);
break;
}
if (update) {
output.push({ sessionId, update });
@@ -1023,6 +1098,7 @@ export function streamEventToAcpNotifications(
toolUseCache: ToolUseCache,
fileContentCache: { [key: string]: string },
client: AgentSideConnection,
logger: Logger,
): SessionNotification[] {
const event = message.event;
switch (event.type) {
@@ -1034,6 +1110,7 @@ export function streamEventToAcpNotifications(
toolUseCache,
fileContentCache,
client,
logger,
);
case "content_block_delta":
return toAcpNotifications(
@@ -1043,6 +1120,7 @@ export function streamEventToAcpNotifications(
toolUseCache,
fileContentCache,
client,
logger,
);
// No content
case "message_start":
@@ -1052,7 +1130,8 @@ export function streamEventToAcpNotifications(
return [];
default:
unreachable(event);
unreachable(event, logger);
return [];
}
}

View File

@@ -5,6 +5,7 @@ export {
toAcpNotifications,
streamEventToAcpNotifications,
type ToolUpdateMeta,
type NewSessionMeta,
} from "./acp-agent.js";
export {
loadManagedSettings,

View File

@@ -377,6 +377,7 @@ Output: Create directory 'foo'`),
const handle = await agent.client.createTerminal({
command: input.command,
env: [{ name: "CLAUDECODE", value: "1" }],
sessionId,
outputByteLimit: 32_000,
});
@@ -582,7 +583,8 @@ In sessions with ${toolNames.killShell} always use it instead of KillShell.`,
content: [{ type: "text", text: "Command killed by timeout." }],
};
default: {
return unreachable(bgTerm);
unreachable(bgTerm);
throw new Error("Unexpected background terminal status");
}
}
},

View File

@@ -20,6 +20,7 @@ import { nodeToWebWritable, nodeToWebReadable } from "../utils.js";
import { markdownEscape, toolInfoFromToolUse, toolUpdateFromToolResult } from "../tools.js";
import { toAcpNotifications, promptToClaude } from "../acp-agent.js";
import { query, SDKAssistantMessage } from "@anthropic-ai/claude-agent-sdk";
import { randomUUID } from "crypto";
describe.skipIf(!process.env.RUN_INTEGRATION_TESTS)("ACP subprocess integration", () => {
let child: ReturnType<typeof spawn>;
@@ -658,6 +659,7 @@ describe("tool conversions", () => {
{},
{},
{} as AgentSideConnection,
console,
),
).toStrictEqual([
{
@@ -829,17 +831,29 @@ describe("prompt conversion", () => {
});
});
describe("SDK behavior", () => {
it.skipIf(!process.env.RUN_INTEGRATION_TESTS)(
"query has a 'default' model",
async () => {
const q = query({ prompt: "hi" });
const models = await q.supportedModels();
const defaultModel = models.find((m) => m.value === "default");
expect(defaultModel).toBeDefined();
},
10000,
);
describe.skipIf(!process.env.RUN_INTEGRATION_TESTS)("SDK behavior", () => {
it("query has a 'default' model", async () => {
const q = query({ prompt: "hi" });
const models = await q.supportedModels();
const defaultModel = models.find((m) => m.value === "default");
expect(defaultModel).toBeDefined();
}, 10000);
it("custom session id", async () => {
const sessionId = randomUUID();
const q = query({
prompt: "hi",
options: {
systemPrompt: { type: "preset", preset: "claude_code" },
extraArgs: { "session-id": sessionId },
settingSources: ["user", "project", "local"],
includePartialMessages: true,
},
});
const { value } = await q.next();
expect(value).toMatchObject({ type: "system", subtype: "init", session_id: sessionId });
}, 10000);
});
describe("permission requests", () => {

View File

@@ -1,72 +0,0 @@
import { describe, expect, it, vi } from "vitest";
import { createMcpServer } from "../mcp-server.js";
const SESSION_ID = "session-id";
function getTool(server: any, name: string) {
const tools = server?._registeredTools;
if (!tools || !tools[name]) {
throw new Error(`Tool ${name} not registered`);
}
return tools[name];
}
function createAgent(overrides: Record<string, unknown> = {}) {
return {
sessions: {
[SESSION_ID]: {},
},
readTextFile: vi.fn(),
writeTextFile: vi.fn(),
backgroundTerminals: {},
clientCapabilities: {},
client: {
sessionUpdate: vi.fn(),
createTerminal: vi.fn(),
requestPermission: vi.fn(),
},
...overrides,
};
}
describe("MCP server read-related tools", () => {
it("reports missing file contents instead of crashing the read tool", async () => {
const agent = createAgent();
agent.readTextFile.mockResolvedValue({});
const server = createMcpServer(agent as any, SESSION_ID, {
fs: { readTextFile: true },
});
const readTool = getTool(server, "Read");
const result = await readTool.callback({
file_path: "/tmp/file.txt",
offset: 1,
limit: 2000,
});
expect(result.content[0].text).toBe("Reading file failed: No file contents for /tmp/file.txt.");
});
it("reports missing contents inside the edit tool and skips writes", async () => {
const agent = createAgent();
agent.readTextFile.mockResolvedValue({});
const server = createMcpServer(agent as any, SESSION_ID, {
fs: { readTextFile: true, writeTextFile: true },
});
const editTool = getTool(server, "Edit");
const result = await editTool.callback({
file_path: "/tmp/file.txt",
old_string: "foo",
new_string: "bar",
replace_all: false,
});
expect(result.content[0].text).toBe("Editing file failed: No file contents for /tmp/file.txt.");
expect(agent.writeTextFile).not.toHaveBeenCalled();
});
});

View File

@@ -6,10 +6,12 @@ import {
BetaCodeExecutionToolResultBlockParam,
BetaRequestMCPToolResultBlockParam,
BetaTextEditorCodeExecutionToolResultBlockParam,
BetaToolSearchToolResultBlockParam,
BetaWebFetchToolResultBlockParam,
BetaWebSearchToolResultBlockParam,
} from "@anthropic-ai/sdk/resources/beta.mjs";
import { HookCallback } from "@anthropic-ai/claude-agent-sdk";
import { Logger } from "./acp-agent.js";
interface ToolInfo {
title: string;
@@ -27,6 +29,7 @@ interface ToolUpdate {
export function toolInfoFromToolUse(
toolUse: any,
cachedFileContent: { [key: string]: string },
logger: Logger = console,
): ToolInfo {
const name = toolUse.name;
const input = toolUse.input;
@@ -131,7 +134,14 @@ export function toolInfoFromToolUse(
title: "Read File",
kind: "read",
content: [],
locations: [{ path: input.file_path, line: input.offset ?? 0 }],
locations: input.file_path
? [
{
path: input.file_path,
line: input.offset ?? 0,
},
]
: [],
};
case "LS":
@@ -163,7 +173,7 @@ export function toolInfoFromToolUse(
newText = newContent.newContent;
affectedLines = newContent.lineNumbers;
} catch (e) {
console.error(e);
logger.error(e);
}
}
return {
@@ -404,7 +414,8 @@ export function toolUpdateFromToolResult(
| BetaCodeExecutionToolResultBlockParam
| BetaBashCodeExecutionToolResultBlockParam
| BetaTextEditorCodeExecutionToolResultBlockParam
| BetaRequestMCPToolResultBlockParam,
| BetaRequestMCPToolResultBlockParam
| BetaToolSearchToolResultBlockParam,
toolUse: any | undefined,
): ToolUpdate {
switch (toolUse?.name) {
@@ -570,19 +581,18 @@ export const registerHookCallback = (
};
/* A callback for Claude Code that is called when receiving a PostToolUse hook */
export const postToolUseHook: HookCallback = async (
input: any,
toolUseID: string | undefined,
): Promise<{ continue: boolean }> => {
if (input.hook_event_name === "PostToolUse" && toolUseID) {
const onPostToolUseHook = toolUseCallbacks[toolUseID]?.onPostToolUseHook;
if (onPostToolUseHook) {
await onPostToolUseHook(toolUseID, input.tool_input, input.tool_response);
delete toolUseCallbacks[toolUseID]; // Cleanup after execution
} else {
console.error(`No onPostToolUseHook found for tool use ID: ${toolUseID}`);
delete toolUseCallbacks[toolUseID];
export const createPostToolUseHook =
(logger: Logger = console): HookCallback =>
async (input: any, toolUseID: string | undefined): Promise<{ continue: boolean }> => {
if (input.hook_event_name === "PostToolUse" && toolUseID) {
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];
}
}
}
return { continue: true };
};
return { continue: true };
};

View File

@@ -4,6 +4,7 @@ import { Readable, Writable } from "node:stream";
import { WritableStream, ReadableStream } from "node:stream/web";
import { readFileSync } from "node:fs";
import { platform } from "node:os";
import { Logger } from "./acp-agent.js";
// Useful for bridging push-based and async-iterator-based code.
export class Pushable<T> implements AsyncIterable<T> {
@@ -75,14 +76,14 @@ export function nodeToWebReadable(nodeStream: Readable): ReadableStream<Uint8Arr
});
}
export function unreachable(value: never): never {
export function unreachable(value: never, logger: Logger = console) {
let valueAsString;
try {
valueAsString = JSON.stringify(value);
} catch {
valueAsString = value;
}
throw new Error(`Unexpected case: ${valueAsString}`);
logger.error(`Unexpected case: ${valueAsString}`);
}
export function sleep(time: number): Promise<void> {