cdx-manager 0.9.1 → 0.9.3

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
@@ -301,7 +301,7 @@ cdx power all default
301
301
  cdx model provider:ollama default
302
302
  ```
303
303
 
304
- `--model` maps to Codex `--model`, Claude `--model`, and Ollama `ollama run <model>`. `--power` maps to Codex `model_reasoning_effort` and Claude `--effort`. `--permission` maps to provider-native permission flags. `--fast on` clears the stored power setting and uses low effort; setting `--power` again disables fast mode. `--priority` is a 0..100 selector preference used as a tie-breaker after readiness and availability. `--rtk on` injects a launch instruction that encourages assistants to use RTK (`rtk <command>`) for noisy terminal commands when RTK is available, while keeping raw commands for exact output. Logics guidance is auto-enabled when `logics-manager` is available; use `--logics off` to disable that guidance for a session, or `--logics on` to pin it explicitly.
304
+ `--model` maps to Codex `--model`, Claude `--model`, and Ollama `ollama run <model>`. `--power` maps to Codex `model_reasoning_effort` and Claude `--effort`; supported values are `minimal`, `low`, `medium`, `high`, and `xhigh`. `--permission` maps to provider-native permission flags. Codex Fast mode is separate from reasoning effort: `--fast on` opts a Codex session into the Codex Fast service tier, while new sessions, `--fast off`, and default launches force the non-Fast `flex` tier. Use `--power low` when you want low reasoning effort without enabling Codex Fast credits. Existing legacy sessions that stored `fast=on` before this split continue to behave as low effort unless the user explicitly sets `--fast on` again. `--priority` is a 0..100 selector preference used as a tie-breaker after readiness and availability. `--rtk on` injects a launch instruction that encourages assistants to use RTK (`rtk <command>`) for noisy terminal commands when RTK is available, while keeping raw commands for exact output. Logics guidance is auto-enabled when `logics-manager` is available; use `--logics off` to disable that guidance for a session, or `--logics on` to pin it explicitly.
305
305
 
306
306
  ### Launch History
307
307
 
@@ -339,7 +339,7 @@ cdx history --summary --from 2026-05-01 --to 2026-05-28
339
339
  | `cdx config <name> [--json]` | Show persistent launch settings for a session |
340
340
  | `cdx configs [--json]` | Show persistent launch settings for all sessions in one table |
341
341
  | `cdx power\|perm\|fast\|model <name\|all\|provider:PROVIDER\|a,b> <value\|default> [--json]` | Shortcut commands for setting or clearing one launch setting |
342
- | `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] [--logics on\|off] [--model MODEL] [--priority 0..100] [--json]` | Persist launch settings for one or more sessions |
342
+ | `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]` | Persist launch settings for one or more sessions |
343
343
  | `cdx unset <name>\|--sessions all\|a,b\|--provider PROVIDER (--power\|--permission\|--fast\|--rtk\|--logics\|--model\|--priority\|--all) [--json]` | Remove persisted launch settings and fall back to provider defaults |
344
344
  | `cdx history [name] [--limit N] [--summary] [--since 7d\|today\|DATE] [--from DATE] [--to DATE] [--json]` | Show recent launch history or aggregate total launch time per assistant, optionally filtered by period |
345
345
  | `cdx last [--json]` | Launch the most recent existing session from launch history |
@@ -358,8 +358,8 @@ cdx history --summary --from 2026-05-01 --to 2026-05-28
358
358
  | `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 |
359
359
  | `cdx notify --next-ready [--poll seconds] [--once] [--schedule] [--refresh] [--json]` | Wait until the recommended session is usable, or schedule the next known reset notification |
360
360
  | `cdx next [--json] [--refresh]` | Select the best next assistant using the same priority logic as `cdx status` |
361
- | `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 |
362
- | `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 |
361
+ | `cdx select --provider PROVIDER [--min-reasoning-effort minimal\|low\|medium\|high\|xhigh] [--min-power minimal\|low\|medium\|high\|xhigh] [--require-ready] [--refresh] --json` | Select a suitable session for headless automation |
362
+ | `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 MODE] [--timeout-seconds N] --json` | Run one headless task and return a stable JSON result |
363
363
  | `cdx stats [name] [--since 7d\|today\|DATE] [--from DATE] [--to DATE] [--json]` | Aggregate launch counts, duration, and known headless token usage by session |
364
364
  | `cdx status [--json] [--refresh]` | Show token usage table for all sessions; JSON returns a versioned payload with structured warnings |
365
365
  | `cdx status --small [--refresh]` / `cdx status -s [--refresh]` | Show compact token usage table without provider, blocking quota, credits, and updated columns |
@@ -597,6 +597,7 @@ Session names are URL-encoded when used as directory or file names. CLI command
597
597
  ## Troubleshooting
598
598
 
599
599
  - **`cdx <name>` fails with "not authenticated"** — run `cdx login <name>` first.
