mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-10-27 12:40:32 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de514862f8 | ||
|
|
9ebdfffd48 | ||
|
|
e6bf33cd30 | ||
|
|
96261a5b50 | ||
|
|
003ae41d18 | ||
|
|
0ba0ec1017 | ||
|
|
f70730f1ab | ||
|
|
13cd9214d7 | ||
|
|
78975d75f5 |
@@ -32,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
|
||||
@@ -39,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
12
package-lock.json
generated
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"name": "@zed-industries/claude-code-acp",
|
||||
"version": "0.2.3",
|
||||
"version": "0.2.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@zed-industries/claude-code-acp",
|
||||
"version": "0.2.3",
|
||||
"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"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"bin": {
|
||||
"claude-code-acp": "./dist/index.js"
|
||||
},
|
||||
"version": "0.2.3",
|
||||
"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",
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -171,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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -277,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
|
||||
@@ -313,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(10000).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 }) => {
|
||||
|
||||
@@ -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]" },
|
||||
|
||||
Reference in New Issue
Block a user