Compare commits

...

6 Commits

Author SHA1 Message Date
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
Ben Brandt
427117dce6 Update to claude-agent-sdk 0.1.50 (#171) 2025-11-24 12:05:38 +00:00
Paul Nelson
b5a7e0f28d Fix read/edit tool error handling so upstream errors surface (#163)
Fixes #162:
- guard both tool callbacks so they return the host-provided error
instead of crashing on missing content
- skip writeTextFile in the edit tool when the initial read fails,
mirroring the read tool behavior
- add regression tests

---------

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2025-11-24 11:37:18 +01:00
Ben Brandt
8ef0b4e4c6 Update to claude-agent-sdk 0.1.46 (#168) 2025-11-19 08:42:32 +00:00
Ben Brandt
959cd6c8e2 Disable experimental terminal auth support for now (#167)
This was causing issues on Windows in the current form, and also has
some other issues that need looking into. Will revisit later.
2025-11-19 08:37:59 +00:00
8 changed files with 469 additions and 645 deletions

View File

@@ -1,5 +1,19 @@
# Changelog
## 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
- Update to @anthropic-ai/claude-agent-sdk@v0.1.50
## 0.10.6
- Disable experimental terminal auth support for now, as it was causing issues on Windows. Will revisit with a fix later.
- Update to @anthropic-ai/claude-agent-sdk@v0.1.46
## 0.10.5
- Better error messages at end of turn if there were any

885
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.5",
"version": "0.10.8",
"description": "An ACP-compatible coding agent powered by the Claude Code SDK (TypeScript)",
"main": "dist/lib.js",
"bin": {
@@ -52,25 +52,25 @@
"license": "Apache-2.0",
"dependencies": {
"@agentclientprotocol/sdk": "0.5.1",
"@anthropic-ai/claude-agent-sdk": "0.1.44",
"@anthropic-ai/claude-code": "2.0.44",
"@anthropic-ai/claude-agent-sdk": "0.1.51",
"@anthropic-ai/claude-code": "2.0.51",
"@modelcontextprotocol/sdk": "1.22.0",
"diff": "8.0.2",
"uuid": "13.0.0"
},
"devDependencies": {
"@anthropic-ai/sdk": "0.69.0",
"@anthropic-ai/sdk": "0.71.0",
"@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.10",
"@typescript-eslint/eslint-plugin": "8.48.0",
"@typescript-eslint/parser": "8.48.0",
"@vitest/ui": "4.0.13",
"eslint": "9.39.1",
"eslint-config-prettier": "10.1.8",
"globals": "16.5.0",
"prettier": "3.6.2",
"ts-node": "10.9.2",
"typescript": "5.9.3",
"vitest": "4.0.10"
"vitest": "4.0.13"
}
}

View File

@@ -53,7 +53,6 @@ import {
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 { fileURLToPath } from "node:url";
type Session = {
query: Query;
@@ -124,17 +123,17 @@ export class ClaudeAcpAgent implements Agent {
};
// If client supports terminal-auth capability, use that instead.
if (request.clientCapabilities?._meta?.["terminal-auth"] === true) {
const cliPath = fileURLToPath(import.meta.resolve("@anthropic-ai/claude-agent-sdk/cli.js"));
// if (request.clientCapabilities?._meta?.["terminal-auth"] === true) {
// const cliPath = fileURLToPath(import.meta.resolve("@anthropic-ai/claude-agent-sdk/cli.js"));
authMethod._meta = {
"terminal-auth": {
command: cliPath,
args: ["/login"],
label: "Claude Code Login",
},
};
}
// authMethod._meta = {
// "terminal-auth": {
// command: "node",
// args: [cliPath, "/login"],
// label: "Claude Code Login",
// },
// };
// }
return {
protocolVersion: 1,
@@ -394,6 +393,7 @@ export class ClaudeAcpAgent implements Agent {
break;
default:
unreachable(message);
break;
}
break;
case "result": {
@@ -431,6 +431,7 @@ export class ClaudeAcpAgent implements Agent {
return { stopReason: "max_turn_requests" };
default:
unreachable(message);
break;
}
break;
}
@@ -515,6 +516,7 @@ export class ClaudeAcpAgent implements Agent {
break;
default:
unreachable(message);
break;
}
}
throw new Error("Session did not end in result");
@@ -968,6 +970,7 @@ export function toAcpNotifications(
}
case "tool_result":
case "tool_search_tool_result":
case "web_fetch_tool_result":
case "web_search_tool_result":
case "code_execution_tool_result":
@@ -1009,6 +1012,7 @@ export function toAcpNotifications(
default:
unreachable(chunk);
break;
}
if (update) {
output.push({ sessionId, update });
@@ -1054,6 +1058,7 @@ export function streamEventToAcpNotifications(
default:
unreachable(event);
return [];
}
}

View File

@@ -101,15 +101,19 @@ Usage:
};
}
const content = await agent.readTextFile({
const readResponse = await agent.readTextFile({
sessionId,
path: input.file_path,
line: input.offset,
limit: input.limit,
});
if (typeof readResponse?.content !== "string") {
throw new Error(`No file contents for ${input.file_path}.`);
}
// Extract lines with byte limit enforcement
const result = extractLinesWithByteLimit(content.content, defaults.maxFileSize);
const result = extractLinesWithByteLimit(readResponse.content, defaults.maxFileSize);
// Construct informative message about what was read
let readInfo = "";
@@ -253,47 +257,62 @@ Usage:
},
},
async (input) => {
const session = agent.sessions[sessionId];
if (!session) {
try {
const session = agent.sessions[sessionId];
if (!session) {
return {
content: [
{
type: "text",
text: "The user has left the building",
},
],
};
}
const readResponse = await agent.readTextFile({
sessionId,
path: input.file_path,
});
if (typeof readResponse?.content !== "string") {
throw new Error(`No file contents for ${input.file_path}.`);
}
const { newContent } = replaceAndCalculateLocation(readResponse.content, [
{
oldText: input.old_string,
newText: input.new_string,
replaceAll: input.replace_all,
},
]);
const patch = diff.createPatch(input.file_path, readResponse.content, newContent);
await agent.writeTextFile({
sessionId,
path: input.file_path,
content: newContent,
});
return {
content: [
{
type: "text",
text: "The user has left the building",
text: patch,
},
],
};
} catch (error: any) {
return {
content: [
{
type: "text",
text: "Editing file failed: " + (error?.message ?? String(error)),
},
],
};
}
const { content } = await agent.readTextFile({
sessionId,
path: input.file_path,
});
const { newContent } = replaceAndCalculateLocation(content, [
{
oldText: input.old_string,
newText: input.new_string,
replaceAll: input.replace_all,
},
]);
const patch = diff.createPatch(input.file_path, content, newContent);
await agent.writeTextFile({
sessionId,
path: input.file_path,
content: newContent,
});
return {
content: [
{
type: "text",
text: patch,
},
],
};
},
);
}
@@ -563,7 +582,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

@@ -0,0 +1,72 @@
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,6 +6,7 @@ import {
BetaCodeExecutionToolResultBlockParam,
BetaRequestMCPToolResultBlockParam,
BetaTextEditorCodeExecutionToolResultBlockParam,
BetaToolSearchToolResultBlockParam,
BetaWebFetchToolResultBlockParam,
BetaWebSearchToolResultBlockParam,
} from "@anthropic-ai/sdk/resources/beta.mjs";
@@ -404,7 +405,8 @@ export function toolUpdateFromToolResult(
| BetaCodeExecutionToolResultBlockParam
| BetaBashCodeExecutionToolResultBlockParam
| BetaTextEditorCodeExecutionToolResultBlockParam
| BetaRequestMCPToolResultBlockParam,
| BetaRequestMCPToolResultBlockParam
| BetaToolSearchToolResultBlockParam,
toolUse: any | undefined,
): ToolUpdate {
switch (toolUse?.name) {

View File

@@ -75,14 +75,14 @@ export function nodeToWebReadable(nodeStream: Readable): ReadableStream<Uint8Arr
});
}
export function unreachable(value: never): never {
export function unreachable(value: never) {
let valueAsString;
try {
valueAsString = JSON.stringify(value);
} catch {
valueAsString = value;
}
throw new Error(`Unexpected case: ${valueAsString}`);
console.error(`Unexpected case: ${valueAsString}`);
}
export function sleep(time: number): Promise<void> {