cdx-manager 0.7.1 → 0.7.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 +16 -5
- package/changelogs/CHANGELOGS_0_7_2.md +41 -0
- package/checksums/release-archives.json +4 -0
- package/install.ps1 +5 -5
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/cli.py +9 -4
- package/src/cli_commands.py +190 -8
- package/src/health.py +8 -0
- package/src/provider_runtime.py +18 -0
- package/src/session_service.py +13 -1
- package/src/update_manager.py +80 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# CDX Manager
|
|
2
2
|
|
|
3
|
-
[](LICENSE) ](LICENSE)  
|
|
4
4
|
|
|
5
5
|
**Run multiple Codex, Claude, Antigravity, and Ollama sessions from one terminal. Switch between accounts instantly.**
|
|
6
6
|
|
|
@@ -134,7 +134,7 @@ For a specific version:
|
|
|
134
134
|
|
|
135
135
|
```bash
|
|
136
136
|
curl -fsSL https://raw.githubusercontent.com/AlexAgo83/cdx-manager/main/install.sh -o install.sh
|
|
137
|
-
CDX_VERSION=v0.7.
|
|
137
|
+
CDX_VERSION=v0.7.2 sh install.sh
|
|
138
138
|
```
|
|
139
139
|
|
|
140
140
|
From source:
|
|
@@ -264,6 +264,7 @@ cdx set personal --power low --permission review
|
|
|
264
264
|
cdx set --sessions all --permission auto
|
|
265
265
|
cdx set --provider ollama --model llama3.2
|
|
266
266
|
cdx set work --priority 80
|
|
267
|
+
cdx set work --rtk on
|
|
267
268
|
cdx power all low
|
|
268
269
|
cdx perm provider:claude review
|
|
269
270
|
cdx model provider:ollama llama3.2
|
|
@@ -277,12 +278,13 @@ cdx unset work --power
|
|
|
277
278
|
cdx unset --sessions work,personal --fast
|
|
278
279
|
cdx unset --provider claude --permission
|
|
279
280
|
cdx unset work --priority
|
|
281
|
+
cdx unset work --rtk
|
|
280
282
|
cdx unset work --all
|
|
281
283
|
cdx power all default
|
|
282
284
|
cdx model provider:ollama default
|
|
283
285
|
```
|
|
284
286
|
|
|
285
|
-
`--power` maps to Codex `model_reasoning_effort` and Claude `--effort`. `--permission` maps to provider-native permission flags. `--fast on` uses low effort when no explicit power is set. `--priority` is a 0..100 selector preference used as a tie-breaker after readiness and availability.
|
|
287
|
+
`--power` maps to Codex `model_reasoning_effort` and Claude `--effort`. `--permission` maps to provider-native permission flags. `--fast on` uses low effort when no explicit power is set. `--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.
|
|
286
288
|
|
|
287
289
|
### Launch History
|
|
288
290
|
|
|
@@ -319,8 +321,8 @@ cdx history --summary --from 2026-05-01 --to 2026-05-28
|
|
|
319
321
|
| `cdx enable <name> [--json]` | Re-enable a disabled session |
|
|
320
322
|
| `cdx config <name> [--json]` | Show persistent launch settings for a session |
|
|
321
323
|
| `cdx power\|perm\|fast\|model <name\|all\|provider:PROVIDER\|a,b> <value\|default> [--json]` | Shortcut commands for setting or clearing one launch setting |
|
|
322
|
-
| `cdx set <name>\|--sessions all\|a,b\|--provider PROVIDER [--power low\|medium\|high\|xhigh\|max] [--permission review\|default\|auto\|full] [--fast on\|off] [--model MODEL] [--priority 0..100] [--json]` | Persist launch settings for one or more sessions |
|
|
323
|
-
| `cdx unset <name>\|--sessions all\|a,b\|--provider PROVIDER (--power\|--permission\|--fast\|--model\|--priority\|--all) [--json]` | Remove persisted launch settings and fall back to provider defaults |
|
|
324
|
+
| `cdx set <name>\|--sessions all\|a,b\|--provider PROVIDER [--power low\|medium\|high\|xhigh\|max] [--permission review\|default\|auto\|full] [--fast on\|off] [--rtk on\|off] [--model MODEL] [--priority 0..100] [--json]` | Persist launch settings for one or more sessions |
|
|
325
|
+
| `cdx unset <name>\|--sessions all\|a,b\|--provider PROVIDER (--power\|--permission\|--fast\|--rtk\|--model\|--priority\|--all) [--json]` | Remove persisted launch settings and fall back to provider defaults |
|
|
324
326
|
| `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 |
|
|
325
327
|
| `cdx last [--json]` | Launch the most recent existing session from launch history |
|
|
326
328
|
| `cdx context show\|path\|init\|edit\|clear\|set [text...] [--json]` | Manage the shared Markdown context for the current workspace |
|
|
@@ -338,6 +340,7 @@ cdx history --summary --from 2026-05-01 --to 2026-05-28
|
|
|
338
340
|
| `cdx notify --next-ready [--poll seconds] [--once] [--schedule] [--refresh] [--json]` | Wait until the recommended session is usable, or schedule the next known reset notification |
|
|
339
341
|
| `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 |
|
|
340
342
|
| `cdx run [session] --cwd PATH (--prompt-file PATH\|--prompt TEXT) [--provider PROVIDER] [--model MODEL] [--reasoning-effort low\|medium\|high] [--power low\|medium\|high] [--permission MODE] [--timeout-seconds N] --json` | Run one headless task and return a stable JSON result |
|
|
343
|
+
| `cdx stats [name] [--since 7d\|today\|DATE] [--from DATE] [--to DATE] [--json]` | Aggregate launch counts, duration, and known headless token usage by session |
|
|
341
344
|
| `cdx status [--json] [--refresh]` | Show token usage table for all sessions; JSON returns a versioned payload with structured warnings |
|
|
342
345
|
| `cdx status --small [--refresh]` / `cdx status -s [--refresh]` | Show compact token usage table without provider, blocking quota, credits, and updated columns |
|
|
343
346
|
| `cdx status <name> [--json] [--refresh]` | Show detailed usage breakdown for one session |
|
|
@@ -369,6 +372,7 @@ Commands with machine-readable output:
|
|
|
369
372
|
- `cdx context ... --json`
|
|
370
373
|
- `cdx handoff ... --json`
|
|
371
374
|
- `cdx history ... --json`
|
|
375
|
+
- `cdx stats ... --json`
|
|
372
376
|
- `cdx last --json`
|
|
373
377
|
- `cdx doctor --json`
|
|
374
378
|
- `cdx repair --json`
|
|
@@ -439,6 +443,13 @@ cdx run \
|
|
|
439
443
|
|
|
440
444
|
The result includes `launcher: "cdx"`, `run_id`, selected `session`, `provider`, `exit_code`, `duration_seconds`, absolute `transcript_path`, `stdout_path`, `stderr_path`, and normalized usage token fields. Codex headless runs use `codex exec --json`; Claude headless runs use `claude --print --output-format json`. Token counts are `null` when the provider does not expose a supported JSON or JSONL usage shape.
|
|
441
445
|
|
|
446
|
+
Known headless token usage is persisted in launch history and can be aggregated later:
|
|
447
|
+
|
|
448
|
+
```bash
|
|
449
|
+
cdx stats --since 7d --json
|
|
450
|
+
cdx stats work
|
|
451
|
+
```
|
|
452
|
+
|
|
442
453
|
`cdx select` exposes the same session selection logic directly:
|
|
443
454
|
|
|
444
455
|
```bash
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Changelog (`0.7.1 -> 0.7.2`)
|
|
2
|
+
|
|
3
|
+
Release date: 2026-05-29
|
|
4
|
+
|
|
5
|
+
## Update Reliability
|
|
6
|
+
|
|
7
|
+
- Fixed Unix standalone update detection for installs under `~/.local/share/cdx-manager/<version>` so `cdx update` uses the standalone installer instead of incorrectly upgrading npm.
|
|
8
|
+
- Added a post-update PATH verification check that runs the resolved `cdx -v` and warns when another older executable shadows the updated install.
|
|
9
|
+
- Reduced the environment passed to the post-update version check so sensitive variables are not forwarded to the verification subprocess.
|
|
10
|
+
- Aligned the PowerShell standalone installer with the Windows launcher by accepting `py`, `python`, or `python3`.
|
|
11
|
+
|
|
12
|
+
## Headless Usage and Stats
|
|
13
|
+
|
|
14
|
+
- Persisted normalized token usage from `cdx run --json` into launch history for future aggregation.
|
|
15
|
+
- Added `cdx stats [name] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]` to aggregate launches, successes, failures, duration, known usage runs, and token totals by session.
|
|
16
|
+
- Continued extracting provider stdout usage even when a headless provider exits with an error, preserving useful usage data from structured failure payloads.
|
|
17
|
+
|
|
18
|
+
## RTK Preference
|
|
19
|
+
|
|
20
|
+
- Added `cdx set <session> --rtk on|off` and `cdx unset <session> --rtk` to store a per-session preference that encourages assistants to use RTK for noisy terminal commands.
|
|
21
|
+
- Injected the RTK preference into interactive and headless launch prompts without rewriting commands automatically.
|
|
22
|
+
- Added `cdx doctor` detection for the `rtk` CLI.
|
|
23
|
+
|
|
24
|
+
## Coverage and Tests
|
|
25
|
+
|
|
26
|
+
- Added focused coverage for update checking, context storage, RTK launch preferences, post-update shadow warnings, and per-session stats aggregation.
|
|
27
|
+
- Fixed Windows CI portability in tests that exercise npm shim paths, version mismatch warnings, and context write byte counts.
|
|
28
|
+
- Increased the Python test suite to 260 tests.
|
|
29
|
+
|
|
30
|
+
## Release Metadata
|
|
31
|
+
|
|
32
|
+
- Updated package metadata, CLI version output, README badge, pinned installer example, and release changelog to `v0.7.2`.
|
|
33
|
+
|
|
34
|
+
## Validation and Regression Evidence
|
|
35
|
+
|
|
36
|
+
- `npm run lint`
|
|
37
|
+
- `npm test`
|
|
38
|
+
- `npm pack --dry-run`
|
|
39
|
+
- `node bin/cdx.js -v`
|
|
40
|
+
- `python3 -m build`
|
|
41
|
+
- `python3 -m twine check dist/*`
|
|
@@ -32,6 +32,10 @@
|
|
|
32
32
|
"v0.7.0": {
|
|
33
33
|
"github_tarball_sha256": "c80231884bf20c9ae74144a1ec16e0685c2fdac67d7d93a3c6219c5ac6fc14dd",
|
|
34
34
|
"github_zip_sha256": "0e6af29e59d3a93a07e07656d6c0c93803a674e65830ef33fa2322649b7139c2"
|
|
35
|
+
},
|
|
36
|
+
"v0.7.1": {
|
|
37
|
+
"github_tarball_sha256": "276cf2f094405bef674290ff8d1f2401f99afc10981c97efe4fe8293801715c8",
|
|
38
|
+
"github_zip_sha256": "5d5cdefec3ebcd61d4149b71895f4d5b79e604cf874b2d567910e578a8164670"
|
|
35
39
|
}
|
|
36
40
|
}
|
|
37
41
|
}
|
package/install.ps1
CHANGED
|
@@ -12,14 +12,14 @@ if (-not $ChecksumsUrl) {
|
|
|
12
12
|
$ChecksumsUrl = "https://raw.githubusercontent.com/$repo/main/checksums/release-archives.json"
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
function
|
|
15
|
+
function Has-Command {
|
|
16
16
|
param([string]$Name)
|
|
17
|
-
|
|
18
|
-
throw "cdx install: missing required command: $Name"
|
|
19
|
-
}
|
|
17
|
+
return [bool](Get-Command $Name -ErrorAction SilentlyContinue)
|
|
20
18
|
}
|
|
21
19
|
|
|
22
|
-
|
|
20
|
+
if (-not (Has-Command py) -and -not (Has-Command python) -and -not (Has-Command python3)) {
|
|
21
|
+
throw "cdx install: missing required command: py, python, or python3"
|
|
22
|
+
}
|
|
23
23
|
|
|
24
24
|
if (-not $Prefix) {
|
|
25
25
|
$Prefix = Join-Path $env:LOCALAPPDATA "cdx-manager"
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
package/src/cli.py
CHANGED
|
@@ -30,6 +30,7 @@ from .cli_commands import (
|
|
|
30
30
|
handle_rename,
|
|
31
31
|
handle_run,
|
|
32
32
|
handle_select,
|
|
33
|
+
handle_stats,
|
|
33
34
|
handle_status,
|
|
34
35
|
handle_set,
|
|
35
36
|
handle_unset,
|
|
@@ -57,7 +58,7 @@ from .status_view import (
|
|
|
57
58
|
)
|
|
58
59
|
from .update_check import check_for_update
|
|
59
60
|
|
|
60
|
-
VERSION = "0.7.
|
|
61
|
+
VERSION = "0.7.2"
|
|
61
62
|
|
|
62
63
|
|
|
63
64
|
# ---------------------------------------------------------------------------
|
|
@@ -79,9 +80,10 @@ def _print_help(use_color=False):
|
|
|
79
80
|
f" {_style('cdx context show|path|init|edit|clear|set [text...] [--json]', '36', use_color)}",
|
|
80
81
|
f" {_style('cdx config <name> [--json]', '36', use_color)}",
|
|
81
82
|
f" {_style('cdx power|perm|fast|model <name|all|provider:PROVIDER|a,b> <value|default> [--json]', '36', use_color)}",
|
|
82
|
-
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] [--model MODEL] [--priority 0..100] [--json]', '36', use_color)}",
|
|
83
|
-
f" {_style('cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--permission|--fast|--model|--priority|--all) [--json]', '36', use_color)}",
|
|
83
|
+
f" {_style('cdx set <name>|--sessions all|a,b|--provider PROVIDER [--power low|medium|high|xhigh|max] [--permission review|default|auto|full] [--fast on|off] [--rtk on|off] [--model MODEL] [--priority 0..100] [--json]', '36', use_color)}",
|
|
84
|
+
f" {_style('cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--permission|--fast|--rtk|--model|--priority|--all) [--json]', '36', use_color)}",
|
|
84
85
|
f" {_style('cdx history [name] [--limit N] [--summary] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]', '36', use_color)}",
|
|
86
|
+
f" {_style('cdx stats [name] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]', '36', use_color)}",
|
|
85
87
|
f" {_style('cdx last [--json]', '36', use_color)}",
|
|
86
88
|
f" {_style('cdx handoff <name> [--json]', '36', use_color)}",
|
|
87
89
|
f" {_style('cdx handoff <source> <target> [--json]', '36', use_color)}",
|
|
@@ -238,7 +240,7 @@ def main(argv, options=None):
|
|
|
238
240
|
"version": VERSION,
|
|
239
241
|
"cwd": options.get("cwd") or os.getcwd(),
|
|
240
242
|
"update_notice": _get_update_notice(service, env, options) if command not in (
|
|
241
|
-
"add", "cp", "ren", "rename", "mv", "rmv", "clean", "doctor", "repair", "update", "ready", "notify", "context", "config", "set", "unset", "power", "perm", "fast", "model", "history", "handoff", "login", "logout", "disable", "enable", "export", "import", "select", "run", "help", "version"
|
|
243
|
+
"add", "cp", "ren", "rename", "mv", "rmv", "clean", "doctor", "repair", "update", "ready", "notify", "context", "config", "set", "unset", "power", "perm", "fast", "model", "history", "stats", "handoff", "login", "logout", "disable", "enable", "export", "import", "select", "run", "help", "version"
|
|
242
244
|
) else None,
|
|
243
245
|
"use_color": use_color,
|
|
244
246
|
}
|
|
@@ -305,6 +307,9 @@ def main(argv, options=None):
|
|
|
305
307
|
if command == "history":
|
|
306
308
|
return handle_history(rest, ctx)
|
|
307
309
|
|
|
310
|
+
if command == "stats":
|
|
311
|
+
return handle_stats(rest, ctx)
|
|
312
|
+
|
|
308
313
|
if command == "last":
|
|
309
314
|
return handle_last(rest, ctx)
|
|
310
315
|
|
package/src/cli_commands.py
CHANGED
|
@@ -43,7 +43,7 @@ from .backup_bundle import read_bundle_meta
|
|
|
43
43
|
from .run_usage import empty_usage, extract_run_usage
|
|
44
44
|
from .status_view import _format_status_detail, _format_status_rows
|
|
45
45
|
from .update_check import LatestReleaseCheckError, fetch_latest_release, fetch_latest_release_or_raise, is_newer_version
|
|
46
|
-
from .update_manager import build_update_plan, format_update_failure, run_update_plan
|
|
46
|
+
from .update_manager import build_update_plan, format_update_failure, run_update_plan, verify_updated_command
|
|
47
47
|
|
|
48
48
|
|
|
49
49
|
STATUS_USAGE = "Usage: cdx status [--json] [--refresh] | cdx status --small|-s [--refresh] | cdx status <name> [--json] [--refresh]"
|
|
@@ -54,11 +54,12 @@ EXPORT_USAGE = "Usage: cdx export <file> [--include-auth] [--force] [--json] [--
|
|
|
54
54
|
IMPORT_USAGE = "Usage: cdx import <file> [--force] [--json] [--sessions name1,name2] [--passphrase-env VAR]"
|
|
55
55
|
CONTEXT_USAGE = "Usage: cdx context show|path|init|edit|clear|set [text...] [--json]"
|
|
56
56
|
HANDOFF_USAGE = "Usage: cdx handoff <name> [--json] | cdx handoff <source> <target> [--json]"
|
|
57
|
-
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] [--model MODEL] [--priority 0..100] [--json]"
|
|
58
|
-
UNSET_USAGE = "Usage: cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--permission|--fast|--model|--priority|--all) [--json]"
|
|
57
|
+
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] [--model MODEL] [--priority 0..100] [--json]"
|
|
58
|
+
UNSET_USAGE = "Usage: cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--permission|--fast|--rtk|--model|--priority|--all) [--json]"
|
|
59
59
|
SETTING_ALIAS_USAGE = "Usage: cdx power|perm|fast|model <name|all|provider:PROVIDER|a,b> <value|default> [--json]"
|
|
60
60
|
CONFIG_USAGE = "Usage: cdx config <name> [--json]"
|
|
61
61
|
HISTORY_USAGE = "Usage: cdx history [name] [--limit N] [--summary] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]"
|
|
62
|
+
STATS_USAGE = "Usage: cdx stats [name] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]"
|
|
62
63
|
LAST_USAGE = "Usage: cdx last [--json]"
|
|
63
64
|
SELECT_USAGE = "Usage: cdx select --provider PROVIDER [--min-reasoning-effort low|medium|high] [--min-power low|medium|high] [--require-ready] [--refresh] --json"
|
|
64
65
|
RUN_USAGE = "Usage: cdx run [session] --cwd PATH (--prompt-file PATH|--prompt TEXT) [--provider PROVIDER] [--model MODEL] [--reasoning-effort low|medium|high] [--power low|medium|high] [--permission review|default|auto|full|workspace-write|read-only|danger-full-access] [--timeout-seconds N] --json"
|
|
@@ -504,6 +505,7 @@ def _parse_set_args(args):
|
|
|
504
505
|
"--power": {"key": "power", "type": "str", "default": None},
|
|
505
506
|
"--permission": {"key": "permission", "type": "str", "default": None},
|
|
506
507
|
"--fast": {"key": "fast", "type": "str", "default": None, "transform": _parse_fast_value},
|
|
508
|
+
"--rtk": {"key": "rtk", "type": "str", "default": None, "transform": _parse_fast_value},
|
|
507
509
|
"--model": {"key": "model", "type": "str", "default": None},
|
|
508
510
|
"--priority": {"key": "priority", "type": "str", "default": None, "transform": _parse_priority_value},
|
|
509
511
|
"--sessions": {"key": "sessions", "type": "str", "default": None, "transform": _parse_set_unset_sessions},
|
|
@@ -520,7 +522,7 @@ def _parse_set_args(args):
|
|
|
520
522
|
raise CdxError(SET_USAGE)
|
|
521
523
|
settings = {
|
|
522
524
|
key: parsed[key]
|
|
523
|
-
for key in ("power", "permission", "fast", "model", "priority")
|
|
525
|
+
for key in ("power", "permission", "fast", "rtk", "model", "priority")
|
|
524
526
|
if parsed[key] is not None
|
|
525
527
|
}
|
|
526
528
|
if not settings:
|
|
@@ -539,6 +541,7 @@ def _parse_unset_args(args):
|
|
|
539
541
|
"--power": {"key": "power", "type": "bool", "default": False},
|
|
540
542
|
"--permission": {"key": "permission", "type": "bool", "default": False},
|
|
541
543
|
"--fast": {"key": "fast", "type": "bool", "default": False},
|
|
544
|
+
"--rtk": {"key": "rtk", "type": "bool", "default": False},
|
|
542
545
|
"--model": {"key": "model", "type": "bool", "default": False},
|
|
543
546
|
"--priority": {"key": "priority", "type": "bool", "default": False},
|
|
544
547
|
"--all": {"key": "all", "type": "bool", "default": False},
|
|
@@ -554,8 +557,8 @@ def _parse_unset_args(args):
|
|
|
554
557
|
raise CdxError(UNSET_USAGE)
|
|
555
558
|
if not parsed["names"] and not parsed["sessions"] and not parsed["provider"]:
|
|
556
559
|
raise CdxError(UNSET_USAGE)
|
|
557
|
-
keys = ["power", "permission", "fast", "model", "priority"] if parsed["all"] else [
|
|
558
|
-
key for key in ("power", "permission", "fast", "model", "priority") if parsed[key]
|
|
560
|
+
keys = ["power", "permission", "fast", "rtk", "model", "priority"] if parsed["all"] else [
|
|
561
|
+
key for key in ("power", "permission", "fast", "rtk", "model", "priority") if parsed[key]
|
|
559
562
|
]
|
|
560
563
|
if not keys:
|
|
561
564
|
raise CdxError(UNSET_USAGE)
|
|
@@ -818,6 +821,22 @@ def _parse_history_args(args, now=None):
|
|
|
818
821
|
}
|
|
819
822
|
|
|
820
823
|
|
|
824
|
+
def _parse_stats_args(args, now=None):
|
|
825
|
+
now = now or datetime.now().astimezone()
|
|
826
|
+
parsed = _parse_flag_args(args, {
|
|
827
|
+
"--since": {"key": "since", "type": "str", "default": None},
|
|
828
|
+
"--from": {"key": "from", "type": "str", "default": None},
|
|
829
|
+
"--to": {"key": "to", "type": "str", "default": None},
|
|
830
|
+
"--json": {"key": "json", "type": "bool", "default": False},
|
|
831
|
+
}, STATS_USAGE, positionals_key="names", max_positionals=1)
|
|
832
|
+
period = _parse_history_period(parsed, now)
|
|
833
|
+
return {
|
|
834
|
+
"name": parsed["names"][0] if parsed["names"] else None,
|
|
835
|
+
"period": period,
|
|
836
|
+
"json": parsed["json"],
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
|
|
821
840
|
def _read_option_value(args, index, usage):
|
|
822
841
|
if index + 1 >= len(args):
|
|
823
842
|
raise CdxError(usage)
|
|
@@ -1225,9 +1244,9 @@ def _run_cdx_error_code(error):
|
|
|
1225
1244
|
|
|
1226
1245
|
def _run_result_payload(ok, parsed, session, run_info=None, error=None, error_source=None, error_code=None):
|
|
1227
1246
|
run_info = run_info or {}
|
|
1228
|
-
usage = (
|
|
1247
|
+
usage = run_info.get("usage") if isinstance(run_info.get("usage"), dict) else (
|
|
1229
1248
|
extract_run_usage(session.get("provider"), run_info.get("stdout_path"))
|
|
1230
|
-
if
|
|
1249
|
+
if session else
|
|
1231
1250
|
empty_usage()
|
|
1232
1251
|
)
|
|
1233
1252
|
return {
|
|
@@ -1324,6 +1343,8 @@ def handle_run(rest, ctx):
|
|
|
1324
1343
|
timeout_seconds=parsed.get("timeout_seconds"),
|
|
1325
1344
|
spawn=ctx.get("spawn_headless") or ctx.get("spawn"),
|
|
1326
1345
|
)
|
|
1346
|
+
usage = extract_run_usage(run_session.get("provider"), run_info.get("stdout_path"))
|
|
1347
|
+
run_info = {**run_info, "usage": usage}
|
|
1327
1348
|
ok = run_info.get("returncode") == 0
|
|
1328
1349
|
if ok:
|
|
1329
1350
|
ctx["service"]["record_launch_history"](session["name"], {
|
|
@@ -1374,6 +1395,7 @@ def _format_launch_config(session):
|
|
|
1374
1395
|
f"power: {launch.get('power') or 'default'}",
|
|
1375
1396
|
f"permission: {launch.get('permission') or 'default'}",
|
|
1376
1397
|
f"fast: {'on' if launch.get('fast') is True else 'off' if launch.get('fast') is False else 'default'}",
|
|
1398
|
+
f"rtk: {'on' if launch.get('rtk') is True else 'off' if launch.get('rtk') is False else 'default'}",
|
|
1377
1399
|
f"model: {launch.get('model') or 'default'}",
|
|
1378
1400
|
f"priority: {launch.get('priority') if launch.get('priority') is not None else 'default'}",
|
|
1379
1401
|
])
|
|
@@ -1543,6 +1565,79 @@ def _summarize_history(entries):
|
|
|
1543
1565
|
)
|
|
1544
1566
|
|
|
1545
1567
|
|
|
1568
|
+
def _token_value(usage, key):
|
|
1569
|
+
if not isinstance(usage, dict):
|
|
1570
|
+
return None
|
|
1571
|
+
value = usage.get(key)
|
|
1572
|
+
try:
|
|
1573
|
+
parsed = int(value)
|
|
1574
|
+
except (TypeError, ValueError):
|
|
1575
|
+
return None
|
|
1576
|
+
return parsed if parsed >= 0 else None
|
|
1577
|
+
|
|
1578
|
+
|
|
1579
|
+
def _summarize_stats(entries):
|
|
1580
|
+
rows = {}
|
|
1581
|
+
for entry in entries:
|
|
1582
|
+
name = entry.get("session_name") or "-"
|
|
1583
|
+
row = rows.setdefault(name, {
|
|
1584
|
+
"session_name": name,
|
|
1585
|
+
"provider": entry.get("provider") or "-",
|
|
1586
|
+
"launches": 0,
|
|
1587
|
+
"successes": 0,
|
|
1588
|
+
"failures": 0,
|
|
1589
|
+
"duration_ms": 0,
|
|
1590
|
+
"usage_runs": 0,
|
|
1591
|
+
"input_tokens": 0,
|
|
1592
|
+
"output_tokens": 0,
|
|
1593
|
+
"reasoning_tokens": 0,
|
|
1594
|
+
"total_tokens": 0,
|
|
1595
|
+
"last_started_at": None,
|
|
1596
|
+
})
|
|
1597
|
+
row["launches"] += 1
|
|
1598
|
+
if entry.get("status") == "success":
|
|
1599
|
+
row["successes"] += 1
|
|
1600
|
+
elif entry.get("status") == "failed":
|
|
1601
|
+
row["failures"] += 1
|
|
1602
|
+
try:
|
|
1603
|
+
row["duration_ms"] += int(entry.get("duration_ms") or 0)
|
|
1604
|
+
except (TypeError, ValueError):
|
|
1605
|
+
pass
|
|
1606
|
+
usage = entry.get("usage") if isinstance(entry.get("usage"), dict) else {}
|
|
1607
|
+
parsed_usage = {
|
|
1608
|
+
key: _token_value(usage, key)
|
|
1609
|
+
for key in ("input_tokens", "output_tokens", "reasoning_tokens", "total_tokens")
|
|
1610
|
+
}
|
|
1611
|
+
if any(value is not None for value in parsed_usage.values()):
|
|
1612
|
+
row["usage_runs"] += 1
|
|
1613
|
+
for key, value in parsed_usage.items():
|
|
1614
|
+
row[key] += value or 0
|
|
1615
|
+
started = entry.get("started_at")
|
|
1616
|
+
if started and (not row["last_started_at"] or started > row["last_started_at"]):
|
|
1617
|
+
row["last_started_at"] = started
|
|
1618
|
+
row["provider"] = entry.get("provider") or row["provider"]
|
|
1619
|
+
return sorted(
|
|
1620
|
+
rows.values(),
|
|
1621
|
+
key=lambda item: (item["total_tokens"], item["duration_ms"], item.get("last_started_at") or "", item["session_name"]),
|
|
1622
|
+
reverse=True,
|
|
1623
|
+
)
|
|
1624
|
+
|
|
1625
|
+
|
|
1626
|
+
def _stats_totals(rows):
|
|
1627
|
+
return {
|
|
1628
|
+
"sessions": len(rows),
|
|
1629
|
+
"launches": sum(row["launches"] for row in rows),
|
|
1630
|
+
"successes": sum(row["successes"] for row in rows),
|
|
1631
|
+
"failures": sum(row["failures"] for row in rows),
|
|
1632
|
+
"duration_ms": sum(row["duration_ms"] for row in rows),
|
|
1633
|
+
"usage_runs": sum(row["usage_runs"] for row in rows),
|
|
1634
|
+
"input_tokens": sum(row["input_tokens"] for row in rows),
|
|
1635
|
+
"output_tokens": sum(row["output_tokens"] for row in rows),
|
|
1636
|
+
"reasoning_tokens": sum(row["reasoning_tokens"] for row in rows),
|
|
1637
|
+
"total_tokens": sum(row["total_tokens"] for row in rows),
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
|
|
1546
1641
|
def _format_history_period(period):
|
|
1547
1642
|
if not _has_history_period(period or {}):
|
|
1548
1643
|
return None
|
|
@@ -1610,6 +1705,55 @@ def _format_history(entries, use_color=False):
|
|
|
1610
1705
|
])
|
|
1611
1706
|
|
|
1612
1707
|
|
|
1708
|
+
def _format_token_count(value):
|
|
1709
|
+
try:
|
|
1710
|
+
amount = int(value)
|
|
1711
|
+
except (TypeError, ValueError):
|
|
1712
|
+
return "-"
|
|
1713
|
+
if amount >= 1000000:
|
|
1714
|
+
return f"{amount / 1000000:.1f}M"
|
|
1715
|
+
if amount >= 1000:
|
|
1716
|
+
return f"{amount / 1000:.1f}K"
|
|
1717
|
+
return str(amount)
|
|
1718
|
+
|
|
1719
|
+
|
|
1720
|
+
def _format_stats(rows, totals, period=None, use_color=False):
|
|
1721
|
+
from .cli_render import _format_relative_age, _pad_table
|
|
1722
|
+
|
|
1723
|
+
if not rows:
|
|
1724
|
+
return "No launch stats for this period." if _has_history_period(period or {}) else "No launch stats."
|
|
1725
|
+
table = [["SESSION", "PROV.", "RUNS", "USAGE", "IN", "OUT", "REASON", "TOTAL", "TIME", "LAST"]]
|
|
1726
|
+
for row in rows:
|
|
1727
|
+
table.append([
|
|
1728
|
+
row["session_name"],
|
|
1729
|
+
row["provider"],
|
|
1730
|
+
str(row["launches"]),
|
|
1731
|
+
str(row["usage_runs"]),
|
|
1732
|
+
_format_token_count(row["input_tokens"]),
|
|
1733
|
+
_format_token_count(row["output_tokens"]),
|
|
1734
|
+
_format_token_count(row["reasoning_tokens"]),
|
|
1735
|
+
_format_token_count(row["total_tokens"]),
|
|
1736
|
+
_format_duration_ms(row["duration_ms"]),
|
|
1737
|
+
_format_relative_age(row.get("last_started_at")),
|
|
1738
|
+
])
|
|
1739
|
+
lines = ["Assistant stats:"]
|
|
1740
|
+
period_line = _format_history_period(period or {})
|
|
1741
|
+
if period_line:
|
|
1742
|
+
lines.extend([period_line, ""])
|
|
1743
|
+
lines.append(_pad_table(table))
|
|
1744
|
+
lines.extend([
|
|
1745
|
+
"",
|
|
1746
|
+
_dim(
|
|
1747
|
+
"Totals: "
|
|
1748
|
+
f"{totals['launches']} runs, {totals['usage_runs']} with usage, "
|
|
1749
|
+
f"{_format_token_count(totals['total_tokens'])} tokens, "
|
|
1750
|
+
f"{_format_duration_ms(totals['duration_ms'])}.",
|
|
1751
|
+
use_color,
|
|
1752
|
+
),
|
|
1753
|
+
])
|
|
1754
|
+
return "\n".join(lines)
|
|
1755
|
+
|
|
1756
|
+
|
|
1613
1757
|
def _apply_launch_settings(parsed, ctx, action="set"):
|
|
1614
1758
|
targets = _resolve_bulk_launch_targets(parsed, ctx["service"])
|
|
1615
1759
|
sessions = [
|
|
@@ -1722,6 +1866,32 @@ def handle_history(rest, ctx):
|
|
|
1722
1866
|
return 0
|
|
1723
1867
|
|
|
1724
1868
|
|
|
1869
|
+
def handle_stats(rest, ctx):
|
|
1870
|
+
now_fn = ctx["options"].get("now") or time.time
|
|
1871
|
+
now = datetime.fromtimestamp(now_fn()).astimezone()
|
|
1872
|
+
parsed = _parse_stats_args(rest, now=now)
|
|
1873
|
+
entries = ctx["service"]["get_launch_history"](parsed["name"], limit=0)
|
|
1874
|
+
entries = _filter_history_period(entries, parsed["period"])
|
|
1875
|
+
rows = _summarize_stats(entries)
|
|
1876
|
+
totals = _stats_totals(rows)
|
|
1877
|
+
message = (
|
|
1878
|
+
f"Calculated stats for {parsed['name']}"
|
|
1879
|
+
if parsed["name"]
|
|
1880
|
+
else "Calculated stats"
|
|
1881
|
+
)
|
|
1882
|
+
if parsed["json"]:
|
|
1883
|
+
_write_json(ctx, _json_success(
|
|
1884
|
+
"stats",
|
|
1885
|
+
message,
|
|
1886
|
+
period=_public_history_period(parsed["period"]),
|
|
1887
|
+
stats=rows,
|
|
1888
|
+
totals=totals,
|
|
1889
|
+
))
|
|
1890
|
+
return 0
|
|
1891
|
+
ctx["out"](f"{_format_stats(rows, totals, period=parsed['period'], use_color=ctx['use_color'])}\n")
|
|
1892
|
+
return 0
|
|
1893
|
+
|
|
1894
|
+
|
|
1725
1895
|
def _resolve_last_launch_session(ctx):
|
|
1726
1896
|
for entry in ctx["service"]["get_launch_history"](limit=0):
|
|
1727
1897
|
name = entry.get("session_name")
|
|
@@ -2352,11 +2522,21 @@ def handle_update(rest, ctx):
|
|
|
2352
2522
|
if failed:
|
|
2353
2523
|
raise CdxError(format_update_failure(results))
|
|
2354
2524
|
|
|
2525
|
+
warnings = []
|
|
2526
|
+
version_warning = verify_updated_command(
|
|
2527
|
+
target_version,
|
|
2528
|
+
runner=ctx["options"].get("runVersionCheck"),
|
|
2529
|
+
env=ctx.get("env"),
|
|
2530
|
+
)
|
|
2531
|
+
if version_warning:
|
|
2532
|
+
warnings.append(version_warning)
|
|
2533
|
+
|
|
2355
2534
|
message = f"Updated cdx-manager to {target_version}"
|
|
2356
2535
|
if json_flag:
|
|
2357
2536
|
_write_json(ctx, _json_success(
|
|
2358
2537
|
"update",
|
|
2359
2538
|
message,
|
|
2539
|
+
warnings=warnings,
|
|
2360
2540
|
updated=True,
|
|
2361
2541
|
current_version=current_version,
|
|
2362
2542
|
target_version=target_version,
|
|
@@ -2365,6 +2545,8 @@ def handle_update(rest, ctx):
|
|
|
2365
2545
|
))
|
|
2366
2546
|
return 0
|
|
2367
2547
|
ctx["out"](f"{_success(message, ctx['use_color'])}\n")
|
|
2548
|
+
for warning in warnings:
|
|
2549
|
+
ctx["out"](f"{_warn(warning['message'], ctx['use_color'])}\n")
|
|
2368
2550
|
return 0
|
|
2369
2551
|
|
|
2370
2552
|
|
package/src/health.py
CHANGED
|
@@ -39,6 +39,14 @@ def collect_health_report(service, base_dir, env=None):
|
|
|
39
39
|
status = "OK" if path else "WARN"
|
|
40
40
|
issues.append(_issue(status, f"{command}_cli", f"{command} CLI {'found' if path else 'not found'}", path))
|
|
41
41
|
|
|
42
|
+
rtk_path = shutil.which("rtk", path=env.get("PATH"))
|
|
43
|
+
issues.append(_issue(
|
|
44
|
+
"OK" if rtk_path else "WARN",
|
|
45
|
+
"rtk_cli",
|
|
46
|
+
"RTK CLI found" if rtk_path else "RTK CLI not found; assistants can still run normally but may use more context on noisy commands",
|
|
47
|
+
rtk_path,
|
|
48
|
+
))
|
|
49
|
+
|
|
42
50
|
script_bin = env.get("CDX_SCRIPT_BIN", "script")
|
|
43
51
|
script_path = shutil.which(script_bin, path=env.get("PATH"))
|
|
44
52
|
issues.append(_issue(
|
package/src/provider_runtime.py
CHANGED
|
@@ -15,6 +15,10 @@ from .errors import CdxError
|
|
|
15
15
|
|
|
16
16
|
LOG_ROTATE_BYTES = 10 * 1024 * 1024 # 10 MB
|
|
17
17
|
REASONING_EFFORT_VALUES = {"low", "medium", "high"}
|
|
18
|
+
RTK_PROMPT = (
|
|
19
|
+
"When running noisy shell commands, prefer RTK wrappers (`rtk <command>`) if `rtk` is available. "
|
|
20
|
+
"Use raw commands when exact, unfiltered output is required."
|
|
21
|
+
)
|
|
18
22
|
LAUNCH_PERMISSION_ARGS = {
|
|
19
23
|
PROVIDER_CLAUDE: {
|
|
20
24
|
"review": ["--permission-mode", "plan"],
|
|
@@ -284,7 +288,20 @@ def _default_script_args(transcript_path, spec):
|
|
|
284
288
|
return ["-q", "-F", transcript_path, spec["command"]] + spec["args"]
|
|
285
289
|
|
|
286
290
|
|
|
291
|
+
def _rtk_enabled(session):
|
|
292
|
+
return (session.get("launch") or {}).get("rtk") is True
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _with_launch_preferences(session, initial_prompt=None):
|
|
296
|
+
if not _rtk_enabled(session):
|
|
297
|
+
return initial_prompt
|
|
298
|
+
if initial_prompt:
|
|
299
|
+
return f"{RTK_PROMPT}\n\n{initial_prompt}"
|
|
300
|
+
return RTK_PROMPT
|
|
301
|
+
|
|
302
|
+
|
|
287
303
|
def _build_launch_spec(session, cwd=None, env_override=None, initial_prompt=None, capture_transcript=True):
|
|
304
|
+
initial_prompt = _with_launch_preferences(session, initial_prompt)
|
|
288
305
|
_validate_initial_prompt(initial_prompt)
|
|
289
306
|
cwd = cwd or os.getcwd()
|
|
290
307
|
env_override = env_override or {}
|
|
@@ -358,6 +375,7 @@ def _validate_initial_prompt(initial_prompt):
|
|
|
358
375
|
|
|
359
376
|
|
|
360
377
|
def _build_headless_launch_spec(session, cwd=None, env_override=None, initial_prompt=None):
|
|
378
|
+
initial_prompt = _with_launch_preferences(session, initial_prompt)
|
|
361
379
|
_validate_initial_prompt(initial_prompt)
|
|
362
380
|
cwd = cwd or os.getcwd()
|
|
363
381
|
env = {**os.environ, **(env_override or {})}
|
package/src/session_service.py
CHANGED
|
@@ -123,6 +123,18 @@ def _normalize_launch_settings(settings):
|
|
|
123
123
|
normalized["fast"] = False
|
|
124
124
|
else:
|
|
125
125
|
raise CdxError(f"Unsupported fast value: {settings['fast']}")
|
|
126
|
+
if "rtk" in settings and settings["rtk"] is not None:
|
|
127
|
+
value = settings["rtk"]
|
|
128
|
+
if isinstance(value, bool):
|
|
129
|
+
normalized["rtk"] = value
|
|
130
|
+
else:
|
|
131
|
+
text = str(value).strip().lower()
|
|
132
|
+
if text in ("on", "true", "1", "yes"):
|
|
133
|
+
normalized["rtk"] = True
|
|
134
|
+
elif text in ("off", "false", "0", "no"):
|
|
135
|
+
normalized["rtk"] = False
|
|
136
|
+
else:
|
|
137
|
+
raise CdxError(f"Unsupported rtk value: {settings['rtk']}")
|
|
126
138
|
if "model" in settings and settings["model"] is not None:
|
|
127
139
|
model = str(settings["model"]).strip()
|
|
128
140
|
if not model:
|
|
@@ -800,7 +812,7 @@ def create_session_service(options=None):
|
|
|
800
812
|
raise CdxError(f"Unknown session: {name}")
|
|
801
813
|
if not keys:
|
|
802
814
|
raise CdxError("At least one launch setting is required.")
|
|
803
|
-
allowed = {"power", "permission", "fast", "model", "priority"}
|
|
815
|
+
allowed = {"power", "permission", "fast", "rtk", "model", "priority"}
|
|
804
816
|
unknown = [key for key in keys if key not in allowed]
|
|
805
817
|
if unknown:
|
|
806
818
|
raise CdxError(f"Unsupported launch setting: {', '.join(unknown)}")
|
package/src/update_manager.py
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import re
|
|
3
|
+
import shutil
|
|
2
4
|
import subprocess
|
|
3
5
|
import sys
|
|
4
6
|
from pathlib import Path
|
|
@@ -22,7 +24,14 @@ def _normalize_version(value):
|
|
|
22
24
|
|
|
23
25
|
|
|
24
26
|
def _is_standalone_install(package_root):
|
|
25
|
-
|
|
27
|
+
if package_root.parent.name == "versions":
|
|
28
|
+
return True
|
|
29
|
+
return (
|
|
30
|
+
package_root.parent.name == "cdx-manager"
|
|
31
|
+
and re.match(r"^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$", package_root.name) is not None
|
|
32
|
+
and (package_root / "install.sh").exists()
|
|
33
|
+
and (package_root / "bin" / "cdx").exists()
|
|
34
|
+
)
|
|
26
35
|
|
|
27
36
|
|
|
28
37
|
def _is_source_checkout(package_root):
|
|
@@ -200,6 +209,76 @@ def run_update_plan(plan, runner=None, env=None):
|
|
|
200
209
|
return results
|
|
201
210
|
|
|
202
211
|
|
|
212
|
+
def _version_check_env(env):
|
|
213
|
+
source = env or os.environ
|
|
214
|
+
allowed = {
|
|
215
|
+
key: source[key]
|
|
216
|
+
for key in ("PATH", "PATHEXT", "SystemRoot", "COMSPEC", "WINDIR")
|
|
217
|
+
if source.get(key)
|
|
218
|
+
}
|
|
219
|
+
return allowed
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def verify_updated_command(target_version, runner=None, env=None):
|
|
223
|
+
target = _normalize_version(target_version)
|
|
224
|
+
if not target:
|
|
225
|
+
return None
|
|
226
|
+
env = env or os.environ
|
|
227
|
+
executable = shutil.which("cdx", path=env.get("PATH"))
|
|
228
|
+
if not executable:
|
|
229
|
+
return {
|
|
230
|
+
"code": "update_version_check_failed",
|
|
231
|
+
"message": "Updated cdx-manager, but no cdx executable was found on PATH.",
|
|
232
|
+
"target_version": target,
|
|
233
|
+
"resolved_version": None,
|
|
234
|
+
"path": None,
|
|
235
|
+
}
|
|
236
|
+
runner = runner or subprocess.run
|
|
237
|
+
try:
|
|
238
|
+
result = runner(
|
|
239
|
+
[executable, "-v"],
|
|
240
|
+
cwd=None,
|
|
241
|
+
env=_version_check_env(env),
|
|
242
|
+
check=False,
|
|
243
|
+
capture_output=True,
|
|
244
|
+
text=True,
|
|
245
|
+
)
|
|
246
|
+
except OSError as error:
|
|
247
|
+
return {
|
|
248
|
+
"code": "update_version_check_failed",
|
|
249
|
+
"message": f"Updated cdx-manager, but failed to verify {executable}: {error}",
|
|
250
|
+
"target_version": target,
|
|
251
|
+
"resolved_version": None,
|
|
252
|
+
"path": executable,
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
version = str(_result_text(result, "stdout") or "").strip().splitlines()
|
|
256
|
+
version = _normalize_version(version[0]) if version else None
|
|
257
|
+
code = _result_code(result)
|
|
258
|
+
if code not in (0, None) or not version:
|
|
259
|
+
message = str(_result_text(result, "stderr") or _result_text(result, "stdout") or "").strip()
|
|
260
|
+
suffix = f": {message}" if message else "."
|
|
261
|
+
return {
|
|
262
|
+
"code": "update_version_check_failed",
|
|
263
|
+
"message": f"Updated cdx-manager, but failed to verify {executable}{suffix}",
|
|
264
|
+
"target_version": target,
|
|
265
|
+
"resolved_version": version,
|
|
266
|
+
"path": executable,
|
|
267
|
+
}
|
|
268
|
+
if version != target:
|
|
269
|
+
return {
|
|
270
|
+
"code": "update_version_mismatch",
|
|
271
|
+
"message": (
|
|
272
|
+
f"Updated cdx-manager to {target}, but PATH resolves {executable} "
|
|
273
|
+
f"which reports {version}."
|
|
274
|
+
),
|
|
275
|
+
"target_version": target,
|
|
276
|
+
"resolved_version": version,
|
|
277
|
+
"path": executable,
|
|
278
|
+
}
|
|
279
|
+
return None
|
|
280
|
+
|
|
281
|
+
|
|
203
282
|
def format_update_failure(results):
|
|
204
283
|
if not results:
|
|
205
284
|
return "Update failed."
|