600
+ - **One of two Codex accounts keeps asking for login** — run `cdx doctor --json` and inspect each Codex session's `codex_auth_file` and `codex_live_auth` checks. Codex sessions use isolated `CODEX_HOME` directories, but newly created sessions seed from the current global `~/.codex/auth.json` when one exists. For two separate Codex accounts, create or repair each session by running `cdx login <name>` for that session; `cdx login` does not log out first, so use `cdx logout <name>` explicitly only when you want to clear that isolated profile.
600
601
  - **`cdx` says no compatible Python 3 interpreter was found** — install Python 3 and make `py -3`, `python`, or `python3` available on PATH.
601
602
  - **`cdx add` succeeds but the session does not appear** — check that `CDX_HOME` is consistent between calls; a mismatch creates two separate registries.
602
603
  - **Status shows `n/a` for all fields** — the Codex app-server rate-limit probe may be unavailable, the session may not be authenticated, and no legacy transcript/history status has been captured yet.
@@ -0,0 +1,33 @@
1
+ # CDX Manager 0.9.2
2
+
3
+ ## Highlights
4
+
5
+ - `cdx import` gains a `--merge` flag to fill in missing data without overwriting what already exists locally.
6
+ - npm and PyPI publication workflows now verify that the release tag's commit has a successful `CI` run before any registry upload step.
7
+
8
+ ## Changes
9
+
10
+ ### cdx import: `--merge` mode
11
+
12
+ `cdx import` previously offered two stances on existing sessions: reject the conflict (default) or erase and replace (`--force`). A third mode is now available:
13
+
14
+ ```
15
+ cdx import backup.cdx --merge
16
+ ```
17
+
18
+ With `--merge`, for each session that already exists locally:
19
+
20
+ - **Session fields** — existing values are kept; fields absent locally are pulled in from the bundle.
21
+ - **Session state** — same merge rule: local data wins, bundle fills the gaps.
22
+ - **Profile files** (auth.json, credentials, etc.) — files that already exist on disk are left untouched; files missing locally are restored from the bundle.
23
+ - **New sessions** in the bundle that have no local counterpart are imported normally.
24
+
25
+ `--force` and `--merge` are mutually exclusive and raise a clear error if combined.
26
+
27
+ ## Validation
28
+
29
+ - `npm run prepublishOnly`
30
+ - `npm pack --dry-run`
31
+ - `python -m unittest discover -s test -p 'test_release_ci_py.py'`
32
+ - `logics-manager lint --require-status`
33
+ - `logics-manager audit --legacy-cutoff-version 1.1.0 --group-by-doc`
@@ -0,0 +1,35 @@
1
+ # CDX Manager 0.9.3
2
+
3
+ ## Highlights
4
+
5
+ - Codex Fast now stays opt-in and no longer silently lowers normal Codex session reasoning effort.
6
+ - Codex authentication diagnostics now detect when local credentials belong to a different account than the live CLI session.
7
+ - `cdx run` usage parsing now covers the Codex 0.141 JSONL `turn.completed` usage shape, including `reasoning_output_tokens`.
8
+
9
+ ## Changes
10
+
11
+ ### Codex Fast launch behavior
12
+
13
+ `cdx set <session> --fast on` now marks Fast as a service-tier choice instead of treating it as a reason to lower power to `low`. Normal Codex sessions continue to launch with their configured reasoning effort and an explicit flex service tier when Fast is off.
14
+
15
+ ### Codex auth isolation diagnostics
16
+
17
+ `cdx doctor` now reads the email from local Codex tokens and compares it with the live `codex login status` account when both are available. If a managed session has local tokens for one account but the CLI is authenticated as another, the report surfaces a repairable `codex_auth_mismatch` issue instead of reporting a vague live-auth failure.
18
+
19
+ The auth probe also avoids trusting local credentials alone for launch and run readiness, which keeps multi-account sessions from accidentally using the wrong global Codex login after a CLI update or account switch.
20
+
21
+ ### Codex 0.141 run usage parsing
22
+
23
+ Codex CLI 0.141 emits headless usage in JSONL `turn.completed` events. `cdx run` now maps the current `reasoning_output_tokens` field into the stable `reasoning_tokens` output and keeps computing total tokens from input plus output when the provider omits `total_tokens`.
24
+
25
+ ## Validation
26
+
27
+ - `cdx doctor --json`
28
+ - `cdx status --json --refresh`
29
+ - `cdx run --provider codex --cwd /Users/alexandreagostini/Documents/cdx-manager --prompt 'Reply exactly: CDX_CODEX_OK' --timeout-seconds 90 --json`
30
+ - `cdx run --provider claude --cwd /Users/alexandreagostini/Documents/cdx-manager --prompt 'Reply exactly: CDX_CLAUDE_OK' --timeout-seconds 90 --json`
31
+ - `npm run lint`
32
+ - `npm test`
33
+ - `logics-manager lint --require-status`
34
+ - `logics-manager health`
35
+ - `git diff --check`
@@ -68,6 +68,14 @@
68
68
  "v0.9.0": {
69
69
  "github_tarball_sha256": "809af5746e1287f4c5dc7a2fb7583e67e212aa250c45be905c32374e5dee26d6",
70
70
  "github_zip_sha256": "9118f81645c00a4a660923a1ae290f48bd458af7ebfcf9f6580a3b91023b7c76"
71
+ },
72
+ "v0.9.1": {
73
+ "github_tarball_sha256": "8cbab620c823aeaf56e72c1a4d20e251a1c7142bfeeb3d516321d59c2c0a621d",
74
+ "github_zip_sha256": "253909ede6dca61937835bd4ee29145ddacaa08fbdfba1b39810da73ae2ccc84"
75
+ },
76
+ "v0.9.2": {
77
+ "github_tarball_sha256": "3e3ae4e4efc63a97dc6623ae8ddff36f04f4b7a0b531878a28c041298223d0b8",
78
+ "github_zip_sha256": "09acd2866770f4dc7f9aaba6aff8308766bb211dabddd7f28bdfde9ace427e78"
71
79
  }
