mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-10-27 20:42:10 +00:00
Compare commits
18 Commits
code-block
...
v0.1.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f79a121c9c | ||
|
|
ace8b0b5e5 | ||
|
|
1d14d35779 | ||
|
|
d39c2adb21 | ||
|
|
48a31df673 | ||
|
|
63d243deef | ||
|
|
048d5c562c | ||
|
|
d6b805a77b | ||
|
|
b83c250f17 | ||
|
|
d7511ac542 | ||
|
|
f18bfeaf8d | ||
|
|
c4674e901a | ||
|
|
dd088b39e3 | ||
|
|
a066b838a2 | ||
|
|
b875e24ad5 | ||
|
|
77f667c619 | ||
|
|
1f2953a4ce | ||
|
|
ce0e1a30ac |
42
.github/workflows/ci.yml
vendored
Normal file
42
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: "ci-${{ github.ref }}"
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: latest
|
||||
cache: "npm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Check formatting
|
||||
run: npm run format:check
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Run tests
|
||||
run: npm run test:run
|
||||
23
.github/workflows/publish.yml
vendored
Normal file
23
.github/workflows/publish.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
name: Publish Package to npmjs
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
|
||||
# Setup .npmrc file to publish to npm
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
- run: npm ci
|
||||
# Add back provenance once repo is public
|
||||
- run: npm publish # --provenance --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
1052
package-lock.json
generated
1052
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
15
package.json
15
package.json
@@ -3,19 +3,24 @@
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"version": "0.1.0",
|
||||
"bin": {
|
||||
"claude-code-acp": "./dist/index.js"
|
||||
},
|
||||
"version": "0.1.6",
|
||||
"description": "An ACP-compatible coding agent powered by the Claude Code SDK (TypeScript)",
|
||||
"main": "dist/acp-agent.js",
|
||||
"main": "dist/index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js",
|
||||
"dev": "npm run build && npm run start",
|
||||
"lint": "eslint src --ext .ts",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"test": "vitest",
|
||||
"test:integration": "RUN_INTEGRATION_TESTS=true vitest run",
|
||||
"test:run": "vitest run",
|
||||
"test:coverage": "vitest run --coverage"
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"keywords": [
|
||||
"claude",
|
||||
@@ -42,7 +47,7 @@
|
||||
"@types/node": "^24.3.0",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@vitest/ui": "^3.2.4",
|
||||
"eslint": "^8.56.0",
|
||||
"prettier": "^3.6.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.4.0",
|
||||
"vitest": "^3.2.4"
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
AuthenticateRequest,
|
||||
CancelNotification,
|
||||
Client,
|
||||
ClientCapabilities,
|
||||
InitializeRequest,
|
||||
InitializeResponse,
|
||||
LoadSessionRequest,
|
||||
@@ -11,12 +12,17 @@ import {
|
||||
NewSessionResponse,
|
||||
PromptRequest,
|
||||
PromptResponse,
|
||||
ReadTextFileRequest,
|
||||
ReadTextFileResponse,
|
||||
RequestError,
|
||||
ToolCallContent,
|
||||
ToolKind,
|
||||
WriteTextFileRequest,
|
||||
WriteTextFileResponse,
|
||||
} from "@zed-industries/agent-client-protocol";
|
||||
import {
|
||||
McpServerConfig,
|
||||
Options,
|
||||
Query,
|
||||
query,
|
||||
SDKAssistantMessage,
|
||||
@@ -54,13 +60,17 @@ export class ClaudeAcpAgent implements Agent {
|
||||
};
|
||||
client: Client;
|
||||
toolUseCache: { [key: string]: any };
|
||||
fileContentCache: { [key: string]: any };
|
||||
clientCapabilities?: ClientCapabilities;
|
||||
|
||||
constructor(client: Client) {
|
||||
this.sessions = {};
|
||||
this.client = client;
|
||||
this.toolUseCache = {};
|
||||
this.fileContentCache = {};
|
||||
}
|
||||
async initialize(params: InitializeRequest): Promise<InitializeResponse> {
|
||||
async initialize(request: InitializeRequest): Promise<InitializeResponse> {
|
||||
this.clientCapabilities = request.clientCapabilities;
|
||||
return {
|
||||
protocolVersion: 1,
|
||||
// todo!()
|
||||
@@ -99,8 +109,11 @@ export class ClaudeAcpAgent implements Agent {
|
||||
}
|
||||
}
|
||||
|
||||
// todo!() auth
|
||||
let server = await createMcpServer(this, sessionId);
|
||||
let server = await createMcpServer(
|
||||
this,
|
||||
sessionId,
|
||||
this.clientCapabilities,
|
||||
);
|
||||
let address = server.address() as AddressInfo;
|
||||
mcpServers["acp"] = {
|
||||
type: "http",
|
||||
@@ -110,17 +123,24 @@ export class ClaudeAcpAgent implements Agent {
|
||||
},
|
||||
};
|
||||
|
||||
let options: Options = {
|
||||
cwd: params.cwd,
|
||||
mcpServers,
|
||||
disallowedTools: [],
|
||||
permissionPromptToolName: "mcp__acp__permission",
|
||||
stderr: (err) => console.error(err),
|
||||
};
|
||||
if (this.clientCapabilities?.fs?.readTextFile) {
|
||||
options.allowedTools = ["mcp__acp__read"];
|
||||
options.disallowedTools!.push("Read");
|
||||
}
|
||||
if (this.clientCapabilities?.fs?.writeTextFile) {
|
||||
options.disallowedTools!.push("Write", "Edit", "MultiEdit");
|
||||
}
|
||||
|
||||
let q = query({
|
||||
prompt: input,
|
||||
options: {
|
||||
cwd: params.cwd,
|
||||
mcpServers,
|
||||
allowedTools: ["mcp__acp__read"],
|
||||
disallowedTools: ["Read", "Write", "Edit", "MultiEdit"],
|
||||
strictMcpConfig: true,
|
||||
permissionPromptToolName: "mcp__acp__permission",
|
||||
stderr: (err) => console.error(err),
|
||||
},
|
||||
options,
|
||||
});
|
||||
this.sessions[sessionId] = {
|
||||
query: q,
|
||||
@@ -196,6 +216,7 @@ export class ClaudeAcpAgent implements Agent {
|
||||
message,
|
||||
params.sessionId,
|
||||
this.toolUseCache,
|
||||
this.fileContentCache,
|
||||
)) {
|
||||
await this.client.sessionUpdate(notification);
|
||||
}
|
||||
@@ -215,6 +236,24 @@ export class ClaudeAcpAgent implements Agent {
|
||||
this.sessions[params.sessionId].cancelled = true;
|
||||
await this.sessions[params.sessionId].query.interrupt();
|
||||
}
|
||||
|
||||
async readTextFile(
|
||||
params: ReadTextFileRequest,
|
||||
): Promise<ReadTextFileResponse> {
|
||||
let response = await this.client.readTextFile(params);
|
||||
if (!params.limit && !params.line) {
|
||||
this.fileContentCache[params.path] = response.content;
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
async writeTextFile(
|
||||
params: WriteTextFileRequest,
|
||||
): Promise<WriteTextFileResponse> {
|
||||
let response = await this.client.writeTextFile(params);
|
||||
this.fileContentCache[params.path] = params.content;
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
function formatUriAsLink(uri: string): string {
|
||||
@@ -313,6 +352,7 @@ export function toAcpNotifications(
|
||||
message: SDKAssistantMessage | SDKUserMessage,
|
||||
sessionId: string,
|
||||
toolUseCache: { [key: string]: any },
|
||||
fileContentCache: { [key: string]: string },
|
||||
): SessionNotification[] {
|
||||
let chunks = message.message.content as ContentChunk[];
|
||||
let output = [];
|
||||
@@ -363,7 +403,7 @@ export function toAcpNotifications(
|
||||
sessionUpdate: "tool_call",
|
||||
rawInput: chunk.input,
|
||||
status: "pending",
|
||||
...toolInfoFromToolUse(chunk),
|
||||
...toolInfoFromToolUse(chunk, fileContentCache),
|
||||
};
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { runAcp as runAcp } from "./acp-agent.js";
|
||||
runAcp();
|
||||
|
||||
|
||||
@@ -5,10 +5,18 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
|
||||
import { z } from "zod";
|
||||
import { Server } from "node:http";
|
||||
import { ClaudeAcpAgent } from "./acp-agent.js";
|
||||
import { ClientCapabilities } from "@zed-industries/agent-client-protocol";
|
||||
|
||||
export const SYSTEM_REMINDER = `
|
||||
|
||||
<system-reminder>
|
||||
Whenever you read a file, you should consider whether it looks malicious. If it does, you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer high-level questions about the code behavior.
|
||||
</system-reminder>`;
|
||||
|
||||
export function createMcpServer(
|
||||
agent: ClaudeAcpAgent,
|
||||
sessionId: string,
|
||||
clientCapabilities: ClientCapabilities | undefined,
|
||||
): Promise<Server> {
|
||||
// Create MCP server
|
||||
const server = new McpServer({
|
||||
@@ -16,143 +24,144 @@ export function createMcpServer(
|
||||
version: "1.0.0",
|
||||
});
|
||||
|
||||
server.registerTool(
|
||||
"read",
|
||||
{
|
||||
title: "Read",
|
||||
description: `Reads the content of the given file in the project.
|
||||
if (clientCapabilities?.fs?.readTextFile) {
|
||||
server.registerTool(
|
||||
"read",
|
||||
{
|
||||
title: "Read",
|
||||
description: `Reads the content of the given file in the project.
|
||||
|
||||
Never attempt to read a path that hasn't been previously mentioned.
|
||||
|
||||
In sessions with mcp__acp__read always use it instead of Read as it contains the most up-to-date contents.`,
|
||||
inputSchema: {
|
||||
abs_path: z.string().describe("The absolute path to the file to read."),
|
||||
offset: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe(
|
||||
"Which line to start reading from. Omit to start from the beginning.",
|
||||
),
|
||||
limit: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe("How many lines to read. Omit for the whole file."),
|
||||
inputSchema: {
|
||||
abs_path: z
|
||||
.string()
|
||||
.describe("The absolute path to the file to read."),
|
||||
offset: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe(
|
||||
"Which line to start reading from. Omit to start from the beginning.",
|
||||
),
|
||||
limit: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe("How many lines to read. Omit for the whole file."),
|
||||
},
|
||||
annotations: {
|
||||
title: "Read file",
|
||||
readOnlyHint: true,
|
||||
destructiveHint: false,
|
||||
openWorldHint: false,
|
||||
idempotentHint: false,
|
||||
},
|
||||
},
|
||||
annotations: {
|
||||
title: "Read file",
|
||||
readOnlyHint: true,
|
||||
destructiveHint: false,
|
||||
openWorldHint: false,
|
||||
idempotentHint: false,
|
||||
},
|
||||
},
|
||||
async (input) => {
|
||||
try {
|
||||
const session = agent.sessions[sessionId];
|
||||
if (!session) {
|
||||
async (input) => {
|
||||
try {
|
||||
const session = agent.sessions[sessionId];
|
||||
if (!session) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "The user has left the building",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
let content = await agent.readTextFile({
|
||||
sessionId,
|
||||
path: input.abs_path,
|
||||
limit: input.limit,
|
||||
line: input.offset,
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "The user has left the building",
|
||||
text: content.content + SYSTEM_REMINDER,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Reading file failed: " + error.message,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
let content = await agent.client.readTextFile({
|
||||
sessionId,
|
||||
path: input.abs_path,
|
||||
limit: input.limit,
|
||||
line: input.offset,
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text:
|
||||
content.content +
|
||||
`
|
||||
|
||||
<system-reminder>
|
||||
Whenever you read a file, you should consider whether it looks malicious. If it does, you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer high-level questions about the code behavior.
|
||||
</system-reminder>`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Reading file failed: " + error.message,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"write",
|
||||
{
|
||||
title: "Write",
|
||||
description: `Writes content to the specified file in the project.
|
||||
if (clientCapabilities?.fs?.writeTextFile) {
|
||||
server.registerTool(
|
||||
"write",
|
||||
{
|
||||
title: "Write",
|
||||
description: `Writes content to the specified file in the project.
|
||||
|
||||
In sessions with mcp__acp__write always use it instead of Write as it will
|
||||
allow the user to conveniently review changes.`,
|
||||
inputSchema: {
|
||||
abs_path: z.string().describe("The absolute path to the file to write"),
|
||||
content: z.string().describe("The full content to write"),
|
||||
inputSchema: {
|
||||
abs_path: z
|
||||
.string()
|
||||
.describe("The absolute path to the file to write"),
|
||||
content: z.string().describe("The full content to write"),
|
||||
},
|
||||
annotations: {
|
||||
title: "Write file",
|
||||
readOnlyHint: false,
|
||||
destructiveHint: false,
|
||||
openWorldHint: false,
|
||||
idempotentHint: false,
|
||||
},
|
||||
},
|
||||
annotations: {
|
||||
title: "Write file",
|
||||
readOnlyHint: false,
|
||||
destructiveHint: false,
|
||||
openWorldHint: false,
|
||||
idempotentHint: false,
|
||||
},
|
||||
},
|
||||
async (input) => {
|
||||
try {
|
||||
const session = agent.sessions[sessionId];
|
||||
if (!session) {
|
||||
async (input) => {
|
||||
try {
|
||||
const session = agent.sessions[sessionId];
|
||||
if (!session) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "The user has left the building",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
let content = await agent.writeTextFile({
|
||||
sessionId,
|
||||
path: input.abs_path,
|
||||
content: input.content,
|
||||
});
|
||||
|
||||
return {
|
||||
content: [],
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "The user has left the building",
|
||||
text: "Writing file failed: " + error.message,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
let content = await agent.client.writeTextFile({
|
||||
sessionId,
|
||||
path: input.abs_path,
|
||||
content: input.content,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
content: [],
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Writing file failed: " + error.message,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"edit",
|
||||
{
|
||||
title: "Edit",
|
||||
description: `Edit a file.
|
||||
server.registerTool(
|
||||
"edit",
|
||||
{
|
||||
title: "Edit",
|
||||
description: `Edit a file.
|
||||
|
||||
In sessions with mcp__acp__edit always use it instead of Edit as it will
|
||||
allow the user to conveniently review changes.
|
||||
@@ -166,23 +175,116 @@ File editing instructions:
|
||||
- For non-unique lines, include enough context to identify them.
|
||||
- Do not escape quotes, newlines, or other characters.
|
||||
- Only edit the specified file.`,
|
||||
inputSchema: {
|
||||
abs_path: z.string().describe("The absolute path to the file to read."),
|
||||
old_text: z
|
||||
.string()
|
||||
.describe("The old text to replace (must be unique in the file)"),
|
||||
new_text: z.string().describe("The new text."),
|
||||
inputSchema: {
|
||||
abs_path: z
|
||||
.string()
|
||||
.describe("The absolute path to the file to read."),
|
||||
old_text: z
|
||||
.string()
|
||||
.describe("The old text to replace (must be unique in the file)"),
|
||||
new_text: z.string().describe("The new text."),
|
||||
},
|
||||
annotations: {
|
||||
title: "Edit file",
|
||||
readOnlyHint: false,
|
||||
destructiveHint: false,
|
||||
openWorldHint: false,
|
||||
idempotentHint: false,
|
||||
},
|
||||
},
|
||||
annotations: {
|
||||
title: "Edit file",
|
||||
readOnlyHint: false,
|
||||
destructiveHint: false,
|
||||
openWorldHint: false,
|
||||
idempotentHint: false,
|
||||
async (input) => {
|
||||
try {
|
||||
const session = agent.sessions[sessionId];
|
||||
if (!session) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "The user has left the building",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
let { content } = await agent.readTextFile({
|
||||
sessionId,
|
||||
path: input.abs_path,
|
||||
});
|
||||
|
||||
const { newContent, lineNumbers } = replaceAndCalculateLocation(
|
||||
content,
|
||||
[
|
||||
{
|
||||
oldText: input.old_text,
|
||||
newText: input.new_text,
|
||||
replaceAll: false,
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
await agent.writeTextFile({
|
||||
sessionId,
|
||||
path: input.abs_path,
|
||||
content: newContent,
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify({ lineNumbers }),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Editing file failed: " + error.message,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
async (input) => {
|
||||
try {
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"multi-edit",
|
||||
{
|
||||
title: "Multi Edit",
|
||||
description: `Edit a file with multiple sequential edits.`,
|
||||
inputSchema: {
|
||||
file_path: z
|
||||
.string()
|
||||
.describe("The absolute path to the file to modify"),
|
||||
edits: z
|
||||
.array(
|
||||
z.object({
|
||||
old_string: z.string().describe("The text to replace"),
|
||||
new_string: z.string().describe("The text to replace it with"),
|
||||
replace_all: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe(
|
||||
"Replace all occurrences of old_string (default false)",
|
||||
),
|
||||
}),
|
||||
)
|
||||
.min(1)
|
||||
.describe(
|
||||
"Array of edit operations to perform sequentially on the file",
|
||||
),
|
||||
},
|
||||
annotations: {
|
||||
title: "Multi Edit file",
|
||||
readOnlyHint: false,
|
||||
destructiveHint: false,
|
||||
openWorldHint: false,
|
||||
idempotentHint: false,
|
||||
},
|
||||
},
|
||||
async (input) => {
|
||||
const session = agent.sessions[sessionId];
|
||||
if (!session) {
|
||||
return {
|
||||
@@ -195,25 +297,23 @@ File editing instructions:
|
||||
};
|
||||
}
|
||||
|
||||
let { content } = await agent.client.readTextFile({
|
||||
let { content } = await agent.readTextFile({
|
||||
sessionId,
|
||||
path: input.abs_path,
|
||||
path: input.file_path,
|
||||
});
|
||||
|
||||
const { newContent, lineNumbers } = replaceAndCalculateLocation(
|
||||
content,
|
||||
[
|
||||
{
|
||||
oldText: input.old_text,
|
||||
newText: input.new_text,
|
||||
replaceAll: false,
|
||||
},
|
||||
],
|
||||
input.edits.map((edit) => ({
|
||||
oldText: edit.old_string,
|
||||
newText: edit.new_string,
|
||||
replaceAll: edit.replace_all ?? false,
|
||||
})),
|
||||
);
|
||||
|
||||
await agent.client.writeTextFile({
|
||||
await agent.writeTextFile({
|
||||
sessionId,
|
||||
path: input.abs_path,
|
||||
path: input.file_path,
|
||||
content: newContent,
|
||||
});
|
||||
|
||||
@@ -225,97 +325,9 @@ File editing instructions:
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Editing file failed: " + error.message,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"multi-edit",
|
||||
{
|
||||
title: "Multi Edit",
|
||||
description: `Edit a file with multiple sequential edits.`,
|
||||
inputSchema: {
|
||||
file_path: z
|
||||
.string()
|
||||
.describe("The absolute path to the file to modify"),
|
||||
edits: z
|
||||
.array(
|
||||
z.object({
|
||||
old_string: z.string().describe("The text to replace"),
|
||||
new_string: z.string().describe("The text to replace it with"),
|
||||
replace_all: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe(
|
||||
"Replace all occurrences of old_string (default false)",
|
||||
),
|
||||
}),
|
||||
)
|
||||
.min(1)
|
||||
.describe(
|
||||
"Array of edit operations to perform sequentially on the file",
|
||||
),
|
||||
},
|
||||
annotations: {
|
||||
title: "Multi Edit file",
|
||||
readOnlyHint: false,
|
||||
destructiveHint: false,
|
||||
openWorldHint: false,
|
||||
idempotentHint: false,
|
||||
},
|
||||
},
|
||||
async (input) => {
|
||||
const session = agent.sessions[sessionId];
|
||||
if (!session) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "The user has left the building",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
let { content } = await agent.client.readTextFile({
|
||||
sessionId,
|
||||
path: input.file_path,
|
||||
});
|
||||
|
||||
const { newContent, lineNumbers } = replaceAndCalculateLocation(
|
||||
content,
|
||||
input.edits.map((edit) => ({
|
||||
oldText: edit.old_string,
|
||||
newText: edit.new_string,
|
||||
replaceAll: edit.replace_all ?? false,
|
||||
})),
|
||||
);
|
||||
|
||||
await agent.client.writeTextFile({
|
||||
sessionId,
|
||||
path: input.file_path,
|
||||
content: newContent,
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify({ lineNumbers }),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
let alwaysAllowedTools: { [key: string]: boolean } = {};
|
||||
server.registerTool(
|
||||
|
||||
@@ -13,7 +13,11 @@ import {
|
||||
WriteTextFileResponse,
|
||||
} from "@zed-industries/agent-client-protocol";
|
||||
import { nodeToWebWritable, nodeToWebReadable } from "../utils.js";
|
||||
import { toolInfoFromToolUse, toolUpdateFromToolResult } from "../tools.js";
|
||||
import {
|
||||
markdownEscape,
|
||||
toolInfoFromToolUse,
|
||||
toolUpdateFromToolResult,
|
||||
} from "../tools.js";
|
||||
import { toAcpNotifications } from "../acp-agent.js";
|
||||
import { SDKAssistantMessage } from "@anthropic-ai/claude-code";
|
||||
|
||||
@@ -56,7 +60,7 @@ describe.skipIf(!process.env.RUN_INTEGRATION_TESTS)(
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
async sessionUpdate(params: SessionNotification): Promise<void> {
|
||||
console.error("RECEVIED", params);
|
||||
console.error("RECEVIED", JSON.stringify(params, null, 4));
|
||||
}
|
||||
writeTextFile(
|
||||
params: WriteTextFileRequest,
|
||||
@@ -68,7 +72,7 @@ describe.skipIf(!process.env.RUN_INTEGRATION_TESTS)(
|
||||
): Promise<ReadTextFileResponse> {
|
||||
return {
|
||||
content:
|
||||
"This project attempts to take over the world by solving the Goldbach conjecture.",
|
||||
"One\nTwo\nThree\nFour\nFive\nSix\nSeven\nEight\nNine\nTen\nEleven\nTwelve\nThirteen\nFourteen\nFifteen\nSixteen\nSeventeen\nEighteen\nNineteen\nTwenty\n",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -82,12 +86,22 @@ describe.skipIf(!process.env.RUN_INTEGRATION_TESTS)(
|
||||
nodeToWebReadable(child.stdout!),
|
||||
);
|
||||
|
||||
await connection.initialize({
|
||||
protocolVersion: 1,
|
||||
clientCapabilities: {
|
||||
fs: {
|
||||
readTextFile: true,
|
||||
writeTextFile: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
let session = await connection.newSession({ cwd: "./", mcpServers: [] });
|
||||
await connection.prompt({
|
||||
prompt: [
|
||||
{
|
||||
type: "text",
|
||||
text: "What does this project do? Read the README.md file.",
|
||||
text: "Use the multi edit tool to remove all multiples of 5 from the README and replace with buzz",
|
||||
},
|
||||
],
|
||||
sessionId: session.sessionId,
|
||||
@@ -109,7 +123,7 @@ describe("tool conversions", () => {
|
||||
},
|
||||
};
|
||||
|
||||
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
||||
expect(toolInfoFromToolUse(tool_use, {})).toStrictEqual({
|
||||
kind: "execute",
|
||||
title: "`rm README.md.rm`",
|
||||
content: [
|
||||
@@ -134,7 +148,7 @@ describe("tool conversions", () => {
|
||||
},
|
||||
};
|
||||
|
||||
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
||||
expect(toolInfoFromToolUse(tool_use, {})).toStrictEqual({
|
||||
kind: "search",
|
||||
title: "Find */**.ts",
|
||||
content: [],
|
||||
@@ -155,7 +169,7 @@ describe("tool conversions", () => {
|
||||
},
|
||||
};
|
||||
|
||||
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
||||
expect(toolInfoFromToolUse(tool_use, {})).toStrictEqual({
|
||||
kind: "think",
|
||||
title: "Handle user's work request",
|
||||
content: [
|
||||
@@ -176,15 +190,16 @@ describe("tool conversions", () => {
|
||||
id: "toolu_01EEqsX7Eb9hpx87KAHVPTey",
|
||||
name: "LS",
|
||||
input: {
|
||||
path: "/Users/benbrandt/github/claude-code-acp",
|
||||
path: "/Users/test/github/claude-code-acp",
|
||||
},
|
||||
};
|
||||
|
||||
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
||||
expect(toolInfoFromToolUse(tool_use, {})).toStrictEqual({
|
||||
kind: "search",
|
||||
title: "/Users/benbrandt/github/claude-code-acp",
|
||||
title:
|
||||
"List the `/Users/test/github/claude-code-acp` directory's contents",
|
||||
content: [],
|
||||
locations: [{ path: "/Users/benbrandt/github/claude-code-acp" }],
|
||||
locations: [],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -198,7 +213,7 @@ describe("tool conversions", () => {
|
||||
},
|
||||
};
|
||||
|
||||
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
||||
expect(toolInfoFromToolUse(tool_use, {})).toStrictEqual({
|
||||
kind: "search",
|
||||
title: 'grep ".*"',
|
||||
content: [],
|
||||
@@ -216,7 +231,7 @@ describe("tool conversions", () => {
|
||||
},
|
||||
};
|
||||
|
||||
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
||||
expect(toolInfoFromToolUse(tool_use, {})).toStrictEqual({
|
||||
kind: "edit",
|
||||
title: "Write /Users/test/project/example.txt",
|
||||
content: [
|
||||
@@ -242,7 +257,7 @@ describe("tool conversions", () => {
|
||||
},
|
||||
};
|
||||
|
||||
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
||||
expect(toolInfoFromToolUse(tool_use, {})).toStrictEqual({
|
||||
kind: "edit",
|
||||
title: "Write /Users/test/project/config.json",
|
||||
content: [
|
||||
@@ -267,15 +282,10 @@ describe("tool conversions", () => {
|
||||
},
|
||||
};
|
||||
|
||||
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
||||
expect(toolInfoFromToolUse(tool_use, {})).toStrictEqual({
|
||||
kind: "read",
|
||||
title: "Read File",
|
||||
content: [
|
||||
{
|
||||
type: "content",
|
||||
content: { type: "text", text: "/Users/test/project/readme.md" },
|
||||
},
|
||||
],
|
||||
content: [],
|
||||
locations: [{ path: "/Users/test/project/readme.md", line: 0 }],
|
||||
});
|
||||
});
|
||||
@@ -290,7 +300,7 @@ describe("tool conversions", () => {
|
||||
},
|
||||
};
|
||||
|
||||
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
||||
expect(toolInfoFromToolUse(tool_use, {})).toStrictEqual({
|
||||
kind: "read",
|
||||
title: "Read /Users/test/project/data.json",
|
||||
content: [],
|
||||
@@ -309,7 +319,7 @@ describe("tool conversions", () => {
|
||||
},
|
||||
};
|
||||
|
||||
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
||||
expect(toolInfoFromToolUse(tool_use, {})).toStrictEqual({
|
||||
kind: "read",
|
||||
title: "Read /Users/test/project/large.txt (1 - 100)",
|
||||
content: [],
|
||||
@@ -329,7 +339,7 @@ describe("tool conversions", () => {
|
||||
},
|
||||
};
|
||||
|
||||
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
||||
expect(toolInfoFromToolUse(tool_use, {})).toStrictEqual({
|
||||
kind: "read",
|
||||
title: "Read /Users/test/project/large.txt (51 - 150)",
|
||||
content: [],
|
||||
@@ -348,7 +358,7 @@ describe("tool conversions", () => {
|
||||
},
|
||||
};
|
||||
|
||||
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
||||
expect(toolInfoFromToolUse(tool_use, {})).toStrictEqual({
|
||||
kind: "read",
|
||||
title: "Read /Users/test/project/large.txt (from line 201)",
|
||||
content: [],
|
||||
@@ -368,7 +378,7 @@ describe("tool conversions", () => {
|
||||
},
|
||||
};
|
||||
|
||||
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
||||
expect(toolInfoFromToolUse(tool_use, {})).toStrictEqual({
|
||||
kind: "fetch",
|
||||
title: "Fetch https://agentclientprotocol.com",
|
||||
content: [
|
||||
@@ -393,7 +403,7 @@ describe("tool conversions", () => {
|
||||
},
|
||||
};
|
||||
|
||||
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
||||
expect(toolInfoFromToolUse(tool_use, {})).toStrictEqual({
|
||||
kind: "fetch",
|
||||
title: '"agentclientprotocol.com"',
|
||||
content: [],
|
||||
@@ -410,7 +420,7 @@ describe("tool conversions", () => {
|
||||
},
|
||||
};
|
||||
|
||||
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
||||
expect(toolInfoFromToolUse(tool_use, {})).toStrictEqual({
|
||||
kind: "execute",
|
||||
title: `Kill Process`,
|
||||
content: [],
|
||||
@@ -427,7 +437,7 @@ describe("tool conversions", () => {
|
||||
},
|
||||
};
|
||||
|
||||
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
||||
expect(toolInfoFromToolUse(tool_use, {})).toStrictEqual({
|
||||
kind: "execute",
|
||||
title: `Tail Logs`,
|
||||
content: [],
|
||||
@@ -512,7 +522,7 @@ describe("tool conversions", () => {
|
||||
session_id: "d056596f-e328-41e9-badd-b07122ae5227",
|
||||
uuid: "b7c3330c-de8f-4bba-ac53-68c7f76ffeb5",
|
||||
};
|
||||
expect(toAcpNotifications(received, "test", {})).toStrictEqual([
|
||||
expect(toAcpNotifications(received, "test", {}, {})).toStrictEqual([
|
||||
{
|
||||
sessionId: "test",
|
||||
update: {
|
||||
@@ -594,6 +604,66 @@ describe("tool conversions", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("should show full diff for multi edit tool", () => {
|
||||
const toolUse = {
|
||||
type: "tool_use",
|
||||
id: "toolu_01DEF456",
|
||||
name: "mcp__acp__multi-edit",
|
||||
input: {
|
||||
file_path: "/Users/test/project/config.json",
|
||||
edits: [
|
||||
{
|
||||
old_string: 'version": 1',
|
||||
new_string: 'version": 2',
|
||||
replace_all: false,
|
||||
},
|
||||
{
|
||||
old_string: 'enabled": false',
|
||||
new_string: 'enabled": true',
|
||||
replace_all: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
let fileCache: { [key: string]: string } = {
|
||||
"/Users/test/project/config.json": JSON.stringify(
|
||||
{
|
||||
version: 1,
|
||||
filler: "filler",
|
||||
enabled: false,
|
||||
},
|
||||
null,
|
||||
4,
|
||||
),
|
||||
};
|
||||
|
||||
expect(toolInfoFromToolUse(toolUse, fileCache)).toEqual({
|
||||
content: [
|
||||
{
|
||||
newText: `{
|
||||
\"version\": 2,
|
||||
\"filler\": \"filler\",
|
||||
\"enabled\": true
|
||||
}`,
|
||||
oldText: `{
|
||||
\"version\": 1,
|
||||
\"filler\": \"filler\",
|
||||
\"enabled\": false
|
||||
}`,
|
||||
path: "/Users/test/project/config.json",
|
||||
type: "diff",
|
||||
},
|
||||
],
|
||||
kind: "edit",
|
||||
locations: [
|
||||
{
|
||||
path: "/Users/test/project/config.json",
|
||||
},
|
||||
],
|
||||
title: "Edit /Users/test/project/config.json",
|
||||
});
|
||||
});
|
||||
|
||||
it("should extract locations from multi-edit tool results", () => {
|
||||
const toolUse = {
|
||||
type: "tool_use",
|
||||
@@ -716,3 +786,17 @@ describe("tool conversions", () => {
|
||||
expect(update).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe("escape markdown", () => {
|
||||
it("should escape markdown characters", () => {
|
||||
let text = "Hello *world*!";
|
||||
let escaped = markdownEscape(text);
|
||||
expect(escaped).toEqual("```\nHello *world*!\n```");
|
||||
|
||||
text = "for example:\n```markdown\nHello *world*!\n```\n";
|
||||
escaped = markdownEscape(text);
|
||||
expect(escaped).toEqual(
|
||||
"````\nfor example:\n```markdown\nHello *world*!\n```\n````",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
178
src/tools.ts
178
src/tools.ts
@@ -1,10 +1,13 @@
|
||||
import { ContentBlock } from "@modelcontextprotocol/sdk/types.js";
|
||||
import {
|
||||
Client,
|
||||
PlanEntry,
|
||||
ToolCallContent,
|
||||
ToolCallLocation,
|
||||
ToolKind,
|
||||
} from "@zed-industries/agent-client-protocol";
|
||||
import { replaceAndCalculateLocation, SYSTEM_REMINDER } from "./mcp-server.js";
|
||||
import { tool } from "@anthropic-ai/claude-code";
|
||||
|
||||
interface ToolInfo {
|
||||
title: string;
|
||||
@@ -19,7 +22,10 @@ interface ToolUpdate {
|
||||
locations?: ToolCallLocation[];
|
||||
}
|
||||
|
||||
export function toolInfoFromToolUse(toolUse: any): ToolInfo {
|
||||
export function toolInfoFromToolUse(
|
||||
toolUse: any,
|
||||
cachedFileContent: { [key: string]: string },
|
||||
): ToolInfo {
|
||||
const name = toolUse.name;
|
||||
const input = toolUse.input;
|
||||
|
||||
@@ -45,18 +51,7 @@ export function toolInfoFromToolUse(toolUse: any): ToolInfo {
|
||||
? `Read Notebook ${input.notebook_path}`
|
||||
: "Read Notebook",
|
||||
kind: "read",
|
||||
content:
|
||||
input && input.notebook_path
|
||||
? [
|
||||
{
|
||||
type: "content",
|
||||
content: {
|
||||
type: "text",
|
||||
text: input.notebook_path.toString(),
|
||||
},
|
||||
},
|
||||
]
|
||||
: [],
|
||||
content: [],
|
||||
locations: input?.notebook_path ? [{ path: input.notebook_path }] : [],
|
||||
};
|
||||
|
||||
@@ -85,7 +80,7 @@ export function toolInfoFromToolUse(toolUse: any): ToolInfo {
|
||||
: "Terminal",
|
||||
kind: "execute",
|
||||
content:
|
||||
input && input.command
|
||||
input && input.description
|
||||
? [
|
||||
{
|
||||
type: "content",
|
||||
@@ -138,28 +133,39 @@ export function toolInfoFromToolUse(toolUse: any): ToolInfo {
|
||||
return {
|
||||
title: "Read File",
|
||||
kind: "read",
|
||||
content:
|
||||
input && input.abs_path
|
||||
? [
|
||||
{
|
||||
type: "content",
|
||||
content: { type: "text", text: input.abs_path.toString() },
|
||||
},
|
||||
]
|
||||
: [],
|
||||
content: [],
|
||||
locations: [{ path: input.abs_path, line: input.offset ?? 0 }],
|
||||
};
|
||||
|
||||
case "LS":
|
||||
return {
|
||||
title: input?.path ? input.path : "Current Directory",
|
||||
title: `List the ${input?.path ? "`" + input.path + "`" : "current"} directory's contents`,
|
||||
kind: "search",
|
||||
content: [],
|
||||
locations: input?.path ? [{ path: input.path }] : [],
|
||||
locations: [],
|
||||
};
|
||||
|
||||
case "mcp__acp__edit":
|
||||
case "Edit":
|
||||
let oldText = input.old_text || null;
|
||||
let newText = input.new_text;
|
||||
|
||||
if (input?.abs_path && input.old_text) {
|
||||
try {
|
||||
let oldContent = cachedFileContent[input.abs_path] || "";
|
||||
let newContent = replaceAndCalculateLocation(oldContent, [
|
||||
{
|
||||
oldText: input.old_text,
|
||||
newText: input.new_text,
|
||||
replaceAll: false,
|
||||
},
|
||||
]);
|
||||
oldText = oldContent;
|
||||
newText = newContent.newContent;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
return {
|
||||
title: input?.abs_path ? `Edit ${input.abs_path}` : "Edit",
|
||||
kind: "edit",
|
||||
@@ -169,8 +175,8 @@ export function toolInfoFromToolUse(toolUse: any): ToolInfo {
|
||||
{
|
||||
type: "diff",
|
||||
path: input.abs_path,
|
||||
oldText: input.old_text || null,
|
||||
newText: input.new_text,
|
||||
oldText,
|
||||
newText,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
@@ -179,20 +185,52 @@ export function toolInfoFromToolUse(toolUse: any): ToolInfo {
|
||||
|
||||
case "mcp__acp__multi-edit":
|
||||
case "MultiEdit":
|
||||
let multiInput = input as {
|
||||
file_path: string;
|
||||
edits: {
|
||||
old_string: string;
|
||||
new_string: string;
|
||||
replace_all?: boolean;
|
||||
}[];
|
||||
};
|
||||
let oldTextMulti = multiInput.edits
|
||||
.map((edit: any) => edit.old_string)
|
||||
.join("\n");
|
||||
let newTextMulti = multiInput.edits
|
||||
.map((edit: any) => edit.new_string)
|
||||
.join("\n");
|
||||
try {
|
||||
if (multiInput.edits && multiInput.file_path) {
|
||||
let oldContent =
|
||||
cachedFileContent[multiInput.file_path] ||
|
||||
multiInput.edits.map((edit: any) => edit.oldText).join("\n");
|
||||
let newContent = replaceAndCalculateLocation(
|
||||
oldContent,
|
||||
multiInput.edits.map((edit) => ({
|
||||
oldText: edit.old_string,
|
||||
newText: edit.new_string,
|
||||
replaceAll: edit.replace_all,
|
||||
})),
|
||||
);
|
||||
oldTextMulti = oldContent;
|
||||
newTextMulti = newContent.newContent;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
// Display it as a normal edit, because end users don't care about
|
||||
// the distinction between edits and multi-edits.
|
||||
return {
|
||||
title: input?.file_path ? `Edit ${input.file_path}` : "Edit",
|
||||
kind: "edit",
|
||||
content:
|
||||
input && input.edits && input.edits.length > 0
|
||||
? input.edits.map((edit: any) => ({
|
||||
type: "diff" as const,
|
||||
path: input.file_path,
|
||||
oldText: edit.old_string || null,
|
||||
newText: edit.new_string,
|
||||
}))
|
||||
: [],
|
||||
content: [
|
||||
{
|
||||
type: "diff" as const,
|
||||
path: input.file_path,
|
||||
oldText: oldTextMulti,
|
||||
newText: newTextMulti,
|
||||
},
|
||||
],
|
||||
locations: input?.file_path ? [{ path: input.file_path }] : [],
|
||||
};
|
||||
|
||||
@@ -447,11 +485,51 @@ export function toolUpdateFromToolResult(
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
case "mcp__acp__write":
|
||||
case "Write":
|
||||
return {};
|
||||
|
||||
case "Read":
|
||||
case "mcp__acp__read":
|
||||
if (Array.isArray(toolResult.content) && toolResult.content.length > 0) {
|
||||
return {
|
||||
content: toolResult.content.map((content: any) => ({
|
||||
type: "content",
|
||||
content:
|
||||
content.type == "text"
|
||||
? {
|
||||
type: "text",
|
||||
text: markdownEscape(
|
||||
content.text.replace(SYSTEM_REMINDER, ""),
|
||||
),
|
||||
}
|
||||
: content,
|
||||
})),
|
||||
};
|
||||
} else if (
|
||||
typeof toolResult.content === "string" &&
|
||||
toolResult.content.length > 0
|
||||
) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "content",
|
||||
content: {
|
||||
type: "text",
|
||||
text: markdownEscape(
|
||||
toolResult.content.replace(SYSTEM_REMINDER, ""),
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
return {};
|
||||
|
||||
case "Task":
|
||||
case "NotebookEdit":
|
||||
case "NotebookRead":
|
||||
case "mcp__acp__write":
|
||||
case "Write":
|
||||
case "TodoWrite":
|
||||
case "exit_plan_mode":
|
||||
case "Bash":
|
||||
@@ -464,21 +542,25 @@ export function toolUpdateFromToolResult(
|
||||
case "WebSearch":
|
||||
case "Other":
|
||||
default: {
|
||||
// This happens for the mcp__acp__read tool,
|
||||
// but may also for others...
|
||||
if (Array.isArray(toolResult.content)) {
|
||||
if (Array.isArray(toolResult.content) && toolResult.content.length > 0) {
|
||||
return {
|
||||
content: toolResult.content.map((content: any) => ({
|
||||
type: "content",
|
||||
content,
|
||||
})),
|
||||
};
|
||||
} else if (typeof toolResult.content === "string") {
|
||||
} else if (
|
||||
typeof toolResult.content === "string" &&
|
||||
toolResult.content.length > 0
|
||||
) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "content",
|
||||
content: { type: "text", text: toolResult.content },
|
||||
content: {
|
||||
type: "text",
|
||||
text: toolResult.content,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -501,3 +583,13 @@ export function planEntries(input: { todos: ClaudePlanEntry[] }): PlanEntry[] {
|
||||
priority: "medium",
|
||||
}));
|
||||
}
|
||||
|
||||
export function markdownEscape(text: string): string {
|
||||
let escape = "```";
|
||||
for (let [m] of text.matchAll(/^```+/gm)) {
|
||||
while (m.length >= escape.length) {
|
||||
escape += "`";
|
||||
}
|
||||
}
|
||||
return escape + "\n" + text + (text.endsWith("\n") ? "" : "\n") + escape;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user