claude-code-acp 0.1.0__tar.gz → 0.2.0__tar.gz
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-0.1.0 → claude_code_acp-0.2.0}/PKG-INFO +1 -1
- {claude_code_acp-0.1.0 → claude_code_acp-0.2.0}/pyproject.toml +1 -1
- {claude_code_acp-0.1.0 → claude_code_acp-0.2.0}/src/claude_code_acp/__init__.py +2 -1
- claude_code_acp-0.2.0/src/claude_code_acp/client.py +317 -0
- {claude_code_acp-0.1.0 → claude_code_acp-0.2.0}/.github/workflows/publish.yml +0 -0
- {claude_code_acp-0.1.0 → claude_code_acp-0.2.0}/.gitignore +0 -0
- {claude_code_acp-0.1.0 → claude_code_acp-0.2.0}/LICENSE +0 -0
- {claude_code_acp-0.1.0 → claude_code_acp-0.2.0}/README.md +0 -0
- {claude_code_acp-0.1.0 → claude_code_acp-0.2.0}/src/claude_code_acp/__main__.py +0 -0
- {claude_code_acp-0.1.0 → claude_code_acp-0.2.0}/src/claude_code_acp/agent.py +0 -0
- {claude_code_acp-0.1.0 → claude_code_acp-0.2.0}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: claude-code-acp
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: ACP-compatible agent for Claude Code (Python version)
|
|
5
5
|
Project-URL: Homepage, https://github.com/yazelin/claude-code-acp-py
|
|
6
6
|
Project-URL: Repository, https://github.com/yazelin/claude-code-acp-py
|
|
@@ -8,10 +8,11 @@ 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
|
+
from .client import ClaudeClient, ClaudeEvents
|
|
11
12
|
|
|
12
13
|
__version__ = "0.1.0"
|
|
13
14
|
|
|
14
|
-
__all__ = ["ClaudeAcpAgent", "main", "run"]
|
|
15
|
+
__all__ = ["ClaudeAcpAgent", "ClaudeClient", "ClaudeEvents", "main", "run"]
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
async def run() -> None:
|
|
@@ -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"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|