72
80
  }
73
81
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cdx-manager",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
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.9.1"
7
+ version = "0.9.3"
8
8
  description = "Terminal session manager for Codex and Claude accounts."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
package/src/cli.py CHANGED
@@ -64,7 +64,7 @@ from .status_view import (
64
64
  )
65
65
  from .update_check import check_for_update, check_logics_manager_for_update
66
66
 
67
- VERSION = "0.9.1"
67
+ VERSION = "0.9.3"
68
68
 
69
69
 
70
70
  # ---------------------------------------------------------------------------
@@ -82,8 +82,8 @@ def _print_help(use_color=False):
82
82
  f" {_style('cdx status --small|-s [--refresh]', '36', use_color)}",
83
83
  f" {_style('cdx status <name> [--json] [--refresh]', '36', use_color)}",
84
84
  f" {_style('cdx next [--json] [--refresh]', '36', use_color)}",
85
- f" {_style('cdx select --provider PROVIDER [--min-reasoning-effort low|medium|high] [--min-power low|medium|high] [--require-ready] [--refresh] --json', '36', use_color)}",
86
- 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)}",
85
+ 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)}",
86
+ 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)}",
87
87
  f" {_style('cdx runs [--limit N] --json', '36', use_color)}",
88
88
  f" {_style('cdx run-status <run_id> --json', '36', use_color)}",
89
89
  f" {_style('cdx run-report <run_id> --json', '36', use_color)}",
@@ -91,7 +91,7 @@ def _print_help(use_color=False):
91
91
  f" {_style('cdx config <name> [--json]', '36', use_color)}",
92
92
  f" {_style('cdx configs [--json]', '36', use_color)}",
93
93
  f" {_style('cdx power|perm|fast|model <name|all|provider:PROVIDER|a,b> <value|default> [--json]', '36', use_color)}",
94
- 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] [--logics on|off] [--model MODEL] [--priority 0..100] [--json]', '36', use_color)}",
94
+ f" {_style('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]', '36', use_color)}",
95
95
  f" {_style('cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--permission|--fast|--rtk|--logics|--model|--priority|--all) [--json]', '36', use_color)}",
96
96
  f" {_style('cdx history [name] [--limit N] [--summary] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]', '36', use_color)}",
97
97
  f" {_style('cdx stats [name] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]', '36', use_color)}",
@@ -108,7 +108,7 @@ def _print_help(use_color=False):
108
108
  f" {_style('cdx rmv <name> [--force] [--json]', '36', use_color)}",
109
109
  f" {_style('cdx clean [name] [--json]', '36', use_color)}",
110
110
  f" {_style('cdx export <file> [--include-auth] [--sessions a,b] [--passphrase-env VAR] [--force] [--json]', '36', use_color)}",
111
- f" {_style('cdx import <file> [--sessions a,b] [--passphrase-env VAR] [--force] [--json]', '36', use_color)}",
111
+ f" {_style('cdx import <file> [--sessions a,b] [--passphrase-env VAR] [--force|--merge] [--json]', '36', use_color)}",
112
112
  f" {_style('cdx doctor [--json]', '36', use_color)}",
113
113
  f" {_style('cdx repair [--dry-run] [--force] [--json]', '36', use_color)}",
114
114
  f" {_style('cdx view [--json]', '36', use_color)}",
@@ -55,10 +55,10 @@ DOCTOR_USAGE = "Usage: cdx doctor [--json]"
55
55
  REPAIR_USAGE = "Usage: cdx repair [--dry-run] [--force] [--json]"
56
56
  UPDATE_USAGE = "Usage: cdx update [--check] [--yes] [--json] [--version TAG]"
57
57
  EXPORT_USAGE = "Usage: cdx export <file> [--include-auth] [--force] [--json] [--sessions name1,name2] [--passphrase-env VAR]"
58
- IMPORT_USAGE = "Usage: cdx import <file> [--force] [--json] [--sessions name1,name2] [--passphrase-env VAR]"
58
+ IMPORT_USAGE = "Usage: cdx import <file> [--force|--merge] [--json] [--sessions name1,name2] [--passphrase-env VAR]"
59
59
  CONTEXT_USAGE = "Usage: cdx context show|path|init|edit|clear|set [text...] [--json]"
