mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-10-27 04:30:59 +00:00
Bash tool
Co-authored-by: Richard Feldman <oss@rtfeldman.com> Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
This commit is contained in:
@@ -11,6 +11,8 @@ import {
|
||||
NewSessionResponse,
|
||||
PromptRequest,
|
||||
PromptResponse,
|
||||
ToolCallContent,
|
||||
ToolKind,
|
||||
} from "@zed-industries/agent-client-protocol";
|
||||
import {
|
||||
McpServerConfig,
|
||||
@@ -32,6 +34,7 @@ import { ContentBlock } from "@zed-industries/agent-client-protocol";
|
||||
import { SessionNotification } from "@zed-industries/agent-client-protocol";
|
||||
import { createMcpServer } from "./mcp-server.js";
|
||||
import { AddressInfo } from "node:net";
|
||||
import { toolContent, toolLabel, toolKind } from "./tools.js";
|
||||
|
||||
// Implement the ACP Agent interface
|
||||
export class ClaudeAcpAgent implements Agent {
|
||||
@@ -265,11 +268,12 @@ function toAcpNotifications(
|
||||
case "tool_use":
|
||||
update = {
|
||||
toolCallId: chunk.id,
|
||||
title: chunk.name,
|
||||
title: toolLabel(chunk),
|
||||
sessionUpdate: "tool_call",
|
||||
kind: "other", // todo!()
|
||||
kind: toolKind(chunk.name), // todo!()
|
||||
rawInput: chunk.input,
|
||||
status: "pending",
|
||||
content: toolContent(chunk),
|
||||
};
|
||||
break;
|
||||
|
||||
@@ -292,68 +296,6 @@ function toAcpNotifications(
|
||||
|
||||
return output;
|
||||
}
|
||||
2025-08-27T11:10:22-06:00 WARN [agent_servers::acp] agent stderr: "content": [
|
||||
2025-08-27T11:10:22-06:00 WARN [agent_servers::acp] agent stderr: {
|
||||
2025-08-27T11:10:22-06:00 WARN [agent_servers::acp] agent stderr: "type": "tool_use",
|
||||
2025-08-27T11:10:22-06:00 WARN [agent_servers::acp] agent stderr: "id": "toolu_01BZ6ZfAoU5Vw1g4YiRa8kv3",
|
||||
2025-08-27T11:10:22-06:00 WARN [agent_servers::acp] agent stderr: "name": "Bash",
|
||||
2025-08-27T11:10:22-06:00 WARN [agent_servers::acp] agent stderr: "input": {
|
||||
2025-08-27T11:10:22-06:00 WARN [agent_servers::acp] agent stderr: "command": "rm README.md.es",
|
||||
2025-08-27T11:10:22-06:00 WARN [agent_servers::acp] agent stderr: "description": "Delete README.md.es file"
|
||||
2025-08-27T11:10:22-06:00 WARN [agent_servers::acp] agent stderr: }
|
||||
2025-08-27T11:10:22-06:00 WARN [agent_servers::acp] agent stderr: }
|
||||
2025-08-27T11:10:22-06:00 WARN [agent_servers::acp] agent stderr: ],
|
||||
|
||||
pub fn label(&self) -> String {
|
||||
match &self {
|
||||
Self::Task(Some(params)) => params.description.clone(),
|
||||
Self::Task(None) => "Task".into(),
|
||||
Self::NotebookRead(Some(params)) => {
|
||||
format!("Read Notebook {}", params.notebook_path.display())
|
||||
}
|
||||
Self::NotebookRead(None) => "Read Notebook".into(),
|
||||
Self::NotebookEdit(Some(params)) => {
|
||||
format!("Edit Notebook {}", params.notebook_path.display())
|
||||
}
|
||||
Self::NotebookEdit(None) => "Edit Notebook".into(),
|
||||
Self::Terminal(Some(params)) => format!("`{}`", params.command),
|
||||
Self::Terminal(None) => "Terminal".into(),
|
||||
Self::ReadFile(_) => "Read File".into(),
|
||||
Self::Ls(Some(params)) => {
|
||||
format!("List Directory {}", params.path.display())
|
||||
}
|
||||
Self::Ls(None) => "List Directory".into(),
|
||||
Self::Edit(Some(params)) => {
|
||||
format!("Edit {}", params.abs_path.display())
|
||||
}
|
||||
Self::Edit(None) => "Edit".into(),
|
||||
Self::MultiEdit(Some(params)) => {
|
||||
format!("Multi Edit {}", params.file_path.display())
|
||||
}
|
||||
Self::MultiEdit(None) => "Multi Edit".into(),
|
||||
Self::Write(Some(params)) => {
|
||||
format!("Write {}", params.abs_path.display())
|
||||
}
|
||||
Self::Write(None) => "Write".into(),
|
||||
Self::Glob(Some(params)) => {
|
||||
format!("Glob `{params}`")
|
||||
}
|
||||
Self::Glob(None) => "Glob".into(),
|
||||
Self::Grep(Some(params)) => format!("`{params}`"),
|
||||
Self::Grep(None) => "Grep".into(),
|
||||
Self::WebFetch(Some(params)) => format!("Fetch {}", params.url),
|
||||
Self::WebFetch(None) => "Fetch".into(),
|
||||
Self::WebSearch(Some(params)) => format!("Web Search: {}", params),
|
||||
Self::WebSearch(None) => "Web Search".into(),
|
||||
Self::TodoWrite(Some(params)) => format!(
|
||||
"Update TODOs: {}",
|
||||
params.todos.iter().map(|todo| &todo.content).join(", ")
|
||||
),
|
||||
Self::TodoWrite(None) => "Update TODOs".into(),
|
||||
Self::ExitPlanMode(_) => "Exit Plan Mode".into(),
|
||||
Self::Other { name, .. } => name.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
export function runAcp() {
|
||||
new AgentSideConnection(
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
WriteTextFileResponse,
|
||||
} from "@zed-industries/agent-client-protocol";
|
||||
import { nodeToWebWritable, nodeToWebReadable } from "../utils.js";
|
||||
import { toolContent, toolKind, toolLabel } from "../tools.js";
|
||||
|
||||
describe("ACP subprocess integration", () => {
|
||||
let child: ReturnType<typeof spawn>;
|
||||
@@ -70,3 +71,29 @@ describe("ACP subprocess integration", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("tool conversions", () => {
|
||||
it("should handle Bash nicely", () => {
|
||||
const tool_use = {
|
||||
type: "tool_use",
|
||||
id: "toolu_01VtsS2mxUFwpBJZYd7BmbC9",
|
||||
name: "Bash",
|
||||
input: {
|
||||
command: "rm README.md.rm",
|
||||
description: "Delete README.md.rm file",
|
||||
},
|
||||
};
|
||||
|
||||
expect(toolKind(tool_use.name)).toBe("execute");
|
||||
expect(toolLabel(tool_use)).toBe("rm README.md.rm");
|
||||
expect(toolContent(tool_use)).toStrictEqual([
|
||||
{
|
||||
content: {
|
||||
text: "Delete README.md.rm file",
|
||||
type: "text",
|
||||
},
|
||||
type: "content",
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
251
src/tools.ts
Normal file
251
src/tools.ts
Normal file
@@ -0,0 +1,251 @@
|
||||
import {
|
||||
ToolCallContent,
|
||||
ToolKind,
|
||||
} from "@zed-industries/agent-client-protocol";
|
||||
|
||||
export function toolLabel(toolUse: any): string {
|
||||
const name = toolUse.name;
|
||||
const input = toolUse.input;
|
||||
|
||||
switch (name) {
|
||||
case "Task":
|
||||
return input?.description ? input.description : "Task";
|
||||
case "NotebookRead":
|
||||
return input?.notebook_path
|
||||
? `Read Notebook ${input.notebook_path}`
|
||||
: "Read Notebook";
|
||||
case "NotebookEdit":
|
||||
return input?.notebook_path
|
||||
? `Edit Notebook ${input.notebook_path}`
|
||||
: "Edit Notebook";
|
||||
case "Terminal":
|
||||
case "Bash":
|
||||
return input?.command ?? "Terminal";
|
||||
case "ReadFile":
|
||||
return "Read File";
|
||||
case "LS":
|
||||
return input?.path ? `List Directory ${input.path}` : "List Directory";
|
||||
case "Edit":
|
||||
return input?.abs_path ? `Edit ${input.abs_path}` : "Edit";
|
||||
case "MultiEdit":
|
||||
return input?.file_path ? `Multi Edit ${input.file_path}` : "Multi Edit";
|
||||
case "Write":
|
||||
return input?.abs_path ? `Write ${input.abs_path}` : "Write";
|
||||
case "Glob":
|
||||
return input ? `Glob \`${input}\`` : "Glob";
|
||||
case "Grep":
|
||||
return input ? `\`${input}\`` : "Grep";
|
||||
case "WebFetch":
|
||||
return input?.url ? `Fetch ${input.url}` : "Fetch";
|
||||
case "WebSearch":
|
||||
return input ? `Web Search: ${input}` : "Web Search";
|
||||
case "TodoWrite":
|
||||
return input?.todos
|
||||
? `Update TODOs: ${input.todos.map((todo: any) => todo.content).join(", ")}`
|
||||
: "Update TODOs";
|
||||
case "exit_plan_mode":
|
||||
return "Exit Plan Mode";
|
||||
default:
|
||||
return name || "Unknown Tool";
|
||||
}
|
||||
}
|
||||
|
||||
export function toolKind(toolName: string): ToolKind {
|
||||
switch (toolName) {
|
||||
case "Task":
|
||||
return "think";
|
||||
case "NotebookRead":
|
||||
return "read";
|
||||
case "NotebookEdit":
|
||||
return "edit";
|
||||
case "Edit":
|
||||
return "edit";
|
||||
case "MultiEdit":
|
||||
return "edit";
|
||||
case "Write":
|
||||
return "edit";
|
||||
case "ReadFile":
|
||||
return "read";
|
||||
case "LS":
|
||||
return "search";
|
||||
case "Glob":
|
||||
return "search";
|
||||
case "Grep":
|
||||
return "search";
|
||||
case "Bash":
|
||||
case "Terminal":
|
||||
return "execute";
|
||||
case "WebSearch":
|
||||
return "search";
|
||||
case "WebFetch":
|
||||
return "fetch";
|
||||
case "TodoWrite":
|
||||
return "think";
|
||||
case "exit_plan_mode":
|
||||
return "think";
|
||||
default:
|
||||
return "other";
|
||||
}
|
||||
}
|
||||
|
||||
export function toolContent(toolUse: any): ToolCallContent[] {
|
||||
const input = toolUse.input;
|
||||
const name = toolUse.name;
|
||||
|
||||
switch (name) {
|
||||
case "Other":
|
||||
return [
|
||||
{
|
||||
type: "content",
|
||||
content: {
|
||||
type: "text",
|
||||
text: `\`\`\`json\n${JSON.stringify(input, null, 2) || "{}"}\`\`\``,
|
||||
},
|
||||
},
|
||||
];
|
||||
case "Task":
|
||||
if (input && input.prompt) {
|
||||
return [
|
||||
{ type: "content", content: { type: "text", text: input.prompt } },
|
||||
];
|
||||
}
|
||||
break;
|
||||
case "NotebookRead":
|
||||
if (input && input.notebook_path) {
|
||||
return [
|
||||
{
|
||||
type: "content",
|
||||
content: { type: "text", text: input.notebook_path.toString() },
|
||||
},
|
||||
];
|
||||
}
|
||||
break;
|
||||
case "NotebookEdit":
|
||||
if (input && input.new_source) {
|
||||
return [
|
||||
{
|
||||
type: "content",
|
||||
content: { type: "text", text: input.new_source },
|
||||
},
|
||||
];
|
||||
}
|
||||
break;
|
||||
case "Bash":
|
||||
if (input && input.command) {
|
||||
return [
|
||||
{
|
||||
type: "content",
|
||||
content: {
|
||||
type: "text",
|
||||
text: input.description,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
break;
|
||||
case "ReadFile":
|
||||
if (input && input.abs_path) {
|
||||
return [
|
||||
{
|
||||
type: "content",
|
||||
content: { type: "text", text: input.abs_path.toString() },
|
||||
},
|
||||
];
|
||||
}
|
||||
break;
|
||||
case "LS":
|
||||
if (input && input.path) {
|
||||
return [
|
||||
{
|
||||
type: "content",
|
||||
content: { type: "text", text: input.path.toString() },
|
||||
},
|
||||
];
|
||||
}
|
||||
break;
|
||||
case "Glob":
|
||||
if (input) {
|
||||
return [
|
||||
{
|
||||
type: "content",
|
||||
content: { type: "text", text: input.toString() },
|
||||
},
|
||||
];
|
||||
}
|
||||
break;
|
||||
case "Grep":
|
||||
if (input) {
|
||||
return [
|
||||
{ type: "content", content: { type: "text", text: `\`${input}\`` } },
|
||||
];
|
||||
}
|
||||
break;
|
||||
case "WebFetch":
|
||||
if (input && input.prompt) {
|
||||
return [
|
||||
{ type: "content", content: { type: "text", text: input.prompt } },
|
||||
];
|
||||
}
|
||||
break;
|
||||
case "WebSearch":
|
||||
if (input) {
|
||||
return [
|
||||
{
|
||||
type: "content",
|
||||
content: { type: "text", text: input.toString() },
|
||||
},
|
||||
];
|
||||
}
|
||||
break;
|
||||
case "exit_plan_mode":
|
||||
if (input && input.plan) {
|
||||
return [
|
||||
{ type: "content", content: { type: "text", text: input.plan } },
|
||||
];
|
||||
}
|
||||
break;
|
||||
case "Edit":
|
||||
if (input && input.abs_path) {
|
||||
return [
|
||||
{
|
||||
type: "diff",
|
||||
path: input.abs_path,
|
||||
oldText: input.old_text || null,
|
||||
newText: input.new_text,
|
||||
},
|
||||
];
|
||||
}
|
||||
break;
|
||||
case "Write":
|
||||
if (input && input.abs_path) {
|
||||
return [
|
||||
{
|
||||
type: "diff",
|
||||
path: input.abs_path,
|
||||
oldText: null,
|
||||
newText: input.content,
|
||||
},
|
||||
];
|
||||
}
|
||||
break;
|
||||
case "MultiEdit":
|
||||
if (input && input.edits && input.edits.length > 0) {
|
||||
// todo: show multiple edits in a multibuffer?
|
||||
const edit = input.edits[0];
|
||||
return [
|
||||
{
|
||||
type: "diff",
|
||||
path: input.file_path,
|
||||
oldText: edit.old_string || null,
|
||||
newText: edit.new_string,
|
||||
},
|
||||
];
|
||||
}
|
||||
break;
|
||||
case "TodoWrite":
|
||||
// These are mapped to plan updates later
|
||||
return [];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
Reference in New Issue
Block a user