cdx-manager 0.10.0 → 0.11.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 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.10.0-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.11.0-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
 
@@ -79,8 +79,6 @@ One command to launch any session. Zero auth juggling.
79
79
  - Auth probe: synchronous subprocess call to `codex login status` or `claude auth status` before any interactive launch.
80
80
  - Signal forwarding: `SIGINT`, `SIGTERM`, and `SIGHUP` are forwarded to the child process and produce clean exit codes.
81
81
  - Test stack: `pytest`, `pytest-cov`, and `ruff` via the documented `dev` dependency extra.
82
- - Python floor decision: `cdx-manager` keeps Python 3.9 support for the 0.9.x line to avoid dropping existing users before a planned compatibility break; classifiers remain aligned with `requires-python = ">=3.9"`.
83
-
84
82
  ---
85
83
 
86
84
  ## Getting Started
@@ -194,18 +192,18 @@ npm install -g .
194
192
 
195
193
  Security note:
196
194
 
197
- - The standalone installers try to resolve official release checksums from `checksums/release-archives.json`.
195
+ - The standalone installers resolve official release checksums from the tagged GitHub Release asset `release-archives.json`.
198
196
  - You can still override verification explicitly through `CDX_SHA256`.
199
- - If no checksum is available, standalone installers fail closed unless `CDX_ALLOW_UNVERIFIED=1` is set.
197
+ - If no checksum is available, standalone installers fail closed unless `CDX_ALLOW_UNVERIFIED=1` is set; that override prints a prominent warning before continuing.
200
198
  - Prefer `npm`, `pipx`, or `uv` when you want registry-backed install flows.
201
- - If you use the standalone script, download it first, inspect it, and prefer a release with an official checksum entry.
199
+ - If you use the standalone script, download it first, inspect it, and prefer a release with the official checksum asset attached.
202
200
 
203
201
  Release maintainer note:
204
202
 
205
203
  - Before publishing npm or PyPI packages, run `npm run release:validate`.
206
204
  - The release tag must match `package.json`, `pyproject.toml`, `src/cli.py`, and `VERSION`.
207
205
  - `checksums/release-archives.json` must include the matching `vX.Y.Z` entry with both `github_tarball_sha256` and `github_zip_sha256`.
208
- - Use `python3 scripts/update_release_checksums.py --tag vX.Y.Z` after the GitHub tag archives exist, commit the checksum update to `main`, then publish the GitHub release only after `npm run release:validate`, `npm run lint`, and `npm test` pass.
206
+ - Use `python3 scripts/update_release_checksums.py --tag vX.Y.Z` after the GitHub tag archives exist, commit the checksum update, attach that file to the tagged GitHub Release as `release-archives.json`, then publish packages only after `npm run release:validate`, `npm run lint`, and `npm test` pass.
209
207
 
210
208
  ### Environment
211
209
 
@@ -523,7 +521,7 @@ Notes:
523
521
  - `--include-auth` is encrypted, requires a passphrase, and exports only provider credential files rather than full profile caches or logs.
524
522
  - Without `--passphrase-env`, `cdx` prompts in an interactive terminal. Non-interactive callers can pass `--passphrase-stdin` to read the passphrase from stdin (one line) instead of exposing it in the child environment.
525
523
  - `--sessions work,perso` exports or imports only a subset.
526
- - `--force` allows overwriting existing destination sessions during import or replacing an existing bundle file during export.
524
+ - `--force` allows overwriting existing destination sessions during import or replacing an existing bundle file during export. Import refuses to overwrite existing sessions from a bundle without auth payloads unless `--allow-authless-force` is also passed.
527
525
  - Auth bundles contain credentials. Treat them like secrets and delete them after transfer.
528
526
 
529
527
  ---
package/bin/cdx CHANGED
@@ -3,21 +3,12 @@
3
3
  import sys
4
4
  from pathlib import Path
5
5
 
6
-
7
6
  ROOT = Path(__file__).resolve().parent.parent
8
7
  if str(ROOT) not in sys.path:
9
8
  sys.path.insert(0, str(ROOT))
10
9
 
11
- from src.cli import format_error, format_json_error, main, wants_json # noqa: E402
12
- from src.errors import CdxError # noqa: E402
10
+ from src.cli import cli_entry # noqa: E402
13
11
 
14
12
 
15
13
  if __name__ == "__main__":
