claude-code-acp 0.1.0__py3-none-any.whl → 0.2.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,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"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-code-acp
3
- Version: 0.1.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
@@ -0,0 +1,9 @@
1
+ claude_code_acp/__init__.py,sha256=gNo2Xfk5Ky94X-LZ-YGIC6XG1i-seRf-o8eoGEwYqgA,714
2
+ claude_code_acp/__main__.py,sha256=zuAdIOmaCsDeRxj2yIl_qMx-74QFaA3nWiS8gti0og0,118
3
+ claude_code_acp/agent.py,sha256=qos32gnMAilOlz6ebllkmuJZS3UgpbAtAZw58r3SYig,20619
4
+ claude_code_acp/client.py,sha256=jKi6tPNqNu0GWTsXxVa1BREGE1wkm_kbIEDulXKGDHE,10680
5
+ claude_code_acp-0.2.0.dist-info/METADATA,sha256=zD0xhBgZRcwKzRNh3HL4OqFW48Ri3VEMlWG0KseoQSk,3383
6
+ claude_code_acp-0.2.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
7
+ claude_code_acp-0.2.0.dist-info/entry_points.txt,sha256=cc_pkg_V_1zctD5CUqjATCxglkf5-UArEMfN3q9We-A,57
8
+ claude_code_acp-0.2.0.dist-info/licenses/LICENSE,sha256=5NCM9Q9UTfsn-VyafO7htdlYyPPO8H-NHYrO5UV9sT4,1064
9
+ claude_code_acp-0.2.0.dist-info/RECORD,,
@@ -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,,