Compare commits

...

19 Commits

Author SHA1 Message Date
Agus Zubiaga
de514862f8 0.2.6 2025-09-04 18:01:30 -03:00
Ben Brandt
9ebdfffd48 Remove option for using an API Key, just support /login
Co-authored-by: Agus Zubiaga <agus@zed.dev>
2025-09-04 08:41:45 -07:00
Agus Zubiaga
e6bf33cd30 0.2.5 2025-09-03 22:02:40 -03:00
Xiangyi Li
96261a5b50 Remove dollar signs from CLI commands in installation section (#22)
Makes it easier to copy and paste the installation commands without
having to manually remove the dollar signs. This is a common convention
in documentation to improve user experience when following installation
instructions.
2025-09-03 22:01:24 -03:00
Agus Zubiaga
003ae41d18 Send available commands over notifications (#20)
See https://github.com/zed-industries/agent-client-protocol/pull/62

---------

Co-authored-by: Cole Miller <cole@zed.dev>
2025-09-03 22:00:54 -03:00
Danilo Leal
0ba0ec1017 Update Claude Code auth button label (#16)
We were using "Claude CLI" which is not the correct product name.
2025-09-03 16:37:50 -04:00
Oli
f70730f1ab Update README for CodeCompanion.nvim (#17)
Added CodeCompanion.nvim to the README, linking to my own setup
instructions.
2025-09-03 12:53:40 -07:00
Agus Zubiaga
13cd9214d7 0.2.4 2025-09-03 15:15:13 -03:00
Agus Zubiaga
78975d75f5 Provide more helpful message on init error 2025-09-03 15:15:03 -03:00
Agus Zubiaga
bbd0774ae5 0.2.3 2025-09-03 15:07:22 -03:00
Agus Zubiaga
76331017b2 Pass node executable path 2025-09-03 15:07:13 -03:00
Agus Zubiaga
90f1447e99 0.2.2 2025-09-03 14:04:10 -03:00
Agus Zubiaga
dae4c9956e Raise timeout 2025-09-03 14:03:47 -03:00
Agus Zubiaga
eb282abb50 Fix readme format 2025-09-03 11:39:33 -03:00
Agus Zubiaga
74c0be057b Add npm badge 2025-09-03 11:36:39 -03:00
Agus Zubiaga
e66091f723 Add permission request note 2025-09-03 11:02:33 -03:00
Agus Zubiaga
1b99ab96a0 Fix test 2025-09-03 09:28:11 -03:00
Agus Zubiaga
8d14d9dd1d 0.2.1 2025-09-03 09:25:44 -03:00
Agus Zubiaga
98b6cd5c07 Fix edit location reporting (#15)
When reporting the edit tool uses, we were accessing a field with a typo
(we should start using Zod, btw), and that caused locations not to be
reported correctly. We also now report all line numbers.

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2025-09-03 09:25:15 -03:00
7 changed files with 89 additions and 49 deletions

View File

@@ -1,12 +1,14 @@
# ACP adapter for Claude Code
[![npm](https://img.shields.io/npm/v/%40zed-industries%2Fclaude-code-acp)](https://www.npmjs.com/package/@zed-industries/claude-code-acp)
Use [Claude Code](https://www.anthropic.com/claude-code) from [ACP-compatible](https://agentclientprotocol.com) clients such as [Zed](https://zed.dev)!
This tool implements an ACP agent by using the official [Claude Code SDK](https://docs.anthropic.com/en/docs/claude-code/sdk/sdk-overview), supporting:
- Context @-mentions
- Images
- Tool calls
- Tool calls (with permission requests)
- Following
- Edit review
- TODO lists
@@ -30,6 +32,8 @@ Read the docs on [External Agent](https://zed.dev/docs/ai/external-agents) suppo
### Other clients
- Neovim via [CodeCompanion.nvim](https://codecompanion.olimorris.dev/configuration/adapters#setup-claude-code-via-acp)
Setup instructions for other clients are coming soon. Feel free to [submit a PR](https://github.com/zed-industries/claude-code-acp/pulls) to add yours!
#### Installation
@@ -37,13 +41,13 @@ Setup instructions for other clients are coming soon. Feel free to [submit a PR]
Install the adapter from `npm`:
```bash
$ npm install @zed-industries/claude-code-acp
npm install @zed-industries/claude-code-acp
```
You can then use `claude-code-acp` as a regular ACP agent:
```
$ ANTHROPIC_API_KEY=sk-... claude-code-acp
ANTHROPIC_API_KEY=sk-... claude-code-acp
```
## License

12
package-lock.json generated
View File

@@ -1,18 +1,18 @@
{
"name": "@zed-industries/claude-code-acp",
"version": "0.2.0",
"version": "0.2.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@zed-industries/claude-code-acp",
"version": "0.2.0",
"version": "0.2.6",
"license": "Apache-2.0",
"dependencies": {
"@anthropic-ai/claude-code": "^1.0.100",
"@modelcontextprotocol/sdk": "^1.17.4",
"@types/express": "^5.0.3",
"@zed-industries/agent-client-protocol": "0.2.0-alpha.4",
"@zed-industries/agent-client-protocol": "0.2.0-alpha.6",
"diff": "^8.0.2",
"express": "^5.1.0",
"minimist": "^1.2.8",
@@ -1797,9 +1797,9 @@
}
},
"node_modules/@zed-industries/agent-client-protocol": {
"version": "0.2.0-alpha.4",
"resolved": "https://registry.npmjs.org/@zed-industries/agent-client-protocol/-/agent-client-protocol-0.2.0-alpha.4.tgz",
"integrity": "sha512-l/MdK0MrvEb55/U3wAtAN/MC04YK+N6vNyakNgLsK2JTcmG54n45gXa0OZ08lYUmEvLe0shdu1dDtzlOV/Luxw==",
"version": "0.2.0-alpha.6",
"resolved": "https://registry.npmjs.org/@zed-industries/agent-client-protocol/-/agent-client-protocol-0.2.0-alpha.6.tgz",
"integrity": "sha512-Mepcrp9ztLKRSkUuKJ7gnxIL1NRoGbSlY+BWr6WyhJ/sqcocaPSiINs0rt24RZGiU8E+TUwy5FNFW5T9tbIR1A==",
"license": "Apache-2.0",
"dependencies": {
"zod": "^3.0.0"

View File

@@ -6,7 +6,7 @@
"bin": {
"claude-code-acp": "./dist/index.js"
},
"version": "0.2.0",
"version": "0.2.6",
"description": "An ACP-compatible coding agent powered by the Claude Code SDK (TypeScript)",
"main": "dist/index.js",
"type": "module",
@@ -46,7 +46,7 @@
"@anthropic-ai/claude-code": "^1.0.100",
"@modelcontextprotocol/sdk": "^1.17.4",
"@types/express": "^5.0.3",
"@zed-industries/agent-client-protocol": "0.2.0-alpha.4",
"@zed-industries/agent-client-protocol": "0.2.0-alpha.6",
"diff": "^8.0.2",
"express": "^5.1.0",
"minimist": "^1.2.8",

View File

@@ -31,7 +31,7 @@ import * as fs from "node:fs";
import * as path from "node:path";
import * as os from "node:os";
import { v7 as uuidv7 } from "uuid";
import { nodeToWebReadable, nodeToWebWritable, Pushable, sleep, unreachable } from "./utils.js";
import { nodeToWebReadable, nodeToWebWritable, Pushable, unreachable } from "./utils.js";
import { SessionNotification } from "@zed-industries/agent-client-protocol";
import { createMcpServer } from "./mcp-server.js";
import { AddressInfo } from "node:net";
@@ -90,18 +90,20 @@ export class ClaudeAcpAgent implements Agent {
authMethods: [
{
description: "Run `claude /login` in the terminal",
name: "Login with Claude CLI",
name: "Log in with Claude Code",
id: "claude-login",
},
{
description: "Anthropic API KEY",
name: "Use Anthropic API Key",
id: "anthropic-api-key",
},
],
};
}
async newSession(params: NewSessionRequest): Promise<NewSessionResponse> {
if (
fs.existsSync(path.resolve(os.homedir(), ".claude.json.backup")) &&
!fs.existsSync(path.resolve(os.homedir(), ".claude.json"))
) {
throw RequestError.authRequired();
}
const sessionId = uuidv7();
const input = new Pushable<SDKUserMessage>();
@@ -134,6 +136,9 @@ export class ClaudeAcpAgent implements Agent {
mcpServers,
permissionPromptToolName: "mcp__acp__permission",
stderr: (err) => console.error(err),
// note: although not documented by the types, passing an absolute path
// here works to find zed's managed node version.
executable: process.execPath as any,
};
const allowedTools = [];
@@ -168,10 +173,18 @@ export class ClaudeAcpAgent implements Agent {
cancelled: false,
};
const availableCommands = await availableSlashCommands(q);
getAvailableSlashCommands(q).then((availableCommands) => {
this.client.sessionUpdate({
sessionId,
update: {
sessionUpdate: "available_commands_update",
availableCommands,
},
});
});
return {
sessionId,
availableCommands,
};
}
@@ -274,7 +287,7 @@ export class ClaudeAcpAgent implements Agent {
}
}
async function availableSlashCommands(query: Query): Promise<AvailableCommand[]> {
async function getAvailableSlashCommands(query: Query): Promise<AvailableCommand[]> {
const UNSUPPORTED_COMMANDS = [
"add-dir",
"agents", // Modal
@@ -310,20 +323,8 @@ async function availableSlashCommands(query: Query): Promise<AvailableCommand[]>
"todos", // Escape Codes
"vim", // Not needed
];
const commands = await Promise.race([
//todo: Do not use `as any` once `supportedCommands` is exposed via the typescript interface
(query as any).supportedCommands(),
sleep(5000).then(() => {
if (
fs.existsSync(path.resolve(os.homedir(), ".claude.json.backup")) &&
!fs.existsSync(path.resolve(os.homedir(), ".claude.json"))
) {
throw RequestError.authRequired();
}
throw new Error("can't load supported slash commands");
}),
]);
//todo: Do not use `as any` once `supportedCommands` is exposed via the typescript interface
const commands = await (query as any).supportedCommands();
return commands
.map((command: { name: string; description: string; argumentHint: string }) => {

View File

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

View File

@@ -2,6 +2,7 @@ import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { spawn, spawnSync } from "child_process";
import {
Agent,
AvailableCommand,
Client,
ClientSideConnection,
NewSessionResponse,
@@ -47,9 +48,15 @@ describe.skipIf(!process.env.RUN_INTEGRATION_TESTS)("ACP subprocess integration"
agent: Agent;
files: Map<string, string> = new Map();
receivedText: string = "";
resolveAvailableCommands: (commands: AvailableCommand[]) => void;
availableCommandsPromise: Promise<AvailableCommand[]>;
constructor(agent: Agent) {
this.agent = agent;
this.resolveAvailableCommands = () => {};
this.availableCommandsPromise = new Promise((resolve) => {
this.resolveAvailableCommands = resolve;
});
}
takeReceivedText() {
@@ -67,11 +74,18 @@ describe.skipIf(!process.env.RUN_INTEGRATION_TESTS)("ACP subprocess integration"
async sessionUpdate(params: SessionNotification): Promise<void> {
console.error("RECEIVED", JSON.stringify(params, null, 4));
if (
params.update.sessionUpdate === "agent_message_chunk" &&
params.update.content.type === "text"
) {
this.receivedText += params.update.content.text;
switch (params.update.sessionUpdate) {
case "agent_message_chunk": {
if (params.update.content.type === "text") {
this.receivedText += params.update.content.text;
}
break;
}
case "available_commands_update":
this.resolveAvailableCommands(params.update.availableCommands);
break;
default:
break;
}
}
@@ -140,12 +154,14 @@ describe.skipIf(!process.env.RUN_INTEGRATION_TESTS)("ACP subprocess integration"
it("should include available commands", async () => {
const { client, connection, newSessionResponse } = await setupTestSession(__dirname);
expect(newSessionResponse.availableCommands).toContainEqual({
const commands = await client.availableCommandsPromise;
expect(commands).toContainEqual({
name: "quick-math",
description: "10 * 3 = 30 (project)",
input: null,
});
expect(newSessionResponse.availableCommands).toContainEqual({
expect(commands).toContainEqual({
name: "say-hello",
description: "Say hello (project)",
input: { hint: "[name]" },
@@ -692,6 +708,11 @@ describe("tool conversions", () => {
locations: [
{
path: "/Users/test/project/config.json",
line: 1,
},
{
path: "/Users/test/project/config.json",
line: 3,
},
],
title: "Edit /Users/test/project/config.json",

View File

@@ -140,21 +140,21 @@ export function toolInfoFromToolUse(
const path = input?.abs_path ?? input?.file_path;
let oldText = input.old_string ?? null;
let newText = input.new_string ?? "";
let line = null;
let affectedLines: number[] = [];
if (path && input.old_text) {
if (path && oldText) {
try {
const oldContent = cachedFileContent[path] || "";
const newContent = replaceAndCalculateLocation(oldContent, [
{
oldText: input.old_string,
newText: input.new_string,
oldText,
newText,
replaceAll: false,
},
]);
oldText = oldContent;
newText = newContent.newContent;
line = newContent.lineNumbers[0];
affectedLines = newContent.lineNumbers;
} catch (e) {
console.error(e);
}
@@ -173,7 +173,11 @@ export function toolInfoFromToolUse(
},
]
: [],
locations: path ? [{ path, line }] : [],
locations: path
? affectedLines.length > 0
? affectedLines.map((line) => ({ line, path }))
: [{ path }]
: [],
};
}
@@ -189,6 +193,7 @@ export function toolInfoFromToolUse(
};
let oldTextMulti = multiInput.edits.map((edit: any) => edit.old_string).join("\n");
let newTextMulti = multiInput.edits.map((edit: any) => edit.new_string).join("\n");
let affectedLines: number[] = [];
try {
if (multiInput.edits && multiInput.file_path) {
const oldContent =
@@ -204,6 +209,7 @@ export function toolInfoFromToolUse(
);
oldTextMulti = oldContent;
newTextMulti = newContent.newContent;
affectedLines = newContent.lineNumbers;
}
} catch (e) {
console.error(e);
@@ -221,7 +227,11 @@ export function toolInfoFromToolUse(
newText: newTextMulti,
},
],
locations: input?.file_path ? [{ path: input.file_path }] : [],
locations: input?.file_path
? affectedLines.length > 0
? affectedLines.map((line) => ({ line, path: input.file_path }))
: [{ path: input.file_path }]
: [],
};
}
case "mcp__acp__write": {