cdx-manager 0.7.3 → 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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # CDX Manager
2
2
 
3
- [![License](https://img.shields.io/badge/license-MIT-4C8BF5)](LICENSE) ![Version](https://img.shields.io/badge/version-v0.7.3-4C8BF5) ![Python](https://img.shields.io/badge/python-3.9%2B-3776AB?logo=python&logoColor=white)
3
+ [![License](https://img.shields.io/badge/license-MIT-4C8BF5)](LICENSE) ![Version](https://img.shields.io/badge/version-v0.7.4-4C8BF5) ![Python](https://img.shields.io/badge/python-3.9%2B-3776AB?logo=python&logoColor=white)
4
4
 
5
5
  **Run multiple Codex, Claude, Antigravity, and Ollama sessions from one terminal. Switch between accounts instantly.**
6
6
 
@@ -118,7 +118,8 @@ With the standalone PowerShell installer:
118
118
 
119
119
  ```powershell
120
120
  Invoke-WebRequest https://raw.githubusercontent.com/AlexAgo83/cdx-manager/main/install.ps1 -OutFile install.ps1
121
- # Optional: set CDX_SHA256 before running if you have a trusted checksum
121
+ # Optional: set CDX_SHA256 before running if you have a trusted checksum.
122
+ # Set CDX_ALLOW_UNVERIFIED=1 only if you intentionally accept an unverified archive.
122
123
  powershell -ExecutionPolicy Bypass -File .\install.ps1
123
124
  ```
124
125
 
@@ -126,7 +127,8 @@ With the standalone GitHub installer:
126
127
 
127
128
  ```bash
128
129
  curl -fsSL https://raw.githubusercontent.com/AlexAgo83/cdx-manager/main/install.sh -o install.sh
129
- # Optional: set CDX_SHA256 before running if you have a trusted checksum
130
+ # Optional: set CDX_SHA256 before running if you have a trusted checksum.
131
+ # Set CDX_ALLOW_UNVERIFIED=1 only if you intentionally accept an unverified archive.
130
132
  sh install.sh
131
133
  ```
132
134
 
@@ -134,7 +136,7 @@ For a specific version:
134
136
 
135
137
  ```bash
136
138
  curl -fsSL https://raw.githubusercontent.com/AlexAgo83/cdx-manager/main/install.sh -o install.sh
137
- CDX_VERSION=v0.7.3 sh install.sh
139
+ CDX_VERSION=v0.7.4 sh install.sh
138
140
  ```
139
141
 
140
142
  From source:
@@ -183,6 +185,7 @@ Security note:
183
185
 
184
186
  - The standalone installers try to resolve official release checksums from `checksums/release-archives.json`.
185
187
  - You can still override verification explicitly through `CDX_SHA256`.
188
+ - If no checksum is available, standalone installers fail closed unless `CDX_ALLOW_UNVERIFIED=1` is set.
186
189
  - Prefer `npm`, `pipx`, or `uv` when you want registry-backed install flows.
187
190
  - If you use the standalone script, download it first, inspect it, and prefer a release with an official checksum entry.
188
191
 
@@ -241,6 +244,9 @@ cdx work
241
244
  # Check usage across all sessions
242
245
  cdx status
243
246
 
247
+ # Pick the best session using the same priority logic as status
248
+ cdx next
249
+
244
250
  # Notify when the next cooling-down assistant is ready
245
251
  cdx ready
246
252
  ```
@@ -256,7 +262,7 @@ cdx ready --refresh
256
262
 
257
263
  ### Persistent Launch Settings
258
264
 
259
- By default, `cdx` launches provider CLIs without forcing model effort, permission mode, or fast behavior. Set only the values you want to pin:
265
+ New sessions start with `power=medium` and `fast=off`, so launches are predictable without enabling fast mode. Set or override only the values you want to pin:
260
266
 
261
267
  ```bash
262
268
  cdx set work --power medium --permission full --fast off
@@ -269,9 +275,10 @@ cdx power all low
269
275
  cdx perm provider:claude review
270
276
  cdx model provider:ollama llama3.2
271
277
  cdx config work
278
+ cdx configs
272
279
  ```
273
280
 
274
- Those values are stored on the session and reapplied every time you run `cdx work`. Remove overrides to return to provider defaults:
281
+ Those values are stored on the session and reapplied every time you run `cdx work`. Remove overrides to return to provider-native defaults:
275
282
 
276
283
  ```bash
277
284
  cdx unset work --power
@@ -320,6 +327,7 @@ cdx history --summary --from 2026-05-01 --to 2026-05-28
320
327
  | `cdx disable <name> [--json]` | Disable a session without deleting it; disabled sessions stay visible and cannot launch |
321
328
  | `cdx enable <name> [--json]` | Re-enable a disabled session |
322
329
  | `cdx config <name> [--json]` | Show persistent launch settings for a session |
330
+ | `cdx configs [--json]` | Show persistent launch settings for all sessions in one table |
323
331
  | `cdx power\|perm\|fast\|model <name\|all\|provider:PROVIDER\|a,b> <value\|default> [--json]` | Shortcut commands for setting or clearing one launch setting |
324
332
  | `cdx set <name>\|--sessions all\|a,b\|--provider PROVIDER [--power low\|medium\|high\|xhigh\|max] [--permission review\|default\|auto\|full] [--fast on\|off] [--rtk on\|off] [--model MODEL] [--priority 0..100] [--json]` | Persist launch settings for one or more sessions |
325
333
  | `cdx unset <name>\|--sessions all\|a,b\|--provider PROVIDER (--power\|--permission\|--fast\|--rtk\|--model\|--priority\|--all) [--json]` | Remove persisted launch settings and fall back to provider defaults |
@@ -338,6 +346,7 @@ cdx history --summary --from 2026-05-01 --to 2026-05-28
338
346
  | `cdx ready [--refresh] [--json]` | Schedule an OS notification for the next cooling-down assistant that becomes ready, then return immediately |
339
347
  | `cdx notify <name> --at-reset [--poll seconds] [--once] [--schedule] [--refresh] [--json]` | Wait for a session reset time or schedule an OS wake-up notification when due |
340
348
  | `cdx notify --next-ready [--poll seconds] [--once] [--schedule] [--refresh] [--json]` | Wait until the recommended session is usable, or schedule the next known reset notification |
349
+ | `cdx next [--json] [--refresh]` | Select the best next assistant using the same priority logic as `cdx status` |
341
350
  | `cdx select --provider PROVIDER [--min-reasoning-effort low\|medium\|high] [--min-power low\|medium\|high] [--require-ready] [--refresh] --json` | Select a suitable session for headless automation |
342
351
  | `cdx run [session] --cwd PATH (--prompt-file PATH\|--prompt TEXT) [--provider PROVIDER] [--model MODEL] [--reasoning-effort low\|medium\|high] [--power low\|medium\|high] [--permission MODE] [--timeout-seconds N] --json` | Run one headless task and return a stable JSON result |
343
352
  | `cdx stats [name] [--since 7d\|today\|DATE] [--from DATE] [--to DATE] [--json]` | Aggregate launch counts, duration, and known headless token usage by session |
@@ -0,0 +1,45 @@
1
+ # Changelog (`0.7.3 -> 0.7.4`)
2
+
3
+ Release date: 2026-06-02
4
+
5
+ ## Security and Privacy
6
+
7
+ - Rejected `.` and `..` as session names so profile and state paths cannot escape their managed directories.
8
+ - Added import-bundle regression coverage for unsafe dot-path session names.
9
+ - Redacted headless `cdx run` prompt arguments before persisting launch history while keeping provider execution unchanged.
10
+ - Made headless `cdx run` perform a live provider auth probe instead of trusting only local credential files.
11
+ - Hardened Claude auth invalidation so invalid Claude credentials are recorded as logged out during refresh and status handling.
12
+
13
+ ## Installer Integrity
14
+
15
+ - Changed standalone Unix and PowerShell installers to fail closed when no official checksum is available.
16
+ - Added the explicit `CDX_ALLOW_UNVERIFIED=1` override for users who intentionally accept an unverified archive.
17
+ - Updated README security guidance for standalone installer checksum behavior.
18
+
19
+ ## Headless and Selection Behavior
20
+
21
+ - Added the next-assistant recommendation command and improved resolved run reasoning-effort reporting.
22
+ - Preserved stable provider-specific launch metadata while adding stricter headless auth validation.
23
+
24
+ ## Maintainability
25
+
26
+ - Extracted headless run prompt, error-code, and JSON payload helpers into `src/run_command.py`.
27
+ - Reduced the size of `src/cli_commands.py` without changing the public `cdx run --json` contract.
28
+
29
+ ## Coverage and Tests
30
+
31
+ - Added regression coverage for dot-path session names, unsafe imported sessions, redacted headless history prompts, forced live auth probes, and unauthenticated headless runs.
32
+ - Increased the Python test suite to 276 tests.
33
+
34
+ ## Release Metadata
35
+
36
+ - Updated package metadata, CLI version output, README badge, pinned installer example, and release changelog to `v0.7.4`.
37
+
38
+ ## Validation and Regression Evidence
39
+
40
+ - `npm run lint`
41
+ - `npm test`
42
+ - `npm audit --omit=dev --json`
43
+ - `sh -n install.sh`
44
+ - PowerShell parser check for `install.ps1`
45
+ - `python3 -m logics_manager lint --require-status`
@@ -40,6 +40,10 @@
40
40
  "v0.7.2": {
41
41
  "github_tarball_sha256": "9e993b15386c7b47895cfaca9c7e92165967ef34ecf8337c64823a5b0f9eb9e0",
42
42
  "github_zip_sha256": "1e5fd926a16811f84137636830e0b27776b9f8f8d2eb8953b620023c8bfe6fdd"
43
+ },
44
+ "v0.7.3": {
45
+ "github_tarball_sha256": "f68ac90a51c723eef9ae2b66106baa954f2fff8129fb5353e8eeb26b258b06c9",
46
+ "github_zip_sha256": "dde2512701808450b39c5c4431317d95d017f9944435db83ac4497b645feb719"
43
47
  }
44
48
  }
45
49
  }
package/install.ps1 CHANGED
@@ -62,7 +62,11 @@ try {
62
62
  throw "cdx install: checksum mismatch for $tag`nexpected: $Sha256`nactual: $actualSha256"
63
63
  }
64
64
  } else {
65
- Write-Warning "No official checksum available for $tag; continuing without verification."
65
+ if ($env:CDX_ALLOW_UNVERIFIED -eq "1") {
66
+ Write-Warning "No official checksum available for $tag; continuing because CDX_ALLOW_UNVERIFIED=1."
67
+ } else {
68
+ throw "cdx install: no official checksum available for $tag. Set CDX_ALLOW_UNVERIFIED=1 to install without checksum verification."
69
+ }
66
70
  }
67
71
  Expand-Archive -Path $archivePath -DestinationPath $extractRoot -Force
68
72
 
package/install.sh CHANGED
@@ -86,7 +86,13 @@ if [ -n "$EXPECTED_SHA256" ]; then
86
86
  exit 1
87
87
  fi
88
88
  else
89
- echo "cdx install: warning: no official checksum available for $TAG; continuing without verification" >&2
89
+ if [ "${CDX_ALLOW_UNVERIFIED:-}" = "1" ]; then
90
+ echo "cdx install: warning: no official checksum available for $TAG; continuing because CDX_ALLOW_UNVERIFIED=1" >&2
91
+ else
92
+ echo "cdx install: no official checksum available for $TAG" >&2
93
+ echo "Set CDX_ALLOW_UNVERIFIED=1 to install without checksum verification." >&2
94
+ exit 1
95
+ fi
90
96
  fi
91
97
 
92
98
  tar -xzf "$TMP_DIR/cdx-manager.tar.gz" -C "$TMP_DIR"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cdx-manager",
3
- "version": "0.7.3",
3
+ "version": "0.7.4",
4
4
  "description": "Terminal session manager for Codex and Claude accounts.",
5
5
  "license": "MIT",
6
6
  "author": "Alexandre Agostini",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "cdx-manager"
7
- version = "0.7.3"
7
+ version = "0.7.4"
8
8
  description = "Terminal session manager for Codex and Claude accounts."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -2,7 +2,7 @@ import inspect
2
2
  import threading
3
3
  from datetime import datetime, timezone
4
4
 
5
- from .claude_usage import refresh_claude_session_status
5
+ from .claude_usage import ClaudeAuthInvalidError, refresh_claude_session_status
6
6
  from .config import PROVIDER_CLAUDE
7
7
  from .errors import CdxError
8
8
 
@@ -83,6 +83,24 @@ def _refresh_claude_sessions(service, refresh_fn=None, target_names=None, force=
83
83
  for t in threads:
84
84
  t.join(timeout=10)
85
85
 
86
+ invalid_auth = sorted({
87
+ item.get("session")
88
+ for item in errors
89
+ if item.get("session") and isinstance(item.get("error"), ClaudeAuthInvalidError)
90
+ })
91
+ if invalid_auth and service.get("update_auth_state"):
92
+ now = datetime.now(timezone.utc).astimezone().isoformat()
93
+ for name in invalid_auth:
94
+ try:
95
+ service["update_auth_state"](name, lambda auth: {
96
+ **auth,
97
+ "status": "logged_out",
98
+ "lastCheckedAt": now,
99
+ "lastLoggedOutAt": now,
100
+ })
101
+ except CdxError:
102
+ pass
103
+
86
104
  for name, usage in results.items():
87
105
  try:
88
106
  service["record_status"](name, usage)
@@ -15,6 +15,10 @@ CLAUDE_STATUS_PROBE_MODEL = os.environ.get("CDX_CLAUDE_STATUS_MODEL", "claude-ha
15
15
  CLAUDE_AUTH_STATUS_TIMEOUT_SECONDS = 15
16
16
 
17
17
 
18
+ class ClaudeAuthInvalidError(CdxError):
19
+ pass
20
+
21
+
18
22
  def _clean_oauth_token(token):
19
23
  if not token:
20
24
  return None
@@ -142,6 +146,10 @@ def fetch_claude_rate_limit_headers(access_token):
142
146
  headers = {k.lower(): v for k, v in resp.getheaders()}
143
147
  except urllib.error.HTTPError as e:
144
148
  headers = {k.lower(): v for k, v in e.headers.items()}
149
+ if e.code == 401:
150
+ message = _read_http_error_message(e)
151
+ suffix = f": {message}" if message else ""
152
+ raise ClaudeAuthInvalidError(f"Claude usage unavailable (HTTP {e.code}{suffix})") from e
145
153
  if (
146
154
  "anthropic-ratelimit-unified-5h-utilization" not in headers
147
155
  and "anthropic-ratelimit-unified-7d-utilization" not in headers
package/src/cli.py CHANGED
@@ -10,6 +10,7 @@ from .cli_commands import (
10
10
  handle_add,
11
11
  handle_clean,
12
12
  handle_config,
13
+ handle_configs,
13
14
  handle_context,
14
15
  handle_copy,
15
16
  handle_doctor,
@@ -25,6 +26,7 @@ from .cli_commands import (
25
26
  handle_logout,
26
27
  handle_launch_setting_alias,
27
28
  handle_notify,
29
+ handle_next,
28
30
  handle_remove,
29
31
  handle_repair,
30
32
  handle_rename,
@@ -58,7 +60,7 @@ from .status_view import (
58
60
  )
59
61
  from .update_check import check_for_update
60
62
 
61
- VERSION = "0.7.3"
63
+ VERSION = "0.7.4"
62
64
 
63
65
 
64
66
  # ---------------------------------------------------------------------------
@@ -75,10 +77,12 @@ def _print_help(use_color=False):
75
77
  f" {_style('cdx status [--json] [--refresh]', '36', use_color)}",
76
78
  f" {_style('cdx status --small|-s [--refresh]', '36', use_color)}",
77
79
  f" {_style('cdx status <name> [--json] [--refresh]', '36', use_color)}",
80
+ f" {_style('cdx next [--json] [--refresh]', '36', use_color)}",
78
81
  f" {_style('cdx select --provider PROVIDER [--min-reasoning-effort low|medium|high] [--min-power low|medium|high] [--require-ready] [--refresh] --json', '36', use_color)}",
79
82
  f" {_style('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', '36', use_color)}",
80
83
  f" {_style('cdx context show|path|init|edit|clear|set [text...] [--json]', '36', use_color)}",
81
84
  f" {_style('cdx config <name> [--json]', '36', use_color)}",
85
+ f" {_style('cdx configs [--json]', '36', use_color)}",
82
86
  f" {_style('cdx power|perm|fast|model <name|all|provider:PROVIDER|a,b> <value|default> [--json]', '36', use_color)}",
83
87
  f" {_style('cdx set <name>|--sessions all|a,b|--provider PROVIDER [--power low|medium|high|xhigh|max] [--permission review|default|auto|full] [--fast on|off] [--rtk on|off] [--model MODEL] [--priority 0..100] [--json]', '36', use_color)}",
84
88
  f" {_style('cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--permission|--fast|--rtk|--model|--priority|--all) [--json]', '36', use_color)}",
@@ -240,7 +244,7 @@ def main(argv, options=None):
240
244
  "version": VERSION,
241
245
  "cwd": options.get("cwd") or os.getcwd(),
242
246
  "update_notice": _get_update_notice(service, env, options) if command not in (
243
- "add", "cp", "ren", "rename", "mv", "rmv", "clean", "doctor", "repair", "update", "ready", "notify", "context", "config", "set", "unset", "power", "perm", "fast", "model", "history", "stats", "handoff", "login", "logout", "disable", "enable", "export", "import", "select", "run", "help", "version"
247
+ "add", "cp", "ren", "rename", "mv", "rmv", "clean", "doctor", "repair", "update", "ready", "notify", "next", "context", "config", "configs", "set", "unset", "power", "perm", "fast", "model", "history", "stats", "handoff", "login", "logout", "disable", "enable", "export", "import", "select", "run", "help", "version"
244
248
  ) else None,
245
249
  "use_color": use_color,
246
250
  }
@@ -289,12 +293,18 @@ def main(argv, options=None):
289
293
  if command == "notify":
290
294
  return handle_notify(rest, ctx)
291
295
 
296
+ if command == "next":
297
+ return handle_next(rest, ctx)
298
+
292
299
  if command == "context":
293
300
  return handle_context(rest, ctx)
294
301
 
295
302
  if command == "config":
296
303
  return handle_config(rest, ctx)
297
304
 
305
+ if command == "configs":
306
+ return handle_configs(rest, ctx)
307
+
298
308
  if command == "set":
299
309
  return handle_set(rest, ctx)
300
310
 
@@ -40,8 +40,9 @@ from .provider_runtime import (
40
40
  )
41
41
  from .repair import format_repair_report, repair_health
42
42
  from .backup_bundle import read_bundle_meta
43
- from .run_usage import empty_usage, extract_run_usage
44
- from .status_view import _format_status_detail, _format_status_rows
43
+ from .run_usage import extract_run_usage
44
+ from .run_command import read_run_prompt, run_cdx_error_code, run_result_payload
45
+ from .status_view import _format_status_detail, _format_status_rows, format_priority_instruction, recommend_priority_rows
45
46
  from .update_check import LatestReleaseCheckError, fetch_latest_release, fetch_latest_release_or_raise, is_newer_version
46
47
  from .update_manager import build_update_plan, format_update_failure, run_update_plan, verify_updated_command
47
48
 
@@ -58,10 +59,12 @@ SET_USAGE = "Usage: cdx set <name>|--sessions all|a,b|--provider PROVIDER [--pow
58
59
  UNSET_USAGE = "Usage: cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--permission|--fast|--rtk|--model|--priority|--all) [--json]"
59
60
  SETTING_ALIAS_USAGE = "Usage: cdx power|perm|fast|model <name|all|provider:PROVIDER|a,b> <value|default> [--json]"
60
61
  CONFIG_USAGE = "Usage: cdx config <name> [--json]"
62
+ CONFIGS_USAGE = "Usage: cdx configs [--json]"
61
63
  HISTORY_USAGE = "Usage: cdx history [name] [--limit N] [--summary] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]"
62
64
  STATS_USAGE = "Usage: cdx stats [name] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]"
63
65
  LAST_USAGE = "Usage: cdx last [--json]"
64
66
  SELECT_USAGE = "Usage: cdx select --provider PROVIDER [--min-reasoning-effort low|medium|high] [--min-power low|medium|high] [--require-ready] [--refresh] --json"
67
+ NEXT_USAGE = "Usage: cdx next [--json] [--refresh]"
65
68
  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"
66
69
  API_SCHEMA_VERSION = 1
67
70
  HANDOFF_TRANSCRIPT_CHARS = 120000
@@ -598,6 +601,13 @@ def _parse_config_args(args):
598
601
  return {"name": parsed["names"][0], "json": parsed["json"]}
599
602
 
600
603
 
604
+ def _parse_configs_args(args):
605
+ parsed = _parse_flag_args(args, {
606
+ "--json": {"key": "json", "type": "bool", "default": False},
607
+ }, CONFIGS_USAGE)
608
+ return {"json": parsed["json"]}
609
+
610
+
601
611
  def _parse_select_args(args):
602
612
  parsed = _parse_flag_args(args, {
603
613
  "--provider": {"key": "provider", "type": "str", "default": None, "transform": lambda value: _parse_provider_filter(value, SELECT_USAGE)},
@@ -623,6 +633,14 @@ def _parse_select_args(args):
623
633
  }
624
634
 
625
635
 
636
+ def _parse_next_args(args):
637
+ parsed = _parse_flag_args(args, {
638
+ "--refresh": {"key": "refresh", "type": "bool", "default": False},
639
+ "--json": {"key": "json", "type": "bool", "default": False},
640
+ }, NEXT_USAGE)
641
+ return {"json": parsed["json"], "refresh": parsed["refresh"]}
642
+
643
+
626
644
  def _parse_timeout_seconds(value):
627
645
  try:
628
646
  parsed = float(value)
@@ -1229,70 +1247,73 @@ def handle_select(rest, ctx):
1229
1247
  return 0
1230
1248
 
1231
1249
 
1232
- def _read_run_prompt(parsed):
1233
- if parsed.get("prompt") is not None:
1234
- return parsed["prompt"]
1235
- try:
1236
- with open(parsed["prompt_file"], "r", encoding="utf-8") as handle:
1237
- return handle.read()
1238
- except OSError as error:
1239
- raise CdxError(f"Unable to read prompt file: {parsed['prompt_file']}") from error
1240
-
1241
-
1242
- def _run_cdx_error_code(error):
1243
- message = str(error)
1244
- if message.startswith("Usage:"):
1245
- return "invalid_request"
1246
- if message.startswith("Invalid cwd:"):
1247
- return "invalid_cwd"
1248
- if message.startswith("Session is disabled:"):
1249
- return "session_disabled"
1250
- if "CLI not found on PATH" in message:
1251
- return "provider_cli_not_found"
1252
- if message.startswith("Failed to start "):
1253
- return "provider_start_failed"
1254
- if (
1255
- message.startswith("Unsupported reasoning effort:")
1256
- or message.startswith("Unsupported power:")
1257
- or "--reasoning-effort and --power must match" in message
1258
- ):
1259
- return "invalid_reasoning_effort"
1260
- return "cdx_error"
1261
-
1262
-
1263
- def _run_result_payload(ok, parsed, session, run_info=None, error=None, error_source=None, error_code=None):
1264
- run_info = run_info or {}
1265
- usage = run_info.get("usage") if isinstance(run_info.get("usage"), dict) else (
1266
- extract_run_usage(session.get("provider"), run_info.get("stdout_path"))
1267
- if session else
1268
- empty_usage()
1269
- )
1270
- return {
1271
- "schema_version": API_SCHEMA_VERSION,
1272
- "ok": bool(ok),
1273
- "action": "run",
1274
- "launcher": "cdx",
1275
- "session": session.get("name") if session else None,
1276
- "provider": session.get("provider") if session else parsed.get("provider"),
1277
- "model": parsed.get("model") or ((session.get("launch") or {}).get("model") if session else None),
1278
- "reasoning_effort": parsed.get("reasoning_effort") or ((session.get("launch") or {}).get("reasoning_effort") if session else None),
1279
- "power": parsed.get("power") or ((session.get("launch") or {}).get("power") if session else None),
1280
- "cwd": os.path.abspath(parsed.get("cwd") or os.getcwd()),
1281
- "run_id": run_info.get("run_id"),
1282
- "exit_code": run_info.get("returncode"),
1283
- "duration_seconds": (run_info.get("duration_ms") / 1000.0) if run_info.get("duration_ms") is not None else None,
1284
- "transcript_path": run_info.get("transcript_path"),
1285
- "stdout_path": run_info.get("stdout_path"),
1286
- "stderr_path": run_info.get("stderr_path"),
1287
- "usage": usage,
1288
- "warnings": [],
1289
- "error": None if ok else {
1290
- "source": error_source or "cdx",
1291
- "code": error_code or "cdx_error",
1292
- "message": str(error) if error else "Run failed.",
1293
- "provider_code": run_info.get("returncode") if error_source == "provider" else None,
1294
- },
1295
- }
1250
+ def _format_next_pct(value):
1251
+ return "n/a" if value is None else f"{value}%"
1252
+
1253
+
1254
+ def _next_action(row):
1255
+ instruction = format_priority_instruction(row, "first")
1256
+ return "refresh" if instruction.startswith("refresh ") else "use"
1257
+
1258
+
1259
+ def _format_next_selection(session, row, use_color=False):
1260
+ from .cli_render import _pad_table
1261
+
1262
+ action = _next_action(row)
1263
+ command = f"cdx status {session['name']} --refresh" if action == "refresh" else f"cdx {session['name']}"
1264
+ rows = [[_style(value, "1", use_color) for value in ["SESSION", "PROVIDER", "OK", "5H", "WEEK", "REASON"]]]
1265
+ rows.append([
1266
+ _style(session["name"], "36", use_color),
1267
+ _dim(session.get("provider") or "-", use_color),
1268
+ _format_next_pct(row.get("available_pct")),
1269
+ _format_next_pct(row.get("remaining_5h_pct")),
1270
+ _format_next_pct(row.get("remaining_week_pct")),
1271
+ format_priority_instruction(row, "first"),
1272
+ ])
1273
+ return "\n".join([
1274
+ _style("Next assistant:", "1", use_color),
1275
+ _pad_table(rows),
1276
+ "",
1277
+ f"{_style('Run:', '1', use_color)} {_style(command, '36', use_color)}",
1278
+ ])
1279
+
1280
+
1281
+ def handle_next(rest, ctx):
1282
+ parsed = _parse_next_args(rest)
1283
+ rows = ctx["service"]["get_status_rows"](force_refresh=parsed["refresh"])
1284
+ priority = recommend_priority_rows(rows)
1285
+ if not priority:
1286
+ message = "No usable session status yet."
1287
+ if parsed["json"]:
1288
+ _write_json(ctx, _json_failure("next", "no_suitable_session", message, selection_policy="status_priority"))
1289
+ return 1
1290
+ ctx["out"](f"{_warn(message, ctx['use_color'])}\n")
1291
+ return 1
1292
+ row = priority[0]
1293
+ session_name = row.get("session_name")
1294
+ session = ctx["service"]["get_session"](session_name)
1295
+ if not session:
1296
+ message = f"Selected status row has no matching session: {session_name}"
1297
+ if parsed["json"]:
1298
+ _write_json(ctx, _json_failure("next", "missing_session", message, selection_policy="status_priority", row=row))
1299
+ return 1
1300
+ raise CdxError(message)
1301
+ action = _next_action(row)
1302
+ message = f"Selected next session {session['name']}"
1303
+ if parsed["json"]:
1304
+ _write_json(ctx, _json_success(
1305
+ "next",
1306
+ message,
1307
+ session=session,
1308
+ row=row,
1309
+ recommended_action=action,
1310
+ command=f"cdx status {session['name']} --refresh" if action == "refresh" else f"cdx {session['name']}",
1311
+ reason=format_priority_instruction(row, "first"),
1312
+ selection_policy="status_priority",
1313
+ ))
1314
+ return 0
1315
+ ctx["out"](f"{_format_next_selection(session, row, ctx['use_color'])}\n")
1316
+ return 0
1296
1317
 
1297
1318
 
1298
1319
  def handle_run(rest, ctx):
@@ -1327,7 +1348,7 @@ def handle_run(rest, ctx):
1327
1348
  cwd = os.path.abspath(parsed["cwd"])
1328
1349
  if not os.path.isdir(cwd):
1329
1350
  raise CdxError(f"Invalid cwd: {parsed['cwd']}")
1330
- prompt = _read_run_prompt(parsed)
1351
+ prompt = read_run_prompt(parsed)
1331
1352
  launch_updates = {}
1332
1353
  if parsed.get("model"):
1333
1354
  launch_updates["model"] = parsed["model"]
@@ -1352,6 +1373,7 @@ def handle_run(rest, ctx):
1352
1373
  stdin_is_tty=False,
1353
1374
  behavior="launch",
1354
1375
  signal_emitter=ctx.get("signal_emitter"),
1376
+ trust_local_credentials=False,
1355
1377
  )
1356
1378
  run_info = _run_headless_provider_command(
1357
1379
  run_session,
@@ -1371,7 +1393,7 @@ def handle_run(rest, ctx):
1371
1393
  "exit_code": 0,
1372
1394
  **run_info,
1373
1395
  })
1374
- _write_json(ctx, _run_result_payload(True, parsed, run_session, run_info=run_info))
1396
+ _write_json(ctx, run_result_payload(API_SCHEMA_VERSION, True, parsed, run_session, run_info=run_info))
1375
1397
  return 0
1376
1398
  message = "Provider process timed out." if run_info.get("timed_out") else "Provider process exited with a non-zero status."
1377
1399
  error = CdxError(message, run_info.get("returncode") or 1)
@@ -1382,7 +1404,8 @@ def handle_run(rest, ctx):
1382
1404
  "exit_code": error.exit_code,
1383
1405
  **run_info,
1384
1406
  })
1385
- _write_json(ctx, _run_result_payload(
1407
+ _write_json(ctx, run_result_payload(
1408
+ API_SCHEMA_VERSION,
1386
1409
  False,
1387
1410
  parsed,
1388
1411
  run_session,
@@ -1394,28 +1417,102 @@ def handle_run(rest, ctx):
1394
1417
  return error.exit_code or 1
1395
1418
  except CdxError as error:
1396
1419
  run_info = getattr(error, "run_info", None)
1397
- _write_json(ctx, _run_result_payload(
1420
+ _write_json(ctx, run_result_payload(
1421
+ API_SCHEMA_VERSION,
1398
1422
  False,
1399
1423
  locals().get("parsed", {}) or {},
1400
1424
  locals().get("session"),
1401
1425
  run_info=run_info,
1402
1426
  error=error,
1403
1427
  error_source="cdx",
1404
- error_code=_run_cdx_error_code(error),
1428
+ error_code=run_cdx_error_code(error),
1405
1429
  ))
1406
1430
  return error.exit_code
1407
1431
 
1408
1432
 
1409
- def _format_launch_config(session):
1433
+ def _format_launch_config(session, use_color=False):
1434
+ from .cli_render import _dim, _pad_table, _style
1435
+
1410
1436
  launch = session.get("launch") or {}
1437
+ rows = [[_style("SETTING", "1", use_color), _style("VALUE", "1", use_color)]]
1438
+ for key, label in [
1439
+ ("power", "Power"),
1440
+ ("permission", "Permission"),
1441
+ ("fast", "Fast"),
1442
+ ("rtk", "RTK"),
1443
+ ("model", "Model"),
1444
+ ("priority", "Priority"),
1445
+ ]:
1446
+ rows.append([
1447
+ _dim(label, use_color),
1448
+ _format_launch_setting_value(launch, key, use_color=use_color),
1449
+ ])
1450
+ provider_label = f"({session['provider']})"
1411
1451
  return "\n".join([
1412
- f"{session['name']} ({session['provider']})",
1413
- f"power: {launch.get('power') or 'default'}",
1414
- f"permission: {launch.get('permission') or 'default'}",
1415
- f"fast: {'on' if launch.get('fast') is True else 'off' if launch.get('fast') is False else 'default'}",
1416
- f"rtk: {'on' if launch.get('rtk') is True else 'off' if launch.get('rtk') is False else 'default'}",
1417
- f"model: {launch.get('model') or 'default'}",
1418
- f"priority: {launch.get('priority') if launch.get('priority') is not None else 'default'}",
1452
+ _style("Launch settings:", "1", use_color),
1453
+ f"{_style(session['name'], '36', use_color)} {_dim(provider_label, use_color)}",
1454
+ _pad_table(rows),
1455
+ "",
1456
+ _dim(_format_launch_settings_hint(session["name"]), use_color),
1457
+ ])
1458
+
1459
+
1460
+ def _format_launch_settings_hint(name="<name>"):
1461
+ return (
1462
+ f"Set a value: cdx set {name} --power medium --permission auto "
1463
+ "--fast on --rtk on --model MODEL --priority 80"
1464
+ )
1465
+
1466
+
1467
+ def _format_launch_setting_value(launch, key, use_color=False):
1468
+ if key == "fast" or key == "rtk":
1469
+ if launch.get(key) is True:
1470
+ return _style("on", "32", use_color)
1471
+ if launch.get(key) is False:
1472
+ return _style("off", "2", use_color)
1473
+ return _dim("default", use_color)
1474
+ value = launch.get(key)
1475
+ if value is None or value == "":
1476
+ return _dim("default", use_color)
1477
+ if key == "priority":
1478
+ return _style(str(value), "33", use_color)
1479
+ if key == "power":
1480
+ return _style(str(value), "96", use_color)
1481
+ if key == "permission":
1482
+ return _style(str(value), "32", use_color)
1483
+ return str(value)
1484
+
1485
+
1486
+ def _format_launch_configs(sessions, use_color=False):
1487
+ from .cli_render import _dim, _pad_table, _style
1488
+
1489
+ if not sessions:
1490
+ return "\n".join([
1491
+ _style("Launch settings:", "1", use_color),
1492
+ "No sessions.",
1493
+ "",
1494
+ _dim(_format_launch_settings_hint(), use_color),
1495
+ ])
1496
+ rows = [[_style(value, "1", use_color) for value in [
1497
+ "SESSION", "PROVIDER", "POWER", "PERMISSION", "FAST", "RTK", "MODEL", "PRIORITY"
1498
+ ]]]
1499
+ for session in sessions:
1500
+ launch = session.get("launch") or {}
1501
+ rows.append([
1502
+ _style(session["name"], "36", use_color),
1503
+ _dim(session.get("provider") or "-", use_color),
1504
+ _format_launch_setting_value(launch, "power", use_color),
1505
+ _format_launch_setting_value(launch, "permission", use_color),
1506
+ _format_launch_setting_value(launch, "fast", use_color),
1507
+ _format_launch_setting_value(launch, "rtk", use_color),
1508
+ _format_launch_setting_value(launch, "model", use_color),
1509
+ _format_launch_setting_value(launch, "priority", use_color),
1510
+ ])
1511
+ return "\n".join([
1512
+ _style("Launch settings:", "1", use_color),
1513
+ _pad_table(rows),
1514
+ "",
1515
+ _dim(_format_launch_settings_hint(), use_color),
1419
1516
  ])
1420
1517
 
1421
1518
 
@@ -1825,7 +1922,7 @@ def _apply_launch_settings(parsed, ctx, action="set"):
1825
1922
  return 0
1826
1923
  ctx["out"](f"{_success(message, ctx['use_color'])}\n")
1827
1924
  if len(sessions) == 1:
1828
- ctx["out"](f"{_format_launch_config(sessions[0])}\n")
1925
+ ctx["out"](f"{_format_launch_config(sessions[0], ctx['use_color'])}\n")
1829
1926
  return 0
1830
1927
 
1831
1928
 
@@ -1853,7 +1950,7 @@ def _clear_launch_settings(parsed, ctx, action="unset"):
1853
1950
  return 0
1854
1951
  ctx["out"](f"{_success(message, ctx['use_color'])}\n")
1855
1952
  if len(sessions) == 1:
1856
- ctx["out"](f"{_format_launch_config(sessions[0])}\n")
1953
+ ctx["out"](f"{_format_launch_config(sessions[0], ctx['use_color'])}\n")
1857
1954
  return 0
1858
1955
 
1859
1956
 
@@ -1881,7 +1978,18 @@ def handle_config(rest, ctx):
1881
1978
  if parsed["json"]:
1882
1979
  _write_json(ctx, _json_success("config", message, session=session, launch=session.get("launch") or {}))
1883
1980
  return 0
1884
- ctx["out"](f"{_format_launch_config(session)}\n")
1981
+ ctx["out"](f"{_format_launch_config(session, ctx['use_color'])}\n")
1982
+ return 0
1983
+
1984
+
1985
+ def handle_configs(rest, ctx):
1986
+ parsed = _parse_configs_args(rest)
1987
+ sessions = ctx["service"]["list_sessions"]()
1988
+ message = f"Listed launch settings for {len(sessions)} session{'s' if len(sessions) != 1 else ''}"
1989
+ if parsed["json"]:
1990
+ _write_json(ctx, _json_success("configs", message, count=len(sessions), sessions=sessions))
1991
+ return 0
1992
+ ctx["out"](f"{_format_launch_configs(sessions, ctx['use_color'])}\n")
1885
1993
  return 0
1886
1994
 
1887
1995
 
package/src/cli_render.py CHANGED
@@ -146,6 +146,9 @@ def _format_sessions(service, use_color=False):
146
146
  lines += [
147
147
  _style("Next actions:", "1", use_color),
148
148
  f" {_style('cdx status', '36', use_color)}",
149
+ f" {_style('cdx next', '36', use_color)}",
150
+ f" {_style('cdx configs', '36', use_color)}",
151
+ f" {_style('cdx stats', '36', use_color)}",
149
152
  f" {_style('cdx ready', '36', use_color)}",
150
153
  f" {_style('cdx perm all default', '36', use_color)}",
151
154
  f" {_style('cdx handoff <source> <target>', '36', use_color)}",
@@ -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
+ }
@@ -23,19 +23,26 @@ RESERVED_SESSION_NAMES = {
23
23
  "add",
24
24
  "clean",
25
25
  "context",
26
+ "configs",
26
27
  "cp",
27
28
  "disable",
28
29
  "doctor",
29
30
  "enable",
30
31
  "export",
32
+ "fast",
31
33
  "help",
32
34
  "handoff",
33
35
  "history",
34
36
  "import",
37
+ "last",
35
38
  "login",
36
39
  "logout",
40
+ "model",
37
41
  "mv",
42
+ "next",
38
43
  "notify",
44
+ "perm",
45
+ "power",
39
46
  "ready",
40
47
  "repair",
41
48
  "ren",
@@ -45,6 +52,7 @@ RESERVED_SESSION_NAMES = {
45
52
  "select",
46
53
  "config",
47
54
  "set",
55
+ "stats",
48
56
  "status",
49
57
  "unset",
50
58
  "update",
@@ -63,6 +71,10 @@ LAUNCH_PERMISSION_VALUES = {"review", "default", "auto", "full"}
63
71
  MAX_LAUNCH_MODEL_LENGTH = 128
64
72
  MIN_LAUNCH_PRIORITY = 0
65
73
  MAX_LAUNCH_PRIORITY = 100
74
+ DEFAULT_LAUNCH_SETTINGS = {
75
+ "power": "medium",
76
+ "fast": False,
77
+ }
66
78
 
67
79
 
68
80
  def _encode(name):
@@ -450,6 +462,8 @@ def create_session_service(options=None):
450
462
  raise CdxError("Session name is required")
451
463
  if str(name) != str(name).strip():
452
464
  raise CdxError("Session name cannot start or end with whitespace")
465
+ if str(name) in (".", ".."):
466
+ raise CdxError("Session name cannot be . or ..")
453
467
  if len(str(name)) > MAX_SESSION_NAME_LENGTH:
454
468
  raise CdxError(f"Session name is too long (max {MAX_SESSION_NAME_LENGTH} characters)")
455
469
  if any(ord(ch) < 32 or ord(ch) == 127 for ch in str(name)):
@@ -544,6 +558,7 @@ def create_session_service(options=None):
544
558
  "lastLaunchedAt": None,
545
559
  "lastStatusAt": None,
546
560
  "lastStatus": None,
561
+ "launch": dict(DEFAULT_LAUNCH_SETTINGS),
547
562
  "auth": {
548
563
  "status": "unknown",
549
564
  "lastCheckedAt": None,
@@ -1117,7 +1132,7 @@ def create_session_service(options=None):
1117
1132
  bundle_bytes = encode_bundle(payload, include_auth=include_auth, passphrase=passphrase)
1118
1133
  if progress_callback:
1119
1134
  progress_callback({"event": "writing_started", "path": file_path, "bundle_size_bytes": len(bundle_bytes)})
1120
- _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)
1121
1136
  with open(file_path, "wb") as handle:
1122
1137
  handle.write(bundle_bytes)
1123
1138
  if sys.platform != "win32":
@@ -87,15 +87,11 @@ def _format_status_rows(rows, use_color=False, small=False):
87
87
  return "SESSION STATUS OK 5H WEEK BLOCK CR RESET 5H RESET WEEK UPDATED\nNo saved sessions yet."
88
88
  headers = [_style(header, "1", use_color) for header in headers]
89
89
  active_rows = [r for r in rows if r.get("enabled", True) is not False]
90
- priority_candidates = [
91
- r for r in active_rows
92
- if _format_auth_status(r) != "logged out"
93
- ]
90
+ priority = recommend_priority_rows(rows)
94
91
  disabled_rows = sorted(
95
92
  [r for r in rows if r.get("enabled", True) is False],
96
93
  key=lambda r: r.get("session_name") or "",
97
94
  )
98
- priority = _recommend_priority_sessions(priority_candidates)
99
95
  priority_names = {r.get("session_name") for r in priority}
100
96
  non_priority_active = [
101
97
  r for r in active_rows
@@ -180,6 +176,19 @@ def _format_auth_status(row):
180
176
  return "unknown"
181
177
 
182
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
+
183
192
  def _recommend_priority_sessions(rows):
184
193
  if not rows:
185
194
  return []