cdx-manager 0.12.4 → 0.15.0
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.
- package/README.md +214 -19
- package/changelogs/CHANGELOGS_0_14_0.md +146 -0
- package/changelogs/CHANGELOGS_0_15_0.md +139 -0
- package/checksums/release-archives.json +12 -0
- package/package.json +3 -3
- package/pyproject.toml +1 -1
- package/src/cli.py +64 -4
- package/src/cli_args.py +324 -39
- package/src/cli_commands.py +251 -3049
- package/src/cli_helpers.py +190 -2
- package/src/cli_render.py +17 -0
- package/src/commands/__init__.py +6 -0
- package/src/commands/auth.py +224 -0
- package/src/commands/backup.py +112 -0
- package/src/commands/context_memory.py +341 -0
- package/src/commands/launch.py +218 -0
- package/src/commands/lifecycle.py +154 -0
- package/src/commands/maintenance.py +723 -0
- package/src/commands/runs.py +849 -0
- package/src/commands/settings.py +107 -0
- package/src/commands/status.py +713 -0
- package/src/config.py +70 -0
- package/src/errors.py +17 -0
- package/src/health.py +151 -3
- package/src/notify.py +11 -5
- package/src/provider_background.py +263 -0
- package/src/provider_runtime.py +142 -15
- package/src/run_command.py +112 -4
- package/src/run_failover.py +221 -0
- package/src/run_registry.py +127 -2
- package/src/session_backup.py +343 -0
- package/src/session_helpers.py +179 -0
- package/src/session_ranking.py +259 -0
- package/src/session_service.py +698 -1426
- package/src/session_status.py +550 -0
- package/src/status_source.py +45 -0
- package/src/status_view.py +15 -34
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cdx-manager",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Terminal session manager for Codex and Claude accounts.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Alexandre Agostini",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"checksums",
|
|
31
31
|
"changelogs",
|
|
32
32
|
"src",
|
|
33
|
-
"!src
|
|
33
|
+
"!src/**/__pycache__",
|
|
34
34
|
"!src/**/*.pyc",
|
|
35
35
|
"install.sh",
|
|
36
36
|
"install.ps1",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"test": "npm run test:py",
|
|
47
47
|
"test:py": "node bin/python-runner.js -m pytest",
|
|
48
48
|
"test:coverage": "node bin/python-runner.js -m pytest --cov=src --cov-report=term-missing",
|
|
49
|
-
"lint": "node --check bin/cdx.js && node --check bin/python-runner.js && node bin/python-runner.js scripts/verify_project_docs.py && node bin/python-runner.js -m ruff check . && node bin/python-runner.js -m py_compile bin/cdx src/*.py scripts/*.py test
|
|
49
|
+
"lint": "node --check bin/cdx.js && node --check bin/python-runner.js && node bin/python-runner.js scripts/verify_project_docs.py && node bin/python-runner.js -m ruff check . && node bin/python-runner.js -m py_compile bin/cdx src/*.py src/commands/*.py scripts/*.py test/*.py",
|
|
50
50
|
"release:validate": "node bin/python-runner.js scripts/verify_release_checksums.py",
|
|
51
51
|
"prepublishOnly": "npm run release:validate && npm run lint && npm test",
|
|
52
52
|
"link": "npm link",
|
package/pyproject.toml
CHANGED
package/src/cli.py
CHANGED
|
@@ -43,7 +43,9 @@ from .cli_commands import (
|
|
|
43
43
|
handle_run,
|
|
44
44
|
handle_run_report,
|
|
45
45
|
handle_run_status,
|
|
46
|
+
handle_run_tail,
|
|
46
47
|
handle_runs,
|
|
48
|
+
handle_schema,
|
|
47
49
|
handle_select,
|
|
48
50
|
handle_set,
|
|
49
51
|
handle_stats,
|
|
@@ -78,7 +80,38 @@ from .status_view import (
|
|
|
78
80
|
)
|
|
79
81
|
from .update_check import check_for_update, check_logics_manager_for_update
|
|
80
82
|
|
|
81
|
-
|
|
83
|
+
|
|
84
|
+
def _resolve_version():
|
|
85
|
+
"""The version, resolved rather than restated.
|
|
86
|
+
|
|
87
|
+
This was a hardcoded string, a fourth copy alongside VERSION, package.json,
|
|
88
|
+
and pyproject.toml, with nothing keeping the four in step — so
|
|
89
|
+
`cdx --version` could report a release it was not.
|
|
90
|
+
|
|
91
|
+
The VERSION file next to the package wins when it exists, because that only
|
|
92
|
+
happens in a checkout, where it is the thing being edited and the installed
|
|
93
|
+
metadata is whatever was last `pip install`ed. Installed packages have no
|
|
94
|
+
such file and fall through to their own metadata.
|
|
95
|
+
"""
|
|
96
|
+
version_file = os.path.join(
|
|
97
|
+
os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "VERSION"
|
|
98
|
+
)
|
|
99
|
+
try:
|
|
100
|
+
with open(version_file, encoding="utf-8") as handle:
|
|
101
|
+
text = handle.read().strip()
|
|
102
|
+
if text:
|
|
103
|
+
return text
|
|
104
|
+
except OSError:
|
|
105
|
+
pass
|
|
106
|
+
try:
|
|
107
|
+
from importlib.metadata import version
|
|
108
|
+
|
|
109
|
+
return version("cdx-manager")
|
|
110
|
+
except Exception: # noqa: BLE001 - any resolution failure falls back below
|
|
111
|
+
return "0.0.0"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
VERSION = _resolve_version()
|
|
82
115
|
|
|
83
116
|
# Public surface: this module is a facade. Names below are imported above
|
|
84
117
|
# purely to be re-exported (consumed by tests and external callers); listing
|
|
@@ -130,7 +163,9 @@ _COMMAND_HANDLERS = {
|
|
|
130
163
|
"run": handle_run,
|
|
131
164
|
"run-report": handle_run_report,
|
|
132
165
|
"run-status": handle_run_status,
|
|
166
|
+
"run-tail": handle_run_tail,
|
|
133
167
|
"runs": handle_runs,
|
|
168
|
+
"schema": handle_schema,
|
|
134
169
|
"select": handle_select,
|
|
135
170
|
"set": handle_set,
|
|
136
171
|
"stats": handle_stats,
|
|
@@ -183,10 +218,12 @@ def _print_help(use_color=False):
|
|
|
183
218
|
f" {_style('cdx next [--json] [--refresh]', '36', use_color)}",
|
|
184
219
|
f" {_style('cdx reset <name> [--yes] [--json]', '36', use_color)}",
|
|
185
220
|
f" {_style('cdx select --provider PROVIDER [--min-reasoning-effort minimal|low|medium|high|xhigh] [--min-power minimal|low|medium|high|xhigh] [--require-ready] [--refresh] --json', '36', use_color)}",
|
|
186
|
-
f" {_style('cdx run [session] --cwd PATH (--prompt-file PATH|--prompt TEXT) [--provider PROVIDER] [--model MODEL] [--kind assistant|code-review] [--reasoning-effort minimal|low|medium|high|xhigh] [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full|workspace-write|read-only|danger-full-access] [--timeout-seconds N] --json', '36', use_color)}",
|
|
187
|
-
f" {_style('cdx runs [--limit N] --json', '36', use_color)}",
|
|
221
|
+
f" {_style('cdx run [session] --cwd PATH (--prompt-file PATH|--prompt TEXT|--prompt-file -) [--provider PROVIDER] [--model MODEL] [--kind assistant|code-review] [--reasoning-effort minimal|low|medium|high|xhigh] [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full|workspace-write|read-only|danger-full-access] [--timeout-seconds N] [--detach] [--refresh] --json', '36', use_color)}",
|
|
222
|
+
f" {_style('cdx runs [--limit N] [--since 7d|today|DATE] --json', '36', use_color)}",
|
|
188
223
|
f" {_style('cdx run-status <run_id> --json', '36', use_color)}",
|
|
189
224
|
f" {_style('cdx run-report <run_id> --json', '36', use_color)}",
|
|
225
|
+
f" {_style('cdx run-tail <run_id> [--lines N] --json', '36', use_color)}",
|
|
226
|
+
f" {_style('cdx schema --json', '36', use_color)}",
|
|
190
227
|
f" {_style('cdx context show|path|init|edit|clear|set|append [text...] [--json]', '36', use_color)}",
|
|
191
228
|
f" {_style('cdx memory [--global|--project NAME_OR_PATH] [show|view|path|init|edit|clear|set|append|list] [text...] [--json]', '36', use_color)}",
|
|
192
229
|
f" {_style('cdx disk [profiles] [--candidates] [--json]', '36', use_color)}",
|
|
@@ -216,7 +253,7 @@ def _print_help(use_color=False):
|
|
|
216
253
|
f" {_style('cdx clean profiles (--tmp|--old-logs DAYS) [--yes] [--json]', '36', use_color)}",
|
|
217
254
|
f" {_style('cdx export <file> [--include-auth] [--sessions a,b] [--passphrase-env VAR|--passphrase-stdin] [--force] [--json]', '36', use_color)}",
|
|
218
255
|
f" {_style('cdx import <file> [--sessions a,b] [--passphrase-env VAR|--passphrase-stdin] [--force|--merge] [--allow-authless-force] [--json]', '36', use_color)}",
|
|
219
|
-
f" {_style('cdx doctor [--severity OK|WARN|FAIL[,OK|WARN|FAIL...]] [--json]', '36', use_color)}",
|
|
256
|
+
f" {_style('cdx doctor [--severity OK|WARN|FAIL[,OK|WARN|FAIL...]] [--check-provider-flags] [--json]', '36', use_color)}",
|
|
220
257
|
f" {_style('cdx repair [--dry-run] [--force] [--json]', '36', use_color)}",
|
|
221
258
|
f" {_style('cdx view [--json]', '36', use_color)}",
|
|
222
259
|
f" {_style('cdx update [all] [--check] [--yes] [--json] [--version TAG]', '36', use_color)}",
|
|
@@ -239,6 +276,25 @@ def wants_json(argv):
|
|
|
239
276
|
|
|
240
277
|
def format_json_error(error):
|
|
241
278
|
message = str(error)
|
|
279
|
+
# An error that already knows its own code keeps it. Only errors that never
|
|
280
|
+
# declared one fall back to sniffing the message prefix below.
|
|
281
|
+
declared = getattr(error, "code", None)
|
|
282
|
+
if declared:
|
|
283
|
+
allowed = getattr(error, "allowed", None)
|
|
284
|
+
# Same field set as run_result_payload's error object, so a caller does
|
|
285
|
+
# not need two shapes depending on which command raised.
|
|
286
|
+
return json.dumps({
|
|
287
|
+
"schema_version": API_SCHEMA_VERSION,
|
|
288
|
+
"ok": False,
|
|
289
|
+
"error": {
|
|
290
|
+
"source": "cdx",
|
|
291
|
+
"code": declared,
|
|
292
|
+
"message": message,
|
|
293
|
+
"arguments": list(getattr(error, "arguments", ()) or []),
|
|
294
|
+
"allowed_values": list(allowed) if allowed else None,
|
|
295
|
+
"exit_code": error.exit_code,
|
|
296
|
+
},
|
|
297
|
+
}, indent=2)
|
|
242
298
|
code = "cdx_error"
|
|
243
299
|
if message.startswith("Usage:"):
|
|
244
300
|
code = "invalid_usage"
|
|
@@ -506,7 +562,11 @@ def main(argv, options=None):
|
|
|
506
562
|
"service": service,
|
|
507
563
|
"signal_emitter": signal_emitter,
|
|
508
564
|
"spawn": spawn,
|
|
565
|
+
"spawn_detached": options.get("spawn_detached"),
|
|
509
566
|
"spawn_headless": options.get("spawn_headless"),
|
|
567
|
+
# Stream `--prompt-file -` reads from. Distinct from "stdin", which is
|
|
568
|
+
# the {"isTTY": ...} descriptor the interactive paths use.
|
|
569
|
+
"prompt_stdin": options.get("prompt_stdin"),
|
|
510
570
|
"spawn_sync": spawn_sync,
|
|
511
571
|
"stdin_is_tty": stdin_is_tty,
|
|
512
572
|
"version": VERSION,
|
package/src/cli_args.py
CHANGED
|
@@ -7,12 +7,21 @@ imported back there and used by the handlers.
|
|
|
7
7
|
|
|
8
8
|
from datetime import datetime, timedelta
|
|
9
9
|
|
|
10
|
-
from .config import
|
|
11
|
-
|
|
10
|
+
from .config import (
|
|
11
|
+
MAX_LAUNCH_BUDGET_USD,
|
|
12
|
+
PERMISSION_ALIASES,
|
|
13
|
+
PERMISSION_INPUT_VALUES,
|
|
14
|
+
PERMISSION_VALUES,
|
|
15
|
+
PROVIDER_CODEX,
|
|
16
|
+
PROVIDERS,
|
|
17
|
+
REASONING_EFFORT_VALUES,
|
|
18
|
+
normalize_permission,
|
|
19
|
+
)
|
|
20
|
+
from .errors import CdxArgumentError, CdxError
|
|
12
21
|
from .provider_runtime import _normalize_reasoning_effort
|
|
13
22
|
|
|
14
23
|
STATUS_USAGE = "Usage: cdx status [--json] [--refresh|--cached] [--timeout SECONDS] | cdx status --small|-s [--refresh|--cached] [--timeout SECONDS] | cdx status <name> [--json] [--refresh|--cached] [--timeout SECONDS]"
|
|
15
|
-
DOCTOR_USAGE = "Usage: cdx doctor [--severity OK|WARN|FAIL[,OK|WARN|FAIL...]] [--json]"
|
|
24
|
+
DOCTOR_USAGE = "Usage: cdx doctor [--severity OK|WARN|FAIL[,OK|WARN|FAIL...]] [--check-provider-flags] [--json]"
|
|
16
25
|
DISK_USAGE = "Usage: cdx disk [profiles] [--candidates] [--json]"
|
|
17
26
|
REPAIR_USAGE = "Usage: cdx repair [--dry-run] [--force] [--json]"
|
|
18
27
|
UPDATE_USAGE = "Usage: cdx update [all] [--check] [--yes] [--json] [--version TAG]"
|
|
@@ -22,8 +31,8 @@ CONTEXT_USAGE = "Usage: cdx context show|path|init|edit|clear|set|append [text..
|
|
|
22
31
|
MEMORY_USAGE = "Usage: cdx memory [--global|--project NAME_OR_PATH] [show|view|path|init|edit|clear|set|append|list] [text...] [--json]"
|
|
23
32
|
HANDOFF_USAGE = "Usage: cdx handoff <name> [--json] | cdx handoff <source> <target> [--json]"
|
|
24
33
|
LABEL_USAGE = "Usage: cdx label <name> <label> [--json] | cdx label <name> --clear [--json]"
|
|
25
|
-
SET_USAGE = "Usage: cdx set <name>|--sessions all|a,b|--provider PROVIDER [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full] [--fast on|off] [--rtk on|off] [--logics on|off] [--model MODEL] [--priority 0..100] [--json]"
|
|
26
|
-
UNSET_USAGE = "Usage: cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--reasoning-effort|--permission|--fast|--rtk|--logics|--model|--priority|--all) [--json]"
|
|
34
|
+
SET_USAGE = "Usage: cdx set <name>|--sessions all|a,b|--provider PROVIDER [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full] [--fast on|off] [--rtk on|off] [--logics on|off] [--model MODEL] [--fallback-model MODEL[,MODEL...]] [--budget USD] [--extra-args 'ARGS'] [--priority 0..100] [--json]"
|
|
35
|
+
UNSET_USAGE = "Usage: cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--reasoning-effort|--permission|--fast|--rtk|--logics|--model|--fallback-model|--budget|--extra-args|--priority|--all) [--json]"
|
|
27
36
|
SETTING_ALIAS_USAGE = "Usage: cdx power|perm|fast|model <name|all|provider:PROVIDER|a,b> <value|default> [--json]"
|
|
28
37
|
CONFIG_USAGE = "Usage: cdx config <name> [--json]"
|
|
29
38
|
CONFIGS_USAGE = "Usage: cdx configs [--json]"
|
|
@@ -34,17 +43,37 @@ RESUME_USAGE = "Usage: cdx resume <name> [--json]"
|
|
|
34
43
|
CAN_RESUME_USAGE = "Usage: cdx can-resume <name> [--json]"
|
|
35
44
|
SELECT_USAGE = "Usage: cdx select --provider PROVIDER [--min-reasoning-effort minimal|low|medium|high|xhigh] [--min-power minimal|low|medium|high|xhigh] [--require-ready] [--refresh] --json"
|
|
36
45
|
NEXT_USAGE = "Usage: cdx next [--json] [--refresh]"
|
|
37
|
-
RUN_USAGE = "Usage: cdx run [session] --cwd PATH (--prompt-file PATH|--prompt TEXT) [--provider PROVIDER] [--model MODEL] [--kind assistant|code-review] [--reasoning-effort minimal|low|medium|high|xhigh] [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full|workspace-write|read-only|danger-full-access] [--timeout-seconds N] --json"
|
|
46
|
+
RUN_USAGE = "Usage: cdx run [session] --cwd PATH (--prompt-file PATH|--prompt TEXT|--prompt-file -) [--provider PROVIDER] [--model MODEL] [--kind assistant|code-review] [--reasoning-effort minimal|low|medium|high|xhigh] [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full|workspace-write|read-only|danger-full-access] [--timeout-seconds N] [--detach] [--failover] [--refresh] --json"
|
|
38
47
|
RUN_JSON_REQUIRED = "cdx run: --json is required."
|
|
39
48
|
RUN_TARGET_REQUIRED = "cdx run: specify a session name or --provider PROVIDER."
|
|
40
49
|
RUN_SESSION_PROVIDER_CONFLICT = "cdx run: cannot specify both a session name and --provider."
|
|
41
50
|
RUN_CWD_REQUIRED = "cdx run: --cwd PATH is required."
|
|
42
51
|
RUN_PROMPT_SOURCE_REQUIRED = "cdx run: specify exactly one prompt source: --prompt TEXT or --prompt-file PATH."
|
|
52
|
+
RUN_DETACH_FAILOVER_CONFLICT = "cdx run: --failover needs to watch the run, which --detach gives up."
|
|
43
53
|
RUN_KIND_VALUES = ("assistant", "code-review")
|
|
44
|
-
|
|
45
|
-
|
|
54
|
+
# Aliases of the shared definitions in config, kept only so the existing
|
|
55
|
+
# RUN_* spellings in this module keep reading naturally. They are references,
|
|
56
|
+
# not copies: config.py is the single owner.
|
|
57
|
+
RUN_PERMISSION_VALUES = PERMISSION_INPUT_VALUES
|
|
58
|
+
RUN_PERMISSION_CANONICAL_VALUES = PERMISSION_VALUES
|
|
59
|
+
RUN_PERMISSION_ALIASES = PERMISSION_ALIASES
|
|
60
|
+
RUN_EFFORT_VALUES = REASONING_EFFORT_VALUES
|
|
61
|
+
RUNS_USAGE = "Usage: cdx runs [--limit N] [--since 7d|today|DATE] --json"
|
|
46
62
|
RUN_STATUS_USAGE = "Usage: cdx run-status <run_id> --json"
|
|
47
63
|
RUN_REPORT_USAGE = "Usage: cdx run-report <run_id> --json"
|
|
64
|
+
RUN_TAIL_USAGE = "Usage: cdx run-tail <run_id> [--lines N] --json"
|
|
65
|
+
SCHEMA_USAGE = "Usage: cdx schema --json"
|
|
66
|
+
|
|
67
|
+
RUN_TAIL_DEFAULT_LINES = 50
|
|
68
|
+
RUN_TAIL_MAX_LINES = 2000
|
|
69
|
+
|
|
70
|
+
# Argument-failure codes emitted in `error.code`. Specific enough that a caller
|
|
71
|
+
# branches on the code alone, never on the human message.
|
|
72
|
+
ARG_CODE_MISSING = "missing_required_argument"
|
|
73
|
+
ARG_CODE_MUTUALLY_EXCLUSIVE = "mutually_exclusive_arguments"
|
|
74
|
+
ARG_CODE_INVALID_VALUE = "invalid_argument_value"
|
|
75
|
+
ARG_CODE_OUT_OF_RANGE = "argument_value_out_of_range"
|
|
76
|
+
ARG_CODE_UNKNOWN = "unknown_argument"
|
|
48
77
|
|
|
49
78
|
|
|
50
79
|
def _parse_json_flag(args):
|
|
@@ -54,7 +83,7 @@ def _parse_json_flag(args):
|
|
|
54
83
|
|
|
55
84
|
|
|
56
85
|
def _parse_doctor_args(args):
|
|
57
|
-
parsed = {"json": False, "severity": None}
|
|
86
|
+
parsed = {"json": False, "severity": None, "check_provider_flags": False}
|
|
58
87
|
index = 0
|
|
59
88
|
while index < len(args):
|
|
60
89
|
arg = args[index]
|
|
@@ -62,6 +91,10 @@ def _parse_doctor_args(args):
|
|
|
62
91
|
parsed["json"] = True
|
|
63
92
|
index += 1
|
|
64
93
|
continue
|
|
94
|
+
if arg == "--check-provider-flags":
|
|
95
|
+
parsed["check_provider_flags"] = True
|
|
96
|
+
index += 1
|
|
97
|
+
continue
|
|
65
98
|
if arg == "--severity":
|
|
66
99
|
if parsed["severity"] is not None or index + 1 >= len(args):
|
|
67
100
|
raise CdxError(DOCTOR_USAGE)
|
|
@@ -197,6 +230,17 @@ def _parse_priority_value(value):
|
|
|
197
230
|
return priority
|
|
198
231
|
|
|
199
232
|
|
|
233
|
+
def _parse_budget_value(value):
|
|
234
|
+
try:
|
|
235
|
+
budget = float(value)
|
|
236
|
+
except (TypeError, ValueError) as error:
|
|
237
|
+
raise CdxError(SET_USAGE) from error
|
|
238
|
+
# NaN compares false against every bound, so reject it before the range check.
|
|
239
|
+
if budget != budget or budget <= 0 or budget > MAX_LAUNCH_BUDGET_USD:
|
|
240
|
+
raise CdxError(SET_USAGE)
|
|
241
|
+
return budget
|
|
242
|
+
|
|
243
|
+
|
|
200
244
|
def _parse_set_args(args):
|
|
201
245
|
parsed = _parse_flag_args(args, {
|
|
202
246
|
"--power": {"key": "power", "type": "str", "default": None},
|
|
@@ -205,6 +249,9 @@ def _parse_set_args(args):
|
|
|
205
249
|
"--rtk": {"key": "rtk", "type": "str", "default": None, "transform": _parse_fast_value},
|
|
206
250
|
"--logics": {"key": "logics", "type": "str", "default": None, "transform": _parse_fast_value},
|
|
207
251
|
"--model": {"key": "model", "type": "str", "default": None},
|
|
252
|
+
"--fallback-model": {"key": "fallback_model", "type": "str", "default": None},
|
|
253
|
+
"--budget": {"key": "budget", "type": "str", "default": None, "transform": _parse_budget_value},
|
|
254
|
+
"--extra-args": {"key": "extra_args", "type": "str", "default": None},
|
|
208
255
|
"--priority": {"key": "priority", "type": "str", "default": None, "transform": _parse_priority_value},
|
|
209
256
|
"--sessions": {"key": "sessions", "type": "str", "default": None, "transform": _parse_set_unset_sessions},
|
|
210
257
|
"--provider": {"key": "provider", "type": "str", "default": None, "transform": lambda value: _parse_provider_filter(value, SET_USAGE)},
|
|
@@ -222,7 +269,7 @@ def _parse_set_args(args):
|
|
|
222
269
|
raise CdxError(SET_USAGE)
|
|
223
270
|
settings = {
|
|
224
271
|
key: parsed[key]
|
|
225
|
-
for key in ("power", "permission", "fast", "rtk", "logics", "model", "priority")
|
|
272
|
+
for key in ("power", "permission", "fast", "rtk", "logics", "model", "fallback_model", "budget", "extra_args", "priority")
|
|
226
273
|
if parsed[key] is not None
|
|
227
274
|
}
|
|
228
275
|
if not settings:
|
|
@@ -245,6 +292,9 @@ def _parse_unset_args(args):
|
|
|
245
292
|
"--rtk": {"key": "rtk", "type": "bool", "default": False},
|
|
246
293
|
"--logics": {"key": "logics", "type": "bool", "default": False},
|
|
247
294
|
"--model": {"key": "model", "type": "bool", "default": False},
|
|
295
|
+
"--fallback-model": {"key": "fallback_model", "type": "bool", "default": False},
|
|
296
|
+
"--budget": {"key": "budget", "type": "bool", "default": False},
|
|
297
|
+
"--extra-args": {"key": "extra_args", "type": "bool", "default": False},
|
|
248
298
|
"--priority": {"key": "priority", "type": "bool", "default": False},
|
|
249
299
|
"--all": {"key": "all", "type": "bool", "default": False},
|
|
250
300
|
"--sessions": {"key": "sessions", "type": "str", "default": None, "transform": _parse_set_unset_sessions},
|
|
@@ -261,9 +311,11 @@ def _parse_unset_args(args):
|
|
|
261
311
|
raise CdxError(UNSET_USAGE)
|
|
262
312
|
if not parsed["names"] and not parsed["sessions"] and not parsed["provider"]:
|
|
263
313
|
raise CdxError(UNSET_USAGE)
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
314
|
+
_UNSET_KEYS = (
|
|
315
|
+
"power", "reasoning_effort", "permission", "fast", "rtk", "logics",
|
|
316
|
+
"model", "fallback_model", "budget", "extra_args", "priority",
|
|
317
|
+
)
|
|
318
|
+
keys = list(_UNSET_KEYS) if parsed["all"] else [key for key in _UNSET_KEYS if parsed[key]]
|
|
267
319
|
if not keys:
|
|
268
320
|
raise CdxError(UNSET_USAGE)
|
|
269
321
|
return {
|
|
@@ -338,7 +390,11 @@ def _parse_run_timeout_seconds(value):
|
|
|
338
390
|
try:
|
|
339
391
|
return _parse_timeout_seconds(value, RUN_USAGE)
|
|
340
392
|
except CdxError as error:
|
|
341
|
-
raise
|
|
393
|
+
raise CdxArgumentError(
|
|
394
|
+
f"cdx run: --timeout-seconds must be a positive number; got {value!r}.",
|
|
395
|
+
code=ARG_CODE_OUT_OF_RANGE,
|
|
396
|
+
arguments=["--timeout-seconds"],
|
|
397
|
+
) from error
|
|
342
398
|
|
|
343
399
|
|
|
344
400
|
def _parse_run_provider(value):
|
|
@@ -346,27 +402,183 @@ def _parse_run_provider(value):
|
|
|
346
402
|
return _parse_provider_filter(value, RUN_USAGE)
|
|
347
403
|
except CdxError as error:
|
|
348
404
|
allowed = "|".join(PROVIDERS)
|
|
349
|
-
raise
|
|
405
|
+
raise CdxArgumentError(
|
|
406
|
+
f"cdx run: invalid --provider {value!r}; allowed values: {allowed}.",
|
|
407
|
+
code=ARG_CODE_INVALID_VALUE,
|
|
408
|
+
arguments=["--provider"],
|
|
409
|
+
allowed=PROVIDERS,
|
|
410
|
+
) from error
|
|
350
411
|
|
|
351
412
|
|
|
352
413
|
def _normalize_run_permission(value):
|
|
353
414
|
if value is None:
|
|
354
415
|
return None
|
|
355
|
-
text =
|
|
356
|
-
|
|
357
|
-
"workspace-write": "default",
|
|
358
|
-
"read-only": "review",
|
|
359
|
-
"danger-full-access": "full",
|
|
360
|
-
}
|
|
361
|
-
text = aliases.get(text, text)
|
|
362
|
-
if text not in ("review", "default", "auto", "full"):
|
|
416
|
+
text = normalize_permission(value)
|
|
417
|
+
if text is None:
|
|
363
418
|
allowed = "|".join(RUN_PERMISSION_VALUES)
|
|
364
|
-
raise
|
|
419
|
+
raise CdxArgumentError(
|
|
420
|
+
f"cdx run: invalid --permission {value!r}; allowed values: {allowed}.",
|
|
421
|
+
code=ARG_CODE_INVALID_VALUE,
|
|
422
|
+
arguments=["--permission"],
|
|
423
|
+
allowed=RUN_PERMISSION_VALUES,
|
|
424
|
+
)
|
|
365
425
|
return text
|
|
366
426
|
|
|
367
427
|
|
|
368
|
-
def
|
|
428
|
+
def cdx_schema():
|
|
429
|
+
"""The machine-readable contract for programmatic callers.
|
|
430
|
+
|
|
431
|
+
Every value here is the same object the parser validates against, not a
|
|
432
|
+
parallel copy — `cdx schema --json` cannot advertise a value `cdx run`
|
|
433
|
+
rejects, which is exactly how the ollama `--experimental-yolo` mapping
|
|
434
|
+
survived unexercised (GitHub issue #8).
|
|
435
|
+
"""
|
|
436
|
+
# No `schema_version` here: the JSON envelope already carries it, and two
|
|
437
|
+
# keys of that name in one payload would be one too many.
|
|
438
|
+
return {
|
|
439
|
+
"enums": {
|
|
440
|
+
"permission": {
|
|
441
|
+
"accepted": list(RUN_PERMISSION_VALUES),
|
|
442
|
+
"canonical": list(RUN_PERMISSION_CANONICAL_VALUES),
|
|
443
|
+
"aliases": dict(RUN_PERMISSION_ALIASES),
|
|
444
|
+
},
|
|
445
|
+
"reasoning_effort": {"accepted": list(RUN_EFFORT_VALUES)},
|
|
446
|
+
"power": {"accepted": list(RUN_EFFORT_VALUES)},
|
|
447
|
+
"kind": {"accepted": list(RUN_KIND_VALUES)},
|
|
448
|
+
"provider": {"accepted": list(PROVIDERS)},
|
|
449
|
+
},
|
|
450
|
+
# Passthrough arguments are deliberately absent from this contract.
|
|
451
|
+
# `cdx set --extra-args` reaches the provider unchanged and cdx never
|
|
452
|
+
# validates it, so publishing it here would claim a guarantee that does
|
|
453
|
+
# not exist. A caller must treat it as opaque.
|
|
454
|
+
"unvalidated": {
|
|
455
|
+
"extra_args": {
|
|
456
|
+
"reason": "Passed to the provider CLI unchanged; cdx neither parses nor verifies these arguments.",
|
|
457
|
+
"not_checked_by": ["cdx schema", "cdx doctor --check-provider-flags"],
|
|
458
|
+
},
|
|
459
|
+
},
|
|
460
|
+
"mutually_exclusive": [
|
|
461
|
+
{
|
|
462
|
+
"command": "run",
|
|
463
|
+
"arguments": ["session", "--provider"],
|
|
464
|
+
"reason": "A session already fixes its provider; pass one or the other.",
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
"command": "run",
|
|
468
|
+
"arguments": ["--prompt-file", "--prompt"],
|
|
469
|
+
"reason": "Exactly one prompt source.",
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
"command": "run",
|
|
473
|
+
"arguments": ["--detach", "--failover"],
|
|
474
|
+
"reason": "Failover has to watch the run to react to a rate limit; --detach stops watching.",
|
|
475
|
+
},
|
|
476
|
+
],
|
|
477
|
+
# Not mutually exclusive: both may be given, and cdx accepts them when
|
|
478
|
+
# they agree. A caller generating validation from `mutually_exclusive`
|
|
479
|
+
# would otherwise reject a command line cdx would have run.
|
|
480
|
+
"must_match": [
|
|
481
|
+
{
|
|
482
|
+
"command": "run",
|
|
483
|
+
"arguments": ["--reasoning-effort", "--power"],
|
|
484
|
+
"reason": "Aliases of one setting; supplying both with different values fails.",
|
|
485
|
+
},
|
|
486
|
+
],
|
|
487
|
+
# Every code a run-family command can put in `error.code`, not just the
|
|
488
|
+
# argument ones — a caller matching exhaustively over this list must not
|
|
489
|
+
# fall through on a code it will certainly see.
|
|
490
|
+
"error_codes": {
|
|
491
|
+
"arguments": [
|
|
492
|
+
ARG_CODE_MISSING,
|
|
493
|
+
ARG_CODE_MUTUALLY_EXCLUSIVE,
|
|
494
|
+
ARG_CODE_INVALID_VALUE,
|
|
495
|
+
ARG_CODE_OUT_OF_RANGE,
|
|
496
|
+
ARG_CODE_UNKNOWN,
|
|
497
|
+
"invalid_reasoning_effort",
|
|
498
|
+
],
|
|
499
|
+
"run": [
|
|
500
|
+
"invalid_cwd",
|
|
501
|
+
"session_disabled",
|
|
502
|
+
"no_suitable_session",
|
|
503
|
+
"provider_cli_not_found",
|
|
504
|
+
"provider_start_failed",
|
|
505
|
+
"provider_failed",
|
|
506
|
+
"provider_timeout",
|
|
507
|
+
"cdx_error",
|
|
508
|
+
],
|
|
509
|
+
"run_lookup": [
|
|
510
|
+
"run_not_found",
|
|
511
|
+
"run_output_unavailable",
|
|
512
|
+
"run_output_unreadable",
|
|
513
|
+
],
|
|
514
|
+
},
|
|
515
|
+
"warning_codes": [
|
|
516
|
+
"network_disabled_by_permission",
|
|
517
|
+
"session_selected_without_status",
|
|
518
|
+
"limit_ignored_with_since",
|
|
519
|
+
],
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def _parse_schema_args(args):
|
|
369
524
|
parsed = _parse_flag_args(args, {
|
|
525
|
+
"--json": {"key": "json", "type": "bool", "default": False},
|
|
526
|
+
}, SCHEMA_USAGE)
|
|
527
|
+
if not parsed["json"]:
|
|
528
|
+
raise CdxError(SCHEMA_USAGE)
|
|
529
|
+
return parsed
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def _parse_run_flag_args(args, schema, usage, positionals_key=None, max_positionals=0):
|
|
533
|
+
"""`_parse_flag_args` is shared by every command and raises a bare usage
|
|
534
|
+
error for an unrecognized flag or a value-less option. Only `run` promises a
|
|
535
|
+
specific code for those, so translate here rather than teaching the shared
|
|
536
|
+
parser about codes it does not need. Transforms that already raised a
|
|
537
|
+
`CdxArgumentError` keep their own, more precise code."""
|
|
538
|
+
try:
|
|
539
|
+
return _parse_flag_args(args, schema, usage, positionals_key, max_positionals)
|
|
540
|
+
except CdxArgumentError:
|
|
541
|
+
raise
|
|
542
|
+
except CdxError as error:
|
|
543
|
+
raise CdxArgumentError(str(error), code=ARG_CODE_UNKNOWN) from error
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def _parse_run_effort(reasoning_effort, power):
|
|
547
|
+
try:
|
|
548
|
+
return _normalize_reasoning_effort(
|
|
549
|
+
reasoning_effort=reasoning_effort,
|
|
550
|
+
power=power,
|
|
551
|
+
usage=RUN_USAGE,
|
|
552
|
+
)
|
|
553
|
+
except CdxError as error:
|
|
554
|
+
message = str(error)
|
|
555
|
+
if "must match" in message:
|
|
556
|
+
raise CdxArgumentError(
|
|
557
|
+
message,
|
|
558
|
+
code=ARG_CODE_MUTUALLY_EXCLUSIVE,
|
|
559
|
+
arguments=["--reasoning-effort", "--power"],
|
|
560
|
+
) from error
|
|
561
|
+
# An empty value raises the bare usage line, which names neither flag.
|
|
562
|
+
# Blame the one actually supplied rather than guessing, since naming the
|
|
563
|
+
# offending argument is the entire point of this error.
|
|
564
|
+
if message.startswith("Unsupported power:") or (power is not None and reasoning_effort is None):
|
|
565
|
+
argument = "--power"
|
|
566
|
+
else:
|
|
567
|
+
argument = "--reasoning-effort"
|
|
568
|
+
# Keeps the pre-existing `invalid_reasoning_effort` code rather than
|
|
569
|
+
# flattening it into the generic invalid-value one: it was already
|
|
570
|
+
# specific, and callers may already branch on it. The new codes exist to
|
|
571
|
+
# split what used to be indistinguishable, not to rename what wasn't.
|
|
572
|
+
raise CdxArgumentError(
|
|
573
|
+
message,
|
|
574
|
+
code="invalid_reasoning_effort",
|
|
575
|
+
arguments=[argument],
|
|
576
|
+
allowed=RUN_EFFORT_VALUES,
|
|
577
|
+
) from error
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def _parse_run_args(args):
|
|
581
|
+
parsed = _parse_run_flag_args(args, {
|
|
370
582
|
"--cwd": {"key": "cwd", "type": "str", "default": None},
|
|
371
583
|
"--prompt-file": {"key": "prompt_file", "type": "str", "default": None},
|
|
372
584
|
"--prompt": {"key": "prompt", "type": "str", "default": None},
|
|
@@ -377,27 +589,51 @@ def _parse_run_args(args):
|
|
|
377
589
|
"--power": {"key": "power", "type": "str", "default": None},
|
|
378
590
|
"--permission": {"key": "permission", "type": "str", "default": None, "transform": _normalize_run_permission},
|
|
379
591
|
"--timeout-seconds": {"key": "timeout_seconds", "type": "str", "default": None, "transform": _parse_run_timeout_seconds},
|
|
592
|
+
"--detach": {"key": "detach", "type": "bool", "default": False},
|
|
593
|
+
"--failover": {"key": "failover", "type": "bool", "default": False},
|
|
594
|
+
"--refresh": {"key": "refresh", "type": "bool", "default": False},
|
|
380
595
|
"--json": {"key": "json", "type": "bool", "default": False},
|
|
381
596
|
}, RUN_USAGE, positionals_key="names", max_positionals=1)
|
|
382
597
|
if not parsed["json"]:
|
|
383
|
-
raise
|
|
598
|
+
raise CdxArgumentError(RUN_JSON_REQUIRED, code=ARG_CODE_MISSING, arguments=["--json"])
|
|
384
599
|
name = parsed["names"][0] if parsed["names"] else None
|
|
385
600
|
if name and parsed["provider"]:
|
|
386
|
-
raise
|
|
601
|
+
raise CdxArgumentError(
|
|
602
|
+
RUN_SESSION_PROVIDER_CONFLICT,
|
|
603
|
+
code=ARG_CODE_MUTUALLY_EXCLUSIVE,
|
|
604
|
+
arguments=["session", "--provider"],
|
|
605
|
+
)
|
|
387
606
|
if not name and not parsed["provider"]:
|
|
388
|
-
raise
|
|
607
|
+
raise CdxArgumentError(RUN_TARGET_REQUIRED, code=ARG_CODE_MISSING, arguments=["session", "--provider"])
|
|
389
608
|
if not parsed["cwd"]:
|
|
390
|
-
raise
|
|
391
|
-
if
|
|
392
|
-
raise
|
|
609
|
+
raise CdxArgumentError(RUN_CWD_REQUIRED, code=ARG_CODE_MISSING, arguments=["--cwd"])
|
|
610
|
+
if parsed["prompt_file"] and parsed["prompt"]:
|
|
611
|
+
raise CdxArgumentError(
|
|
612
|
+
RUN_PROMPT_SOURCE_REQUIRED,
|
|
613
|
+
code=ARG_CODE_MUTUALLY_EXCLUSIVE,
|
|
614
|
+
arguments=["--prompt-file", "--prompt"],
|
|
615
|
+
)
|
|
616
|
+
if parsed["detach"] and parsed["failover"]:
|
|
617
|
+
raise CdxArgumentError(
|
|
618
|
+
RUN_DETACH_FAILOVER_CONFLICT,
|
|
619
|
+
code=ARG_CODE_MUTUALLY_EXCLUSIVE,
|
|
620
|
+
arguments=["--detach", "--failover"],
|
|
621
|
+
)
|
|
622
|
+
if not parsed["prompt_file"] and not parsed["prompt"]:
|
|
623
|
+
raise CdxArgumentError(
|
|
624
|
+
RUN_PROMPT_SOURCE_REQUIRED,
|
|
625
|
+
code=ARG_CODE_MISSING,
|
|
626
|
+
arguments=["--prompt-file", "--prompt"],
|
|
627
|
+
)
|
|
393
628
|
if parsed["kind"] not in RUN_KIND_VALUES:
|
|
394
629
|
allowed = "|".join(RUN_KIND_VALUES)
|
|
395
|
-
raise
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
630
|
+
raise CdxArgumentError(
|
|
631
|
+
f"cdx run: invalid --kind {parsed['kind']!r}; allowed values: {allowed}.",
|
|
632
|
+
code=ARG_CODE_INVALID_VALUE,
|
|
633
|
+
arguments=["--kind"],
|
|
634
|
+
allowed=RUN_KIND_VALUES,
|
|
635
|
+
)
|
|
636
|
+
effort = _parse_run_effort(parsed["reasoning_effort"], parsed["power"])
|
|
401
637
|
return {
|
|
402
638
|
"name": name,
|
|
403
639
|
"provider": parsed["provider"],
|
|
@@ -408,6 +644,9 @@ def _parse_run_args(args):
|
|
|
408
644
|
"model": parsed["model"],
|
|
409
645
|
"permission": parsed["permission"],
|
|
410
646
|
"timeout_seconds": parsed["timeout_seconds"],
|
|
647
|
+
"detach": parsed["detach"],
|
|
648
|
+
"failover": parsed["failover"],
|
|
649
|
+
"refresh": parsed["refresh"],
|
|
411
650
|
"reasoning_effort": effort.get("reasoning_effort"),
|
|
412
651
|
"power": effort.get("power"),
|
|
413
652
|
}
|
|
@@ -709,11 +948,28 @@ def _parse_import_args(args):
|
|
|
709
948
|
return parsed
|
|
710
949
|
|
|
711
950
|
|
|
712
|
-
def _parse_runs_args(rest):
|
|
713
|
-
|
|
951
|
+
def _parse_runs_args(rest, now=None):
|
|
952
|
+
now = now or datetime.now().astimezone()
|
|
953
|
+
parsed = _parse_flag_args(rest, {
|
|
714
954
|
"--limit": {"key": "limit", "type": "str", "default": 20, "transform": _parse_positive_int},
|
|
955
|
+
"--since": {"key": "since", "type": "str", "default": None},
|
|
715
956
|
"--json": {"key": "json", "type": "bool", "default": False},
|
|
716
957
|
}, RUNS_USAGE, max_positionals=0)
|
|
958
|
+
since = None
|
|
959
|
+
if parsed["since"] is not None:
|
|
960
|
+
# Same cursor forms as `cdx history --since`, parsed by the same code:
|
|
961
|
+
# a caller that learned one syntax does not learn a second one.
|
|
962
|
+
since = _parse_since_value(parsed["since"], now, RUNS_USAGE)
|
|
963
|
+
return {
|
|
964
|
+
"limit": parsed["limit"],
|
|
965
|
+
"since": since,
|
|
966
|
+
# A cursor selects by completion time, not by row count. Keeping the
|
|
967
|
+
# caller's own --limit would silently reintroduce the miss it removes:
|
|
968
|
+
# a watchdog behind by more than `limit` completions would never see
|
|
969
|
+
# the older ones at all.
|
|
970
|
+
"limit_given": any(arg == "--limit" or arg.startswith("--limit=") for arg in rest),
|
|
971
|
+
"json": parsed["json"],
|
|
972
|
+
}
|
|
717
973
|
|
|
718
974
|
|
|
719
975
|
def _parse_run_id_json_args(rest, usage):
|
|
@@ -723,3 +979,32 @@ def _parse_run_id_json_args(rest, usage):
|
|
|
723
979
|
if not parsed["json"] or len(parsed["ids"]) != 1:
|
|
724
980
|
raise CdxError(usage)
|
|
725
981
|
return {"run_id": parsed["ids"][0], "json": True}
|
|
982
|
+
|
|
983
|
+
|
|
984
|
+
def _parse_run_tail_args(rest):
|
|
985
|
+
parsed = _parse_flag_args(rest, {
|
|
986
|
+
"--lines": {"key": "lines", "type": "str", "default": RUN_TAIL_DEFAULT_LINES,
|
|
987
|
+
"transform": _parse_run_tail_lines},
|
|
988
|
+
"--json": {"key": "json", "type": "bool", "default": False},
|
|
989
|
+
}, RUN_TAIL_USAGE, positionals_key="ids", max_positionals=1)
|
|
990
|
+
if not parsed["json"] or len(parsed["ids"]) != 1:
|
|
991
|
+
raise CdxError(RUN_TAIL_USAGE)
|
|
992
|
+
return {"run_id": parsed["ids"][0], "lines": parsed["lines"], "json": True}
|
|
993
|
+
|
|
994
|
+
|
|
995
|
+
def _parse_run_tail_lines(value):
|
|
996
|
+
try:
|
|
997
|
+
parsed = int(value)
|
|
998
|
+
except (TypeError, ValueError) as error:
|
|
999
|
+
raise CdxArgumentError(
|
|
1000
|
+
f"cdx run-tail: --lines must be an integer; got {value!r}.",
|
|
1001
|
+
code=ARG_CODE_INVALID_VALUE,
|
|
1002
|
+
arguments=["--lines"],
|
|
1003
|
+
) from error
|
|
1004
|
+
if not 1 <= parsed <= RUN_TAIL_MAX_LINES:
|
|
1005
|
+
raise CdxArgumentError(
|
|
1006
|
+
f"cdx run-tail: --lines must be between 1 and {RUN_TAIL_MAX_LINES}; got {parsed}.",
|
|
1007
|
+
code=ARG_CODE_OUT_OF_RANGE,
|
|
1008
|
+
arguments=["--lines"],
|
|
1009
|
+
)
|
|
1010
|
+
return parsed
|