orca-cli-sdk 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.
@@ -0,0 +1,226 @@
1
+ Metadata-Version: 2.5
2
+ Name: orca-cli-sdk
3
+ Version: 0.1.0
4
+ Summary: Typed Python SDK (sync and asyncio) for the Orca CLI
5
+ Project-URL: Homepage, https://github.com/batprem/orca-sdk-python
6
+ Project-URL: Repository, https://github.com/batprem/orca-sdk-python
7
+ Project-URL: Issues, https://github.com/batprem/orca-sdk-python/issues
8
+ Author: Prem Chotepanit
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: agents,cli,orca,sdk,worktrees
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: POSIX
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
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+
25
+ # orca-cli-sdk
26
+
27
+ A typed Python SDK for the Orca CLI, with both sync and `asyncio` clients.
28
+
29
+ - **Covers every command.** All 287 commands in Orca CLI 1.4.237 are wrapped, generated from the CLI's own schema (`orca agent-context --json`).
30
+ - **Mirrors the CLI.** `orca worktree list --limit 5` becomes `client.worktree.list(limit=5)`.
31
+ - **Uses the JSON envelope.** Commands run with `--json`, the SDK parses the `{ok, result, error}` envelope, and you get `result` back.
32
+ - **Typed exceptions.** Covers a missing binary, CLI errors (with `code`/`message`/`data`), timeouts and bad JSON.
33
+ - **No runtime dependencies.** Needs Python 3.10+.
34
+
35
+ ```bash
36
+ uv add orca-cli-sdk # or: pip install orca-cli-sdk
37
+ # import name: orca_sdk
38
+ ```
39
+
40
+ ## Quick start
41
+
42
+ ```python
43
+ from orca_sdk import Orca
44
+
45
+ orca = Orca()
46
+
47
+ status = orca.status()
48
+ print(status["runtime"]["state"]) # "ready"
49
+
50
+ for project in orca.project.list()["projects"]:
51
+ print(project["id"], project["displayName"])
52
+
53
+ repos = orca.repo.list()
54
+ worktrees = orca.worktree.list(repo="name:orca", limit=10)
55
+ ```
56
+
57
+ ### Async
58
+
59
+ ```python
60
+ import asyncio
61
+ from orca_sdk import AsyncOrca
62
+
63
+ async def main() -> None:
64
+ async with AsyncOrca() as orca:
65
+ status, projects = await asyncio.gather(orca.status(), orca.project.list())
66
+ print(status["graph"]["state"], len(projects["projects"]))
67
+
68
+ asyncio.run(main())
69
+ ```
70
+
71
+ `AsyncOrca` has the same methods and signatures as `Orca`, but each command must be awaited. It runs the CLI with `asyncio.create_subprocess_exec`. If a command times out or is cancelled, the SDK kills the CLI process.
72
+
73
+ ## Finding the `orca` binary
74
+
75
+ The binary is located the first time it is needed, checking in this order:
76
+
77
+ 1. `Orca(binary="/path/to/orca")`
78
+ 2. the `ORCA_BIN` environment variable
79
+ 3. `orca` on `PATH`
80
+ 4. `/home/box/Applications/bin/orca`
81
+
82
+ If none of these is an executable file, the SDK raises `OrcaBinaryNotFoundError`. `orca_sdk.find_binary()` runs the same lookup.
83
+
84
+ ## Mapping CLI commands to methods
85
+
86
+ | CLI | Python |
87
+ | --- | --- |
88
+ | `orca status` | `orca.status()` |
89
+ | `orca worktree create --name t --agent codex --prompt "hi"` | `orca.worktree.create(name="t", agent="codex", prompt="hi")` |
90
+ | `orca worktree rm --worktree active --force` | `orca.worktree.rm(worktree="active", force=True)` |
91
+ | `orca terminal wait --terminal t1 --for exit --timeout-ms 60000` | `orca.terminal.wait(terminal="t1", for_="exit", timeout_ms=60000)` |
92
+ | `orca reference add <url> <url> --worktree active` | `orca.reference.add(["https://…", "https://…"], worktree="active")` |
93
+ | `orca agent hooks status` | `orca.agent.hooks.status()` |
94
+ | `orca linear team list` | `orca.linear.team.list()` |
95
+ | `orca terminal-daemon status` | `orca.terminal_daemon.status()` |
96
+ | `orca serve service status` | `orca.serve.service.status()` (`orca.serve(...)` runs `serve` itself) |
97
+
98
+ Naming rules:
99
+
100
+ - Hyphens become underscores (`setup-clone` → `setup_clone`).
101
+ - Python keywords get a trailing `_` (`--for` → `for_`, `orca is` → `orca.is_`).
102
+ - Positional arguments are positional parameters.
103
+ - Flags are keyword-only parameters.
104
+ - Flags the usage string marks as required are required keywords.
105
+
106
+ How values become arguments:
107
+
108
+ - Boolean flags take `True`.
109
+ - Repeatable flags (`--reference`, `--label`, …) take a list.
110
+ - `None`/`False` leaves the flag out.
111
+
112
+ Every method's docstring holds the CLI usage, notes and examples, so `help(orca.worktree.create)` shows what `orca worktree create --help` would.
113
+
114
+ The CLI's groups are all available, including:
115
+
116
+ - `project`, `repo`, `worktree`, `agent`, `terminal`
117
+ - `automations`, `host`, `environment`, `skills`, `reference`, `account`
118
+ - `orchestration`, `artifacts`, `file`, `tab`, `linear`, `jira`, `computer`, `emulator`, `storage`, `serve`
119
+
120
+ Top-level commands are methods on the client: `status`, `search`, `open`, `agent_context`, `snapshot`, `goto`, `click`, and the rest.
121
+
122
+ ## Options: timeouts, env vars, remote runtimes
123
+
124
+ ```python
125
+ orca = Orca(
126
+ timeout=120, # seconds per command (default 60, None = no limit)
127
+ env={"NO_COLOR": "1"}, # extra env vars for the subprocess
128
+ inherit_env=True, # start from os.environ (default)
129
+ cwd="/path/to/worktree", # Orca infers the current worktree from cwd
130
+ )
131
+
132
+ # Route every command to a saved remote Orca server (adds --environment <name>)
133
+ remote = Orca(environment="build-box")
134
+
135
+ # ...or to one reached by a pairing code (passed as $ORCA_PAIRING_CODE so it
136
+ # does not appear in the process list)
137
+ paired = Orca(pairing_code="orca://pair?...")
138
+
139
+ # Derive a client with different options; the original is unchanged
140
+ slow = orca.with_options(timeout=600)
141
+ slow.terminal.wait(terminal="term_123", for_="exit", timeout_ms=600_000)
142
+ local = remote.with_options(environment=None)
143
+ ```
144
+
145
+ Some commands only run on the local host and reject routing: `account list`, `host list` and `environment list`. The `environment …` management commands also take `--environment` as a *selector* (which server to act on), not as routing. Calling any of these on a client that has `environment` or `pairing_code` set raises `OrcaUsageError` before running anything. Use `with_options(environment=None, pairing_code=None)` to call them.
146
+
147
+ ```python
148
+ orca.environment.show(environment="build-box") # selector, not routing
149
+ ```
150
+
151
+ ## Errors
152
+
153
+ ```python
154
+ from orca_sdk import (
155
+ OrcaError, # base class
156
+ OrcaBinaryNotFoundError, # no orca executable
157
+ OrcaCommandError, # the CLI reported ok=false or exited non-zero
158
+ OrcaTimeoutError, # killed after the timeout
159
+ OrcaJSONDecodeError, # --json output was not valid JSON
160
+ OrcaUsageError, # e.g. routing a host-local command
161
+ )
162
+
163
+ try:
164
+ orca.repo.show(repo="name:missing")
165
+ except OrcaCommandError as err:
166
+ print(err.code) # "repo_not_found"
167
+ print(err.message)
168
+ print(err.data) # error.data from the envelope, if any
169
+ print(err.next_steps) # the CLI's suggested next steps
170
+ print(err.returncode, err.argv, err.stderr)
171
+ ```
172
+
173
+ If the CLI fails without printing a JSON envelope, you still get an `OrcaCommandError`, with `code == "process_error"` and the message taken from stderr.
174
+
175
+ ## Raw access
176
+
177
+ `run_command` runs any argument list. It returns the full `OrcaResponse`: `ok`, `result`, `error`, `id`, `meta`, `stdout`, `stderr`, `returncode` and `argv`.
178
+
179
+ ```python
180
+ resp = orca.run_command(["worktree", "ps", "--limit", "3"])
181
+ print(resp.meta["runtimeId"], resp.result)
182
+
183
+ resp = orca.run_command(["repo", "show", "--repo", "x"], check=False) # don't raise
184
+ if not resp.ok:
185
+ print(resp.error["code"])
186
+
187
+ orca.run_command(["claude-teams", "--help"], json=False) # plain-text commands
188
+ ```
189
+
190
+ To feed stdin, pass `stdin=` to `run_command` or to the generated methods that read stdin (for example `orca.speech.openai_key.set(stdin=key)` or `--text-stdin` commands).
191
+
192
+ The bundled schema is available at runtime:
193
+
194
+ ```python
195
+ from orca_sdk.schema import load_schema, get_command, command_names
196
+ get_command("worktree create")["notes"]
197
+ ```
198
+
199
+ ## Development
200
+
201
+ ```bash
202
+ uv sync
203
+ uv run pytest # unit tests (subprocess is mocked)
204
+ ORCA_INTEGRATION=1 uv run pytest # also run read-only tests against the real orca
205
+ uv build
206
+ ```
207
+
208
+ The integration tests only run read-only commands: `status`, `project list`, `repo list`, `worktree list`, `host list`, `environment list`, `skills list`, `agent-context` and a `repo show` that is expected to fail.
209
+
210
+ ### Regenerating the wrappers
211
+
212
+ `src/orca_sdk/_generated.py` is generated, so don't edit it by hand. To regenerate it after an Orca upgrade:
213
+
214
+ ```bash
215
+ orca agent-context --json > src/orca_sdk/data/agent-context.json
216
+ # optional, improves flag typing: a dump of `orca <cmd> --help` for every command,
217
+ # each section headed by a line `== orca <cmd> --help`
218
+ python scripts/generate.py --help-dump /path/to/orca-cli-help.txt -v
219
+ uv run pytest
220
+ ```
221
+
222
+ The schema lists each command's flag names but not whether a flag takes a value. The generator works that out from the usage string, then from the `--help` text, which it caches in `data/flag-types.json`. If neither settles it, the generator uses how the same flag is written in other commands. `-v` lists the flags typed that way. `tests/test_generated.py` checks that the generated file is up to date and that every command builds its expected argv.
223
+
224
+ ## License
225
+
226
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,16 @@
1
+ orca_sdk/__init__.py,sha256=ohBz6dS2WD8IWS-75LLMbia3rRHwlo93mWLohSLIgic,899
2
+ orca_sdk/_async_client.py,sha256=R5SpUfcEM9ivUU94wALfPwkaXqvoms-obHuPm6dMm6g,5629
3
+ orca_sdk/_client.py,sha256=FQ6M2ZG_7eOopnp8bcM-7BjVKjY6vrCNFIR-UBbZIAU,6605
4
+ orca_sdk/_core.py,sha256=XHELsfhuyYfAsoE1kEdLpajSIohuOvsx8njbW3RYcpM,9237
5
+ orca_sdk/_exceptions.py,sha256=1LwZ_b59Aid4nOL4DFUeNfPfUWuYq-09mLfgPtpy9qM,2914
6
+ orca_sdk/_generated.py,sha256=nZdXuNKkZ0IBpgF-fBVvxTteUUgY0OWumDICfDwRaec,518171
7
+ orca_sdk/_resource.py,sha256=jbnvl-vX6dDHTVJ-aybKtYN7Ll19Hq2CaGM0Uo3Jo70,2268
8
+ orca_sdk/_version.py,sha256=DXl7Me4wOFvKowSwVDKR8H7K8YhAeI5Rd9FrofOSKow,126
9
+ orca_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ orca_sdk/schema.py,sha256=Bnf2DlCGpE6mWuv6i7PWs-J1b-j-YW791qjcc6JjwfQ,1063
11
+ orca_sdk/data/agent-context.json,sha256=4srThmqaiHWG9ZxM9YNS9OaTHQGiNwetghWorq3-6og,234529
12
+ orca_sdk/data/flag-types.json,sha256=PD04unlB49w6iGPt5_DiH9VGU82nMXdj7GH8YfmNMHE,42443
13
+ orca_cli_sdk-0.1.0.dist-info/METADATA,sha256=W3h29s8vKSZOl6KwMqxhA4vJoEahpIQS8W65GD625uQ,9365
14
+ orca_cli_sdk-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
15
+ orca_cli_sdk-0.1.0.dist-info/licenses/LICENSE,sha256=CiAnvUq7WtvLsARjQc88u2xrII5O5lZaE-zK9bWbZqs,1061
16
+ orca_cli_sdk-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Prem
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.
orca_sdk/__init__.py ADDED
@@ -0,0 +1,41 @@
1
+ """Python SDK for the Orca CLI.
2
+
3
+ >>> from orca_sdk import Orca
4
+ >>> client = Orca()
5
+ >>> client.status()["runtime"]["state"]
6
+ 'ready'
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from ._async_client import AsyncOrca
12
+ from ._client import Orca
13
+ from ._core import BINARY_ENV_VAR, DEFAULT_BINARY, DEFAULT_TIMEOUT, NOT_GIVEN, OrcaResponse, find_binary
14
+ from ._exceptions import (
15
+ OrcaBinaryNotFoundError,
16
+ OrcaCommandError,
17
+ OrcaError,
18
+ OrcaJSONDecodeError,
19
+ OrcaTimeoutError,
20
+ OrcaUsageError,
21
+ )
22
+ from ._version import CLI_VERSION, __version__
23
+
24
+ __all__ = [
25
+ "BINARY_ENV_VAR",
26
+ "CLI_VERSION",
27
+ "DEFAULT_BINARY",
28
+ "DEFAULT_TIMEOUT",
29
+ "NOT_GIVEN",
30
+ "AsyncOrca",
31
+ "Orca",
32
+ "OrcaBinaryNotFoundError",
33
+ "OrcaCommandError",
34
+ "OrcaError",
35
+ "OrcaJSONDecodeError",
36
+ "OrcaResponse",
37
+ "OrcaTimeoutError",
38
+ "OrcaUsageError",
39
+ "__version__",
40
+ "find_binary",
41
+ ]
@@ -0,0 +1,171 @@
1
+ """Asynchronous Orca client built on :mod:`asyncio` subprocesses."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import contextlib
7
+ import subprocess
8
+ from collections.abc import Mapping, Sequence
9
+ from typing import Any
10
+
11
+ from ._core import (
12
+ DEFAULT_TIMEOUT,
13
+ NOT_GIVEN,
14
+ ClientConfig,
15
+ OrcaResponse,
16
+ PathLike,
17
+ PreparedCall,
18
+ find_binary,
19
+ make_config,
20
+ parse_output,
21
+ prepare,
22
+ prepare_invoke,
23
+ )
24
+ from ._exceptions import OrcaBinaryNotFoundError, OrcaTimeoutError
25
+ from ._generated import AsyncOrcaCommands
26
+ from ._resource import FlagValue
27
+
28
+ __all__ = ["AsyncOrca"]
29
+
30
+
31
+ class AsyncOrca(AsyncOrcaCommands):
32
+ """Async counterpart of :class:`~orca_sdk.Orca`; every command is awaitable.
33
+
34
+ >>> async with AsyncOrca() as client:
35
+ ... status = await client.status()
36
+
37
+ Accepts the same options as :class:`~orca_sdk.Orca`.
38
+ """
39
+
40
+ def __init__(
41
+ self,
42
+ *,
43
+ binary: PathLike | None = None,
44
+ timeout: float | None = DEFAULT_TIMEOUT,
45
+ environment: str | None = None,
46
+ pairing_code: str | None = None,
47
+ env: Mapping[str, str] | None = None,
48
+ inherit_env: bool = True,
49
+ cwd: PathLike | None = None,
50
+ ) -> None:
51
+ super().__init__()
52
+ self._config = make_config(binary, timeout, environment, pairing_code, env, inherit_env, cwd)
53
+ self._binary: str | None = None
54
+
55
+ @classmethod
56
+ def _from_config(cls, config: ClientConfig) -> AsyncOrca:
57
+ client = cls.__new__(cls)
58
+ AsyncOrcaCommands.__init__(client)
59
+ client._config = config
60
+ client._binary = None
61
+ return client
62
+
63
+ @property
64
+ def binary(self) -> str:
65
+ """Resolved path of the ``orca`` executable (located lazily)."""
66
+ if self._binary is None:
67
+ self._binary = find_binary(self._config.binary)
68
+ return self._binary
69
+
70
+ @property
71
+ def config(self) -> ClientConfig:
72
+ """The options this client runs commands with (timeout, environment, ...)."""
73
+ return self._config
74
+
75
+ def with_options(
76
+ self,
77
+ *,
78
+ binary: PathLike | None = NOT_GIVEN,
79
+ timeout: float | None = NOT_GIVEN,
80
+ environment: str | None = NOT_GIVEN,
81
+ pairing_code: str | None = NOT_GIVEN,
82
+ env: Mapping[str, str] | None = NOT_GIVEN,
83
+ inherit_env: bool = NOT_GIVEN,
84
+ cwd: PathLike | None = NOT_GIVEN,
85
+ ) -> AsyncOrca:
86
+ """Return a copy of this client with some options changed."""
87
+ config = self._config.updated(
88
+ binary=binary,
89
+ timeout=timeout,
90
+ environment=environment,
91
+ pairing_code=pairing_code,
92
+ env=env,
93
+ inherit_env=inherit_env,
94
+ cwd=cwd,
95
+ )
96
+ return self._from_config(make_config(**vars(config)))
97
+
98
+ async def run_command(
99
+ self,
100
+ args: Sequence[str],
101
+ *,
102
+ json: bool = True,
103
+ remote: bool = True,
104
+ stdin: str | bytes | None = None,
105
+ timeout: float | None = NOT_GIVEN,
106
+ check: bool = True,
107
+ ) -> OrcaResponse:
108
+ """Run ``orca <args...>``; see :meth:`orca_sdk.Orca.run_command`."""
109
+ call = prepare(
110
+ self._config, self.binary, list(args), json=json, remote=remote, stdin=stdin, timeout=timeout
111
+ )
112
+ return await self._execute(call, check=check)
113
+
114
+ async def _invoke(
115
+ self,
116
+ command: str,
117
+ positionals: Sequence[Any],
118
+ flags: Sequence[tuple[str, FlagValue]],
119
+ *,
120
+ json: bool,
121
+ remote: bool,
122
+ stdin: str | bytes | None = None,
123
+ ) -> Any:
124
+ call = prepare_invoke(
125
+ self._config, self.binary, command, positionals, flags, json=json, remote=remote, stdin=stdin
126
+ )
127
+ return (await self._execute(call)).result
128
+
129
+ async def _execute(self, call: PreparedCall, *, check: bool = True) -> OrcaResponse:
130
+ try:
131
+ proc = await asyncio.create_subprocess_exec(
132
+ *call.argv,
133
+ stdin=subprocess.PIPE if call.stdin is not None else subprocess.DEVNULL,
134
+ stdout=subprocess.PIPE,
135
+ stderr=subprocess.PIPE,
136
+ env=call.env,
137
+ cwd=call.cwd,
138
+ )
139
+ except FileNotFoundError as exc:
140
+ if exc.filename == call.argv[0]:
141
+ raise OrcaBinaryNotFoundError(
142
+ f"orca binary not found: {call.argv[0]}", [call.argv[0]]
143
+ ) from exc
144
+ raise
145
+ try:
146
+ stdout, stderr = await asyncio.wait_for(proc.communicate(call.stdin), timeout=call.timeout)
147
+ except asyncio.TimeoutError as exc:
148
+ with contextlib.suppress(ProcessLookupError):
149
+ proc.kill()
150
+ with contextlib.suppress(Exception):
151
+ await proc.wait()
152
+ raise OrcaTimeoutError(call.timeout or 0.0, call.argv) from exc
153
+ except BaseException:
154
+ # Cancellation: don't leave the CLI running.
155
+ with contextlib.suppress(ProcessLookupError):
156
+ proc.kill()
157
+ raise
158
+ assert proc.returncode is not None
159
+ return parse_output(call, proc.returncode, stdout, stderr, check=check)
160
+
161
+ async def aclose(self) -> None:
162
+ """No-op; present so the client can be used as an async context manager."""
163
+
164
+ async def __aenter__(self) -> AsyncOrca:
165
+ return self
166
+
167
+ async def __aexit__(self, *exc: object) -> None:
168
+ await self.aclose()
169
+
170
+ def __repr__(self) -> str:
171
+ return f"AsyncOrca(binary={self._config.binary or self._binary!r}, environment={self._config.environment!r})"
orca_sdk/_client.py ADDED
@@ -0,0 +1,195 @@
1
+ """Synchronous Orca client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+ from collections.abc import Mapping, Sequence
7
+ from typing import Any
8
+
9
+ from ._core import (
10
+ DEFAULT_TIMEOUT,
11
+ NOT_GIVEN,
12
+ ClientConfig,
13
+ OrcaResponse,
14
+ PathLike,
15
+ PreparedCall,
16
+ find_binary,
17
+ make_config,
18
+ parse_output,
19
+ prepare,
20
+ prepare_invoke,
21
+ )
22
+ from ._exceptions import OrcaBinaryNotFoundError, OrcaTimeoutError
23
+ from ._generated import OrcaCommands
24
+ from ._resource import FlagValue
25
+
26
+ __all__ = ["Orca"]
27
+
28
+
29
+ class Orca(OrcaCommands):
30
+ """Run Orca CLI commands from Python.
31
+
32
+ Every CLI command is available as a method mirroring its path, e.g.
33
+ ``orca worktree list --limit 5`` is ``client.worktree.list(limit=5)`` and
34
+ ``orca status`` is ``client.status()``. JSON-capable commands return the
35
+ ``result`` from the CLI's ``{ok, result, error}`` envelope; failures raise
36
+ :class:`~orca_sdk.OrcaCommandError`.
37
+
38
+ Args:
39
+ binary: Path to the ``orca`` executable. Defaults to ``$ORCA_BIN``,
40
+ then ``orca`` on ``$PATH``, then ``/home/box/Applications/bin/orca``.
41
+ timeout: Seconds before a command is killed (``None`` waits forever).
42
+ environment: Saved remote Orca server (id or name) passed as
43
+ ``--environment`` to every command that accepts it.
44
+ pairing_code: ``orca://pair?...`` code for a remote runtime, passed to
45
+ the CLI through ``$ORCA_PAIRING_CODE``.
46
+ env: Extra environment variables for the subprocess.
47
+ inherit_env: Start from ``os.environ`` (default) or an empty environment.
48
+ cwd: Working directory for the subprocess; Orca infers the current
49
+ worktree from it.
50
+ """
51
+
52
+ def __init__(
53
+ self,
54
+ *,
55
+ binary: PathLike | None = None,
56
+ timeout: float | None = DEFAULT_TIMEOUT,
57
+ environment: str | None = None,
58
+ pairing_code: str | None = None,
59
+ env: Mapping[str, str] | None = None,
60
+ inherit_env: bool = True,
61
+ cwd: PathLike | None = None,
62
+ ) -> None:
63
+ super().__init__()
64
+ self._config = make_config(binary, timeout, environment, pairing_code, env, inherit_env, cwd)
65
+ self._binary: str | None = None
66
+
67
+ @classmethod
68
+ def _from_config(cls, config: ClientConfig) -> Orca:
69
+ client = cls.__new__(cls)
70
+ OrcaCommands.__init__(client)
71
+ client._config = config
72
+ client._binary = None
73
+ return client
74
+
75
+ @property
76
+ def binary(self) -> str:
77
+ """Resolved path of the ``orca`` executable (located lazily)."""
78
+ if self._binary is None:
79
+ self._binary = find_binary(self._config.binary)
80
+ return self._binary
81
+
82
+ @property
83
+ def config(self) -> ClientConfig:
84
+ """The options this client runs commands with (timeout, environment, ...)."""
85
+ return self._config
86
+
87
+ def with_options(
88
+ self,
89
+ *,
90
+ binary: PathLike | None = NOT_GIVEN,
91
+ timeout: float | None = NOT_GIVEN,
92
+ environment: str | None = NOT_GIVEN,
93
+ pairing_code: str | None = NOT_GIVEN,
94
+ env: Mapping[str, str] | None = NOT_GIVEN,
95
+ inherit_env: bool = NOT_GIVEN,
96
+ cwd: PathLike | None = NOT_GIVEN,
97
+ ) -> Orca:
98
+ """Return a copy of this client with some options changed.
99
+
100
+ >>> client.with_options(timeout=300).terminal.wait(terminal="term_1", for_="exit")
101
+ """
102
+ config = self._config.updated(
103
+ binary=binary,
104
+ timeout=timeout,
105
+ environment=environment,
106
+ pairing_code=pairing_code,
107
+ env=env,
108
+ inherit_env=inherit_env,
109
+ cwd=cwd,
110
+ )
111
+ return self._from_config(make_config(**vars(config)))
112
+
113
+ def run_command(
114
+ self,
115
+ args: Sequence[str],
116
+ *,
117
+ json: bool = True,
118
+ remote: bool = True,
119
+ stdin: str | bytes | None = None,
120
+ timeout: float | None = NOT_GIVEN,
121
+ check: bool = True,
122
+ ) -> OrcaResponse:
123
+ """Run ``orca <args...>`` and return the parsed response.
124
+
125
+ Use this for commands or flags the generated wrappers do not cover.
126
+ ``--json`` is appended when ``json`` is true; ``remote`` controls
127
+ whether the client's ``environment``/``pairing_code`` are applied.
128
+ With ``check=False`` a failing envelope is returned instead of raised.
129
+ """
130
+ call = prepare(
131
+ self._config, self.binary, list(args), json=json, remote=remote, stdin=stdin, timeout=timeout
132
+ )
133
+ return self._execute(call, check=check)
134
+
135
+ def _invoke(
136
+ self,
137
+ command: str,
138
+ positionals: Sequence[Any],
139
+ flags: Sequence[tuple[str, FlagValue]],
140
+ *,
141
+ json: bool,
142
+ remote: bool,
143
+ stdin: str | bytes | None = None,
144
+ ) -> Any:
145
+ call = prepare_invoke(
146
+ self._config, self.binary, command, positionals, flags, json=json, remote=remote, stdin=stdin
147
+ )
148
+ return self._execute(call).result
149
+
150
+ def _execute(self, call: PreparedCall, *, check: bool = True) -> OrcaResponse:
151
+ try:
152
+ proc = subprocess.run(
153
+ call.argv,
154
+ input=call.stdin,
155
+ stdin=None if call.stdin is not None else subprocess.DEVNULL,
156
+ capture_output=True,
157
+ env=call.env,
158
+ cwd=call.cwd,
159
+ timeout=call.timeout,
160
+ check=False,
161
+ )
162
+ except subprocess.TimeoutExpired as exc:
163
+ raise OrcaTimeoutError(
164
+ call.timeout or 0.0,
165
+ call.argv,
166
+ stdout=_text(exc.stdout),
167
+ stderr=_text(exc.stderr),
168
+ ) from exc
169
+ except FileNotFoundError as exc:
170
+ if exc.filename == call.argv[0]:
171
+ raise OrcaBinaryNotFoundError(
172
+ f"orca binary not found: {call.argv[0]}", [call.argv[0]]
173
+ ) from exc
174
+ raise
175
+ return parse_output(call, proc.returncode, proc.stdout, proc.stderr, check=check)
176
+
177
+ def close(self) -> None:
178
+ """No-op; present so the client can be used as a context manager."""
179
+
180
+ def __enter__(self) -> Orca:
181
+ return self
182
+
183
+ def __exit__(self, *exc: object) -> None:
184
+ self.close()
185
+
186
+ def __repr__(self) -> str:
187
+ return (
188
+ f"Orca(binary={self._config.binary or self._binary!r}, environment={self._config.environment!r})"
189
+ )
190
+
191
+
192
+ def _text(data: bytes | str | None) -> str:
193
+ if isinstance(data, bytes):
194
+ return data.decode("utf-8", errors="replace")
195
+ return data or ""