cdx-manager 0.14.0 → 0.15.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,20 +1,52 @@
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.14.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.15.0-4C8BF5) ![Python](https://img.shields.io/badge/python-3.9%2B-3776AB?logo=python&logoColor=white)
4
4
 
5
- **Run multiple Codex, Claude, Antigravity, and Ollama sessions from one terminal. Switch between accounts instantly.**
5
+ **Stop guessing which AI account still has quota.** `cdx` tracks the rate-limit window of every Codex and Claude account you own, tells you which one is usable right now, and launches it with isolated auth — in one command.
6
6
 
7
- If you use AI coding tools at scale ; multiple accounts, multiple providers : you know the friction: re-authenticating, losing context, juggling environment variables. `cdx` removes all of that.
7
+ If you pay for several AI coding subscriptions, you know the dance: you hit a 5-hour limit mid-task, guess which other account still has room, re-authenticate, and lose your context on the way. `cdx` turns that into `cdx next`.
8
8
 
9
- One command to launch any session. Zero auth juggling.
9
+ ![cdx status: remaining 5-hour and weekly quota for fifteen accounts, with the recommended one first](docs/assets/status.png)
10
10
 
11
- <img width="213" height="227" alt="image" src="https://github.com/user-attachments/assets/f15f449c-d23e-47fe-a455-17c7386f9be2" />
12
- <img width="645" height="129" alt="image" src="https://github.com/user-attachments/assets/34bcb395-f832-4da6-9247-3e5022e75e56" />
11
+ ---
12
+
13
+ ## Quick Start
14
+
15
+ ```bash
16
+ npm install -g cdx-manager
17
+
18
+ cdx add codex work # register an isolated Codex profile named "work"
19
+ cdx add claude perso # and a Claude one
20
+ cdx status # every account's remaining quota, side by side
21
+ cdx next # which account should I use right now?
22
+ ```
23
+
24
+ Supported providers: `codex`, `claude`, `antigravity`, `ollama`.
25
+
26
+ `cdx next` answers the question you actually have, and prints the command to run:
27
+
28
+ ![cdx next: work1 selected at 100% quota, with the reason and the command to run](docs/assets/next.png)
29
+
30
+ ---
31
+
32
+ ## The Core Five
33
+
34
+ Everything else is optional. These five cover the daily loop:
35
+
36
+ | Command | What it gives you |
37
+ |---|---|
38
+ | `cdx add <provider> <name>` | An isolated account profile — its own auth, its own home |
39
+ | `cdx <name>` | Launch that account, auth checked first |
40
+ | `cdx status` | Remaining 5-hour and weekly quota for every account at once |
41
+ | `cdx next` | The account you should use right now, and why |
42
+ | `cdx run --prompt ... --json` | The same routing, headless, for scripts and agents |
13
43
 
14
44
  ---
15
45
 
16
46
  ## Table of Contents
17
47
 
48
+ - [Quick Start](#quick-start)
49
+ - [The Core Five](#the-core-five)
18
50
  - [What it does](#what-it-does)
19
51
  - [Technical Overview](#technical-overview)
20
52
  - [Getting Started](#getting-started)
@@ -32,24 +64,58 @@ One command to launch any session. Zero auth juggling.
32
64
 
33
65
  ## What it does
34
66
 
67
+ ### Quota-aware routing
68
+
69
+ The reason `cdx` exists: knowing, across every account you own, which one you can actually use.
70
+
71
+ - **Usage at a glance.** `cdx status` shows token usage, 5-hour window quota, weekly quota, last-updated timestamps, priority guidance, and the last launched session in one aligned table.
72
+ - **Pick for me.** `cdx next` selects the best available assistant from your pinned priorities and the live quota picture, shows the reason, and prints the exact command to run.
73
+ - **Next-ready notification.** `cdx ready` schedules a native system notification for the next assistant that comes back from cooldown, then returns immediately. `cdx notify` covers reset times and scheduled wake-ups.
74
+ - **Passive status resolution.** Codex status is read from the local Codex app-server rate-limit API when available, with legacy transcript/history parsing kept as a fallback.
75
+ - **Banked reset visibility.** Eligible Codex accounts show the number of manually redeemable bonus resets in `cdx status` and expose reset details in JSON output.
76
+
77
+ ### Isolated multi-account sessions
78
+
79
+ ![cdx: every registered session with its provider, state, pinned launch settings and age](docs/assets/list.png)
80
+
35
81
  - **Multiple providers, one tool.** Register as many Codex, Claude, Antigravity, or Ollama sessions as you need. Codex and Claude get isolated auth environments; Antigravity is launchable through `agy` with OS-keyring auth; Ollama runs local models through `ollama run`.
36
82
  - **Instant launch.** `cdx work` opens your "work" session. `cdx personal` opens another. No config files to edit mid-flow.
37
83
  - **Quick relaunch.** `cdx last` reopens the most recently launched assistant profile.
84
+ - **Resume by identity, not by recency.** Each session records the provider's own conversation id — imposed at launch for Claude, read back from the rollout for Codex — so `cdx resume <name>` reopens *that* conversation regardless of your working directory or of which session ran last. `cdx can-resume --json` reports the strategy and where the id came from, so an automated caller can tell a named resume from a best-effort one.
38
85
  - **Auth guardrails.** `cdx` checks authentication before launching. If a session is not logged in, it tells you exactly what to run — no silent failures.
39
- - **Optional session labels.** `cdx label <name> <label>` adds a short human label; list and full status tables show a `LABEL` column only when at least one session has one.
40
- - **Usage at a glance.** `cdx status` shows token usage, 5-hour window quota, weekly quota, last-updated timestamps, priority guidance, and the last launched session in one aligned table.
41
- - **Next-ready notification.** `cdx ready` schedules a native system notification for the next assistant that comes back from cooldown, then returns immediately.
42
- - **Session control.** Disable a session without deleting it when an account is temporarily out of credits; disabled sessions remain visible and sort last.
43
86
  - **Persistent launch settings.** Pin per-session power, permission, and fast-mode preferences once; `cdx` reapplies them on every launch until you unset them.
87
+ - **Spend ceiling and model fallback.** `cdx set <name> --budget 5` caps what a headless run may spend, and `--fallback-model sonnet,haiku` lets a run continue on the next model when the first is overloaded. Both are Claude Code flags that the provider accepts with `--print` only, so they apply to `cdx run` and not to interactive launches; `cdx configs` says so.
88
+ - **Session control.** Disable a session without deleting it when an account is temporarily out of credits; disabled sessions remain visible and sort last.
89
+ - **Optional session labels.** `cdx label <name> <label>` adds a short human label; list and full status tables show a `LABEL` column only when at least one session has one.
90
+ - **Clean removal.** `cdx rmv` wipes a session and its entire auth directory. No orphaned files, no stale credentials.
91
+
92
+ ### Headless automation
93
+
94
+ - **One task, one JSON result.** `cdx run` executes a prompt against a chosen or auto-selected session and returns a stable payload; `--detach` returns a `run_id` immediately.
95
+ - **Survives a rate limit.** `cdx run --failover` notices that a run stopped because its account ran out, moves the task to the next account the ranking allows, and carries on — one `run_id` for the whole task, with every account it occupied listed in `cdx run-report`. A spent account is always confirmed against its own refreshed quota first, so a healthy account is never abandoned. From there two routes migrate a run: the provider said so in words cdx recognises, or — since that wording differs between an enterprise workspace out of credits, a personal plan hitting its 5-hour window, and Claude's own phrasing — simply that the account's quota is gone and the run failed. `cdx run-report` names which route applied. Exhausting every account reports `failover_exhausted`, which a caller can tell apart from the task itself failing.
96
+ - **Native background delegation (opt-in).** With `CDX_EXPERIMENTAL_NATIVE_BG=1`, `cdx run --detach` hands the run to Claude Code's own background agents (`--bg`) instead of cdx's detached launcher. The run keeps its cdx `run_id`, closes out with its token usage and final answer read from the provider's own session transcript, and the parked session is stopped once its task is done. Off by default for one release while it settles; the in-house launcher is unchanged and is what every run takes otherwise. Codex keeps the in-house path while its `cloud`/`exec-server` surfaces are marked experimental.
97
+ - **Observable runs.** `cdx run-status`, `cdx run-tail`, `cdx run-report`, and `cdx runs --since` follow a run while it happens and after it ends.
98
+ - **Selection without launching.** `cdx select --provider ... --require-ready --json` answers "which session should this job use?" for orchestrators.
99
+ - **Escape hatch for unmapped flags.** `cdx set <name> --extra-args "--add-dir ../shared"` passes provider arguments cdx does not model straight through, so a flag your provider shipped last week never forces you to abandon cdx. These arguments are explicitly unvalidated: cdx does not parse or verify them, `cdx schema --json` lists them under `unvalidated` rather than describing them, and `cdx doctor --check-provider-flags` ignores them. They are split into literal argv entries and never passed through a shell.
100
+ - **Validated contract.** `cdx schema --json` publishes the enums, mutually-exclusive argument groups, and error codes programmatic callers should validate against.
101
+
102
+ ### Context handoff
103
+
104
+ - **Shared handoff context.** Keep a per-workspace Markdown context, or build one from a source session transcript, and install it into another assistant session before switching providers or accounts.
105
+ - **Session transcript capture.** Every launch is recorded to a local log file via `script`, giving you a full terminal transcript for each session.
106
+
107
+ ### Maintenance
108
+
44
109
  - **Launch history.** Inspect recent launches with provider, result, duration, working directory, launch settings, and transcript path.
110
+ - **Aggregate usage.** `cdx stats` totals runs, token usage, and time spent per session over any period.
111
+
112
+ ![cdx stats: runs, input and output tokens, total time and last use per session](docs/assets/stats.png)
113
+
45
114
  - **Disk usage and cleanup.** `cdx disk` reports `CDX_HOME` usage, `cdx disk profiles --candidates` identifies reclaimable profile caches/logs with evidence, and `cdx clean profiles ...` applies explicit cleanup actions.
115
+ - **Health and repair.** `cdx doctor` inspects dependencies, permissions, and orphan profiles; `cdx repair` plans or applies safe fixes.
116
+ - **Portable bundles.** `cdx export` / `cdx import` move sessions between machines, with passphrase-encrypted auth data when you ask for it.
46
117
  - **Update prompts.** Periodic update checks surface `cdx update` directly in the `cdx`, `cdx status`, and launch output when a newer release is available. When `logics-manager` is installed, `cdx` can also suggest `logics-manager self-update`.
47
118
  - **Logics viewer shortcut.** `cdx view` opens the Logics browser/focus viewer through `logics-manager view` when the companion CLI is installed. All viewer flags are forwarded: `--lan`, `--lan-rw`, `--focus <ref>`, `--read`, `--port`, `--host`, `--refresh-interval`, `--tls`, `--tls-cert`, `--tls-key`, `--open`, `--no-open`. `cdx view --json` reports availability and update diagnostics without opening the viewer.
48
- - **Shared handoff context.** Keep a per-workspace Markdown context, or build one from a source session transcript, and install it into another assistant session before switching providers or accounts.
49
- - **Passive status resolution.** Codex status is read from the local Codex app-server rate-limit API when available, with legacy transcript/history parsing kept as a fallback.
50
- - **Banked reset visibility.** Eligible Codex accounts show the number of manually redeemable bonus resets in `cdx status` and expose reset details in JSON output.
51
- - **Session transcript capture.** Every launch is recorded to a local log file via `script`, giving you a full terminal transcript for each session.
52
- - **Clean removal.** `cdx rmv` wipes a session and its entire auth directory. No orphaned files, no stale credentials.
53
119
 
54
120
  ---
55
121
 
@@ -357,8 +423,8 @@ cdx history --summary --from 2026-05-01 --to 2026-05-28
357
423
  | `cdx config <name> [--json]` | Show persistent launch settings for a session |
358
424
  | `cdx configs [--json]` | Show persistent launch settings for all sessions in one table |
359
425
  | `cdx power\|perm\|fast\|model <name\|all\|provider:PROVIDER\|a,b> <value\|default> [--json]` | Shortcut commands for setting or clearing one launch setting |
360
- | `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 |
361
- | `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 |
426
+ | `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] [--fallback-model MODEL[,MODEL...]] [--budget USD] [--extra-args 'ARGS'] [--priority 0..100] [--json]` | Persist launch settings for one or more sessions |
427
+ | `cdx unset <name>\|--sessions all\|a,b\|--provider PROVIDER (--power\|--permission\|--fast\|--rtk\|--logics\|--model\|--fallback-model\|--budget\|--extra-args\|--priority\|--all) [--json]` | Remove persisted launch settings and fall back to provider defaults |
362
428
  | `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 |
363
429
  | `cdx last [--json]` | Launch the most recent existing session from launch history |
364
430
  | `cdx resume <name> [--json]` | Resume the provider-native conversation for a session using the named command form |
@@ -384,7 +450,7 @@ cdx history --summary --from 2026-05-01 --to 2026-05-28
384
450
  | `cdx notify --next-ready [--poll seconds] [--once] [--schedule] [--refresh] [--json]` | Wait until the recommended session is usable, or schedule the next known reset notification |
385
451
  | `cdx next [--json] [--refresh]` | Select the best next assistant using the same priority logic as `cdx status` |
386
452
  | `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 |
387
- | `cdx run [session] --cwd PATH (--prompt-file PATH\|--prompt TEXT\|--prompt-file -) [--provider PROVIDER] [--model MODEL] [--reasoning-effort minimal\|low\|medium\|high\|xhigh] [--power minimal\|low\|medium\|high\|xhigh] [--permission MODE] [--timeout-seconds N] [--detach] [--refresh] --json` | Run one headless task and return a stable JSON result; `--detach` returns the `run_id` at launch without waiting, `--prompt-file -` reads the prompt from stdin |
453
+ | `cdx run [session] --cwd PATH (--prompt-file PATH\|--prompt TEXT\|--prompt-file -) [--provider PROVIDER] [--model MODEL] [--reasoning-effort minimal\|low\|medium\|high\|xhigh] [--power minimal\|low\|medium\|high\|xhigh] [--permission MODE] [--timeout-seconds N] [--detach] [--failover] [--refresh] --json` | Run one headless task and return a stable JSON result; `--detach` returns the `run_id` at launch without waiting, `--failover` continues the task on the next account when this one hits its rate limit, `--prompt-file -` reads the prompt from stdin |
388
454
  | `cdx run-report <run_id> --json` | Full report, transcript metadata, and final payload for a run |
389
455
  | `cdx run-status <run_id> --json` | Status of one run by id |
390
456
  | `cdx run-tail <run_id> [--lines N] --json` | Last lines of a run's own output, while it is still running or after it finished |
@@ -0,0 +1,139 @@
1
+ # CDX Manager 0.15.0
2
+
3
+ > Quota awareness stops being advice. A headless run that hits a rate limit now
4
+ > continues on an account that still has quota, and `cdx resume` reopens the
5
+ > conversation cdx named rather than whatever ran here last.
6
+
7
+ ## Read this first
8
+
9
+ Two defaults change for existing sessions.
10
+
11
+ - **`cdx resume <name>` changes meaning.** It used to run `codex resume --last`
12
+ or `claude --continue`, both of which mean "the most recent conversation in
13
+ this directory". It now names the provider's own conversation id, so it
14
+ reopens the conversation *that session* last had, regardless of your working
15
+ directory or of what ran since. Sessions with no recorded id keep the old
16
+ behaviour; `cdx can-resume --json` tells you which applies.
17
+ - **Every Claude launch now carries `--session-id`.** cdx mints the id and
18
+ passes it, which is what makes the above possible.
19
+
20
+ ## Highlights
21
+
22
+ - `cdx run --failover` continues a task on the next eligible account when the
23
+ one it started on runs out, keeping one `run_id` for the whole task.
24
+ - `cdx set --budget` caps what a headless run may spend; `--fallback-model`
25
+ lets it continue on another model when the first is unavailable.
26
+ - `cdx set --extra-args` passes provider flags cdx does not model straight
27
+ through, so a flag your provider shipped last week no longer forces you out
28
+ of cdx.
29
+ - Resume and handoff are anchored on a provider-native conversation id instead
30
+ of on recency.
31
+ - Opt-in delegation of `cdx run --detach` to Claude Code's own background
32
+ agents, behind `CDX_EXPERIMENTAL_NATIVE_BG=1`.
33
+
34
+ ## Changes
35
+
36
+ ### Failover across accounts
37
+
38
+ `cdx run --failover` detects that a run stopped because its account is spent,
39
+ re-ranks the remaining sessions through the same ranking `cdx next` uses,
40
+ carries the task across, and continues. The whole task keeps one `run_id`;
41
+ `cdx run-report` lists every session it occupied, in order, with the reason for
42
+ each transition, and `cdx runs` shows it once.
43
+
44
+ Migration requires the account's own refreshed rate-limit status to confirm it
45
+ is spent, because the two ways of being wrong are not symmetric: missing a rate
46
+ limit costs nothing new, while inventing one would move a healthy run off a
47
+ working account and spend a second account's quota redoing it.
48
+
49
+ Two routes trigger it. `provider_reported_exhaustion` when the provider says so
50
+ in words cdx recognises, and `account_status_exhausted` when the quota is gone
51
+ and the run failed, whatever the wording. The second exists because the wording
52
+ is not one thing — an enterprise workspace out of credits, a personal plan
53
+ hitting its 5-hour window, and Claude's own phrasing are different sentences,
54
+ and only the first has been observed.
55
+
56
+ Exhausting every eligible account reports `failover_exhausted`, and hitting the
57
+ transition bound reports `failover_limit_reached`; both are distinct from the
58
+ task itself failing. `--failover` and `--detach` are mutually exclusive:
59
+ failover has to watch the run to react.
60
+
61
+ ### Spend ceiling and model fallback
62
+
63
+ `cdx set <name> --budget 5` and `--fallback-model sonnet,haiku` persist like
64
+ every other launch setting. Both are Claude Code flags the provider accepts
65
+ only alongside `--print`, so they apply to `cdx run` and to nothing else;
66
+ `cdx configs` says so rather than letting a launch fail. `--fallback-model`
67
+ takes a comma-separated list, tried in order.
68
+
69
+ ### Resume by identity
70
+
71
+ Sessions record the provider's own conversation id and where it came from.
72
+ Claude accepts a caller-supplied id, so cdx mints it at launch — provenance
73
+ `imposed`. Codex mints its own, so cdx reads it back from the rollout after a
74
+ run — provenance `observed`, and legitimately absent until the session has run
75
+ once. `cdx can-resume --json` reports the strategy, the id, and its provenance,
76
+ so an automated caller can tell a named resume from a best-effort one.
77
+
78
+ ### Passthrough for unmapped provider flags
79
+
80
+ `cdx set <name> --extra-args "--add-dir ../shared"` reaches the provider
81
+ command line unmodified, after the arguments cdx maps itself, so a passthrough
82
+ value overrides the setting it duplicates. These arguments are explicitly
83
+ unvalidated: `cdx schema --json` lists them under `unvalidated` rather than
84
+ describing them, and `cdx doctor --check-provider-flags` ignores them. They are
85
+ split into literal argv entries and never passed through a shell.
86
+
87
+ ### Background delegation (opt-in)
88
+
89
+ With `CDX_EXPERIMENTAL_NATIVE_BG=1`, `cdx run --detach` hands the run to Claude
90
+ Code's background agents. The run keeps its cdx `run_id`, closes out with the
91
+ token usage and final answer read from the provider's own session transcript,
92
+ and the parked session is stopped once its task is done. Off by default for
93
+ this release: it works, but it has been exercised far less than the in-house
94
+ launcher every run takes today. Codex keeps the in-house path while its
95
+ `cloud` and `exec-server` surfaces are marked experimental.
96
+
97
+ ### Onboarding and docs
98
+
99
+ `cdx` with no sessions registered now points at `cdx add` instead of listing
100
+ ten commands that all return nothing. The README leads with the quota routing
101
+ rather than with session management, and its screenshots are generated from a
102
+ synthetic home (`scripts/seed_demo_home.py`) so they carry no account names.
103
+
104
+ ### Fixes
105
+
106
+ - A status merge adopted the candidate's timestamp even when it was older,
107
+ dating a record by its stalest contributor. Since `updated_at` drives the
108
+ freshness check, a just-resolved status read as expired and re-probed the
109
+ provider on every call.
110
+
111
+ ## Validation
112
+
113
+ - `npm run lint`
114
+ - `npm test` — 629 tests.
115
+ - CI green on ubuntu-latest and windows-latest.
116
+ - Also run on a physical Windows machine: 623 passed, 5 skipped. One failure,
117
+ `test_concurrent_starts_do_not_lose_records`, reproduces identically on the
118
+ `v0.14.0` tag on that machine and passes in CI — a pre-existing contention
119
+ flake in the registry lock, not a regression.
120
+ - End-to-end across eleven live sessions and both providers: fourteen real
121
+ runs, usage extracted on every one, six Codex accounts each recording a
122
+ distinct conversation id and five Claude accounts recording theirs.
123
+ - End-to-end failover on a genuinely exhausted account: `work6` migrated to
124
+ `work1` and the task succeeded, recorded as one run.
125
+ - End-to-end resume by identity: a minted id accepted as `--session-id`, then
126
+ `--resume` on that id recalling the previous turn's answer.
127
+ - `--extra-args` and the budget/fallback settings exercised against real
128
+ provider CLIs, not only in tests.
129
+ - `logics-manager lint --require-status`
130
+ - `logics-manager audit --group-by-doc` — 0 blocking, 0 warnings.
131
+
132
+ ## Known gaps
133
+
134
+ - The rate-limit wording of a personal-plan Codex account and of Claude has not
135
+ been observed; only an enterprise workspace out of credits has. The
136
+ `account_status_exhausted` route covers the unobserved cases, so this costs
137
+ the precise reason rather than the failover itself.
138
+ - Background delegation has been exercised on one account with one prompt
139
+ shape, which is why it is opt-in.
@@ -160,6 +160,18 @@
160
160
  "v0.12.2": {
161
161
  "github_tarball_sha256": "5214f9f85f6bdc2c53bded938c91e28411de3c573bbcff81ebecb20dea6d99c2",
162
162
  "github_zip_sha256": "8f447cdea9e921f7428c1c72db5a19f40f447a877bb2bc8dc1c8bd5e34daf070"
163
+ },
164
+ "v0.12.3": {
165
+ "github_tarball_sha256": "b41cf8a99c36d3f03a3556db604316dcfdf14e2eb9090c86f25190b8a0db866a",
166
+ "github_zip_sha256": "6a8e384a88e736607dbc0b6e9953fb61192e77ded259fac2de35b03ad2fa71bd"
167
+ },
168
+ "v0.12.4": {
169
+ "github_tarball_sha256": "877e54c9f435acb41947c43c8618c84d88e0dc0c7cb77c5dd5dc56e13ee80b7f",
170
+ "github_zip_sha256": "7db7f28ab8b295446ba0a38e6e3cbbcc8855dd7333ebc9877917d14b4caaf903"
171
+ },
172
+ "v0.14.0": {
173
+ "github_tarball_sha256": "5da107556442d3566a7b0787708ae700e38afdf99e74209fffd2484ce3a33d94",
174
+ "github_zip_sha256": "bd4e070582bfc641e6176a8b8be8d7afd1ce9d76fc59c1dbeb238bae548bc699"
163
175
  }
164
176
  }
165
177
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cdx-manager",
3
- "version": "0.14.0",
3
+ "version": "0.15.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.14.0"
7
+ version = "0.15.0"
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_args.py CHANGED
@@ -8,6 +8,7 @@ imported back there and used by the handlers.
8
8
  from datetime import datetime, timedelta
9
9
 
10
10
  from .config import (
11
+ MAX_LAUNCH_BUDGET_USD,
11
12
  PERMISSION_ALIASES,
12
13
  PERMISSION_INPUT_VALUES,
13
14
  PERMISSION_VALUES,
@@ -30,8 +31,8 @@ CONTEXT_USAGE = "Usage: cdx context show|path|init|edit|clear|set|append [text..
30
31
  MEMORY_USAGE = "Usage: cdx memory [--global|--project NAME_OR_PATH] [show|view|path|init|edit|clear|set|append|list] [text...] [--json]"
31
32
  HANDOFF_USAGE = "Usage: cdx handoff <name> [--json] | cdx handoff <source> <target> [--json]"
32
33
  LABEL_USAGE = "Usage: cdx label <name> <label> [--json] | cdx label <name> --clear [--json]"
33
- 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]"
34
- UNSET_USAGE = "Usage: cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--reasoning-effort|--permission|--fast|--rtk|--logics|--model|--priority|--all) [--json]"
34
+ 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] [--fallback-model MODEL[,MODEL...]] [--budget USD] [--extra-args 'ARGS'] [--priority 0..100] [--json]"
35
+ UNSET_USAGE = "Usage: cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--reasoning-effort|--permission|--fast|--rtk|--logics|--model|--fallback-model|--budget|--extra-args|--priority|--all) [--json]"
35
36
  SETTING_ALIAS_USAGE = "Usage: cdx power|perm|fast|model <name|all|provider:PROVIDER|a,b> <value|default> [--json]"
36
37
  CONFIG_USAGE = "Usage: cdx config <name> [--json]"
37
38
  CONFIGS_USAGE = "Usage: cdx configs [--json]"
@@ -42,12 +43,13 @@ RESUME_USAGE = "Usage: cdx resume <name> [--json]"
42
43
  CAN_RESUME_USAGE = "Usage: cdx can-resume <name> [--json]"
43
44
  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"
44
45
  NEXT_USAGE = "Usage: cdx next [--json] [--refresh]"
45
- RUN_USAGE = "Usage: cdx run [session] --cwd PATH (--prompt-file PATH|--prompt TEXT|--prompt-file -) [--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] [--detach] [--refresh] --json"
46
+ RUN_USAGE = "Usage: cdx run [session] --cwd PATH (--prompt-file PATH|--prompt TEXT|--prompt-file -) [--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] [--detach] [--failover] [--refresh] --json"
46
47
  RUN_JSON_REQUIRED = "cdx run: --json is required."
47
48
  RUN_TARGET_REQUIRED = "cdx run: specify a session name or --provider PROVIDER."
48
49
  RUN_SESSION_PROVIDER_CONFLICT = "cdx run: cannot specify both a session name and --provider."
49
50
  RUN_CWD_REQUIRED = "cdx run: --cwd PATH is required."
50
51
  RUN_PROMPT_SOURCE_REQUIRED = "cdx run: specify exactly one prompt source: --prompt TEXT or --prompt-file PATH."
52
+ RUN_DETACH_FAILOVER_CONFLICT = "cdx run: --failover needs to watch the run, which --detach gives up."
51
53
  RUN_KIND_VALUES = ("assistant", "code-review")
52
54
  # Aliases of the shared definitions in config, kept only so the existing
53
55
  # RUN_* spellings in this module keep reading naturally. They are references,
@@ -228,6 +230,17 @@ def _parse_priority_value(value):
228
230
  return priority
229
231
 
230
232
 
233
+ def _parse_budget_value(value):
234
+ try:
235
+ budget = float(value)
236
+ except (TypeError, ValueError) as error:
237
+ raise CdxError(SET_USAGE) from error
238
+ # NaN compares false against every bound, so reject it before the range check.
239
+ if budget != budget or budget <= 0 or budget > MAX_LAUNCH_BUDGET_USD:
240
+ raise CdxError(SET_USAGE)
241
+ return budget
242
+
243
+
231
244
  def _parse_set_args(args):
232
245
  parsed = _parse_flag_args(args, {
233
246
  "--power": {"key": "power", "type": "str", "default": None},
@@ -236,6 +249,9 @@ def _parse_set_args(args):
236
249
  "--rtk": {"key": "rtk", "type": "str", "default": None, "transform": _parse_fast_value},
237
250
  "--logics": {"key": "logics", "type": "str", "default": None, "transform": _parse_fast_value},
238
251
  "--model": {"key": "model", "type": "str", "default": None},
252
+ "--fallback-model": {"key": "fallback_model", "type": "str", "default": None},
253
+ "--budget": {"key": "budget", "type": "str", "default": None, "transform": _parse_budget_value},
254
+ "--extra-args": {"key": "extra_args", "type": "str", "default": None},
239
255
  "--priority": {"key": "priority", "type": "str", "default": None, "transform": _parse_priority_value},
240
256
  "--sessions": {"key": "sessions", "type": "str", "default": None, "transform": _parse_set_unset_sessions},
241
257
  "--provider": {"key": "provider", "type": "str", "default": None, "transform": lambda value: _parse_provider_filter(value, SET_USAGE)},
@@ -253,7 +269,7 @@ def _parse_set_args(args):
253
269
  raise CdxError(SET_USAGE)
254
270
  settings = {
255
271
  key: parsed[key]
256
- for key in ("power", "permission", "fast", "rtk", "logics", "model", "priority")
272
+ for key in ("power", "permission", "fast", "rtk", "logics", "model", "fallback_model", "budget", "extra_args", "priority")
257
273
  if parsed[key] is not None
258
274
  }
259
275
  if not settings:
@@ -276,6 +292,9 @@ def _parse_unset_args(args):
276
292
  "--rtk": {"key": "rtk", "type": "bool", "default": False},
277
293
  "--logics": {"key": "logics", "type": "bool", "default": False},
278
294
  "--model": {"key": "model", "type": "bool", "default": False},
295
+ "--fallback-model": {"key": "fallback_model", "type": "bool", "default": False},
296
+ "--budget": {"key": "budget", "type": "bool", "default": False},
297
+ "--extra-args": {"key": "extra_args", "type": "bool", "default": False},
279
298
  "--priority": {"key": "priority", "type": "bool", "default": False},
280
299
  "--all": {"key": "all", "type": "bool", "default": False},
281
300
  "--sessions": {"key": "sessions", "type": "str", "default": None, "transform": _parse_set_unset_sessions},
@@ -292,9 +311,11 @@ def _parse_unset_args(args):
292
311
  raise CdxError(UNSET_USAGE)
293
312
  if not parsed["names"] and not parsed["sessions"] and not parsed["provider"]:
294
313
  raise CdxError(UNSET_USAGE)
295
- keys = ["power", "reasoning_effort", "permission", "fast", "rtk", "logics", "model", "priority"] if parsed["all"] else [
296
- key for key in ("power", "reasoning_effort", "permission", "fast", "rtk", "logics", "model", "priority") if parsed[key]
297
- ]
314
+ _UNSET_KEYS = (
315
+ "power", "reasoning_effort", "permission", "fast", "rtk", "logics",
316
+ "model", "fallback_model", "budget", "extra_args", "priority",
317
+ )
318
+ keys = list(_UNSET_KEYS) if parsed["all"] else [key for key in _UNSET_KEYS if parsed[key]]
298
319
  if not keys:
299
320
  raise CdxError(UNSET_USAGE)
300
321
  return {
@@ -426,6 +447,16 @@ def cdx_schema():
426
447
  "kind": {"accepted": list(RUN_KIND_VALUES)},
427
448
  "provider": {"accepted": list(PROVIDERS)},
428
449
  },
450
+ # Passthrough arguments are deliberately absent from this contract.
451
+ # `cdx set --extra-args` reaches the provider unchanged and cdx never
452
+ # validates it, so publishing it here would claim a guarantee that does
453
+ # not exist. A caller must treat it as opaque.
454
+ "unvalidated": {
455
+ "extra_args": {
456
+ "reason": "Passed to the provider CLI unchanged; cdx neither parses nor verifies these arguments.",
457
+ "not_checked_by": ["cdx schema", "cdx doctor --check-provider-flags"],
458
+ },
459
+ },
429
460
  "mutually_exclusive": [
430
461
  {
431
462
  "command": "run",
@@ -437,6 +468,11 @@ def cdx_schema():
437
468
  "arguments": ["--prompt-file", "--prompt"],
438
469
  "reason": "Exactly one prompt source.",
439
470
  },
471
+ {
472
+ "command": "run",
473
+ "arguments": ["--detach", "--failover"],
474
+ "reason": "Failover has to watch the run to react to a rate limit; --detach stops watching.",
475
+ },
440
476
  ],
441
477
  # Not mutually exclusive: both may be given, and cdx accepts them when
442
478
  # they agree. A caller generating validation from `mutually_exclusive`
@@ -554,6 +590,7 @@ def _parse_run_args(args):
554
590
  "--permission": {"key": "permission", "type": "str", "default": None, "transform": _normalize_run_permission},
555
591
  "--timeout-seconds": {"key": "timeout_seconds", "type": "str", "default": None, "transform": _parse_run_timeout_seconds},
556
592
  "--detach": {"key": "detach", "type": "bool", "default": False},
593
+ "--failover": {"key": "failover", "type": "bool", "default": False},
557
594
  "--refresh": {"key": "refresh", "type": "bool", "default": False},
558
595
  "--json": {"key": "json", "type": "bool", "default": False},
559
596
  }, RUN_USAGE, positionals_key="names", max_positionals=1)
@@ -576,6 +613,12 @@ def _parse_run_args(args):
576
613
  code=ARG_CODE_MUTUALLY_EXCLUSIVE,
577
614
  arguments=["--prompt-file", "--prompt"],
578
615
  )
616
+ if parsed["detach"] and parsed["failover"]:
617
+ raise CdxArgumentError(
618
+ RUN_DETACH_FAILOVER_CONFLICT,
619
+ code=ARG_CODE_MUTUALLY_EXCLUSIVE,
620
+ arguments=["--detach", "--failover"],
621
+ )
579
622
  if not parsed["prompt_file"] and not parsed["prompt"]:
580
623
  raise CdxArgumentError(
581
624
  RUN_PROMPT_SOURCE_REQUIRED,
@@ -602,6 +645,7 @@ def _parse_run_args(args):
602
645
  "permission": parsed["permission"],
603
646
  "timeout_seconds": parsed["timeout_seconds"],
604
647
  "detach": parsed["detach"],
648
+ "failover": parsed["failover"],
605
649
  "refresh": parsed["refresh"],
606
650
  "reasoning_effort": effort.get("reasoning_effort"),
607
651
  "power": effort.get("power"),
@@ -487,6 +487,9 @@ def _format_launch_config(session, use_color=False):
487
487
  ("rtk", "RTK"),
488
488
  ("logics", "Logics"),
489
489
  ("model", "Model"),
490
+ ("fallback_model", "Fallback"),
491
+ ("budget", "Budget"),
492
+ ("extra_args", "Extra args"),
490
493
  ("priority", "Priority"),
491
494
  ]:
492
495
  rows.append([
@@ -520,6 +523,10 @@ def _format_launch_setting_value(launch, key, use_color=False):
520
523
  value = launch.get(key)
521
524
  if value is None or value == "":
522
525
  return _dim("default", use_color)
526
+ if key == "budget":
527
+ from .provider_runtime import _format_budget_arg
528
+
529
+ return _style(_format_budget_arg(value), "33", use_color)
523
530
  if key == "priority":
524
531
  return _style(str(value), "33", use_color)
525
532
  if key == "power":
@@ -535,6 +542,12 @@ def _resume_capability_for_session(session, ctx):
535
542
  "provider": session["provider"],
536
543
  "resumable": bool(capability.get("resumable")),
537
544
  "strategy": capability.get("strategy"),
545
+ # A caller deciding whether to trust a resume needs to tell a named
546
+ # conversation from "whatever ran here last", and where that name came
547
+ # from: Claude's is imposed at launch, Codex's is observed afterwards
548
+ # and is legitimately absent until the session has run once.
549
+ "identity": capability.get("identity"),
550
+ "provenance": capability.get("provenance"),
538
551
  "reason": capability.get("reason"),
539
552
  "command_preview": capability.get("command_preview") or [],
540
553
  }
package/src/cli_render.py CHANGED
@@ -120,8 +120,25 @@ def format_error(error, env=None, stderr=None):
120
120
  return _style(str(error), "31", _should_use_color(env or os.environ, stderr or sys.stderr))
121
121
 
122
122
 
123
+ def _format_onboarding(use_color=False):
124
+ return "\n".join([
125
+ _style("No sessions yet.", "1", use_color),
126
+ "",
127
+ "A session is an isolated account profile for a provider CLI.",
128
+ "Register your first one, then launch it by name:",
129
+ "",
130
+ f" {_style('cdx add codex work', '36', use_color)} {_dim('register a Codex session named work', use_color)}",
131
+ f" {_style('cdx work', '36', use_color)} {_dim('log in if needed, then launch it', use_color)}",
132
+ "",
133
+ f"{_dim('Providers: codex, claude, antigravity, ollama (ollama needs --model).', use_color)}",
134
+ f"{_dim('Run cdx help for the full command list.', use_color)}",
135
+ ])
136
+
137
+
123
138
  def _format_sessions(service, use_color=False):
124
139
  rows = service["format_list_rows"]()
140
+ if not rows:
141
+ return _format_onboarding(use_color=use_color)
125
142
  has_provider = any(r.get("provider") for r in rows)
126
143
  has_label = any(r.get("label") for r in rows)
127
144
  has_launch = any(r.get("launch") for r in rows)