pyclaudecli 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.
- pyclaudecli/__init__.py +14 -0
- pyclaudecli/__main__.py +61 -0
- pyclaudecli/_process.py +241 -0
- pyclaudecli/client.py +581 -0
- pyclaudecli/exceptions.py +32 -0
- pyclaudecli-0.1.0.dist-info/METADATA +94 -0
- pyclaudecli-0.1.0.dist-info/RECORD +10 -0
- pyclaudecli-0.1.0.dist-info/WHEEL +4 -0
- pyclaudecli-0.1.0.dist-info/entry_points.txt +2 -0
- pyclaudecli-0.1.0.dist-info/licenses/LICENSE +21 -0
pyclaudecli/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""pyclaudecli: a Python library that wraps the `claude` CLI (Claude Code)."""
|
|
2
|
+
|
|
3
|
+
from .client import ClaudeCLI, build_flags
|
|
4
|
+
from .exceptions import ClaudeCLIError, ClaudeNotFoundError, ClaudeTimeoutError
|
|
5
|
+
|
|
6
|
+
__version__ = "0.1.0"
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"ClaudeCLI",
|
|
10
|
+
"build_flags",
|
|
11
|
+
"ClaudeCLIError",
|
|
12
|
+
"ClaudeNotFoundError",
|
|
13
|
+
"ClaudeTimeoutError",
|
|
14
|
+
]
|
pyclaudecli/__main__.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""`python -m pyclaudecli` — a minimal CLI wrapper for quick scripting.
|
|
2
|
+
|
|
3
|
+
For anything beyond a one-off prompt, use `pyclaudecli.ClaudeCLI` directly.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
from typing import List, Optional, Tuple
|
|
10
|
+
|
|
11
|
+
from .client import ClaudeCLI
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def parse_args(argv: List[str]) -> Tuple[str, str]:
|
|
15
|
+
model = "haiku"
|
|
16
|
+
prompt_parts = []
|
|
17
|
+
index = 0
|
|
18
|
+
|
|
19
|
+
while index < len(argv):
|
|
20
|
+
arg = argv[index]
|
|
21
|
+
|
|
22
|
+
if arg in {"--model", "-m"}:
|
|
23
|
+
if index + 1 >= len(argv):
|
|
24
|
+
raise SystemExit("Missing value for --model.")
|
|
25
|
+
model = argv[index + 1]
|
|
26
|
+
index += 2
|
|
27
|
+
continue
|
|
28
|
+
|
|
29
|
+
if arg.startswith("--model="):
|
|
30
|
+
model = arg.split("=", 1)[1]
|
|
31
|
+
index += 1
|
|
32
|
+
continue
|
|
33
|
+
|
|
34
|
+
prompt_parts.append(arg)
|
|
35
|
+
index += 1
|
|
36
|
+
|
|
37
|
+
prompt = " ".join(prompt_parts).strip() if prompt_parts else "Hello, Claude!"
|
|
38
|
+
return model, prompt
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
42
|
+
if argv is None:
|
|
43
|
+
argv = sys.argv[1:]
|
|
44
|
+
|
|
45
|
+
model, prompt = parse_args(argv)
|
|
46
|
+
client = ClaudeCLI()
|
|
47
|
+
try:
|
|
48
|
+
result = client.run(["--print", "--model", model, prompt], check=False)
|
|
49
|
+
except Exception as exc: # ClaudeNotFoundError, etc.
|
|
50
|
+
print(str(exc), file=sys.stderr)
|
|
51
|
+
return 1
|
|
52
|
+
|
|
53
|
+
if result.stdout:
|
|
54
|
+
print(result.stdout, end="")
|
|
55
|
+
if result.stderr:
|
|
56
|
+
print(result.stderr, file=sys.stderr, end="")
|
|
57
|
+
return result.returncode
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
if __name__ == "__main__":
|
|
61
|
+
raise SystemExit(main())
|
pyclaudecli/_process.py
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""Low-level process plumbing shared by ClaudeCLI's methods."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import select
|
|
8
|
+
import subprocess
|
|
9
|
+
import time
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Callable, Iterator, Optional, Sequence
|
|
12
|
+
|
|
13
|
+
from .exceptions import ClaudeCLIError, ClaudeNotFoundError, ClaudeTimeoutError
|
|
14
|
+
|
|
15
|
+
_URL_PATTERN = re.compile(r"https://\S+")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class CommandResult:
|
|
20
|
+
args: list
|
|
21
|
+
returncode: int
|
|
22
|
+
stdout: str
|
|
23
|
+
stderr: str
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def ok(self) -> bool:
|
|
27
|
+
return self.returncode == 0
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def run(
|
|
31
|
+
binary: str,
|
|
32
|
+
args: Sequence[str],
|
|
33
|
+
*,
|
|
34
|
+
cwd: Optional[str] = None,
|
|
35
|
+
env: Optional[dict] = None,
|
|
36
|
+
input_text: Optional[str] = None,
|
|
37
|
+
timeout: Optional[float] = None,
|
|
38
|
+
check: bool = True,
|
|
39
|
+
) -> CommandResult:
|
|
40
|
+
command = [binary, *args]
|
|
41
|
+
try:
|
|
42
|
+
completed = subprocess.run(
|
|
43
|
+
command,
|
|
44
|
+
cwd=cwd,
|
|
45
|
+
env=env,
|
|
46
|
+
input=input_text,
|
|
47
|
+
capture_output=True,
|
|
48
|
+
text=True,
|
|
49
|
+
timeout=timeout,
|
|
50
|
+
)
|
|
51
|
+
except FileNotFoundError as exc:
|
|
52
|
+
raise ClaudeNotFoundError(
|
|
53
|
+
f"'{binary}' was not found on PATH. Is Claude Code installed?",
|
|
54
|
+
cmd=command,
|
|
55
|
+
) from exc
|
|
56
|
+
except subprocess.TimeoutExpired as exc:
|
|
57
|
+
stdout = exc.stdout.decode() if isinstance(exc.stdout, bytes) else (exc.stdout or "")
|
|
58
|
+
stderr = exc.stderr.decode() if isinstance(exc.stderr, bytes) else (exc.stderr or "")
|
|
59
|
+
raise ClaudeTimeoutError(
|
|
60
|
+
f"'{' '.join(command)}' did not finish within {timeout}s",
|
|
61
|
+
cmd=command,
|
|
62
|
+
stdout=stdout,
|
|
63
|
+
stderr=stderr,
|
|
64
|
+
) from exc
|
|
65
|
+
|
|
66
|
+
result = CommandResult(command, completed.returncode, completed.stdout, completed.stderr)
|
|
67
|
+
if check and not result.ok:
|
|
68
|
+
detail = (result.stderr or result.stdout or "").strip()
|
|
69
|
+
raise ClaudeCLIError(
|
|
70
|
+
f"'{' '.join(command)}' exited with {result.returncode}: {detail}",
|
|
71
|
+
returncode=result.returncode,
|
|
72
|
+
stdout=result.stdout,
|
|
73
|
+
stderr=result.stderr,
|
|
74
|
+
cmd=command,
|
|
75
|
+
)
|
|
76
|
+
return result
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def stream_lines(
|
|
80
|
+
binary: str,
|
|
81
|
+
args: Sequence[str],
|
|
82
|
+
*,
|
|
83
|
+
cwd: Optional[str] = None,
|
|
84
|
+
env: Optional[dict] = None,
|
|
85
|
+
) -> Iterator[str]:
|
|
86
|
+
"""Runs a command and yields decoded stdout lines as they arrive."""
|
|
87
|
+
command = [binary, *args]
|
|
88
|
+
try:
|
|
89
|
+
proc = subprocess.Popen(
|
|
90
|
+
command,
|
|
91
|
+
cwd=cwd,
|
|
92
|
+
env=env,
|
|
93
|
+
stdout=subprocess.PIPE,
|
|
94
|
+
stderr=subprocess.STDOUT,
|
|
95
|
+
text=True,
|
|
96
|
+
bufsize=1,
|
|
97
|
+
)
|
|
98
|
+
except FileNotFoundError as exc:
|
|
99
|
+
raise ClaudeNotFoundError(
|
|
100
|
+
f"'{binary}' was not found on PATH. Is Claude Code installed?",
|
|
101
|
+
cmd=command,
|
|
102
|
+
) from exc
|
|
103
|
+
|
|
104
|
+
assert proc.stdout is not None
|
|
105
|
+
try:
|
|
106
|
+
for line in proc.stdout:
|
|
107
|
+
yield line.rstrip("\n")
|
|
108
|
+
finally:
|
|
109
|
+
proc.stdout.close()
|
|
110
|
+
returncode = proc.wait()
|
|
111
|
+
if returncode != 0:
|
|
112
|
+
raise ClaudeCLIError(
|
|
113
|
+
f"'{' '.join(command)}' exited with {returncode}",
|
|
114
|
+
returncode=returncode,
|
|
115
|
+
cmd=command,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def run_interactive(
|
|
120
|
+
binary: str,
|
|
121
|
+
args: Sequence[str],
|
|
122
|
+
*,
|
|
123
|
+
cwd: Optional[str] = None,
|
|
124
|
+
env: Optional[dict] = None,
|
|
125
|
+
) -> int:
|
|
126
|
+
"""Runs a command with stdio inherited from the current process.
|
|
127
|
+
|
|
128
|
+
For subcommands that need a real terminal (attach, setup-token, an
|
|
129
|
+
interactive mcp/import picker) rather than captured output.
|
|
130
|
+
"""
|
|
131
|
+
command = [binary, *args]
|
|
132
|
+
try:
|
|
133
|
+
return subprocess.call(command, cwd=cwd, env=env)
|
|
134
|
+
except FileNotFoundError as exc:
|
|
135
|
+
raise ClaudeNotFoundError(
|
|
136
|
+
f"'{binary}' was not found on PATH. Is Claude Code installed?",
|
|
137
|
+
cmd=command,
|
|
138
|
+
) from exc
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def oauth_login(
|
|
142
|
+
binary: str,
|
|
143
|
+
args: Sequence[str],
|
|
144
|
+
*,
|
|
145
|
+
cwd: Optional[str] = None,
|
|
146
|
+
env: Optional[dict] = None,
|
|
147
|
+
code: Optional[str] = None,
|
|
148
|
+
code_provider: Optional[Callable[[str], str]] = None,
|
|
149
|
+
on_output: Optional[Callable[[str], None]] = None,
|
|
150
|
+
prompt_marker: str = "Paste code here",
|
|
151
|
+
timeout: float = 180,
|
|
152
|
+
) -> int:
|
|
153
|
+
"""Drives an interactive OAuth login (`claude auth login` / `mcp login`).
|
|
154
|
+
|
|
155
|
+
Streams the child's output (forwarding it to `on_output`, if given) until
|
|
156
|
+
it prints its "paste the code" prompt, then writes back `code` — or
|
|
157
|
+
whatever `code_provider(url)` returns, where `url` is the login URL
|
|
158
|
+
found in the output so far. Falls back to `input()` if neither is given.
|
|
159
|
+
Returns the child's exit code.
|
|
160
|
+
"""
|
|
161
|
+
command = [binary, *args]
|
|
162
|
+
try:
|
|
163
|
+
proc = subprocess.Popen(
|
|
164
|
+
command,
|
|
165
|
+
cwd=cwd,
|
|
166
|
+
env=env,
|
|
167
|
+
stdin=subprocess.PIPE,
|
|
168
|
+
stdout=subprocess.PIPE,
|
|
169
|
+
stderr=subprocess.STDOUT,
|
|
170
|
+
text=True,
|
|
171
|
+
bufsize=1,
|
|
172
|
+
)
|
|
173
|
+
except FileNotFoundError as exc:
|
|
174
|
+
raise ClaudeNotFoundError(
|
|
175
|
+
f"'{binary}' was not found on PATH. Is Claude Code installed?",
|
|
176
|
+
cmd=command,
|
|
177
|
+
) from exc
|
|
178
|
+
|
|
179
|
+
assert proc.stdout is not None and proc.stdin is not None
|
|
180
|
+
fd = proc.stdout.fileno()
|
|
181
|
+
buffer = ""
|
|
182
|
+
url: Optional[str] = None
|
|
183
|
+
deadline = time.time() + timeout
|
|
184
|
+
|
|
185
|
+
while time.time() < deadline and proc.poll() is None:
|
|
186
|
+
ready, _, _ = select.select([fd], [], [], 0.5)
|
|
187
|
+
if not ready:
|
|
188
|
+
continue
|
|
189
|
+
|
|
190
|
+
chunk = os.read(fd, 4096).decode(errors="replace")
|
|
191
|
+
if not chunk:
|
|
192
|
+
break
|
|
193
|
+
|
|
194
|
+
buffer += chunk
|
|
195
|
+
if on_output:
|
|
196
|
+
on_output(chunk)
|
|
197
|
+
|
|
198
|
+
if url is None:
|
|
199
|
+
match = _URL_PATTERN.search(buffer)
|
|
200
|
+
if match:
|
|
201
|
+
url = match.group(0)
|
|
202
|
+
|
|
203
|
+
if prompt_marker in buffer:
|
|
204
|
+
if code is not None:
|
|
205
|
+
entered_code = code
|
|
206
|
+
elif code_provider is not None:
|
|
207
|
+
entered_code = code_provider(url or "")
|
|
208
|
+
else:
|
|
209
|
+
entered_code = input("\nPaste the code from the browser here: ").strip()
|
|
210
|
+
|
|
211
|
+
proc.stdin.write(entered_code + "\n")
|
|
212
|
+
proc.stdin.flush()
|
|
213
|
+
proc.stdin.close()
|
|
214
|
+
buffer = ""
|
|
215
|
+
break
|
|
216
|
+
else:
|
|
217
|
+
if proc.poll() is None:
|
|
218
|
+
proc.kill()
|
|
219
|
+
raise ClaudeTimeoutError(
|
|
220
|
+
f"'{' '.join(command)}' did not produce a login prompt within {timeout}s",
|
|
221
|
+
cmd=command,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
drain_deadline = time.time() + 30
|
|
225
|
+
while proc.poll() is None and time.time() < drain_deadline:
|
|
226
|
+
ready, _, _ = select.select([fd], [], [], 0.5)
|
|
227
|
+
if ready:
|
|
228
|
+
chunk = os.read(fd, 4096).decode(errors="replace")
|
|
229
|
+
if not chunk:
|
|
230
|
+
break
|
|
231
|
+
if on_output:
|
|
232
|
+
on_output(chunk)
|
|
233
|
+
|
|
234
|
+
if proc.poll() is None:
|
|
235
|
+
proc.kill()
|
|
236
|
+
raise ClaudeTimeoutError(
|
|
237
|
+
f"'{' '.join(command)}' did not finish after the code was submitted",
|
|
238
|
+
cmd=command,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
return proc.returncode
|
pyclaudecli/client.py
ADDED
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
"""ClaudeCLI: a Python wrapper around every `claude` CLI command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from typing import Any, Callable, Dict, Iterator, List, Optional, Sequence, Union
|
|
8
|
+
|
|
9
|
+
from . import _process
|
|
10
|
+
from .exceptions import ClaudeCLIError
|
|
11
|
+
|
|
12
|
+
_ANSI_PATTERN = re.compile(r"\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07]*\x07")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _kebab(name: str) -> str:
|
|
16
|
+
return "--" + name.replace("_", "-")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def build_flags(options: Dict[str, Any]) -> List[str]:
|
|
20
|
+
"""Converts a {python_name: value} dict into CLI flags.
|
|
21
|
+
|
|
22
|
+
None/False -> omitted. True -> bare flag. list/tuple -> the flag once,
|
|
23
|
+
followed by each value (matches commander.js variadic `<x...>` options).
|
|
24
|
+
Anything else -> the flag followed by str(value).
|
|
25
|
+
"""
|
|
26
|
+
flags: List[str] = []
|
|
27
|
+
for key, value in options.items():
|
|
28
|
+
if value is None or value is False:
|
|
29
|
+
continue
|
|
30
|
+
flag = _kebab(key)
|
|
31
|
+
if value is True:
|
|
32
|
+
flags.append(flag)
|
|
33
|
+
elif isinstance(value, (list, tuple)):
|
|
34
|
+
if not value:
|
|
35
|
+
continue
|
|
36
|
+
flags.append(flag)
|
|
37
|
+
flags.extend(str(v) for v in value)
|
|
38
|
+
else:
|
|
39
|
+
flags.append(flag)
|
|
40
|
+
flags.append(str(value))
|
|
41
|
+
return flags
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ClaudeCLI:
|
|
45
|
+
"""Wraps the `claude` CLI (Claude Code) for programmatic use.
|
|
46
|
+
|
|
47
|
+
Every method shells out to the real `claude` binary, so whatever is
|
|
48
|
+
installed and authenticated on this machine is what runs. Methods that
|
|
49
|
+
capture output raise `ClaudeCLIError` (or a subclass) on a non-zero exit;
|
|
50
|
+
pass `check=False` to a raw call via `.run()` if you'd rather inspect the
|
|
51
|
+
result yourself.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
binary: str = "claude",
|
|
57
|
+
*,
|
|
58
|
+
cwd: Optional[str] = None,
|
|
59
|
+
env: Optional[dict] = None,
|
|
60
|
+
timeout: Optional[float] = None,
|
|
61
|
+
) -> None:
|
|
62
|
+
self.binary = binary
|
|
63
|
+
self.cwd = cwd
|
|
64
|
+
self.env = env
|
|
65
|
+
self.timeout = timeout
|
|
66
|
+
|
|
67
|
+
# -- internals -----------------------------------------------------
|
|
68
|
+
|
|
69
|
+
def run(
|
|
70
|
+
self,
|
|
71
|
+
args: Sequence[str],
|
|
72
|
+
*,
|
|
73
|
+
input_text: Optional[str] = None,
|
|
74
|
+
timeout: Optional[float] = None,
|
|
75
|
+
check: bool = True,
|
|
76
|
+
) -> _process.CommandResult:
|
|
77
|
+
"""Runs `claude <args>` and returns the captured result."""
|
|
78
|
+
return _process.run(
|
|
79
|
+
self.binary,
|
|
80
|
+
args,
|
|
81
|
+
cwd=self.cwd,
|
|
82
|
+
env=self.env,
|
|
83
|
+
input_text=input_text,
|
|
84
|
+
timeout=timeout if timeout is not None else self.timeout,
|
|
85
|
+
check=check,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def _text(self, args: Sequence[str], **kwargs: Any) -> str:
|
|
89
|
+
return self.run(args, **kwargs).stdout
|
|
90
|
+
|
|
91
|
+
def _json(self, args: Sequence[str], **kwargs: Any) -> Any:
|
|
92
|
+
result = self.run(args, **kwargs)
|
|
93
|
+
try:
|
|
94
|
+
return json.loads(result.stdout)
|
|
95
|
+
except json.JSONDecodeError as exc:
|
|
96
|
+
raise ClaudeCLIError(
|
|
97
|
+
f"Expected JSON from `{self.binary} {' '.join(args)}`, "
|
|
98
|
+
f"got: {result.stdout[:200]!r}",
|
|
99
|
+
returncode=result.returncode,
|
|
100
|
+
stdout=result.stdout,
|
|
101
|
+
stderr=result.stderr,
|
|
102
|
+
cmd=[self.binary, *args],
|
|
103
|
+
) from exc
|
|
104
|
+
|
|
105
|
+
def _interactive(self, args: Sequence[str]) -> int:
|
|
106
|
+
return _process.run_interactive(self.binary, args, cwd=self.cwd, env=self.env)
|
|
107
|
+
|
|
108
|
+
# -- version / health ------------------------------------------------
|
|
109
|
+
|
|
110
|
+
def version(self) -> str:
|
|
111
|
+
"""`claude --version`"""
|
|
112
|
+
return self._text(["--version"]).strip()
|
|
113
|
+
|
|
114
|
+
def doctor(self) -> str:
|
|
115
|
+
"""`claude doctor` — health-checks the local installation."""
|
|
116
|
+
return self._text(["doctor"])
|
|
117
|
+
|
|
118
|
+
def update(self) -> str:
|
|
119
|
+
"""`claude update` — checks for and installs updates."""
|
|
120
|
+
return self._text(["update"])
|
|
121
|
+
|
|
122
|
+
def install(self, target: Optional[str] = None, *, force: bool = False) -> str:
|
|
123
|
+
"""`claude install [target]` (target: stable, latest, or a version)."""
|
|
124
|
+
args = ["install"] + ([target] if target else []) + build_flags({"force": force})
|
|
125
|
+
return self._text(args)
|
|
126
|
+
|
|
127
|
+
# -- prompting (non-interactive / --print) ----------------------------
|
|
128
|
+
|
|
129
|
+
def prompt(
|
|
130
|
+
self,
|
|
131
|
+
text: str,
|
|
132
|
+
*,
|
|
133
|
+
model: Optional[str] = None,
|
|
134
|
+
output_format: Optional[str] = None,
|
|
135
|
+
system_prompt: Optional[str] = None,
|
|
136
|
+
append_system_prompt: Optional[str] = None,
|
|
137
|
+
allowed_tools: Optional[Sequence[str]] = None,
|
|
138
|
+
disallowed_tools: Optional[Sequence[str]] = None,
|
|
139
|
+
tools: Optional[Sequence[str]] = None,
|
|
140
|
+
add_dir: Optional[Sequence[str]] = None,
|
|
141
|
+
permission_mode: Optional[str] = None,
|
|
142
|
+
permission_prompts: Optional[str] = None,
|
|
143
|
+
mcp_config: Optional[Sequence[str]] = None,
|
|
144
|
+
settings: Optional[str] = None,
|
|
145
|
+
session_id: Optional[str] = None,
|
|
146
|
+
resume: Optional[str] = None,
|
|
147
|
+
continue_session: bool = False,
|
|
148
|
+
fork_session: bool = False,
|
|
149
|
+
effort: Optional[str] = None,
|
|
150
|
+
fallback_model: Optional[str] = None,
|
|
151
|
+
max_budget_usd: Optional[float] = None,
|
|
152
|
+
json_schema: Optional[str] = None,
|
|
153
|
+
betas: Optional[Sequence[str]] = None,
|
|
154
|
+
no_session_persistence: bool = False,
|
|
155
|
+
dangerously_skip_permissions: bool = False,
|
|
156
|
+
restricted: bool = False,
|
|
157
|
+
input_text: Optional[str] = None,
|
|
158
|
+
timeout: Optional[float] = None,
|
|
159
|
+
extra_flags: Optional[Dict[str, Any]] = None,
|
|
160
|
+
) -> Union[str, dict]:
|
|
161
|
+
"""Runs a single non-interactive prompt (`claude --print ...`).
|
|
162
|
+
|
|
163
|
+
Returns the response text, or the parsed JSON result object when
|
|
164
|
+
`output_format="json"`. Anything not exposed as a named parameter
|
|
165
|
+
(e.g. `--cloud`, `--worktree`, `--file`) can be passed via
|
|
166
|
+
`extra_flags={"worktree": True}`.
|
|
167
|
+
"""
|
|
168
|
+
options = dict(
|
|
169
|
+
model=model,
|
|
170
|
+
output_format=output_format,
|
|
171
|
+
system_prompt=system_prompt,
|
|
172
|
+
append_system_prompt=append_system_prompt,
|
|
173
|
+
allowed_tools=allowed_tools,
|
|
174
|
+
disallowed_tools=disallowed_tools,
|
|
175
|
+
tools=tools,
|
|
176
|
+
add_dir=add_dir,
|
|
177
|
+
permission_mode=permission_mode,
|
|
178
|
+
permission_prompts=permission_prompts,
|
|
179
|
+
mcp_config=mcp_config,
|
|
180
|
+
settings=settings,
|
|
181
|
+
session_id=session_id,
|
|
182
|
+
resume=resume,
|
|
183
|
+
fork_session=fork_session,
|
|
184
|
+
effort=effort,
|
|
185
|
+
fallback_model=fallback_model,
|
|
186
|
+
max_budget_usd=max_budget_usd,
|
|
187
|
+
json_schema=json_schema,
|
|
188
|
+
betas=betas,
|
|
189
|
+
no_session_persistence=no_session_persistence,
|
|
190
|
+
dangerously_skip_permissions=dangerously_skip_permissions,
|
|
191
|
+
restricted=restricted,
|
|
192
|
+
)
|
|
193
|
+
options.update(extra_flags or {})
|
|
194
|
+
if options.get("output_format") == "stream-json":
|
|
195
|
+
options["verbose"] = True # required by the CLI for --print + stream-json
|
|
196
|
+
|
|
197
|
+
args = ["--print"] + build_flags(options)
|
|
198
|
+
if continue_session:
|
|
199
|
+
args.append("--continue")
|
|
200
|
+
args.append(text)
|
|
201
|
+
|
|
202
|
+
result = self.run(args, input_text=input_text, timeout=timeout)
|
|
203
|
+
if output_format == "json":
|
|
204
|
+
return json.loads(result.stdout)
|
|
205
|
+
return result.stdout
|
|
206
|
+
|
|
207
|
+
def prompt_json(self, text: str, **kwargs: Any) -> dict:
|
|
208
|
+
"""`prompt()` forced to `--output-format json`; returns the parsed dict."""
|
|
209
|
+
kwargs["output_format"] = "json"
|
|
210
|
+
return self.prompt(text, **kwargs) # type: ignore[return-value]
|
|
211
|
+
|
|
212
|
+
def prompt_stream(self, text: str, **kwargs: Any) -> Iterator[dict]:
|
|
213
|
+
"""`prompt()` with `--output-format stream-json`; yields each event dict."""
|
|
214
|
+
extra_flags = dict(kwargs.pop("extra_flags", None) or {})
|
|
215
|
+
extra_flags.update(kwargs)
|
|
216
|
+
extra_flags["output_format"] = "stream-json"
|
|
217
|
+
extra_flags["verbose"] = True # required by the CLI for --print + stream-json
|
|
218
|
+
|
|
219
|
+
options = {k: v for k, v in extra_flags.items() if v is not None and v is not False}
|
|
220
|
+
args = ["--print"] + build_flags(options) + [text]
|
|
221
|
+
for line in _process.stream_lines(self.binary, args, cwd=self.cwd, env=self.env):
|
|
222
|
+
line = line.strip()
|
|
223
|
+
if line:
|
|
224
|
+
yield json.loads(line)
|
|
225
|
+
|
|
226
|
+
# -- background sessions ("agents") -----------------------------------
|
|
227
|
+
|
|
228
|
+
def start_background(
|
|
229
|
+
self,
|
|
230
|
+
task: str,
|
|
231
|
+
*,
|
|
232
|
+
model: Optional[str] = None,
|
|
233
|
+
name: Optional[str] = None,
|
|
234
|
+
resume: Optional[str] = None,
|
|
235
|
+
extra_flags: Optional[Dict[str, Any]] = None,
|
|
236
|
+
) -> str:
|
|
237
|
+
"""`claude --bg <task>`; returns the short session id it prints."""
|
|
238
|
+
options = dict(model=model, name=name, resume=resume)
|
|
239
|
+
options.update(extra_flags or {})
|
|
240
|
+
args = ["--bg"] + build_flags(options) + [task]
|
|
241
|
+
output = self._text(args)
|
|
242
|
+
match = re.search(r"backgrounded\W*([0-9a-fA-F]+)", output)
|
|
243
|
+
if not match:
|
|
244
|
+
raise ClaudeCLIError(
|
|
245
|
+
f"Could not find a session id in `claude --bg` output: {output!r}",
|
|
246
|
+
stdout=output,
|
|
247
|
+
cmd=[self.binary, *args],
|
|
248
|
+
)
|
|
249
|
+
return match.group(1)
|
|
250
|
+
|
|
251
|
+
def list_agents(self, *, all: bool = False, cwd: Optional[str] = None) -> List[dict]:
|
|
252
|
+
"""`claude agents --json`; lists interactive and background sessions."""
|
|
253
|
+
args = ["agents", "--json"] + build_flags({"all": all, "cwd": cwd})
|
|
254
|
+
return self._json(args)
|
|
255
|
+
|
|
256
|
+
def attach(self, session_id: str) -> int:
|
|
257
|
+
"""`claude attach <id>` — opens a background session in this terminal.
|
|
258
|
+
|
|
259
|
+
Requires a real TTY (stdio is inherited, not captured).
|
|
260
|
+
"""
|
|
261
|
+
return self._interactive(["attach", session_id])
|
|
262
|
+
|
|
263
|
+
def logs(self, session_id: str, *, strip_ansi: bool = False) -> str:
|
|
264
|
+
"""`claude logs <id>` — a raw terminal snapshot of recent output.
|
|
265
|
+
|
|
266
|
+
Pass `strip_ansi=True` for a best-effort plain-text version.
|
|
267
|
+
"""
|
|
268
|
+
text = self._text(["logs", session_id])
|
|
269
|
+
return _ANSI_PATTERN.sub("", text) if strip_ansi else text
|
|
270
|
+
|
|
271
|
+
def stop(self, session_id: str) -> str:
|
|
272
|
+
"""`claude stop <id>` — stops a background session (keeps its state)."""
|
|
273
|
+
return self._text(["stop", session_id])
|
|
274
|
+
|
|
275
|
+
def rm(self, session_id: str) -> str:
|
|
276
|
+
"""`claude rm <id>` — deletes a (stopped) background session."""
|
|
277
|
+
return self._text(["rm", session_id])
|
|
278
|
+
|
|
279
|
+
def respawn(self, session_id: Optional[str] = None, *, all: bool = False) -> str:
|
|
280
|
+
"""`claude respawn [id]` / `claude respawn --all`."""
|
|
281
|
+
args = ["respawn"] + ([session_id] if session_id else []) + build_flags({"all": all})
|
|
282
|
+
return self._text(args)
|
|
283
|
+
|
|
284
|
+
# -- auth --------------------------------------------------------------
|
|
285
|
+
|
|
286
|
+
def auth_status(self, *, as_json: bool = True) -> Union[dict, str]:
|
|
287
|
+
"""`claude auth status`."""
|
|
288
|
+
if as_json:
|
|
289
|
+
return self._json(["auth", "status", "--json"])
|
|
290
|
+
return self._text(["auth", "status", "--text"])
|
|
291
|
+
|
|
292
|
+
def auth_login(
|
|
293
|
+
self,
|
|
294
|
+
*,
|
|
295
|
+
email: Optional[str] = None,
|
|
296
|
+
console: bool = False,
|
|
297
|
+
sso: bool = False,
|
|
298
|
+
code: Optional[str] = None,
|
|
299
|
+
code_provider: Optional[Callable[[str], str]] = None,
|
|
300
|
+
on_output: Optional[Callable[[str], None]] = None,
|
|
301
|
+
timeout: float = 180,
|
|
302
|
+
) -> int:
|
|
303
|
+
"""Drives `claude auth login` end to end.
|
|
304
|
+
|
|
305
|
+
Prints (via `on_output`, or stdout if not given) the sign-in URL for
|
|
306
|
+
you to open and complete in a browser, then forwards the resulting
|
|
307
|
+
code back to the CLI. Pass `code` to supply it non-interactively, or
|
|
308
|
+
`code_provider(url)` to fetch it programmatically (e.g. from your own
|
|
309
|
+
browser-automation step); otherwise it's read from stdin.
|
|
310
|
+
"""
|
|
311
|
+
args = ["auth", "login"]
|
|
312
|
+
args += build_flags({"console": console, "sso": sso, "email": email})
|
|
313
|
+
return _process.oauth_login(
|
|
314
|
+
self.binary,
|
|
315
|
+
args,
|
|
316
|
+
cwd=self.cwd,
|
|
317
|
+
env=self.env,
|
|
318
|
+
code=code,
|
|
319
|
+
code_provider=code_provider,
|
|
320
|
+
on_output=on_output or (lambda chunk: print(chunk, end="", flush=True)),
|
|
321
|
+
timeout=timeout,
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
def auth_logout(self) -> str:
|
|
325
|
+
"""`claude auth logout`."""
|
|
326
|
+
return self._text(["auth", "logout"])
|
|
327
|
+
|
|
328
|
+
def setup_token(self) -> int:
|
|
329
|
+
"""`claude setup-token` — sets up a long-lived auth token.
|
|
330
|
+
|
|
331
|
+
Interactive (may involve a browser step); stdio is inherited.
|
|
332
|
+
"""
|
|
333
|
+
return self._interactive(["setup-token"])
|
|
334
|
+
|
|
335
|
+
# -- MCP servers ---------------------------------------------------------
|
|
336
|
+
|
|
337
|
+
def mcp_list(self) -> str:
|
|
338
|
+
"""`claude mcp list`."""
|
|
339
|
+
return self._text(["mcp", "list"])
|
|
340
|
+
|
|
341
|
+
def mcp_get(self, name: str) -> str:
|
|
342
|
+
"""`claude mcp get <name>`."""
|
|
343
|
+
return self._text(["mcp", "get", name])
|
|
344
|
+
|
|
345
|
+
def mcp_add(
|
|
346
|
+
self,
|
|
347
|
+
name: str,
|
|
348
|
+
command_or_url: str,
|
|
349
|
+
*args: str,
|
|
350
|
+
transport: Optional[str] = None,
|
|
351
|
+
header: Optional[Sequence[str]] = None,
|
|
352
|
+
env: Optional[Sequence[str]] = None,
|
|
353
|
+
scope: Optional[str] = None,
|
|
354
|
+
client_id: Optional[str] = None,
|
|
355
|
+
client_secret: bool = False,
|
|
356
|
+
callback_port: Optional[int] = None,
|
|
357
|
+
) -> str:
|
|
358
|
+
"""`claude mcp add <name> <commandOrUrl> [args...]`."""
|
|
359
|
+
options = build_flags(
|
|
360
|
+
dict(
|
|
361
|
+
transport=transport,
|
|
362
|
+
header=header,
|
|
363
|
+
env=env,
|
|
364
|
+
scope=scope,
|
|
365
|
+
client_id=client_id,
|
|
366
|
+
client_secret=client_secret,
|
|
367
|
+
callback_port=callback_port,
|
|
368
|
+
)
|
|
369
|
+
)
|
|
370
|
+
return self._text(["mcp", "add", *options, name, command_or_url, *args])
|
|
371
|
+
|
|
372
|
+
def mcp_add_json(
|
|
373
|
+
self,
|
|
374
|
+
name: str,
|
|
375
|
+
config: Union[str, dict],
|
|
376
|
+
*,
|
|
377
|
+
scope: Optional[str] = None,
|
|
378
|
+
client_secret: bool = False,
|
|
379
|
+
) -> str:
|
|
380
|
+
"""`claude mcp add-json <name> <json>`. `config` may be a dict or JSON string."""
|
|
381
|
+
payload = json.dumps(config) if isinstance(config, dict) else config
|
|
382
|
+
options = build_flags({"scope": scope, "client_secret": client_secret})
|
|
383
|
+
return self._text(["mcp", "add-json", *options, name, payload])
|
|
384
|
+
|
|
385
|
+
def mcp_add_from_claude_desktop(self, *, scope: Optional[str] = None) -> str:
|
|
386
|
+
"""`claude mcp add-from-claude-desktop` (Mac and WSL only)."""
|
|
387
|
+
return self._text(["mcp", "add-from-claude-desktop", *build_flags({"scope": scope})])
|
|
388
|
+
|
|
389
|
+
def mcp_remove(self, name: str, *, scope: Optional[str] = None) -> str:
|
|
390
|
+
"""`claude mcp remove <name>`."""
|
|
391
|
+
return self._text(["mcp", "remove", *build_flags({"scope": scope}), name])
|
|
392
|
+
|
|
393
|
+
def mcp_login(self, name: str, *, no_browser: bool = False) -> int:
|
|
394
|
+
"""`claude mcp login <name>` — interactive OAuth for an MCP server."""
|
|
395
|
+
return self._interactive(["mcp", "login", *build_flags({"no_browser": no_browser}), name])
|
|
396
|
+
|
|
397
|
+
def mcp_logout(self, name: str) -> str:
|
|
398
|
+
"""`claude mcp logout <name>`."""
|
|
399
|
+
return self._text(["mcp", "logout", name])
|
|
400
|
+
|
|
401
|
+
def mcp_reset_project_choices(self) -> str:
|
|
402
|
+
"""`claude mcp reset-project-choices`."""
|
|
403
|
+
return self._text(["mcp", "reset-project-choices"])
|
|
404
|
+
|
|
405
|
+
# -- plugins ---------------------------------------------------------------
|
|
406
|
+
|
|
407
|
+
def plugin_list(self, *, as_json: bool = False) -> Union[str, list]:
|
|
408
|
+
"""`claude plugin list`."""
|
|
409
|
+
args = ["plugin", "list"] + build_flags({"json": as_json})
|
|
410
|
+
return self._json(args) if as_json else self._text(args)
|
|
411
|
+
|
|
412
|
+
def plugin_install(
|
|
413
|
+
self,
|
|
414
|
+
plugin: str,
|
|
415
|
+
*,
|
|
416
|
+
scope: Optional[str] = None,
|
|
417
|
+
yes: bool = False,
|
|
418
|
+
config: Optional[Sequence[str]] = None,
|
|
419
|
+
accept_command: Optional[str] = None,
|
|
420
|
+
as_json: bool = False,
|
|
421
|
+
) -> Union[str, dict]:
|
|
422
|
+
"""`claude plugin install <plugin>` (use `plugin@marketplace` to pin one)."""
|
|
423
|
+
options = build_flags(
|
|
424
|
+
dict(scope=scope, yes=yes, config=config, accept_command=accept_command, json=as_json)
|
|
425
|
+
)
|
|
426
|
+
args = ["plugin", "install", *options, plugin]
|
|
427
|
+
return self._json(args) if as_json else self._text(args)
|
|
428
|
+
|
|
429
|
+
def plugin_uninstall(
|
|
430
|
+
self,
|
|
431
|
+
plugin: str,
|
|
432
|
+
*,
|
|
433
|
+
scope: Optional[str] = None,
|
|
434
|
+
keep_data: bool = False,
|
|
435
|
+
prune: bool = False,
|
|
436
|
+
yes: bool = False,
|
|
437
|
+
as_json: bool = False,
|
|
438
|
+
) -> Union[str, dict]:
|
|
439
|
+
"""`claude plugin uninstall <plugin>`."""
|
|
440
|
+
options = build_flags(
|
|
441
|
+
dict(scope=scope, keep_data=keep_data, prune=prune, yes=yes, json=as_json)
|
|
442
|
+
)
|
|
443
|
+
args = ["plugin", "uninstall", *options, plugin]
|
|
444
|
+
return self._json(args) if as_json else self._text(args)
|
|
445
|
+
|
|
446
|
+
def plugin_enable(self, plugin: str, *, scope: Optional[str] = None, as_json: bool = False) -> Union[str, dict]:
|
|
447
|
+
"""`claude plugin enable <plugin>`."""
|
|
448
|
+
args = ["plugin", "enable", *build_flags({"scope": scope, "json": as_json}), plugin]
|
|
449
|
+
return self._json(args) if as_json else self._text(args)
|
|
450
|
+
|
|
451
|
+
def plugin_disable(
|
|
452
|
+
self,
|
|
453
|
+
plugin: Optional[str] = None,
|
|
454
|
+
*,
|
|
455
|
+
all: bool = False,
|
|
456
|
+
scope: Optional[str] = None,
|
|
457
|
+
as_json: bool = False,
|
|
458
|
+
) -> Union[str, dict]:
|
|
459
|
+
"""`claude plugin disable [plugin]` (or `all=True` for every plugin)."""
|
|
460
|
+
options = build_flags({"all": all, "scope": scope, "json": as_json})
|
|
461
|
+
args = ["plugin", "disable", *options] + ([plugin] if plugin else [])
|
|
462
|
+
return self._json(args) if as_json else self._text(args)
|
|
463
|
+
|
|
464
|
+
def plugin_update(
|
|
465
|
+
self,
|
|
466
|
+
plugin: str,
|
|
467
|
+
*,
|
|
468
|
+
scope: Optional[str] = None,
|
|
469
|
+
yes: bool = False,
|
|
470
|
+
accept_command: Optional[str] = None,
|
|
471
|
+
as_json: bool = False,
|
|
472
|
+
) -> Union[str, dict]:
|
|
473
|
+
"""`claude plugin update <plugin>`."""
|
|
474
|
+
options = build_flags(dict(scope=scope, yes=yes, accept_command=accept_command, json=as_json))
|
|
475
|
+
args = ["plugin", "update", *options, plugin]
|
|
476
|
+
return self._json(args) if as_json else self._text(args)
|
|
477
|
+
|
|
478
|
+
def plugin_details(self, name: str) -> str:
|
|
479
|
+
"""`claude plugin details <name>`."""
|
|
480
|
+
return self._text(["plugin", "details", name])
|
|
481
|
+
|
|
482
|
+
def plugin_validate(self, path: str, *, strict: bool = False, as_json: bool = False) -> Union[str, dict]:
|
|
483
|
+
"""`claude plugin validate <path>`."""
|
|
484
|
+
args = ["plugin", "validate", *build_flags({"strict": strict, "json": as_json}), path]
|
|
485
|
+
return self._json(args) if as_json else self._text(args)
|
|
486
|
+
|
|
487
|
+
def plugin_prune(self, *, scope: Optional[str] = None, yes: bool = False, dry_run: bool = False) -> str:
|
|
488
|
+
"""`claude plugin prune` — removes orphaned auto-installed dependencies."""
|
|
489
|
+
args = ["plugin", "prune", *build_flags({"scope": scope, "yes": yes, "dry_run": dry_run})]
|
|
490
|
+
return self._text(args)
|
|
491
|
+
|
|
492
|
+
def plugin_marketplace_list(self) -> str:
|
|
493
|
+
"""`claude plugin marketplace list`."""
|
|
494
|
+
return self._text(["plugin", "marketplace", "list"])
|
|
495
|
+
|
|
496
|
+
def plugin_marketplace_add(self, source: str) -> str:
|
|
497
|
+
"""`claude plugin marketplace add <source>` (URL, path, or GitHub repo)."""
|
|
498
|
+
return self._text(["plugin", "marketplace", "add", source])
|
|
499
|
+
|
|
500
|
+
def plugin_marketplace_remove(self, name: str) -> str:
|
|
501
|
+
"""`claude plugin marketplace remove <name>`."""
|
|
502
|
+
return self._text(["plugin", "marketplace", "remove", name])
|
|
503
|
+
|
|
504
|
+
def plugin_marketplace_update(self, name: Optional[str] = None) -> str:
|
|
505
|
+
"""`claude plugin marketplace update [name]` (updates all if omitted)."""
|
|
506
|
+
return self._text(["plugin", "marketplace", "update"] + ([name] if name else []))
|
|
507
|
+
|
|
508
|
+
# -- project state -----------------------------------------------------
|
|
509
|
+
|
|
510
|
+
def project_purge(self, path: Optional[str] = None) -> str:
|
|
511
|
+
"""`claude project purge [path]` — deletes all local state for a project."""
|
|
512
|
+
return self._text(["project", "purge"] + ([path] if path else []))
|
|
513
|
+
|
|
514
|
+
# -- auto mode -----------------------------------------------------------
|
|
515
|
+
|
|
516
|
+
def auto_mode_config(self) -> dict:
|
|
517
|
+
"""`claude auto-mode config` — the effective classifier config as JSON."""
|
|
518
|
+
return self._json(["auto-mode", "config"])
|
|
519
|
+
|
|
520
|
+
def auto_mode_defaults(self) -> dict:
|
|
521
|
+
"""`claude auto-mode defaults` — the shipped default rules as JSON."""
|
|
522
|
+
return self._json(["auto-mode", "defaults"])
|
|
523
|
+
|
|
524
|
+
def auto_mode_reset(self) -> str:
|
|
525
|
+
"""`claude auto-mode reset` — removes custom rules from user settings."""
|
|
526
|
+
return self._text(["auto-mode", "reset"])
|
|
527
|
+
|
|
528
|
+
def auto_mode_critique(self) -> str:
|
|
529
|
+
"""`claude auto-mode critique` — AI feedback on your custom rules."""
|
|
530
|
+
return self._text(["auto-mode", "critique"])
|
|
531
|
+
|
|
532
|
+
# -- misc ---------------------------------------------------------------
|
|
533
|
+
|
|
534
|
+
def import_config(
|
|
535
|
+
self,
|
|
536
|
+
source: str,
|
|
537
|
+
*,
|
|
538
|
+
dry_run: bool = False,
|
|
539
|
+
yes: Union[bool, str, None] = None,
|
|
540
|
+
) -> str:
|
|
541
|
+
"""`claude import <source>` (source: codex, gemini, or cursor).
|
|
542
|
+
|
|
543
|
+
Pass `dry_run=True` and/or `yes=True` (or a digest string) for
|
|
544
|
+
non-interactive use; otherwise the CLI may prompt interactively.
|
|
545
|
+
"""
|
|
546
|
+
args = ["import", source] + build_flags({"dry_run": dry_run, "yes": yes})
|
|
547
|
+
return self._text(args)
|
|
548
|
+
|
|
549
|
+
def ultrareview(
|
|
550
|
+
self,
|
|
551
|
+
target: Optional[str] = None,
|
|
552
|
+
*,
|
|
553
|
+
as_json: bool = False,
|
|
554
|
+
post: bool = False,
|
|
555
|
+
timeout_minutes: Optional[int] = None,
|
|
556
|
+
timeout: Optional[float] = None,
|
|
557
|
+
) -> Union[str, dict]:
|
|
558
|
+
"""`claude ultrareview [target]` — cloud multi-agent code review.
|
|
559
|
+
|
|
560
|
+
`target` is a PR number or base branch; omit it to review the current
|
|
561
|
+
branch. `timeout` (seconds) bounds this call locally; `timeout_minutes`
|
|
562
|
+
is the CLI's own remote-review budget (default 45).
|
|
563
|
+
"""
|
|
564
|
+
options = build_flags({"json": as_json, "post": post, "timeout": timeout_minutes})
|
|
565
|
+
args = ["ultrareview", *options] + ([target] if target else [])
|
|
566
|
+
result = self.run(args, timeout=timeout)
|
|
567
|
+
if as_json:
|
|
568
|
+
return json.loads(result.stdout)
|
|
569
|
+
return result.stdout
|
|
570
|
+
|
|
571
|
+
def start_gateway(self, *, config: Optional[str] = None):
|
|
572
|
+
"""`claude gateway` — starts the enterprise auth/telemetry gateway.
|
|
573
|
+
|
|
574
|
+
This is a long-running server, so it's started with `Popen` (not
|
|
575
|
+
waited on) and the process handle is returned; call `.terminate()`
|
|
576
|
+
on it when you're done.
|
|
577
|
+
"""
|
|
578
|
+
import subprocess
|
|
579
|
+
|
|
580
|
+
args = [self.binary, "gateway", *build_flags({"config": config})]
|
|
581
|
+
return subprocess.Popen(args, cwd=self.cwd, env=self.env)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Exceptions raised by pyclaudecli."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional, Sequence
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ClaudeCLIError(Exception):
|
|
9
|
+
"""Raised when the `claude` CLI exits with a non-zero status."""
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
message: str,
|
|
14
|
+
*,
|
|
15
|
+
returncode: Optional[int] = None,
|
|
16
|
+
stdout: Optional[str] = None,
|
|
17
|
+
stderr: Optional[str] = None,
|
|
18
|
+
cmd: Optional[Sequence[str]] = None,
|
|
19
|
+
) -> None:
|
|
20
|
+
super().__init__(message)
|
|
21
|
+
self.returncode = returncode
|
|
22
|
+
self.stdout = stdout
|
|
23
|
+
self.stderr = stderr
|
|
24
|
+
self.cmd = list(cmd) if cmd is not None else None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ClaudeNotFoundError(ClaudeCLIError):
|
|
28
|
+
"""Raised when the `claude` binary can't be found on PATH."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ClaudeTimeoutError(ClaudeCLIError):
|
|
32
|
+
"""Raised when a `claude` invocation exceeds its timeout."""
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pyclaudecli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python library wrapping the Claude Code CLI: prompts, background agents, auth, MCP, plugins, and more
|
|
5
|
+
Author-email: Suriya Ravichandran <suriyaravichandran@itrendsolution.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: anthropic,automation,claude,claude-code,cli,sdk,wrapper
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Python: >=3.8
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# pyclaudecli
|
|
25
|
+
|
|
26
|
+
A Python library that wraps the [Claude Code](https://claude.com/claude-code) CLI (`claude`) so you can drive it from Python instead of shelling out by hand: one-shot prompts, JSON/streaming output, background agents, authentication, MCP servers, plugins, and the rest of the CLI's surface.
|
|
27
|
+
|
|
28
|
+
It's a thin wrapper, not a reimplementation — every call runs the real `claude` binary, so it always reflects whatever version, auth, and config you have installed locally.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install pyclaudecli
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Requires the `claude` CLI itself to be installed and on `PATH` (see the [Claude Code docs](https://claude.com/claude-code)).
|
|
37
|
+
|
|
38
|
+
## Quickstart
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from pyclaudecli import ClaudeCLI
|
|
42
|
+
|
|
43
|
+
claude = ClaudeCLI()
|
|
44
|
+
|
|
45
|
+
# One-shot prompt
|
|
46
|
+
print(claude.prompt("Summarize this repo's README.", model="haiku"))
|
|
47
|
+
|
|
48
|
+
# Structured result (cost, session id, etc.)
|
|
49
|
+
result = claude.prompt_json("What's 2+2?", model="haiku")
|
|
50
|
+
print(result["result"], result["total_cost_usd"])
|
|
51
|
+
|
|
52
|
+
# Live streaming events
|
|
53
|
+
for event in claude.prompt_stream("Write a haiku about tests.", model="haiku"):
|
|
54
|
+
print(event["type"])
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Background agents
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
session_id = claude.start_background("Refactor the auth module", model="sonnet")
|
|
61
|
+
claude.list_agents()
|
|
62
|
+
claude.logs(session_id, strip_ansi=True)
|
|
63
|
+
claude.stop(session_id)
|
|
64
|
+
claude.rm(session_id)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Auth
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
claude.auth_status() # {"loggedIn": True, "email": "...", ...}
|
|
71
|
+
|
|
72
|
+
# Prints the sign-in URL, then forwards the pasted code to finish login
|
|
73
|
+
claude.auth_login()
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## MCP servers and plugins
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
claude.mcp_add("sentry", "https://mcp.sentry.dev/mcp", transport="http")
|
|
80
|
+
claude.mcp_list()
|
|
81
|
+
|
|
82
|
+
claude.plugin_install("some-plugin", yes=True)
|
|
83
|
+
claude.plugin_list(as_json=True)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
See `ClaudeCLI`'s docstrings for the full method list — it covers every top-level `claude` command (`auth`, `mcp`, `plugin`, `project`, `agents`/background sessions, `auto-mode`, `doctor`, `update`, `install`, `import`, `ultrareview`, `gateway`) plus the main prompt flags. Anything not exposed as a named parameter can still be passed through via each method's `extra_flags` dict.
|
|
87
|
+
|
|
88
|
+
## Errors
|
|
89
|
+
|
|
90
|
+
All CLI failures raise `ClaudeCLIError` (or `ClaudeNotFoundError` / `ClaudeTimeoutError`), carrying `returncode`, `stdout`, and `stderr`.
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
MIT
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
pyclaudecli/__init__.py,sha256=mhEn48gp_bZiMgBmYu3QFNShOp8vbeFojYm5WJqeKbY,352
|
|
2
|
+
pyclaudecli/__main__.py,sha256=kof4pG7QHlkQr1w7xYqH8EwPAnNqj8PFjjCFX-RKy5E,1540
|
|
3
|
+
pyclaudecli/_process.py,sha256=jp7rvHqBY7XLE0CSrE9cI2mmEa61qlLBFW19EQDezv8,7076
|
|
4
|
+
pyclaudecli/client.py,sha256=ZBIzKc68Lmd6fYx8CzZCF9m7TvyzWPZ9iWDVLkLVivg,22830
|
|
5
|
+
pyclaudecli/exceptions.py,sha256=G2-5Vp2hBK-Q7jck-ybq2G6NRi5hHpxty9d7fLArUuA,870
|
|
6
|
+
pyclaudecli-0.1.0.dist-info/METADATA,sha256=3HW7O-83nukvLd1GwFKPVIY21dH_C8IJ-kE2XxKWWdw,3310
|
|
7
|
+
pyclaudecli-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
8
|
+
pyclaudecli-0.1.0.dist-info/entry_points.txt,sha256=RzYhNNBMvPbe6g3zH4OwC1NLmz38mYEePKDtryrR5c4,58
|
|
9
|
+
pyclaudecli-0.1.0.dist-info/licenses/LICENSE,sha256=--FgFwZp9RlY-pXA4xV-pw-3oeGsyNk2vjIr3bQRnBY,1076
|
|
10
|
+
pyclaudecli-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Suriya Ravichandran
|
|
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.
|