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:
Conrad Irwin
2025-08-27 11:46:19 -06:00
parent 42b4daaf62
commit 581e9f8785
3 changed files with 284 additions and 64 deletions

View File

@@ -11,6 +11,8 @@ import {
NewSessionResponse, NewSessionResponse,
PromptRequest, PromptRequest,
PromptResponse, PromptResponse,
ToolCallContent,
ToolKind,
} from "@zed-industries/agent-client-protocol"; } from "@zed-industries/agent-client-protocol";
import { import {
McpServerConfig, McpServerConfig,
@@ -32,6 +34,7 @@ import { ContentBlock } from "@zed-industries/agent-client-protocol";
import { SessionNotification } from "@zed-industries/agent-client-protocol"; import { SessionNotification } from "@zed-industries/agent-client-protocol";
import { createMcpServer } from "./mcp-server.js"; import { createMcpServer } from "./mcp-server.js";
import { AddressInfo } from "node:net"; import { AddressInfo } from "node:net";
import { toolContent, toolLabel, toolKind } from "./tools.js";
// Implement the ACP Agent interface // Implement the ACP Agent interface
export class ClaudeAcpAgent implements Agent { export class ClaudeAcpAgent implements Agent {
@@ -265,11 +268,12 @@ function toAcpNotifications(
case "tool_use": case "tool_use":
update = { update = {
toolCallId: chunk.id, toolCallId: chunk.id,
title: chunk.name, title: toolLabel(chunk),
sessionUpdate: "tool_call", sessionUpdate: "tool_call",
kind: "other", // todo!() kind: toolKind(chunk.name), // todo!()
rawInput: chunk.input, rawInput: chunk.input,
status: "pending", status: "pending",
content: toolContent(chunk),
}; };
break; break;
@@ -292,68 +296,6 @@ function toAcpNotifications(
return output; 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() { export function runAcp() {
new AgentSideConnection( new AgentSideConnection(

View File

@@ -14,6 +14,7 @@ import {
WriteTextFileResponse, WriteTextFileResponse,
} from "@zed-industries/agent-client-protocol"; } from "@zed-industries/agent-client-protocol";
import { nodeToWebWritable, nodeToWebReadable } from "../utils.js"; import { nodeToWebWritable, nodeToWebReadable } from "../utils.js";
import { toolContent, toolKind, toolLabel } from "../tools.js";
describe("ACP subprocess integration", () => { describe("ACP subprocess integration", () => {
let child: ReturnType<typeof spawn>; 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
View 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 [];
}