60
60
  HANDOFF_USAGE = "Usage: cdx handoff <name> [--json] | cdx handoff <source> <target> [--json]"
61
- SET_USAGE = "Usage: cdx set <name>|--sessions all|a,b|--provider PROVIDER [--power low|medium|high|xhigh|max] [--permission review|default|auto|full] [--fast on|off] [--rtk on|off] [--logics on|off] [--model MODEL] [--priority 0..100] [--json]"
61
+ 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]"
62
62
  UNSET_USAGE = "Usage: cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--permission|--fast|--rtk|--logics|--model|--priority|--all) [--json]"
63
63
  SETTING_ALIAS_USAGE = "Usage: cdx power|perm|fast|model <name|all|provider:PROVIDER|a,b> <value|default> [--json]"
64
64
  CONFIG_USAGE = "Usage: cdx config <name> [--json]"
@@ -66,9 +66,9 @@ CONFIGS_USAGE = "Usage: cdx configs [--json]"
66
66
  HISTORY_USAGE = "Usage: cdx history [name] [--limit N] [--summary] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]"
67
67
  STATS_USAGE = "Usage: cdx stats [name] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]"
68
68
  LAST_USAGE = "Usage: cdx last [--json]"
69
- SELECT_USAGE = "Usage: cdx select --provider PROVIDER [--min-reasoning-effort low|medium|high] [--min-power low|medium|high] [--require-ready] [--refresh] --json"
69
+ SELECT_USAGE = "Usage: cdx select --provider PROVIDER [--min-reasoning-effort minimal|low|medium|high|xhigh] [--min-power minimal|low|medium|high|xhigh] [--require-ready] [--refresh] --json"
70
70
  NEXT_USAGE = "Usage: cdx next [--json] [--refresh]"
71
- RUN_USAGE = "Usage: cdx run [session] --cwd PATH (--prompt-file PATH|--prompt TEXT) [--provider PROVIDER] [--model MODEL] [--kind assistant|code-review] [--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"
71
+ RUN_USAGE = "Usage: 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"
72
72
  RUNS_USAGE = "Usage: cdx runs [--limit N] --json"
73
73
  RUN_STATUS_USAGE = "Usage: cdx run-status <run_id> --json"
74
74
  RUN_REPORT_USAGE = "Usage: cdx run-report <run_id> --json"
@@ -1000,6 +1000,7 @@ def _parse_export_args(args):
1000
1000
  def _parse_import_args(args):
1001
1001
  parsed = _parse_flag_args(args, {
1002
1002
  "--force": {"key": "force", "type": "bool", "default": False},
1003
+ "--merge": {"key": "merge", "type": "bool", "default": False},
1003
1004
  "--json": {"key": "json", "type": "bool", "default": False},
1004
1005
  "--sessions": {
1005
1006
  "key": "session_names",
@@ -1012,6 +1013,8 @@ def _parse_import_args(args):
1012
1013
  parsed["file_path"] = parsed.pop("positionals")[0] if parsed["positionals"] else None
1013
1014
  if not parsed["file_path"]:
1014
1015
  raise CdxError(IMPORT_USAGE)
1016
+ if parsed["force"] and parsed["merge"]:
1017
+ raise CdxError("--force and --merge are mutually exclusive.")
1015
1018
  return parsed
1016
1019
 
1017
1020
 
@@ -1680,8 +1683,8 @@ def _resolve_bulk_launch_targets(parsed, service):
1680
1683
 
1681
1684
 
1682
1685
  def _reasoning_rank(value):
1683
- order = {"low": 0, "medium": 1, "high": 2, "xhigh": 2, "max": 2}
1684
- return order.get(str(value or "low").lower(), 0)
1686
+ order = {"minimal": 0, "low": 1, "medium": 2, "high": 3, "xhigh": 4}
1687
+ return order.get(str(value or "low").lower(), -1)
1685
1688
 
1686
1689
 
1687
1690
  def _session_reasoning_effort(session):
@@ -1690,7 +1693,7 @@ def _session_reasoning_effort(session):
1690
1693
  launch.get("reasoning_effort")
1691
1694
  or launch.get("reasoningEffort")
1692
1695
  or launch.get("power")
1693
- or ("low" if launch.get("fast") is True else None)
1696
+ or ("low" if launch.get("fast") is True and launch.get("fastMode") != "service_tier" else None)
1694
1697
  or "low"
1695
1698
  )
1696
1699
 
@@ -2287,6 +2290,7 @@ def handle_doctor(rest, ctx):
2287
2290
  ctx["service"],
2288
2291
  ctx["service"]["base_dir"],
2289
2292
  env=ctx.get("env"),
2293
+ spawn_sync=ctx.get("spawn_sync"),
2290
2294
  )
2291
2295
  if json_flag:
2292
2296
  _write_json(ctx, _json_success("doctor", "Collected health report", report=report))
@@ -2643,6 +2647,7 @@ def handle_import(rest, ctx):
2643
2647
  passphrase=passphrase,
2644
2648
  session_names=parsed["session_names"],
2645
2649
  force=parsed["force"],
2650
+ merge=parsed["merge"],
2646
2651
  )
