claudestream 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.
- claudestream/__init__.py +120 -0
- claudestream/_async_session.py +290 -0
- claudestream/_cli.py +208 -0
- claudestream/_process.py +240 -0
- claudestream/_protocol.py +328 -0
- claudestream/_sync_session.py +188 -0
- claudestream/events.py +260 -0
- claudestream/messages.py +105 -0
- claudestream/policy.py +152 -0
- claudestream-0.1.0.dist-info/METADATA +11 -0
- claudestream-0.1.0.dist-info/RECORD +14 -0
- claudestream-0.1.0.dist-info/WHEEL +4 -0
- claudestream-0.1.0.dist-info/entry_points.txt +2 -0
- claudestream-0.1.0.dist-info/licenses/LICENSE +21 -0
claudestream/__init__.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""A Python library and CLI for streaming Claude Code's JSON protocol."""
|
|
2
|
+
|
|
3
|
+
from claudestream._async_session import AsyncSession, ClaudeStreamError
|
|
4
|
+
from claudestream._sync_session import SyncSession
|
|
5
|
+
from claudestream.events import (
|
|
6
|
+
AssistantMessage,
|
|
7
|
+
AssistantText,
|
|
8
|
+
ApiRetry,
|
|
9
|
+
CompactBoundary,
|
|
10
|
+
ContentBlock,
|
|
11
|
+
Event,
|
|
12
|
+
McpRequest,
|
|
13
|
+
PermissionRequest,
|
|
14
|
+
RateLimit,
|
|
15
|
+
Result,
|
|
16
|
+
StreamDelta,
|
|
17
|
+
SystemInit,
|
|
18
|
+
TextBlock,
|
|
19
|
+
Thinking,
|
|
20
|
+
ThinkingBlock,
|
|
21
|
+
ToolResult,
|
|
22
|
+
ToolResultBlock,
|
|
23
|
+
ToolResultMessage,
|
|
24
|
+
ToolUse,
|
|
25
|
+
ToolUseBlock,
|
|
26
|
+
UnknownEvent,
|
|
27
|
+
Usage,
|
|
28
|
+
)
|
|
29
|
+
from claudestream.policy import (
|
|
30
|
+
Allow,
|
|
31
|
+
AllowAllPolicy,
|
|
32
|
+
AllowBuiltinsPolicy,
|
|
33
|
+
AllowListPolicy,
|
|
34
|
+
CallbackPolicy,
|
|
35
|
+
Deny,
|
|
36
|
+
Policy,
|
|
37
|
+
allow_all,
|
|
38
|
+
allow_builtins,
|
|
39
|
+
allow_list,
|
|
40
|
+
callback,
|
|
41
|
+
deny_all,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
# Sessions
|
|
46
|
+
"AsyncSession",
|
|
47
|
+
"SyncSession",
|
|
48
|
+
"ClaudeStreamError",
|
|
49
|
+
# Events
|
|
50
|
+
"Event",
|
|
51
|
+
"SystemInit",
|
|
52
|
+
"ApiRetry",
|
|
53
|
+
"CompactBoundary",
|
|
54
|
+
"AssistantMessage",
|
|
55
|
+
"AssistantText",
|
|
56
|
+
"ToolResultMessage",
|
|
57
|
+
"ToolUse",
|
|
58
|
+
"ToolResult",
|
|
59
|
+
"Thinking",
|
|
60
|
+
"StreamDelta",
|
|
61
|
+
"Result",
|
|
62
|
+
"RateLimit",
|
|
63
|
+
"PermissionRequest",
|
|
64
|
+
"McpRequest",
|
|
65
|
+
"UnknownEvent",
|
|
66
|
+
# Content blocks
|
|
67
|
+
"TextBlock",
|
|
68
|
+
"ToolUseBlock",
|
|
69
|
+
"ThinkingBlock",
|
|
70
|
+
"ToolResultBlock",
|
|
71
|
+
"ContentBlock",
|
|
72
|
+
"Usage",
|
|
73
|
+
# Policy
|
|
74
|
+
"Policy",
|
|
75
|
+
"Allow",
|
|
76
|
+
"Deny",
|
|
77
|
+
"AllowAllPolicy",
|
|
78
|
+
"AllowBuiltinsPolicy",
|
|
79
|
+
"AllowListPolicy",
|
|
80
|
+
"CallbackPolicy",
|
|
81
|
+
"allow_all",
|
|
82
|
+
"deny_all",
|
|
83
|
+
"allow_builtins",
|
|
84
|
+
"allow_list",
|
|
85
|
+
"callback",
|
|
86
|
+
# Convenience
|
|
87
|
+
"print_prompt",
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def print_prompt(
|
|
92
|
+
prompt: str,
|
|
93
|
+
*,
|
|
94
|
+
model: str | None = None,
|
|
95
|
+
cwd: str | None = None,
|
|
96
|
+
binary: str | None = None,
|
|
97
|
+
policy: Policy | None = None,
|
|
98
|
+
system_prompt: str | None = None,
|
|
99
|
+
extra_args: list[str] | None = None,
|
|
100
|
+
env: dict[str, str] | None = None,
|
|
101
|
+
) -> str:
|
|
102
|
+
"""One-shot convenience: send a prompt and return the full response text.
|
|
103
|
+
|
|
104
|
+
Creates a SyncSession, sends one message, collects AssistantText events,
|
|
105
|
+
and returns the concatenated text. For claudewheel integration.
|
|
106
|
+
"""
|
|
107
|
+
parts: list[str] = []
|
|
108
|
+
with SyncSession(
|
|
109
|
+
model=model,
|
|
110
|
+
cwd=cwd,
|
|
111
|
+
binary=binary,
|
|
112
|
+
policy=policy,
|
|
113
|
+
system_prompt=system_prompt,
|
|
114
|
+
extra_args=extra_args,
|
|
115
|
+
env=env,
|
|
116
|
+
) as session:
|
|
117
|
+
for event in session.send(prompt):
|
|
118
|
+
if isinstance(event, AssistantText):
|
|
119
|
+
parts.append(event.text)
|
|
120
|
+
return "".join(parts)
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"""Async session for Claude Code stream-json protocol."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
from collections.abc import AsyncIterator
|
|
8
|
+
from typing import Any, Callable
|
|
9
|
+
|
|
10
|
+
from claudestream.events import (
|
|
11
|
+
Event,
|
|
12
|
+
McpRequest,
|
|
13
|
+
PermissionRequest,
|
|
14
|
+
Result,
|
|
15
|
+
SystemInit,
|
|
16
|
+
UnknownEvent,
|
|
17
|
+
)
|
|
18
|
+
from claudestream.messages import AllowPermission, DenyPermission, UserMessage
|
|
19
|
+
from claudestream.policy import Allow, Deny, Policy, policy_to_flags
|
|
20
|
+
from claudestream._process import ProcessConfig, ProcessManager, find_binary, check_version
|
|
21
|
+
from claudestream._protocol import flatten_event, read_events, write_message
|
|
22
|
+
|
|
23
|
+
log = logging.getLogger("claudestream")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ClaudeStreamError(Exception):
|
|
27
|
+
"""Raised when the Claude Code subprocess fails."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, message: str, exit_code: int | None = None):
|
|
30
|
+
super().__init__(message)
|
|
31
|
+
self.exit_code = exit_code
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AsyncSession:
|
|
35
|
+
"""Async session managing a Claude Code subprocess.
|
|
36
|
+
|
|
37
|
+
Usage::
|
|
38
|
+
|
|
39
|
+
async with AsyncSession(model="sonnet") as session:
|
|
40
|
+
async for event in session.send("hello"):
|
|
41
|
+
print(event)
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
*,
|
|
47
|
+
model: str | None = None,
|
|
48
|
+
cwd: str | None = None,
|
|
49
|
+
binary: str | None = None,
|
|
50
|
+
policy: Policy | None = None,
|
|
51
|
+
system_prompt: str | None = None,
|
|
52
|
+
extra_args: list[str] | None = None,
|
|
53
|
+
env: dict[str, str] | None = None,
|
|
54
|
+
):
|
|
55
|
+
self._binary = find_binary(binary)
|
|
56
|
+
self._policy = policy
|
|
57
|
+
self._callbacks: dict[type, list[Callable]] = {}
|
|
58
|
+
self._active_turn = False
|
|
59
|
+
self._last_result: Result | None = None
|
|
60
|
+
|
|
61
|
+
# Convert policy to CLI flags, then route them to ProcessConfig fields
|
|
62
|
+
policy_flags = policy_to_flags(policy)
|
|
63
|
+
|
|
64
|
+
permission_mode: str | None = None
|
|
65
|
+
permission_prompt_tool: str | None = None
|
|
66
|
+
allowed_tools: list[str] = []
|
|
67
|
+
skip_permissions = False
|
|
68
|
+
|
|
69
|
+
remaining_flags: list[str] = []
|
|
70
|
+
i = 0
|
|
71
|
+
while i < len(policy_flags):
|
|
72
|
+
flag = policy_flags[i]
|
|
73
|
+
if flag == "--dangerously-skip-permissions":
|
|
74
|
+
skip_permissions = True
|
|
75
|
+
i += 1
|
|
76
|
+
elif flag == "--permission-mode":
|
|
77
|
+
permission_mode = policy_flags[i + 1]
|
|
78
|
+
i += 2
|
|
79
|
+
elif flag == "--permission-prompt-tool":
|
|
80
|
+
permission_prompt_tool = policy_flags[i + 1]
|
|
81
|
+
i += 2
|
|
82
|
+
elif flag == "--allowedTools":
|
|
83
|
+
allowed_tools = policy_flags[i + 1].split(",")
|
|
84
|
+
i += 2
|
|
85
|
+
else:
|
|
86
|
+
remaining_flags.append(flag)
|
|
87
|
+
i += 1
|
|
88
|
+
|
|
89
|
+
all_extra = list(extra_args or [])
|
|
90
|
+
if skip_permissions:
|
|
91
|
+
all_extra.append("--dangerously-skip-permissions")
|
|
92
|
+
all_extra.extend(remaining_flags)
|
|
93
|
+
|
|
94
|
+
self._process_mgr = ProcessManager(ProcessConfig(
|
|
95
|
+
binary=self._binary,
|
|
96
|
+
cwd=cwd,
|
|
97
|
+
model=model,
|
|
98
|
+
system_prompt=system_prompt,
|
|
99
|
+
permission_mode=permission_mode,
|
|
100
|
+
allowed_tools=allowed_tools,
|
|
101
|
+
permission_prompt_tool=permission_prompt_tool,
|
|
102
|
+
extra_args=all_extra,
|
|
103
|
+
env=env,
|
|
104
|
+
))
|
|
105
|
+
|
|
106
|
+
# Session metadata (populated from SystemInit)
|
|
107
|
+
self._session_id: str | None = None
|
|
108
|
+
self._model_name: str | None = None
|
|
109
|
+
self._tools: list[str] = []
|
|
110
|
+
self._claude_version: str | None = None
|
|
111
|
+
|
|
112
|
+
# --- Properties ---
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def session_id(self) -> str | None:
|
|
116
|
+
return self._session_id
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def model_name(self) -> str | None:
|
|
120
|
+
return self._model_name
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def tools(self) -> list[str]:
|
|
124
|
+
return self._tools
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def claude_version(self) -> str | None:
|
|
128
|
+
return self._claude_version
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def last_result(self) -> Result | None:
|
|
132
|
+
return self._last_result
|
|
133
|
+
|
|
134
|
+
# --- Context manager ---
|
|
135
|
+
|
|
136
|
+
async def __aenter__(self) -> AsyncSession:
|
|
137
|
+
await self._start()
|
|
138
|
+
return self
|
|
139
|
+
|
|
140
|
+
async def __aexit__(self, *exc_info: Any) -> None:
|
|
141
|
+
await self.close()
|
|
142
|
+
|
|
143
|
+
async def _start(self) -> None:
|
|
144
|
+
"""Start the subprocess and read the SystemInit event."""
|
|
145
|
+
self._claude_version = await check_version(self._binary)
|
|
146
|
+
|
|
147
|
+
await self._process_mgr.start()
|
|
148
|
+
|
|
149
|
+
# Read events until we get SystemInit
|
|
150
|
+
async for event in read_events(self._process_mgr.stdout):
|
|
151
|
+
if isinstance(event, SystemInit):
|
|
152
|
+
self._session_id = event.session_id
|
|
153
|
+
self._model_name = event.model
|
|
154
|
+
self._tools = list(event.tools)
|
|
155
|
+
log.debug(
|
|
156
|
+
"session started: id=%s model=%s tools=%d",
|
|
157
|
+
self._session_id,
|
|
158
|
+
self._model_name,
|
|
159
|
+
len(self._tools),
|
|
160
|
+
)
|
|
161
|
+
break
|
|
162
|
+
elif isinstance(event, UnknownEvent):
|
|
163
|
+
log.debug("unknown event during init: %s", event.type)
|
|
164
|
+
|
|
165
|
+
async def close(self) -> None:
|
|
166
|
+
"""Shut down the session and kill the subprocess."""
|
|
167
|
+
await self._process_mgr.close()
|
|
168
|
+
|
|
169
|
+
# --- Sending messages ---
|
|
170
|
+
|
|
171
|
+
async def send(self, prompt: str, *, raw: bool = False) -> AsyncIterator[Event]:
|
|
172
|
+
"""Send a message and yield events until the turn completes.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
prompt: The message to send.
|
|
176
|
+
raw: If True, yield raw protocol events (AssistantMessage,
|
|
177
|
+
ToolResultMessage). If False (default), yield flattened
|
|
178
|
+
convenience events (AssistantText, ToolUse, etc.).
|
|
179
|
+
|
|
180
|
+
Yields:
|
|
181
|
+
Event objects until a Result event is received.
|
|
182
|
+
|
|
183
|
+
Raises:
|
|
184
|
+
RuntimeError: If called while a previous turn is still active.
|
|
185
|
+
ClaudeStreamError: If the subprocess dies unexpectedly.
|
|
186
|
+
"""
|
|
187
|
+
if self._active_turn:
|
|
188
|
+
raise RuntimeError(
|
|
189
|
+
"Cannot send while a previous turn is active. "
|
|
190
|
+
"Drain the event iterator or wait for the Result event."
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
self._active_turn = True
|
|
194
|
+
self._last_result = None
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
msg = UserMessage(content=prompt, session_id=self._session_id or "")
|
|
198
|
+
await write_message(self._process_mgr.stdin, msg)
|
|
199
|
+
|
|
200
|
+
async for event in self._read_turn(raw=raw):
|
|
201
|
+
yield event
|
|
202
|
+
finally:
|
|
203
|
+
self._active_turn = False
|
|
204
|
+
|
|
205
|
+
async def _read_turn(self, *, raw: bool) -> AsyncIterator[Event]:
|
|
206
|
+
"""Read events for a single turn until Result is received."""
|
|
207
|
+
if not self._process_mgr.is_alive:
|
|
208
|
+
raise ClaudeStreamError(
|
|
209
|
+
"Claude subprocess is not running",
|
|
210
|
+
exit_code=None,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
async for event in read_events(self._process_mgr.stdout):
|
|
214
|
+
# Handle permission requests via policy
|
|
215
|
+
if isinstance(event, PermissionRequest):
|
|
216
|
+
handled = await self._handle_permission(event)
|
|
217
|
+
if handled:
|
|
218
|
+
continue
|
|
219
|
+
|
|
220
|
+
# Flatten or pass through
|
|
221
|
+
if raw:
|
|
222
|
+
events_to_yield = [event]
|
|
223
|
+
else:
|
|
224
|
+
events_to_yield = flatten_event(event)
|
|
225
|
+
|
|
226
|
+
for evt in events_to_yield:
|
|
227
|
+
# Fire callbacks before yielding
|
|
228
|
+
for cb in self._callbacks.get(type(evt), []):
|
|
229
|
+
cb(evt)
|
|
230
|
+
yield evt
|
|
231
|
+
|
|
232
|
+
# Turn completes on Result
|
|
233
|
+
if isinstance(event, Result):
|
|
234
|
+
self._last_result = event
|
|
235
|
+
return
|
|
236
|
+
|
|
237
|
+
# stdout closed without a Result event
|
|
238
|
+
rc = self._process_mgr._process.returncode if self._process_mgr._process else None
|
|
239
|
+
raise ClaudeStreamError(
|
|
240
|
+
"Claude subprocess closed stdout without sending a Result event",
|
|
241
|
+
exit_code=rc,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
async def _handle_permission(self, request: PermissionRequest) -> bool:
|
|
245
|
+
"""Apply policy to a permission request. Returns True if handled."""
|
|
246
|
+
if self._policy is None:
|
|
247
|
+
return False
|
|
248
|
+
|
|
249
|
+
decision = self._policy.decide(request.tool_name, request.tool_input)
|
|
250
|
+
|
|
251
|
+
if decision is None:
|
|
252
|
+
return False
|
|
253
|
+
|
|
254
|
+
if isinstance(decision, Allow):
|
|
255
|
+
updated = decision.updated_input if decision.updated_input else request.tool_input
|
|
256
|
+
msg = AllowPermission(
|
|
257
|
+
request_id=request.request_id,
|
|
258
|
+
updated_input=updated,
|
|
259
|
+
)
|
|
260
|
+
else:
|
|
261
|
+
msg = DenyPermission(
|
|
262
|
+
request_id=request.request_id,
|
|
263
|
+
message=decision.message,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
await write_message(self._process_mgr.stdin, msg)
|
|
267
|
+
return True
|
|
268
|
+
|
|
269
|
+
# --- Callbacks ---
|
|
270
|
+
|
|
271
|
+
def on(self, event_type: type[Event], handler: Callable[[Any], None]) -> None:
|
|
272
|
+
"""Register a callback for a specific event type.
|
|
273
|
+
|
|
274
|
+
The callback fires during iteration, before the event is yielded.
|
|
275
|
+
"""
|
|
276
|
+
if event_type not in self._callbacks:
|
|
277
|
+
self._callbacks[event_type] = []
|
|
278
|
+
self._callbacks[event_type].append(handler)
|
|
279
|
+
|
|
280
|
+
# --- Permission response (for consumer-handled requests) ---
|
|
281
|
+
|
|
282
|
+
async def respond_allow(self, request_id: str, updated_input: dict) -> None:
|
|
283
|
+
"""Allow a permission request that was surfaced to the consumer."""
|
|
284
|
+
msg = AllowPermission(request_id=request_id, updated_input=updated_input)
|
|
285
|
+
await write_message(self._process_mgr.stdin, msg)
|
|
286
|
+
|
|
287
|
+
async def respond_deny(self, request_id: str, message: str = "Denied by user") -> None:
|
|
288
|
+
"""Deny a permission request that was surfaced to the consumer."""
|
|
289
|
+
msg = DenyPermission(request_id=request_id, message=message)
|
|
290
|
+
await write_message(self._process_mgr.stdin, msg)
|
claudestream/_cli.py
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""CLI entry point for claudestream."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
import strictcli
|
|
9
|
+
|
|
10
|
+
from claudestream import (
|
|
11
|
+
AssistantMessage,
|
|
12
|
+
AssistantText,
|
|
13
|
+
AsyncSession,
|
|
14
|
+
ClaudeStreamError,
|
|
15
|
+
Event,
|
|
16
|
+
PermissionRequest,
|
|
17
|
+
RateLimit,
|
|
18
|
+
Result,
|
|
19
|
+
StreamDelta,
|
|
20
|
+
SyncSession,
|
|
21
|
+
Thinking,
|
|
22
|
+
ToolResult,
|
|
23
|
+
ToolUse,
|
|
24
|
+
ToolResultMessage,
|
|
25
|
+
ApiRetry,
|
|
26
|
+
SystemInit,
|
|
27
|
+
UnknownEvent,
|
|
28
|
+
allow_all,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
app = strictcli.App(
|
|
32
|
+
name="claudestream",
|
|
33
|
+
version="0.1.0",
|
|
34
|
+
help="Stream Claude Code's JSON protocol",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# --- send command ---
|
|
39
|
+
|
|
40
|
+
@app.command("send", help="Send a prompt and display the response")
|
|
41
|
+
@strictcli.arg("prompt", help="The prompt to send")
|
|
42
|
+
@strictcli.flag("model", type=str, default="", help="Model to use (e.g. sonnet, opus)", short="m")
|
|
43
|
+
@strictcli.flag("cwd", type=str, default="", help="Working directory for Claude")
|
|
44
|
+
@strictcli.flag("raw", type=bool, default=False, help="Show raw protocol events instead of flattened")
|
|
45
|
+
@strictcli.flag("json-output", type=bool, default=False, help="Output events as JSON lines")
|
|
46
|
+
@strictcli.flag("skip-permissions", type=bool, default=False, help="Skip all permission prompts")
|
|
47
|
+
def cmd_send(
|
|
48
|
+
prompt: str,
|
|
49
|
+
model: str = "",
|
|
50
|
+
cwd: str = "",
|
|
51
|
+
raw: bool = False,
|
|
52
|
+
json_output: bool = False,
|
|
53
|
+
skip_permissions: bool = False,
|
|
54
|
+
) -> None:
|
|
55
|
+
policy = allow_all() if skip_permissions else None
|
|
56
|
+
with SyncSession(
|
|
57
|
+
model=model or None,
|
|
58
|
+
cwd=cwd or None,
|
|
59
|
+
policy=policy,
|
|
60
|
+
) as session:
|
|
61
|
+
for event in session.send(prompt, raw=raw):
|
|
62
|
+
if json_output:
|
|
63
|
+
_print_json(event)
|
|
64
|
+
else:
|
|
65
|
+
_print_event(event)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# --- stream command ---
|
|
69
|
+
|
|
70
|
+
@app.command("stream", help="Stream a prompt with real-time token output")
|
|
71
|
+
@strictcli.arg("prompt", help="The prompt to send")
|
|
72
|
+
@strictcli.flag("model", type=str, default="", help="Model to use", short="m")
|
|
73
|
+
@strictcli.flag("cwd", type=str, default="", help="Working directory for Claude")
|
|
74
|
+
@strictcli.flag("skip-permissions", type=bool, default=False, help="Skip all permission prompts")
|
|
75
|
+
def cmd_stream(
|
|
76
|
+
prompt: str,
|
|
77
|
+
model: str = "",
|
|
78
|
+
cwd: str = "",
|
|
79
|
+
skip_permissions: bool = False,
|
|
80
|
+
) -> None:
|
|
81
|
+
policy = allow_all() if skip_permissions else None
|
|
82
|
+
with SyncSession(
|
|
83
|
+
model=model or None,
|
|
84
|
+
cwd=cwd or None,
|
|
85
|
+
policy=policy,
|
|
86
|
+
) as session:
|
|
87
|
+
for event in session.send(prompt):
|
|
88
|
+
if isinstance(event, StreamDelta) and event.text:
|
|
89
|
+
sys.stdout.write(event.text)
|
|
90
|
+
sys.stdout.flush()
|
|
91
|
+
elif isinstance(event, AssistantText):
|
|
92
|
+
# Fallback if streaming deltas aren't available
|
|
93
|
+
pass
|
|
94
|
+
elif isinstance(event, Result):
|
|
95
|
+
sys.stdout.write("\n")
|
|
96
|
+
sys.stdout.flush()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# --- events command ---
|
|
100
|
+
|
|
101
|
+
@app.command("events", help="Debug: show all raw protocol events")
|
|
102
|
+
@strictcli.arg("prompt", help="The prompt to send")
|
|
103
|
+
@strictcli.flag("model", type=str, default="", help="Model to use", short="m")
|
|
104
|
+
@strictcli.flag("cwd", type=str, default="", help="Working directory for Claude")
|
|
105
|
+
@strictcli.flag("skip-permissions", type=bool, default=False, help="Skip all permission prompts")
|
|
106
|
+
def cmd_events(
|
|
107
|
+
prompt: str,
|
|
108
|
+
model: str = "",
|
|
109
|
+
cwd: str = "",
|
|
110
|
+
skip_permissions: bool = False,
|
|
111
|
+
) -> None:
|
|
112
|
+
policy = allow_all() if skip_permissions else None
|
|
113
|
+
with SyncSession(
|
|
114
|
+
model=model or None,
|
|
115
|
+
cwd=cwd or None,
|
|
116
|
+
policy=policy,
|
|
117
|
+
) as session:
|
|
118
|
+
for event in session.send(prompt, raw=True):
|
|
119
|
+
_print_json(event)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# --- repl command ---
|
|
123
|
+
|
|
124
|
+
@app.command("repl", help="Interactive multi-turn REPL")
|
|
125
|
+
@strictcli.flag("model", type=str, default="", help="Model to use", short="m")
|
|
126
|
+
@strictcli.flag("cwd", type=str, default="", help="Working directory for Claude")
|
|
127
|
+
@strictcli.flag("skip-permissions", type=bool, default=False, help="Skip all permission prompts")
|
|
128
|
+
def cmd_repl(
|
|
129
|
+
model: str = "",
|
|
130
|
+
cwd: str = "",
|
|
131
|
+
skip_permissions: bool = False,
|
|
132
|
+
) -> None:
|
|
133
|
+
policy = allow_all() if skip_permissions else None
|
|
134
|
+
try:
|
|
135
|
+
with SyncSession(
|
|
136
|
+
model=model or None,
|
|
137
|
+
cwd=cwd or None,
|
|
138
|
+
policy=policy,
|
|
139
|
+
) as session:
|
|
140
|
+
print(f"claudestream repl (model: {session.model_name or 'default'})")
|
|
141
|
+
print("Type your prompts. Ctrl-D to exit.\n")
|
|
142
|
+
while True:
|
|
143
|
+
try:
|
|
144
|
+
prompt = input("> ")
|
|
145
|
+
except EOFError:
|
|
146
|
+
print("\nBye.")
|
|
147
|
+
break
|
|
148
|
+
if not prompt.strip():
|
|
149
|
+
continue
|
|
150
|
+
for event in session.send(prompt):
|
|
151
|
+
if isinstance(event, AssistantText):
|
|
152
|
+
sys.stdout.write(event.text)
|
|
153
|
+
sys.stdout.flush()
|
|
154
|
+
elif isinstance(event, ToolUse):
|
|
155
|
+
print(f"\n[tool: {event.name}]")
|
|
156
|
+
elif isinstance(event, ToolResult):
|
|
157
|
+
content = event.content if isinstance(event.content, str) else str(event.content)
|
|
158
|
+
if len(content) > 200:
|
|
159
|
+
content = content[:200] + "..."
|
|
160
|
+
print(f"[result: {content}]")
|
|
161
|
+
elif isinstance(event, Result):
|
|
162
|
+
print(f"\n[cost: ${event.total_cost_usd:.4f}]")
|
|
163
|
+
print()
|
|
164
|
+
except ClaudeStreamError as e:
|
|
165
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
166
|
+
sys.exit(1)
|
|
167
|
+
except KeyboardInterrupt:
|
|
168
|
+
print("\nInterrupted.")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# --- Helpers ---
|
|
172
|
+
|
|
173
|
+
def _print_event(event: Event) -> None:
|
|
174
|
+
"""Pretty-print an event to stdout."""
|
|
175
|
+
if isinstance(event, AssistantText):
|
|
176
|
+
sys.stdout.write(event.text)
|
|
177
|
+
sys.stdout.flush()
|
|
178
|
+
elif isinstance(event, ToolUse):
|
|
179
|
+
print(f"\n--- Tool: {event.name} ---")
|
|
180
|
+
print(json.dumps(event.input, indent=2))
|
|
181
|
+
elif isinstance(event, ToolResult):
|
|
182
|
+
content = event.content if isinstance(event.content, str) else str(event.content)
|
|
183
|
+
if len(content) > 500:
|
|
184
|
+
content = content[:500] + "..."
|
|
185
|
+
print(f"--- Result ---\n{content}")
|
|
186
|
+
elif isinstance(event, Thinking):
|
|
187
|
+
print(f"[thinking: {event.text[:100]}...]")
|
|
188
|
+
elif isinstance(event, Result):
|
|
189
|
+
print(f"\n--- Done ({event.duration_ms:.0f}ms, ${event.total_cost_usd:.4f}) ---")
|
|
190
|
+
elif isinstance(event, ApiRetry):
|
|
191
|
+
print(f"[retry {event.attempt}/{event.max_retries}: {event.error}]", file=sys.stderr)
|
|
192
|
+
elif isinstance(event, PermissionRequest):
|
|
193
|
+
print(f"[permission needed: {event.tool_name}]", file=sys.stderr)
|
|
194
|
+
elif isinstance(event, StreamDelta):
|
|
195
|
+
if event.text:
|
|
196
|
+
sys.stdout.write(event.text)
|
|
197
|
+
sys.stdout.flush()
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _print_json(event: Event) -> None:
|
|
201
|
+
"""Print an event as a JSON line."""
|
|
202
|
+
import dataclasses
|
|
203
|
+
d = dataclasses.asdict(event)
|
|
204
|
+
print(json.dumps(d))
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def main() -> None:
|
|
208
|
+
app.run()
|