cdx-manager 0.25.0 → 0.25.2

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
@@ -4,7 +4,7 @@
4
4
 
5
5
  <br clear="left"/>
6
6
 
7
- [![License](https://img.shields.io/badge/license-MIT-4C8BF5)](LICENSE) ![Version](https://img.shields.io/badge/version-v0.25.0-4C8BF5) ![Python](https://img.shields.io/badge/python-3.9%2B-3776AB?logo=python&logoColor=white)
7
+ [![License](https://img.shields.io/badge/license-MIT-4C8BF5)](LICENSE) ![Version](https://img.shields.io/badge/version-v0.25.2-4C8BF5) ![Python](https://img.shields.io/badge/python-3.9%2B-3776AB?logo=python&logoColor=white)
8
8
 
9
9
  **The right AI coding account, when you need it.** CDX Manager keeps your Codex and Claude accounts in separate profiles, shows which ones have quota, and helps you pick the next usable session from your terminal.
10
10
 
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.25.0
1
+ 0.25.2
@@ -0,0 +1,14 @@
1
+ # CDX Manager 0.25.1
2
+
3
+ Release date: 2026-10-31
4
+
5
+ This patch release preserves shared Playwright browsers in isolated workspaces. (#73)
6
+
7
+ ## Fixes
8
+
9
+ - Force `PLAYWRIGHT_SKIP_BROWSER_GC=1` in isolated preparation and provider environments so Playwright does not delete shared browser revisions whose package references are only visible on the host. Browser installation remains available; cache cleanup remains an explicit operator operation. (#73)
10
+
11
+ ## Maintenance
12
+
13
+ - Record tray companion checksums for v0.25.0. (`c766e752cbf79c74bcc2a80ab39dc5f7364d7fd7`)
14
+ - Record release archive checksums for v0.25.0. (`feb0fbc276b0456d238cf64fada6aa34a96890fa`)
@@ -0,0 +1,15 @@
1
+ # CDX Manager 0.25.2
2
+
3
+ Release date: 2026-10-31
4
+
5
+ This patch release improves usage attribution and makes unverifiable run budgets fail explicitly.
6
+
7
+ ## Fixes
8
+
9
+ - Preserve observed Codex model evidence for usage pricing. Keep costs unknown when model evidence is missing or mixed without per-model counters, and fail unverifiable budgets with `budget_unverifiable` and exit code 125. (#76)
10
+ - Attribute CDX usage to canonical source repositories so project statistics group runs by their source repository rather than isolated execution directories. (#75)
11
+
12
+ ## Maintenance
13
+
14
+ - Record tray companion checksums for v0.25.1. (`e3530977b2b95cd3ec39bbe962725f275fef4370`)
15
+ - Record release archive checksums for v0.25.1. (`59b5d276abc5877554d9ffc44b468b53b12dd1d4`)
@@ -683,6 +683,28 @@
683
683
  "x86_64-pc-windows-msvc": "21dfebd0265bd8705d381517357cd8166cb0e2da37b27ed3e3485c892d9289ee",
684
684
  "x86_64-unknown-linux-musl": "8477262e55f31ee2217382048b967846478a34f222beeb4242fa9c4c2942f747"
685
685
  }
686
+ },
687
+ "v0.25.0": {
688
+ "github_tarball_sha256": "5372027a03afb12763993816b8421ca9387ca5d6d4eb5508e9b915fe08344957",
689
+ "github_zip_sha256": "2ca9873cff13f5fcf526fa3275bf9735f6a19fff2dbf1d98b95f891de2bbf448",
690
+ "tray_assets": {
691
+ "aarch64-apple-darwin": "7138e93a8ec02ed3435b012be22ddc58cf758eb692ca930769be4b8493ee6ca3",
692
+ "aarch64-unknown-linux-musl": "40ffad4815394fddc9e410cef2ba5c12af05bc364bbb3f510c630f59bd1e8559",
693
+ "x86_64-apple-darwin": "e8587030436e7134e9889c7a73e86ac62632b0b79331531c1e5e475f932f3ad5",
694
+ "x86_64-pc-windows-msvc": "2493b0c8212ccdbf5e69756af07afe47c73474407f79604d671142d5192aa642",
695
+ "x86_64-unknown-linux-musl": "53024f4908847452ce6ac399ca03725b95b628df721856da6e2ed9399691455f"
696
+ }
697
+ },
698
+ "v0.25.1": {
699
+ "github_tarball_sha256": "7899a8cb3f3eb2d83d10b921a4c22c16f1d7eba4681c564b7b09812699892ad6",
700
+ "github_zip_sha256": "19f9646e1e74761f588b9ef195a2a90914a48ad2621a67f9d85057417db8cd19",
701
+ "tray_assets": {
702
+ "aarch64-apple-darwin": "d5d5fd19ad3a2e8bd3bcd72a959a217bbfa2fea351bcd739b94265ceb1fdd2c4",
703
+ "aarch64-unknown-linux-musl": "e8824cd7ec91e81bf7be1f2af53689de0aa8bbb9f7037d9e2e51fed35837a8a3",
704
+ "x86_64-apple-darwin": "d507a6f35894a7125cef7dc67f98e485e08774221d9dc92698c11b0f9c5354c3",
705
+ "x86_64-pc-windows-msvc": "183ef3664489888a356ad9a6096b68c913d433c513d6d10e9890e65667e1e417",
706
+ "x86_64-unknown-linux-musl": "5660cfd08c0580db43dee5664fe95fc4cd528a6495c40167e34c122d3c4fcd4a"
707
+ }
686
708
  }
687
709
  }
688
710
  }
package/docs/cli.md CHANGED
@@ -28,6 +28,7 @@
28
28
  | `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] [--notify on\|off] [--agent-state on\|off] [--notify-preview on\|off] [--scrub-env on\|off] [--shared-memory on\|off] [--retro on\|off] [--policy use\|ask-first\|never] [--model MODEL] [--fallback-model MODEL[,MODEL...]] [--budget USD] [--extra-args 'ARGS'] [--priority 0..100] [--json]` | Persist launch settings for one or more sessions |
29
29
  | `cdx unset <name>\|--sessions all\|a,b\|--provider PROVIDER (--power\|--reasoning-effort\|--permission\|--fast\|--rtk\|--logics\|--notify\|--agent-state\|--notify-preview\|--scrub-env\|--shared-memory\|--retro\|--policy\|--model\|--fallback-model\|--budget\|--extra-args\|--priority\|--all) [--json]` | Remove persisted launch settings and fall back to provider defaults |
30
30
  | `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 |
31
+ | `cdx history backfill-projects [--mapping FILE] [--dry-run \| --apply] [--json]` | Recover source repository attribution from exact recorded run evidence or an operator mapping; defaults to a read-only preview |
31
32
  | `cdx history backfill-models [--dry-run \| --apply] [--json]` | Recover missing historical usage models from evidence tied to each run; defaults to a read-only preview |
32
33
  | `cdx last [--json]` | Launch the most recent existing session from launch history |
33
34
  | `cdx resume <name> [--conversation <id>] [--json]` | Resume the provider-native conversation for a session using the named command form; `--conversation` reopens that exact conversation and refuses with `conversation_not_found` if the profile has no such transcript |
@@ -62,7 +63,7 @@
62
63
  | `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 |
63
64
  | `cdx runs [--limit N] [--since 7d\|today\|DATE] [--running] --json` | List recent runs; `--since` returns every run completed after the cursor and ignores `--limit`; `--running` returns every run still running, uncapped. A run's `kind` is one of the values `cdx schema --json` lists under `enums.kind` |
64
65
  | `cdx schema --json` | Publish the enums, mutually-exclusive argument groups, and error codes programmatic callers should validate against |
65
- | `cdx stats [name] [--since 7d\|today\|DATE] [--from DATE] [--to DATE] [--by session\|project\|model] [--json]` | Aggregate launch counts, duration, and known token usage by session, for headless and interactive runs alike. `--by project` groups runs by launch folder (labelled by the shortest trailing part that tells folders apart; `project` in JSON holds the full path), `--by model` by the model that served them (same totals, same pricing; default `session`). PROV. lists every provider a row saw, such as `claude+codex`. Without a session filter it also prints what rtk saved over the period (`rtk` in JSON), read with `rtk gain` from `RTK_DB_PATH` when set, else the shared history, and the older per-profile ones. |
66
+ | `cdx stats [name] [--since 7d\|today\|DATE] [--from DATE] [--to DATE] [--by session\|project\|model] [--json]` | Aggregate launch counts, duration, and known token usage by session, for headless and interactive runs alike. `--by project` groups runs by the recorded canonical source repository (labelled by the shortest distinguishing path suffix; `project` in JSON holds the canonical identity, `session_name` the display label, and `-` explicitly means unknown), `--by model` by the model that served them (same totals, same pricing; default `session`). PROV. lists every provider a row saw, such as `claude+codex`. Without a session filter it also prints what rtk saved over the period (`rtk` in JSON), read with `rtk gain` from `RTK_DB_PATH` when set, else the shared history, and the older per-profile ones. |
66
67
  | `cdx status [--json] [--refresh\|--cached] [--timeout SECONDS]` | Show token usage table for all sessions; empty credits, block, and reset columns are omitted; `--cached` skips live provider probes and returns only stored status; Claude rows also report the auth type (`setup-token` or `full-login`) in a TYPE column and as `auth_type` in JSON. |
67
68
  | `cdx status --small [--refresh\|--cached] [--timeout SECONDS]` / `cdx status -s [--refresh\|--cached] [--timeout SECONDS]` | Show compact token usage table without provider, blocking quota, credits, and updated columns |
68
69
  | `cdx status <name> [--json] [--refresh\|--cached] [--timeout SECONDS]` | Show detailed usage breakdown for one session; `--cached` avoids live provider refreshes |
@@ -253,7 +254,7 @@ At or above the limit, cdx terminates the provider process tree, skips further r
253
254
 
254
255
  With `--detach`, the launch still exits successfully and includes `budget_usd`; cdx always uses its own child supervisor for budgeted runs, including when native background delegation is enabled. Inspect `cdx run-status RUN_ID --json` or `cdx run-report RUN_ID --json` for the child's final payload and budget error. `--continue-run` starts a fresh budget for the new run, including in detached mode.
255
256
 
256
- Conservative assumption: this is a stop based on observed, priceable usage, not a prepaid billing limit. Provider updates can arrive late or only at completion, and termination can take time, so actual spend can exceed the amount. Missing usage, missing continuation boundaries, mixed/unobserved models, and unknown prices are not treated as free tokens and cannot trigger the budget; their estimated cost stays `null`. Supply a `CDX_TOKEN_PRICES` override for a model missing from the table. Existing profile-level `cdx set --budget` settings remain the provider-native Claude cap and apply independently.
257
+ Conservative assumption: this is a stop based on observed, priceable usage, not a prepaid billing limit. Provider updates can arrive late or only at completion, and termination can take time, so actual spend can exceed the amount. Before the first measurement, `budget_status` is `usage_not_measured`; absence is not zero spend. Once usage is measured, missing continuation boundaries, mixed/unobserved models, and unknown prices stop further execution/recovery with exit **125**, `error.source: "cdx"`, `error.code: "budget_unverifiable"`, `budget_status: "unverifiable"`, and `cost_usd: null`. This is a measurement failure, not a claim that the dollar limit was exceeded or enforced. Supply a `CDX_TOKEN_PRICES` override for a model missing from the table. Existing profile-level `cdx set --budget` settings remain the provider-native Claude cap and apply independently.
257
258
 
258
259
  Every usage record, whichever path produced it, uses one set of definitions:
259
260
 
@@ -289,6 +290,10 @@ The figure uses standard public list prices, excluding Fast/priority, Batch/Flex
289
290
 
290
291
  `cdx stats` counts completed history attempts. A headless failover has one logical run in `cdx runs` but one history row per attempted session, including failed attempts that reported usage. Period filtering includes the entire run in every period it overlaps, so adjacent daily results can both include a run crossing midnight; adding daily totals may exceed a combined period. JSON `accounting` names this policy and the current price-table source. `usage_runs` and `priced_runs` show how much of the token total has a dollar estimate. Live runs are absent until their history row is written, and unknown usage is different from measured zero.
291
292
 
293
+ Completed results, registry records, and history JSON include `cost_status` (`estimated` or `unknown`) and nullable `cost_reason`: `usage_not_measured`, `model_not_observed`, `model_price_unknown`, `attempt_usage_unavailable`, or `mixed_models_without_counters`. Stats include `unpriced_reasons` counts alongside `priced_runs` and `unpriced_models`. These are list-price estimates, not billing receipts. `budget_status` is `not_requested`, `usage_not_measured`, `estimated`, `exceeded`, or `unverifiable`.
294
+
295
+ New headless attempts have distinct `attempt_id` values and artifact paths, including retries on the same profile. Final payloads and registry reports expose `attempts` with their individual model evidence, counters, and estimates; top-level usage describes the final attempt, while a budget-exceeded spend includes all attempts. New isolated Codex attempts mount only a fresh sessions directory from volatile `/dev/shm` storage. The supervisor reads the exact `thread.started` rollout while it exists and at closeout, requiring matching session identity, turn timestamps within this attempt, and token counters consistent with stdout. It exports `usage_evidence` containing only the stdout attempt identity, thread id, timestamp bounds, turn ids, observed models, and normalized counters; live rollout counters can establish the first measurement before `turn.completed`. No requested model, agent-authored cost report, current profile setting, prompt, message, or tool output is used as model evidence. The raw temporary sessions are removed at closeout, including cancellation and startup failure; they are never copied into persistent run artifacts. Host profiles are not mounted. Mixed serving models stay unpriced without attributable counters. Isolated continuation without comparable launch boundaries stays unmeasured rather than charging an older conversation to the new attempt.
296
+
292
297
  Completed run JSON and registry reports expose `usage_model` separately from the requested `model`. Pricing uses the observed model from provider output; Codex can also supply it through the exact rollout identified by `thread.started.thread_id`. Configured defaults alone never establish serving-model identity, and missing or mixed model evidence stays unpriced. `--by model` groups Claude's `[1m]` context variant with its base model while preserving the original model string and per-run pricing. New runs record this evidence automatically. To recover missing models in existing history, run:
293
298
 
294
299
  ```bash
@@ -297,9 +302,9 @@ cdx history backfill-models --dry-run --json # Machine-readable preview
297
302
  cdx history backfill-models --apply # Save proven models with a backup
298
303
  ```
299
304
 
300
- The command considers only history rows with usage and an empty or placeholder `usage_model`. Evidence is checked in order: the row's preserved JSON/JSONL stdout, a preserved run-registry report with the same run id, profile, provider and stdout path, then the exact Codex thread rollout or Claude session transcript. Thread/session ids must come from the row or its stdout, or from a preserved transcript path recorded as an exact conversation-id match. The current profile is used only to locate an exact transcript under its auth home; its configured model and latest conversation/transcript never establish a historical model. The requested `model` in a report is also ignored.
305
+ The command considers only history rows with usage and an empty or placeholder `usage_model`. Evidence is checked in order: a supervisor-preserved isolated observation tied to the exact stdout attempt and counters, the row's preserved JSON/JSONL stdout, a preserved run-registry report with the same run id, profile, provider and stdout path, then the exact Codex thread rollout or Claude session transcript. Thread/session ids must come from the row or its stdout, or from a preserved transcript path recorded as an exact conversation-id match. The current profile is used only to locate an exact transcript under its auth home; its configured model and latest conversation/transcript never establish a historical model. The requested `model` in a report is also ignored.
301
306
 
302
- Only one serving model is accepted. Claude `[1m]` variants count as the same model for this check and for stats grouping; the observed spelling is preserved. Mixed models, incomplete turn models, malformed or oversized evidence, conflicting identities and missing exact transcripts stay unknown. For resumed conversations, the conservative rule is that all recorded turns in the exact transcript must agree: this command does not infer historical turn boundaries. Ambiguous higher-priority evidence stops recovery rather than being overridden by a lower-priority source. Missing files may fall through to the next source.
307
+ Run recovery in dry-run mode first and review every evidence kind and unknown reason before applying. A missing isolated rollout with no preserved observation cannot be reconstructed from a current profile: its historical model and cost remain unknown. No historical recovery is applied automatically. Only one serving model is accepted. Claude `[1m]` variants count as the same model for this check and for stats grouping; the observed spelling is preserved. Mixed models, incomplete turn models, malformed or oversized evidence, conflicting identities and missing exact transcripts stay unknown. For resumed conversations, the conservative rule is that all recorded turns in the exact transcript must agree: this command does not infer historical turn boundaries. Ambiguous higher-priority evidence stops recovery rather than being overridden by a lower-priority source. Missing files may fall through to the next source.
303
308
 
304
309
  Text and JSON output include counts by evidence kind, counts of unknown reasons and up to 20 samples. Dry-run leaves history and run reports unchanged. `--apply` changes only missing models and their evidence annotation, preserving token counts and unrelated history bytes. It holds the history lock, saves the original bytes to a unique `state/launch_history.jsonl.models-<id>.bak` file, then atomically replaces history. The backup path is printed; a repeated apply leaves already recovered rows alone. `--apply` and `--dry-run` cannot be combined. Subsequent `cdx stats` calls price recovered models when the local price table knows them; unknown models remain unpriced.
305
310
 
@@ -459,3 +464,84 @@ It returns accepted values for `permission` (with its aliases and canonical form
459
464
  Codex ties network access to its sandbox, so any permission below `full` leaves the run unable to resolve DNS — `gh`, `curl`, and package installs fail inside an otherwise successful run. Show these warnings to whoever reads your output rather than discarding them; a run that succeeds while quietly doing less is the case they exist for.
460
465
 
461
466
  Claude result records with nonempty `permission_denials` produce a warning with code `permission_denials`, the `provider`, and the structured `permission_denials` list. Exit code 0 still means `ok: true`: a denied tool may have been replaced successfully. Empty or absent denials add no warning. With `--scrub-env on`, Claude forces its default permission mode; allow tools needed by unattended runs explicitly using `--allowedTools` via `--extra-args` (see [operations](operations.md)).
467
+
468
+ ### Source repository attribution and external launchers
469
+
470
+ CDX captures `project` (an absolute canonical source repository path) and
471
+ `project_source` before starting a provider or applying workspace isolation.
472
+ Normal Git launches use the repository root, including launches from subfolders.
473
+ Linked Git worktrees use their common Git directory's source repository, so
474
+ normal and worktree launches share an identity even after worktree deletion.
475
+ Unrelated repositories with the same basename retain separate full-path keys.
476
+ Without Git evidence or an explicit source, attribution is unknown (`project:
477
+ null`, `project_source: "unknown"` in history; `project: "-"` in project stats).
478
+ Old cwd-only history also stays unknown: stats never inspect today's filesystem
479
+ or profile settings to guess yesterday's project.
480
+
481
+ Trusted producers such as Jarvis/remote-ops must resolve the source repository
482
+ **before** creating a disposable mission directory, worktree, or temporary clone,
483
+ and pass it to the installed CDX launcher:
484
+
485
+ ```bash
486
+ cdx run work --cwd /temporary/mission --project /repos/remote-ops --prompt 'Fix the issue' --isolate-workspace --json
487
+ cdx work --dir /temporary/mission --project /repos/remote-ops
488
+ ```
489
+
490
+ `--project` is a trusted operator assertion, not provider output, a profile
491
+ setting, or an environment variable. It must be an absolute normalized path,
492
+ with symlinks already resolved by the producer; it may refer to a deleted source.
493
+ A plain temporary clone has no reliable link to a local source repository: the
494
+ producer must supply `--project` rather than expecting inference from a remote
495
+ URL or directory naming convention. Existing launch-directory validation and
496
+ execution `cwd` are unchanged. CDX records the separate identity in history,
497
+ runtime/registry records, and launch/result JSON. Failover attempts and detached
498
+ children retain the launch identity. `--continue-run` inherits the parent's
499
+ recorded identity (including unknown) and rejects a different `--project`.
500
+ Neither continuation nor backfill reattributes an older run from current profile
501
+ configuration.
502
+
503
+ Consumers should use stats `project` as the stable grouping key and
504
+ `session_name` as the disambiguated display label. In particular, dependent
505
+ remote-ops `scripts/cdx-stats.py` should not reduce `project` to `Path(...).name`:
506
+ that loses disambiguation between unrelated repositories named alike. Token,
507
+ duration, cost, and attempt counts keep the existing accounting rules; the
508
+ existing `totals.sessions` field counts groups for the chosen grouping.
509
+
510
+ ### Recover historical project attribution
511
+
512
+ ```bash
513
+ cdx history backfill-projects --mapping projects.json # Preview (default)
514
+ cdx history backfill-projects --mapping projects.json --dry-run --json
515
+ cdx history backfill-projects --mapping projects.json --apply
516
+ ```
517
+
518
+ The mapping format is deliberately generic, with exact CDX run IDs, not mission
519
+ IDs, profile names, prefixes, or cwd values:
520
+
521
+ ```json
522
+ {"version":1,"projects":[{"run_id":"exact CDX run id","project":"/repos/remote-ops"}]}
523
+ ```
524
+
525
+ Operators must build this file from attributable external evidence, such as an
526
+ exact launch receipt. CDX never imports Jarvis, scans its private state, guesses
527
+ from disposable path prefixes, or resolves deleted worktrees. The supplied
528
+ Tower observation (84 disposable groups attributed by receipts to remote-ops
529
+ 66, cdx-manager 10, logics-manager 6, city-jump 2) is diagnostic evidence, not a
530
+ mapping: actual exact CDX run IDs must be obtained on Tower before repair.
531
+
532
+ Without a mapping, the command can recover only from trusted project fields
533
+ already preserved for the exact run in CDX history or its run registry. A cwd-only
534
+ report is insufficient evidence. All attempts sharing an exact run ID must agree.
535
+ Invalid schema, contradictory duplicate mapping entries, and unknown exact run
536
+ IDs fail validation. An attribution conflict with known history or recorded run
537
+ evidence blocks the entire batch, preserving the original history and returning
538
+ exit status 1 with the conflict counts.
539
+
540
+ Results report `recovered` (recoverable history rows), `unchanged`, `unresolved`,
541
+ and `conflicting` counts, plus `applied` and `backup_path`. Counts are history
542
+ attempts, matching existing history accounting. Dry runs write nothing. Apply
543
+ holds the existing history lock, writes an original-byte `.projects-*.bak`
544
+ backup, and atomically replaces history only when the complete batch has no
545
+ conflict. Unrelated fields, usage, and malformed/unrelated history lines survive.
546
+ Repeating the same mapping is a no-op and creates no additional backup. The
547
+ registry and provider transcripts are never rewritten by backfill.
@@ -240,3 +240,12 @@ credential sync.
240
240
  - **`cdx login` hangs** — the provider's login flow requires a browser or device code. Follow the on-screen instructions in the terminal that opened.
241
241
  - **`make install` says `npm link` is not found** — ensure Node.js and npm are installed and in your PATH.
242
242
  - **On Windows, `doctor` warns that `script` is missing** — this is expected on many setups. Codex still launches, but transcript capture stays disabled unless you point `CDX_SCRIPT_BIN` to a compatible wrapper.
243
+
244
+
245
+ ### Playwright in isolated workspaces
246
+
247
+ Isolated preparation and provider environments force `PLAYWRIGHT_SKIP_BROWSER_GC=1`.
248
+ The browser cache is shared with host services, but package paths differ inside
249
+ `/cache` and on the host. Playwright garbage collection cannot see every user's
250
+ package references and must not delete other users' browser revisions. Browser
251
+ installation remains available; cache cleanup is an explicit operator operation.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cdx-manager",
3
- "version": "0.25.0",
3
+ "version": "0.25.2",
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.25.0"
7
+ version = "0.25.2"
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
@@ -245,7 +245,7 @@ def _print_help(use_color=False):
245
245
  f" {_style('cdx reset <name> [--yes] [--json]', '36', use_color)}",
246
246
  f" {_style('cdx auth refresh <name|all> [--json]', '36', use_color)}",
247
247
  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)}",
248
- f" {_style('cdx run [session] --cwd PATH (--prompt-file PATH|--prompt TEXT|--prompt-file -) [--provider PROVIDER] [--model MODEL] [--kind assistant|code-review|retro] [--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] [--label TEXT] [--max-parallel N] [--continue-run RUN_ID] [--detach] [--failover] [--refresh] --json', '36', use_color)}",
248
+ f" {_style('cdx run [session] --cwd PATH [--project PATH] (--prompt-file PATH|--prompt TEXT|--prompt-file -) [--provider PROVIDER] [--model MODEL] [--kind assistant|code-review|retro] [--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] [--label TEXT] [--max-parallel N] [--continue-run RUN_ID] [--detach] [--failover] [--refresh] --json', '36', use_color)}",
249
249
  f" {_style('cdx runs [--limit N] [--since 7d|today|DATE] [--running] --json', '36', use_color)}",
250
250
  f" {_style('cdx run-status <run_id> --json', '36', use_color)}",
251
251
  f" {_style('cdx run-report <run_id> --json', '36', use_color)}",
@@ -260,10 +260,11 @@ def _print_help(use_color=False):
260
260
  f" {_style('cdx power|perm|fast|model <name|all|provider:PROVIDER|a,b> <value|default> [--json]', '36', use_color)}",
261
261
  f" {_style(SET_USAGE[len('Usage: '):], '36', use_color)}",
262
262
  f" {_style('cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--permission|--fast|--rtk|--logics|--notify|--notify-preview|--model|--priority|--all) [--json]', '36', use_color)}",
263
+ f" {_style('cdx history backfill-projects [--mapping FILE] [--dry-run|--apply] [--json]', '36', use_color)}",
263
264
  f" {_style('cdx history [name] [--limit N] [--summary] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]', '36', use_color)}",
264
265
  f" {_style('cdx stats [name] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--by session|project|model] [--json]', '36', use_color)}",
265
266
  f" {_style('cdx last [--json]', '36', use_color)}",
266
- f" {_style('cdx resume <name> [--conversation <id>] [--json]', '36', use_color)}",
267
+ f" {_style('cdx resume <name> [--project PATH] [--conversation <id>] [--json]', '36', use_color)}",
267
268
  f" {_style('cdx can-resume <name> [--conversation <id>] [--json]', '36', use_color)}",
268
269
  f" {_style('cdx rescue [<session>] [--to <target>] [--yes] [--dry-run] [--json]', '36', use_color)}",
269
270
  f" {_style('cdx retro [--limit N] [--json]', '36', use_color)}",
@@ -295,7 +296,7 @@ def _print_help(use_color=False):
295
296
  _style("Notifications:", "1", use_color),
296
297
  f" {_style('cdx set <name> --notify on', '36', use_color)} # agent finishes or needs attention",
297
298
  f" {_style('cdx ready', '36', use_color)} # next session to become ready",
298
- f" {_style('cdx <name> [--on-busy fail|ask] [--json]', '36', use_color)}",
299
+ f" {_style('cdx <name> [--project PATH] [--on-busy fail|ask] [--json]', '36', use_color)}",
299
300
  " --on-busy: fail immediately if already running; ask (default) prompts only with a terminal and without --json",
300
301
  f" {_style('cdx <name> --prompt TEXT|--prompt-file PATH', '36', use_color)} # start with a first message",
301
302
  f" {_style('cdx <name> --dir PATH --task TEXT --end-state TEXT', '36', use_color)} # start on a task, with what the go-ahead covers",
@@ -637,7 +638,8 @@ def main(argv, options=None):
637
638
  if handler:
638
639
  return handler(rest, ctx)
639
640
 
640
- from .commands.launch import _parse_launch_args, _read_launch_prompt
641
+ from .commands.launch import _parse_launch_args, _pop_value_flag, _read_launch_prompt
642
+ project_path, rest = _pop_value_flag(rest, "--project", "Usage: cdx <name> [--project PATH]")
641
643
  directory, launch_args, prompt, on_busy = _parse_launch_args(rest)
642
644
  if all(arg in ("--json", "-r", "--resume") for arg in launch_args):
643
645
  resume = "-r" in launch_args or "--resume" in launch_args
@@ -648,7 +650,7 @@ def main(argv, options=None):
648
650
  if has_prompt and (ctx["service"]["get_session"](command) or {}).get("provider") == PROVIDER_OLLAMA:
649
651
  raise CdxError("cdx <name>: --prompt and --prompt-file are not supported for Ollama sessions.")
650
652
  return handle_launch(command, ctx, initial_prompt=_read_launch_prompt(prompt), resume=resume,
651
- directory=directory, on_busy=on_busy)
653
+ directory=directory, on_busy=on_busy, project_path=project_path)
652
654
 
653
655
  raise CdxError(f"Unknown command: {command}. Use cdx --help.")
654
656
 
package/src/cli_args.py CHANGED
@@ -43,12 +43,12 @@ HISTORY_USAGE = "Usage: cdx history [name] [--limit N] [--summary] [--since 7d|t
43
43
  STATS_USAGE = "Usage: cdx stats [name] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--by session|project|model] [--json]"
44
44
  STATS_GROUPS = ("session", "project", "model")
45
45
  LAST_USAGE = "Usage: cdx last [--json]"
46
- RESUME_USAGE = "Usage: cdx resume <name> [--conversation <id>] [--on-busy fail|ask] [--json]"
46
+ RESUME_USAGE = "Usage: cdx resume <name> [--project PATH] [--conversation <id>] [--on-busy fail|ask] [--json]"
47
47
  CAN_RESUME_USAGE = "Usage: cdx can-resume <name> [--conversation <id>] [--json]"
48
48
  SESSIONS_USAGE = "Usage: cdx sessions [--all] --json"
49
49
  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"
50
50
  NEXT_USAGE = "Usage: cdx next [--json] [--refresh]"
51
- RUN_USAGE = "Usage: cdx run [session] (--cwd|--dir) PATH (--prompt-file PATH|--prompt TEXT|--prompt-file -) [--provider PROVIDER] [--model MODEL] [--kind assistant|code-review|retro] [--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] [--network-access] [--isolate-workspace] [--timeout-seconds N] [--budget USD] [--label TEXT] [--max-parallel N] [--continue-run RUN_ID] [--detach] [--failover] [--refresh] --json"
51
+ RUN_USAGE = "Usage: cdx run [session] (--cwd|--dir) PATH [--project PATH] (--prompt-file PATH|--prompt TEXT|--prompt-file -) [--provider PROVIDER] [--model MODEL] [--kind assistant|code-review|retro] [--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] [--network-access] [--isolate-workspace] [--timeout-seconds N] [--budget USD] [--label TEXT] [--max-parallel N] [--continue-run RUN_ID] [--detach] [--failover] [--refresh] --json"
52
52
  RUN_JSON_REQUIRED = "cdx run: --json is required."
53
53
  RUN_TARGET_REQUIRED = "cdx run: specify a session name or --provider PROVIDER."
54
54
  RUN_SESSION_PROVIDER_CONFLICT = "cdx run: cannot specify both a session name and --provider."
@@ -552,6 +552,7 @@ def cdx_schema():
552
552
  "provider_failed",
553
553
  "provider_timeout",
554
554
  "budget_exceeded",
555
+ "budget_unverifiable",
555
556
  "run_cancelled",
556
557
  "too_many_runs",
557
558
  "failover_exhausted",
@@ -672,6 +673,7 @@ def _parse_max_parallel(value):
672
673
 
673
674
  def _parse_run_args(args):
674
675
  parsed = _parse_run_flag_args(args, {
676
+ "--project": {"key": "project", "type": "str", "default": None},
675
677
  "--cwd": {"key": "cwd", "type": "str", "default": None},
676
678
  "--dir": {"key": "cwd", "type": "str", "default": None},
677
679
  "--prompt-file": {"key": "prompt_file", "type": "str", "default": None},
@@ -735,6 +737,7 @@ def _parse_run_args(args):
735
737
  "name": name,
736
738
  "provider": parsed["provider"],
737
739
  "cwd": parsed["cwd"],
740
+ "project": parsed["project"],
738
741
  "prompt_file": parsed["prompt_file"],
739
742
  "prompt": parsed["prompt"],
740
743
  "continue_run": parsed["continue_run"],
@@ -36,7 +36,7 @@ from ..provider_runtime import (
36
36
  from ..run_command import PROMPT_STDIN, read_run_prompt
37
37
  from ..run_usage import estimate_cost, format_cache_ratio
38
38
 
39
- LAUNCH_USAGE = "Usage: cdx <name> [--dir PATH] [--prompt TEXT|--prompt-file PATH|--task TEXT [--end-state TEXT]] [--resume] [--on-busy fail|ask] [--json]"
39
+ LAUNCH_USAGE = "Usage: cdx <name> [--dir PATH] [--project PATH] [--prompt TEXT|--prompt-file PATH|--task TEXT [--end-state TEXT]] [--resume] [--on-busy fail|ask] [--json]"
40
40
 
41
41
  #: The first message of a session started with --task. The end state is what the
42
42
  #: user's go-ahead covers: a bare "show me first" made sessions stop after a commit
@@ -206,6 +206,7 @@ def handle_can_resume(rest, ctx):
206
206
  return 0
207
207
 
208
208
  def handle_resume(rest, ctx):
209
+ project_path, rest = _pop_value_flag(rest, "--project", RESUME_USAGE)
209
210
  conversation_id, rest = _pop_value_flag(rest, "--conversation", RESUME_USAGE)
210
211
  directory, rest, prompt, on_busy = _parse_launch_args(rest)
211
212
  if prompt["prompt"] is not None or prompt["prompt_file"] is not None:
@@ -214,7 +215,7 @@ def handle_resume(rest, ctx):
214
215
  if len(args) != 1:
215
216
  raise CdxError(RESUME_USAGE)
216
217
  return handle_launch(args[0], ctx, resume=True, force_json=json_flag, directory=directory,
217
- conversation_id=conversation_id, on_busy=on_busy)
218
+ conversation_id=conversation_id, on_busy=on_busy, project_path=project_path)
218
219
 
219
220
 
220
221
  def _notification_provisioning_enabled(session, ctx, json_flag):
@@ -258,7 +259,7 @@ def _link_shared_memory(session, ctx, cwd, json_flag):
258
259
 
259
260
 
260
261
  def handle_launch(command, ctx, initial_prompt=None, resume=False, force_json=None, directory=None,
261
- conversation_id=None, on_busy="ask"):
262
+ conversation_id=None, on_busy="ask", project_path=None):
262
263
  json_flag = "--json" in ctx.get("raw_args", ctx["options"].get("raw_args", []))
263
264
  if force_json is not None:
264
265
  json_flag = force_json
@@ -278,7 +279,10 @@ def handle_launch(command, ctx, initial_prompt=None, resume=False, force_json=No
278
279
  raise CdxError(
279
280
  f"Provider {session['provider']} does not support native resume through cdx."
280
281
  )
282
+ from ..project_identity import resolve_project
283
+
281
284
  cwd = _launch_directory(session, ctx, directory, json_flag)
285
+ project = resolve_project(cwd, project_path)
282
286
  session = ctx["service"]["launch_session"](command, resume=resume)
283
287
  if conversation_id:
284
288
  session = with_requested_conversation(session, conversation_id)
@@ -330,7 +334,7 @@ def handle_launch(command, ctx, initial_prompt=None, resume=False, force_json=No
330
334
  def runtime_lifecycle(event, info):
331
335
  nonlocal runtime_run_id
332
336
  if event == "started":
333
- runtime = ctx["service"]["start_session_runtime"](session["name"], {**info, "cwd": cwd})
337
+ runtime = ctx["service"]["start_session_runtime"](session["name"], {**info, "cwd": cwd, **project})
334
338
  runtime_run_id = runtime.get("runId")
335
339
  elif event == "finished" and runtime_run_id:
336
340
  ctx["service"]["finish_session_runtime"](
@@ -382,6 +386,8 @@ def handle_launch(command, ctx, initial_prompt=None, resume=False, force_json=No
382
386
  "error": str(error),
383
387
  "exit_code": error.exit_code,
384
388
  **run_info,
389
+ **project,
390
+ "run_id": runtime_run_id,
385
391
  })
386
392
  raise
387
393
  success_run_info = _attach_interactive_usage(
@@ -395,13 +401,15 @@ def handle_launch(command, ctx, initial_prompt=None, resume=False, force_json=No
395
401
  "cwd": cwd,
396
402
  "exit_code": 0,
397
403
  **success_run_info,
404
+ **project,
405
+ "run_id": runtime_run_id,
398
406
  })
399
407
  if not json_flag:
400
408
  ctx["out"](f"{_goodbye_line(session, success_run_info, ctx['use_color'])}\n")
401
409
  if retro.start(session, cwd, success_run_info.get("provider_transcript_path"), env=ctx.get("env")) and not json_flag:
402
410
  ctx["out"](f"{_dim('Session retro started; read it later with `cdx retro`', ctx['use_color'])}\n")
403
411
  if json_flag:
404
- extra = {"session": ctx["service"]["get_session"](session["name"]), "cwd": cwd}
412
+ extra = {"session": ctx["service"]["get_session"](session["name"]), "cwd": cwd, **project, "run_id": runtime_run_id}
405
413
  if capability:
406
414
  extra["resume"] = capability
407
415
  _write_json(ctx, _json_success("resume" if resume else "launch", message, warnings=warnings, **extra))