2647
2652
  session_count = len(result["session_names"])
2648
2653
  auth_suffix = " with auth" if result["include_auth"] else ""
@@ -2668,11 +2673,6 @@ def handle_login(rest, ctx):
2668
2673
  session = ctx["service"]["get_session"](args[0])
2669
2674
  if not session:
2670
2675
  raise CdxError(f"Unknown session: {args[0]}")
2671
- if session["provider"] == PROVIDER_CODEX:
2672
- _run_interactive_provider_command(
2673
- session, "logout", spawn=ctx.get("spawn"), env_override=ctx.get("env"),
2674
- signal_emitter=ctx.get("signal_emitter")
2675
- )
2676
2676
  _run_interactive_provider_command(
2677
2677
  session, "login", spawn=ctx.get("spawn"), env_override=ctx.get("env"),
2678
2678
  signal_emitter=ctx.get("signal_emitter")
@@ -2683,6 +2683,7 @@ def handle_login(rest, ctx):
2683
2683
  spawn_sync=ctx.get("spawn_sync"),
2684
2684
  env_override=ctx.get("env"),
2685
2685
  behavior="probe-only",
2686
+ trust_local_credentials=False,
2686
2687
  )
2687
2688
  if not auth_probe.get("authenticated") and session["provider"] == PROVIDER_CLAUDE:
2688
2689
  _bootstrap_claude_setup_token(session, ctx)
@@ -2692,6 +2693,7 @@ def handle_login(rest, ctx):
2692
2693
  spawn_sync=ctx.get("spawn_sync"),
2693
2694
  env_override=ctx.get("env"),
2694
2695
  behavior="probe-only",
2696
+ trust_local_credentials=False,
2695
2697
  )
2696
2698
  if not auth_probe.get("authenticated"):
2697
2699
  raise CdxError(
@@ -2855,6 +2857,7 @@ def handle_launch(command, ctx, initial_prompt=None):
2855
2857
  stdin_is_tty=ctx["stdin_is_tty"],
2856
2858
  behavior="launch",
2857
2859
  signal_emitter=ctx.get("signal_emitter"),
2860
+ trust_local_credentials=False,
2858
2861
  )
2859
2862
  message = f"Launching {session['provider']} session {session['name']}"
2860
2863
  if not json_flag:
package/src/health.py CHANGED
@@ -6,6 +6,8 @@ import tempfile
6
6
  from urllib.parse import quote, unquote
7
7
 
8
8
  from .cli_render import _pad_table, _style
9
+ from .config import PROVIDER_CODEX
10
+ from .provider_runtime import codex_auth_diagnostic
9
11
 
10
12
 
11
13
  def _encode(name):
@@ -30,7 +32,7 @@ def _issue(status, code, message, detail=None, repairable=False):
30
32
  }
31
33
 
32
34
 
33
- def collect_health_report(service, base_dir, env=None):
35
+ def collect_health_report(service, base_dir, env=None, spawn_sync=None):
34
36
  env = env or os.environ
35
37
  issues = []
36
38
 
@@ -79,11 +81,48 @@ def collect_health_report(service, base_dir, env=None):
79
81
  state_path = _state_file_path(base_dir, name)
80
82
  if not os.path.isfile(state_path):
81
83
  issues.append(_issue("FAIL", "missing_state", f"session {name} state file is missing", state_path, True))
84
+ if session.get("provider") == PROVIDER_CODEX:
85
+ issues.extend(_codex_auth_issues(session, spawn_sync=spawn_sync, env=env))
82
86
 
83
87
  issues.extend(_collect_profile_issues(base_dir, session_names))
84
88
  return {"base_dir": base_dir, "issues": issues, "summary": summarize_health(issues)}
85
89
 
86
90
 
91
+ def _codex_auth_issues(session, spawn_sync=None, env=None):
92
+ name = session["name"]
93
+ diag = codex_auth_diagnostic(session, spawn_sync=spawn_sync, env_override=env)
94
+ identity = diag.get("account_email") or "unknown account"
95
+ issues = [
96
+ _issue(
97
+ "OK" if diag.get("auth_json_exists") else "WARN",
98
+ "codex_auth_file",
99
+ f"session {name} Codex auth file {'found' if diag.get('auth_json_exists') else 'missing'} ({identity})",
100
+ {
101
+ "session": name,
102
+ "auth_home": diag.get("auth_home"),
103
+ "auth_json_exists": diag.get("auth_json_exists"),
104
+ "local_tokens_present": diag.get("local_tokens_present"),
105
+ "account_email": diag.get("account_email"),
106
+ },
107
+ )
108
+ ]
109
+ live_status = diag.get("live_status")
110
+ live_ok = live_status == "authenticated"
111
+ live_warn = live_status in ("logged_out", "error")
112
+ issues.append(_issue(
113
+ "OK" if live_ok else "WARN" if live_warn else "WARN",
114
+ "codex_live_auth",
115
+ f"session {name} live Codex auth status: {live_status}",
116
+ {
117
+ "session": name,
118
+ "auth_home": diag.get("auth_home"),
119
+ "live_status": live_status,
120
+ "live_error": diag.get("live_error"),
121
+ },
122
+ ))
123
+ return issues
124
+
125
+
87
126
  def _check_cdx_home(base_dir):
