adaptorch-cli 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,14 @@
1
+ Copyright (c) 2026 ClassicMate. All rights reserved.
2
+
3
+ This software and associated documentation files (the "Software") are proprietary
4
+ and confidential. Unauthorized copying, distribution, modification, public
5
+ display, or any use of the Software other than as expressly authorized by ClassicMate
6
+ is strictly prohibited.
7
+
8
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
9
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
10
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
11
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
12
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
13
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
14
+ SOFTWARE.
@@ -0,0 +1,46 @@
1
+ Metadata-Version: 2.4
2
+ Name: adaptorch-cli
3
+ Version: 0.1.0
4
+ Summary: Command-line client for AdaptOrch
5
+ Author: ClassicMate
6
+ License-Expression: LicenseRef-Proprietary
7
+ Project-URL: Homepage, https://adaptorch.com
8
+ Project-URL: Repository, https://github.com/dmae97/Adaptorch-MCP
9
+ Project-URL: Issues, https://github.com/dmae97/Adaptorch-MCP/issues
10
+ Project-URL: Documentation, https://adaptorch.com/mcp-docs
11
+ Requires-Python: >=3.11
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: adaptorch-client<0.2,>=0.1
15
+ Dynamic: license-file
16
+
17
+ # adaptorch-cli
18
+
19
+ Independent AdaptOrch SaaS CLI. Installs the `adaptorchctl` command and uses `adaptorch-client`; it does not invoke `adaptorch` or `adaptorch-mcp`.
20
+
21
+ Log in once and every command picks the credential up:
22
+
23
+ ```bash
24
+ adaptorchctl auth login # prompts for the key, verifies it, stores it at
25
+ # ~/.config/adaptorch/config.json (mode 0600)
26
+ adaptorchctl auth status # shows credential_source: env | config | none
27
+ adaptorchctl auth logout # removes the stored key
28
+ ```
29
+
30
+ `ADAPTORCH_API_KEY` still works and always wins over the stored key, so CI
31
+ keeps injecting secrets via the environment. `config set provider.{name,model,
32
+ api_key}` persists BYOK wiring for `run submit`; the env vars
33
+ `ADAPTORCH_PROVIDER`, `ADAPTORCH_PROVIDER_MODEL`, and
34
+ `ADAPTORCH_PROVIDER_API_KEY` override the stored values. Provider credentials
35
+ become `X-Provider`, `X-Provider-Model`, and `X-Provider-Key` only on
36
+ `run submit`; reads and cancellation never receive the provider key.
37
+ `ADAPTORCH_API_KEY` remains the separate tenant key.
38
+
39
+ Install the current `adaptorch-client` and `adaptorch-cli` source packages together.
40
+ A git push does not replace a previously published PyPI version.
41
+
42
+ See [`../../docs/adaptorchctl-usage.ko.md`](../../docs/adaptorchctl-usage.ko.md) for usage.
43
+
44
+ ## License
45
+
46
+ Proprietary — Copyright ClassicMate. All rights reserved. See [LICENSE](https://github.com/dmae97/Adaptorch-MCP/blob/main/LICENSE).
@@ -0,0 +1,30 @@
1
+ # adaptorch-cli
2
+
3
+ Independent AdaptOrch SaaS CLI. Installs the `adaptorchctl` command and uses `adaptorch-client`; it does not invoke `adaptorch` or `adaptorch-mcp`.
4
+
5
+ Log in once and every command picks the credential up:
6
+
7
+ ```bash
8
+ adaptorchctl auth login # prompts for the key, verifies it, stores it at
9
+ # ~/.config/adaptorch/config.json (mode 0600)
10
+ adaptorchctl auth status # shows credential_source: env | config | none
11
+ adaptorchctl auth logout # removes the stored key
12
+ ```
13
+
14
+ `ADAPTORCH_API_KEY` still works and always wins over the stored key, so CI
15
+ keeps injecting secrets via the environment. `config set provider.{name,model,
16
+ api_key}` persists BYOK wiring for `run submit`; the env vars
17
+ `ADAPTORCH_PROVIDER`, `ADAPTORCH_PROVIDER_MODEL`, and
18
+ `ADAPTORCH_PROVIDER_API_KEY` override the stored values. Provider credentials
19
+ become `X-Provider`, `X-Provider-Model`, and `X-Provider-Key` only on
20
+ `run submit`; reads and cancellation never receive the provider key.
21
+ `ADAPTORCH_API_KEY` remains the separate tenant key.
22
+
23
+ Install the current `adaptorch-client` and `adaptorch-cli` source packages together.
24
+ A git push does not replace a previously published PyPI version.
25
+
26
+ See [`../../docs/adaptorchctl-usage.ko.md`](../../docs/adaptorchctl-usage.ko.md) for usage.
27
+
28
+ ## License
29
+
30
+ Proprietary — Copyright ClassicMate. All rights reserved. See [LICENSE](https://github.com/dmae97/Adaptorch-MCP/blob/main/LICENSE).
@@ -0,0 +1,40 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "adaptorch-cli"
7
+ version = "0.1.0"
8
+ description = "Command-line client for AdaptOrch"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "LicenseRef-Proprietary"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "ClassicMate" }]
14
+ dependencies = ["adaptorch-client>=0.1,<0.2"]
15
+
16
+ [project.urls]
17
+ Homepage = "https://adaptorch.com"
18
+ Repository = "https://github.com/dmae97/Adaptorch-MCP"
19
+ Issues = "https://github.com/dmae97/Adaptorch-MCP/issues"
20
+ Documentation = "https://adaptorch.com/mcp-docs"
21
+
22
+ [project.scripts]
23
+ adaptorchctl = "adaptorch_cli.cli:entrypoint"
24
+
25
+ [tool.setuptools.packages.find]
26
+ where = ["src"]
27
+
28
+ [tool.setuptools.package-data]
29
+ adaptorch_cli = ["py.typed"]
30
+
31
+ [tool.ruff]
32
+ line-length = 100
33
+ target-version = "py311"
34
+
35
+ [tool.ruff.lint]
36
+ select = ["E", "F", "I", "UP", "B"]
37
+
38
+ [tool.mypy]
39
+ python_version = "3.11"
40
+ strict = true
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ """AdaptOrch command-line client."""
@@ -0,0 +1,3 @@
1
+ from adaptorch_cli.cli import entrypoint
2
+
3
+ entrypoint()
@@ -0,0 +1,351 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import math
6
+ import sys
7
+ import uuid
8
+ from collections.abc import Sequence
9
+ from pathlib import Path
10
+ from typing import Never, Protocol, TypeAlias
11
+
12
+ from adaptorch_client import (
13
+ AdaptOrchAPIError,
14
+ AdaptOrchClient,
15
+ ClientConfig,
16
+ ProviderCredential,
17
+ validate_api_url,
18
+ )
19
+
20
+ from adaptorch_cli import config_store
21
+ from adaptorch_cli.parser import build_parser, parse_args
22
+
23
+ JSONValue: TypeAlias = bool | int | float | str | list["JSONValue"] | dict[str, "JSONValue"] | None
24
+ JSONMapping: TypeAlias = dict[str, JSONValue]
25
+
26
+ _MAX_SUBMIT_BYTES = 8 * 1024 * 1024
27
+
28
+
29
+ class PayloadResult(Protocol):
30
+ def to_payload(self) -> JSONMapping: ...
31
+
32
+
33
+ def _write_json(payload: JSONMapping) -> None:
34
+ print(json.dumps(payload, allow_nan=False, sort_keys=True, separators=(",", ":")))
35
+
36
+
37
+ def _reject_json_constant(_value: str) -> Never:
38
+ raise ValueError("non-finite JSON number")
39
+
40
+
41
+ def _finite_float(text: str) -> float:
42
+ value = float(text)
43
+ if not math.isfinite(value):
44
+ raise ValueError("non-finite JSON number")
45
+ return value
46
+
47
+
48
+ def _validated_api_url(api_url: str, parser: argparse.ArgumentParser) -> str:
49
+ try:
50
+ return validate_api_url(api_url)
51
+ except ValueError:
52
+ parser.error(
53
+ "invalid --api-url: expected an https:// origin (or exact loopback http://)"
54
+ " without credentials, path, query, or fragment"
55
+ )
56
+
57
+
58
+ def _read_submit_bytes(file_name: str, parser: argparse.ArgumentParser) -> bytes:
59
+ try:
60
+ if file_name == "-":
61
+ return sys.stdin.buffer.read(_MAX_SUBMIT_BYTES + 1)
62
+ with Path(file_name).open("rb") as stream:
63
+ return stream.read(_MAX_SUBMIT_BYTES + 1)
64
+ except OSError:
65
+ parser.error("submit input must be a readable JSON object")
66
+
67
+
68
+ def _read_submit_payload(file_name: str, parser: argparse.ArgumentParser) -> JSONMapping:
69
+ raw = _read_submit_bytes(file_name, parser)
70
+ if len(raw) > _MAX_SUBMIT_BYTES:
71
+ parser.error("submit input exceeds the 8 MiB limit")
72
+ try:
73
+ value: JSONValue = json.loads(
74
+ raw.decode("utf-8"),
75
+ parse_constant=_reject_json_constant,
76
+ parse_float=_finite_float,
77
+ )
78
+ # pi-lens-ignore: unreachable-except
79
+ except RecursionError:
80
+ parser.error("submit input JSON is too deeply nested")
81
+ except ValueError:
82
+ parser.error("submit input must be a readable JSON object")
83
+ if not isinstance(value, dict):
84
+ parser.error("submit input must be a JSON object")
85
+ return value
86
+
87
+
88
+ def _require_client(api_url: str) -> AdaptOrchClient:
89
+ api_key, _source = config_store.resolve_api_key()
90
+ if not api_key:
91
+ print(
92
+ "authentication required: run `adaptorchctl auth login` or set ADAPTORCH_API_KEY",
93
+ file=sys.stderr,
94
+ )
95
+ raise SystemExit(3)
96
+ return AdaptOrchClient(ClientConfig(api_url=api_url, api_key=api_key))
97
+
98
+
99
+ def _submission_credential() -> ProviderCredential | None:
100
+ resolved, _source = config_store.resolve_provider_credential()
101
+ if resolved is None:
102
+ return None
103
+ provider, model, key = resolved
104
+ return ProviderCredential(provider, model, key)
105
+
106
+
107
+ def _read_secret(prompt: str, *, stdin_flag: bool) -> str:
108
+ """Read a secret from stdin (piped or --value-stdin) or a hidden prompt.
109
+
110
+ Never echoed: values on argv are rejected upstream by _CREDENTIAL_FLAGS.
111
+ """
112
+ if stdin_flag or not sys.stdin.isatty():
113
+ value = sys.stdin.read().strip()
114
+ else:
115
+ import getpass
116
+
117
+ value = getpass.getpass(prompt).strip()
118
+ return value
119
+
120
+
121
+ def _result_payload(result: PayloadResult) -> JSONMapping:
122
+ return result.to_payload()
123
+
124
+
125
+ def _auth_command(
126
+ args: argparse.Namespace,
127
+ api_url: str,
128
+ parser: argparse.ArgumentParser,
129
+ ) -> JSONMapping:
130
+ sub: str = args.auth_command
131
+ if sub == "status":
132
+ api_key, source = config_store.resolve_api_key()
133
+ return {
134
+ "authenticated": api_key is not None,
135
+ "credential_source": source,
136
+ "api_url": api_url,
137
+ }
138
+ if sub == "logout":
139
+ removed = config_store.clear_credentials()
140
+ return {"logged_out": True, "removed_stored_key": removed}
141
+ if sub == "login":
142
+ login_url = getattr(args, "login_api_url", None) or api_url
143
+ try:
144
+ login_url = validate_api_url(login_url)
145
+ except ValueError:
146
+ parser.error(
147
+ "invalid --api-url: expected an https:// origin (or exact loopback http://)"
148
+ )
149
+ key = _read_secret("AdaptOrch API key: ", stdin_flag=False)
150
+ if not key:
151
+ parser.error("no API key provided on stdin or at the prompt")
152
+ verified: bool | None = None
153
+ if not args.no_verify:
154
+ try:
155
+ whoami = AdaptOrchClient(ClientConfig(api_url=login_url, api_key=key)).whoami()
156
+ verified = True
157
+ _ = whoami
158
+ except AdaptOrchAPIError as error:
159
+ print(
160
+ f"login verification failed (HTTP {error.status_code}); "
161
+ "key not saved. Retry with --no-verify to store anyway.",
162
+ file=sys.stderr,
163
+ )
164
+ raise SystemExit(3) from error
165
+ config_store.store_credentials(api_key=key, api_url=login_url)
166
+ return {
167
+ "logged_in": True,
168
+ "api_url": login_url,
169
+ "verified": bool(verified),
170
+ "config_path": str(config_store.config_path()),
171
+ }
172
+ parser.error("unknown auth command")
173
+
174
+
175
+ def _config_command(
176
+ args: argparse.Namespace,
177
+ api_url: str,
178
+ parser: argparse.ArgumentParser,
179
+ ) -> JSONMapping:
180
+ sub: str = args.config_command
181
+ if sub == "get":
182
+ view = config_store.config_view()
183
+ view["api_url"] = api_url
184
+ return view
185
+ if sub == "set":
186
+ key: str = args.key
187
+ if key not in config_store.configurable_keys():
188
+ parser.error(
189
+ "unknown config key; expected one of: "
190
+ + ", ".join(config_store.configurable_keys())
191
+ )
192
+ value = getattr(args, "value", None)
193
+ if config_store.is_secret_key(key):
194
+ if value is not None:
195
+ parser.error(f"{key} is a secret; pass it via --value-stdin or a piped stdin")
196
+ value = _read_secret("Value: ", stdin_flag=args.value_stdin)
197
+ elif args.value_stdin:
198
+ value = sys.stdin.read().strip()
199
+ elif value is None:
200
+ parser.error("config set requires a value (or --value-stdin)")
201
+ if key == "api_url":
202
+ try:
203
+ value = validate_api_url(value)
204
+ except ValueError:
205
+ parser.error(
206
+ "invalid api_url: expected an https:// origin (or exact loopback http://)"
207
+ )
208
+ config_store.set_config_value(key, value)
209
+ return {"set": key, "config_path": str(config_store.config_path())}
210
+ if sub == "unset":
211
+ removed = config_store.unset_config_value(args.key)
212
+ return {"unset": args.key, "removed": removed}
213
+ parser.error("unknown config command")
214
+
215
+
216
+ def _run_command(
217
+ args: argparse.Namespace,
218
+ api_url: str,
219
+ parser: argparse.ArgumentParser,
220
+ ) -> tuple[JSONMapping, bool]:
221
+ command: str = args.run_command
222
+ match command:
223
+ case "submit":
224
+ file_name: str = args.file
225
+ request_id: str | None = args.request_id
226
+ payload = _read_submit_payload(file_name, parser)
227
+ submit_result = _require_client(api_url).submit_run(
228
+ payload,
229
+ idempotency_key=request_id or str(uuid.uuid4()),
230
+ provider_credential=_submission_credential(),
231
+ )
232
+ return _result_payload(submit_result), False
233
+ case "list":
234
+ status: str | None = args.status
235
+ project_id: str | None = args.project_id
236
+ list_result = _require_client(api_url).list_runs(
237
+ status=status,
238
+ project_id=project_id,
239
+ )
240
+ return _result_payload(list_result), False
241
+ case "get":
242
+ run_id: str = args.run_id
243
+ return _result_payload(_require_client(api_url).get_run(run_id)), True
244
+ case "cancel":
245
+ cancel_run_id: str = args.run_id
246
+ reason: str | None = args.reason
247
+ cancel_result = _require_client(api_url).cancel_run(
248
+ cancel_run_id,
249
+ reason=reason,
250
+ )
251
+ return _result_payload(cancel_result), False
252
+ case _:
253
+ parser.error("unknown run command")
254
+
255
+
256
+ def _execute(
257
+ args: argparse.Namespace,
258
+ api_url: str,
259
+ parser: argparse.ArgumentParser,
260
+ ) -> tuple[JSONMapping, bool]:
261
+ command: str = args.command
262
+ match command:
263
+ case "auth":
264
+ return _auth_command(args, api_url, parser), False
265
+ case "config":
266
+ return _config_command(args, api_url, parser), False
267
+ case "whoami":
268
+ return _result_payload(_require_client(api_url).whoami()), False
269
+ case "capabilities":
270
+ return _result_payload(_require_client(api_url).capabilities()), False
271
+ case "run":
272
+ return _run_command(args, api_url, parser)
273
+ case "evidence":
274
+ evidence_run_id: str = args.run_id
275
+ return _result_payload(_require_client(api_url).get_evidence(evidence_run_id)), False
276
+ case "artifact":
277
+ artifact_run_id: str = args.run_id
278
+ return _result_payload(_require_client(api_url).list_artifacts(artifact_run_id)), False
279
+ case _:
280
+ parser.error("unknown command")
281
+
282
+
283
+ def _api_exit_code(error: AdaptOrchAPIError) -> int:
284
+ status_code = error.status_code
285
+ if status_code in {401, 403}:
286
+ return 3
287
+ if status_code == 404:
288
+ return 4
289
+ if status_code == 409:
290
+ return 5
291
+ if status_code == 429:
292
+ return 6
293
+ if status_code is None or 500 <= status_code <= 599:
294
+ return 7
295
+ return 10
296
+
297
+
298
+ def _payload_status(payload: JSONMapping) -> str:
299
+ status = payload.get("status")
300
+ if isinstance(status, str):
301
+ return status
302
+ data = payload.get("data")
303
+ if isinstance(data, dict):
304
+ nested = data.get("status")
305
+ if isinstance(nested, str):
306
+ return nested
307
+ return ""
308
+
309
+
310
+ def _run_status_exit(payload: JSONMapping) -> int:
311
+ normalized = _payload_status(payload).lower()
312
+ if normalized == "failed":
313
+ return 8
314
+ if normalized == "cancelled":
315
+ return 9
316
+ if normalized == "inconclusive":
317
+ return 10
318
+ return 0
319
+
320
+
321
+ def main(argv: Sequence[str] | None = None) -> int:
322
+ parser = build_parser()
323
+ args = parse_args(argv)
324
+ flag_url = getattr(args, "api_url", None)
325
+ if flag_url is not None and not flag_url.strip():
326
+ parser.error("invalid --api-url: expected an https:// origin (or exact loopback http://)")
327
+ resolved_url, _url_source = config_store.resolve_api_url(flag_url)
328
+ api_url = _validated_api_url(resolved_url, parser)
329
+ try:
330
+ payload, check_run_status = _execute(args, api_url, parser)
331
+ try:
332
+ _write_json(payload)
333
+ except ValueError:
334
+ print("response contained non-encodable JSON values", file=sys.stderr)
335
+ return 7
336
+ return _run_status_exit(payload) if check_run_status else 0
337
+ # pi-lens-ignore: unreachable-except
338
+ except KeyboardInterrupt:
339
+ print("interrupted", file=sys.stderr)
340
+ return 130
341
+ # pi-lens-ignore: unreachable-except
342
+ except AdaptOrchAPIError as error:
343
+ code = _api_exit_code(error)
344
+ print(f"request failed (HTTP {error.status_code})", file=sys.stderr)
345
+ return code
346
+ except ValueError:
347
+ parser.error("invalid CLI input")
348
+
349
+
350
+ def entrypoint() -> None:
351
+ raise SystemExit(main())