16
- try:
17
- raise SystemExit(main(sys.argv[1:]))
18
- except CdxError as error:
19
- if wants_json(sys.argv[1:]):
20
- sys.stderr.write(f"{format_json_error(error)}\n")
21
- else:
22
- sys.stderr.write(f"{format_error(error)}\n")
23
- raise SystemExit(error.exit_code)
14
+ cli_entry()
@@ -0,0 +1,51 @@
1
+ # CDX Manager 0.11.0
2
+
3
+ ## Highlights
4
+
5
+ - Fixed Codex structured status parsing so five-hour and weekly reset windows stay mapped to the correct columns.
6
+ - Hardened backup import, provider auth probing, release verification, and Windows launcher generation.
7
+ - Closed the July 2026 review remediation corpus and follow-up Logics workflow with release-ready validation.
8
+
9
+ ## Changes
10
+
11
+ ### Codex status reset windows
12
+
13
+ Structured Codex status payloads now preserve the distinction between the five-hour and weekly reset timestamps. `cdx status` no longer mirrors the weekly reset into the five-hour reset column, or the reverse, when provider output includes both windows.
14
+
15
+ ### Safer session import and credentials handling
16
+
17
+ `cdx import --force` refuses to overwrite existing sessions from bundles without auth payloads unless `--allow-authless-force` is explicitly supplied.
18
+
19
+ Bundle import now validates selected profile paths, base64 payloads, malformed profile collections, and non-object profile entries before touching existing session profiles. Existing profiles are renamed aside during force import and restored if a per-session import fails.
20
+
21
+ ### Provider auth reliability
22
+
23
+ Provider auth probes are bounded by a 15 second timeout. Timeouts now surface as a degraded authentication state instead of being persisted as `logged_out`.
24
+
25
+ Codex interactive launches wait on the per-auth-home lock with a bounded retry instead of running unlocked while another process may be rotating OAuth credentials.
26
+
27
+ Stored `reasoning_effort` and `power` launch settings now clear each other correctly, so setting one cannot leave the other silently shadowing it.
28
+
29
+ ### CLI robustness and release hardening
30
+
31
+ `cdx history` skips corrupt JSONL lines, `cdx status` tolerates sessions removed during a concurrent scan, `cdx clean profiles` routes profile cleanup flags consistently, `cdx disk --candidates` validates arguments before scanning, and `cdx view --json` now reports a proper failure when the Logics viewer is unavailable.
32
+
33
+ The npm launcher routes through the same `cli_entry()` path as the Python entrypoint. Standalone installers and publish workflows now use tagged GitHub Release checksum assets rather than main-branch checksum metadata, and `CDX_ALLOW_UNVERIFIED=1` prints a prominent warning before continuing.
34
+
35
+ The standalone Windows installer now generates `cdx.cmd` with the installed version directory in the launcher path.
36
+
37
+ ### Logics documentation
38
+
39
+ The July 2026 review remediation corpus and post-review follow-up corpus are closed with implementation reports, validation evidence, and a clean Logics audit.
40
+
41
+ ## Validation
42
+
43
+ - `npm run release:validate`
44
+ - `npm run lint`
45
+ - `npm test`
46
+ - `logics-manager lint --require-status`
47
+ - `logics-manager audit`
48
+ - `git diff --check`
49
+ - `npm --cache /private/tmp/cdx-npm-cache pack --dry-run`
50
+ - `python -m build`
51
+ - `python -m twine check dist/*`
@@ -132,6 +132,10 @@
132
132
  "v0.9.16": {
133
133
  "github_tarball_sha256": "0876509c201485824f6e5fe7b4a96ceb41cc68279fbe6dda4891864cf5be93ec",
134
134
  "github_zip_sha256": "08072957a0421d257cecfa8bed73cceee80d1c8857c59ac45378ac8ce3e0362d"
135
+ },
136
+ "v0.10.0": {
137
+ "github_tarball_sha256": "20c473f4b17806d565c454ad4aa28758433665d8431251ebb937ab6a909e4adb",
138
+ "github_zip_sha256": "b6a7be359d8dc2a11d8c77749878875419fabd7cd5a6951502dec09d7791e211"
135
139
  }
136
140
  }
137
141
  }
package/install.ps1 CHANGED
@@ -9,13 +9,7 @@ $ErrorActionPreference = "Stop"
9
9
 
10
10
  $repo = "AlexAgo83/cdx-manager"
11
11
  if (-not $ChecksumsUrl) {
12
- $ChecksumsUrl = "https://raw.githubusercontent.com/$repo/main/checksums/release-archives.json"
13
- }
14
- $defaultChecksumsUrl = "https://raw.githubusercontent.com/$repo/main/checksums/release-archives.json"
15
- $checksumsApiUrl = if ($env:CDX_CHECKSUMS_API_URL) {
16
- $env:CDX_CHECKSUMS_API_URL
17
- } else {
18
- "https://api.github.com/repos/$repo/contents/checksums/release-archives.json?ref=main"
12
+ $ChecksumsUrl = $null
19
13
  }