88
127
  try:
89
128
  os.makedirs(base_dir, exist_ok=True)
@@ -7,6 +7,7 @@ import shutil
7
7
  import subprocess
8
8
  import sys
9
9
  import uuid
10
+ import base64
10
11
  from datetime import datetime, timezone
11
12
 
12
13
  from .config import PROVIDER_ANTIGRAVITY, PROVIDER_CLAUDE, PROVIDER_CODEX, PROVIDER_OLLAMA
@@ -14,7 +15,7 @@ from .errors import CdxError
14
15
 
15
16
 
16
17
  LOG_ROTATE_BYTES = 10 * 1024 * 1024 # 10 MB
17
- REASONING_EFFORT_VALUES = {"low", "medium", "high"}
18
+ REASONING_EFFORT_VALUES = {"minimal", "low", "medium", "high", "xhigh"}
18
19
  RTK_PROMPT = (
19
20
  "When running noisy shell commands, prefer RTK wrappers (`rtk <command>`) if `rtk` is available. "
20
21
  "Use raw commands when exact, unfiltered output is required."
@@ -148,6 +149,64 @@ def _has_local_codex_auth(auth_home):
148
149
  )
149
150
 
150
151
 
152
+ def _decode_jwt_claims(token):
153
+ if not token or "." not in str(token):
154
+ return {}
155
+ parts = str(token).split(".")
156
+ if len(parts) < 2:
157
+ return {}
158
+ padding = "=" * (-len(parts[1]) % 4)
159
+ try:
160
+ decoded = base64.urlsafe_b64decode(parts[1] + padding)
161
+ return json.loads(decoded.decode("utf-8"))
162
+ except (ValueError, json.JSONDecodeError, UnicodeDecodeError):
163
+ return {}
164
+
165
+
166
+ def _read_codex_account_email(auth_home):
167
+ try:
168
+ with open(os.path.join(auth_home, "auth.json"), "r", encoding="utf-8") as handle:
169
+ auth = json.load(handle)
170
+ except (FileNotFoundError, OSError, json.JSONDecodeError):
171
+ return None
172
+ tokens = auth.get("tokens") if isinstance(auth, dict) else {}
173
+ if not isinstance(tokens, dict):
174
+ return None
175
+ for token_name in ("id_token", "access_token"):
176
+ claims = _decode_jwt_claims(tokens.get(token_name))
177
+ email = claims.get("email")
178
+ if not email and token_name == "access_token":
179
+ profile = claims.get("https://api.openai.com/profile") or {}
180
+ email = profile.get("email") if isinstance(profile, dict) else None
181
+ if email:
182
+ return str(email).strip().lower()
183
+ return None
184
+
185
+
186
+ def codex_auth_diagnostic(session, spawn_sync=None, env_override=None):
187
+ auth_home = _get_auth_home(session)
188
+ auth_path = os.path.join(auth_home, "auth.json")
189
+ result = {
190
+ "auth_home": auth_home,
191
+ "auth_json_exists": os.path.isfile(auth_path),
192
+ "local_tokens_present": _has_local_codex_auth(auth_home),
193
+ "account_email": _read_codex_account_email(auth_home),
194
+ "live_status": "unknown",
195
+ "live_error": None,
196
+ }
197
+ try:
198
+ result["live_status"] = "authenticated" if _probe_provider_auth(
199
+ session,
200
+ spawn_sync=spawn_sync,
201
+ env_override=env_override,
202
+ trust_local_credentials=False,
203
+ ) else "logged_out"
204
+ except CdxError as error:
205
+ result["live_status"] = "error"
206
+ result["live_error"] = str(error)
207
+ return result
208
+
209
+
151
210
  def _read_claude_account_email(auth_home):
152
211
  config_path = os.path.join(auth_home, ".claude.json")
153
212
  try:
@@ -196,11 +255,25 @@ def _launch_power(session):
196
255
  power = launch.get("reasoning_effort") or launch.get("reasoningEffort") or launch.get("power")
197
256
  if power:
198
257
  return power
199
- if launch.get("fast") is True:
258
+ if _legacy_fast_low_effort(launch):
200
259
  return "low"
201
260
  return None
202
261
 
203
262
 
263
+ def _legacy_fast_low_effort(launch):
264
+ return launch.get("fast") is True and launch.get("fastMode") != "service_tier"
265
+
266
+
267
+ def _codex_fast_enabled(launch):
268
+ return launch.get("fast") is True and launch.get("fastMode") == "service_tier"
269
+
270
+
271
+ def _codex_fast_config_args(launch):
272
+ if _codex_fast_enabled(launch):
273
+ return ["-c", 'service_tier="fast"', "-c", "features.fast_mode=true"]
274
+ return ["-c", 'service_tier="flex"', "-c", "features.fast_mode=false"]
275
+
276
+
204
277
  def _normalize_reasoning_effort(reasoning_effort=None, power=None, usage="Unsupported reasoning effort."):
