cdx-manager 0.6.4 → 0.7.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 +48 -8
- package/changelogs/CHANGELOGS_0_6_5.md +35 -0
- package/changelogs/CHANGELOGS_0_7_0.md +49 -0
- package/checksums/release-archives.json +8 -0
- package/install.sh +2 -2
- package/package.json +1 -1
- package/pyproject.toml +2 -2
- package/src/backup_bundle.py +40 -11
- package/src/claude_refresh.py +1 -0
- package/src/cli.py +15 -4
- package/src/cli_commands.py +453 -29
- package/src/provider_runtime.py +174 -10
- package/src/session_service.py +19 -1
package/src/cli_commands.py
CHANGED
|
@@ -33,7 +33,9 @@ from .notify import (
|
|
|
33
33
|
from .provider_runtime import (
|
|
34
34
|
_ensure_session_authentication,
|
|
35
35
|
_list_launch_transcript_paths,
|
|
36
|
+
_normalize_reasoning_effort,
|
|
36
37
|
_probe_provider_auth,
|
|
38
|
+
_run_headless_provider_command,
|
|
37
39
|
_run_interactive_provider_command,
|
|
38
40
|
)
|
|
39
41
|
from .repair import format_repair_report, repair_health
|
|
@@ -51,12 +53,14 @@ EXPORT_USAGE = "Usage: cdx export <file> [--include-auth] [--force] [--json] [--
|
|
|
51
53
|
IMPORT_USAGE = "Usage: cdx import <file> [--force] [--json] [--sessions name1,name2] [--passphrase-env VAR]"
|
|
52
54
|
CONTEXT_USAGE = "Usage: cdx context show|path|init|edit|clear|set [text...] [--json]"
|
|
53
55
|
HANDOFF_USAGE = "Usage: cdx handoff <name> [--json] | cdx handoff <source> <target> [--json]"
|
|
54
|
-
SET_USAGE = "Usage: cdx set <name>|--sessions all|a,b|--provider PROVIDER [--power low|medium|high|xhigh|max] [--permission review|default|auto|full] [--fast on|off] [--model MODEL] [--json]"
|
|
55
|
-
UNSET_USAGE = "Usage: cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--permission|--fast|--model|--all) [--json]"
|
|
56
|
+
SET_USAGE = "Usage: cdx set <name>|--sessions all|a,b|--provider PROVIDER [--power low|medium|high|xhigh|max] [--permission review|default|auto|full] [--fast on|off] [--model MODEL] [--priority 0..100] [--json]"
|
|
57
|
+
UNSET_USAGE = "Usage: cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--permission|--fast|--model|--priority|--all) [--json]"
|
|
56
58
|
SETTING_ALIAS_USAGE = "Usage: cdx power|perm|fast|model <name|all|provider:PROVIDER|a,b> <value|default> [--json]"
|
|
57
59
|
CONFIG_USAGE = "Usage: cdx config <name> [--json]"
|
|
58
60
|
HISTORY_USAGE = "Usage: cdx history [name] [--limit N] [--summary] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]"
|
|
59
61
|
LAST_USAGE = "Usage: cdx last [--json]"
|
|
62
|
+
SELECT_USAGE = "Usage: cdx select --provider PROVIDER [--min-reasoning-effort low|medium|high] [--min-power low|medium|high] [--require-ready] [--refresh] --json"
|
|
63
|
+
RUN_USAGE = "Usage: cdx run [session] --cwd PATH (--prompt-file PATH|--prompt TEXT) [--provider PROVIDER] [--model MODEL] [--reasoning-effort low|medium|high] [--power low|medium|high] [--permission review|default|auto|full|workspace-write|read-only|danger-full-access] [--timeout-seconds N] --json"
|
|
60
64
|
API_SCHEMA_VERSION = 1
|
|
61
65
|
HANDOFF_TRANSCRIPT_CHARS = 120000
|
|
62
66
|
HANDOFF_NATIVE_TRANSCRIPT_CANDIDATES = 64
|
|
@@ -78,6 +82,23 @@ def _json_success(action, message, warnings=None, **extra):
|
|
|
78
82
|
return payload
|
|
79
83
|
|
|
80
84
|
|
|
85
|
+
def _json_failure(action, code, message, source="cdx", exit_code=1, warnings=None, **extra):
|
|
86
|
+
payload = {
|
|
87
|
+
"schema_version": API_SCHEMA_VERSION,
|
|
88
|
+
"ok": False,
|
|
89
|
+
"action": action,
|
|
90
|
+
"warnings": warnings or [],
|
|
91
|
+
"error": {
|
|
92
|
+
"source": source,
|
|
93
|
+
"code": code,
|
|
94
|
+
"message": message,
|
|
95
|
+
"exit_code": exit_code,
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
payload.update(extra)
|
|
99
|
+
return payload
|
|
100
|
+
|
|
101
|
+
|
|
81
102
|
def _write_json(ctx, payload):
|
|
82
103
|
ctx["out"](f"{json.dumps(payload, indent=2)}\n")
|
|
83
104
|
|
|
@@ -467,12 +488,23 @@ def _parse_fast_value(value):
|
|
|
467
488
|
raise CdxError(SET_USAGE)
|
|
468
489
|
|
|
469
490
|
|
|
491
|
+
def _parse_priority_value(value):
|
|
492
|
+
try:
|
|
493
|
+
priority = int(value)
|
|
494
|
+
except (TypeError, ValueError) as error:
|
|
495
|
+
raise CdxError(SET_USAGE) from error
|
|
496
|
+
if priority < 0 or priority > 100:
|
|
497
|
+
raise CdxError(SET_USAGE)
|
|
498
|
+
return priority
|
|
499
|
+
|
|
500
|
+
|
|
470
501
|
def _parse_set_args(args):
|
|
471
502
|
parsed = _parse_flag_args(args, {
|
|
472
503
|
"--power": {"key": "power", "type": "str", "default": None},
|
|
473
504
|
"--permission": {"key": "permission", "type": "str", "default": None},
|
|
474
505
|
"--fast": {"key": "fast", "type": "str", "default": None, "transform": _parse_fast_value},
|
|
475
506
|
"--model": {"key": "model", "type": "str", "default": None},
|
|
507
|
+
"--priority": {"key": "priority", "type": "str", "default": None, "transform": _parse_priority_value},
|
|
476
508
|
"--sessions": {"key": "sessions", "type": "str", "default": None, "transform": _parse_set_unset_sessions},
|
|
477
509
|
"--provider": {"key": "provider", "type": "str", "default": None, "transform": lambda value: _parse_provider_filter(value, SET_USAGE)},
|
|
478
510
|
"--json": {"key": "json", "type": "bool", "default": False},
|
|
@@ -487,7 +519,7 @@ def _parse_set_args(args):
|
|
|
487
519
|
raise CdxError(SET_USAGE)
|
|
488
520
|
settings = {
|
|
489
521
|
key: parsed[key]
|
|
490
|
-
for key in ("power", "permission", "fast", "model")
|
|
522
|
+
for key in ("power", "permission", "fast", "model", "priority")
|
|
491
523
|
if parsed[key] is not None
|
|
492
524
|
}
|
|
493
525
|
if not settings:
|
|
@@ -507,6 +539,7 @@ def _parse_unset_args(args):
|
|
|
507
539
|
"--permission": {"key": "permission", "type": "bool", "default": False},
|
|
508
540
|
"--fast": {"key": "fast", "type": "bool", "default": False},
|
|
509
541
|
"--model": {"key": "model", "type": "bool", "default": False},
|
|
542
|
+
"--priority": {"key": "priority", "type": "bool", "default": False},
|
|
510
543
|
"--all": {"key": "all", "type": "bool", "default": False},
|
|
511
544
|
"--sessions": {"key": "sessions", "type": "str", "default": None, "transform": _parse_set_unset_sessions},
|
|
512
545
|
"--provider": {"key": "provider", "type": "str", "default": None, "transform": lambda value: _parse_provider_filter(value, UNSET_USAGE)},
|
|
@@ -520,8 +553,8 @@ def _parse_unset_args(args):
|
|
|
520
553
|
raise CdxError(UNSET_USAGE)
|
|
521
554
|
if not parsed["names"] and not parsed["sessions"] and not parsed["provider"]:
|
|
522
555
|
raise CdxError(UNSET_USAGE)
|
|
523
|
-
keys = ["power", "permission", "fast", "model"] if parsed["all"] else [
|
|
524
|
-
key for key in ("power", "permission", "fast", "model") if parsed[key]
|
|
556
|
+
keys = ["power", "permission", "fast", "model", "priority"] if parsed["all"] else [
|
|
557
|
+
key for key in ("power", "permission", "fast", "model", "priority") if parsed[key]
|
|
525
558
|
]
|
|
526
559
|
if not keys:
|
|
527
560
|
raise CdxError(UNSET_USAGE)
|
|
@@ -543,6 +576,99 @@ def _parse_config_args(args):
|
|
|
543
576
|
return {"name": parsed["names"][0], "json": parsed["json"]}
|
|
544
577
|
|
|
545
578
|
|
|
579
|
+
def _parse_select_args(args):
|
|
580
|
+
parsed = _parse_flag_args(args, {
|
|
581
|
+
"--provider": {"key": "provider", "type": "str", "default": None, "transform": lambda value: _parse_provider_filter(value, SELECT_USAGE)},
|
|
582
|
+
"--min-reasoning-effort": {"key": "min_reasoning_effort", "type": "str", "default": None},
|
|
583
|
+
"--min-power": {"key": "min_power", "type": "str", "default": None},
|
|
584
|
+
"--require-ready": {"key": "require_ready", "type": "bool", "default": False},
|
|
585
|
+
"--refresh": {"key": "refresh", "type": "bool", "default": False},
|
|
586
|
+
"--json": {"key": "json", "type": "bool", "default": False},
|
|
587
|
+
}, SELECT_USAGE)
|
|
588
|
+
if not parsed["provider"] or not parsed["json"]:
|
|
589
|
+
raise CdxError(SELECT_USAGE)
|
|
590
|
+
effort = _normalize_reasoning_effort(
|
|
591
|
+
reasoning_effort=parsed["min_reasoning_effort"],
|
|
592
|
+
power=parsed["min_power"],
|
|
593
|
+
usage=SELECT_USAGE,
|
|
594
|
+
).get("reasoning_effort")
|
|
595
|
+
return {
|
|
596
|
+
"provider": parsed["provider"],
|
|
597
|
+
"min_reasoning_effort": effort,
|
|
598
|
+
"require_ready": parsed["require_ready"],
|
|
599
|
+
"refresh": parsed["refresh"],
|
|
600
|
+
"json": parsed["json"],
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def _parse_timeout_seconds(value):
|
|
605
|
+
try:
|
|
606
|
+
parsed = float(value)
|
|
607
|
+
except (TypeError, ValueError) as error:
|
|
608
|
+
raise CdxError(RUN_USAGE) from error
|
|
609
|
+
if parsed <= 0:
|
|
610
|
+
raise CdxError(RUN_USAGE)
|
|
611
|
+
return parsed
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def _normalize_run_permission(value):
|
|
615
|
+
if value is None:
|
|
616
|
+
return None
|
|
617
|
+
text = str(value).strip().lower()
|
|
618
|
+
aliases = {
|
|
619
|
+
"workspace-write": "default",
|
|
620
|
+
"read-only": "review",
|
|
621
|
+
"danger-full-access": "full",
|
|
622
|
+
}
|
|
623
|
+
text = aliases.get(text, text)
|
|
624
|
+
if text not in ("review", "default", "auto", "full"):
|
|
625
|
+
raise CdxError(RUN_USAGE)
|
|
626
|
+
return text
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def _parse_run_args(args):
|
|
630
|
+
parsed = _parse_flag_args(args, {
|
|
631
|
+
"--cwd": {"key": "cwd", "type": "str", "default": None},
|
|
632
|
+
"--prompt-file": {"key": "prompt_file", "type": "str", "default": None},
|
|
633
|
+
"--prompt": {"key": "prompt", "type": "str", "default": None},
|
|
634
|
+
"--provider": {"key": "provider", "type": "str", "default": None, "transform": lambda value: _parse_provider_filter(value, RUN_USAGE)},
|
|
635
|
+
"--model": {"key": "model", "type": "str", "default": None},
|
|
636
|
+
"--reasoning-effort": {"key": "reasoning_effort", "type": "str", "default": None},
|
|
637
|
+
"--power": {"key": "power", "type": "str", "default": None},
|
|
638
|
+
"--permission": {"key": "permission", "type": "str", "default": None, "transform": _normalize_run_permission},
|
|
639
|
+
"--timeout-seconds": {"key": "timeout_seconds", "type": "str", "default": None, "transform": _parse_timeout_seconds},
|
|
640
|
+
"--json": {"key": "json", "type": "bool", "default": False},
|
|
641
|
+
}, RUN_USAGE, positionals_key="names", max_positionals=1)
|
|
642
|
+
if not parsed["json"]:
|
|
643
|
+
raise CdxError(RUN_USAGE)
|
|
644
|
+
name = parsed["names"][0] if parsed["names"] else None
|
|
645
|
+
if name and parsed["provider"]:
|
|
646
|
+
raise CdxError(RUN_USAGE)
|
|
647
|
+
if not name and not parsed["provider"]:
|
|
648
|
+
raise CdxError(RUN_USAGE)
|
|
649
|
+
if not parsed["cwd"]:
|
|
650
|
+
raise CdxError(RUN_USAGE)
|
|
651
|
+
if bool(parsed["prompt_file"]) == bool(parsed["prompt"]):
|
|
652
|
+
raise CdxError(RUN_USAGE)
|
|
653
|
+
effort = _normalize_reasoning_effort(
|
|
654
|
+
reasoning_effort=parsed["reasoning_effort"],
|
|
655
|
+
power=parsed["power"],
|
|
656
|
+
usage=RUN_USAGE,
|
|
657
|
+
)
|
|
658
|
+
return {
|
|
659
|
+
"name": name,
|
|
660
|
+
"provider": parsed["provider"],
|
|
661
|
+
"cwd": parsed["cwd"],
|
|
662
|
+
"prompt_file": parsed["prompt_file"],
|
|
663
|
+
"prompt": parsed["prompt"],
|
|
664
|
+
"model": parsed["model"],
|
|
665
|
+
"permission": parsed["permission"],
|
|
666
|
+
"timeout_seconds": parsed["timeout_seconds"],
|
|
667
|
+
"reasoning_effort": effort.get("reasoning_effort"),
|
|
668
|
+
"power": effort.get("power"),
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
|
|
546
672
|
def _parse_positive_int(value):
|
|
547
673
|
try:
|
|
548
674
|
parsed = int(value)
|
|
@@ -892,19 +1018,18 @@ def _bootstrap_claude_setup_token(session, ctx):
|
|
|
892
1018
|
"Claude setup-token completed, but cdx could not capture the token. "
|
|
893
1019
|
"Run claude setup-token and save the token under credentials/default.json."
|
|
894
1020
|
)
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
transcript = handle.read()
|
|
898
|
-
finally:
|
|
899
|
-
try:
|
|
900
|
-
os.remove(transcript_path)
|
|
901
|
-
except OSError:
|
|
902
|
-
pass
|
|
1021
|
+
with open(transcript_path, "r", encoding="utf-8", errors="replace") as handle:
|
|
1022
|
+
transcript = handle.read()
|
|
903
1023
|
token = _extract_claude_oauth_token(transcript)
|
|
904
1024
|
if not token:
|
|
905
1025
|
raise CdxError(
|
|
906
|
-
"Claude setup-token completed, but cdx could not find CLAUDE_CODE_OAUTH_TOKEN in the output."
|
|
1026
|
+
"Claude setup-token completed, but cdx could not find CLAUDE_CODE_OAUTH_TOKEN in the output. "
|
|
1027
|
+
f"Transcript kept at: {transcript_path}"
|
|
907
1028
|
)
|
|
1029
|
+
try:
|
|
1030
|
+
os.remove(transcript_path)
|
|
1031
|
+
except OSError:
|
|
1032
|
+
pass
|
|
908
1033
|
return _write_claude_oauth_token(session.get("authHome") or "", token)
|
|
909
1034
|
|
|
910
1035
|
|
|
@@ -1026,6 +1151,223 @@ def handle_enable(rest, ctx):
|
|
|
1026
1151
|
return 0
|
|
1027
1152
|
|
|
1028
1153
|
|
|
1154
|
+
def handle_select(rest, ctx):
|
|
1155
|
+
parsed = _parse_select_args(rest)
|
|
1156
|
+
selected = _select_headless_session(
|
|
1157
|
+
ctx,
|
|
1158
|
+
parsed["provider"],
|
|
1159
|
+
min_reasoning_effort=parsed["min_reasoning_effort"],
|
|
1160
|
+
require_ready=parsed["require_ready"],
|
|
1161
|
+
force_refresh=parsed["refresh"],
|
|
1162
|
+
)
|
|
1163
|
+
policy = "ready_then_cooldown_then_health_then_priority_then_name"
|
|
1164
|
+
if not selected:
|
|
1165
|
+
message = "No suitable session found."
|
|
1166
|
+
_write_json(ctx, _json_failure(
|
|
1167
|
+
"select",
|
|
1168
|
+
"no_suitable_session",
|
|
1169
|
+
message,
|
|
1170
|
+
provider=parsed["provider"],
|
|
1171
|
+
min_reasoning_effort=parsed["min_reasoning_effort"],
|
|
1172
|
+
require_ready=parsed["require_ready"],
|
|
1173
|
+
selection_policy=policy,
|
|
1174
|
+
))
|
|
1175
|
+
return 1
|
|
1176
|
+
session = selected["session"]
|
|
1177
|
+
row = selected.get("row") or {}
|
|
1178
|
+
reason = "highest availability suitable session"
|
|
1179
|
+
_write_json(ctx, _json_success(
|
|
1180
|
+
"select",
|
|
1181
|
+
f"Selected session {session['name']}",
|
|
1182
|
+
session=session["name"],
|
|
1183
|
+
provider=session["provider"],
|
|
1184
|
+
reason=reason,
|
|
1185
|
+
selection_policy=policy,
|
|
1186
|
+
min_reasoning_effort=parsed["min_reasoning_effort"],
|
|
1187
|
+
reasoning_effort=_session_reasoning_effort(session),
|
|
1188
|
+
available_pct=row.get("available_pct"),
|
|
1189
|
+
auth_status=row.get("auth_status"),
|
|
1190
|
+
))
|
|
1191
|
+
return 0
|
|
1192
|
+
|
|
1193
|
+
|
|
1194
|
+
def _read_run_prompt(parsed):
|
|
1195
|
+
if parsed.get("prompt") is not None:
|
|
1196
|
+
return parsed["prompt"]
|
|
1197
|
+
try:
|
|
1198
|
+
with open(parsed["prompt_file"], "r", encoding="utf-8") as handle:
|
|
1199
|
+
return handle.read()
|
|
1200
|
+
except OSError as error:
|
|
1201
|
+
raise CdxError(f"Unable to read prompt file: {parsed['prompt_file']}") from error
|
|
1202
|
+
|
|
1203
|
+
|
|
1204
|
+
def _run_usage_payload():
|
|
1205
|
+
return {
|
|
1206
|
+
"input_tokens": None,
|
|
1207
|
+
"output_tokens": None,
|
|
1208
|
+
"reasoning_tokens": None,
|
|
1209
|
+
"total_tokens": None,
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
|
|
1213
|
+
def _run_cdx_error_code(error):
|
|
1214
|
+
message = str(error)
|
|
1215
|
+
if message.startswith("Usage:"):
|
|
1216
|
+
return "invalid_request"
|
|
1217
|
+
if message.startswith("Invalid cwd:"):
|
|
1218
|
+
return "invalid_cwd"
|
|
1219
|
+
if message.startswith("Session is disabled:"):
|
|
1220
|
+
return "session_disabled"
|
|
1221
|
+
if "CLI not found on PATH" in message:
|
|
1222
|
+
return "provider_cli_not_found"
|
|
1223
|
+
if message.startswith("Failed to start "):
|
|
1224
|
+
return "provider_start_failed"
|
|
1225
|
+
if (
|
|
1226
|
+
message.startswith("Unsupported reasoning effort:")
|
|
1227
|
+
or message.startswith("Unsupported power:")
|
|
1228
|
+
or "--reasoning-effort and --power must match" in message
|
|
1229
|
+
):
|
|
1230
|
+
return "invalid_reasoning_effort"
|
|
1231
|
+
return "cdx_error"
|
|
1232
|
+
|
|
1233
|
+
|
|
1234
|
+
def _run_result_payload(ok, parsed, session, run_info=None, error=None, error_source=None, error_code=None):
|
|
1235
|
+
run_info = run_info or {}
|
|
1236
|
+
return {
|
|
1237
|
+
"schema_version": API_SCHEMA_VERSION,
|
|
1238
|
+
"ok": bool(ok),
|
|
1239
|
+
"action": "run",
|
|
1240
|
+
"session": session.get("name") if session else None,
|
|
1241
|
+
"provider": session.get("provider") if session else parsed.get("provider"),
|
|
1242
|
+
"model": parsed.get("model") or ((session.get("launch") or {}).get("model") if session else None),
|
|
1243
|
+
"reasoning_effort": parsed.get("reasoning_effort") or ((session.get("launch") or {}).get("reasoning_effort") if session else None),
|
|
1244
|
+
"power": parsed.get("power") or ((session.get("launch") or {}).get("power") if session else None),
|
|
1245
|
+
"cwd": os.path.abspath(parsed.get("cwd") or os.getcwd()),
|
|
1246
|
+
"run_id": run_info.get("run_id"),
|
|
1247
|
+
"exit_code": run_info.get("returncode"),
|
|
1248
|
+
"duration_seconds": (run_info.get("duration_ms") / 1000.0) if run_info.get("duration_ms") is not None else None,
|
|
1249
|
+
"transcript_path": run_info.get("transcript_path"),
|
|
1250
|
+
"stdout_path": run_info.get("stdout_path"),
|
|
1251
|
+
"stderr_path": run_info.get("stderr_path"),
|
|
1252
|
+
"usage": _run_usage_payload(),
|
|
1253
|
+
"warnings": [],
|
|
1254
|
+
"error": None if ok else {
|
|
1255
|
+
"source": error_source or "cdx",
|
|
1256
|
+
"code": error_code or "cdx_error",
|
|
1257
|
+
"message": str(error) if error else "Run failed.",
|
|
1258
|
+
"provider_code": run_info.get("returncode") if error_source == "provider" else None,
|
|
1259
|
+
},
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
|
|
1263
|
+
def handle_run(rest, ctx):
|
|
1264
|
+
try:
|
|
1265
|
+
parsed = _parse_run_args(rest)
|
|
1266
|
+
session = None
|
|
1267
|
+
if parsed["name"]:
|
|
1268
|
+
session = ctx["service"]["get_session"](parsed["name"])
|
|
1269
|
+
if not session:
|
|
1270
|
+
raise CdxError(f"Unknown session: {parsed['name']}")
|
|
1271
|
+
if session.get("enabled", True) is False:
|
|
1272
|
+
raise CdxError(f"Session is disabled: {parsed['name']}")
|
|
1273
|
+
else:
|
|
1274
|
+
selected = _select_headless_session(
|
|
1275
|
+
ctx,
|
|
1276
|
+
parsed["provider"],
|
|
1277
|
+
min_reasoning_effort=parsed["reasoning_effort"],
|
|
1278
|
+
require_ready=True,
|
|
1279
|
+
force_refresh=False,
|
|
1280
|
+
)
|
|
1281
|
+
if not selected:
|
|
1282
|
+
_write_json(ctx, _json_failure(
|
|
1283
|
+
"run",
|
|
1284
|
+
"no_suitable_session",
|
|
1285
|
+
"No suitable session found.",
|
|
1286
|
+
provider=parsed["provider"],
|
|
1287
|
+
))
|
|
1288
|
+
return 1
|
|
1289
|
+
session = selected["session"]
|
|
1290
|
+
|
|
1291
|
+
cwd = os.path.abspath(parsed["cwd"])
|
|
1292
|
+
if not os.path.isdir(cwd):
|
|
1293
|
+
raise CdxError(f"Invalid cwd: {parsed['cwd']}")
|
|
1294
|
+
prompt = _read_run_prompt(parsed)
|
|
1295
|
+
launch_updates = {}
|
|
1296
|
+
if parsed.get("model"):
|
|
1297
|
+
launch_updates["model"] = parsed["model"]
|
|
1298
|
+
if parsed.get("permission"):
|
|
1299
|
+
launch_updates["permission"] = parsed["permission"]
|
|
1300
|
+
if parsed.get("reasoning_effort"):
|
|
1301
|
+
launch_updates["reasoning_effort"] = parsed["reasoning_effort"]
|
|
1302
|
+
launch_updates["power"] = parsed["reasoning_effort"]
|
|
1303
|
+
run_session = {
|
|
1304
|
+
**session,
|
|
1305
|
+
"launch": {
|
|
1306
|
+
**(session.get("launch") or {}),
|
|
1307
|
+
**launch_updates,
|
|
1308
|
+
},
|
|
1309
|
+
}
|
|
1310
|
+
_ensure_session_authentication(
|
|
1311
|
+
run_session,
|
|
1312
|
+
ctx["service"],
|
|
1313
|
+
spawn=ctx.get("spawn"),
|
|
1314
|
+
spawn_sync=ctx.get("spawn_sync"),
|
|
1315
|
+
env_override=ctx.get("env"),
|
|
1316
|
+
stdin_is_tty=False,
|
|
1317
|
+
behavior="launch",
|
|
1318
|
+
signal_emitter=ctx.get("signal_emitter"),
|
|
1319
|
+
)
|
|
1320
|
+
run_info = _run_headless_provider_command(
|
|
1321
|
+
run_session,
|
|
1322
|
+
cwd=cwd,
|
|
1323
|
+
env_override=ctx.get("env"),
|
|
1324
|
+
initial_prompt=prompt,
|
|
1325
|
+
timeout_seconds=parsed.get("timeout_seconds"),
|
|
1326
|
+
spawn=ctx.get("spawn_headless") or ctx.get("spawn"),
|
|
1327
|
+
)
|
|
1328
|
+
ok = run_info.get("returncode") == 0
|
|
1329
|
+
if ok:
|
|
1330
|
+
ctx["service"]["record_launch_history"](session["name"], {
|
|
1331
|
+
"status": "success",
|
|
1332
|
+
"cwd": cwd,
|
|
1333
|
+
"exit_code": 0,
|
|
1334
|
+
**run_info,
|
|
1335
|
+
})
|
|
1336
|
+
_write_json(ctx, _run_result_payload(True, parsed, run_session, run_info=run_info))
|
|
1337
|
+
return 0
|
|
1338
|
+
message = "Provider process timed out." if run_info.get("timed_out") else "Provider process exited with a non-zero status."
|
|
1339
|
+
error = CdxError(message, run_info.get("returncode") or 1)
|
|
1340
|
+
ctx["service"]["record_launch_history"](session["name"], {
|
|
1341
|
+
"status": "failed",
|
|
1342
|
+
"cwd": cwd,
|
|
1343
|
+
"error": str(error),
|
|
1344
|
+
"exit_code": error.exit_code,
|
|
1345
|
+
**run_info,
|
|
1346
|
+
})
|
|
1347
|
+
_write_json(ctx, _run_result_payload(
|
|
1348
|
+
False,
|
|
1349
|
+
parsed,
|
|
1350
|
+
run_session,
|
|
1351
|
+
run_info=run_info,
|
|
1352
|
+
error=error,
|
|
1353
|
+
error_source="provider",
|
|
1354
|
+
error_code="provider_timeout" if run_info.get("timed_out") else "provider_failed",
|
|
1355
|
+
))
|
|
1356
|
+
return error.exit_code or 1
|
|
1357
|
+
except CdxError as error:
|
|
1358
|
+
run_info = getattr(error, "run_info", None)
|
|
1359
|
+
_write_json(ctx, _run_result_payload(
|
|
1360
|
+
False,
|
|
1361
|
+
locals().get("parsed", {}) or {},
|
|
1362
|
+
locals().get("session"),
|
|
1363
|
+
run_info=run_info,
|
|
1364
|
+
error=error,
|
|
1365
|
+
error_source="cdx",
|
|
1366
|
+
error_code=_run_cdx_error_code(error),
|
|
1367
|
+
))
|
|
1368
|
+
return error.exit_code
|
|
1369
|
+
|
|
1370
|
+
|
|
1029
1371
|
def _format_launch_config(session):
|
|
1030
1372
|
launch = session.get("launch") or {}
|
|
1031
1373
|
return "\n".join([
|
|
@@ -1034,6 +1376,7 @@ def _format_launch_config(session):
|
|
|
1034
1376
|
f"permission: {launch.get('permission') or 'default'}",
|
|
1035
1377
|
f"fast: {'on' if launch.get('fast') is True else 'off' if launch.get('fast') is False else 'default'}",
|
|
1036
1378
|
f"model: {launch.get('model') or 'default'}",
|
|
1379
|
+
f"priority: {launch.get('priority') if launch.get('priority') is not None else 'default'}",
|
|
1037
1380
|
])
|
|
1038
1381
|
|
|
1039
1382
|
|
|
@@ -1063,6 +1406,87 @@ def _resolve_bulk_launch_targets(parsed, service):
|
|
|
1063
1406
|
return targets
|
|
1064
1407
|
|
|
1065
1408
|
|
|
1409
|
+
def _reasoning_rank(value):
|
|
1410
|
+
order = {"low": 0, "medium": 1, "high": 2, "xhigh": 2, "max": 2}
|
|
1411
|
+
return order.get(str(value or "low").lower(), 0)
|
|
1412
|
+
|
|
1413
|
+
|
|
1414
|
+
def _session_reasoning_effort(session):
|
|
1415
|
+
launch = session.get("launch") or {}
|
|
1416
|
+
return (
|
|
1417
|
+
launch.get("reasoning_effort")
|
|
1418
|
+
or launch.get("reasoningEffort")
|
|
1419
|
+
or launch.get("power")
|
|
1420
|
+
or ("low" if launch.get("fast") is True else None)
|
|
1421
|
+
or "low"
|
|
1422
|
+
)
|
|
1423
|
+
|
|
1424
|
+
|
|
1425
|
+
def _session_selection_priority(session):
|
|
1426
|
+
launch = session.get("launch") or {}
|
|
1427
|
+
try:
|
|
1428
|
+
return int(launch.get("priority") or 0)
|
|
1429
|
+
except (TypeError, ValueError):
|
|
1430
|
+
return 0
|
|
1431
|
+
|
|
1432
|
+
|
|
1433
|
+
def _row_blocks_ready(row):
|
|
1434
|
+
if row.get("provider") in (PROVIDER_ANTIGRAVITY, PROVIDER_OLLAMA):
|
|
1435
|
+
auth = "n/a"
|
|
1436
|
+
else:
|
|
1437
|
+
auth = row.get("auth_status") or "unknown"
|
|
1438
|
+
if auth not in ("authenticated", "n/a"):
|
|
1439
|
+
return True
|
|
1440
|
+
available = row.get("available_pct")
|
|
1441
|
+
if available is not None:
|
|
1442
|
+
try:
|
|
1443
|
+
if float(available) <= 0:
|
|
1444
|
+
return True
|
|
1445
|
+
except (TypeError, ValueError):
|
|
1446
|
+
return True
|
|
1447
|
+
return False
|
|
1448
|
+
|
|
1449
|
+
|
|
1450
|
+
def _select_headless_session(ctx, provider, min_reasoning_effort=None, require_ready=False, force_refresh=False):
|
|
1451
|
+
sessions = [
|
|
1452
|
+
session for session in ctx["service"]["list_sessions"]()
|
|
1453
|
+
if session.get("provider") == provider and session.get("enabled", True) is not False
|
|
1454
|
+
]
|
|
1455
|
+
minimum = _reasoning_rank(min_reasoning_effort)
|
|
1456
|
+
sessions = [
|
|
1457
|
+
session for session in sessions
|
|
1458
|
+
if _reasoning_rank(_session_reasoning_effort(session)) >= minimum
|
|
1459
|
+
]
|
|
1460
|
+
rows = ctx["service"]["get_status_rows"](force_refresh=force_refresh)
|
|
1461
|
+
row_by_name = {row.get("session_name"): row for row in rows}
|
|
1462
|
+
candidates = []
|
|
1463
|
+
for session in sessions:
|
|
1464
|
+
row = row_by_name.get(session["name"], {})
|
|
1465
|
+
if require_ready and _row_blocks_ready(row):
|
|
1466
|
+
continue
|
|
1467
|
+
available = row.get("available_pct")
|
|
1468
|
+
try:
|
|
1469
|
+
available_sort = float(available) if available is not None else -1.0
|
|
1470
|
+
except (TypeError, ValueError):
|
|
1471
|
+
available_sort = -1.0
|
|
1472
|
+
cooldown_sort = 1 if available_sort > 0 else 0
|
|
1473
|
+
candidates.append({
|
|
1474
|
+
"session": session,
|
|
1475
|
+
"row": row,
|
|
1476
|
+
"sort_key": (
|
|
1477
|
+
-cooldown_sort,
|
|
1478
|
+
-available_sort,
|
|
1479
|
+
-_session_selection_priority(session),
|
|
1480
|
+
_reasoning_rank(_session_reasoning_effort(session)),
|
|
1481
|
+
session["name"],
|
|
1482
|
+
),
|
|
1483
|
+
})
|
|
1484
|
+
if not candidates:
|
|
1485
|
+
return None
|
|
1486
|
+
candidates.sort(key=lambda item: item["sort_key"])
|
|
1487
|
+
return candidates[0]
|
|
1488
|
+
|
|
1489
|
+
|
|
1066
1490
|
def _format_bulk_launch_summary(sessions):
|
|
1067
1491
|
names = [session["name"] for session in sessions]
|
|
1068
1492
|
if len(names) <= 8:
|
|
@@ -1599,6 +2023,20 @@ def handle_status(rest, ctx):
|
|
|
1599
2023
|
if len(args) == 1 and parsed["small"]:
|
|
1600
2024
|
raise CdxError(STATUS_USAGE)
|
|
1601
2025
|
|
|
2026
|
+
auth_refresh = _refresh_claude_auth_states(
|
|
2027
|
+
ctx["service"],
|
|
2028
|
+
target_names=args if len(args) == 1 else None,
|
|
2029
|
+
spawn_sync=ctx.get("spawn_sync"),
|
|
2030
|
+
env_override=ctx.get("env"),
|
|
2031
|
+
)
|
|
2032
|
+
warnings = [
|
|
2033
|
+
{
|
|
2034
|
+
"code": "claude_auth_probe_failed",
|
|
2035
|
+
"session": item.get("session") or "unknown",
|
|
2036
|
+
"message": item.get("error") or "unknown error",
|
|
2037
|
+
}
|
|
2038
|
+
for item in auth_refresh.get("errors", [])
|
|
2039
|
+
]
|
|
1602
2040
|
refresh_result = _refresh_claude_sessions(
|
|
1603
2041
|
ctx["service"],
|
|
1604
2042
|
ctx.get("refresh_fn"),
|
|
@@ -1612,27 +2050,13 @@ def handle_status(rest, ctx):
|
|
|
1612
2050
|
}
|
|
1613
2051
|
for item in refresh_result.get("errors", [])
|
|
1614
2052
|
]
|
|
1615
|
-
warnings
|
|
2053
|
+
warnings.extend([
|
|
1616
2054
|
{
|
|
1617
2055
|
"code": "claude_refresh_failed",
|
|
1618
2056
|
"session": item.get("session") or "unknown",
|
|
1619
2057
|
"message": item.get("error") or "unknown error",
|
|
1620
2058
|
}
|
|
1621
2059
|
for item in refresh_errors
|
|
1622
|
-
]
|
|
1623
|
-
auth_refresh = _refresh_claude_auth_states(
|
|
1624
|
-
ctx["service"],
|
|
1625
|
-
target_names=args if len(args) == 1 else None,
|
|
1626
|
-
spawn_sync=ctx.get("spawn_sync"),
|
|
1627
|
-
env_override=ctx.get("env"),
|
|
1628
|
-
)
|
|
1629
|
-
warnings.extend([
|
|
1630
|
-
{
|
|
1631
|
-
"code": "claude_auth_probe_failed",
|
|
1632
|
-
"session": item.get("session") or "unknown",
|
|
1633
|
-
"message": item.get("error") or "unknown error",
|
|
1634
|
-
}
|
|
1635
|
-
for item in auth_refresh.get("errors", [])
|
|
1636
2060
|
])
|
|
1637
2061
|
update_warning = _update_notice_warning(ctx)
|
|
1638
2062
|
if update_warning:
|