20
14
 
21
15
  function Has-Command {
@@ -45,10 +39,16 @@ if ($Version.StartsWith("v")) {
45
39
  $tag = "v$Version"
46
40
  }
47
41
 
42
+ $defaultChecksumsUrl = "https://github.com/$repo/releases/download/$tag/release-archives.json"
43
+ if (-not $ChecksumsUrl) {
44
+ $ChecksumsUrl = $defaultChecksumsUrl
45
+ }
46
+
48
47
  $tmpRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("cdx-install-" + [guid]::NewGuid().ToString("N"))
49
48
  $archivePath = Join-Path $tmpRoot "cdx-manager.zip"
50
49
  $extractRoot = Join-Path $tmpRoot "extract"
51
- $targetDir = Join-Path $installRoot $tag.TrimStart("v")
50
+ $versionDir = $tag.TrimStart("v")
51
+ $targetDir = Join-Path $installRoot $versionDir
52
52
  $archiveUrl = "https://github.com/$repo/archive/refs/tags/$tag.zip"
53
53
 
54
54
  New-Item -ItemType Directory -Force -Path $tmpRoot, $extractRoot, $binDir, $installRoot | Out-Null
@@ -62,15 +62,6 @@ try {
62
62
  } catch {
63
63
  }
64
64
  }
