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.
@@ -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
- __version__ = "0.1.0"
13
-
14
- __all__ = ["ClaudeAcpAgent", "main", "run"]
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
+ [![PyPI](https://img.shields.io/pypi/v/claude-code-acp)](https://pypi.org/project/claude-code-acp/)
27
+ [![Python](https://img.shields.io/pypi/pyversions/claude-code-acp)](https://pypi.org/project/claude-code-acp/)
28
+ [![License](https://img.shields.io/github/license/yazelin/claude-code-acp-py)](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
- [![PyPI](https://img.shields.io/pypi/v/claude-code-acp)](https://pypi.org/project/claude-code-acp/)
27
- [![Python](https://img.shields.io/pypi/pyversions/claude-code-acp)](https://pypi.org/project/claude-code-acp/)
28
- [![License](https://img.shields.io/github/license/yazelin/claude-code-acp-py)](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,,