Compare commits

...

5 Commits

Author SHA1 Message Date
Conrad Irwin
532daf0e7e v0.1.8 2025-09-02 12:33:40 -07:00
Conrad Irwin
d6e5910631 Fix load errors when SDK is busted 2025-09-02 12:33:11 -07:00
Agus Zubiaga
0d5e78f1ac Fix info for builtin tools (#11) 2025-09-02 15:50:21 -03:00
Agus Zubiaga
02fac67f52 Do not send tool_call_update for TodoWrite (#10)
When we receive a `TodoWrite` tool use, we send a plan update instead of
a tool call. However, we were still trying to send a tool call update
when we received the `TodoWrite` tool result, which caused an ACP error.

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2025-09-02 11:53:54 -03:00
Agus Zubiaga
1ef3ba327d Unpin SDK now that entrypoint is fixed (#9)
Co-authored-by: Bennet <bennet@zed.dev>
2025-09-02 11:38:45 -03:00
7 changed files with 92 additions and 56 deletions

8
package-lock.json generated
View File

@@ -9,7 +9,7 @@
"version": "0.1.7",
"license": "Apache-2.0",
"dependencies": {
"@anthropic-ai/claude-code": "1.0.96",
"@anthropic-ai/claude-code": "^1.0.100",
"@modelcontextprotocol/sdk": "^1.17.4",
"@types/express": "^5.0.3",
"@zed-industries/agent-client-protocol": "0.2.0-alpha.4",
@@ -32,9 +32,9 @@
}
},
"node_modules/@anthropic-ai/claude-code": {
"version": "1.0.96",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code/-/claude-code-1.0.96.tgz",
"integrity": "sha512-xnxhYzuh6PYlMcw56REMQiGMW20WaLLOvG8L8TObq70zhNKs3dro7nhYwHRe1c2ubTr20oIJK0aSkyD2BpO8nA==",
"version": "1.0.100",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code/-/claude-code-1.0.100.tgz",
"integrity": "sha512-b4FRo3t46kPawzE8pT7nxtDc9NdYPTFaPoGGns/ZNqsqRu2jKMiSjjt5DiFvfaBZA8Tnt2qXbv4nd1vqR9Ru8Q==",
"license": "SEE LICENSE IN README.md",
"bin": {
"claude": "cli.js"

View File

@@ -6,7 +6,7 @@
"bin": {
"claude-code-acp": "./dist/index.js"
},
"version": "0.1.7",
"version": "0.1.8",
"description": "An ACP-compatible coding agent powered by the Claude Code SDK (TypeScript)",
"main": "dist/index.js",
"type": "module",
@@ -34,7 +34,7 @@
"author": "Zed Industries",
"license": "Apache-2.0",
"dependencies": {
"@anthropic-ai/claude-code": "1.0.96",
"@anthropic-ai/claude-code": "^1.0.100",
"@modelcontextprotocol/sdk": "^1.17.4",
"@types/express": "^5.0.3",
"@zed-industries/agent-client-protocol": "0.2.0-alpha.4",

View File

@@ -27,11 +27,15 @@ import {
SDKAssistantMessage,
SDKUserMessage,
} from "@anthropic-ai/claude-code";
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,
sleep,
unreachable,
} from "./utils.js";
import { SessionNotification } from "@zed-industries/agent-client-protocol";
@@ -164,7 +168,7 @@ export class ClaudeAcpAgent implements Agent {
options.disallowedTools = disallowedTools;
}
let q = query({
const q = query({
prompt: input,
options,
});
@@ -320,8 +324,20 @@ async function availableSlashCommands(
"vim", // Not needed
];
//todo: Do not use `as any` once `supportedCommands` is exposed via the typescript interface
const commands = await (query as any).supportedCommands();
const commands = await Promise.race([
//todo: Do not use `as any` once `supportedCommands` is exposed via the typescript interface
(query as any).supportedCommands(),
sleep(5000).then(() => {
if (
fs.existsSync(path.resolve(os.homedir(), ".claude.json.backup")) &&
!fs.existsSync(path.resolve(os.homedir(), ".claude.json"))
) {
throw RequestError.authRequired();
}
throw new Error("can't load supported slash commands");
}),
]);
return commands
.map(
(command: {
@@ -440,14 +456,16 @@ function promptToClaude(prompt: PromptRequest): SDKUserMessage {
export function toAcpNotifications(
message: SDKAssistantMessage | SDKUserMessage,
sessionId: string,
toolUseCache: { [key: string]: any },
toolUseCache: {
[key: string]: { type: "tool_use"; id: string; name: string; input: any };
},
fileContentCache: { [key: string]: string },
): SessionNotification[] {
let chunks = message.message.content as ContentChunk[];
let output = [];
// Only handle the first chunk for streaming; extend as needed for batching
for (const chunk of chunks) {
let update: SessionNotification["update"];
let update: SessionNotification["update"] | null = null;
switch (chunk.type) {
case "text":
update = {
@@ -497,19 +515,32 @@ export function toAcpNotifications(
}
break;
case "tool_result":
update = {
toolCallId: chunk.tool_use_id,
sessionUpdate: "tool_call_update",
status: chunk.is_error ? "failed" : "completed",
...toolUpdateFromToolResult(chunk, toolUseCache[chunk.tool_use_id]),
};
case "tool_result": {
const toolUse = toolUseCache[chunk.tool_use_id];
if (!toolUse) {
console.error(
`[claude-code-acp] Got a tool result for tool use that wasn't tracked: ${chunk.tool_use_id}`,
);
break;
}
if (toolUse.name !== "TodoWrite") {
update = {
toolCallId: chunk.tool_use_id,
sessionUpdate: "tool_call_update",
status: chunk.is_error ? "failed" : "completed",
...toolUpdateFromToolResult(chunk, toolUseCache[chunk.tool_use_id]),
};
}
break;
}
default:
throw new Error("unhandled chunk type: " + chunk.type);
}
output.push({ sessionId, update });
if (update) {
output.push({ sessionId, update });
}
}
return output;

View File

@@ -10,6 +10,7 @@ import {
TerminalOutputResponse,
} from "@zed-industries/agent-client-protocol";
import { tool } from "@anthropic-ai/claude-code";
import { sleep } from "./utils.js";
export const SYSTEM_REMINDER = `
@@ -171,9 +172,9 @@ In sessions with mcp__acp__edit always use it instead of Edit as it will
allow the user to conveniently review changes.
File editing instructions:
- The \`old_text\` param must match existing file content, including indentation.
- The \`old_text\` param must come from the actual file, not an outline.
- The \`old_text\` section must not be empty.
- The \`old_string\` param must match existing file content, including indentation.
- The \`old_string\` param must come from the actual file, not an outline.
- The \`old_string\` section must not be empty.
- Be minimal with replacements:
- For unique lines, include only those lines.
- For non-unique lines, include enough context to identify them.
@@ -183,10 +184,10 @@ File editing instructions:
abs_path: z
.string()
.describe("The absolute path to the file to read."),
old_text: z
old_string: z
.string()
.describe("The old text to replace (must be unique in the file)"),
new_text: z.string().describe("The new text."),
new_string: z.string().describe("The new text."),
},
annotations: {
title: "Edit file",
@@ -219,8 +220,8 @@ File editing instructions:
content,
[
{
oldText: input.old_text,
newText: input.new_text,
oldText: input.old_string,
newText: input.new_string,
replaceAll: false,
},
],
@@ -598,10 +599,6 @@ File editing instructions:
return b.slice(i);
}
function sleep(time: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, time));
}
function toolCommandOutput(
status: "started" | "aborted" | "exited" | "killed" | "timedOut",
output: TerminalOutputResponse,

View File

@@ -309,7 +309,7 @@ describe("tool conversions", () => {
id: "toolu_01ABC123XYZ789",
name: "Write",
input: {
abs_path: "/Users/test/project/example.txt",
file_path: "/Users/test/project/example.txt",
content: "Hello, World!\nThis is test content.",
},
};
@@ -361,7 +361,7 @@ describe("tool conversions", () => {
id: "toolu_01MNO456PQR789",
name: "Read",
input: {
abs_path: "/Users/test/project/readme.md",
file_path: "/Users/test/project/readme.md",
},
};
@@ -664,8 +664,8 @@ describe("tool conversions", () => {
name: "edit",
input: {
abs_path: "/Users/test/project/example.txt",
old_text: "old content",
new_text: "new content",
old_string: "old content",
new_string: "new content",
},
};
@@ -795,8 +795,8 @@ describe("tool conversions", () => {
name: "mcp__acp__edit",
input: {
abs_path: "/Users/test/project/main.py",
old_text: "def hello():",
new_text: "def hello_world():",
old_string: "def hello():",
new_string: "def hello_world():",
},
};
@@ -823,8 +823,8 @@ describe("tool conversions", () => {
name: "edit",
input: {
abs_path: "/Users/test/project/notfound.txt",
old_text: "nonexistent",
new_text: "replacement",
old_string: "nonexistent",
new_string: "replacement",
},
};
@@ -849,8 +849,8 @@ describe("tool conversions", () => {
name: "edit",
input: {
abs_path: "/Users/test/project/test.txt",
old_text: "old",
new_text: "new",
old_string: "old",
new_string: "new",
},
};

View File

@@ -137,7 +137,7 @@ export function toolInfoFromToolUse(
title: "Read File",
kind: "read",
content: [],
locations: [{ path: input.abs_path, line: input.offset ?? 0 }],
locations: [{ path: input.file_path, line: input.offset ?? 0 }],
};
case "LS":
@@ -149,42 +149,46 @@ export function toolInfoFromToolUse(
};
case "mcp__acp__edit":
case "Edit":
let oldText = input.old_text || null;
let newText = input.new_text;
case "Edit": {
const path = input?.abs_path ?? input?.file_path;
let oldText = input.old_string ?? null;
let newText = input.new_string ?? "";
let line = null;
if (input?.abs_path && input.old_text) {
if (path && input.old_text) {
try {
let oldContent = cachedFileContent[input.abs_path] || "";
let oldContent = cachedFileContent[path] || "";
let newContent = replaceAndCalculateLocation(oldContent, [
{
oldText: input.old_text,
newText: input.new_text,
oldText: input.old_string,
newText: input.new_string,
replaceAll: false,
},
]);
oldText = oldContent;
newText = newContent.newContent;
line = newContent.lineNumbers[0];
} catch (e) {
console.error(e);
}
}
return {
title: input?.abs_path ? `Edit ${input.abs_path}` : "Edit",
title: path ? `Edit ${path}` : "Edit",
kind: "edit",
content:
input && input.abs_path
input && path
? [
{
type: "diff",
path: input.abs_path,
path,
oldText,
newText,
},
]
: [],
locations: input?.abs_path ? [{ path: input.abs_path }] : [],
locations: path ? [{ path, line }] : [],
};
}
case "mcp__acp__multi-edit":
case "MultiEdit":
@@ -265,20 +269,20 @@ export function toolInfoFromToolUse(
case "Write":
return {
title: input?.abs_path ? `Write ${input.abs_path}` : "Write",
title: input?.file_path ? `Write ${input.file_path}` : "Write",
kind: "edit",
content:
input && input.abs_path
input && input.file_path
? [
{
type: "diff",
path: input.abs_path,
path: input.file_path,
oldText: null,
newText: input.content,
},
]
: [],
locations: input?.abs_path ? [{ path: input.abs_path }] : [],
locations: input?.file_path ? [{ path: input.file_path }] : [],
};
case "Glob": {

View File

@@ -77,3 +77,7 @@ export function nodeToWebReadable(
export function unreachable(value: never): never {
throw new Error(`Unexpected case: ${value}`);
}
export function sleep(time: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, time));
}