205
278
  effort = str(reasoning_effort).strip().lower() if reasoning_effort is not None else None
206
279
  alias = str(power).strip().lower() if power is not None else None
@@ -269,6 +342,7 @@ def _launch_config_args(session):
269
342
  return args
270
343
  if power:
271
344
  args += ["-c", f'model_reasoning_effort="{power}"']
345
+ args += _codex_fast_config_args(launch)
272
346
  if permission:
273
347
  args += LAUNCH_PERMISSION_ARGS[PROVIDER_CODEX].get(permission, [])
274
348
  return args
@@ -482,6 +556,7 @@ def _build_headless_launch_spec(session, cwd=None, env_override=None, initial_pr
482
556
  args += ["--model", model]
483
557
  if power:
484
558
  args += ["-c", f'model_reasoning_effort="{power}"']
559
+ args += _codex_fast_config_args(launch)
485
560
  if permission:
486
561
  args += HEADLESS_CODEX_PERMISSION_ARGS.get(permission, [])
487
562
  if initial_prompt:
@@ -43,7 +43,7 @@ def run_payload_reasoning_effort(parsed, session):
43
43
  or launch.get("reasoning_effort")
44
44
  or launch.get("reasoningEffort")
45
45
  or launch.get("power")
46
- or ("low" if launch.get("fast") is True else None)
46
+ or ("low" if launch.get("fast") is True and launch.get("fastMode") != "service_tier" else None)
47
47
  )
48
48
 
49
49
 
package/src/run_usage.py CHANGED
@@ -91,6 +91,7 @@ def _usage_from_dict(value):
91
91
  output_tokens = _int_value(usage.get("output_tokens"), usage.get("completion_tokens"))
92
92
  reasoning_tokens = _int_value(
93
93
  usage.get("reasoning_tokens"),
94
+ usage.get("reasoning_output_tokens"),
94
95
  _nested_int(usage, "output_tokens_details", "reasoning_tokens"),
95
96
  _nested_int(usage, "completion_tokens_details", "reasoning_tokens"),
96
97
  )