65
- if ((-not $Sha256) -and ($ChecksumsUrl -eq $defaultChecksumsUrl)) {
66
- try {
67
- $checksumsResponse = Invoke-RestMethod -Uri $checksumsApiUrl
68
- $checksumsJson = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($checksumsResponse.content))
69
- $checksums = $checksumsJson | ConvertFrom-Json
70
- $Sha256 = $checksums.releases.$tag.github_zip_sha256
71
- } catch {
72
- }
73
- }
74
65
  if ($Sha256) {
75
66
  $actualSha256 = (Get-FileHash -Algorithm SHA256 -Path $archivePath).Hash.ToLowerInvariant()
76
67
  if ($actualSha256 -ne $Sha256.ToLowerInvariant()) {
@@ -78,7 +69,9 @@ try {
78
69
  }
79
70
  } else {
80
71
  if ($env:CDX_ALLOW_UNVERIFIED -eq "1") {
81
- Write-Warning "No official checksum available for $tag; continuing because CDX_ALLOW_UNVERIFIED=1."
72
+ Write-Warning "Installing $tag without checksum verification."
73
+ Write-Warning "CDX_ALLOW_UNVERIFIED=1 disables archive integrity checks."
74
+ Write-Warning "Continue only if you trust the download source."
82
75
  } else {
83
76
  throw "cdx install: no official checksum available for $tag. Set CDX_ALLOW_UNVERIFIED=1 to install without checksum verification."
84
77
  }
@@ -99,7 +92,7 @@ try {
99
92
  $launcherPath = Join-Path $binDir "cdx.cmd"
100
93
  $launcher = @"
101
94
  @echo off
102
- set SCRIPT=%~dp0..\versions\${($tag.TrimStart("v"))}\bin\cdx
95
+ set SCRIPT=%~dp0..\versions\$versionDir\bin\cdx
103
96
  where py >nul 2>nul
104
97
  if %ERRORLEVEL%==0 (
105
98
  py -3 "%SCRIPT%" %*
package/install.sh CHANGED
@@ -6,9 +6,6 @@ VERSION="${CDX_VERSION:-}"
6
6
  PREFIX="${PREFIX:-$HOME/.local}"
7
7
  BIN_DIR="${BIN_DIR:-$PREFIX/bin}"
8
8
  INSTALL_ROOT="${CDX_INSTALL_ROOT:-$PREFIX/share/cdx-manager}"
9
- DEFAULT_CHECKSUMS_URL="https://raw.githubusercontent.com/$REPO/main/checksums/release-archives.json"
10
- CHECKSUMS_URL="${CDX_CHECKSUMS_URL:-$DEFAULT_CHECKSUMS_URL}"
11
- CHECKSUMS_API_URL="${CDX_CHECKSUMS_API_URL:-https://api.github.com/repos/$REPO/contents/checksums/release-archives.json?ref=main}"
12
9
 
13
10
  need() {
14
11
  if ! command -v "$1" >/dev/null 2>&1; then
@@ -53,27 +50,6 @@ if value:
53
50
  ' "$1"
54
51
  }
55
52
 
56
- resolve_expected_sha256_from_api() {
57
- curl -fsSL "$CHECKSUMS_API_URL" |
58
- python3 -c '
59
- import base64
60
- import json
61
- import sys
62
-
63
- tag = sys.argv[1]
64
- try:
65
- response = json.load(sys.stdin)
66
- payload = json.loads(base64.b64decode(response.get("content") or b"").decode("utf-8"))
67
- except Exception:
68
- raise SystemExit(1)
69
-
70
- release = (payload.get("releases") or {}).get(tag) or {}
71
- value = release.get("github_tarball_sha256")
72
- if value:
73
- print(value)
74
- ' "$1"
75
- }
76
-
77
53
  if [ -z "$VERSION" ]; then
78
54
  VERSION="$(
79
55
  curl -fsSL "https://api.github.com/repos/$REPO/releases/latest" |
@@ -86,6 +62,9 @@ case "$VERSION" in
86
62
  *) TAG="v$VERSION" ;;
87
63
  esac
88
64
 
65
+ DEFAULT_CHECKSUMS_URL="https://github.com/$REPO/releases/download/$TAG/release-archives.json"
66
+ CHECKSUMS_URL="${CDX_CHECKSUMS_URL:-$DEFAULT_CHECKSUMS_URL}"
67
+
89
68
  TMP_DIR="$(mktemp -d)"
90
69
  cleanup() {
91
70
  rm -rf "$TMP_DIR"
@@ -99,10 +78,6 @@ EXPECTED_SHA256="${CDX_SHA256:-}"
99
78
  if [ -z "$EXPECTED_SHA256" ]; then
100
79
  EXPECTED_SHA256="$(resolve_expected_sha256 "$TAG" 2>/dev/null || true)"
101
80
  fi
102
- if [ -z "$EXPECTED_SHA256" ] && [ "$CHECKSUMS_URL" = "$DEFAULT_CHECKSUMS_URL" ]; then
103
- EXPECTED_SHA256="$(resolve_expected_sha256_from_api "$TAG" 2>/dev/null || true)"
104
- fi
105
-
106
81
  if [ -n "$EXPECTED_SHA256" ]; then
107
82
  ACTUAL_SHA256="$(sha256_file "$TMP_DIR/cdx-manager.tar.gz")"
108
83
  if [ "$ACTUAL_SHA256" != "$EXPECTED_SHA256" ]; then
@@ -113,7 +88,9 @@ if [ -n "$EXPECTED_SHA256" ]; then
113
88
  fi
114
89
  else
115
90
  if [ "${CDX_ALLOW_UNVERIFIED:-}" = "1" ]; then
116
- echo "cdx install: warning: no official checksum available for $TAG; continuing because CDX_ALLOW_UNVERIFIED=1" >&2
91
+ echo "cdx install: WARNING: installing $TAG without checksum verification." >&2
92
+ echo "cdx install: WARNING: CDX_ALLOW_UNVERIFIED=1 disables archive integrity checks." >&2
93
+ echo "cdx install: WARNING: continue only if you trust the download source." >&2
117
94
  else
118
95
  echo "cdx install: no official checksum available for $TAG" >&2
119
96
  echo "Set CDX_ALLOW_UNVERIFIED=1 to install without checksum verification." >&2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cdx-manager",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
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.10.0"
7
+ version = "0.11.0"
8
8
  description = "Terminal session manager for Codex and Claude accounts."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -48,40 +48,24 @@ def _refresh_claude_sessions(service, refresh_fn=None, target_names=None, force=
48
48
  errors = []
49
49
  results = {}
50
50
 
51
- if inspect.iscoroutinefunction(refresh_fn):
52
- import asyncio
51
+ threads = []
53
52
 
54
- async def fetch_all():
55
- async def fetch_async(s):
56
- try:
57
- usage = await refresh_fn(s)
58
- if usage:
59
- results[s["name"]] = usage
60
- except Exception as e:
61
- errors.append({"session": s["name"], "error": e})
62
-
63
- await asyncio.gather(*(fetch_async(s) for s in claude_sessions))
64
-
65
- asyncio.run(fetch_all())
66
- else:
67
- threads = []
68
-
69
- def fetch(s):
70
- try:
71
- usage = refresh_fn(s)
72
- if inspect.isawaitable(usage):
73
- raise CdxError("Claude refresh function returned an awaitable from a sync callable.")
74
- if usage:
75
- results[s["name"]] = usage
76
- except Exception as e:
77
- errors.append({"session": s["name"], "error": e})
78
-
79
- for s in claude_sessions:
80
- t = threading.Thread(target=fetch, args=(s,), daemon=True)
81
- threads.append(t)
82
- t.start()
83
- for t in threads:
84
- t.join(timeout=10)
53
+ def fetch(s):
54
+ try:
55
+ usage = refresh_fn(s)
56
+ if inspect.isawaitable(usage):
57
+ raise CdxError("Claude refresh function returned an awaitable from a sync callable.")
58
+ if usage:
59
+ results[s["name"]] = usage
60
+ except Exception as e:
61
+ errors.append({"session": s["name"], "error": e})
62
+
63
+ for s in claude_sessions:
64
+ t = threading.Thread(target=fetch, args=(s,), daemon=True)
65
+ threads.append(t)
66
+ t.start()
67
+ for t in threads:
68
+ t.join(timeout=10)
85
69
 
86
70
  # A worker that outlived its join can still insert into results/errors;
87
71
  # snapshot both so late writes can't mutate what we iterate.
package/src/cli.py CHANGED
@@ -53,6 +53,7 @@ from .cli_commands import (
53
53
  from .cli_commands import (
54
54
  _format_bytes as _format_disk_bytes,
55
55
  )
56
+ from .cli_helpers import _update_notice_warnings
56
57
  from .cli_render import (
57
58
  _format_sessions,
58
59
  _pad_table,
@@ -75,7 +76,7 @@ from .status_view import (
75
76
  )
76
77
  from .update_check import check_for_update, check_logics_manager_for_update
77
78
 
78
- VERSION = "0.10.0"
79
+ VERSION = "0.11.0"
79
80
 
80
81
  # Public surface: this module is a facade. Names below are imported above
81
82
  # purely to be re-exported (consumed by tests and external callers); listing
@@ -178,7 +179,7 @@ def _print_help(use_color=False):
178
179
  f" {_style('cdx next [--json] [--refresh]', '36', use_color)}",
179
180
  f" {_style('cdx reset <name> [--yes] [--json]', '36', use_color)}",
180
181
  f" {_style('cdx select --provider PROVIDER [--min-reasoning-effort minimal|low|medium|high|xhigh] [--min-power minimal|low|medium|high|xhigh] [--require-ready] [--refresh] --json', '36', use_color)}",
181
- f" {_style('cdx run [session] --cwd PATH (--prompt-file PATH|--prompt TEXT) [--provider PROVIDER] [--model MODEL] [--reasoning-effort minimal|low|medium|high|xhigh] [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full|workspace-write|read-only|danger-full-access] [--timeout-seconds N] --json', '36', use_color)}",
182
+ f" {_style('cdx run [session] --cwd PATH (--prompt-file PATH|--prompt TEXT) [--provider PROVIDER] [--model MODEL] [--kind assistant|code-review] [--reasoning-effort minimal|low|medium|high|xhigh] [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full|workspace-write|read-only|danger-full-access] [--timeout-seconds N] --json', '36', use_color)}",
182
183
  f" {_style('cdx runs [--limit N] --json', '36', use_color)}",
183
184
  f" {_style('cdx run-status <run_id> --json', '36', use_color)}",
184
185
  f" {_style('cdx run-report <run_id> --json', '36', use_color)}",
@@ -426,40 +427,8 @@ def _get_disk_cleanup_notice(service, options):
426
427
  }
427
428
 
428
429
 
429
- def _update_warning_payload(notices):
430
- if isinstance(notices, dict):
431
- notices = [notices]
432
- if not notices:
433
- return []
434
- warnings = []
435
- for notice in notices:
436
- if notice.get("code") == "disk_cleanup_available":
437
- warnings.append({
438
- "code": "disk_cleanup_available",
439
- "message": notice["message"],
440
- **{key: value for key, value in notice.items() if key not in ("code", "message")},
441
- })
442
- continue
443
- tool = notice.get("tool") or "cdx-manager"
444
- current = notice.get("current_version") or VERSION
445
- command = notice.get("update_command") or ("cdx update" if tool == "cdx-manager" else None)
446
- message = f"Update available: {tool} {notice['latest_version']} (current {current})"
447
- if command:
448
- message = f"{message}. Run: {command}"
449
- warnings.append({
450
- "code": "update_available" if tool == "cdx-manager" else f"{tool.replace('-', '_')}_update_available",
451
- "message": message,
452
- "tool": tool,
453
- "latest_version": notice["latest_version"],
454
- "current_version": current,
455
- "update_command": command,
456
- "url": notice.get("url"),
457
- })
458
- return warnings
459
-
460
-
461
430
  def _update_warning_text(notices):
462
- payloads = _update_warning_payload(notices)
431
+ payloads = _update_notice_warnings({"update_notices": notices or [], "version": VERSION})
463
432
  if not payloads:
464
433
  return None
465
434
  return "\n".join(payload["message"] for payload in payloads)
@@ -571,7 +540,7 @@ def _list_json_payload(rows, notices=None):
571
540
  "ok": True,
572
541
  "action": "list",
573
542
  "message": "Listed known sessions",
574
- "warnings": _update_warning_payload(notices),
543
+ "warnings": _update_notice_warnings({"update_notices": notices or [], "version": VERSION}),
575
544
  "sessions": rows,
576
545
  }
577
546
 
package/src/cli_args.py CHANGED
@@ -17,11 +17,11 @@ DISK_USAGE = "Usage: cdx disk [profiles] [--candidates] [--json]"
17
17
  REPAIR_USAGE = "Usage: cdx repair [--dry-run] [--force] [--json]"
18
18
  UPDATE_USAGE = "Usage: cdx update [--check] [--yes] [--json] [--version TAG]"
19
19
  EXPORT_USAGE = "Usage: cdx export <file> [--include-auth] [--force] [--json] [--sessions name1,name2] [--passphrase-env VAR|--passphrase-stdin]"
20
- IMPORT_USAGE = "Usage: cdx import <file> [--force|--merge] [--json] [--sessions name1,name2] [--passphrase-env VAR|--passphrase-stdin]"
20
+ IMPORT_USAGE = "Usage: cdx import <file> [--force|--merge] [--allow-authless-force] [--json] [--sessions name1,name2] [--passphrase-env VAR|--passphrase-stdin]"
21
21
  CONTEXT_USAGE = "Usage: cdx context show|path|init|edit|clear|set [text...] [--json]"
22
22
  HANDOFF_USAGE = "Usage: cdx handoff <name> [--json] | cdx handoff <source> <target> [--json]"
23
23
  SET_USAGE = "Usage: cdx set <name>|--sessions all|a,b|--provider PROVIDER [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full] [--fast on|off] [--rtk on|off] [--logics on|off] [--model MODEL] [--priority 0..100] [--json]"
24
- UNSET_USAGE = "Usage: cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--permission|--fast|--rtk|--logics|--model|--priority|--all) [--json]"
24
+ UNSET_USAGE = "Usage: cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--reasoning-effort|--permission|--fast|--rtk|--logics|--model|--priority|--all) [--json]"
25
25
  SETTING_ALIAS_USAGE = "Usage: cdx power|perm|fast|model <name|all|provider:PROVIDER|a,b> <value|default> [--json]"
26
26
  CONFIG_USAGE = "Usage: cdx config <name> [--json]"
27
27
  CONFIGS_USAGE = "Usage: cdx configs [--json]"
@@ -186,6 +186,7 @@ def _parse_set_args(args):
186
186
  def _parse_unset_args(args):
187
187
  parsed = _parse_flag_args(args, {
188
188
  "--power": {"key": "power", "type": "bool", "default": False},
189
+ "--reasoning-effort": {"key": "reasoning_effort", "type": "bool", "default": False},
189
190
  "--permission": {"key": "permission", "type": "bool", "default": False},
190
191
  "--fast": {"key": "fast", "type": "bool", "default": False},
191
192
  "--rtk": {"key": "rtk", "type": "bool", "default": False},
@@ -207,8 +208,8 @@ def _parse_unset_args(args):
207
208
  raise CdxError(UNSET_USAGE)
208
209
  if not parsed["names"] and not parsed["sessions"] and not parsed["provider"]:
209
210
  raise CdxError(UNSET_USAGE)
210
- keys = ["power", "permission", "fast", "rtk", "logics", "model", "priority"] if parsed["all"] else [
211
- key for key in ("power", "permission", "fast", "rtk", "logics", "model", "priority") if parsed[key]
211
+ keys = ["power", "reasoning_effort", "permission", "fast", "rtk", "logics", "model", "priority"] if parsed["all"] else [
212
+ key for key in ("power", "reasoning_effort", "permission", "fast", "rtk", "logics", "model", "priority") if parsed[key]
212
213
  ]
213
214
  if not keys:
214
215
  raise CdxError(UNSET_USAGE)
@@ -405,18 +406,18 @@ def _format_period_datetime(value):
405
406
  return value.isoformat(timespec="seconds")
406
407
 
407
408
 
408
- def _parse_history_period(parsed, now):
409
+ def _parse_history_period(parsed, now, usage=HISTORY_USAGE):
409
410
  since = parsed.get("since")
410
411
  from_value = parsed.get("from")
411
412
  to_value = parsed.get("to")
412
413
  if since and from_value:
413
- raise CdxError("Usage: cdx history cannot combine --since and --from.")
414
- start = _parse_since_value(since, now, HISTORY_USAGE) if since else None
414
+ raise CdxError(f"{usage} cannot combine --since and --from.")
415
+ start = _parse_since_value(since, now, usage) if since else None
415
416
  if from_value:
416
- start = _parse_datetime_value(from_value, HISTORY_USAGE, end_of_day=False)
417
- end = _parse_datetime_value(to_value, HISTORY_USAGE, end_of_day=True) if to_value else None
417
+ start = _parse_datetime_value(from_value, usage, end_of_day=False)
418
+ end = _parse_datetime_value(to_value, usage, end_of_day=True) if to_value else None
418
419
  if start and end and start.timestamp() > end.timestamp():
419
- raise CdxError("Usage: cdx history period start must be before period end.")
420
+ raise CdxError(f"{usage} period start must be before period end.")
420
421
  return {
421
422
  "from": _format_period_datetime(start),
422
423
  "to": _format_period_datetime(end),
@@ -480,7 +481,7 @@ def _parse_history_args(args, now=None):
480
481
  "--to": {"key": "to", "type": "str", "default": None},
481
482
  "--json": {"key": "json", "type": "bool", "default": False},
482
483
  }, HISTORY_USAGE, positionals_key="names", max_positionals=1)
483
- period = _parse_history_period(parsed, now)
484
+ period = _parse_history_period(parsed, now, HISTORY_USAGE)
484
485
  return {
485
486
  "name": parsed["names"][0] if parsed["names"] else None,
486
487
  "limit": parsed["limit"],
@@ -498,7 +499,7 @@ def _parse_stats_args(args, now=None):
498
499
  "--to": {"key": "to", "type": "str", "default": None},
499
500
  "--json": {"key": "json", "type": "bool", "default": False},
500
501
  }, STATS_USAGE, positionals_key="names", max_positionals=1)
501
- period = _parse_history_period(parsed, now)
502
+ period = _parse_history_period(parsed, now, STATS_USAGE)
502
503
  return {
503
504
  "name": parsed["names"][0] if parsed["names"] else None,
504
505
  "period": period,
@@ -612,6 +613,7 @@ def _parse_import_args(args):
612
613
  parsed = _parse_flag_args(args, {
613
614
  "--force": {"key": "force", "type": "bool", "default": False},
614
615
  "--merge": {"key": "merge", "type": "bool", "default": False},
616
+ "--allow-authless-force": {"key": "allow_authless_force", "type": "bool", "default": False},
615
617
  "--json": {"key": "json", "type": "bool", "default": False},
616
618
  "--sessions": {
617
619
  "key": "session_names",
@@ -56,6 +56,7 @@ from .cli_args import (
56
56
  from .cli_helpers import (
57
57
  API_SCHEMA_VERSION,
58
58
  _build_handoff_context,
59
+ _format_bytes,
59
60
  _format_export_report,
60
61
  _handoff_launch_prompt,
61
62
  _json_failure,
@@ -96,10 +97,12 @@ from .notify import (
96
97
  wait_for_notification_event,
97
98
  )
98
99
  from .provider_runtime import (
100
+ AUTH_PROBE_AUTHENTICATED,
101
+ AUTH_PROBE_DEGRADED,
99
102
  _ensure_session_authentication,
100
103
  _headless_artifact_paths,
101
104
  _list_launch_transcript_paths,
102
- _probe_provider_auth,
105
+ _probe_provider_auth_status,
103
106
  _run_headless_provider_command,
104
107
  _run_interactive_provider_command,
105
108
  get_resume_capability,
@@ -171,14 +174,6 @@ def _resolve_confirmation(confirm_fn, name):
171
174
  return confirmed
172
175
 
173
176
 
174
- def _format_bytes(value):
175
- size = float(value)
176
- for unit in ("B", "KB", "MB", "GB", "TB"):
177
- if size < 1024 or unit == "TB":
178
- return f"{int(size)} {unit}" if unit == "B" or size.is_integer() else f"{size:.1f} {unit}"
179
- size /= 1024
180
-
181
-
182
177
  def _directory_size_bytes(path, runner=None):
183
178
  if not os.path.exists(path):
184
179
  raise CdxError(f"CDX home does not exist: {path}")
@@ -468,6 +463,13 @@ def _handle_clean_profiles(args, ctx, json_flag):
468
463
  raise CdxError(usage)
469
464
  old_logs = _parse_days(args[index + 1], usage)
470
465
  args = [arg for i, arg in enumerate(args) if i not in (index, index + 1)]
466
+ else:
467
+ old_log_equals = [arg for arg in args if arg.startswith("--old-logs=")]
468
+ if old_log_equals:
469
+ if len(old_log_equals) > 1:
470
+ raise CdxError(usage)
471
+ old_logs = _parse_days(old_log_equals[0].split("=", 1)[1], usage)
472
+ args = [arg for arg in args if arg != old_log_equals[0]]
471
473
  args = [arg for arg in args if arg != "--tmp"]
472
474
  if args or clean_tmp == (old_logs is not None):
473
475
  raise CdxError(usage)
@@ -527,6 +529,8 @@ def handle_disk(rest, ctx):
527
529
  "--json": {"key": "json", "type": "bool", "default": False},
528
530
  }, DISK_USAGE, positionals_key="targets", max_positionals=1)
529
531
  target = parsed["targets"][0] if parsed["targets"] else "home"
532
+ if parsed["candidates"] and target != "profiles":
533
+ raise CdxError(DISK_USAGE)
530
534
  if target == "home":
531
535
  path = ctx["service"]["base_dir"]
532
536
  elif target == "profiles":
@@ -558,8 +562,6 @@ def handle_disk(rest, ctx):
558
562
  if target == "profiles":
559
563
  payload["children"] = _directory_child_sizes(path, runner=ctx["options"].get("diskUsageRunner"), progress=progress)
560
564
  if parsed["candidates"]:
561
- if target != "profiles":
562
- raise CdxError(DISK_USAGE)
563
565
  candidates = _collect_profile_cleanup_candidates(
564
566
  ctx["service"]["base_dir"],
565
567
  runner=ctx["options"].get("diskUsageRunner"),
@@ -1735,8 +1737,12 @@ def handle_last(rest, ctx):
1735
1737
 
1736
1738
  def handle_clean(rest, ctx):
1737
1739
  json_flag, args = _parse_json_flag(rest)
1738
- if args[:1] == ["profiles"] and any(arg in ("--tmp", "--old-logs") for arg in args[1:]):
1739
- return _handle_clean_profiles(args[1:], ctx, json_flag)
1740
+ profile_flags = ("--tmp", "--old-logs")
1741
+ has_profile_flag = any(arg in profile_flags or arg.startswith("--old-logs=") for arg in args)
1742
+ if args[:1] == ["profiles"] or has_profile_flag:
1743
+ if args[:1] == ["profiles"]:
1744
+ return _handle_clean_profiles(args[1:], ctx, json_flag)
1745
+ return _handle_clean_profiles(args, ctx, json_flag)
1740
1746
  yes = "--yes" in args
1741
1747
  args = [arg for arg in args if arg != "--yes"]
1742
1748
  service = ctx["service"]
@@ -2190,17 +2196,23 @@ def _refresh_claude_auth_states(service, target_names=None, spawn_sync=None, env
2190
2196
  if (session.get("auth") or {}).get("status") not in ("authenticated", "logged_out"):
2191
2197
  continue
2192
2198
  try:
2193
- authenticated = _probe_provider_auth(
2199
+ auth_status = _probe_provider_auth_status(
2194
2200
  session,
2195
2201
  spawn_sync=spawn_sync,
2196
2202
  env_override=env_override,
2197
2203
  )
2204
+ if auth_status == AUTH_PROBE_DEGRADED:
2205
+ errors.append({
2206
+ "session": session["name"],
2207
+ "error": "Auth probe timed out; authentication status is degraded.",
2208
+ })
2209
+ continue
2198
2210
  now = _local_now_iso()
2199
- service["update_auth_state"](session["name"], lambda auth, authenticated=authenticated, now=now: {
2211
+ service["update_auth_state"](session["name"], lambda auth, auth_status=auth_status, now=now: {
2200
2212
  **auth,
2201
- "status": "authenticated" if authenticated else "logged_out",
2213
+ "status": "authenticated" if auth_status == AUTH_PROBE_AUTHENTICATED else "logged_out",
2202
2214
  "lastCheckedAt": now,
2203
- **({"lastAuthenticatedAt": now} if authenticated else {}),
2215
+ **({"lastAuthenticatedAt": now} if auth_status == AUTH_PROBE_AUTHENTICATED else {}),
2204
2216
  })
2205
2217
  updated.append(session["name"])
2206
2218
  except Exception as error:
@@ -2326,6 +2338,7 @@ def handle_import(rest, ctx):
2326
2338
  session_names=parsed["session_names"],
2327
2339
  force=parsed["force"],
2328
2340
  merge=parsed["merge"],
2341
+ allow_authless_force=parsed["allow_authless_force"],
2329
2342
  )
2330
2343
  session_count = len(result["session_names"])
2331
2344
  auth_suffix = " with auth" if result["include_auth"] else ""