claude-code-acp 0.1.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.
@@ -0,0 +1,29 @@
1
+ """
2
+ Claude Code ACP - ACP-compatible agent for Claude Code (Python version).
3
+
4
+ This package bridges the Claude Agent SDK with the Agent Client Protocol (ACP),
5
+ allowing Claude Code to work with any ACP-compatible client like Zed, Neovim, etc.
6
+ """
7
+
8
+ import asyncio
9
+
10
+ from .agent import ClaudeAcpAgent
11
+
12
+ __version__ = "0.1.0"
13
+
14
+ __all__ = ["ClaudeAcpAgent", "main", "run"]
15
+
16
+
17
+ async def run() -> None:
18
+ """Run the Claude ACP agent."""
19
+ from acp import run_agent
20
+ await run_agent(ClaudeAcpAgent())
21
+
22
+
23
+ def main() -> None:
24
+ """Entry point for the claude-code-acp command."""
25
+ asyncio.run(run())
26
+
27
+
28
+ if __name__ == "__main__":
29
+ main()
@@ -0,0 +1,6 @@
1
+ """Entry point for running claude-code-acp as a module."""
2
+
3
+ from . import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,615 @@
1
+ """
2
+ Claude ACP Agent - Bridges Claude Agent SDK with ACP protocol.
3
+
4
+ This module implements the ACP Agent interface, converting Claude SDK
5
+ messages to ACP session updates for bidirectional communication.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ from dataclasses import dataclass, field
12
+ from typing import Any
13
+ from uuid import uuid4
14
+
15
+ from acp import (
16
+ Agent,
17
+ InitializeResponse,
18
+ NewSessionResponse,
19
+ PromptResponse,
20
+ SetSessionModeResponse,
21
+ start_tool_call,
22
+ text_block,
23
+ update_agent_message,
24
+ update_agent_thought,
25
+ update_tool_call,
26
+ )
27
+ from acp.interfaces import Client
28
+ from acp.schema import (
29
+ AgentCapabilities,
30
+ AudioContentBlock,
31
+ ClientCapabilities,
32
+ EmbeddedResourceContentBlock,
33
+ HttpMcpServer,
34
+ ImageContentBlock,
35
+ Implementation,
36
+ McpServerStdio,
37
+ PermissionOption,
38
+ PromptCapabilities,
39
+ ResourceContentBlock,
40
+ SessionCapabilities,
41
+ SseMcpServer,
42
+ TextContentBlock,
43
+ )
44
+
45
+ from claude_agent_sdk import (
46
+ AssistantMessage,
47
+ ClaudeAgentOptions,
48
+ ClaudeSDKClient,
49
+ Message,
50
+ PermissionMode,
51
+ PermissionResultAllow,
52
+ PermissionResultDeny,
53
+ ResultMessage,
54
+ SystemMessage,
55
+ TextBlock,
56
+ ThinkingBlock,
57
+ ToolPermissionContext,
58
+ ToolResultBlock,
59
+ ToolUseBlock,
60
+ UserMessage,
61
+ )
62
+ from claude_agent_sdk.types import StreamEvent
63
+
64
+ logger = logging.getLogger(__name__)
65
+
66
+
67
+ @dataclass
68
+ class Session:
69
+ """Represents an active Claude session."""
70
+
71
+ session_id: str
72
+ cwd: str
73
+ permission_mode: PermissionMode = "default"
74
+ cancelled: bool = False
75
+ tool_use_cache: dict[str, ToolUseBlock] = field(default_factory=dict)
76
+
77
+
78
+ class ClaudeAcpAgent(Agent):
79
+ """
80
+ ACP Agent implementation that bridges Claude Agent SDK with ACP protocol.
81
+
82
+ This agent:
83
+ 1. Receives ACP requests from clients (Zed, Neovim, etc.)
84
+ 2. Converts them to Claude Agent SDK format
85
+ 3. Streams Claude responses back as ACP session updates
86
+ 4. Handles bidirectional communication (permissions, file ops, etc.)
87
+ """
88
+
89
+ def __init__(self) -> None:
90
+ self._conn: Client | None = None
91
+ self._sessions: dict[str, Session] = {}
92
+
93
+ def on_connect(self, conn: Client) -> None:
94
+ """Called when an ACP client connects."""
95
+ self._conn = conn
96
+ logger.info("ACP client connected")
97
+
98
+ async def initialize(
99
+ self,
100
+ protocol_version: int,
101
+ client_capabilities: ClientCapabilities | None = None,
102
+ client_info: Implementation | None = None,
103
+ **kwargs: Any,
104
+ ) -> InitializeResponse:
105
+ """Handle ACP initialize request."""
106
+ logger.info(f"Initialize request from {client_info}")
107
+
108
+ return InitializeResponse(
109
+ protocol_version=protocol_version,
110
+ agent_capabilities=AgentCapabilities(
111
+ prompt_capabilities=PromptCapabilities(
112
+ image=True,
113
+ embedded_context=True,
114
+ ),
115
+ session_capabilities=SessionCapabilities(
116
+ fork={},
117
+ list={},
118
+ resume={},
119
+ ),
120
+ ),
121
+ agent_info=Implementation(
122
+ name="claude-code-acp-py",
123
+ title="Claude Code (Python)",
124
+ version="0.1.0",
125
+ ),
126
+ auth_methods=[
127
+ {
128
+ "id": "claude-login",
129
+ "name": "Log in with Claude Code",
130
+ "description": "Run `claude /login` in the terminal",
131
+ }
132
+ ],
133
+ )
134
+
135
+ async def new_session(
136
+ self,
137
+ cwd: str,
138
+ mcp_servers: list[HttpMcpServer | SseMcpServer | McpServerStdio],
139
+ **kwargs: Any,
140
+ ) -> NewSessionResponse:
141
+ """Create a new Claude session."""
142
+ session_id = str(uuid4())
143
+
144
+ self._sessions[session_id] = Session(
145
+ session_id=session_id,
146
+ cwd=cwd,
147
+ )
148
+
149
+ logger.info(f"New session created: {session_id} in {cwd}")
150
+
151
+ return NewSessionResponse(
152
+ session_id=session_id,
153
+ modes={
154
+ "current_mode_id": "default",
155
+ "available_modes": [
156
+ {
157
+ "id": "default",
158
+ "name": "Default",
159
+ "description": "Standard behavior, prompts for dangerous operations",
160
+ },
161
+ {
162
+ "id": "acceptEdits",
163
+ "name": "Accept Edits",
164
+ "description": "Auto-accept file edit operations",
165
+ },
166
+ {
167
+ "id": "plan",
168
+ "name": "Plan Mode",
169
+ "description": "Planning mode, no actual tool execution",
170
+ },
171
+ {
172
+ "id": "bypassPermissions",
173
+ "name": "Bypass Permissions",
174
+ "description": "Bypass all permission checks",
175
+ },
176
+ ],
177
+ },
178
+ )
179
+
180
+ async def set_session_mode(
181
+ self, mode_id: str, session_id: str, **kwargs: Any
182
+ ) -> SetSessionModeResponse | None:
183
+ """Change the permission mode for a session."""
184
+ if session_id not in self._sessions:
185
+ raise ValueError(f"Session not found: {session_id}")
186
+
187
+ valid_modes = ["default", "acceptEdits", "plan", "bypassPermissions", "dontAsk"]
188
+ if mode_id not in valid_modes:
189
+ raise ValueError(f"Invalid mode: {mode_id}")
190
+
191
+ self._sessions[session_id].permission_mode = mode_id # type: ignore
192
+ logger.info(f"Session {session_id} mode changed to {mode_id}")
193
+
194
+ return SetSessionModeResponse()
195
+
196
+ async def prompt(
197
+ self,
198
+ prompt: list[
199
+ TextContentBlock
200
+ | ImageContentBlock
201
+ | AudioContentBlock
202
+ | ResourceContentBlock
203
+ | EmbeddedResourceContentBlock
204
+ ],
205
+ session_id: str,
206
+ **kwargs: Any,
207
+ ) -> PromptResponse:
208
+ """
209
+ Handle a prompt from the ACP client.
210
+
211
+ This is the main method that:
212
+ 1. Converts ACP prompt to Claude format
213
+ 2. Streams Claude responses via ClaudeSDKClient
214
+ 3. Converts Claude messages to ACP updates
215
+ """
216
+ if session_id not in self._sessions:
217
+ raise ValueError(f"Session not found: {session_id}")
218
+
219
+ session = self._sessions[session_id]
220
+ session.cancelled = False
221
+
222
+ # Convert ACP prompt to text
223
+ prompt_text = self._convert_prompt_to_text(prompt)
224
+
225
+ logger.info(f"Prompt for session {session_id}: {prompt_text[:100]}...")
226
+
227
+ # Build Claude options with permission callback for bidirectional communication
228
+ options = ClaudeAgentOptions(
229
+ cwd=session.cwd,
230
+ permission_mode=session.permission_mode,
231
+ include_partial_messages=True,
232
+ )
233
+
234
+ # Add permission callback if not bypassing permissions
235
+ if session.permission_mode != "bypassPermissions":
236
+ options = ClaudeAgentOptions(
237
+ cwd=session.cwd,
238
+ permission_mode=session.permission_mode,
239
+ include_partial_messages=True,
240
+ can_use_tool=self._create_permission_handler(session_id),
241
+ )
242
+
243
+ try:
244
+ # Use ClaudeSDKClient for streaming mode (required for can_use_tool callback)
245
+ async with ClaudeSDKClient(options) as client:
246
+ # Send the query
247
+ await client.query(prompt_text)
248
+
249
+ # Receive and process messages
250
+ async for message in client.receive_response():
251
+ if session.cancelled:
252
+ await client.interrupt()
253
+ return PromptResponse(stop_reason="cancelled")
254
+
255
+ await self._handle_message(session_id, message)
256
+
257
+ except Exception as e:
258
+ logger.error(f"Error in prompt: {e}")
259
+ raise
260
+
261
+ return PromptResponse(stop_reason="end_turn")
262
+
263
+ async def cancel(self, session_id: str, **kwargs: Any) -> None:
264
+ """Cancel the current operation for a session."""
265
+ if session_id in self._sessions:
266
+ self._sessions[session_id].cancelled = True
267
+ logger.info(f"Session {session_id} cancelled")
268
+
269
+ # --- Conversion Methods ---
270
+
271
+ def _convert_prompt_to_text(
272
+ self,
273
+ prompt: list[
274
+ TextContentBlock
275
+ | ImageContentBlock
276
+ | AudioContentBlock
277
+ | ResourceContentBlock
278
+ | EmbeddedResourceContentBlock
279
+ ],
280
+ ) -> str:
281
+ """Convert ACP prompt blocks to text for Claude."""
282
+ parts = []
283
+
284
+ for block in prompt:
285
+ if isinstance(block, dict):
286
+ block_type = block.get("type")
287
+ if block_type == "text":
288
+ parts.append(block.get("text", ""))
289
+ elif block_type == "resource":
290
+ resource = block.get("resource", {})
291
+ if "text" in resource:
292
+ uri = resource.get("uri", "unknown")
293
+ parts.append(f"\n<context ref=\"{uri}\">\n{resource['text']}\n</context>")
294
+ elif block_type == "resource_link":
295
+ uri = block.get("uri", "")
296
+ name = block.get("name", uri.split("/")[-1])
297
+ parts.append(f"[@{name}]({uri})")
298
+ elif hasattr(block, "text"):
299
+ parts.append(block.text)
300
+
301
+ return "\n".join(parts)
302
+
303
+ async def _handle_message(self, session_id: str, message: Message) -> None:
304
+ """Convert and emit a Claude message as ACP updates."""
305
+ if self._conn is None:
306
+ return
307
+
308
+ session = self._sessions.get(session_id)
309
+ if session is None:
310
+ return
311
+
312
+ if isinstance(message, AssistantMessage):
313
+ await self._handle_assistant_message(session_id, message, session)
314
+
315
+ elif isinstance(message, StreamEvent):
316
+ await self._handle_stream_event(session_id, message)
317
+
318
+ elif isinstance(message, SystemMessage):
319
+ # System messages (init, status, etc.) - log but don't emit
320
+ logger.debug(f"System message: {message.subtype}")
321
+
322
+ elif isinstance(message, ResultMessage):
323
+ # Result message - session complete
324
+ logger.info(f"Session {session_id} completed: {message.subtype}")
325
+
326
+ elif isinstance(message, UserMessage):
327
+ # User messages from history - usually skip
328
+ pass
329
+
330
+ async def _handle_assistant_message(
331
+ self, session_id: str, message: AssistantMessage, session: Session
332
+ ) -> None:
333
+ """Handle an assistant message from Claude."""
334
+ if self._conn is None:
335
+ return
336
+
337
+ for block in message.content:
338
+ if isinstance(block, TextBlock):
339
+ # Text content
340
+ await self._conn.session_update(
341
+ session_id,
342
+ update_agent_message(text_block(block.text)),
343
+ )
344
+
345
+ elif isinstance(block, ThinkingBlock):
346
+ # Thinking/reasoning content
347
+ await self._conn.session_update(
348
+ session_id,
349
+ update_agent_thought(text_block(block.thinking)),
350
+ )
351
+
352
+ elif isinstance(block, ToolUseBlock):
353
+ # Tool invocation
354
+ session.tool_use_cache[block.id] = block
355
+
356
+ await self._conn.session_update(
357
+ session_id,
358
+ start_tool_call(
359
+ tool_call_id=block.id,
360
+ title=self._get_tool_title(block.name, block.input),
361
+ status="pending",
362
+ raw_input=block.input,
363
+ ),
364
+ )
365
+
366
+ elif isinstance(block, ToolResultBlock):
367
+ # Tool result
368
+ status = "failed" if block.is_error else "completed"
369
+
370
+ await self._conn.session_update(
371
+ session_id,
372
+ update_tool_call(
373
+ tool_call_id=block.tool_use_id,
374
+ status=status,
375
+ raw_output=block.content,
376
+ ),
377
+ )
378
+
379
+ async def _handle_stream_event(self, session_id: str, event: StreamEvent) -> None:
380
+ """Handle a streaming event from Claude."""
381
+ if self._conn is None:
382
+ return
383
+
384
+ event_data = event.event
385
+ event_type = event_data.get("type")
386
+
387
+ if event_type == "content_block_delta":
388
+ delta = event_data.get("delta", {})
389
+ delta_type = delta.get("type")
390
+
391
+ if delta_type == "text_delta":
392
+ text = delta.get("text", "")
393
+ if text:
394
+ await self._conn.session_update(
395
+ session_id,
396
+ update_agent_message(text_block(text)),
397
+ )
398
+
399
+ elif delta_type == "thinking_delta":
400
+ thinking = delta.get("thinking", "")
401
+ if thinking:
402
+ await self._conn.session_update(
403
+ session_id,
404
+ update_agent_thought(text_block(thinking)),
405
+ )
406
+
407
+ def _get_tool_title(self, tool_name: str, tool_input: dict[str, Any]) -> str:
408
+ """Generate a human-readable title for a tool call."""
409
+ if tool_name == "Read":
410
+ path = tool_input.get("file_path", tool_input.get("path", ""))
411
+ return f"Read {path}"
412
+ elif tool_name in ["Write", "Edit"]:
413
+ path = tool_input.get("file_path", tool_input.get("path", ""))
414
+ return f"{tool_name} {path}"
415
+ elif tool_name == "Bash":
416
+ cmd = tool_input.get("command", "")
417
+ return f"Run: {cmd[:50]}..." if len(cmd) > 50 else f"Run: {cmd}"
418
+ elif tool_name == "Glob":
419
+ pattern = tool_input.get("pattern", "")
420
+ return f"Find files: {pattern}"
421
+ elif tool_name == "Grep":
422
+ pattern = tool_input.get("pattern", "")
423
+ return f"Search: {pattern}"
424
+ else:
425
+ return tool_name
426
+
427
+ def _create_permission_handler(self, session_id: str):
428
+ """Create a permission handler for bidirectional permission requests."""
429
+
430
+ async def can_use_tool(
431
+ tool_name: str,
432
+ tool_input: dict[str, Any],
433
+ context: ToolPermissionContext,
434
+ ) -> PermissionResultAllow | PermissionResultDeny:
435
+ logger.info(f"🔧 Permission requested for tool: {tool_name}")
436
+
437
+ if self._conn is None:
438
+ logger.warning("No ACP connection for permission request")
439
+ return PermissionResultDeny(message="No ACP connection")
440
+
441
+ session = self._sessions.get(session_id)
442
+ if session is None:
443
+ return PermissionResultDeny(message="Session not found")
444
+
445
+ # For certain modes, auto-allow
446
+ if session.permission_mode == "bypassPermissions":
447
+ return PermissionResultAllow()
448
+
449
+ if session.permission_mode == "acceptEdits" and tool_name in [
450
+ "Write",
451
+ "Edit",
452
+ "MultiEdit",
453
+ ]:
454
+ return PermissionResultAllow()
455
+
456
+ # Request permission from ACP client
457
+ tool_use_id = str(uuid4())
458
+
459
+ response = await self._conn.request_permission(
460
+ options=[
461
+ PermissionOption(
462
+ kind="allow_always",
463
+ name="Always Allow",
464
+ option_id="allow_always",
465
+ ),
466
+ PermissionOption(
467
+ kind="allow_once",
468
+ name="Allow",
469
+ option_id="allow",
470
+ ),
471
+ PermissionOption(
472
+ kind="reject_once",
473
+ name="Reject",
474
+ option_id="reject",
475
+ ),
476
+ ],
477
+ session_id=session_id,
478
+ tool_call={
479
+ "tool_call_id": tool_use_id,
480
+ "title": self._get_tool_title(tool_name, tool_input),
481
+ "raw_input": tool_input,
482
+ },
483
+ )
484
+
485
+ outcome = response.get("outcome", {})
486
+ if outcome.get("outcome") == "selected":
487
+ option_id = outcome.get("option_id")
488
+ if option_id in ["allow", "allow_always"]:
489
+ return PermissionResultAllow()
490
+
491
+ return PermissionResultDeny(message="User denied permission")
492
+
493
+ return can_use_tool
494
+
495
+ # --- Additional ACP Methods ---
496
+
497
+ async def list_sessions(
498
+ self,
499
+ cursor: str | None = None,
500
+ cwd: str | None = None,
501
+ **kwargs: Any,
502
+ ):
503
+ """List available sessions."""
504
+ from acp.schema import ListSessionsResponse, SessionInfo
505
+
506
+ sessions = []
507
+ for session_id, session in self._sessions.items():
508
+ if cwd is None or session.cwd == cwd:
509
+ sessions.append(
510
+ SessionInfo(
511
+ sessionId=session_id,
512
+ cwd=session.cwd,
513
+ )
514
+ )
515
+
516
+ return ListSessionsResponse(sessions=sessions)
517
+
518
+ async def load_session(
519
+ self,
520
+ cwd: str,
521
+ mcp_servers: list,
522
+ session_id: str,
523
+ **kwargs: Any,
524
+ ):
525
+ """Load an existing session."""
526
+ from acp.schema import LoadSessionResponse
527
+
528
+ if session_id not in self._sessions:
529
+ return None
530
+
531
+ session = self._sessions[session_id]
532
+ session.cwd = cwd
533
+
534
+ return LoadSessionResponse()
535
+
536
+ async def fork_session(
537
+ self,
538
+ cwd: str,
539
+ session_id: str,
540
+ mcp_servers: list | None = None,
541
+ **kwargs: Any,
542
+ ):
543
+ """Fork an existing session."""
544
+ from acp.schema import ForkSessionResponse
545
+
546
+ if session_id not in self._sessions:
547
+ raise ValueError(f"Session not found: {session_id}")
548
+
549
+ new_session_id = str(uuid4())
550
+ old_session = self._sessions[session_id]
551
+
552
+ self._sessions[new_session_id] = Session(
553
+ session_id=new_session_id,
554
+ cwd=cwd,
555
+ permission_mode=old_session.permission_mode,
556
+ )
557
+
558
+ logger.info(f"Forked session {session_id} to {new_session_id}")
559
+
560
+ return ForkSessionResponse(session_id=new_session_id)
561
+
562
+ async def resume_session(
563
+ self,
564
+ cwd: str,
565
+ session_id: str,
566
+ mcp_servers: list | None = None,
567
+ **kwargs: Any,
568
+ ):
569
+ """Resume an existing session."""
570
+ from acp.schema import ResumeSessionResponse
571
+
572
+ if session_id not in self._sessions:
573
+ raise ValueError(f"Session not found: {session_id}")
574
+
575
+ session = self._sessions[session_id]
576
+ session.cwd = cwd
577
+ session.cancelled = False
578
+
579
+ logger.info(f"Resumed session {session_id}")
580
+
581
+ return ResumeSessionResponse()
582
+
583
+ async def authenticate(self, method_id: str, **kwargs: Any):
584
+ """Handle authentication requests."""
585
+ from acp.schema import AuthenticateResponse
586
+
587
+ logger.info(f"Authentication requested: {method_id}")
588
+
589
+ # For Claude login, the user needs to run `claude /login` in terminal
590
+ # The AuthenticateResponse is empty per ACP spec - auth status is handled differently
591
+ return AuthenticateResponse()
592
+
593
+ async def set_session_model(
594
+ self,
595
+ model_id: str,
596
+ session_id: str,
597
+ **kwargs: Any,
598
+ ):
599
+ """Set the model for a session (stub - Claude CLI handles model selection)."""
600
+ from acp.schema import SetSessionModelResponse
601
+
602
+ logger.info(f"Model change requested for session {session_id}: {model_id}")
603
+ # Note: Claude CLI handles model selection, this is just for compatibility
604
+ return SetSessionModelResponse()
605
+
606
+ # --- Extension Methods ---
607
+
608
+ async def ext_method(self, method: str, params: dict[str, Any]) -> dict[str, Any]:
609
+ """Handle extension method calls."""
610
+ logger.info(f"Extension method: {method}")
611
+ return {"status": "ok"}
612
+
613
+ async def ext_notification(self, method: str, params: dict[str, Any]) -> None:
614
+ """Handle extension notifications."""
615
+ logger.info(f"Extension notification: {method}")
@@ -0,0 +1,111 @@
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
@@ -0,0 +1,8 @@
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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ claude-code-acp = claude_code_acp:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 yazelin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.