@@ -66,8 +66,8 @@ RESERVED_SESSION_NAMES = {
66
66
  STATUS_CACHE_TTL_SECONDS = 60
67
67
  CLAUDE_STATUS_CACHE_TTL_SECONDS = 10 * 60
68
68
  MAX_STATUS_WORKERS = 8
69
- LAUNCH_POWER_VALUES = {"low", "medium", "high", "xhigh", "max"}
70
- LAUNCH_REASONING_EFFORT_VALUES = {"low", "medium", "high"}
69
+ LAUNCH_POWER_VALUES = {"minimal", "low", "medium", "high", "xhigh"}
70
+ LAUNCH_REASONING_EFFORT_VALUES = {"minimal", "low", "medium", "high", "xhigh"}
71
71
  LAUNCH_PERMISSION_VALUES = {"review", "default", "auto", "full"}
72
72
  MAX_LAUNCH_MODEL_LENGTH = 128
73
73
  MIN_LAUNCH_PRIORITY = 0
@@ -107,7 +107,7 @@ def _seed_codex_auth_from_global(auth_home, env=None):
107
107
  return True
108
108
 
109
109
 
110
- def _normalize_launch_settings(settings):
110
+ def _normalize_launch_settings(settings, mark_fast_service_tier=True):
111
111
  normalized = {}
112
112
  if not settings:
113
113
  return normalized
@@ -138,6 +138,12 @@ def _normalize_launch_settings(settings):
138
138
  normalized["fast"] = False
139
139
  else:
140
140
  raise CdxError(f"Unsupported fast value: {settings['fast']}")
141
+ if normalized["fast"] is True and mark_fast_service_tier:
142
+ normalized["fastMode"] = "service_tier"
143
+ else:
144
+ normalized.pop("fastMode", None)
145
+ if settings.get("fastMode") == "service_tier" and normalized["fast"] is True:
146
+ normalized["fastMode"] = "service_tier"
141
147
  if "rtk" in settings and settings["rtk"] is not None:
142
148
  value = settings["rtk"]
143
149
  if isinstance(value, bool):
@@ -827,17 +833,15 @@ def create_session_service(options=None):
827
833
  updates = _normalize_launch_settings(settings)
828
834
  if not updates:
829
835
  raise CdxError("At least one launch setting is required.")
830
- current = _normalize_launch_settings(session.get("launch") or {})
836
+ current = _normalize_launch_settings(session.get("launch") or {}, mark_fast_service_tier=False)
831
837
  launch = {**current, **updates}
832
838
  explicit_power = "power" in updates or "reasoning_effort" in updates
833
- if explicit_power and "fast" not in updates:
839
+ if explicit_power and "fast" not in updates and launch.get("fastMode") != "service_tier":
834
840
  launch["fast"] = False
835
- if "fast" in updates and not explicit_power:
836
- if updates["fast"] is True:
837
- launch.pop("power", None)
838
- launch.pop("reasoning_effort", None)
839
- launch.pop("reasoningEffort", None)
840
- elif not any(key in launch for key in ("power", "reasoning_effort", "reasoningEffort")):
841
+ launch.pop("fastMode", None)
842
+ if updates.get("fast") is False:
843
+ launch.pop("fastMode", None)
844
+ if not any(key in launch for key in ("power", "reasoning_effort", "reasoningEffort")):
841
845
  launch["power"] = DEFAULT_LAUNCH_SETTINGS["power"]
842
846
  now = _local_now_iso()
843
847
  return store["update_session"](name, lambda s: {
@@ -1173,7 +1177,7 @@ def create_session_service(options=None):
1173
1177
  "bundle_size_bytes": len(bundle_bytes),
1174
1178
  }
1175
1179
 
1176
- def import_bundle(file_path, passphrase=None, session_names=None, force=False):
1180
+ def import_bundle(file_path, passphrase=None, session_names=None, force=False, merge=False):
1177
1181
  if not file_path or not os.path.isfile(file_path):
1178
1182
  raise CdxError(f"Bundle file not found: {file_path}")
1179
1183
  with open(file_path, "rb") as handle:
@@ -1193,15 +1197,18 @@ def create_session_service(options=None):
1193
1197
 
1194
1198
  existing = {session["name"] for session in list_sessions()}
1195
1199
  conflicts = [name for name in names if name in existing]
1196
- if conflicts and not force:
1200
+ if conflicts and not force and not merge:
1197
1201
  raise CdxError(f"Import would overwrite existing sessions: {', '.join(conflicts)}")
1198
1202
 
1199
1203
  for session_payload in imported_sessions:
1200
1204
  name = session_payload["name"]
1201
1205
  _validate_new_session_name(name)
1202
1206
  provider = _normalize_provider(session_payload["provider"])
1203
- if name in existing:
1207
+ is_existing = name in existing
1208
+
1209
+ if is_existing and force:
1204
1210
  remove_session(name)
1211
+ is_existing = False
1205
1212
 
1206
1213
  session_root = _get_session_root(name)
1207
1214
  auth_home = _get_session_auth_home(name, provider)
@@ -1210,18 +1217,38 @@ def create_session_service(options=None):
1210
1217
  _ensure_private_dir(session_root)
1211
1218
  _ensure_private_dir(auth_home)
1212
1219
 
1213
- session_record = {
1214
- **session_payload,
1215
- "provider": provider,
1216
- "enabled": session_payload.get("enabled", True) is not False,
1217
- "sessionRoot": session_root,
1218
- "authHome": auth_home,
1219
- }
1220
- store["replace_session"](name, session_record)
1220
+ if is_existing and merge:
1221
+ existing_record = store["get_session"](name) or {}
1222
+ bundle_record = {
1223
+ **session_payload,
1224
+ "provider": provider,
1225
+ "enabled": session_payload.get("enabled", True) is not False,
1226
+ "sessionRoot": session_root,
1227
+ "authHome": auth_home,
1228
+ }
1229
+ # Existing values take precedence; bundle fills in missing keys only.
1230
+ merged_record = {**bundle_record, **{k: v for k, v in existing_record.items() if v is not None}}
1231
+ merged_record["sessionRoot"] = session_root
1232
+ merged_record["authHome"] = auth_home
1233
+ store["replace_session"](name, merged_record)
1234
+ else:
1235
+ session_record = {
1236
+ **session_payload,
1237
+ "provider": provider,
1238
+ "enabled": session_payload.get("enabled", True) is not False,
1239
+ "sessionRoot": session_root,
1240
+ "authHome": auth_home,
1241
+ }
1242
+ store["replace_session"](name, session_record)
1221
1243
 
1222
1244
  state = (payload.get("states") or {}).get(name)
1223
1245
  if state is not None:
1224
- store["write_session_state"](name, state)
1246
+ if is_existing and merge:
1247
+ existing_state = store["read_session_state"](name) or {}
1248
+ merged_state = {**state, **{k: v for k, v in existing_state.items() if v is not None}}
1249
+ store["write_session_state"](name, merged_state)
1250
+ else:
1251
+ store["write_session_state"](name, state)
1225
1252
 
1226
1253
  for item in (payload.get("profiles") or {}).get(name, []):
1227
1254
  rel_path = _safe_relpath(item.get("path"))
@@ -1230,6 +1257,9 @@ def create_session_service(options=None):
1230
1257
  except (AttributeError, ValueError, UnicodeEncodeError) as error:
1231
1258
  raise CdxError(f"Bundle contains invalid file data for session {name}: {rel_path}") from error
1232
1259
  dest_path = os.path.join(session_root, rel_path)
1260
+ # In merge mode, skip files that already exist locally.
1261
+ if is_existing and merge and os.path.exists(dest_path):
1262
+ continue
1233
1263
  _ensure_private_dir(os.path.dirname(dest_path))
1234
1264
  with open(dest_path, "wb") as handle:
1235
1265
  handle.write(content)