claude-p 0.1.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.
@@ -0,0 +1,21 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ release:
6
+ types: [published]
7
+
8
+ permissions:
9
+ contents: read
10
+ id-token: write
11
+
12
+ jobs:
13
+ publish:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: astral-sh/setup-uv@v5
18
+ - run: uv build
19
+ - run: uvx twine check dist/*
20
+ - uses: pypa/gh-action-pypi-publish@release/v1
21
+
@@ -0,0 +1,9 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .pytest_cache/
4
+ .venv/
5
+ dist/
6
+ build/
7
+ *.egg-info/
8
+ .DS_Store
9
+
claude_p-0.1.0/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Equality Machine
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.
22
+
@@ -0,0 +1,128 @@
1
+ Metadata-Version: 2.4
2
+ Name: claude-p
3
+ Version: 0.1.0
4
+ Summary: Claude -p compatible wrapper backed by interactive Claude Code subscription sessions
5
+ Project-URL: Homepage, https://github.com/Equality-Machine/claude-p
6
+ Project-URL: Repository, https://github.com/Equality-Machine/claude-p
7
+ Project-URL: Issues, https://github.com/Equality-Machine/claude-p/issues
8
+ Author: Equality Machine
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: agent,claude,claude-code,cli,sdk
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
+ Classifier: Programming Language :: Python :: 3.13
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+
23
+ # claude-p
24
+
25
+ `claude-p` is a `claude -p` compatible Python CLI and SDK backed by the
26
+ interactive Claude Code TUI.
27
+
28
+ Use it when `claude -p` is unavailable in an environment, but interactive
29
+ `claude` works with the local Claude Code subscription login state.
30
+
31
+ ## Install
32
+
33
+ ```bash
34
+ pip install claude-p
35
+ ```
36
+
37
+ ## CLI
38
+
39
+ Default output matches `claude -p`: plain text.
40
+
41
+ ```bash
42
+ claude-p.py "Respond exactly: hello"
43
+ ```
44
+
45
+ Structured outputs:
46
+
47
+ ```bash
48
+ claude-p.py "Respond exactly: hello" --output-format json
49
+ claude-p.py "Respond exactly: hello" --output-format stream-json --include-partial-messages
50
+ ```
51
+
52
+ The CLI accepts a broad subset of `claude -p` flags, including:
53
+
54
+ - `-p`, `--print`
55
+ - `--model`
56
+ - `--tools`
57
+ - `--permission-mode`
58
+ - `--output-format text|json|stream-json`
59
+ - `--include-partial-messages`
60
+ - `--session-id`
61
+ - `--cwd`
62
+ - common Claude Code context/config flags such as `--system-prompt`,
63
+ `--append-system-prompt`, `--mcp-config`, `--settings`, `--plugin-dir`,
64
+ `--allowedTools`, `--disallowedTools`, `--resume`, and `--continue`
65
+
66
+ Known limits:
67
+
68
+ - Token usage, cost, and exact rate-limit fields are best-effort placeholders.
69
+ - Hook lifecycle events from `claude -p --include-hook-events` are not replayed yet.
70
+ - `--input-format stream-json` is accepted but not implemented.
71
+ - `--bare` conflicts with the subscription-login goal because Claude bare mode
72
+ bypasses OAuth/keychain auth.
73
+
74
+ ## Python SDK
75
+
76
+ The API is intentionally shaped like the official Claude Agent SDK:
77
+
78
+ ```python
79
+ import asyncio
80
+ from claude_p import ClaudePOptions, query
81
+
82
+
83
+ async def main():
84
+ options = ClaudePOptions(
85
+ model="sonnet",
86
+ tools="default",
87
+ permission_mode="default",
88
+ )
89
+
90
+ async for message in query("这个目录里有多少个文件", options=options):
91
+ print(message)
92
+
93
+
94
+ asyncio.run(main())
95
+ ```
96
+
97
+ For a single final result:
98
+
99
+ ```python
100
+ import asyncio
101
+ from claude_p import ClaudePClient, ClaudePOptions
102
+
103
+
104
+ async def main():
105
+ async with ClaudePClient(ClaudePOptions(model="sonnet")) as client:
106
+ result = await client.run("Respond exactly: SDK_OK")
107
+ print(result.result)
108
+
109
+
110
+ asyncio.run(main())
111
+ ```
112
+
113
+ ## How it works
114
+
115
+ The wrapper does not call `claude -p`.
116
+
117
+ It:
118
+
119
+ 1. starts interactive `claude` in a pseudo-TTY;
120
+ 2. passes a deterministic `--session-id`;
121
+ 3. waits for the TUI response to complete;
122
+ 4. reads Claude Code's canonical session JSONL from
123
+ `~/.claude/projects/**/<session-id>.jsonl`;
124
+ 5. emits text/json/stream-json output compatible with `claude -p`.
125
+
126
+ The session JSONL is required because terminal rendering is lossy and can drop
127
+ characters during redraws.
128
+
@@ -0,0 +1,106 @@
1
+ # claude-p
2
+
3
+ `claude-p` is a `claude -p` compatible Python CLI and SDK backed by the
4
+ interactive Claude Code TUI.
5
+
6
+ Use it when `claude -p` is unavailable in an environment, but interactive
7
+ `claude` works with the local Claude Code subscription login state.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pip install claude-p
13
+ ```
14
+
15
+ ## CLI
16
+
17
+ Default output matches `claude -p`: plain text.
18
+
19
+ ```bash
20
+ claude-p.py "Respond exactly: hello"
21
+ ```
22
+
23
+ Structured outputs:
24
+
25
+ ```bash
26
+ claude-p.py "Respond exactly: hello" --output-format json
27
+ claude-p.py "Respond exactly: hello" --output-format stream-json --include-partial-messages
28
+ ```
29
+
30
+ The CLI accepts a broad subset of `claude -p` flags, including:
31
+
32
+ - `-p`, `--print`
33
+ - `--model`
34
+ - `--tools`
35
+ - `--permission-mode`
36
+ - `--output-format text|json|stream-json`
37
+ - `--include-partial-messages`
38
+ - `--session-id`
39
+ - `--cwd`
40
+ - common Claude Code context/config flags such as `--system-prompt`,
41
+ `--append-system-prompt`, `--mcp-config`, `--settings`, `--plugin-dir`,
42
+ `--allowedTools`, `--disallowedTools`, `--resume`, and `--continue`
43
+
44
+ Known limits:
45
+
46
+ - Token usage, cost, and exact rate-limit fields are best-effort placeholders.
47
+ - Hook lifecycle events from `claude -p --include-hook-events` are not replayed yet.
48
+ - `--input-format stream-json` is accepted but not implemented.
49
+ - `--bare` conflicts with the subscription-login goal because Claude bare mode
50
+ bypasses OAuth/keychain auth.
51
+
52
+ ## Python SDK
53
+
54
+ The API is intentionally shaped like the official Claude Agent SDK:
55
+
56
+ ```python
57
+ import asyncio
58
+ from claude_p import ClaudePOptions, query
59
+
60
+
61
+ async def main():
62
+ options = ClaudePOptions(
63
+ model="sonnet",
64
+ tools="default",
65
+ permission_mode="default",
66
+ )
67
+
68
+ async for message in query("这个目录里有多少个文件", options=options):
69
+ print(message)
70
+
71
+
72
+ asyncio.run(main())
73
+ ```
74
+
75
+ For a single final result:
76
+
77
+ ```python
78
+ import asyncio
79
+ from claude_p import ClaudePClient, ClaudePOptions
80
+
81
+
82
+ async def main():
83
+ async with ClaudePClient(ClaudePOptions(model="sonnet")) as client:
84
+ result = await client.run("Respond exactly: SDK_OK")
85
+ print(result.result)
86
+
87
+
88
+ asyncio.run(main())
89
+ ```
90
+
91
+ ## How it works
92
+
93
+ The wrapper does not call `claude -p`.
94
+
95
+ It:
96
+
97
+ 1. starts interactive `claude` in a pseudo-TTY;
98
+ 2. passes a deterministic `--session-id`;
99
+ 3. waits for the TUI response to complete;
100
+ 4. reads Claude Code's canonical session JSONL from
101
+ `~/.claude/projects/**/<session-id>.jsonl`;
102
+ 5. emits text/json/stream-json output compatible with `claude -p`.
103
+
104
+ The session JSONL is required because terminal rendering is lossy and can drop
105
+ characters during redraws.
106
+
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env python3
2
+ """Repository-local entrypoint for development checkouts."""
3
+
4
+ from claude_p.cli import main
5
+
6
+
7
+ if __name__ == "__main__":
8
+ raise SystemExit(main())
9
+
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.25"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "claude-p"
7
+ version = "0.1.0"
8
+ description = "Claude -p compatible wrapper backed by interactive Claude Code subscription sessions"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ authors = [
13
+ { name = "Equality Machine" }
14
+ ]
15
+ keywords = ["claude", "claude-code", "agent", "sdk", "cli"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ ]
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/Equality-Machine/claude-p"
29
+ Repository = "https://github.com/Equality-Machine/claude-p"
30
+ Issues = "https://github.com/Equality-Machine/claude-p/issues"
31
+
32
+ [project.scripts]
33
+ claude-p = "claude_p.cli:main"
34
+ "claude-p.py" = "claude_p.cli:main"
35
+
36
+ [tool.hatch.build.targets.wheel]
37
+ packages = ["src/claude_p"]
38
+
@@ -0,0 +1,16 @@
1
+ """Python SDK for the interactive Claude Code `claude -p` fallback."""
2
+
3
+ from .sdk import ClaudePClient, ClaudePOptions, query
4
+ from .types import AssistantMessage, ResultMessage, SDKMessage, StreamEventMessage, SystemMessage
5
+
6
+ __all__ = [
7
+ "AssistantMessage",
8
+ "ClaudePClient",
9
+ "ClaudePOptions",
10
+ "ResultMessage",
11
+ "SDKMessage",
12
+ "StreamEventMessage",
13
+ "SystemMessage",
14
+ "query",
15
+ ]
16
+
@@ -0,0 +1,6 @@
1
+ from .cli import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ raise SystemExit(main())
6
+
@@ -0,0 +1,702 @@
1
+ #!/usr/bin/env python3
2
+ """Claude Code interactive-TUI backend with `claude -p` compatible output.
3
+
4
+ This script does not invoke `claude -p`. It starts interactive `claude` under a
5
+ pseudo-TTY, captures the rendered terminal, extracts the assistant answer, and
6
+ emits text/json/stream-json output shaped like `claude -p`.
7
+
8
+ Compatibility target:
9
+ - Same line-oriented JSON transport.
10
+ - Same core event families: system init, stream_event message_start,
11
+ content_block_start/delta/stop, assistant, message_delta, message_stop, result.
12
+ - Usage/cost/tool events are best-effort placeholders because the interactive
13
+ TUI does not expose a machine-readable protocol.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import argparse
19
+ import glob
20
+ import json
21
+ import os
22
+ from pathlib import Path
23
+ import pty
24
+ import re
25
+ import select
26
+ import signal
27
+ import subprocess
28
+ import sys
29
+ import time
30
+ import uuid
31
+
32
+
33
+ ANSI_RE = re.compile(r"\x1b\[[0-?]*[ -/]*[@-~]")
34
+ OSC_RE = re.compile(r"\x1b\][^\x07]*(?:\x07|\x1b\\)")
35
+ SPINNER_RE = re.compile(r"\n?[✳✶✻✽✢·].*$", re.DOTALL)
36
+
37
+
38
+ def warn(message: str) -> None:
39
+ print(f"claude_tui_agent.py: warning: {message}", file=sys.stderr)
40
+
41
+
42
+ def append_flag(cmd: list[str], enabled: bool, flag: str) -> None:
43
+ if enabled:
44
+ cmd.append(flag)
45
+
46
+
47
+ def append_value(cmd: list[str], flag: str, value: str | None) -> None:
48
+ if value is not None:
49
+ cmd.extend([flag, value])
50
+
51
+
52
+ def append_optional_value(cmd: list[str], flag: str, value: str | None) -> None:
53
+ if value is None:
54
+ return
55
+ cmd.append(flag)
56
+ if value:
57
+ cmd.append(value)
58
+
59
+
60
+ def append_repeated_values(cmd: list[str], flag: str, values: list[str] | None) -> None:
61
+ if not values:
62
+ return
63
+ for value in values:
64
+ cmd.extend([flag, value])
65
+
66
+
67
+ def now_ms(start: float) -> int:
68
+ return int((time.time() - start) * 1000)
69
+
70
+
71
+ def emit(obj: dict, enabled: bool = True) -> None:
72
+ if enabled:
73
+ print(json.dumps(obj, ensure_ascii=False, separators=(",", ":")), flush=True)
74
+
75
+
76
+ def clean_terminal(text: str) -> str:
77
+ text = OSC_RE.sub("", text)
78
+ text = ANSI_RE.sub("", text)
79
+ return text.replace("\r", "").replace("\u00a0", " ")
80
+
81
+
82
+ def normalize_answer(text: str) -> str:
83
+ text = clean_terminal(text)
84
+ text = SPINNER_RE.sub("", text)
85
+ # Drop common TUI chrome if it leaked into the block.
86
+ text = re.split(r"\n?────────────────", text, maxsplit=1)[0]
87
+ return text.strip()
88
+
89
+
90
+ def extract_assistant_snapshot(transcript: str) -> str:
91
+ clean = clean_terminal(transcript)
92
+ marker = clean.rfind("⏺")
93
+ if marker < 0:
94
+ return ""
95
+ after = clean[marker + len("⏺") :]
96
+ return normalize_answer(after)
97
+
98
+
99
+ def classify_failure(transcript: str, assistant_text: str, timed_out: bool) -> str | None:
100
+ if assistant_text:
101
+ return None
102
+ low = transcript.lower()
103
+ if "do you trust" in low or "workspace trust" in low:
104
+ return "workspace_trust_blocked"
105
+ if "permission" in low and ("allow" in low or "deny" in low):
106
+ return "tool_approval_blocked"
107
+ if timed_out:
108
+ return "assistant_output_timeout"
109
+ return "assistant_output_not_found"
110
+
111
+
112
+ def build_usage(output_text: str) -> dict:
113
+ # The TUI does not expose reliable token/cost data. Keep shape-compatible
114
+ # fields with null/zero values and mark the source in result metadata.
115
+ approx_output_tokens = max(1, len(output_text.split()))
116
+ return {
117
+ "input_tokens": None,
118
+ "cache_creation_input_tokens": None,
119
+ "cache_read_input_tokens": None,
120
+ "output_tokens": approx_output_tokens,
121
+ "server_tool_use": {"web_search_requests": 0, "web_fetch_requests": 0},
122
+ "service_tier": None,
123
+ "cache_creation": {"ephemeral_1h_input_tokens": None, "ephemeral_5m_input_tokens": None},
124
+ "iterations": [
125
+ {
126
+ "input_tokens": None,
127
+ "output_tokens": approx_output_tokens,
128
+ "cache_read_input_tokens": None,
129
+ "cache_creation_input_tokens": None,
130
+ "cache_creation": {
131
+ "ephemeral_5m_input_tokens": None,
132
+ "ephemeral_1h_input_tokens": None,
133
+ },
134
+ "type": "message",
135
+ }
136
+ ],
137
+ "speed": None,
138
+ }
139
+
140
+
141
+ def extract_text_from_content(content: object) -> str:
142
+ if isinstance(content, str):
143
+ return content
144
+ if not isinstance(content, list):
145
+ return ""
146
+ parts: list[str] = []
147
+ for block in content:
148
+ if isinstance(block, dict) and block.get("type") == "text":
149
+ text = block.get("text")
150
+ if isinstance(text, str):
151
+ parts.append(text)
152
+ return "".join(parts).strip()
153
+
154
+
155
+ def canonical_json_if_equivalent(left: str, right: str) -> str | None:
156
+ try:
157
+ left_obj = json.loads(left)
158
+ right_obj = json.loads(right)
159
+ except json.JSONDecodeError:
160
+ return None
161
+ if left_obj != right_obj:
162
+ return None
163
+ return json.dumps(right_obj, ensure_ascii=False, separators=(",", ":"))
164
+
165
+
166
+ def read_persisted_assistant(session_id: str) -> dict | None:
167
+ """Read Claude Code's persisted JSONL for exact final assistant text.
168
+
169
+ The interactive terminal is a lossy rendering surface: wide glyphs, cursor
170
+ redraws, and spinner updates can drop or smear characters in the captured
171
+ TTY transcript. Claude Code still writes the canonical session JSONL for
172
+ interactive sessions. When available, use it as the source of truth for the
173
+ final assistant message while keeping the TUI transcript as provenance.
174
+ """
175
+ pattern = str(Path.home() / ".claude" / "projects" / "**" / f"{session_id}.jsonl")
176
+ paths = [Path(p) for p in glob.glob(pattern, recursive=True)]
177
+ if not paths:
178
+ return None
179
+ path = max(paths, key=lambda p: p.stat().st_mtime)
180
+ latest: dict | None = None
181
+ try:
182
+ with path.open() as f:
183
+ for line in f:
184
+ try:
185
+ event = json.loads(line)
186
+ except json.JSONDecodeError:
187
+ continue
188
+ if event.get("type") != "assistant":
189
+ continue
190
+ message = event.get("message")
191
+ if not isinstance(message, dict):
192
+ continue
193
+ text = extract_text_from_content(message.get("content"))
194
+ if not text:
195
+ continue
196
+ latest = {
197
+ "path": str(path),
198
+ "text": text,
199
+ "message": message,
200
+ "model": message.get("model"),
201
+ "message_id": message.get("id"),
202
+ "usage": message.get("usage"),
203
+ "stop_reason": message.get("stop_reason"),
204
+ }
205
+ except OSError:
206
+ return None
207
+ return latest
208
+
209
+
210
+ def run_tui(args: argparse.Namespace, stream_json: bool) -> tuple[str, str, int | None, bool, float]:
211
+ cmd = ["claude", "--session-id", args.session_id]
212
+
213
+ # Pass through options that the interactive `claude` entrypoint itself
214
+ # understands. Print-only options are handled by this wrapper and are not
215
+ # forwarded.
216
+ append_repeated_values(cmd, "--add-dir", args.add_dir)
217
+ append_value(cmd, "--agent", args.agent)
218
+ append_value(cmd, "--agents", args.agents)
219
+ append_flag(cmd, args.allow_dangerously_skip_permissions, "--allow-dangerously-skip-permissions")
220
+ append_repeated_values(cmd, "--allowedTools", args.allowed_tools)
221
+ append_value(cmd, "--append-system-prompt", args.append_system_prompt)
222
+ append_repeated_values(cmd, "--betas", args.betas)
223
+ append_flag(cmd, args.brief, "--brief")
224
+ append_flag(cmd, args.chrome, "--chrome")
225
+ append_flag(cmd, args.no_chrome, "--no-chrome")
226
+ append_flag(cmd, args.continue_session, "--continue")
227
+ append_flag(cmd, args.dangerously_skip_permissions, "--dangerously-skip-permissions")
228
+ append_optional_value(cmd, "--debug", args.debug)
229
+ append_value(cmd, "--debug-file", args.debug_file)
230
+ append_flag(cmd, args.disable_slash_commands, "--disable-slash-commands")
231
+ append_repeated_values(cmd, "--disallowedTools", args.disallowed_tools)
232
+ append_value(cmd, "--effort", args.effort)
233
+ append_flag(cmd, args.exclude_dynamic_system_prompt_sections, "--exclude-dynamic-system-prompt-sections")
234
+ append_repeated_values(cmd, "--file", args.files)
235
+ append_flag(cmd, args.fork_session, "--fork-session")
236
+ append_optional_value(cmd, "--from-pr", args.from_pr)
237
+ append_flag(cmd, args.ide, "--ide")
238
+ append_value(cmd, "--json-schema", args.json_schema)
239
+ append_repeated_values(cmd, "--mcp-config", args.mcp_config)
240
+ append_flag(cmd, args.mcp_debug, "--mcp-debug")
241
+ append_value(cmd, "--tools", args.tools)
242
+ append_value(cmd, "--model", args.model)
243
+ append_value(cmd, "--name", args.name)
244
+ append_value(cmd, "--permission-mode", args.permission_mode)
245
+ append_repeated_values(cmd, "--plugin-dir", args.plugin_dir)
246
+ append_value(cmd, "--remote-control-session-name-prefix", args.remote_control_session_name_prefix)
247
+ append_optional_value(cmd, "--resume", args.resume)
248
+ append_value(cmd, "--setting-sources", args.setting_sources)
249
+ append_value(cmd, "--settings", args.settings)
250
+ append_flag(cmd, args.strict_mcp_config, "--strict-mcp-config")
251
+ append_value(cmd, "--system-prompt", args.system_prompt)
252
+ append_optional_value(cmd, "--tmux", args.tmux)
253
+ append_optional_value(cmd, "--worktree", args.worktree)
254
+
255
+ cmd.append(args.prompt)
256
+ master, slave = pty.openpty()
257
+ env = {**os.environ, "NO_COLOR": "1", "TERM": args.term}
258
+ start = time.time()
259
+ proc = subprocess.Popen(
260
+ cmd,
261
+ stdin=slave,
262
+ stdout=slave,
263
+ stderr=slave,
264
+ cwd=args.cwd,
265
+ env=env,
266
+ )
267
+ os.close(slave)
268
+
269
+ raw = bytearray()
270
+ last_output = time.time()
271
+ last_snapshot = ""
272
+ timed_out = True
273
+
274
+ try:
275
+ while time.time() - start < args.timeout_sec:
276
+ ready, _, _ = select.select([master], [], [], 0.2)
277
+ if ready:
278
+ try:
279
+ data = os.read(master, 65536)
280
+ except OSError:
281
+ break
282
+ if not data:
283
+ break
284
+ raw.extend(data)
285
+ last_output = time.time()
286
+
287
+ if args.emit_terminal_delta:
288
+ emit(
289
+ {
290
+ "type": "tui_terminal_delta",
291
+ "text": clean_terminal(data.decode("utf-8", "replace")),
292
+ "uuid": str(uuid.uuid4()),
293
+ "session_id": args.session_id,
294
+ },
295
+ enabled=stream_json,
296
+ )
297
+
298
+ snapshot = extract_assistant_snapshot(raw.decode("utf-8", "replace"))
299
+ if snapshot and snapshot != last_snapshot:
300
+ if args.live_tui_deltas:
301
+ delta = snapshot[len(last_snapshot) :] if snapshot.startswith(last_snapshot) else snapshot
302
+ if delta.strip():
303
+ emit(
304
+ {
305
+ "type": "stream_event",
306
+ "event": {
307
+ "type": "content_block_delta",
308
+ "index": 0,
309
+ "delta": {"type": "text_delta", "text": delta},
310
+ },
311
+ "session_id": args.session_id,
312
+ "parent_tool_use_id": None,
313
+ "uuid": str(uuid.uuid4()),
314
+ },
315
+ enabled=stream_json,
316
+ )
317
+ last_snapshot = snapshot
318
+
319
+ transcript = raw.decode("utf-8", "replace")
320
+ if last_snapshot and time.time() - last_output >= args.quiet_after_sec:
321
+ timed_out = False
322
+ break
323
+ if proc.poll() is not None:
324
+ timed_out = False
325
+ break
326
+ finally:
327
+ if proc.poll() is None:
328
+ proc.send_signal(signal.SIGTERM)
329
+ try:
330
+ proc.wait(timeout=2)
331
+ except subprocess.TimeoutExpired:
332
+ proc.kill()
333
+ os.close(master)
334
+
335
+ transcript = clean_terminal(raw.decode("utf-8", "replace"))
336
+ answer = extract_assistant_snapshot(transcript)
337
+ return transcript, answer, proc.returncode, timed_out, start
338
+
339
+
340
+ def main() -> int:
341
+ parser = argparse.ArgumentParser()
342
+ parser.add_argument("prompt", nargs="?")
343
+ parser.add_argument("--cwd", default=os.getcwd())
344
+
345
+ # Common Claude Code options. The goal is CLI compatibility with the print
346
+ # path while still using the interactive TUI backend internally.
347
+ parser.add_argument("-p", "--print", dest="print_mode", action="store_true", help="Accepted for claude -p compatibility.")
348
+ parser.add_argument("--add-dir", action="append", default=[])
349
+ parser.add_argument("--agent")
350
+ parser.add_argument("--agents")
351
+ parser.add_argument("--allow-dangerously-skip-permissions", action="store_true")
352
+ parser.add_argument("--allowedTools", "--allowed-tools", dest="allowed_tools", action="append", default=[])
353
+ parser.add_argument("--append-system-prompt")
354
+ parser.add_argument("--bare", action="store_true")
355
+ parser.add_argument("--betas", action="append", default=[])
356
+ parser.add_argument("--brief", action="store_true")
357
+ parser.add_argument("--chrome", action="store_true")
358
+ parser.add_argument("--no-chrome", action="store_true")
359
+ parser.add_argument("-c", "--continue", dest="continue_session", action="store_true")
360
+ parser.add_argument("--dangerously-skip-permissions", action="store_true")
361
+ parser.add_argument("-d", "--debug", nargs="?", const="")
362
+ parser.add_argument("--debug-file")
363
+ parser.add_argument("--disable-slash-commands", action="store_true")
364
+ parser.add_argument("--disallowedTools", "--disallowed-tools", dest="disallowed_tools", action="append", default=[])
365
+ parser.add_argument("--effort")
366
+ parser.add_argument("--exclude-dynamic-system-prompt-sections", action="store_true")
367
+ parser.add_argument("--fallback-model")
368
+ parser.add_argument("--file", dest="files", action="append", default=[])
369
+ parser.add_argument("--fork-session", action="store_true")
370
+ parser.add_argument("--from-pr", nargs="?", const="")
371
+ parser.add_argument("--ide", action="store_true")
372
+ parser.add_argument("--model", default="sonnet")
373
+ parser.add_argument("--tools", default="default")
374
+ parser.add_argument("--permission-mode", default="default")
375
+ parser.add_argument(
376
+ "--output-format",
377
+ choices=["text", "json", "stream-json"],
378
+ default="text",
379
+ help="Output format, matching claude -p. Default: text.",
380
+ )
381
+ parser.add_argument("--verbose", action="store_true", help="Accepted for claude -p CLI compatibility.")
382
+ parser.add_argument("--include-hook-events", action="store_true")
383
+ parser.add_argument(
384
+ "--include-partial-messages",
385
+ action="store_true",
386
+ help="Accepted for claude -p CLI compatibility. With the TUI backend, stream-json emits one final text delta by default.",
387
+ )
388
+ parser.add_argument("--input-format", choices=["text", "stream-json"], default="text")
389
+ parser.add_argument("--json-schema")
390
+ parser.add_argument("--max-budget-usd")
391
+ parser.add_argument("--mcp-config", action="append", default=[])
392
+ parser.add_argument("--mcp-debug", action="store_true")
393
+ parser.add_argument("-n", "--name")
394
+ parser.add_argument("--no-session-persistence", action="store_true")
395
+ parser.add_argument("--plugin-dir", action="append", default=[])
396
+ parser.add_argument("--remote-control-session-name-prefix")
397
+ parser.add_argument("--replay-user-messages", action="store_true")
398
+ parser.add_argument("-r", "--resume", nargs="?", const="")
399
+ parser.add_argument("--setting-sources")
400
+ parser.add_argument("--settings")
401
+ parser.add_argument("--strict-mcp-config", action="store_true")
402
+ parser.add_argument("--system-prompt")
403
+ parser.add_argument("--tmux", nargs="?", const="")
404
+ parser.add_argument("-v", "--version", action="store_true")
405
+ parser.add_argument("-w", "--worktree", nargs="?", const="")
406
+
407
+ # Wrapper-only controls.
408
+ parser.add_argument("--timeout-sec", type=float, default=90)
409
+ parser.add_argument("--quiet-after-sec", type=float, default=3)
410
+ parser.add_argument("--session-id", default=str(uuid.uuid4()))
411
+ parser.add_argument("--term", default="xterm-256color")
412
+ parser.add_argument("--raw-log")
413
+ parser.add_argument("--emit-terminal-delta", action="store_true")
414
+ parser.add_argument(
415
+ "--live-tui-deltas",
416
+ action="store_true",
417
+ help="Emit live text deltas from the lossy TUI surface. Default buffers until persisted JSONL final text is available.",
418
+ )
419
+ args = parser.parse_args()
420
+
421
+ if args.version:
422
+ subprocess.run(["claude", "--version"], check=False)
423
+ return 0
424
+
425
+ if args.prompt is None:
426
+ if sys.stdin.isatty():
427
+ parser.error("prompt is required unless stdin provides input")
428
+ args.prompt = sys.stdin.read()
429
+
430
+ unsupported: list[str] = []
431
+ if args.input_format != "text":
432
+ unsupported.append("--input-format stream-json")
433
+ if args.replay_user_messages:
434
+ unsupported.append("--replay-user-messages")
435
+ if args.no_session_persistence:
436
+ unsupported.append("--no-session-persistence")
437
+ if args.bare:
438
+ unsupported.append("--bare")
439
+ if args.max_budget_usd:
440
+ unsupported.append("--max-budget-usd")
441
+ if args.fallback_model:
442
+ unsupported.append("--fallback-model")
443
+ if unsupported:
444
+ for flag in unsupported:
445
+ warn(f"{flag} is not supported by the interactive subscription backend; continuing without exact claude -p semantics")
446
+
447
+ stream_json = args.output_format == "stream-json"
448
+ message_id = f"msg_tui_{uuid.uuid4().hex[:24]}"
449
+ start = time.time()
450
+
451
+ emit(
452
+ {
453
+ "type": "system",
454
+ "subtype": "init",
455
+ "cwd": args.cwd,
456
+ "session_id": args.session_id,
457
+ "tools": [],
458
+ "mcp_servers": [],
459
+ "model": args.model,
460
+ "permissionMode": args.permission_mode,
461
+ "apiKeySource": "interactive_tui_subscription",
462
+ "claude_code_version": None,
463
+ "output_style": "default",
464
+ "uuid": str(uuid.uuid4()),
465
+ "fast_mode_state": "off",
466
+ },
467
+ enabled=stream_json,
468
+ )
469
+ emit(
470
+ {
471
+ "type": "system",
472
+ "subtype": "status",
473
+ "status": "requesting",
474
+ "uuid": str(uuid.uuid4()),
475
+ "session_id": args.session_id,
476
+ },
477
+ enabled=stream_json,
478
+ )
479
+ emit(
480
+ {
481
+ "type": "stream_event",
482
+ "event": {
483
+ "type": "message_start",
484
+ "message": {
485
+ "model": args.model,
486
+ "id": message_id,
487
+ "type": "message",
488
+ "role": "assistant",
489
+ "content": [],
490
+ "stop_reason": None,
491
+ "stop_sequence": None,
492
+ "stop_details": None,
493
+ "usage": {
494
+ "input_tokens": None,
495
+ "cache_creation_input_tokens": None,
496
+ "cache_read_input_tokens": None,
497
+ "output_tokens": None,
498
+ "service_tier": None,
499
+ },
500
+ },
501
+ },
502
+ "session_id": args.session_id,
503
+ "parent_tool_use_id": None,
504
+ "uuid": str(uuid.uuid4()),
505
+ "ttft_ms": None,
506
+ },
507
+ enabled=stream_json,
508
+ )
509
+ emit(
510
+ {
511
+ "type": "stream_event",
512
+ "event": {"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}},
513
+ "session_id": args.session_id,
514
+ "parent_tool_use_id": None,
515
+ "uuid": str(uuid.uuid4()),
516
+ },
517
+ enabled=stream_json,
518
+ )
519
+
520
+ transcript, tui_answer, exit_code, timed_out, run_start = run_tui(args, stream_json)
521
+ if args.raw_log:
522
+ path = Path(args.raw_log)
523
+ path.parent.mkdir(parents=True, exist_ok=True)
524
+ path.write_text(transcript)
525
+
526
+ persisted = read_persisted_assistant(args.session_id)
527
+ answer = persisted["text"] if persisted else tui_answer
528
+ final_answer_source = "session_jsonl" if persisted else "tui_transcript"
529
+ if persisted and tui_answer and tui_answer != persisted["text"]:
530
+ canonical = canonical_json_if_equivalent(tui_answer, persisted["text"])
531
+ if canonical is not None:
532
+ answer = canonical
533
+ final_answer_source = "json_canonicalized_from_matching_tui_and_session_jsonl"
534
+ final_model = persisted.get("model") if persisted else args.model
535
+ message_id = persisted.get("message_id") if persisted and persisted.get("message_id") else message_id
536
+
537
+ if answer and not args.live_tui_deltas:
538
+ emit(
539
+ {
540
+ "type": "stream_event",
541
+ "event": {
542
+ "type": "content_block_delta",
543
+ "index": 0,
544
+ "delta": {"type": "text_delta", "text": answer},
545
+ },
546
+ "session_id": args.session_id,
547
+ "parent_tool_use_id": None,
548
+ "uuid": str(uuid.uuid4()),
549
+ },
550
+ enabled=stream_json,
551
+ )
552
+
553
+ failure = classify_failure(transcript, answer, timed_out)
554
+ is_error = failure is not None
555
+ usage = build_usage(answer)
556
+ duration_ms = now_ms(start)
557
+
558
+ if args.output_format == "text":
559
+ if answer:
560
+ print(answer)
561
+ return 0 if not is_error else 2
562
+
563
+ if args.output_format == "json":
564
+ print(
565
+ json.dumps(
566
+ {
567
+ "type": "result",
568
+ "subtype": "success" if not is_error else "error",
569
+ "is_error": is_error,
570
+ "duration_ms": duration_ms,
571
+ "duration_api_ms": None,
572
+ "num_turns": 1,
573
+ "result": answer,
574
+ "session_id": args.session_id,
575
+ "total_cost_usd": None,
576
+ "usage": usage,
577
+ "terminal_reason": "completed" if not is_error else failure,
578
+ "interactive_tui_backend": {
579
+ "raw_log": args.raw_log,
580
+ "session_jsonl": persisted.get("path") if persisted else None,
581
+ "tui_answer": tui_answer,
582
+ "final_answer_source": final_answer_source,
583
+ "timed_out": timed_out,
584
+ "exit_code": exit_code,
585
+ "extraction_confidence": "high" if persisted else ("medium" if answer else "none"),
586
+ },
587
+ },
588
+ ensure_ascii=False,
589
+ separators=(",", ":"),
590
+ )
591
+ )
592
+ return 0 if not is_error else 2
593
+
594
+ emit(
595
+ {
596
+ "type": "assistant",
597
+ "message": {
598
+ "model": final_model,
599
+ "id": message_id,
600
+ "type": "message",
601
+ "role": "assistant",
602
+ "content": [{"type": "text", "text": answer}] if answer else [],
603
+ "stop_reason": "end_turn" if not is_error else None,
604
+ "stop_sequence": None,
605
+ "stop_details": None,
606
+ "usage": {
607
+ "input_tokens": None,
608
+ "cache_creation_input_tokens": None,
609
+ "cache_read_input_tokens": None,
610
+ "output_tokens": usage["output_tokens"],
611
+ "service_tier": None,
612
+ },
613
+ "context_management": None,
614
+ },
615
+ "parent_tool_use_id": None,
616
+ "session_id": args.session_id,
617
+ "uuid": str(uuid.uuid4()),
618
+ }
619
+ )
620
+ emit(
621
+ {
622
+ "type": "stream_event",
623
+ "event": {"type": "content_block_stop", "index": 0},
624
+ "session_id": args.session_id,
625
+ "parent_tool_use_id": None,
626
+ "uuid": str(uuid.uuid4()),
627
+ }
628
+ )
629
+ emit(
630
+ {
631
+ "type": "stream_event",
632
+ "event": {
633
+ "type": "message_delta",
634
+ "delta": {"stop_reason": "end_turn" if not is_error else "error", "stop_sequence": None, "stop_details": None},
635
+ "usage": {
636
+ "input_tokens": None,
637
+ "cache_creation_input_tokens": None,
638
+ "cache_read_input_tokens": None,
639
+ "output_tokens": usage["output_tokens"],
640
+ "iterations": usage["iterations"],
641
+ },
642
+ "context_management": {"applied_edits": []},
643
+ },
644
+ "session_id": args.session_id,
645
+ "parent_tool_use_id": None,
646
+ "uuid": str(uuid.uuid4()),
647
+ }
648
+ )
649
+ emit(
650
+ {
651
+ "type": "stream_event",
652
+ "event": {"type": "message_stop"},
653
+ "session_id": args.session_id,
654
+ "parent_tool_use_id": None,
655
+ "uuid": str(uuid.uuid4()),
656
+ }
657
+ )
658
+ emit(
659
+ {
660
+ "type": "rate_limit_event",
661
+ "rate_limit_info": {"status": "unknown"},
662
+ "session_id": args.session_id,
663
+ "uuid": str(uuid.uuid4()),
664
+ }
665
+ )
666
+ emit(
667
+ {
668
+ "type": "result",
669
+ "subtype": "success" if not is_error else "error",
670
+ "is_error": is_error,
671
+ "api_error_status": None,
672
+ "duration_ms": duration_ms,
673
+ "duration_api_ms": None,
674
+ "num_turns": 1,
675
+ "result": answer,
676
+ "stop_reason": "end_turn" if not is_error else None,
677
+ "session_id": args.session_id,
678
+ "total_cost_usd": None,
679
+ "usage": usage,
680
+ "modelUsage": {},
681
+ "permission_denials": [],
682
+ "terminal_reason": "completed" if not is_error else failure,
683
+ "fast_mode_state": "off",
684
+ "uuid": str(uuid.uuid4()),
685
+ "interactive_tui_backend": {
686
+ "raw_log": args.raw_log,
687
+ "session_jsonl": persisted.get("path") if persisted else None,
688
+ "tui_answer": tui_answer,
689
+ "final_answer_source": final_answer_source,
690
+ "timed_out": timed_out,
691
+ "exit_code": exit_code,
692
+ "extraction_confidence": "high" if persisted else ("medium" if answer else "none"),
693
+ "compatibility_note": "Shape-compatible with claude -p stream-json core events; usage/cost/tool events are best-effort because TUI has no machine protocol.",
694
+ },
695
+ }
696
+ )
697
+
698
+ return 0 if not is_error else 2
699
+
700
+
701
+ if __name__ == "__main__":
702
+ raise SystemExit(main())
@@ -0,0 +1,154 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from dataclasses import dataclass, field
5
+ import json
6
+ import os
7
+ import sys
8
+ from typing import Any, AsyncIterator, Iterable
9
+
10
+ from .types import AssistantMessage, ResultMessage, SDKMessage, StreamEventMessage, SystemMessage
11
+
12
+
13
+ @dataclass
14
+ class ClaudePOptions:
15
+ """Options for the interactive Claude Code `claude -p` fallback.
16
+
17
+ The names mirror the official Claude Agent SDK's options style while
18
+ preserving CLI compatibility with `claude -p`.
19
+ """
20
+
21
+ cwd: str | None = None
22
+ model: str = "sonnet"
23
+ tools: str | Iterable[str] = "default"
24
+ permission_mode: str = "default"
25
+ output_format: str = "stream-json"
26
+ system_prompt: str | None = None
27
+ append_system_prompt: str | None = None
28
+ allowed_tools: list[str] = field(default_factory=list)
29
+ disallowed_tools: list[str] = field(default_factory=list)
30
+ mcp_config: list[str] = field(default_factory=list)
31
+ settings: str | None = None
32
+ session_id: str | None = None
33
+ timeout_sec: float = 90
34
+ quiet_after_sec: float = 3
35
+ raw_log: str | None = None
36
+ include_partial_messages: bool = True
37
+ executable: str | None = None
38
+ extra_args: list[str] = field(default_factory=list)
39
+
40
+ def command(self, prompt: str) -> list[str]:
41
+ if self.executable:
42
+ cmd = [self.executable, prompt]
43
+ else:
44
+ cmd = [sys.executable, "-m", "claude_p.cli", prompt]
45
+ cmd.extend(["--model", self.model])
46
+ if isinstance(self.tools, str):
47
+ cmd.extend(["--tools", self.tools])
48
+ else:
49
+ cmd.extend(["--tools", ",".join(self.tools)])
50
+ cmd.extend(["--permission-mode", self.permission_mode])
51
+ cmd.extend(["--output-format", self.output_format])
52
+ cmd.extend(["--timeout-sec", str(self.timeout_sec)])
53
+ cmd.extend(["--quiet-after-sec", str(self.quiet_after_sec)])
54
+ if self.cwd:
55
+ cmd.extend(["--cwd", self.cwd])
56
+ if self.system_prompt:
57
+ cmd.extend(["--system-prompt", self.system_prompt])
58
+ if self.append_system_prompt:
59
+ cmd.extend(["--append-system-prompt", self.append_system_prompt])
60
+ for tool in self.allowed_tools:
61
+ cmd.extend(["--allowedTools", tool])
62
+ for tool in self.disallowed_tools:
63
+ cmd.extend(["--disallowedTools", tool])
64
+ for config in self.mcp_config:
65
+ cmd.extend(["--mcp-config", config])
66
+ if self.settings:
67
+ cmd.extend(["--settings", self.settings])
68
+ if self.session_id:
69
+ cmd.extend(["--session-id", self.session_id])
70
+ if self.raw_log:
71
+ cmd.extend(["--raw-log", self.raw_log])
72
+ if self.include_partial_messages:
73
+ cmd.append("--include-partial-messages")
74
+ cmd.extend(self.extra_args)
75
+ return cmd
76
+
77
+
78
+ def _message_from_raw(raw: dict[str, Any]) -> SDKMessage:
79
+ typ = raw.get("type")
80
+ if typ == "system":
81
+ return SystemMessage(type="system", raw=raw, subtype=raw.get("subtype"))
82
+ if typ == "stream_event":
83
+ return StreamEventMessage(type="stream_event", raw=raw, event=raw.get("event"))
84
+ if typ == "assistant":
85
+ content = raw.get("message", {}).get("content", [])
86
+ text_parts = [
87
+ block.get("text", "")
88
+ for block in content
89
+ if isinstance(block, dict) and block.get("type") == "text"
90
+ ]
91
+ return AssistantMessage(type="assistant", raw=raw, text="".join(text_parts))
92
+ if typ == "result":
93
+ return ResultMessage(
94
+ type="result",
95
+ raw=raw,
96
+ result=raw.get("result", ""),
97
+ is_error=bool(raw.get("is_error")),
98
+ session_id=raw.get("session_id"),
99
+ terminal_reason=raw.get("terminal_reason"),
100
+ )
101
+ return SDKMessage(type=str(typ or "raw"), raw=raw)
102
+
103
+
104
+ async def query(prompt: str, *, options: ClaudePOptions | None = None) -> AsyncIterator[SDKMessage]:
105
+ """Run a prompt and yield stream-json messages.
106
+
107
+ This mirrors the official Claude Agent SDK's `query(...)` shape, but the
108
+ backend is interactive Claude Code instead of `claude -p`.
109
+ """
110
+
111
+ options = options or ClaudePOptions()
112
+ options.output_format = "stream-json"
113
+ proc = await asyncio.create_subprocess_exec(
114
+ *options.command(prompt),
115
+ cwd=options.cwd or os.getcwd(),
116
+ stdout=asyncio.subprocess.PIPE,
117
+ stderr=asyncio.subprocess.PIPE,
118
+ )
119
+ assert proc.stdout is not None
120
+ async for raw_line in proc.stdout:
121
+ line = raw_line.decode("utf-8", "replace").strip()
122
+ if not line:
123
+ continue
124
+ yield _message_from_raw(json.loads(line))
125
+ stderr = await proc.stderr.read() if proc.stderr else b""
126
+ code = await proc.wait()
127
+ if code != 0:
128
+ raise RuntimeError(stderr.decode("utf-8", "replace") or f"claude-p exited with {code}")
129
+
130
+
131
+ class ClaudePClient:
132
+ """Small client wrapper inspired by ClaudeSDKClient."""
133
+
134
+ def __init__(self, options: ClaudePOptions | None = None):
135
+ self.options = options or ClaudePOptions()
136
+
137
+ async def __aenter__(self) -> "ClaudePClient":
138
+ return self
139
+
140
+ async def __aexit__(self, exc_type, exc, tb) -> None:
141
+ return None
142
+
143
+ async def query(self, prompt: str) -> AsyncIterator[SDKMessage]:
144
+ async for message in query(prompt, options=self.options):
145
+ yield message
146
+
147
+ async def run(self, prompt: str) -> ResultMessage:
148
+ result: ResultMessage | None = None
149
+ async for message in self.query(prompt):
150
+ if isinstance(message, ResultMessage):
151
+ result = message
152
+ if result is None:
153
+ raise RuntimeError("claude-p did not emit a result message")
154
+ return result
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, Literal
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class SDKMessage:
9
+ """Base message wrapper returned by the Python SDK."""
10
+
11
+ type: str
12
+ raw: dict[str, Any]
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class SystemMessage(SDKMessage):
17
+ subtype: str | None = None
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class StreamEventMessage(SDKMessage):
22
+ event: dict[str, Any] | None = None
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class AssistantMessage(SDKMessage):
27
+ text: str
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class ResultMessage(SDKMessage):
32
+ result: str
33
+ is_error: bool
34
+ session_id: str | None = None
35
+ terminal_reason: str | None = None
36
+
37
+
38
+ MessageKind = Literal["system", "stream_event", "assistant", "result", "raw"]
39
+
@@ -0,0 +1,10 @@
1
+ from claude_p import ClaudePOptions
2
+
3
+
4
+ def test_options_command_includes_stream_json():
5
+ cmd = ClaudePOptions(model="sonnet", tools="").command("hello")
6
+ assert "--output-format" in cmd
7
+ assert "stream-json" in cmd
8
+ assert "--tools" in cmd
9
+ assert "" in cmd
10
+
claude_p-0.1.0/uv.lock ADDED
@@ -0,0 +1,8 @@
1
+ version = 1
2
+ revision = 2
3
+ requires-python = ">=3.10"
4
+
5
+ [[package]]
6
+ name = "claude-p"
7
+ version = "0.1.0"
8
+ source = { editable = "." }