claude-code-acp 0.1.0__py3-none-any.whl → 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- claude_code_acp/__init__.py +14 -4
- claude_code_acp/acp_client.py +374 -0
- claude_code_acp/client.py +317 -0
- claude_code_acp-0.3.0.dist-info/METADATA +418 -0
- claude_code_acp-0.3.0.dist-info/RECORD +10 -0
- claude_code_acp-0.1.0.dist-info/METADATA +0 -111
- claude_code_acp-0.1.0.dist-info/RECORD +0 -8
- {claude_code_acp-0.1.0.dist-info → claude_code_acp-0.3.0.dist-info}/WHEEL +0 -0
- {claude_code_acp-0.1.0.dist-info → claude_code_acp-0.3.0.dist-info}/entry_points.txt +0 -0
- {claude_code_acp-0.1.0.dist-info → claude_code_acp-0.3.0.dist-info}/licenses/LICENSE +0 -0
claude_code_acp/__init__.py
CHANGED
|
@@ -8,10 +8,20 @@ allowing Claude Code to work with any ACP-compatible client like Zed, Neovim, et
|
|
|
8
8
|
import asyncio
|
|
9
9
|
|
|
10
10
|
from .agent import ClaudeAcpAgent
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
from .client import ClaudeClient, ClaudeEvents
|
|
12
|
+
from .acp_client import AcpClient, AcpClientEvents
|
|
13
|
+
|
|
14
|
+
__version__ = "0.2.0"
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"ClaudeAcpAgent",
|
|
18
|
+
"ClaudeClient",
|
|
19
|
+
"ClaudeEvents",
|
|
20
|
+
"AcpClient",
|
|
21
|
+
"AcpClientEvents",
|
|
22
|
+
"main",
|
|
23
|
+
"run",
|
|
24
|
+
]
|
|
15
25
|
|
|
16
26
|
|
|
17
27
|
async def run() -> None:
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ACP Client - Connect to any ACP-compatible agent.
|
|
3
|
+
|
|
4
|
+
This module provides a client that can connect to any ACP agent
|
|
5
|
+
(like claude-code-acp, or Zed's TypeScript version) via subprocess.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import logging
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Any, Callable, Coroutine
|
|
14
|
+
|
|
15
|
+
from acp.client.connection import ClientSideConnection
|
|
16
|
+
from acp.schema import (
|
|
17
|
+
AgentMessageChunk,
|
|
18
|
+
AgentThoughtChunk,
|
|
19
|
+
Implementation,
|
|
20
|
+
PermissionOption,
|
|
21
|
+
RequestPermissionResponse,
|
|
22
|
+
TextContentBlock,
|
|
23
|
+
ToolCallProgress,
|
|
24
|
+
ToolCallStart,
|
|
25
|
+
ToolCallUpdate,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
__all__ = ["AcpClient", "AcpClientEvents"]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class AcpClientEvents:
|
|
35
|
+
"""Event handlers for ACP client."""
|
|
36
|
+
|
|
37
|
+
on_text: Callable[[str], Coroutine[Any, Any, None]] | None = None
|
|
38
|
+
on_thinking: Callable[[str], Coroutine[Any, Any, None]] | None = None
|
|
39
|
+
on_tool_start: Callable[[str, str, dict], Coroutine[Any, Any, None]] | None = None
|
|
40
|
+
on_tool_end: Callable[[str, str, Any], Coroutine[Any, Any, None]] | None = None
|
|
41
|
+
on_permission: Callable[[str, dict, list], Coroutine[Any, Any, str]] | None = None
|
|
42
|
+
on_error: Callable[[Exception], Coroutine[Any, Any, None]] | None = None
|
|
43
|
+
on_complete: Callable[[], Coroutine[Any, Any, None]] | None = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class AcpClient:
|
|
47
|
+
"""
|
|
48
|
+
ACP Client that connects to any ACP-compatible agent.
|
|
49
|
+
|
|
50
|
+
This client spawns an ACP agent as a subprocess and communicates
|
|
51
|
+
via the Agent Client Protocol (JSON-RPC over stdio).
|
|
52
|
+
|
|
53
|
+
Example:
|
|
54
|
+
```python
|
|
55
|
+
# Connect to claude-code-acp
|
|
56
|
+
client = AcpClient(command="claude-code-acp")
|
|
57
|
+
|
|
58
|
+
# Or connect to the TypeScript version
|
|
59
|
+
client = AcpClient(command="npx", args=["@anthropic/claude-code-acp"])
|
|
60
|
+
|
|
61
|
+
@client.on_text
|
|
62
|
+
async def handle_text(text):
|
|
63
|
+
print(text, end="")
|
|
64
|
+
|
|
65
|
+
async with client:
|
|
66
|
+
await client.prompt("Hello!")
|
|
67
|
+
```
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
command: str = "claude-code-acp",
|
|
73
|
+
args: list[str] | None = None,
|
|
74
|
+
cwd: str = ".",
|
|
75
|
+
env: dict[str, str] | None = None,
|
|
76
|
+
):
|
|
77
|
+
"""
|
|
78
|
+
Initialize the ACP client.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
command: The ACP agent command to run.
|
|
82
|
+
args: Additional arguments for the command.
|
|
83
|
+
cwd: Working directory for the agent.
|
|
84
|
+
env: Additional environment variables.
|
|
85
|
+
"""
|
|
86
|
+
self.command = command
|
|
87
|
+
self.args = args or []
|
|
88
|
+
self.cwd = cwd
|
|
89
|
+
self.env = env
|
|
90
|
+
self.events = AcpClientEvents()
|
|
91
|
+
|
|
92
|
+
self._process: asyncio.subprocess.Process | None = None
|
|
93
|
+
self._connection: ClientSideConnection | None = None
|
|
94
|
+
self._session_id: str | None = None
|
|
95
|
+
self._text_buffer = ""
|
|
96
|
+
self._initialized = False
|
|
97
|
+
|
|
98
|
+
# --- Event decorators ---
|
|
99
|
+
|
|
100
|
+
def on_text(self, func: Callable[[str], Coroutine[Any, Any, None]]):
|
|
101
|
+
"""Register handler for text responses."""
|
|
102
|
+
self.events.on_text = func
|
|
103
|
+
return func
|
|
104
|
+
|
|
105
|
+
def on_thinking(self, func: Callable[[str], Coroutine[Any, Any, None]]):
|
|
106
|
+
"""Register handler for thinking blocks."""
|
|
107
|
+
self.events.on_thinking = func
|
|
108
|
+
return func
|
|
109
|
+
|
|
110
|
+
def on_tool_start(self, func: Callable[[str, str, dict], Coroutine[Any, Any, None]]):
|
|
111
|
+
"""Register handler for tool start events."""
|
|
112
|
+
self.events.on_tool_start = func
|
|
113
|
+
return func
|
|
114
|
+
|
|
115
|
+
def on_tool_end(self, func: Callable[[str, str, Any], Coroutine[Any, Any, None]]):
|
|
116
|
+
"""Register handler for tool end events."""
|
|
117
|
+
self.events.on_tool_end = func
|
|
118
|
+
return func
|
|
119
|
+
|
|
120
|
+
def on_permission(self, func: Callable[[str, dict, list], Coroutine[Any, Any, str]]):
|
|
121
|
+
"""
|
|
122
|
+
Register handler for permission requests.
|
|
123
|
+
|
|
124
|
+
The handler receives (name, input, options) and should return
|
|
125
|
+
the option_id to select (e.g., "allow", "reject", "allow_always").
|
|
126
|
+
"""
|
|
127
|
+
self.events.on_permission = func
|
|
128
|
+
return func
|
|
129
|
+
|
|
130
|
+
def on_error(self, func: Callable[[Exception], Coroutine[Any, Any, None]]):
|
|
131
|
+
"""Register handler for errors."""
|
|
132
|
+
self.events.on_error = func
|
|
133
|
+
return func
|
|
134
|
+
|
|
135
|
+
def on_complete(self, func: Callable[[], Coroutine[Any, Any, None]]):
|
|
136
|
+
"""Register handler for completion."""
|
|
137
|
+
self.events.on_complete = func
|
|
138
|
+
return func
|
|
139
|
+
|
|
140
|
+
# --- Connection management ---
|
|
141
|
+
|
|
142
|
+
async def connect(self) -> None:
|
|
143
|
+
"""Connect to the ACP agent."""
|
|
144
|
+
if self._process is not None:
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
# Spawn the agent process
|
|
148
|
+
self._process = await asyncio.create_subprocess_exec(
|
|
149
|
+
self.command,
|
|
150
|
+
*self.args,
|
|
151
|
+
stdin=asyncio.subprocess.PIPE,
|
|
152
|
+
stdout=asyncio.subprocess.PIPE,
|
|
153
|
+
stderr=asyncio.subprocess.PIPE,
|
|
154
|
+
cwd=self.cwd,
|
|
155
|
+
env=self.env,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
if self._process.stdin is None or self._process.stdout is None:
|
|
159
|
+
raise RuntimeError("Failed to create subprocess pipes")
|
|
160
|
+
|
|
161
|
+
# Create ACP connection
|
|
162
|
+
# Note: ClientSideConnection expects (writer, reader) - stdin is writer, stdout is reader
|
|
163
|
+
self._connection = ClientSideConnection(
|
|
164
|
+
to_client=self._create_client_handler(),
|
|
165
|
+
input_stream=self._process.stdin,
|
|
166
|
+
output_stream=self._process.stdout,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Initialize the connection
|
|
170
|
+
init_response = await self._connection.initialize(
|
|
171
|
+
protocol_version=1,
|
|
172
|
+
client_info=Implementation(
|
|
173
|
+
name="claude-code-acp-client",
|
|
174
|
+
version="0.2.0",
|
|
175
|
+
),
|
|
176
|
+
)
|
|
177
|
+
logger.info(f"Connected to agent: {init_response.agent_info}")
|
|
178
|
+
self._initialized = True
|
|
179
|
+
|
|
180
|
+
async def disconnect(self) -> None:
|
|
181
|
+
"""Disconnect from the ACP agent."""
|
|
182
|
+
if self._connection:
|
|
183
|
+
await self._connection.close()
|
|
184
|
+
self._connection = None
|
|
185
|
+
|
|
186
|
+
if self._process:
|
|
187
|
+
self._process.terminate()
|
|
188
|
+
await self._process.wait()
|
|
189
|
+
self._process = None
|
|
190
|
+
|
|
191
|
+
self._initialized = False
|
|
192
|
+
self._session_id = None
|
|
193
|
+
|
|
194
|
+
async def __aenter__(self) -> "AcpClient":
|
|
195
|
+
await self.connect()
|
|
196
|
+
return self
|
|
197
|
+
|
|
198
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
199
|
+
await self.disconnect()
|
|
200
|
+
|
|
201
|
+
# --- Session management ---
|
|
202
|
+
|
|
203
|
+
async def new_session(self) -> str:
|
|
204
|
+
"""Create a new session."""
|
|
205
|
+
if not self._connection:
|
|
206
|
+
raise RuntimeError("Not connected")
|
|
207
|
+
|
|
208
|
+
response = await self._connection.new_session(
|
|
209
|
+
cwd=self.cwd,
|
|
210
|
+
mcp_servers=[],
|
|
211
|
+
)
|
|
212
|
+
self._session_id = response.session_id
|
|
213
|
+
return self._session_id
|
|
214
|
+
|
|
215
|
+
async def prompt(self, text: str) -> str:
|
|
216
|
+
"""
|
|
217
|
+
Send a prompt and receive the response.
|
|
218
|
+
|
|
219
|
+
Events will be dispatched to registered handlers as they arrive.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
text: The prompt text to send.
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
The full text response.
|
|
226
|
+
"""
|
|
227
|
+
if not self._connection:
|
|
228
|
+
raise RuntimeError("Not connected")
|
|
229
|
+
|
|
230
|
+
if not self._session_id:
|
|
231
|
+
await self.new_session()
|
|
232
|
+
|
|
233
|
+
self._text_buffer = ""
|
|
234
|
+
|
|
235
|
+
try:
|
|
236
|
+
response = await self._connection.prompt(
|
|
237
|
+
prompt=[TextContentBlock(type="text", text=text)],
|
|
238
|
+
session_id=self._session_id,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
if self.events.on_complete:
|
|
242
|
+
await self.events.on_complete()
|
|
243
|
+
|
|
244
|
+
return self._text_buffer
|
|
245
|
+
|
|
246
|
+
except Exception as e:
|
|
247
|
+
if self.events.on_error:
|
|
248
|
+
await self.events.on_error(e)
|
|
249
|
+
raise
|
|
250
|
+
|
|
251
|
+
async def cancel(self) -> None:
|
|
252
|
+
"""Cancel the current operation."""
|
|
253
|
+
if self._connection and self._session_id:
|
|
254
|
+
await self._connection.cancel(session_id=self._session_id)
|
|
255
|
+
|
|
256
|
+
async def set_mode(self, mode: str) -> None:
|
|
257
|
+
"""Set the permission mode."""
|
|
258
|
+
if not self._connection or not self._session_id:
|
|
259
|
+
raise RuntimeError("No active session")
|
|
260
|
+
|
|
261
|
+
await self._connection.set_session_mode(
|
|
262
|
+
mode_id=mode,
|
|
263
|
+
session_id=self._session_id,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
# --- Internal handlers ---
|
|
267
|
+
|
|
268
|
+
def _create_client_handler(self):
|
|
269
|
+
"""Create the client handler for receiving agent messages."""
|
|
270
|
+
client = self
|
|
271
|
+
|
|
272
|
+
class ClientHandler:
|
|
273
|
+
"""Handles incoming messages from the agent."""
|
|
274
|
+
|
|
275
|
+
async def session_update(self, session_id: str, update: Any) -> None:
|
|
276
|
+
"""Handle session updates from the agent."""
|
|
277
|
+
update_type = type(update).__name__
|
|
278
|
+
|
|
279
|
+
if isinstance(update, AgentMessageChunk):
|
|
280
|
+
content = getattr(update, "content", None)
|
|
281
|
+
if content and hasattr(content, "text"):
|
|
282
|
+
text = content.text
|
|
283
|
+
if text and text not in client._text_buffer:
|
|
284
|
+
client._text_buffer += text
|
|
285
|
+
if client.events.on_text:
|
|
286
|
+
await client.events.on_text(text)
|
|
287
|
+
|
|
288
|
+
elif isinstance(update, AgentThoughtChunk):
|
|
289
|
+
content = getattr(update, "content", None)
|
|
290
|
+
if content and hasattr(content, "text"):
|
|
291
|
+
if client.events.on_thinking:
|
|
292
|
+
await client.events.on_thinking(content.text)
|
|
293
|
+
|
|
294
|
+
elif isinstance(update, ToolCallStart):
|
|
295
|
+
if client.events.on_tool_start:
|
|
296
|
+
await client.events.on_tool_start(
|
|
297
|
+
update.tool_call_id,
|
|
298
|
+
update.title or "",
|
|
299
|
+
update.raw_input or {},
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
elif isinstance(update, ToolCallProgress):
|
|
303
|
+
if client.events.on_tool_end:
|
|
304
|
+
await client.events.on_tool_end(
|
|
305
|
+
update.tool_call_id,
|
|
306
|
+
update.status or "",
|
|
307
|
+
update.raw_output,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
async def request_permission(
|
|
311
|
+
self,
|
|
312
|
+
options: list[PermissionOption],
|
|
313
|
+
session_id: str,
|
|
314
|
+
tool_call: ToolCallUpdate,
|
|
315
|
+
**kwargs: Any,
|
|
316
|
+
) -> RequestPermissionResponse:
|
|
317
|
+
"""Handle permission requests from the agent."""
|
|
318
|
+
name = tool_call.title or "Unknown"
|
|
319
|
+
raw_input = tool_call.raw_input or {}
|
|
320
|
+
option_list = [{"id": o.option_id, "name": o.name} for o in options]
|
|
321
|
+
|
|
322
|
+
# Default to allow
|
|
323
|
+
selected_id = "allow"
|
|
324
|
+
|
|
325
|
+
if client.events.on_permission:
|
|
326
|
+
selected_id = await client.events.on_permission(
|
|
327
|
+
name, raw_input, option_list
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
return RequestPermissionResponse(
|
|
331
|
+
outcome={"outcome": "selected", "option_id": selected_id}
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
async def write_text_file(self, **kwargs) -> None:
|
|
335
|
+
"""Handle write file requests (stub)."""
|
|
336
|
+
pass
|
|
337
|
+
|
|
338
|
+
async def read_text_file(self, **kwargs) -> dict:
|
|
339
|
+
"""Handle read file requests (stub)."""
|
|
340
|
+
return {"content": ""}
|
|
341
|
+
|
|
342
|
+
async def create_terminal(self, **kwargs) -> dict:
|
|
343
|
+
"""Handle terminal creation (stub)."""
|
|
344
|
+
return {"terminal_id": "stub"}
|
|
345
|
+
|
|
346
|
+
async def terminal_output(self, **kwargs) -> dict:
|
|
347
|
+
"""Handle terminal output requests (stub)."""
|
|
348
|
+
return {"output": ""}
|
|
349
|
+
|
|
350
|
+
async def release_terminal(self, **kwargs) -> None:
|
|
351
|
+
"""Handle terminal release (stub)."""
|
|
352
|
+
pass
|
|
353
|
+
|
|
354
|
+
async def wait_for_terminal_exit(self, **kwargs) -> dict:
|
|
355
|
+
"""Handle terminal exit wait (stub)."""
|
|
356
|
+
return {"exit_code": 0}
|
|
357
|
+
|
|
358
|
+
async def kill_terminal(self, **kwargs) -> None:
|
|
359
|
+
"""Handle terminal kill (stub)."""
|
|
360
|
+
pass
|
|
361
|
+
|
|
362
|
+
async def ext_method(self, method: str, params: dict) -> dict:
|
|
363
|
+
"""Handle extension methods."""
|
|
364
|
+
return {}
|
|
365
|
+
|
|
366
|
+
async def ext_notification(self, method: str, params: dict) -> None:
|
|
367
|
+
"""Handle extension notifications."""
|
|
368
|
+
pass
|
|
369
|
+
|
|
370
|
+
def on_connect(self, conn: Any) -> None:
|
|
371
|
+
"""Called when connected."""
|
|
372
|
+
pass
|
|
373
|
+
|
|
374
|
+
return ClientHandler()
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Event-driven Claude client wrapper.
|
|
3
|
+
|
|
4
|
+
Provides a simple, decorator-based API for interacting with Claude.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Any, Callable, Coroutine
|
|
12
|
+
|
|
13
|
+
from .agent import ClaudeAcpAgent
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class ClaudeEvents:
|
|
18
|
+
"""Event handlers for Claude responses."""
|
|
19
|
+
|
|
20
|
+
on_text: Callable[[str], Coroutine[Any, Any, None]] | None = None
|
|
21
|
+
on_thinking: Callable[[str], Coroutine[Any, Any, None]] | None = None
|
|
22
|
+
on_tool_start: Callable[[str, str, dict], Coroutine[Any, Any, None]] | None = None
|
|
23
|
+
on_tool_end: Callable[[str, str, Any], Coroutine[Any, Any, None]] | None = None
|
|
24
|
+
on_permission: Callable[[str, dict], Coroutine[Any, Any, bool]] | None = None
|
|
25
|
+
on_error: Callable[[Exception], Coroutine[Any, Any, None]] | None = None
|
|
26
|
+
on_complete: Callable[[], Coroutine[Any, Any, None]] | None = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ClaudeClient:
|
|
30
|
+
"""
|
|
31
|
+
Event-driven Claude client using ACP.
|
|
32
|
+
|
|
33
|
+
Example:
|
|
34
|
+
```python
|
|
35
|
+
client = ClaudeClient(cwd="/path/to/project")
|
|
36
|
+
|
|
37
|
+
@client.on_text
|
|
38
|
+
async def handle_text(text):
|
|
39
|
+
print(text, end="", flush=True)
|
|
40
|
+
|
|
41
|
+
@client.on_tool_start
|
|
42
|
+
async def handle_tool(tool_id, name, input):
|
|
43
|
+
print(f"Running: {name}")
|
|
44
|
+
|
|
45
|
+
@client.on_permission
|
|
46
|
+
async def handle_permission(name, input):
|
|
47
|
+
return True # or prompt user
|
|
48
|
+
|
|
49
|
+
response = await client.query("What files are in this directory?")
|
|
50
|
+
```
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __init__(self, cwd: str = "."):
|
|
54
|
+
"""
|
|
55
|
+
Initialize the Claude client.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
cwd: Working directory for Claude operations.
|
|
59
|
+
"""
|
|
60
|
+
self.cwd = cwd
|
|
61
|
+
self.agent = ClaudeAcpAgent()
|
|
62
|
+
self.session_id: str | None = None
|
|
63
|
+
self.events = ClaudeEvents()
|
|
64
|
+
self._text_buffer = ""
|
|
65
|
+
self._seen_text = set() # Track seen text to avoid duplicates
|
|
66
|
+
|
|
67
|
+
# --- Decorator-based event registration ---
|
|
68
|
+
|
|
69
|
+
def on_text(self, func: Callable[[str], Coroutine[Any, Any, None]]):
|
|
70
|
+
"""
|
|
71
|
+
Register handler for text responses.
|
|
72
|
+
|
|
73
|
+
The handler receives streaming text chunks as they arrive.
|
|
74
|
+
|
|
75
|
+
Example:
|
|
76
|
+
@client.on_text
|
|
77
|
+
async def handle_text(text: str):
|
|
78
|
+
print(text, end="", flush=True)
|
|
79
|
+
"""
|
|
80
|
+
self.events.on_text = func
|
|
81
|
+
return func
|
|
82
|
+
|
|
83
|
+
def on_thinking(self, func: Callable[[str], Coroutine[Any, Any, None]]):
|
|
84
|
+
"""
|
|
85
|
+
Register handler for thinking/reasoning blocks.
|
|
86
|
+
|
|
87
|
+
Example:
|
|
88
|
+
@client.on_thinking
|
|
89
|
+
async def handle_thinking(text: str):
|
|
90
|
+
print(f"[Thinking: {text}]")
|
|
91
|
+
"""
|
|
92
|
+
self.events.on_thinking = func
|
|
93
|
+
return func
|
|
94
|
+
|
|
95
|
+
def on_tool_start(
|
|
96
|
+
self, func: Callable[[str, str, dict], Coroutine[Any, Any, None]]
|
|
97
|
+
):
|
|
98
|
+
"""
|
|
99
|
+
Register handler for tool call start events.
|
|
100
|
+
|
|
101
|
+
Args to handler:
|
|
102
|
+
tool_id: Unique identifier for the tool call
|
|
103
|
+
name: Human-readable tool name/title
|
|
104
|
+
input: Tool input parameters
|
|
105
|
+
|
|
106
|
+
Example:
|
|
107
|
+
@client.on_tool_start
|
|
108
|
+
async def handle_tool_start(tool_id: str, name: str, input: dict):
|
|
109
|
+
print(f"🔧 Starting: {name}")
|
|
110
|
+
"""
|
|
111
|
+
self.events.on_tool_start = func
|
|
112
|
+
return func
|
|
113
|
+
|
|
114
|
+
def on_tool_end(self, func: Callable[[str, str, Any], Coroutine[Any, Any, None]]):
|
|
115
|
+
"""
|
|
116
|
+
Register handler for tool completion events.
|
|
117
|
+
|
|
118
|
+
Args to handler:
|
|
119
|
+
tool_id: Unique identifier for the tool call
|
|
120
|
+
status: "completed" or "failed"
|
|
121
|
+
output: Tool output/result
|
|
122
|
+
|
|
123
|
+
Example:
|
|
124
|
+
@client.on_tool_end
|
|
125
|
+
async def handle_tool_end(tool_id: str, status: str, output: Any):
|
|
126
|
+
icon = "✅" if status == "completed" else "❌"
|
|
127
|
+
print(f" {icon}")
|
|
128
|
+
"""
|
|
129
|
+
self.events.on_tool_end = func
|
|
130
|
+
return func
|
|
131
|
+
|
|
132
|
+
def on_permission(
|
|
133
|
+
self, func: Callable[[str, dict], Coroutine[Any, Any, bool]]
|
|
134
|
+
):
|
|
135
|
+
"""
|
|
136
|
+
Register handler for permission requests.
|
|
137
|
+
|
|
138
|
+
The handler should return True to allow, False to deny.
|
|
139
|
+
|
|
140
|
+
Example:
|
|
141
|
+
@client.on_permission
|
|
142
|
+
async def handle_permission(name: str, input: dict) -> bool:
|
|
143
|
+
response = input("Allow {name}? [y/N]: ")
|
|
144
|
+
return response.lower() == "y"
|
|
145
|
+
"""
|
|
146
|
+
self.events.on_permission = func
|
|
147
|
+
return func
|
|
148
|
+
|
|
149
|
+
def on_error(self, func: Callable[[Exception], Coroutine[Any, Any, None]]):
|
|
150
|
+
"""
|
|
151
|
+
Register handler for errors.
|
|
152
|
+
|
|
153
|
+
Example:
|
|
154
|
+
@client.on_error
|
|
155
|
+
async def handle_error(e: Exception):
|
|
156
|
+
print(f"Error: {e}")
|
|
157
|
+
"""
|
|
158
|
+
self.events.on_error = func
|
|
159
|
+
return func
|
|
160
|
+
|
|
161
|
+
def on_complete(self, func: Callable[[], Coroutine[Any, Any, None]]):
|
|
162
|
+
"""
|
|
163
|
+
Register handler for query completion.
|
|
164
|
+
|
|
165
|
+
Example:
|
|
166
|
+
@client.on_complete
|
|
167
|
+
async def handle_complete():
|
|
168
|
+
print("\\n--- Done ---")
|
|
169
|
+
"""
|
|
170
|
+
self.events.on_complete = func
|
|
171
|
+
return func
|
|
172
|
+
|
|
173
|
+
# --- Main API ---
|
|
174
|
+
|
|
175
|
+
async def start_session(self) -> str:
|
|
176
|
+
"""
|
|
177
|
+
Start a new Claude session.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
The session ID.
|
|
181
|
+
"""
|
|
182
|
+
session = await self.agent.new_session(cwd=self.cwd, mcp_servers=[])
|
|
183
|
+
self.session_id = session.session_id
|
|
184
|
+
return self.session_id
|
|
185
|
+
|
|
186
|
+
async def query(self, prompt: str) -> str:
|
|
187
|
+
"""
|
|
188
|
+
Send a query and receive events via registered handlers.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
prompt: The message to send to Claude.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
The full text response as a string.
|
|
195
|
+
|
|
196
|
+
Example:
|
|
197
|
+
response = await client.query("Explain this code")
|
|
198
|
+
print(f"Full response: {response}")
|
|
199
|
+
"""
|
|
200
|
+
if not self.session_id:
|
|
201
|
+
await self.start_session()
|
|
202
|
+
|
|
203
|
+
self._text_buffer = ""
|
|
204
|
+
self._seen_text = set()
|
|
205
|
+
|
|
206
|
+
# Wire up the event handler
|
|
207
|
+
self.agent._conn = self._create_event_handler()
|
|
208
|
+
|
|
209
|
+
try:
|
|
210
|
+
await self.agent.prompt(
|
|
211
|
+
prompt=[{"type": "text", "text": prompt}],
|
|
212
|
+
session_id=self.session_id,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
if self.events.on_complete:
|
|
216
|
+
await self.events.on_complete()
|
|
217
|
+
|
|
218
|
+
except Exception as e:
|
|
219
|
+
if self.events.on_error:
|
|
220
|
+
await self.events.on_error(e)
|
|
221
|
+
raise
|
|
222
|
+
|
|
223
|
+
return self._text_buffer
|
|
224
|
+
|
|
225
|
+
async def set_mode(self, mode: str) -> None:
|
|
226
|
+
"""
|
|
227
|
+
Set the permission mode for the session.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
mode: One of "default", "acceptEdits", "plan", "bypassPermissions"
|
|
231
|
+
"""
|
|
232
|
+
if not self.session_id:
|
|
233
|
+
await self.start_session()
|
|
234
|
+
|
|
235
|
+
await self.agent.set_session_mode(mode_id=mode, session_id=self.session_id)
|
|
236
|
+
|
|
237
|
+
def _create_event_handler(self):
|
|
238
|
+
"""Create the internal event handler that bridges to user callbacks."""
|
|
239
|
+
client = self
|
|
240
|
+
|
|
241
|
+
class EventHandler:
|
|
242
|
+
async def session_update(self, session_id: str, update: Any) -> None:
|
|
243
|
+
update_type = type(update).__name__
|
|
244
|
+
|
|
245
|
+
if "AgentMessageChunk" in update_type:
|
|
246
|
+
content = getattr(update, "content", None)
|
|
247
|
+
if content and hasattr(content, "text"):
|
|
248
|
+
text = content.text
|
|
249
|
+
if not text:
|
|
250
|
+
return
|
|
251
|
+
|
|
252
|
+
# Smart deduplication for streaming:
|
|
253
|
+
# - If buffer is empty, this is new text
|
|
254
|
+
# - If text is already in buffer, skip (duplicate)
|
|
255
|
+
# - If text extends buffer, only emit the new part
|
|
256
|
+
current_len = len(client._text_buffer)
|
|
257
|
+
|
|
258
|
+
if current_len == 0:
|
|
259
|
+
# First chunk
|
|
260
|
+
client._text_buffer = text
|
|
261
|
+
if client.events.on_text:
|
|
262
|
+
await client.events.on_text(text)
|
|
263
|
+
elif text in client._text_buffer:
|
|
264
|
+
# Exact duplicate, skip
|
|
265
|
+
pass
|
|
266
|
+
elif client._text_buffer in text:
|
|
267
|
+
# Text extends buffer - emit only new part
|
|
268
|
+
new_part = text[current_len:]
|
|
269
|
+
if new_part:
|
|
270
|
+
client._text_buffer = text
|
|
271
|
+
if client.events.on_text:
|
|
272
|
+
await client.events.on_text(new_part)
|
|
273
|
+
else:
|
|
274
|
+
# Completely new text chunk
|
|
275
|
+
client._text_buffer += text
|
|
276
|
+
if client.events.on_text:
|
|
277
|
+
await client.events.on_text(text)
|
|
278
|
+
|
|
279
|
+
elif "AgentThoughtChunk" in update_type:
|
|
280
|
+
content = getattr(update, "content", None)
|
|
281
|
+
if content and hasattr(content, "text"):
|
|
282
|
+
if client.events.on_thinking:
|
|
283
|
+
await client.events.on_thinking(content.text)
|
|
284
|
+
|
|
285
|
+
elif "ToolCallStart" in update_type:
|
|
286
|
+
if client.events.on_tool_start:
|
|
287
|
+
await client.events.on_tool_start(
|
|
288
|
+
getattr(update, "tool_call_id", ""),
|
|
289
|
+
getattr(update, "title", ""),
|
|
290
|
+
getattr(update, "raw_input", {}),
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
elif "ToolCallProgress" in update_type:
|
|
294
|
+
if client.events.on_tool_end:
|
|
295
|
+
await client.events.on_tool_end(
|
|
296
|
+
getattr(update, "tool_call_id", ""),
|
|
297
|
+
getattr(update, "status", ""),
|
|
298
|
+
getattr(update, "raw_output", None),
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
async def request_permission(self, **kwargs: Any) -> dict:
|
|
302
|
+
tool_call = kwargs.get("tool_call", {})
|
|
303
|
+
name = tool_call.get("title", "Unknown")
|
|
304
|
+
raw_input = tool_call.get("raw_input", {})
|
|
305
|
+
|
|
306
|
+
approved = True
|
|
307
|
+
if client.events.on_permission:
|
|
308
|
+
approved = await client.events.on_permission(name, raw_input)
|
|
309
|
+
|
|
310
|
+
if approved:
|
|
311
|
+
return {"outcome": {"outcome": "selected", "option_id": "allow"}}
|
|
312
|
+
return {"outcome": {"outcome": "selected", "option_id": "reject"}}
|
|
313
|
+
|
|
314
|
+
return EventHandler()
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
__all__ = ["ClaudeClient", "ClaudeEvents"]
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: claude-code-acp
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: ACP-compatible agent for Claude Code (Python version)
|
|
5
|
+
Project-URL: Homepage, https://github.com/yazelin/claude-code-acp-py
|
|
6
|
+
Project-URL: Repository, https://github.com/yazelin/claude-code-acp-py
|
|
7
|
+
Project-URL: Issues, https://github.com/yazelin/claude-code-acp-py/issues
|
|
8
|
+
Author: yazelin
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: acp,agent,anthropic,claude,claude-code,python
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: agent-client-protocol>=0.7.0
|
|
21
|
+
Requires-Dist: claude-agent-sdk>=0.1.29
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# Claude Code ACP (Python)
|
|
25
|
+
|
|
26
|
+
[](https://pypi.org/project/claude-code-acp/)
|
|
27
|
+
[](https://pypi.org/project/claude-code-acp/)
|
|
28
|
+
[](https://github.com/yazelin/claude-code-acp-py/blob/main/LICENSE)
|
|
29
|
+
|
|
30
|
+
**Python implementation of ACP (Agent Client Protocol) for Claude Code.**
|
|
31
|
+
|
|
32
|
+
This package bridges the [Claude Agent SDK](https://github.com/anthropics/claude-agent-sdk-python) with the [Agent Client Protocol (ACP)](https://agentclientprotocol.com/), providing two ways to use Claude:
|
|
33
|
+
|
|
34
|
+
1. **ACP Server** - Connect Claude to any ACP-compatible editor (Zed, Neovim, JetBrains, etc.)
|
|
35
|
+
2. **Python Client** - Event-driven API for building Python applications with Claude
|
|
36
|
+
|
|
37
|
+
## Features
|
|
38
|
+
|
|
39
|
+
- **Uses Claude CLI subscription** - No API key needed, uses your existing Claude subscription
|
|
40
|
+
- **Full ACP protocol support** - Compatible with Zed, Neovim, and other ACP clients
|
|
41
|
+
- **Bidirectional communication** - Permission requests, tool calls, streaming responses
|
|
42
|
+
- **Event-driven Python API** - Decorator-based handlers for easy integration
|
|
43
|
+
- **Session management** - Create, fork, resume, list sessions
|
|
44
|
+
- **Multiple permission modes** - default, acceptEdits, plan, bypassPermissions
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install claude-code-acp
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Or with uv:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
uv tool install claude-code-acp
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Requirements
|
|
59
|
+
|
|
60
|
+
- Python 3.10+
|
|
61
|
+
- Claude CLI installed and authenticated (`claude /login`)
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Components
|
|
66
|
+
|
|
67
|
+
| Class | Type | Description |
|
|
68
|
+
|-------|------|-------------|
|
|
69
|
+
| `ClaudeAcpAgent` | ACP Server | For editors (Zed, Neovim) to connect |
|
|
70
|
+
| `ClaudeClient` | Python API | Event-driven wrapper (uses agent internally) |
|
|
71
|
+
| `AcpClient` | ACP Client | Connect to any ACP agent via subprocess |
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Usage 1: ACP Server for Editors
|
|
76
|
+
|
|
77
|
+
Run as an ACP server to connect Claude to your editor:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
claude-code-acp
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Zed Editor
|
|
84
|
+
|
|
85
|
+
Add to your Zed `settings.json`:
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"agent_servers": {
|
|
90
|
+
"Claude Code Python": {
|
|
91
|
+
"type": "custom",
|
|
92
|
+
"command": "claude-code-acp",
|
|
93
|
+
"args": [],
|
|
94
|
+
"env": {}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Then open the Agent Panel (`Ctrl+?` / `Cmd+?`) and select "Claude Code Python" from the `+` menu.
|
|
101
|
+
|
|
102
|
+
### Other Editors
|
|
103
|
+
|
|
104
|
+
Any [ACP-compatible client](https://agentclientprotocol.com/overview/clients) can connect by spawning `claude-code-acp` as a subprocess and communicating via stdio.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Usage 2: Python Event-Driven API
|
|
109
|
+
|
|
110
|
+
Use `ClaudeClient` for building Python applications with Claude:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
import asyncio
|
|
114
|
+
from claude_code_acp import ClaudeClient
|
|
115
|
+
|
|
116
|
+
async def main():
|
|
117
|
+
client = ClaudeClient(cwd=".")
|
|
118
|
+
|
|
119
|
+
@client.on_text
|
|
120
|
+
async def handle_text(text: str):
|
|
121
|
+
"""Called for each text chunk from Claude."""
|
|
122
|
+
print(text, end="", flush=True)
|
|
123
|
+
|
|
124
|
+
@client.on_tool_start
|
|
125
|
+
async def handle_tool_start(tool_id: str, name: str, input: dict):
|
|
126
|
+
"""Called when Claude starts using a tool."""
|
|
127
|
+
print(f"\n🔧 {name}")
|
|
128
|
+
|
|
129
|
+
@client.on_tool_end
|
|
130
|
+
async def handle_tool_end(tool_id: str, status: str, output):
|
|
131
|
+
"""Called when a tool completes."""
|
|
132
|
+
icon = "✅" if status == "completed" else "❌"
|
|
133
|
+
print(f" {icon}")
|
|
134
|
+
|
|
135
|
+
@client.on_permission
|
|
136
|
+
async def handle_permission(name: str, input: dict) -> bool:
|
|
137
|
+
"""Called when Claude needs permission. Return True to allow."""
|
|
138
|
+
print(f"🔐 Permission requested: {name}")
|
|
139
|
+
return True # or prompt user
|
|
140
|
+
|
|
141
|
+
@client.on_complete
|
|
142
|
+
async def handle_complete():
|
|
143
|
+
"""Called when the query completes."""
|
|
144
|
+
print("\n--- Done ---")
|
|
145
|
+
|
|
146
|
+
# Send a query
|
|
147
|
+
response = await client.query("Create a hello.py file that prints Hello World")
|
|
148
|
+
print(f"\nFull response: {response}")
|
|
149
|
+
|
|
150
|
+
asyncio.run(main())
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Event Handlers
|
|
154
|
+
|
|
155
|
+
| Decorator | Arguments | Description |
|
|
156
|
+
|-----------|-----------|-------------|
|
|
157
|
+
| `@client.on_text` | `(text: str)` | Streaming text chunks from Claude |
|
|
158
|
+
| `@client.on_thinking` | `(text: str)` | Thinking/reasoning blocks |
|
|
159
|
+
| `@client.on_tool_start` | `(tool_id, name, input)` | Tool execution started |
|
|
160
|
+
| `@client.on_tool_end` | `(tool_id, status, output)` | Tool execution completed |
|
|
161
|
+
| `@client.on_permission` | `(name, input) -> bool` | Permission request (return True/False) |
|
|
162
|
+
| `@client.on_error` | `(exception)` | Error occurred |
|
|
163
|
+
| `@client.on_complete` | `()` | Query completed |
|
|
164
|
+
|
|
165
|
+
### Client Methods
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
# Start a new session
|
|
169
|
+
session_id = await client.start_session()
|
|
170
|
+
|
|
171
|
+
# Send a query (returns full response text)
|
|
172
|
+
response = await client.query("Your prompt here")
|
|
173
|
+
|
|
174
|
+
# Set permission mode
|
|
175
|
+
await client.set_mode("acceptEdits") # or "default", "plan", "bypassPermissions"
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Usage 3: ACP Client (Connect to Any Agent)
|
|
181
|
+
|
|
182
|
+
Use `AcpClient` to connect to any ACP-compatible agent:
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
import asyncio
|
|
186
|
+
from claude_code_acp import AcpClient
|
|
187
|
+
|
|
188
|
+
async def main():
|
|
189
|
+
# Connect to claude-code-acp (Python version)
|
|
190
|
+
client = AcpClient(command="claude-code-acp")
|
|
191
|
+
|
|
192
|
+
# Or connect to the TypeScript version
|
|
193
|
+
# client = AcpClient(command="npx", args=["@zed-industries/claude-code-acp"])
|
|
194
|
+
|
|
195
|
+
# Or any other ACP agent
|
|
196
|
+
# client = AcpClient(command="my-custom-agent")
|
|
197
|
+
|
|
198
|
+
@client.on_text
|
|
199
|
+
async def handle_text(text: str):
|
|
200
|
+
print(text, end="", flush=True)
|
|
201
|
+
|
|
202
|
+
@client.on_tool_start
|
|
203
|
+
async def handle_tool(tool_id: str, name: str, input: dict):
|
|
204
|
+
print(f"\n🔧 {name}")
|
|
205
|
+
|
|
206
|
+
@client.on_permission
|
|
207
|
+
async def handle_permission(name: str, input: dict, options: list) -> str:
|
|
208
|
+
"""Return option_id: 'allow', 'reject', or 'allow_always'"""
|
|
209
|
+
print(f"🔐 Permission: {name}")
|
|
210
|
+
return "allow"
|
|
211
|
+
|
|
212
|
+
@client.on_complete
|
|
213
|
+
async def handle_complete():
|
|
214
|
+
print("\n--- Done ---")
|
|
215
|
+
|
|
216
|
+
async with client:
|
|
217
|
+
response = await client.prompt("What files are here?")
|
|
218
|
+
|
|
219
|
+
asyncio.run(main())
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### AcpClient vs ClaudeClient
|
|
223
|
+
|
|
224
|
+
| Feature | `ClaudeClient` | `AcpClient` |
|
|
225
|
+
|---------|---------------|-------------|
|
|
226
|
+
| Uses | Claude Agent SDK directly | Any ACP agent via subprocess |
|
|
227
|
+
| Connection | In-process | Subprocess + stdio |
|
|
228
|
+
| Agents | Claude only | Any ACP-compatible agent |
|
|
229
|
+
| Use case | Simple Python apps | Multi-agent, testing, flexibility |
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## Architecture
|
|
234
|
+
|
|
235
|
+
```
|
|
236
|
+
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
237
|
+
│ Your Application │
|
|
238
|
+
├─────────────────────────────────────────────────────────────────────────────┤
|
|
239
|
+
│ │
|
|
240
|
+
│ ┌─────────────────┐ ┌─────────────────────────────┐ │
|
|
241
|
+
│ │ Zed/Neovim │ │ Python Application │ │
|
|
242
|
+
│ │ (ACP Client) │ │ │ │
|
|
243
|
+
│ └────────┬────────┘ │ client = ClaudeClient() │ │
|
|
244
|
+
│ │ │ │ │
|
|
245
|
+
│ │ ACP Protocol │ @client.on_text │ │
|
|
246
|
+
│ │ (stdio/JSON-RPC) │ async def handle(text): │ │
|
|
247
|
+
│ │ │ print(text) │ │
|
|
248
|
+
│ ▼ │ │ │
|
|
249
|
+
│ ┌────────────────────────────────────────┴─────────────────────────────┐ │
|
|
250
|
+
│ │ │ │
|
|
251
|
+
│ │ claude-code-acp (This Package) │ │
|
|
252
|
+
│ │ │ │
|
|
253
|
+
│ │ ┌─────────────────────┐ ┌─────────────────────────────────┐ │ │
|
|
254
|
+
│ │ │ ClaudeAcpAgent │ │ ClaudeClient │ │ │
|
|
255
|
+
│ │ │ (ACP Server) │ │ (Event-driven wrapper) │ │ │
|
|
256
|
+
│ │ └──────────┬──────────┘ └───────────────┬─────────────────┘ │ │
|
|
257
|
+
│ │ │ │ │ │
|
|
258
|
+
│ │ └────────────────┬────────────────┘ │ │
|
|
259
|
+
│ │ │ │ │
|
|
260
|
+
│ └───────────────────────────────┼───────────────────────────────────────┘ │
|
|
261
|
+
│ │ │
|
|
262
|
+
│ ▼ │
|
|
263
|
+
│ ┌─────────────────────────────┐ │
|
|
264
|
+
│ │ Claude Agent SDK │ │
|
|
265
|
+
│ │ (claude-agent-sdk) │ │
|
|
266
|
+
│ └──────────────┬──────────────┘ │
|
|
267
|
+
│ │ │
|
|
268
|
+
│ ▼ │
|
|
269
|
+
│ ┌─────────────────────────────┐ │
|
|
270
|
+
│ │ Claude CLI │ │
|
|
271
|
+
│ │ (Your Claude Subscription) │ │
|
|
272
|
+
│ └─────────────────────────────┘ │
|
|
273
|
+
│ │
|
|
274
|
+
└─────────────────────────────────────────────────────────────────────────────┘
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## What We Built
|
|
280
|
+
|
|
281
|
+
This project combines two official SDKs to create a complete Python solution:
|
|
282
|
+
|
|
283
|
+
### Integrated Components
|
|
284
|
+
|
|
285
|
+
| Component | Source | Purpose |
|
|
286
|
+
|-----------|--------|---------|
|
|
287
|
+
| [Agent Client Protocol SDK](https://github.com/anthropics/agent-client-protocol) | Anthropic | ACP server/client protocol implementation |
|
|
288
|
+
| [Claude Agent SDK](https://github.com/anthropics/claude-agent-sdk-python) | Anthropic | Claude CLI wrapper with streaming support |
|
|
289
|
+
|
|
290
|
+
### Our Contributions
|
|
291
|
+
|
|
292
|
+
1. **ClaudeAcpAgent** (`agent.py`)
|
|
293
|
+
- Bridges Claude Agent SDK with ACP protocol
|
|
294
|
+
- Converts Claude messages to ACP session updates
|
|
295
|
+
- Handles bidirectional permission requests
|
|
296
|
+
- Session management (create, fork, resume, list)
|
|
297
|
+
|
|
298
|
+
2. **ClaudeClient** (`client.py`)
|
|
299
|
+
- Event-driven Python API with decorators
|
|
300
|
+
- Smart text deduplication for streaming
|
|
301
|
+
- Simple permission handling
|
|
302
|
+
- Clean async/await interface
|
|
303
|
+
|
|
304
|
+
3. **ACP Server Entry Point**
|
|
305
|
+
- Standalone `claude-code-acp` command
|
|
306
|
+
- Direct integration with Zed and other ACP clients
|
|
307
|
+
- No configuration needed
|
|
308
|
+
|
|
309
|
+
### Why This Package?
|
|
310
|
+
|
|
311
|
+
| Approach | API Key | Subscription | ACP Support | Event-Driven |
|
|
312
|
+
|----------|---------|--------------|-------------|--------------|
|
|
313
|
+
| Anthropic API directly | ✅ Required | ❌ | ❌ | ❌ |
|
|
314
|
+
| Claude Agent SDK | ❌ | ✅ Uses CLI | ❌ | Partial |
|
|
315
|
+
| **claude-code-acp** | ❌ | ✅ Uses CLI | ✅ Full | ✅ Full |
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
## Examples
|
|
320
|
+
|
|
321
|
+
### Simple Chat
|
|
322
|
+
|
|
323
|
+
```python
|
|
324
|
+
import asyncio
|
|
325
|
+
from claude_code_acp import ClaudeClient
|
|
326
|
+
|
|
327
|
+
async def main():
|
|
328
|
+
client = ClaudeClient()
|
|
329
|
+
|
|
330
|
+
@client.on_text
|
|
331
|
+
async def on_text(text):
|
|
332
|
+
print(text, end="")
|
|
333
|
+
|
|
334
|
+
while True:
|
|
335
|
+
user_input = input("\nYou: ")
|
|
336
|
+
if user_input.lower() == "quit":
|
|
337
|
+
break
|
|
338
|
+
await client.query(user_input)
|
|
339
|
+
|
|
340
|
+
asyncio.run(main())
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### File Operations with Permission Control
|
|
344
|
+
|
|
345
|
+
```python
|
|
346
|
+
import asyncio
|
|
347
|
+
from claude_code_acp import ClaudeClient
|
|
348
|
+
|
|
349
|
+
async def main():
|
|
350
|
+
client = ClaudeClient(cwd="/path/to/project")
|
|
351
|
+
|
|
352
|
+
@client.on_text
|
|
353
|
+
async def on_text(text):
|
|
354
|
+
print(text, end="")
|
|
355
|
+
|
|
356
|
+
@client.on_permission
|
|
357
|
+
async def on_permission(name, input):
|
|
358
|
+
response = input(f"Allow '{name}'? [y/N]: ")
|
|
359
|
+
return response.lower() == "y"
|
|
360
|
+
|
|
361
|
+
await client.query("Refactor the main.py file to use async/await")
|
|
362
|
+
|
|
363
|
+
asyncio.run(main())
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### Auto-approve Mode
|
|
367
|
+
|
|
368
|
+
```python
|
|
369
|
+
import asyncio
|
|
370
|
+
from claude_code_acp import ClaudeClient
|
|
371
|
+
|
|
372
|
+
async def main():
|
|
373
|
+
client = ClaudeClient(cwd=".")
|
|
374
|
+
|
|
375
|
+
# Bypass all permission checks
|
|
376
|
+
await client.set_mode("bypassPermissions")
|
|
377
|
+
|
|
378
|
+
@client.on_text
|
|
379
|
+
async def on_text(text):
|
|
380
|
+
print(text, end="")
|
|
381
|
+
|
|
382
|
+
await client.query("Create a complete Flask app with tests")
|
|
383
|
+
|
|
384
|
+
asyncio.run(main())
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
---
|
|
388
|
+
|
|
389
|
+
## Development
|
|
390
|
+
|
|
391
|
+
```bash
|
|
392
|
+
# Clone
|
|
393
|
+
git clone https://github.com/yazelin/claude-code-acp-py
|
|
394
|
+
cd claude-code-acp-py
|
|
395
|
+
|
|
396
|
+
# Install dependencies
|
|
397
|
+
uv sync
|
|
398
|
+
|
|
399
|
+
# Run locally
|
|
400
|
+
uv run claude-code-acp
|
|
401
|
+
|
|
402
|
+
# Run tests
|
|
403
|
+
uv run python -c "from claude_code_acp import ClaudeClient; print('OK')"
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
---
|
|
407
|
+
|
|
408
|
+
## Related Projects
|
|
409
|
+
|
|
410
|
+
- [claude-code-acp](https://github.com/zed-industries/claude-code-acp) - TypeScript version by Zed Industries
|
|
411
|
+
- [agent-client-protocol](https://github.com/anthropics/agent-client-protocol) - ACP specification and SDKs
|
|
412
|
+
- [claude-agent-sdk-python](https://github.com/anthropics/claude-agent-sdk-python) - Official Claude Agent SDK
|
|
413
|
+
|
|
414
|
+
---
|
|
415
|
+
|
|
416
|
+
## License
|
|
417
|
+
|
|
418
|
+
MIT
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
claude_code_acp/__init__.py,sha256=sY6dO2pXQnnImk6ChOl7MF3HOrp6GBdIiLl5CseuaWs,828
|
|
2
|
+
claude_code_acp/__main__.py,sha256=zuAdIOmaCsDeRxj2yIl_qMx-74QFaA3nWiS8gti0og0,118
|
|
3
|
+
claude_code_acp/acp_client.py,sha256=E0e8XBco3cBV4sPi4txXpS13WPXRDodwYqHpXmCJd4A,12572
|
|
4
|
+
claude_code_acp/agent.py,sha256=qos32gnMAilOlz6ebllkmuJZS3UgpbAtAZw58r3SYig,20619
|
|
5
|
+
claude_code_acp/client.py,sha256=jKi6tPNqNu0GWTsXxVa1BREGE1wkm_kbIEDulXKGDHE,10680
|
|
6
|
+
claude_code_acp-0.3.0.dist-info/METADATA,sha256=gGQDM94g6UpnPsIDPXkWQyZ6Ig2SanGxI3vvIbixRew,15477
|
|
7
|
+
claude_code_acp-0.3.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
8
|
+
claude_code_acp-0.3.0.dist-info/entry_points.txt,sha256=cc_pkg_V_1zctD5CUqjATCxglkf5-UArEMfN3q9We-A,57
|
|
9
|
+
claude_code_acp-0.3.0.dist-info/licenses/LICENSE,sha256=5NCM9Q9UTfsn-VyafO7htdlYyPPO8H-NHYrO5UV9sT4,1064
|
|
10
|
+
claude_code_acp-0.3.0.dist-info/RECORD,,
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: claude-code-acp
|
|
3
|
-
Version: 0.1.0
|
|
4
|
-
Summary: ACP-compatible agent for Claude Code (Python version)
|
|
5
|
-
Project-URL: Homepage, https://github.com/yazelin/claude-code-acp-py
|
|
6
|
-
Project-URL: Repository, https://github.com/yazelin/claude-code-acp-py
|
|
7
|
-
Project-URL: Issues, https://github.com/yazelin/claude-code-acp-py/issues
|
|
8
|
-
Author: yazelin
|
|
9
|
-
License-Expression: MIT
|
|
10
|
-
License-File: LICENSE
|
|
11
|
-
Keywords: acp,agent,anthropic,claude,claude-code,python
|
|
12
|
-
Classifier: Development Status :: 3 - Alpha
|
|
13
|
-
Classifier: Intended Audience :: Developers
|
|
14
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
-
Classifier: Programming Language :: Python :: 3
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
-
Requires-Python: >=3.10
|
|
20
|
-
Requires-Dist: agent-client-protocol>=0.7.0
|
|
21
|
-
Requires-Dist: claude-agent-sdk>=0.1.29
|
|
22
|
-
Description-Content-Type: text/markdown
|
|
23
|
-
|
|
24
|
-
# Claude Code ACP (Python)
|
|
25
|
-
|
|
26
|
-
[](https://pypi.org/project/claude-code-acp/)
|
|
27
|
-
[](https://pypi.org/project/claude-code-acp/)
|
|
28
|
-
[](https://github.com/yazelin/claude-code-acp-py/blob/main/LICENSE)
|
|
29
|
-
|
|
30
|
-
ACP-compatible agent for Claude Code using the Python SDK.
|
|
31
|
-
|
|
32
|
-
This package bridges the [Claude Agent SDK](https://github.com/anthropics/claude-agent-sdk-python) with the [Agent Client Protocol (ACP)](https://agentclientprotocol.com/), allowing Claude Code to work with any ACP-compatible client like [Zed](https://zed.dev), Neovim, JetBrains IDEs, etc.
|
|
33
|
-
|
|
34
|
-
## Features
|
|
35
|
-
|
|
36
|
-
- Full ACP protocol support
|
|
37
|
-
- Bidirectional communication (permission requests, tool calls)
|
|
38
|
-
- Uses your Claude CLI subscription (no API key needed)
|
|
39
|
-
- Session management (create, fork, resume, list)
|
|
40
|
-
- Multiple permission modes (default, acceptEdits, plan, bypassPermissions)
|
|
41
|
-
|
|
42
|
-
## Installation
|
|
43
|
-
|
|
44
|
-
```bash
|
|
45
|
-
pip install claude-code-acp
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
Or with uv:
|
|
49
|
-
|
|
50
|
-
```bash
|
|
51
|
-
uv add claude-code-acp
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
## Requirements
|
|
55
|
-
|
|
56
|
-
- Python 3.10+
|
|
57
|
-
- Claude CLI installed and authenticated (`claude /login`)
|
|
58
|
-
|
|
59
|
-
## Usage
|
|
60
|
-
|
|
61
|
-
### As a standalone ACP server
|
|
62
|
-
|
|
63
|
-
```bash
|
|
64
|
-
claude-code-acp
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
### With Zed Editor
|
|
68
|
-
|
|
69
|
-
Add to your Zed `settings.json`:
|
|
70
|
-
|
|
71
|
-
```json
|
|
72
|
-
{
|
|
73
|
-
"agent_servers": {
|
|
74
|
-
"Claude Code Python": {
|
|
75
|
-
"type": "custom",
|
|
76
|
-
"command": "claude-code-acp",
|
|
77
|
-
"args": [],
|
|
78
|
-
"env": {}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
Then open the Agent Panel (`Ctrl+?`) and select "Claude Code Python" from the `+` menu.
|
|
85
|
-
|
|
86
|
-
### As a library
|
|
87
|
-
|
|
88
|
-
```python
|
|
89
|
-
import asyncio
|
|
90
|
-
from claude_code_acp import ClaudeAcpAgent
|
|
91
|
-
from acp import run_agent
|
|
92
|
-
|
|
93
|
-
async def main():
|
|
94
|
-
agent = ClaudeAcpAgent()
|
|
95
|
-
await run_agent(agent)
|
|
96
|
-
|
|
97
|
-
asyncio.run(main())
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
## How it works
|
|
101
|
-
|
|
102
|
-
```
|
|
103
|
-
┌─────────────┐ ACP ┌──────────────────┐ SDK ┌─────────────┐
|
|
104
|
-
│ Zed/IDE │ ◄──────────► │ claude-code-acp │ ◄────────► │ Claude CLI │
|
|
105
|
-
│ (ACP Client)│ (stdio) │ (This package) │ │(Subscription)│
|
|
106
|
-
└─────────────┘ └──────────────────┘ └─────────────┘
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
## License
|
|
110
|
-
|
|
111
|
-
MIT
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
claude_code_acp/__init__.py,sha256=CsH-yvcnqCtSci91gwsWeLBpvhYAX1_61QrBSndg2U0,635
|
|
2
|
-
claude_code_acp/__main__.py,sha256=zuAdIOmaCsDeRxj2yIl_qMx-74QFaA3nWiS8gti0og0,118
|
|
3
|
-
claude_code_acp/agent.py,sha256=qos32gnMAilOlz6ebllkmuJZS3UgpbAtAZw58r3SYig,20619
|
|
4
|
-
claude_code_acp-0.1.0.dist-info/METADATA,sha256=aaNmQTUTBgMVFfWKZoUxyedNUkwVc0sLjf-3DIGAOMM,3383
|
|
5
|
-
claude_code_acp-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
6
|
-
claude_code_acp-0.1.0.dist-info/entry_points.txt,sha256=cc_pkg_V_1zctD5CUqjATCxglkf5-UArEMfN3q9We-A,57
|
|
7
|
-
claude_code_acp-0.1.0.dist-info/licenses/LICENSE,sha256=5NCM9Q9UTfsn-VyafO7htdlYyPPO8H-NHYrO5UV9sT4,1064
|
|
8
|
-
claude_code_acp-0.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|