cdx-manager 0.7.2 → 0.7.4

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.
@@ -39,6 +39,7 @@ HEADLESS_CODEX_PERMISSION_ARGS = {
39
39
  "auto": ["-s", "workspace-write", "-c", 'approval_policy="never"'],
40
40
  "full": ["--dangerously-bypass-approvals-and-sandbox"],
41
41
  }
42
+ REDACTED_PROMPT_ARG = "[prompt redacted]"
42
43
 
43
44
 
44
45
  def _home_env_overrides(auth_home):
@@ -108,6 +109,12 @@ def _has_local_claude_auth(auth_home):
108
109
  return bool(isinstance(oauth, dict) and _clean_oauth_token(oauth.get("accessToken")))
109
110
 
110
111
 
112
+ def _read_claude_launch_oauth_token(auth_home):
113
+ if _has_local_claude_auth(auth_home):
114
+ return None
115
+ return _read_anthropic_oauth_token(auth_home)
116
+
117
+
111
118
  def _has_local_codex_auth(auth_home):
112
119
  try:
113
120
  with open(os.path.join(auth_home, "auth.json"), "r", encoding="utf-8") as handle:
@@ -312,7 +319,7 @@ def _build_launch_spec(session, cwd=None, env_override=None, initial_prompt=None
312
319
  args.append(initial_prompt)
313
320
  auth_home = _get_auth_home(session)
314
321
  claude_env = _claude_env(env, auth_home)
315
- oauth_token = _read_anthropic_oauth_token(auth_home)
322
+ oauth_token = _read_claude_launch_oauth_token(auth_home)
316
323
  if oauth_token:
317
324
  claude_env["CLAUDE_CODE_OAUTH_TOKEN"] = oauth_token
318
325
  return _wrap_launch_with_transcript(session, {
@@ -393,7 +400,7 @@ def _build_headless_launch_spec(session, cwd=None, env_override=None, initial_pr
393
400
  args.append(initial_prompt)
394
401
  auth_home = _get_auth_home(session)
395
402
  claude_env = _claude_env(env, auth_home)
396
- oauth_token = _read_anthropic_oauth_token(auth_home)
403
+ oauth_token = _read_claude_launch_oauth_token(auth_home)
397
404
  if oauth_token:
398
405
  claude_env["CLAUDE_CODE_OAUTH_TOKEN"] = oauth_token
399
406
  return {
@@ -401,6 +408,7 @@ def _build_headless_launch_spec(session, cwd=None, env_override=None, initial_pr
401
408
  "args": args,
402
409
  "options": {"cwd": cwd, "env": claude_env},
403
410
  "label": "claude",
411
+ "sensitive_args": [initial_prompt] if initial_prompt else [],
404
412
  }
405
413
 
406
414
  if session["provider"] == PROVIDER_CODEX:
@@ -418,6 +426,7 @@ def _build_headless_launch_spec(session, cwd=None, env_override=None, initial_pr
418
426
  "args": args,
419
427
  "options": {"cwd": cwd, "env": {**env, "CODEX_HOME": _get_auth_home(session)}},
420
428
  "label": "codex",
429
+ "sensitive_args": [initial_prompt] if initial_prompt else [],
421
430
  }
422
431
 
423
432
  return _build_launch_spec(
@@ -468,7 +477,7 @@ def _headless_run_info(paths, spec, start_time, returncode):
468
477
  "ended_at": end_time.isoformat().replace("+00:00", "Z"),
469
478
  "duration_ms": int((end_time - start_time).total_seconds() * 1000),
470
479
  "command": spec.get("command"),
471
- "args": list(spec.get("args") or []),
480
+ "args": _redact_sensitive_args(spec),
472
481
  "label": spec.get("label"),
473
482
  "pid": None,
474
483
  "returncode": returncode,
@@ -476,6 +485,14 @@ def _headless_run_info(paths, spec, start_time, returncode):
476
485
  }
477
486
 
478
487
 
488
+ def _redact_sensitive_args(spec):
489
+ args = list(spec.get("args") or [])
490
+ sensitive = {value for value in (spec.get("sensitive_args") or []) if value}
491
+ if not sensitive:
492
+ return args
493
+ return [REDACTED_PROMPT_ARG if arg in sensitive else arg for arg in args]
494
+
495
+
479
496
  def _run_headless_provider_command(session, cwd=None, env_override=None, initial_prompt=None,
480
497
  timeout_seconds=None, spawn=None, run_id=None):
481
498
  spawn = spawn or subprocess.Popen
@@ -545,7 +562,7 @@ def _run_headless_provider_command(session, cwd=None, env_override=None, initial
545
562
  "ended_at": end_time.isoformat().replace("+00:00", "Z"),
546
563
  "duration_ms": int((end_time - start_time).total_seconds() * 1000),
547
564
  "command": spec.get("command"),
548
- "args": list(spec.get("args") or []),
565
+ "args": _redact_sensitive_args(spec),
549
566
  "label": spec.get("label"),
550
567
  "pid": getattr(child, "pid", None),
551
568
  "returncode": returncode,
@@ -558,7 +575,7 @@ def _build_login_status_spec(session, env_override=None):
558
575
  if session["provider"] == PROVIDER_CLAUDE:
559
576
  auth_home = _get_auth_home(session)
560
577
  env = _claude_env(env, auth_home)
561
- oauth_token = _read_anthropic_oauth_token(auth_home)
578
+ oauth_token = _read_claude_launch_oauth_token(auth_home)
562
579
  if oauth_token:
563
580
  env["CLAUDE_CODE_OAUTH_TOKEN"] = oauth_token
564
581
 
@@ -639,13 +656,14 @@ def _resolve_command(command, env=None):
639
656
  return shutil.which(command, path=env.get("PATH")) or command
640
657
 
641
658
 
642
- def _probe_provider_auth(session, spawn_sync=None, env_override=None):
659
+ def _probe_provider_auth(session, spawn_sync=None, env_override=None, trust_local_credentials=True):
643
660
  spawn_sync = spawn_sync or subprocess.run
644
661
  spec = _build_login_status_spec(session, env_override)
645
- if session.get("provider") == PROVIDER_CLAUDE and _has_local_claude_auth(_get_auth_home(session)):
646
- return True
647
- if session.get("provider") == PROVIDER_CODEX and _has_local_codex_auth(_get_auth_home(session)):
648
- return True
662
+ if trust_local_credentials:
663
+ if session.get("provider") == PROVIDER_CLAUDE and _has_local_claude_auth(_get_auth_home(session)):
664
+ return True
665
+ if session.get("provider") == PROVIDER_CODEX and _has_local_codex_auth(_get_auth_home(session)):
666
+ return True
649
667
  try:
650
668
  if spawn_sync is subprocess.run:
651
669
  command = _resolve_command(spec["command"], spec["env"])
@@ -830,8 +848,17 @@ def _should_retry_without_transcript(spec):
830
848
 
831
849
  def _ensure_session_authentication(session, service, spawn=None, spawn_sync=None,
832
850
  stdin_is_tty=True, env_override=None, behavior="launch",
833
- signal_emitter=None):
834
- is_authenticated = _probe_provider_auth(session, spawn_sync=spawn_sync, env_override=env_override)
851
+ signal_emitter=None, trust_local_credentials=True):
852
+ if behavior == "launch" and (session.get("auth") or {}).get("status") == "logged_out":
853
+ raise CdxError(
854
+ f"Session {session['name']} is not authenticated. Run: cdx login {session['name']}"
855
+ )
856
+ is_authenticated = _probe_provider_auth(
857
+ session,
858
+ spawn_sync=spawn_sync,
859
+ env_override=env_override,
860
+ trust_local_credentials=trust_local_credentials,
861
+ )
835
862
  if is_authenticated:
836
863
  return {"authenticated": True, "checked": True}
837
864
  if behavior == "probe-only":
@@ -0,0 +1,83 @@
1
+ import os
2
+
3
+ from .errors import CdxError
4
+ from .run_usage import empty_usage, extract_run_usage
5
+
6
+
7
+ def read_run_prompt(parsed):
8
+ if parsed.get("prompt") is not None:
9
+ return parsed["prompt"]
10
+ try:
11
+ with open(parsed["prompt_file"], "r", encoding="utf-8") as handle:
12
+ return handle.read()
13
+ except OSError as error:
14
+ raise CdxError(f"Unable to read prompt file: {parsed['prompt_file']}") from error
15
+
16
+
17
+ def run_cdx_error_code(error):
18
+ message = str(error)
19
+ if message.startswith("Usage:"):
20
+ return "invalid_request"
21
+ if message.startswith("Invalid cwd:"):
22
+ return "invalid_cwd"
23
+ if message.startswith("Session is disabled:"):
24
+ return "session_disabled"
25
+ if "CLI not found on PATH" in message:
26
+ return "provider_cli_not_found"
27
+ if message.startswith("Failed to start "):
28
+ return "provider_start_failed"
29
+ if (
30
+ message.startswith("Unsupported reasoning effort:")
31
+ or message.startswith("Unsupported power:")
32
+ or "--reasoning-effort and --power must match" in message
33
+ ):
34
+ return "invalid_reasoning_effort"
35
+ return "cdx_error"
36
+
37
+
38
+ def run_payload_reasoning_effort(parsed, session):
39
+ launch = (session.get("launch") or {}) if session else {}
40
+ return (
41
+ parsed.get("reasoning_effort")
42
+ or parsed.get("power")
43
+ or launch.get("reasoning_effort")
44
+ or launch.get("reasoningEffort")
45
+ or launch.get("power")
46
+ or ("low" if launch.get("fast") is True else None)
47
+ )
48
+
49
+
50
+ def run_result_payload(api_schema_version, ok, parsed, session, run_info=None,
51
+ error=None, error_source=None, error_code=None):
52
+ run_info = run_info or {}
53
+ usage = run_info.get("usage") if isinstance(run_info.get("usage"), dict) else (
54
+ extract_run_usage(session.get("provider"), run_info.get("stdout_path"))
55
+ if session else
56
+ empty_usage()
57
+ )
58
+ return {
59
+ "schema_version": api_schema_version,
60
+ "ok": bool(ok),
61
+ "action": "run",
62
+ "launcher": "cdx",
63
+ "session": session.get("name") if session else None,
64
+ "provider": session.get("provider") if session else parsed.get("provider"),
65
+ "model": parsed.get("model") or ((session.get("launch") or {}).get("model") if session else None),
66
+ "reasoning_effort": run_payload_reasoning_effort(parsed, session),
67
+ "power": parsed.get("power") or ((session.get("launch") or {}).get("power") if session else None),
68
+ "cwd": os.path.abspath(parsed.get("cwd") or os.getcwd()),
69
+ "run_id": run_info.get("run_id"),
70
+ "exit_code": run_info.get("returncode"),
71
+ "duration_seconds": (run_info.get("duration_ms") / 1000.0) if run_info.get("duration_ms") is not None else None,
72
+ "transcript_path": run_info.get("transcript_path"),
73
+ "stdout_path": run_info.get("stdout_path"),
74
+ "stderr_path": run_info.get("stderr_path"),
75
+ "usage": usage,
76
+ "warnings": [],
77
+ "error": None if ok else {
78
+ "source": error_source or "cdx",
79
+ "code": error_code or "cdx_error",
80
+ "message": str(error) if error else "Run failed.",
81
+ "provider_code": run_info.get("returncode") if error_source == "provider" else None,
82
+ },
83
+ }
@@ -5,6 +5,7 @@ import base64
5
5
  import sys
6
6
  import tempfile
7
7
  import uuid
8
+ from concurrent.futures import ThreadPoolExecutor, as_completed
8
9
  from datetime import datetime, timezone
9
10
  from urllib.parse import quote
10
11
 
@@ -22,19 +23,26 @@ RESERVED_SESSION_NAMES = {
22
23
  "add",
23
24
  "clean",
24
25
  "context",
26
+ "configs",
25
27
  "cp",
26
28
  "disable",
27
29
  "doctor",
28
30
  "enable",
29
31
  "export",
32
+ "fast",
30
33
  "help",
31
34
  "handoff",
32
35
  "history",
33
36
  "import",
37
+ "last",
34
38
  "login",
35
39
  "logout",
40
+ "model",
36
41
  "mv",
42
+ "next",
37
43
  "notify",
44
+ "perm",
45
+ "power",
38
46
  "ready",
39
47
  "repair",
40
48
  "ren",
@@ -44,6 +52,7 @@ RESERVED_SESSION_NAMES = {
44
52
  "select",
45
53
  "config",
46
54
  "set",
55
+ "stats",
47
56
  "status",
48
57
  "unset",
49
58
  "update",
@@ -55,12 +64,17 @@ RESERVED_SESSION_NAMES = {
55
64
  }
56
65
  STATUS_CACHE_TTL_SECONDS = 60
57
66
  CLAUDE_STATUS_CACHE_TTL_SECONDS = 10 * 60
67
+ MAX_STATUS_WORKERS = 8
58
68
  LAUNCH_POWER_VALUES = {"low", "medium", "high", "xhigh", "max"}
59
69
  LAUNCH_REASONING_EFFORT_VALUES = {"low", "medium", "high"}
60
70
  LAUNCH_PERMISSION_VALUES = {"review", "default", "auto", "full"}
61
71
  MAX_LAUNCH_MODEL_LENGTH = 128
62
72
  MIN_LAUNCH_PRIORITY = 0
63
73
  MAX_LAUNCH_PRIORITY = 100
74
+ DEFAULT_LAUNCH_SETTINGS = {
75
+ "power": "medium",
76
+ "fast": False,
77
+ }
64
78
 
65
79
 
66
80
  def _encode(name):
@@ -448,6 +462,8 @@ def create_session_service(options=None):
448
462
  raise CdxError("Session name is required")
449
463
  if str(name) != str(name).strip():
450
464
  raise CdxError("Session name cannot start or end with whitespace")
465
+ if str(name) in (".", ".."):
466
+ raise CdxError("Session name cannot be . or ..")
451
467
  if len(str(name)) > MAX_SESSION_NAME_LENGTH:
452
468
  raise CdxError(f"Session name is too long (max {MAX_SESSION_NAME_LENGTH} characters)")
453
469
  if any(ord(ch) < 32 or ord(ch) == 127 for ch in str(name)):
@@ -542,6 +558,7 @@ def create_session_service(options=None):
542
558
  "lastLaunchedAt": None,
543
559
  "lastStatusAt": None,
544
560
  "lastStatus": None,
561
+ "launch": dict(DEFAULT_LAUNCH_SETTINGS),
545
562
  "auth": {
546
563
  "status": "unknown",
547
564
  "lastCheckedAt": None,
@@ -939,14 +956,9 @@ def create_session_service(options=None):
939
956
 
940
957
  def get_status_rows(progress_callback=None, force_refresh=False, cache_ttl_seconds=STATUS_CACHE_TTL_SECONDS):
941
958
  sessions = list_sessions()
942
- if progress_callback:
943
- progress_callback({
944
- "event": "status_started",
945
- "session_count": len(sessions),
946
- })
947
- resolved = []
948
- for s in sessions:
949
- cache_hit = (
959
+
960
+ def _status_cache_hit(s):
961
+ return (
950
962
  s.get("enabled", True) is False
951
963
  or (
952
964
  s.get("lastStatus")
@@ -954,28 +966,56 @@ def create_session_service(options=None):
954
966
  and _is_status_cache_fresh(s, ttl_seconds=cache_ttl_seconds)
955
967
  )
956
968
  )
957
- if progress_callback and not cache_hit:
958
- progress_callback({
959
- "event": "session_started",
960
- "session_name": s["name"],
961
- "provider": s["provider"],
962
- })
969
+
970
+ cache_hits = {
971
+ s["name"]: _status_cache_hit(s)
972
+ for s in sessions
973
+ }
974
+ if progress_callback:
975
+ progress_callback({
976
+ "event": "status_started",
977
+ "session_count": len(sessions),
978
+ "check_count": sum(1 for cache_hit in cache_hits.values() if not cache_hit),
979
+ })
980
+
981
+ def _resolve_row_session(s):
963
982
  status = _resolve_session_status(
964
983
  s,
965
984
  force_refresh=force_refresh,
966
985
  cache_ttl_seconds=cache_ttl_seconds,
967
986
  )
968
- if progress_callback:
969
- progress_callback({
970
- "event": "session_finished",
971
- "session_name": s["name"],
972
- "has_status": bool(status),
973
- })
974
- resolved.append({
987
+ return {
975
988
  **s,
976
989
  "lastStatus": status,
977
990
  "lastStatusAt": (status and status.get("updated_at")) or s.get("lastStatusAt"),
978
- })
991
+ }
992
+
993
+ resolved_by_name = {}
994
+ if sessions:
995
+ max_workers = min(MAX_STATUS_WORKERS, len(sessions))
996
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
997
+ futures = {}
998
+ for s in sessions:
999
+ cache_hit = cache_hits[s["name"]]
1000
+ if progress_callback and not cache_hit:
1001
+ progress_callback({
1002
+ "event": "session_started",
1003
+ "session_name": s["name"],
1004
+ "provider": s["provider"],
1005
+ })
1006
+ futures[executor.submit(_resolve_row_session, s)] = s
1007
+ for future in as_completed(futures):
1008
+ s = futures[future]
1009
+ resolved = future.result()
1010
+ resolved_by_name[s["name"]] = resolved
1011
+ if progress_callback:
1012
+ progress_callback({
1013
+ "event": "session_finished",
1014
+ "session_name": s["name"],
1015
+ "has_status": bool(resolved.get("lastStatus")),
1016
+ "cache_hit": cache_hits[s["name"]],
1017
+ })
1018
+ resolved = [resolved_by_name[s["name"]] for s in sessions]
979
1019
 
980
1020
  def sort_key(s):
981
1021
  at = s.get("lastStatusAt") or ""
@@ -1092,7 +1132,7 @@ def create_session_service(options=None):
1092
1132
  bundle_bytes = encode_bundle(payload, include_auth=include_auth, passphrase=passphrase)
1093
1133
  if progress_callback:
1094
1134
  progress_callback({"event": "writing_started", "path": file_path, "bundle_size_bytes": len(bundle_bytes)})
1095
- _ensure_private_dir(os.path.dirname(os.path.abspath(file_path)) or ".")
1135
+ os.makedirs(os.path.dirname(os.path.abspath(file_path)) or ".", exist_ok=True)
1096
1136
  with open(file_path, "wb") as handle:
1097
1137
  handle.write(bundle_bytes)
1098
1138
  if sys.platform != "win32":
@@ -1,4 +1,5 @@
1
1
  from datetime import datetime
2
+ from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
2
3
 
3
4
  from .cli_render import (
4
5
  _dim,
@@ -61,6 +62,17 @@ def _style_reset_time(value, use_color=False):
61
62
  return text
62
63
 
63
64
 
65
+ def _format_credits(value, empty="n/a"):
66
+ if value is None:
67
+ return empty
68
+ try:
69
+ normalized = str(value).strip().replace(",", "")
70
+ rounded = Decimal(normalized).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
71
+ return f"{rounded:.2f}"
72
+ except (InvalidOperation, ValueError):
73
+ return str(value)
74
+
75
+
64
76
  def _format_status_rows(rows, use_color=False, small=False):
65
77
  has_provider = len({r["provider"] for r in rows}) > 1 and not small
66
78
  if small:
@@ -75,15 +87,11 @@ def _format_status_rows(rows, use_color=False, small=False):
75
87
  return "SESSION STATUS OK 5H WEEK BLOCK CR RESET 5H RESET WEEK UPDATED\nNo saved sessions yet."
76
88
  headers = [_style(header, "1", use_color) for header in headers]
77
89
  active_rows = [r for r in rows if r.get("enabled", True) is not False]
78
- priority_candidates = [
79
- r for r in active_rows
80
- if _format_auth_status(r) != "logged out"
81
- ]
90
+ priority = recommend_priority_rows(rows)
82
91
  disabled_rows = sorted(
83
92
  [r for r in rows if r.get("enabled", True) is False],
84
93
  key=lambda r: r.get("session_name") or "",
85
94
  )
86
- priority = _recommend_priority_sessions(priority_candidates)
87
95
  priority_names = {r.get("session_name") for r in priority}
88
96
  non_priority_active = [
89
97
  r for r in active_rows
@@ -114,7 +122,7 @@ def _format_status_rows(rows, use_color=False, small=False):
114
122
  base += usage_columns
115
123
  else:
116
124
  block = "-" if r.get("enabled", True) is False else _format_blocking_quota(r)
117
- credits = str(r["credits"]) if r.get("credits") is not None else "-"
125
+ credits = _format_credits(r.get("credits"), empty="-")
118
126
  base += usage_columns[:3] + [
119
127
  _style(block, "33" if block not in ("?", "-") else "2", use_color),
120
128
  _style(credits, "33" if r.get("credits") is not None else "2", use_color),
@@ -168,6 +176,19 @@ def _format_auth_status(row):
168
176
  return "unknown"
169
177
 
170
178
 
179
+ def recommend_priority_rows(rows):
180
+ active_rows = [r for r in rows if r.get("enabled", True) is not False]
181
+ priority_candidates = [
182
+ r for r in active_rows
183
+ if _format_auth_status(r) != "logged out"
184
+ ]
185
+ return _recommend_priority_sessions(priority_candidates)
186
+
187
+
188
+ def format_priority_instruction(row, position="first"):
189
+ return _priority_instruction(row, position)
190
+
191
+
171
192
  def _recommend_priority_sessions(rows):
172
193
  if not rows:
173
194
  return []
@@ -332,7 +353,7 @@ def _format_status_detail(row, use_color=False):
332
353
  f"{_style('5h left:', '1', use_color)} {_style_pct(row.get('remaining_5h_pct'), use_color)}",
333
354
  f"{_style('Week left:', '1', use_color)} {_style_pct(row.get('remaining_week_pct'), use_color)}",
334
355
  f"{_style('Block:', '1', use_color)} {_style(_format_blocking_quota(row), '33', use_color)}",
335
- f"{_style('Credits:', '1', use_color)} {_style(row['credits'] if row.get('credits') is not None else 'n/a', '33' if row.get('credits') is not None else '2', use_color)}",
356
+ f"{_style('Credits:', '1', use_color)} {_style(_format_credits(row.get('credits')), '33' if row.get('credits') is not None else '2', use_color)}",
336
357
  f"{_style('5h reset:', '1', use_color)} {_style_reset_time(row.get('reset_5h_at'), use_color)}",
337
358
  f"{_style('Week reset:', '1', use_color)} {_style_reset_time(row.get('reset_week_at'), use_color)}",
338
359
  f"{_style('Updated:', '1', use_color)} {_dim(_format_relative_age(row.get('updated_at')), use_color)}",