Compare commits

...

6 Commits

Author SHA1 Message Date
Bennet Bo Fenner
48df285bd0 Prep v0.9.0 (#134) 2025-10-31 16:50:55 +01:00
Bennet Bo Fenner
b138c84cdb Support slash commands coming from MCP prompts (#133)
Closes #115. Thanks to @joeljohansson99 for reporting/debugging.
2025-10-31 16:32:30 +01:00
Bennet Fenner
0735aca015 Update UNSUPPORTED_COMMANDS (#132)
The SDK seems to now filter out slash commands that result in
errors/crashes.

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2025-10-30 17:17:47 +01:00
Ben Brandt
847d62dacb Revert entrypoint changes (#127) 2025-10-27 12:45:58 +00:00
Ben Brandt
becf1d4f0a Update to claude agent sdk 0.1.27 (#125)
Also simplifies the permission mode workaround so we don't have to
change the profile immediately, which could have raced with client
changes anwyay.
2025-10-27 12:34:43 +01:00
Guillaume Sabran
5491bb8dcc Allow for the package to be used as a dependency, while preserving existing behavior when run (#124)
This change will allow for this package to be used directly, without
launching a subprocess and communicating over stdio.

---------

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2025-10-27 11:10:41 +00:00
8 changed files with 121 additions and 62 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
node_modules
dist
.DS_Store

View File

@@ -1,5 +1,19 @@
# Changelog
## 0.9.0
- Support slash commands coming from MCP servers (Prompts)
## 0.8.0
- Revert changes to filename for cli entrypoint
- Provide library entrypoint via lib.ts
## 0.7.0
- Allow importing from this package as a library in addition to running it as a CLI. Allows for easier integration into existing node applications.
- Update to @anthropic-ai/claude-agent-sdk@v0.1.27
## 0.6.10
- Provide `agentInfo` on initialization response.

20
package-lock.json generated
View File

@@ -1,17 +1,17 @@
{
"name": "@zed-industries/claude-code-acp",
"version": "0.6.10",
"version": "0.9.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@zed-industries/claude-code-acp",
"version": "0.6.10",
"version": "0.9.0",
"license": "Apache-2.0",
"dependencies": {
"@agentclientprotocol/sdk": "0.5.1",
"@anthropic-ai/claude-agent-sdk": "0.1.26",
"@anthropic-ai/claude-code": "2.0.26",
"@anthropic-ai/claude-agent-sdk": "0.1.27",
"@anthropic-ai/claude-code": "2.0.27",
"@modelcontextprotocol/sdk": "1.20.2",
"diff": "8.0.2",
"express": "5.1.0",
@@ -49,9 +49,9 @@
}
},
"node_modules/@anthropic-ai/claude-agent-sdk": {
"version": "0.1.26",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.1.26.tgz",
"integrity": "sha512-daVGe5x6KOFy9DKyDojw15eljBP+tPx+spAyGll/Rp/B6t+NP0Oiwpaiib0afjbCRbqXkWEHUJPE3yDFrMcZlA==",
"version": "0.1.27",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.1.27.tgz",
"integrity": "sha512-HuMPW6spj2q8FODiP/WBCqUZAYGwDPoI1EpicP9KUXvuYk+2MZQYSaD7oiN6iNPupR2T5oJ2HY/D9OzjyCD2Mw==",
"license": "SEE LICENSE IN README.md",
"engines": {
"node": ">=18.0.0"
@@ -69,9 +69,9 @@
}
},
"node_modules/@anthropic-ai/claude-code": {
"version": "2.0.26",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code/-/claude-code-2.0.26.tgz",
"integrity": "sha512-bk/sxGdTEbI0N7g+pTwYq5CNUjdAcvIKXvf/x5oRV9q24b42uq3gijrZGycSbWmtYTIjbzFAHeWw5n7Ic43/6w==",
"version": "2.0.27",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code/-/claude-code-2.0.27.tgz",
"integrity": "sha512-Ml4tnBeJu+K7MUFLTg8q7U2BE39Vx2D+KI7t86QnFX/K90c58ekFDUUDnfAp1PqTe9vRms6vARJiWzNxrePinA==",
"license": "SEE LICENSE IN README.md",
"bin": {
"claude": "cli.js"

View File

@@ -3,12 +3,12 @@
"publishConfig": {
"access": "public"
},
"version": "0.9.0",
"description": "An ACP-compatible coding agent powered by the Claude Code SDK (TypeScript)",
"main": "dist/lib.js",
"bin": {
"claude-code-acp": "./dist/index.js"
},
"version": "0.6.10",
"description": "An ACP-compatible coding agent powered by the Claude Code SDK (TypeScript)",
"main": "dist/index.js",
"type": "module",
"files": [
"dist/",
@@ -52,8 +52,8 @@
"license": "Apache-2.0",
"dependencies": {
"@agentclientprotocol/sdk": "0.5.1",
"@anthropic-ai/claude-agent-sdk": "0.1.26",
"@anthropic-ai/claude-code": "2.0.26",
"@anthropic-ai/claude-agent-sdk": "0.1.27",
"@anthropic-ai/claude-code": "2.0.27",
"@modelcontextprotocol/sdk": "1.20.2",
"diff": "8.0.2",
"express": "5.1.0",

View File

@@ -210,9 +210,10 @@ export class ClaudeAcpAgent implements Agent {
mcpServers,
systemPrompt,
settingSources: ["user", "project", "local"],
// If we want bypassPermissions to be an option, we have to start the session with it on.
// We change it immediately back to default below before returning the session.
permissionMode: IS_ROOT ? permissionMode : "bypassPermissions",
// If we want bypassPermissions to be an option, we have to allow it here.
// But it doesn't work in root mode, so we only activate it if it will work.
allowDangerouslySkipPermissions: !IS_ROOT,
permissionMode,
permissionPromptToolName: PERMISSION_TOOL_NAME,
stderr: (err) => console.error(err),
// note: although not documented by the types, passing an absolute path
@@ -258,10 +259,6 @@ export class ClaudeAcpAgent implements Agent {
const availableCommands = await getAvailableSlashCommands(q);
const models = await getAvailableModels(q);
// Change it back to default
if (!IS_ROOT) {
await q.setPermissionMode(permissionMode);
}
// Needs to happen after we return the session
setTimeout(() => {
@@ -518,54 +515,30 @@ async function getAvailableModels(query: Query): Promise<SessionModelState> {
async function getAvailableSlashCommands(query: Query): Promise<AvailableCommand[]> {
const UNSUPPORTED_COMMANDS = [
"add-dir",
"agents", // Modal
"bashes", // Modal
"bug", // Modal
"clear", // Escape Codes
"config", // Modal
"context", // Escape Codes
"cost", // Escape Codes
"doctor", // Escape Codes
"exit",
"export", // Modal
"help", // Modal
"hooks", // Modal
"ide", // Modal
"install-github-app", // Modal
"context",
"cost",
"login",
"logout",
"memory",
"mcp",
"migrate-installer", // Modal
"model", // Not supported via SDK?
"output-style", // Modal
"output-style:new", // Modal
"permissions", // Modal
"privacy-settings",
"release-notes", // Escape Codes
"resume",
"status", // Not supported via SDK?
"statusline", // Not needed
"terminal-setup", // Not needed
"todos", // Escape Codes
"vim", // Not needed
"output-style:new",
"release-notes",
"todos",
];
const commands = await query.supportedCommands();
return commands
.map((command) => {
const input = command.argumentHint ? { hint: command.argumentHint } : null;
let name = command.name;
if (command.name.endsWith(" (MCP)")) {
name = `mcp:${name.replace(" (MCP)", "")}`;
}
return {
name: command.name,
name,
description: command.description || "",
input,
};
})
.filter(
(command: AvailableCommand) =>
!(command.name.match(/\(MCP\)/) || UNSUPPORTED_COMMANDS.includes(command.name)),
);
.filter((command: AvailableCommand) => !UNSUPPORTED_COMMANDS.includes(command.name));
}
function formatUriAsLink(uri: string): string {
@@ -585,15 +558,23 @@ function formatUriAsLink(uri: string): string {
}
}
function promptToClaude(prompt: PromptRequest): SDKUserMessage {
export function promptToClaude(prompt: PromptRequest): SDKUserMessage {
const content: any[] = [];
const context: any[] = [];
for (const chunk of prompt.prompt) {
switch (chunk.type) {
case "text":
content.push({ type: "text", text: chunk.text });
case "text": {
let text = chunk.text;
// change /mcp:server:command args -> /server:command (MCP) args
const mcpMatch = text.match(/^\/mcp:([^:\s]+):(\S+)(\s+.*)?$/);
if (mcpMatch) {
const [, server, command, args] = mcpMatch;
text = `/${server}:${command} (MCP)${args || ""}`;
}
content.push({ type: "text", text });
break;
}
case "resource_link": {
const formattedUri = formatUriAsLink(chunk.uri);
content.push({

View File

@@ -19,7 +19,7 @@ process.on("unhandledRejection", (reason, promise) => {
console.error("Unhandled Rejection at:", promise, "reason:", reason);
});
import { runAcp as runAcp } from "./acp-agent.js";
import { runAcp } from "./acp-agent.js";
runAcp();
// Keep process alive

25
src/lib.ts Normal file
View File

@@ -0,0 +1,25 @@
// Export the main agent class and utilities for library usage
export {
ClaudeAcpAgent,
runAcp,
toAcpNotifications,
streamEventToAcpNotifications,
} from "./acp-agent.js";
export {
loadManagedSettings,
applyEnvironmentSettings,
nodeToWebReadable,
nodeToWebWritable,
Pushable,
unreachable,
} from "./utils.js";
export {
createMcpServer,
createPermissionMcpServer,
PERMISSION_TOOL_NAME,
toolNames,
} from "./mcp-server.js";
export { toolInfoFromToolUse, planEntries, toolUpdateFromToolResult } from "./tools.js";
// Export types
export type { ClaudePlanEntry } from "./tools.js";

View File

@@ -17,7 +17,7 @@ import {
} from "@agentclientprotocol/sdk";
import { nodeToWebWritable, nodeToWebReadable } from "../utils.js";
import { markdownEscape, toolInfoFromToolUse, toolUpdateFromToolResult } from "../tools.js";
import { toAcpNotifications } from "../acp-agent.js";
import { toAcpNotifications, promptToClaude } from "../acp-agent.js";
import { query, SDKAssistantMessage } from "@anthropic-ai/claude-agent-sdk";
describe.skipIf(!process.env.RUN_INTEGRATION_TESTS)("ACP subprocess integration", () => {
@@ -783,6 +783,44 @@ describe("escape markdown", () => {
});
});
describe("prompt conversion", () => {
it("should not change built-in slash commands", () => {
const message = promptToClaude({
sessionId: "test",
prompt: [
{
type: "text",
text: "/compact args",
},
],
});
expect(message.message.content).toEqual([
{
text: "/compact args",
type: "text",
},
]);
});
it("should remove MCP prefix from MCP slash commands", () => {
const message = promptToClaude({
sessionId: "test",
prompt: [
{
type: "text",
text: "/mcp:server:name args",
},
],
});
expect(message.message.content).toEqual([
{
text: "/server:name (MCP) args",
type: "text",
},
]);
});
});
describe("SDK behavior", () => {
it.skipIf(!process.env.RUN_INTEGRATION_TESTS)(
"query has a 'default' model",