@zed-industries/claude-code-acp
Advanced tools
+10
-5
@@ -11,2 +11,3 @@ import { AgentSideConnection, ndJsonStream, RequestError, } from "@agentclientprotocol/sdk"; | ||
| import { randomUUID } from "node:crypto"; | ||
| export const CLAUDE_CONFIG_DIR = process.env.CLAUDE ?? path.join(os.homedir(), ".claude"); | ||
| // Bypass Permissions doesn't work if we are a root/sudo user | ||
@@ -68,3 +69,5 @@ const IS_ROOT = (process.geteuid?.() ?? process.getuid?.()) === 0; | ||
| } | ||
| const sessionId = randomUUID(); | ||
| // Extract options from _meta if provided | ||
| const userProvidedOptions = params._meta?.claudeCode?.options; | ||
| const sessionId = userProvidedOptions?.resume || randomUUID(); | ||
| const input = new Pushable(); | ||
@@ -117,4 +120,7 @@ const mcpServers = {}; | ||
| const permissionMode = "default"; | ||
| // Extract options from _meta if provided | ||
| const userProvidedOptions = params._meta?.claudeCode?.options; | ||
| const extraArgs = { ...userProvidedOptions?.extraArgs }; | ||
| if (userProvidedOptions?.resume === undefined) { | ||
| // Set our own session id if not resuming an existing session. | ||
| extraArgs["session-id"] = sessionId; | ||
| } | ||
| const options = { | ||
@@ -129,4 +135,3 @@ systemPrompt, | ||
| mcpServers: { ...(userProvidedOptions?.mcpServers || {}), ...mcpServers }, | ||
| // Set our own session id | ||
| extraArgs: { ...userProvidedOptions?.extraArgs, "session-id": sessionId }, | ||
| extraArgs, | ||
| // If we want bypassPermissions to be an option, we have to allow it here. | ||
@@ -133,0 +138,0 @@ // But it doesn't work in root mode, so we only activate it if it will work. |
+65
-31
@@ -55,3 +55,6 @@ var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) { | ||
| import { z } from "zod"; | ||
| import { CLAUDE_CONFIG_DIR } from "./acp-agent.js"; | ||
| import * as diff from "diff"; | ||
| import * as path from "node:path"; | ||
| import * as fs from "node:fs/promises"; | ||
| import { sleep, unreachable, extractLinesWithByteLimit } from "./utils.js"; | ||
@@ -83,2 +86,50 @@ export const SYSTEM_REMINDER = ` | ||
| export function createMcpServer(agent, sessionId, clientCapabilities) { | ||
| /** | ||
| * This checks if a given path is related to internal agent persistence and if the agent should be allowed to read/write from here. | ||
| * We let the agent do normal fs operations on these paths so that it can persist its state. | ||
| * However, we block access to settings files for security reasons. | ||
| */ | ||
| function internalPath(file_path) { | ||
| return (file_path.startsWith(CLAUDE_CONFIG_DIR) && | ||
| !file_path.startsWith(path.join(CLAUDE_CONFIG_DIR, "settings.json")) && | ||
| !file_path.startsWith(path.join(CLAUDE_CONFIG_DIR, "session-env"))); | ||
| } | ||
| async function readTextFile(input) { | ||
| if (internalPath(input.file_path)) { | ||
| const content = await fs.readFile(input.file_path, "utf8"); | ||
| // eslint-disable-next-line eqeqeq | ||
| if (input.offset != null || input.limit != null) { | ||
| const lines = content.split("\n"); | ||
| // Apply offset and limit if provided | ||
| const offset = input.offset ?? 1; | ||
| const limit = input.limit ?? lines.length; | ||
| // Extract the requested lines (offset is 1-based) | ||
| const startIndex = Math.max(0, offset - 1); | ||
| const endIndex = Math.min(lines.length, startIndex + limit); | ||
| const selectedLines = lines.slice(startIndex, endIndex); | ||
| return { content: selectedLines.join("\n") }; | ||
| } | ||
| else { | ||
| return { content }; | ||
| } | ||
| } | ||
| return agent.readTextFile({ | ||
| sessionId, | ||
| path: input.file_path, | ||
| line: input.offset, | ||
| limit: input.limit, | ||
| }); | ||
| } | ||
| async function writeTextFile(input) { | ||
| if (internalPath(input.file_path)) { | ||
| await fs.writeFile(input.file_path, input.content, "utf8"); | ||
| } | ||
| else { | ||
| await agent.writeTextFile({ | ||
| sessionId, | ||
| path: input.file_path, | ||
| content: input.content, | ||
| }); | ||
| } | ||
| } | ||
| // Create MCP server | ||
@@ -136,8 +187,3 @@ const server = new McpServer({ name: "acp", version: "1.0.0" }, { capabilities: { tools: {} } }); | ||
| } | ||
| const readResponse = await agent.readTextFile({ | ||
| sessionId, | ||
| path: input.file_path, | ||
| line: input.offset, | ||
| limit: input.limit, | ||
| }); | ||
| const readResponse = await readTextFile(input); | ||
| if (typeof readResponse?.content !== "string") { | ||
@@ -150,3 +196,3 @@ throw new Error(`No file contents for ${input.file_path}.`); | ||
| let readInfo = ""; | ||
| if (input.offset > 1 || result.wasLimited) { | ||
| if ((input.offset && input.offset > 1) || result.wasLimited) { | ||
| readInfo = "\n\n<file-read-info>"; | ||
@@ -225,7 +271,3 @@ if (result.wasLimited) { | ||
| } | ||
| await agent.writeTextFile({ | ||
| sessionId, | ||
| path: input.file_path, | ||
| content: input.content, | ||
| }); | ||
| await writeTextFile(input); | ||
| return { | ||
@@ -270,3 +312,3 @@ content: [], | ||
| .optional() | ||
| .describe("Replace all occurences of old_string (default false)"), | ||
| .describe("Replace all occurrences of old_string (default false)"), | ||
| }, | ||
@@ -293,5 +335,4 @@ annotations: { | ||
| } | ||
| const readResponse = await agent.readTextFile({ | ||
| sessionId, | ||
| path: input.file_path, | ||
| const readResponse = await readTextFile({ | ||
| file_path: input.file_path, | ||
| }); | ||
@@ -309,7 +350,3 @@ if (typeof readResponse?.content !== "string") { | ||
| const patch = diff.createPatch(input.file_path, readResponse.content, newContent); | ||
| await agent.writeTextFile({ | ||
| sessionId, | ||
| path: input.file_path, | ||
| content: newContent, | ||
| }); | ||
| await writeTextFile({ file_path: input.file_path, content: newContent }); | ||
| return { | ||
@@ -344,6 +381,3 @@ content: [ | ||
| command: z.string().describe("The command to execute"), | ||
| timeout: z | ||
| .number() | ||
| .default(2 * 60 * 1000) | ||
| .describe(`Optional timeout in milliseconds (max ${2 * 60 * 1000})`), | ||
| timeout: z.number().describe(`Optional timeout in milliseconds (max ${2 * 60 * 1000})`), | ||
| description: z.string().optional() | ||
@@ -417,3 +451,3 @@ .describe(`Clear, concise description of what this command does in 5-10 words, in active voice. Examples: | ||
| abortPromise.then(() => ({ status: "aborted", exitStatus: null })), | ||
| sleep(input.timeout).then(async () => { | ||
| sleep(input.timeout ?? 2 * 60 * 1000).then(async () => { | ||
| if (agent.backgroundTerminals[handle.id]?.status === "started") { | ||
@@ -481,3 +515,3 @@ await handle.kill(); | ||
| description: `- Retrieves output from a running or completed background bash shell | ||
| - Takes a shell_id parameter identifying the shell | ||
| - Takes a bash_id parameter identifying the shell | ||
| - Always returns only new output since the last check | ||
@@ -487,5 +521,5 @@ - Returns stdout and stderr output along with shell status | ||
| In sessions with ${toolNames.bashOutput} always use it instead of BashOutput.`, | ||
| In sessions with ${toolNames.bashOutput} always use it for output from Bash commands instead of TaskOutput.`, | ||
| inputSchema: { | ||
| shell_id: z | ||
| bash_id: z | ||
| .string() | ||
@@ -495,5 +529,5 @@ .describe(`The id of the background bash command as returned by \`${toolNames.bash}\``), | ||
| }, async (input) => { | ||
| const bgTerm = agent.backgroundTerminals[input.shell_id]; | ||
| const bgTerm = agent.backgroundTerminals[input.bash_id]; | ||
| if (!bgTerm) { | ||
| throw new Error(`Unknown shell ${input.shell_id}`); | ||
| throw new Error(`Unknown shell ${input.bash_id}`); | ||
| } | ||
@@ -500,0 +534,0 @@ if (bgTerm.status === "started") { |
+6
-6
@@ -6,3 +6,3 @@ { | ||
| }, | ||
| "version": "0.12.2", | ||
| "version": "0.12.3", | ||
| "description": "An ACP-compatible coding agent powered by the Claude Code SDK (TypeScript)", | ||
@@ -55,4 +55,4 @@ "main": "dist/lib.js", | ||
| "dependencies": { | ||
| "@agentclientprotocol/sdk": "0.8.0", | ||
| "@anthropic-ai/claude-agent-sdk": "0.1.61", | ||
| "@agentclientprotocol/sdk": "0.9.0", | ||
| "@anthropic-ai/claude-agent-sdk": "0.1.65", | ||
| "@modelcontextprotocol/sdk": "1.24.3", | ||
@@ -63,5 +63,5 @@ "diff": "8.0.2" | ||
| "@anthropic-ai/sdk": "0.71.2", | ||
| "@types/node": "24.10.1", | ||
| "@typescript-eslint/eslint-plugin": "8.48.1", | ||
| "@typescript-eslint/parser": "8.48.1", | ||
| "@types/node": "25.0.0", | ||
| "@typescript-eslint/eslint-plugin": "8.49.0", | ||
| "@typescript-eslint/parser": "8.49.0", | ||
| "eslint": "9.39.1", | ||
@@ -68,0 +68,0 @@ "eslint-config-prettier": "10.1.8", |
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 4 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 3 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
154523
1.3%3385
1.17%17
6.25%+ Added
+ Added
- Removed
- Removed