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/provider_runtime.py
CHANGED
|
@@ -6,6 +6,7 @@ import shlex
|
|
|
6
6
|
import shutil
|
|
7
7
|
import subprocess
|
|
8
8
|
import sys
|
|
9
|
+
import uuid
|
|
9
10
|
from datetime import datetime, timezone
|
|
10
11
|
|
|
11
12
|
from .config import PROVIDER_ANTIGRAVITY, PROVIDER_CLAUDE, PROVIDER_CODEX, PROVIDER_OLLAMA
|
|
@@ -13,6 +14,7 @@ from .errors import CdxError
|
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
LOG_ROTATE_BYTES = 10 * 1024 * 1024 # 10 MB
|
|
17
|
+
REASONING_EFFORT_VALUES = {"low", "medium", "high"}
|
|
16
18
|
LAUNCH_PERMISSION_ARGS = {
|
|
17
19
|
PROVIDER_CLAUDE: {
|
|
18
20
|
"review": ["--permission-mode", "plan"],
|
|
@@ -96,6 +98,23 @@ def _has_local_claude_auth(auth_home):
|
|
|
96
98
|
return bool(isinstance(oauth, dict) and _clean_oauth_token(oauth.get("accessToken")))
|
|
97
99
|
|
|
98
100
|
|
|
101
|
+
def _has_local_codex_auth(auth_home):
|
|
102
|
+
try:
|
|
103
|
+
with open(os.path.join(auth_home, "auth.json"), "r", encoding="utf-8") as handle:
|
|
104
|
+
auth = json.load(handle)
|
|
105
|
+
except (FileNotFoundError, OSError, json.JSONDecodeError):
|
|
106
|
+
return False
|
|
107
|
+
if not isinstance(auth, dict):
|
|
108
|
+
return False
|
|
109
|
+
tokens = auth.get("tokens")
|
|
110
|
+
if not isinstance(tokens, dict):
|
|
111
|
+
return False
|
|
112
|
+
return any(
|
|
113
|
+
_clean_oauth_token(tokens.get(name))
|
|
114
|
+
for name in ("id_token", "access_token", "refresh_token")
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
99
118
|
def _read_claude_account_email(auth_home):
|
|
100
119
|
config_path = os.path.join(auth_home, ".claude.json")
|
|
101
120
|
try:
|
|
@@ -141,7 +160,7 @@ def _build_launch_transcript_path(session):
|
|
|
141
160
|
|
|
142
161
|
def _launch_power(session):
|
|
143
162
|
launch = session.get("launch") or {}
|
|
144
|
-
power = launch.get("power")
|
|
163
|
+
power = launch.get("reasoning_effort") or launch.get("reasoningEffort") or launch.get("power")
|
|
145
164
|
if power:
|
|
146
165
|
return power
|
|
147
166
|
if launch.get("fast") is True:
|
|
@@ -149,6 +168,28 @@ def _launch_power(session):
|
|
|
149
168
|
return None
|
|
150
169
|
|
|
151
170
|
|
|
171
|
+
def _normalize_reasoning_effort(reasoning_effort=None, power=None, usage="Unsupported reasoning effort."):
|
|
172
|
+
effort = str(reasoning_effort).strip().lower() if reasoning_effort is not None else None
|
|
173
|
+
alias = str(power).strip().lower() if power is not None else None
|
|
174
|
+
if effort == "":
|
|
175
|
+
raise CdxError(usage)
|
|
176
|
+
if alias == "":
|
|
177
|
+
raise CdxError(usage)
|
|
178
|
+
if effort and effort not in REASONING_EFFORT_VALUES:
|
|
179
|
+
raise CdxError(f"Unsupported reasoning effort: {reasoning_effort}")
|
|
180
|
+
if alias and alias not in REASONING_EFFORT_VALUES:
|
|
181
|
+
raise CdxError(f"Unsupported power: {power}")
|
|
182
|
+
if effort and alias and effort != alias:
|
|
183
|
+
raise CdxError("--reasoning-effort and --power must match when both are provided.")
|
|
184
|
+
resolved = effort or alias
|
|
185
|
+
if not resolved:
|
|
186
|
+
return {}
|
|
187
|
+
return {
|
|
188
|
+
"reasoning_effort": resolved,
|
|
189
|
+
"power": resolved,
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
|
|
152
193
|
def _launch_config_args(session):
|
|
153
194
|
launch = session.get("launch") or {}
|
|
154
195
|
args = []
|
|
@@ -237,7 +278,7 @@ def _default_script_args(transcript_path, spec):
|
|
|
237
278
|
return ["-q", "-F", transcript_path, spec["command"]] + spec["args"]
|
|
238
279
|
|
|
239
280
|
|
|
240
|
-
def _build_launch_spec(session, cwd=None, env_override=None, initial_prompt=None):
|
|
281
|
+
def _build_launch_spec(session, cwd=None, env_override=None, initial_prompt=None, capture_transcript=True):
|
|
241
282
|
if initial_prompt is not None:
|
|
242
283
|
if not isinstance(initial_prompt, str):
|
|
243
284
|
raise CdxError("initial_prompt must be a string.")
|
|
@@ -263,7 +304,7 @@ def _build_launch_spec(session, cwd=None, env_override=None, initial_prompt=None
|
|
|
263
304
|
"env": claude_env,
|
|
264
305
|
},
|
|
265
306
|
"label": "claude",
|
|
266
|
-
}, env=env)
|
|
307
|
+
}, capture_transcript=capture_transcript, env=env)
|
|
267
308
|
if session["provider"] == PROVIDER_ANTIGRAVITY:
|
|
268
309
|
args = _launch_config_args(session)
|
|
269
310
|
if initial_prompt:
|
|
@@ -276,7 +317,7 @@ def _build_launch_spec(session, cwd=None, env_override=None, initial_prompt=None
|
|
|
276
317
|
"env": {**env, **_antigravity_env_overrides(_get_auth_home(session))},
|
|
277
318
|
},
|
|
278
319
|
"label": "antigravity",
|
|
279
|
-
}, env=env)
|
|
320
|
+
}, capture_transcript=capture_transcript, env=env)
|
|
280
321
|
if session["provider"] == PROVIDER_OLLAMA:
|
|
281
322
|
launch = session.get("launch") or {}
|
|
282
323
|
model = launch.get("model") or session["name"]
|
|
@@ -292,7 +333,7 @@ def _build_launch_spec(session, cwd=None, env_override=None, initial_prompt=None
|
|
|
292
333
|
"env": ollama_env,
|
|
293
334
|
},
|
|
294
335
|
"label": "ollama",
|
|
295
|
-
}, env=env)
|
|
336
|
+
}, capture_transcript=capture_transcript, env=env)
|
|
296
337
|
args = ["--no-alt-screen", "--cd", cwd] + _launch_config_args(session)
|
|
297
338
|
if initial_prompt:
|
|
298
339
|
args.append(initial_prompt)
|
|
@@ -303,7 +344,132 @@ def _build_launch_spec(session, cwd=None, env_override=None, initial_prompt=None
|
|
|
303
344
|
"env": {**env, "CODEX_HOME": _get_auth_home(session)},
|
|
304
345
|
},
|
|
305
346
|
"label": "codex",
|
|
306
|
-
}, env=env)
|
|
347
|
+
}, capture_transcript=capture_transcript, env=env)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _headless_artifact_paths(session, run_id=None):
|
|
351
|
+
run_id = run_id or uuid.uuid4().hex
|
|
352
|
+
log_dir = _get_launch_transcript_dir(session)
|
|
353
|
+
os.makedirs(log_dir, exist_ok=True)
|
|
354
|
+
prefix = os.path.join(log_dir, f"cdx-run-{run_id}")
|
|
355
|
+
return {
|
|
356
|
+
"run_id": run_id,
|
|
357
|
+
"transcript_path": os.path.abspath(f"{prefix}.log"),
|
|
358
|
+
"stdout_path": os.path.abspath(f"{prefix}.stdout.log"),
|
|
359
|
+
"stderr_path": os.path.abspath(f"{prefix}.stderr.log"),
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _combine_headless_transcript(paths):
|
|
364
|
+
transcript_path = paths["transcript_path"]
|
|
365
|
+
with open(transcript_path, "w", encoding="utf-8", errors="replace") as transcript:
|
|
366
|
+
for label, path_key in (("stdout", "stdout_path"), ("stderr", "stderr_path")):
|
|
367
|
+
path = paths.get(path_key)
|
|
368
|
+
if not path:
|
|
369
|
+
continue
|
|
370
|
+
transcript.write(f"--- {label} ---\n")
|
|
371
|
+
try:
|
|
372
|
+
with open(path, "r", encoding="utf-8", errors="replace") as handle:
|
|
373
|
+
content = handle.read()
|
|
374
|
+
except OSError:
|
|
375
|
+
content = ""
|
|
376
|
+
transcript.write(content)
|
|
377
|
+
if content and not content.endswith("\n"):
|
|
378
|
+
transcript.write("\n")
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _headless_run_info(paths, spec, start_time, returncode):
|
|
382
|
+
end_time = datetime.now(timezone.utc)
|
|
383
|
+
return {
|
|
384
|
+
**paths,
|
|
385
|
+
"started_at": start_time.isoformat().replace("+00:00", "Z"),
|
|
386
|
+
"ended_at": end_time.isoformat().replace("+00:00", "Z"),
|
|
387
|
+
"duration_ms": int((end_time - start_time).total_seconds() * 1000),
|
|
388
|
+
"command": spec.get("command"),
|
|
389
|
+
"args": list(spec.get("args") or []),
|
|
390
|
+
"label": spec.get("label"),
|
|
391
|
+
"pid": None,
|
|
392
|
+
"returncode": returncode,
|
|
393
|
+
"timed_out": False,
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _run_headless_provider_command(session, cwd=None, env_override=None, initial_prompt=None,
|
|
398
|
+
timeout_seconds=None, spawn=None, run_id=None):
|
|
399
|
+
spawn = spawn or subprocess.Popen
|
|
400
|
+
spec = _build_launch_spec(
|
|
401
|
+
session,
|
|
402
|
+
cwd=cwd,
|
|
403
|
+
env_override=env_override,
|
|
404
|
+
initial_prompt=initial_prompt,
|
|
405
|
+
capture_transcript=False,
|
|
406
|
+
)
|
|
407
|
+
paths = _headless_artifact_paths(session, run_id=run_id)
|
|
408
|
+
start_time = datetime.now(timezone.utc)
|
|
409
|
+
command = spec["command"]
|
|
410
|
+
if spawn is subprocess.Popen:
|
|
411
|
+
command = _resolve_command(command, spec.get("options", {}).get("env"))
|
|
412
|
+
|
|
413
|
+
child = None
|
|
414
|
+
timed_out = False
|
|
415
|
+
with open(paths["stdout_path"], "w", encoding="utf-8", errors="replace") as stdout_file, \
|
|
416
|
+
open(paths["stderr_path"], "w", encoding="utf-8", errors="replace") as stderr_file:
|
|
417
|
+
try:
|
|
418
|
+
child = spawn(
|
|
419
|
+
[command] + spec["args"],
|
|
420
|
+
stdout=stdout_file,
|
|
421
|
+
stderr=stderr_file,
|
|
422
|
+
**{k: v for k, v in spec.get("options", {}).items() if k not in ("stdio", "stdout", "stderr")},
|
|
423
|
+
)
|
|
424
|
+
except FileNotFoundError as error:
|
|
425
|
+
_combine_headless_transcript(paths)
|
|
426
|
+
cdx_error = CdxError(f"{spec['label']} CLI not found on PATH: {spec['command']}", 127)
|
|
427
|
+
cdx_error.run_info = _headless_run_info(paths, spec, start_time, 127)
|
|
428
|
+
raise cdx_error from error
|
|
429
|
+
except OSError as error:
|
|
430
|
+
_combine_headless_transcript(paths)
|
|
431
|
+
cdx_error = CdxError(f"Failed to start {spec['label']}: {error}", 126)
|
|
432
|
+
cdx_error.run_info = _headless_run_info(paths, spec, start_time, 126)
|
|
433
|
+
raise cdx_error from error
|
|
434
|
+
try:
|
|
435
|
+
if timeout_seconds is None:
|
|
436
|
+
child.wait()
|
|
437
|
+
else:
|
|
438
|
+
child.wait(timeout=timeout_seconds)
|
|
439
|
+
except TypeError:
|
|
440
|
+
child.wait()
|
|
441
|
+
except subprocess.TimeoutExpired:
|
|
442
|
+
timed_out = True
|
|
443
|
+
try:
|
|
444
|
+
child.terminate()
|
|
445
|
+
child.wait(timeout=5)
|
|
446
|
+
except Exception:
|
|
447
|
+
try:
|
|
448
|
+
child.kill()
|
|
449
|
+
except Exception:
|
|
450
|
+
pass
|
|
451
|
+
try:
|
|
452
|
+
child.wait()
|
|
453
|
+
except Exception:
|
|
454
|
+
pass
|
|
455
|
+
|
|
456
|
+
_combine_headless_transcript(paths)
|
|
457
|
+
end_time = datetime.now(timezone.utc)
|
|
458
|
+
returncode = getattr(child, "returncode", None) if child is not None else None
|
|
459
|
+
if timed_out and (returncode is None or returncode == 0):
|
|
460
|
+
returncode = 124
|
|
461
|
+
return {
|
|
462
|
+
**paths,
|
|
463
|
+
"started_at": start_time.isoformat().replace("+00:00", "Z"),
|
|
464
|
+
"ended_at": end_time.isoformat().replace("+00:00", "Z"),
|
|
465
|
+
"duration_ms": int((end_time - start_time).total_seconds() * 1000),
|
|
466
|
+
"command": spec.get("command"),
|
|
467
|
+
"args": list(spec.get("args") or []),
|
|
468
|
+
"label": spec.get("label"),
|
|
469
|
+
"pid": getattr(child, "pid", None),
|
|
470
|
+
"returncode": returncode,
|
|
471
|
+
"timed_out": timed_out,
|
|
472
|
+
}
|
|
307
473
|
|
|
308
474
|
|
|
309
475
|
def _build_login_status_spec(session, env_override=None):
|
|
@@ -397,10 +563,8 @@ def _probe_provider_auth(session, spawn_sync=None, env_override=None):
|
|
|
397
563
|
spec = _build_login_status_spec(session, env_override)
|
|
398
564
|
if session.get("provider") == PROVIDER_CLAUDE and _has_local_claude_auth(_get_auth_home(session)):
|
|
399
565
|
return True
|
|
400
|
-
if session.get("provider") == PROVIDER_CODEX:
|
|
401
|
-
|
|
402
|
-
if os.path.isfile(auth_path):
|
|
403
|
-
return True
|
|
566
|
+
if session.get("provider") == PROVIDER_CODEX and _has_local_codex_auth(_get_auth_home(session)):
|
|
567
|
+
return True
|
|
404
568
|
try:
|
|
405
569
|
if spawn_sync is subprocess.run:
|
|
406
570
|
command = _resolve_command(spec["command"], spec["env"])
|
package/src/session_service.py
CHANGED
|
@@ -40,6 +40,8 @@ RESERVED_SESSION_NAMES = {
|
|
|
40
40
|
"ren",
|
|
41
41
|
"rename",
|
|
42
42
|
"rmv",
|
|
43
|
+
"run",
|
|
44
|
+
"select",
|
|
43
45
|
"config",
|
|
44
46
|
"set",
|
|
45
47
|
"status",
|
|
@@ -54,8 +56,11 @@ RESERVED_SESSION_NAMES = {
|
|
|
54
56
|
STATUS_CACHE_TTL_SECONDS = 60
|
|
55
57
|
CLAUDE_STATUS_CACHE_TTL_SECONDS = 10 * 60
|
|
56
58
|
LAUNCH_POWER_VALUES = {"low", "medium", "high", "xhigh", "max"}
|
|
59
|
+
LAUNCH_REASONING_EFFORT_VALUES = {"low", "medium", "high"}
|
|
57
60
|
LAUNCH_PERMISSION_VALUES = {"review", "default", "auto", "full"}
|
|
58
61
|
MAX_LAUNCH_MODEL_LENGTH = 128
|
|
62
|
+
MIN_LAUNCH_PRIORITY = 0
|
|
63
|
+
MAX_LAUNCH_PRIORITY = 100
|
|
59
64
|
|
|
60
65
|
|
|
61
66
|
def _encode(name):
|
|
@@ -96,6 +101,11 @@ def _normalize_launch_settings(settings):
|
|
|
96
101
|
if power not in LAUNCH_POWER_VALUES:
|
|
97
102
|
raise CdxError(f"Unsupported power: {settings['power']}")
|
|
98
103
|
normalized["power"] = power
|
|
104
|
+
if "reasoning_effort" in settings and settings["reasoning_effort"] is not None:
|
|
105
|
+
effort = str(settings["reasoning_effort"]).strip().lower()
|
|
106
|
+
if effort not in LAUNCH_REASONING_EFFORT_VALUES:
|
|
107
|
+
raise CdxError(f"Unsupported reasoning effort: {settings['reasoning_effort']}")
|
|
108
|
+
normalized["reasoning_effort"] = effort
|
|
99
109
|
if "permission" in settings and settings["permission"] is not None:
|
|
100
110
|
permission = str(settings["permission"]).strip().lower()
|
|
101
111
|
if permission not in LAUNCH_PERMISSION_VALUES:
|
|
@@ -120,6 +130,14 @@ def _normalize_launch_settings(settings):
|
|
|
120
130
|
if len(model) > MAX_LAUNCH_MODEL_LENGTH or any(ord(ch) < 32 or ord(ch) == 127 for ch in model):
|
|
121
131
|
raise CdxError("Model contains unsupported characters.")
|
|
122
132
|
normalized["model"] = model
|
|
133
|
+
if "priority" in settings and settings["priority"] is not None:
|
|
134
|
+
try:
|
|
135
|
+
priority = int(settings["priority"])
|
|
136
|
+
except (TypeError, ValueError) as error:
|
|
137
|
+
raise CdxError(f"Unsupported priority: {settings['priority']}") from error
|
|
138
|
+
if priority < MIN_LAUNCH_PRIORITY or priority > MAX_LAUNCH_PRIORITY:
|
|
139
|
+
raise CdxError(f"Unsupported priority: {settings['priority']}")
|
|
140
|
+
normalized["priority"] = priority
|
|
123
141
|
return normalized
|
|
124
142
|
|
|
125
143
|
|
|
@@ -773,7 +791,7 @@ def create_session_service(options=None):
|
|
|
773
791
|
raise CdxError(f"Unknown session: {name}")
|
|
774
792
|
if not keys:
|
|
775
793
|
raise CdxError("At least one launch setting is required.")
|
|
776
|
-
allowed = {"power", "permission", "fast", "model"}
|
|
794
|
+
allowed = {"power", "permission", "fast", "model", "priority"}
|
|
777
795
|
unknown = [key for key in keys if key not in allowed]
|
|
778
796
|
if unknown:
|
|
779
797
|
raise CdxError(f"Unsupported launch setting: {', '.join(unknown)}")
|