cdx-manager 0.7.3 → 0.7.5
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 -7
- package/changelogs/CHANGELOGS_0_7_4.md +45 -0
- package/changelogs/CHANGELOGS_0_7_5.md +35 -0
- package/checksums/release-archives.json +8 -0
- package/install.ps1 +5 -1
- package/install.sh +7 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/claude_refresh.py +19 -1
- package/src/claude_usage.py +8 -0
- package/src/cli.py +12 -2
- package/src/cli_commands.py +190 -82
- package/src/cli_render.py +3 -0
- package/src/provider_runtime.py +50 -15
- package/src/run_command.py +83 -0
- package/src/session_service.py +26 -1
- package/src/status_view.py +14 -5
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
|
|
|
@@ -118,7 +118,8 @@ With the standalone PowerShell installer:
|
|
|
118
118
|
|
|
119
119
|
```powershell
|
|
120
120
|
Invoke-WebRequest https://raw.githubusercontent.com/AlexAgo83/cdx-manager/main/install.ps1 -OutFile install.ps1
|
|
121
|
-
# Optional: set CDX_SHA256 before running if you have a trusted checksum
|
|
121
|
+
# Optional: set CDX_SHA256 before running if you have a trusted checksum.
|
|
122
|
+
# Set CDX_ALLOW_UNVERIFIED=1 only if you intentionally accept an unverified archive.
|
|
122
123
|
powershell -ExecutionPolicy Bypass -File .\install.ps1
|
|
123
124
|
```
|
|
124
125
|
|
|
@@ -126,7 +127,8 @@ With the standalone GitHub installer:
|
|
|
126
127
|
|
|
127
128
|
```bash
|
|
128
129
|
curl -fsSL https://raw.githubusercontent.com/AlexAgo83/cdx-manager/main/install.sh -o install.sh
|
|
129
|
-
# Optional: set CDX_SHA256 before running if you have a trusted checksum
|
|
130
|
+
# Optional: set CDX_SHA256 before running if you have a trusted checksum.
|
|
131
|
+
# Set CDX_ALLOW_UNVERIFIED=1 only if you intentionally accept an unverified archive.
|
|
130
132
|
sh install.sh
|
|
131
133
|
```
|
|
132
134
|
|
|
@@ -134,7 +136,7 @@ For a specific version:
|
|
|
134
136
|
|
|
135
137
|
```bash
|
|
136
138
|
curl -fsSL https://raw.githubusercontent.com/AlexAgo83/cdx-manager/main/install.sh -o install.sh
|
|
137
|
-
CDX_VERSION=v0.7.
|
|
139
|
+
CDX_VERSION=v0.7.5 sh install.sh
|
|
138
140
|
```
|
|
139
141
|
|
|
140
142
|
From source:
|
|
@@ -183,6 +185,7 @@ Security note:
|
|
|
183
185
|
|
|
184
186
|
- The standalone installers try to resolve official release checksums from `checksums/release-archives.json`.
|
|
185
187
|
- You can still override verification explicitly through `CDX_SHA256`.
|
|
188
|
+
- If no checksum is available, standalone installers fail closed unless `CDX_ALLOW_UNVERIFIED=1` is set.
|
|
186
189
|
- Prefer `npm`, `pipx`, or `uv` when you want registry-backed install flows.
|
|
187
190
|
- If you use the standalone script, download it first, inspect it, and prefer a release with an official checksum entry.
|
|
188
191
|
|
|
@@ -241,6 +244,9 @@ cdx work
|
|
|
241
244
|
# Check usage across all sessions
|
|
242
245
|
cdx status
|
|
243
246
|
|
|
247
|
+
# Pick the best session using the same priority logic as status
|
|
248
|
+
cdx next
|
|
249
|
+
|
|
244
250
|
# Notify when the next cooling-down assistant is ready
|
|
245
251
|
cdx ready
|
|
246
252
|
```
|
|
@@ -256,7 +262,7 @@ cdx ready --refresh
|
|
|
256
262
|
|
|
257
263
|
### Persistent Launch Settings
|
|
258
264
|
|
|
259
|
-
|
|
265
|
+
New sessions start with `power=medium` and `fast=off`, so launches are predictable without enabling fast mode. Set or override only the values you want to pin:
|
|
260
266
|
|
|
261
267
|
```bash
|
|
262
268
|
cdx set work --power medium --permission full --fast off
|
|
@@ -269,9 +275,10 @@ cdx power all low
|
|
|
269
275
|
cdx perm provider:claude review
|
|
270
276
|
cdx model provider:ollama llama3.2
|
|
271
277
|
cdx config work
|
|
278
|
+
cdx configs
|
|
272
279
|
```
|
|
273
280
|
|
|
274
|
-
Those values are stored on the session and reapplied every time you run `cdx work`. Remove overrides to return to provider defaults:
|
|
281
|
+
Those values are stored on the session and reapplied every time you run `cdx work`. Remove overrides to return to provider-native defaults:
|
|
275
282
|
|
|
276
283
|
```bash
|
|
277
284
|
cdx unset work --power
|
|
@@ -284,7 +291,7 @@ cdx power all default
|
|
|
284
291
|
cdx model provider:ollama default
|
|
285
292
|
```
|
|
286
293
|
|
|
287
|
-
`--power` maps to Codex `model_reasoning_effort` and Claude `--effort`. `--permission` maps to provider-native permission flags. `--fast on` uses low effort
|
|
294
|
+
`--model` maps to Codex `--model`, Claude `--model`, and Ollama `ollama run <model>`. `--power` maps to Codex `model_reasoning_effort` and Claude `--effort`. `--permission` maps to provider-native permission flags. `--fast on` clears the stored power setting and uses low effort; setting `--power` again disables fast mode. `--priority` is a 0..100 selector preference used as a tie-breaker after readiness and availability. `--rtk on` injects a launch instruction that encourages assistants to use RTK (`rtk <command>`) for noisy terminal commands when RTK is available, while keeping raw commands for exact output.
|
|
288
295
|
|
|
289
296
|
### Launch History
|
|
290
297
|
|
|
@@ -320,6 +327,7 @@ cdx history --summary --from 2026-05-01 --to 2026-05-28
|
|
|
320
327
|
| `cdx disable <name> [--json]` | Disable a session without deleting it; disabled sessions stay visible and cannot launch |
|
|
321
328
|
| `cdx enable <name> [--json]` | Re-enable a disabled session |
|
|
322
329
|
| `cdx config <name> [--json]` | Show persistent launch settings for a session |
|
|
330
|
+
| `cdx configs [--json]` | Show persistent launch settings for all sessions in one table |
|
|
323
331
|
| `cdx power\|perm\|fast\|model <name\|all\|provider:PROVIDER\|a,b> <value\|default> [--json]` | Shortcut commands for setting or clearing one launch setting |
|
|
324
332
|
| `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
333
|
| `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 |
|
|
@@ -338,6 +346,7 @@ cdx history --summary --from 2026-05-01 --to 2026-05-28
|
|
|
338
346
|
| `cdx ready [--refresh] [--json]` | Schedule an OS notification for the next cooling-down assistant that becomes ready, then return immediately |
|
|
339
347
|
| `cdx notify <name> --at-reset [--poll seconds] [--once] [--schedule] [--refresh] [--json]` | Wait for a session reset time or schedule an OS wake-up notification when due |
|
|
340
348
|
| `cdx notify --next-ready [--poll seconds] [--once] [--schedule] [--refresh] [--json]` | Wait until the recommended session is usable, or schedule the next known reset notification |
|
|
349
|
+
| `cdx next [--json] [--refresh]` | Select the best next assistant using the same priority logic as `cdx status` |
|
|
341
350
|
| `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 |
|
|
342
351
|
| `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
352
|
| `cdx stats [name] [--since 7d\|today\|DATE] [--from DATE] [--to DATE] [--json]` | Aggregate launch counts, duration, and known headless token usage by session |
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Changelog (`0.7.3 -> 0.7.4`)
|
|
2
|
+
|
|
3
|
+
Release date: 2026-06-02
|
|
4
|
+
|
|
5
|
+
## Security and Privacy
|
|
6
|
+
|
|
7
|
+
- Rejected `.` and `..` as session names so profile and state paths cannot escape their managed directories.
|
|
8
|
+
- Added import-bundle regression coverage for unsafe dot-path session names.
|
|
9
|
+
- Redacted headless `cdx run` prompt arguments before persisting launch history while keeping provider execution unchanged.
|
|
10
|
+
- Made headless `cdx run` perform a live provider auth probe instead of trusting only local credential files.
|
|
11
|
+
- Hardened Claude auth invalidation so invalid Claude credentials are recorded as logged out during refresh and status handling.
|
|
12
|
+
|
|
13
|
+
## Installer Integrity
|
|
14
|
+
|
|
15
|
+
- Changed standalone Unix and PowerShell installers to fail closed when no official checksum is available.
|
|
16
|
+
- Added the explicit `CDX_ALLOW_UNVERIFIED=1` override for users who intentionally accept an unverified archive.
|
|
17
|
+
- Updated README security guidance for standalone installer checksum behavior.
|
|
18
|
+
|
|
19
|
+
## Headless and Selection Behavior
|
|
20
|
+
|
|
21
|
+
- Added the next-assistant recommendation command and improved resolved run reasoning-effort reporting.
|
|
22
|
+
- Preserved stable provider-specific launch metadata while adding stricter headless auth validation.
|
|
23
|
+
|
|
24
|
+
## Maintainability
|
|
25
|
+
|
|
26
|
+
- Extracted headless run prompt, error-code, and JSON payload helpers into `src/run_command.py`.
|
|
27
|
+
- Reduced the size of `src/cli_commands.py` without changing the public `cdx run --json` contract.
|
|
28
|
+
|
|
29
|
+
## Coverage and Tests
|
|
30
|
+
|
|
31
|
+
- Added regression coverage for dot-path session names, unsafe imported sessions, redacted headless history prompts, forced live auth probes, and unauthenticated headless runs.
|
|
32
|
+
- Increased the Python test suite to 276 tests.
|
|
33
|
+
|
|
34
|
+
## Release Metadata
|
|
35
|
+
|
|
36
|
+
- Updated package metadata, CLI version output, README badge, pinned installer example, and release changelog to `v0.7.4`.
|
|
37
|
+
|
|
38
|
+
## Validation and Regression Evidence
|
|
39
|
+
|
|
40
|
+
- `npm run lint`
|
|
41
|
+
- `npm test`
|
|
42
|
+
- `npm audit --omit=dev --json`
|
|
43
|
+
- `sh -n install.sh`
|
|
44
|
+
- PowerShell parser check for `install.ps1`
|
|
45
|
+
- `python3 -m logics_manager lint --require-status`
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Changelog (`0.7.4 -> 0.7.5`)
|
|
2
|
+
|
|
3
|
+
Release date: 2026-06-02
|
|
4
|
+
|
|
5
|
+
## Launch Settings
|
|
6
|
+
|
|
7
|
+
- Applied persisted `--model` settings to interactive Codex and Claude launches, matching the existing headless behavior.
|
|
8
|
+
- Switched Codex model launches to the explicit `--model` flag for both interactive and headless runs.
|
|
9
|
+
- Documented provider-specific model mapping for Codex, Claude, and Ollama.
|
|
10
|
+
|
|
11
|
+
## Fast Mode
|
|
12
|
+
|
|
13
|
+
- Fixed `--fast on` so it clears the default stored power setting and actually launches Codex and Claude with low effort.
|
|
14
|
+
- Made explicit `--power` settings disable fast mode again, keeping launch settings readable and predictable.
|
|
15
|
+
- Restored `power=medium` when `--fast off` is set and no explicit power is present.
|
|
16
|
+
|
|
17
|
+
## Coverage and Tests
|
|
18
|
+
|
|
19
|
+
- Added regression coverage for persisted model settings on interactive Codex and Claude launches.
|
|
20
|
+
- Added regression coverage for `fast on` launching Codex with `model_reasoning_effort="low"` and Claude with `--effort low`.
|
|
21
|
+
- Increased the Python test suite to 280 tests.
|
|
22
|
+
|
|
23
|
+
## Release Metadata
|
|
24
|
+
|
|
25
|
+
- Updated package metadata, CLI version output, README badge, pinned installer example, and release changelog to `v0.7.5`.
|
|
26
|
+
|
|
27
|
+
## Validation and Regression Evidence
|
|
28
|
+
|
|
29
|
+
- `npm run lint`
|
|
30
|
+
- `npm test`
|
|
31
|
+
- `python3 -m pytest -q`
|
|
32
|
+
- `npm audit --omit=dev --json`
|
|
33
|
+
- `sh -n install.sh`
|
|
34
|
+
- PowerShell parser check for `install.ps1`
|
|
35
|
+
- `python3 -m logics_manager lint --require-status`
|
|
@@ -40,6 +40,14 @@
|
|
|
40
40
|
"v0.7.2": {
|
|
41
41
|
"github_tarball_sha256": "9e993b15386c7b47895cfaca9c7e92165967ef34ecf8337c64823a5b0f9eb9e0",
|
|
42
42
|
"github_zip_sha256": "1e5fd926a16811f84137636830e0b27776b9f8f8d2eb8953b620023c8bfe6fdd"
|
|
43
|
+
},
|
|
44
|
+
"v0.7.3": {
|
|
45
|
+
"github_tarball_sha256": "f68ac90a51c723eef9ae2b66106baa954f2fff8129fb5353e8eeb26b258b06c9",
|
|
46
|
+
"github_zip_sha256": "dde2512701808450b39c5c4431317d95d017f9944435db83ac4497b645feb719"
|
|
47
|
+
},
|
|
48
|
+
"v0.7.4": {
|
|
49
|
+
"github_tarball_sha256": "02b399899e611300ddf367bfb8c22ad57b499e94265e50256c8afe3f4bd73851",
|
|
50
|
+
"github_zip_sha256": "87043d49cdd50dcd7d51349984ea4abfb1a2ff50f2eab69c75a421bedd2f2fe8"
|
|
43
51
|
}
|
|
44
52
|
}
|
|
45
53
|
}
|
package/install.ps1
CHANGED
|
@@ -62,7 +62,11 @@ try {
|
|
|
62
62
|
throw "cdx install: checksum mismatch for $tag`nexpected: $Sha256`nactual: $actualSha256"
|
|
63
63
|
}
|
|
64
64
|
} else {
|
|
65
|
-
|
|
65
|
+
if ($env:CDX_ALLOW_UNVERIFIED -eq "1") {
|
|
66
|
+
Write-Warning "No official checksum available for $tag; continuing because CDX_ALLOW_UNVERIFIED=1."
|
|
67
|
+
} else {
|
|
68
|
+
throw "cdx install: no official checksum available for $tag. Set CDX_ALLOW_UNVERIFIED=1 to install without checksum verification."
|
|
69
|
+
}
|
|
66
70
|
}
|
|
67
71
|
Expand-Archive -Path $archivePath -DestinationPath $extractRoot -Force
|
|
68
72
|
|
package/install.sh
CHANGED
|
@@ -86,7 +86,13 @@ if [ -n "$EXPECTED_SHA256" ]; then
|
|
|
86
86
|
exit 1
|
|
87
87
|
fi
|
|
88
88
|
else
|
|
89
|
-
|
|
89
|
+
if [ "${CDX_ALLOW_UNVERIFIED:-}" = "1" ]; then
|
|
90
|
+
echo "cdx install: warning: no official checksum available for $TAG; continuing because CDX_ALLOW_UNVERIFIED=1" >&2
|
|
91
|
+
else
|
|
92
|
+
echo "cdx install: no official checksum available for $TAG" >&2
|
|
93
|
+
echo "Set CDX_ALLOW_UNVERIFIED=1 to install without checksum verification." >&2
|
|
94
|
+
exit 1
|
|
95
|
+
fi
|
|
90
96
|
fi
|
|
91
97
|
|
|
92
98
|
tar -xzf "$TMP_DIR/cdx-manager.tar.gz" -C "$TMP_DIR"
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
package/src/claude_refresh.py
CHANGED
|
@@ -2,7 +2,7 @@ import inspect
|
|
|
2
2
|
import threading
|
|
3
3
|
from datetime import datetime, timezone
|
|
4
4
|
|
|
5
|
-
from .claude_usage import refresh_claude_session_status
|
|
5
|
+
from .claude_usage import ClaudeAuthInvalidError, refresh_claude_session_status
|
|
6
6
|
from .config import PROVIDER_CLAUDE
|
|
7
7
|
from .errors import CdxError
|
|
8
8
|
|
|
@@ -83,6 +83,24 @@ def _refresh_claude_sessions(service, refresh_fn=None, target_names=None, force=
|
|
|
83
83
|
for t in threads:
|
|
84
84
|
t.join(timeout=10)
|
|
85
85
|
|
|
86
|
+
invalid_auth = sorted({
|
|
87
|
+
item.get("session")
|
|
88
|
+
for item in errors
|
|
89
|
+
if item.get("session") and isinstance(item.get("error"), ClaudeAuthInvalidError)
|
|
90
|
+
})
|
|
91
|
+
if invalid_auth and service.get("update_auth_state"):
|
|
92
|
+
now = datetime.now(timezone.utc).astimezone().isoformat()
|
|
93
|
+
for name in invalid_auth:
|
|
94
|
+
try:
|
|
95
|
+
service["update_auth_state"](name, lambda auth: {
|
|
96
|
+
**auth,
|
|
97
|
+
"status": "logged_out",
|
|
98
|
+
"lastCheckedAt": now,
|
|
99
|
+
"lastLoggedOutAt": now,
|
|
100
|
+
})
|
|
101
|
+
except CdxError:
|
|
102
|
+
pass
|
|
103
|
+
|
|
86
104
|
for name, usage in results.items():
|
|
87
105
|
try:
|
|
88
106
|
service["record_status"](name, usage)
|
package/src/claude_usage.py
CHANGED
|
@@ -15,6 +15,10 @@ CLAUDE_STATUS_PROBE_MODEL = os.environ.get("CDX_CLAUDE_STATUS_MODEL", "claude-ha
|
|
|
15
15
|
CLAUDE_AUTH_STATUS_TIMEOUT_SECONDS = 15
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
class ClaudeAuthInvalidError(CdxError):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
18
22
|
def _clean_oauth_token(token):
|
|
19
23
|
if not token:
|
|
20
24
|
return None
|
|
@@ -142,6 +146,10 @@ def fetch_claude_rate_limit_headers(access_token):
|
|
|
142
146
|
headers = {k.lower(): v for k, v in resp.getheaders()}
|
|
143
147
|
except urllib.error.HTTPError as e:
|
|
144
148
|
headers = {k.lower(): v for k, v in e.headers.items()}
|
|
149
|
+
if e.code == 401:
|
|
150
|
+
message = _read_http_error_message(e)
|
|
151
|
+
suffix = f": {message}" if message else ""
|
|
152
|
+
raise ClaudeAuthInvalidError(f"Claude usage unavailable (HTTP {e.code}{suffix})") from e
|
|
145
153
|
if (
|
|
146
154
|
"anthropic-ratelimit-unified-5h-utilization" not in headers
|
|
147
155
|
and "anthropic-ratelimit-unified-7d-utilization" not in headers
|
package/src/cli.py
CHANGED
|
@@ -10,6 +10,7 @@ from .cli_commands import (
|
|
|
10
10
|
handle_add,
|
|
11
11
|
handle_clean,
|
|
12
12
|
handle_config,
|
|
13
|
+
handle_configs,
|
|
13
14
|
handle_context,
|
|
14
15
|
handle_copy,
|
|
15
16
|
handle_doctor,
|
|
@@ -25,6 +26,7 @@ from .cli_commands import (
|
|
|
25
26
|
handle_logout,
|
|
26
27
|
handle_launch_setting_alias,
|
|
27
28
|
handle_notify,
|
|
29
|
+
handle_next,
|
|
28
30
|
handle_remove,
|
|
29
31
|
handle_repair,
|
|
30
32
|
handle_rename,
|
|
@@ -58,7 +60,7 @@ from .status_view import (
|
|
|
58
60
|
)
|
|
59
61
|
from .update_check import check_for_update
|
|
60
62
|
|
|
61
|
-
VERSION = "0.7.
|
|
63
|
+
VERSION = "0.7.5"
|
|
62
64
|
|
|
63
65
|
|
|
64
66
|
# ---------------------------------------------------------------------------
|
|
@@ -75,10 +77,12 @@ def _print_help(use_color=False):
|
|
|
75
77
|
f" {_style('cdx status [--json] [--refresh]', '36', use_color)}",
|
|
76
78
|
f" {_style('cdx status --small|-s [--refresh]', '36', use_color)}",
|
|
77
79
|
f" {_style('cdx status <name> [--json] [--refresh]', '36', use_color)}",
|
|
80
|
+
f" {_style('cdx next [--json] [--refresh]', '36', use_color)}",
|
|
78
81
|
f" {_style('cdx select --provider PROVIDER [--min-reasoning-effort low|medium|high] [--min-power low|medium|high] [--require-ready] [--refresh] --json', '36', use_color)}",
|
|
79
82
|
f" {_style('cdx run [session] --cwd PATH (--prompt-file PATH|--prompt TEXT) [--provider PROVIDER] [--model MODEL] [--reasoning-effort low|medium|high] [--power low|medium|high] [--permission review|default|auto|full|workspace-write|read-only|danger-full-access] [--timeout-seconds N] --json', '36', use_color)}",
|
|
80
83
|
f" {_style('cdx context show|path|init|edit|clear|set [text...] [--json]', '36', use_color)}",
|
|
81
84
|
f" {_style('cdx config <name> [--json]', '36', use_color)}",
|
|
85
|
+
f" {_style('cdx configs [--json]', '36', use_color)}",
|
|
82
86
|
f" {_style('cdx power|perm|fast|model <name|all|provider:PROVIDER|a,b> <value|default> [--json]', '36', use_color)}",
|
|
83
87
|
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
88
|
f" {_style('cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--permission|--fast|--rtk|--model|--priority|--all) [--json]', '36', use_color)}",
|
|
@@ -240,7 +244,7 @@ def main(argv, options=None):
|
|
|
240
244
|
"version": VERSION,
|
|
241
245
|
"cwd": options.get("cwd") or os.getcwd(),
|
|
242
246
|
"update_notice": _get_update_notice(service, env, options) if command not in (
|
|
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"
|
|
247
|
+
"add", "cp", "ren", "rename", "mv", "rmv", "clean", "doctor", "repair", "update", "ready", "notify", "next", "context", "config", "configs", "set", "unset", "power", "perm", "fast", "model", "history", "stats", "handoff", "login", "logout", "disable", "enable", "export", "import", "select", "run", "help", "version"
|
|
244
248
|
) else None,
|
|
245
249
|
"use_color": use_color,
|
|
246
250
|
}
|
|
@@ -289,12 +293,18 @@ def main(argv, options=None):
|
|
|
289
293
|
if command == "notify":
|
|
290
294
|
return handle_notify(rest, ctx)
|
|
291
295
|
|
|
296
|
+
if command == "next":
|
|
297
|
+
return handle_next(rest, ctx)
|
|
298
|
+
|
|
292
299
|
if command == "context":
|
|
293
300
|
return handle_context(rest, ctx)
|
|
294
301
|
|
|
295
302
|
if command == "config":
|
|
296
303
|
return handle_config(rest, ctx)
|
|
297
304
|
|
|
305
|
+
if command == "configs":
|
|
306
|
+
return handle_configs(rest, ctx)
|
|
307
|
+
|
|
298
308
|
if command == "set":
|
|
299
309
|
return handle_set(rest, ctx)
|
|
300
310
|
|
package/src/cli_commands.py
CHANGED
|
@@ -40,8 +40,9 @@ from .provider_runtime import (
|
|
|
40
40
|
)
|
|
41
41
|
from .repair import format_repair_report, repair_health
|
|
42
42
|
from .backup_bundle import read_bundle_meta
|
|
43
|
-
from .run_usage import
|
|
44
|
-
from .
|
|
43
|
+
from .run_usage import extract_run_usage
|
|
44
|
+
from .run_command import read_run_prompt, run_cdx_error_code, run_result_payload
|
|
45
|
+
from .status_view import _format_status_detail, _format_status_rows, format_priority_instruction, recommend_priority_rows
|
|
45
46
|
from .update_check import LatestReleaseCheckError, fetch_latest_release, fetch_latest_release_or_raise, is_newer_version
|
|
46
47
|
from .update_manager import build_update_plan, format_update_failure, run_update_plan, verify_updated_command
|
|
47
48
|
|
|
@@ -58,10 +59,12 @@ SET_USAGE = "Usage: cdx set <name>|--sessions all|a,b|--provider PROVIDER [--pow
|
|
|
58
59
|
UNSET_USAGE = "Usage: cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--permission|--fast|--rtk|--model|--priority|--all) [--json]"
|
|
59
60
|
SETTING_ALIAS_USAGE = "Usage: cdx power|perm|fast|model <name|all|provider:PROVIDER|a,b> <value|default> [--json]"
|
|
60
61
|
CONFIG_USAGE = "Usage: cdx config <name> [--json]"
|
|
62
|
+
CONFIGS_USAGE = "Usage: cdx configs [--json]"
|
|
61
63
|
HISTORY_USAGE = "Usage: cdx history [name] [--limit N] [--summary] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]"
|
|
62
64
|
STATS_USAGE = "Usage: cdx stats [name] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]"
|
|
63
65
|
LAST_USAGE = "Usage: cdx last [--json]"
|
|
64
66
|
SELECT_USAGE = "Usage: cdx select --provider PROVIDER [--min-reasoning-effort low|medium|high] [--min-power low|medium|high] [--require-ready] [--refresh] --json"
|
|
67
|
+
NEXT_USAGE = "Usage: cdx next [--json] [--refresh]"
|
|
65
68
|
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"
|
|
66
69
|
API_SCHEMA_VERSION = 1
|
|
67
70
|
HANDOFF_TRANSCRIPT_CHARS = 120000
|
|
@@ -598,6 +601,13 @@ def _parse_config_args(args):
|
|
|
598
601
|
return {"name": parsed["names"][0], "json": parsed["json"]}
|
|
599
602
|
|
|
600
603
|
|
|
604
|
+
def _parse_configs_args(args):
|
|
605
|
+
parsed = _parse_flag_args(args, {
|
|
606
|
+
"--json": {"key": "json", "type": "bool", "default": False},
|
|
607
|
+
}, CONFIGS_USAGE)
|
|
608
|
+
return {"json": parsed["json"]}
|
|
609
|
+
|
|
610
|
+
|
|
601
611
|
def _parse_select_args(args):
|
|
602
612
|
parsed = _parse_flag_args(args, {
|
|
603
613
|
"--provider": {"key": "provider", "type": "str", "default": None, "transform": lambda value: _parse_provider_filter(value, SELECT_USAGE)},
|
|
@@ -623,6 +633,14 @@ def _parse_select_args(args):
|
|
|
623
633
|
}
|
|
624
634
|
|
|
625
635
|
|
|
636
|
+
def _parse_next_args(args):
|
|
637
|
+
parsed = _parse_flag_args(args, {
|
|
638
|
+
"--refresh": {"key": "refresh", "type": "bool", "default": False},
|
|
639
|
+
"--json": {"key": "json", "type": "bool", "default": False},
|
|
640
|
+
}, NEXT_USAGE)
|
|
641
|
+
return {"json": parsed["json"], "refresh": parsed["refresh"]}
|
|
642
|
+
|
|
643
|
+
|
|
626
644
|
def _parse_timeout_seconds(value):
|
|
627
645
|
try:
|
|
628
646
|
parsed = float(value)
|
|
@@ -1229,70 +1247,73 @@ def handle_select(rest, ctx):
|
|
|
1229
1247
|
return 0
|
|
1230
1248
|
|
|
1231
1249
|
|
|
1232
|
-
def
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
def
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
"
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
"
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1250
|
+
def _format_next_pct(value):
|
|
1251
|
+
return "n/a" if value is None else f"{value}%"
|
|
1252
|
+
|
|
1253
|
+
|
|
1254
|
+
def _next_action(row):
|
|
1255
|
+
instruction = format_priority_instruction(row, "first")
|
|
1256
|
+
return "refresh" if instruction.startswith("refresh ") else "use"
|
|
1257
|
+
|
|
1258
|
+
|
|
1259
|
+
def _format_next_selection(session, row, use_color=False):
|
|
1260
|
+
from .cli_render import _pad_table
|
|
1261
|
+
|
|
1262
|
+
action = _next_action(row)
|
|
1263
|
+
command = f"cdx status {session['name']} --refresh" if action == "refresh" else f"cdx {session['name']}"
|
|
1264
|
+
rows = [[_style(value, "1", use_color) for value in ["SESSION", "PROVIDER", "OK", "5H", "WEEK", "REASON"]]]
|
|
1265
|
+
rows.append([
|
|
1266
|
+
_style(session["name"], "36", use_color),
|
|
1267
|
+
_dim(session.get("provider") or "-", use_color),
|
|
1268
|
+
_format_next_pct(row.get("available_pct")),
|
|
1269
|
+
_format_next_pct(row.get("remaining_5h_pct")),
|
|
1270
|
+
_format_next_pct(row.get("remaining_week_pct")),
|
|
1271
|
+
format_priority_instruction(row, "first"),
|
|
1272
|
+
])
|
|
1273
|
+
return "\n".join([
|
|
1274
|
+
_style("Next assistant:", "1", use_color),
|
|
1275
|
+
_pad_table(rows),
|
|
1276
|
+
"",
|
|
1277
|
+
f"{_style('Run:', '1', use_color)} {_style(command, '36', use_color)}",
|
|
1278
|
+
])
|
|
1279
|
+
|
|
1280
|
+
|
|
1281
|
+
def handle_next(rest, ctx):
|
|
1282
|
+
parsed = _parse_next_args(rest)
|
|
1283
|
+
rows = ctx["service"]["get_status_rows"](force_refresh=parsed["refresh"])
|
|
1284
|
+
priority = recommend_priority_rows(rows)
|
|
1285
|
+
if not priority:
|
|
1286
|
+
message = "No usable session status yet."
|
|
1287
|
+
if parsed["json"]:
|
|
1288
|
+
_write_json(ctx, _json_failure("next", "no_suitable_session", message, selection_policy="status_priority"))
|
|
1289
|
+
return 1
|
|
1290
|
+
ctx["out"](f"{_warn(message, ctx['use_color'])}\n")
|
|
1291
|
+
return 1
|
|
1292
|
+
row = priority[0]
|
|
1293
|
+
session_name = row.get("session_name")
|
|
1294
|
+
session = ctx["service"]["get_session"](session_name)
|
|
1295
|
+
if not session:
|
|
1296
|
+
message = f"Selected status row has no matching session: {session_name}"
|
|
1297
|
+
if parsed["json"]:
|
|
1298
|
+
_write_json(ctx, _json_failure("next", "missing_session", message, selection_policy="status_priority", row=row))
|
|
1299
|
+
return 1
|
|
1300
|
+
raise CdxError(message)
|
|
1301
|
+
action = _next_action(row)
|
|
1302
|
+
message = f"Selected next session {session['name']}"
|
|
1303
|
+
if parsed["json"]:
|
|
1304
|
+
_write_json(ctx, _json_success(
|
|
1305
|
+
"next",
|
|
1306
|
+
message,
|
|
1307
|
+
session=session,
|
|
1308
|
+
row=row,
|
|
1309
|
+
recommended_action=action,
|
|
1310
|
+
command=f"cdx status {session['name']} --refresh" if action == "refresh" else f"cdx {session['name']}",
|
|
1311
|
+
reason=format_priority_instruction(row, "first"),
|
|
1312
|
+
selection_policy="status_priority",
|
|
1313
|
+
))
|
|
1314
|
+
return 0
|
|
1315
|
+
ctx["out"](f"{_format_next_selection(session, row, ctx['use_color'])}\n")
|
|
1316
|
+
return 0
|
|
1296
1317
|
|
|
1297
1318
|
|
|
1298
1319
|
def handle_run(rest, ctx):
|
|
@@ -1327,7 +1348,7 @@ def handle_run(rest, ctx):
|
|
|
1327
1348
|
cwd = os.path.abspath(parsed["cwd"])
|
|
1328
1349
|
if not os.path.isdir(cwd):
|
|
1329
1350
|
raise CdxError(f"Invalid cwd: {parsed['cwd']}")
|
|
1330
|
-
prompt =
|
|
1351
|
+
prompt = read_run_prompt(parsed)
|
|
1331
1352
|
launch_updates = {}
|
|
1332
1353
|
if parsed.get("model"):
|
|
1333
1354
|
launch_updates["model"] = parsed["model"]
|
|
@@ -1352,6 +1373,7 @@ def handle_run(rest, ctx):
|
|
|
1352
1373
|
stdin_is_tty=False,
|
|
1353
1374
|
behavior="launch",
|
|
1354
1375
|
signal_emitter=ctx.get("signal_emitter"),
|
|
1376
|
+
trust_local_credentials=False,
|
|
1355
1377
|
)
|
|
1356
1378
|
run_info = _run_headless_provider_command(
|
|
1357
1379
|
run_session,
|
|
@@ -1371,7 +1393,7 @@ def handle_run(rest, ctx):
|
|
|
1371
1393
|
"exit_code": 0,
|
|
1372
1394
|
**run_info,
|
|
1373
1395
|
})
|
|
1374
|
-
_write_json(ctx,
|
|
1396
|
+
_write_json(ctx, run_result_payload(API_SCHEMA_VERSION, True, parsed, run_session, run_info=run_info))
|
|
1375
1397
|
return 0
|
|
1376
1398
|
message = "Provider process timed out." if run_info.get("timed_out") else "Provider process exited with a non-zero status."
|
|
1377
1399
|
error = CdxError(message, run_info.get("returncode") or 1)
|
|
@@ -1382,7 +1404,8 @@ def handle_run(rest, ctx):
|
|
|
1382
1404
|
"exit_code": error.exit_code,
|
|
1383
1405
|
**run_info,
|
|
1384
1406
|
})
|
|
1385
|
-
_write_json(ctx,
|
|
1407
|
+
_write_json(ctx, run_result_payload(
|
|
1408
|
+
API_SCHEMA_VERSION,
|
|
1386
1409
|
False,
|
|
1387
1410
|
parsed,
|
|
1388
1411
|
run_session,
|
|
@@ -1394,28 +1417,102 @@ def handle_run(rest, ctx):
|
|
|
1394
1417
|
return error.exit_code or 1
|
|
1395
1418
|
except CdxError as error:
|
|
1396
1419
|
run_info = getattr(error, "run_info", None)
|
|
1397
|
-
_write_json(ctx,
|
|
1420
|
+
_write_json(ctx, run_result_payload(
|
|
1421
|
+
API_SCHEMA_VERSION,
|
|
1398
1422
|
False,
|
|
1399
1423
|
locals().get("parsed", {}) or {},
|
|
1400
1424
|
locals().get("session"),
|
|
1401
1425
|
run_info=run_info,
|
|
1402
1426
|
error=error,
|
|
1403
1427
|
error_source="cdx",
|
|
1404
|
-
error_code=
|
|
1428
|
+
error_code=run_cdx_error_code(error),
|
|
1405
1429
|
))
|
|
1406
1430
|
return error.exit_code
|
|
1407
1431
|
|
|
1408
1432
|
|
|
1409
|
-
def _format_launch_config(session):
|
|
1433
|
+
def _format_launch_config(session, use_color=False):
|
|
1434
|
+
from .cli_render import _dim, _pad_table, _style
|
|
1435
|
+
|
|
1410
1436
|
launch = session.get("launch") or {}
|
|
1437
|
+
rows = [[_style("SETTING", "1", use_color), _style("VALUE", "1", use_color)]]
|
|
1438
|
+
for key, label in [
|
|
1439
|
+
("power", "Power"),
|
|
1440
|
+
("permission", "Permission"),
|
|
1441
|
+
("fast", "Fast"),
|
|
1442
|
+
("rtk", "RTK"),
|
|
1443
|
+
("model", "Model"),
|
|
1444
|
+
("priority", "Priority"),
|
|
1445
|
+
]:
|
|
1446
|
+
rows.append([
|
|
1447
|
+
_dim(label, use_color),
|
|
1448
|
+
_format_launch_setting_value(launch, key, use_color=use_color),
|
|
1449
|
+
])
|
|
1450
|
+
provider_label = f"({session['provider']})"
|
|
1411
1451
|
return "\n".join([
|
|
1412
|
-
|
|
1413
|
-
f"
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1452
|
+
_style("Launch settings:", "1", use_color),
|
|
1453
|
+
f"{_style(session['name'], '36', use_color)} {_dim(provider_label, use_color)}",
|
|
1454
|
+
_pad_table(rows),
|
|
1455
|
+
"",
|
|
1456
|
+
_dim(_format_launch_settings_hint(session["name"]), use_color),
|
|
1457
|
+
])
|
|
1458
|
+
|
|
1459
|
+
|
|
1460
|
+
def _format_launch_settings_hint(name="<name>"):
|
|
1461
|
+
return (
|
|
1462
|
+
f"Set a value: cdx set {name} --power medium --permission auto "
|
|
1463
|
+
"--fast on --rtk on --model MODEL --priority 80"
|
|
1464
|
+
)
|
|
1465
|
+
|
|
1466
|
+
|
|
1467
|
+
def _format_launch_setting_value(launch, key, use_color=False):
|
|
1468
|
+
if key == "fast" or key == "rtk":
|
|
1469
|
+
if launch.get(key) is True:
|
|
1470
|
+
return _style("on", "32", use_color)
|
|
1471
|
+
if launch.get(key) is False:
|
|
1472
|
+
return _style("off", "2", use_color)
|
|
1473
|
+
return _dim("default", use_color)
|
|
1474
|
+
value = launch.get(key)
|
|
1475
|
+
if value is None or value == "":
|
|
1476
|
+
return _dim("default", use_color)
|
|
1477
|
+
if key == "priority":
|
|
1478
|
+
return _style(str(value), "33", use_color)
|
|
1479
|
+
if key == "power":
|
|
1480
|
+
return _style(str(value), "96", use_color)
|
|
1481
|
+
if key == "permission":
|
|
1482
|
+
return _style(str(value), "32", use_color)
|
|
1483
|
+
return str(value)
|
|
1484
|
+
|
|
1485
|
+
|
|
1486
|
+
def _format_launch_configs(sessions, use_color=False):
|
|
1487
|
+
from .cli_render import _dim, _pad_table, _style
|
|
1488
|
+
|
|
1489
|
+
if not sessions:
|
|
1490
|
+
return "\n".join([
|
|
1491
|
+
_style("Launch settings:", "1", use_color),
|
|
1492
|
+
"No sessions.",
|
|
1493
|
+
"",
|
|
1494
|
+
_dim(_format_launch_settings_hint(), use_color),
|
|
1495
|
+
])
|
|
1496
|
+
rows = [[_style(value, "1", use_color) for value in [
|
|
1497
|
+
"SESSION", "PROVIDER", "POWER", "PERMISSION", "FAST", "RTK", "MODEL", "PRIORITY"
|
|
1498
|
+
]]]
|
|
1499
|
+
for session in sessions:
|
|
1500
|
+
launch = session.get("launch") or {}
|
|
1501
|
+
rows.append([
|
|
1502
|
+
_style(session["name"], "36", use_color),
|
|
1503
|
+
_dim(session.get("provider") or "-", use_color),
|
|
1504
|
+
_format_launch_setting_value(launch, "power", use_color),
|
|
1505
|
+
_format_launch_setting_value(launch, "permission", use_color),
|
|
1506
|
+
_format_launch_setting_value(launch, "fast", use_color),
|
|
1507
|
+
_format_launch_setting_value(launch, "rtk", use_color),
|
|
1508
|
+
_format_launch_setting_value(launch, "model", use_color),
|
|
1509
|
+
_format_launch_setting_value(launch, "priority", use_color),
|
|
1510
|
+
])
|
|
1511
|
+
return "\n".join([
|
|
1512
|
+
_style("Launch settings:", "1", use_color),
|
|
1513
|
+
_pad_table(rows),
|
|
1514
|
+
"",
|
|
1515
|
+
_dim(_format_launch_settings_hint(), use_color),
|
|
1419
1516
|
])
|
|
1420
1517
|
|
|
1421
1518
|
|
|
@@ -1825,7 +1922,7 @@ def _apply_launch_settings(parsed, ctx, action="set"):
|
|
|
1825
1922
|
return 0
|
|
1826
1923
|
ctx["out"](f"{_success(message, ctx['use_color'])}\n")
|
|
1827
1924
|
if len(sessions) == 1:
|
|
1828
|
-
ctx["out"](f"{_format_launch_config(sessions[0])}\n")
|
|
1925
|
+
ctx["out"](f"{_format_launch_config(sessions[0], ctx['use_color'])}\n")
|
|
1829
1926
|
return 0
|
|
1830
1927
|
|
|
1831
1928
|
|
|
@@ -1853,7 +1950,7 @@ def _clear_launch_settings(parsed, ctx, action="unset"):
|
|
|
1853
1950
|
return 0
|
|
1854
1951
|
ctx["out"](f"{_success(message, ctx['use_color'])}\n")
|
|
1855
1952
|
if len(sessions) == 1:
|
|
1856
|
-
ctx["out"](f"{_format_launch_config(sessions[0])}\n")
|
|
1953
|
+
ctx["out"](f"{_format_launch_config(sessions[0], ctx['use_color'])}\n")
|
|
1857
1954
|
return 0
|
|
1858
1955
|
|
|
1859
1956
|
|
|
@@ -1881,7 +1978,18 @@ def handle_config(rest, ctx):
|
|
|
1881
1978
|
if parsed["json"]:
|
|
1882
1979
|
_write_json(ctx, _json_success("config", message, session=session, launch=session.get("launch") or {}))
|
|
1883
1980
|
return 0
|
|
1884
|
-
ctx["out"](f"{_format_launch_config(session)}\n")
|
|
1981
|
+
ctx["out"](f"{_format_launch_config(session, ctx['use_color'])}\n")
|
|
1982
|
+
return 0
|
|
1983
|
+
|
|
1984
|
+
|
|
1985
|
+
def handle_configs(rest, ctx):
|
|
1986
|
+
parsed = _parse_configs_args(rest)
|
|
1987
|
+
sessions = ctx["service"]["list_sessions"]()
|
|
1988
|
+
message = f"Listed launch settings for {len(sessions)} session{'s' if len(sessions) != 1 else ''}"
|
|
1989
|
+
if parsed["json"]:
|
|
1990
|
+
_write_json(ctx, _json_success("configs", message, count=len(sessions), sessions=sessions))
|
|
1991
|
+
return 0
|
|
1992
|
+
ctx["out"](f"{_format_launch_configs(sessions, ctx['use_color'])}\n")
|
|
1885
1993
|
return 0
|
|
1886
1994
|
|
|
1887
1995
|
|
package/src/cli_render.py
CHANGED
|
@@ -146,6 +146,9 @@ def _format_sessions(service, use_color=False):
|
|
|
146
146
|
lines += [
|
|
147
147
|
_style("Next actions:", "1", use_color),
|
|
148
148
|
f" {_style('cdx status', '36', use_color)}",
|
|
149
|
+
f" {_style('cdx next', '36', use_color)}",
|
|
150
|
+
f" {_style('cdx configs', '36', use_color)}",
|
|
151
|
+
f" {_style('cdx stats', '36', use_color)}",
|
|
149
152
|
f" {_style('cdx ready', '36', use_color)}",
|
|
150
153
|
f" {_style('cdx perm all default', '36', use_color)}",
|
|
151
154
|
f" {_style('cdx handoff <source> <target>', '36', use_color)}",
|
package/src/provider_runtime.py
CHANGED
|
@@ -39,6 +39,7 @@ HEADLESS_CODEX_PERMISSION_ARGS = {
|
|
|
39
39
|
"auto": ["-s", "workspace-write", "-c", 'approval_policy="never"'],
|
|
40
40
|
"full": ["--dangerously-bypass-approvals-and-sandbox"],
|
|
41
41
|
}
|
|
42
|
+
REDACTED_PROMPT_ARG = "[prompt redacted]"
|
|
42
43
|
|
|
43
44
|
|
|
44
45
|
def _home_env_overrides(auth_home):
|
|
@@ -108,6 +109,12 @@ def _has_local_claude_auth(auth_home):
|
|
|
108
109
|
return bool(isinstance(oauth, dict) and _clean_oauth_token(oauth.get("accessToken")))
|
|
109
110
|
|
|
110
111
|
|
|
112
|
+
def _read_claude_launch_oauth_token(auth_home):
|
|
113
|
+
if _has_local_claude_auth(auth_home):
|
|
114
|
+
return None
|
|
115
|
+
return _read_anthropic_oauth_token(auth_home)
|
|
116
|
+
|
|
117
|
+
|
|
111
118
|
def _has_local_codex_auth(auth_home):
|
|
112
119
|
try:
|
|
113
120
|
with open(os.path.join(auth_home, "auth.json"), "r", encoding="utf-8") as handle:
|
|
@@ -307,12 +314,16 @@ def _build_launch_spec(session, cwd=None, env_override=None, initial_prompt=None
|
|
|
307
314
|
env_override = env_override or {}
|
|
308
315
|
env = {**os.environ, **env_override}
|
|
309
316
|
if session["provider"] == PROVIDER_CLAUDE:
|
|
310
|
-
|
|
317
|
+
launch = session.get("launch") or {}
|
|
318
|
+
args = ["--name", session["name"]]
|
|
319
|
+
if launch.get("model"):
|
|
320
|
+
args += ["--model", launch["model"]]
|
|
321
|
+
args += _launch_config_args(session)
|
|
311
322
|
if initial_prompt:
|
|
312
323
|
args.append(initial_prompt)
|
|
313
324
|
auth_home = _get_auth_home(session)
|
|
314
325
|
claude_env = _claude_env(env, auth_home)
|
|
315
|
-
oauth_token =
|
|
326
|
+
oauth_token = _read_claude_launch_oauth_token(auth_home)
|
|
316
327
|
if oauth_token:
|
|
317
328
|
claude_env["CLAUDE_CODE_OAUTH_TOKEN"] = oauth_token
|
|
318
329
|
return _wrap_launch_with_transcript(session, {
|
|
@@ -353,7 +364,11 @@ def _build_launch_spec(session, cwd=None, env_override=None, initial_prompt=None
|
|
|
353
364
|
},
|
|
354
365
|
"label": "ollama",
|
|
355
366
|
}, capture_transcript=capture_transcript, env=env)
|
|
356
|
-
|
|
367
|
+
launch = session.get("launch") or {}
|
|
368
|
+
args = ["--no-alt-screen", "--cd", cwd]
|
|
369
|
+
if launch.get("model"):
|
|
370
|
+
args += ["--model", launch["model"]]
|
|
371
|
+
args += _launch_config_args(session)
|
|
357
372
|
if initial_prompt:
|
|
358
373
|
args.append(initial_prompt)
|
|
359
374
|
return _wrap_launch_with_transcript(session, {
|
|
@@ -393,7 +408,7 @@ def _build_headless_launch_spec(session, cwd=None, env_override=None, initial_pr
|
|
|
393
408
|
args.append(initial_prompt)
|
|
394
409
|
auth_home = _get_auth_home(session)
|
|
395
410
|
claude_env = _claude_env(env, auth_home)
|
|
396
|
-
oauth_token =
|
|
411
|
+
oauth_token = _read_claude_launch_oauth_token(auth_home)
|
|
397
412
|
if oauth_token:
|
|
398
413
|
claude_env["CLAUDE_CODE_OAUTH_TOKEN"] = oauth_token
|
|
399
414
|
return {
|
|
@@ -401,12 +416,13 @@ def _build_headless_launch_spec(session, cwd=None, env_override=None, initial_pr
|
|
|
401
416
|
"args": args,
|
|
402
417
|
"options": {"cwd": cwd, "env": claude_env},
|
|
403
418
|
"label": "claude",
|
|
419
|
+
"sensitive_args": [initial_prompt] if initial_prompt else [],
|
|
404
420
|
}
|
|
405
421
|
|
|
406
422
|
if session["provider"] == PROVIDER_CODEX:
|
|
407
423
|
args = ["exec", "--json", "-C", cwd]
|
|
408
424
|
if model:
|
|
409
|
-
args += ["
|
|
425
|
+
args += ["--model", model]
|
|
410
426
|
if power:
|
|
411
427
|
args += ["-c", f'model_reasoning_effort="{power}"']
|
|
412
428
|
if permission:
|
|
@@ -418,6 +434,7 @@ def _build_headless_launch_spec(session, cwd=None, env_override=None, initial_pr
|
|
|
418
434
|
"args": args,
|
|
419
435
|
"options": {"cwd": cwd, "env": {**env, "CODEX_HOME": _get_auth_home(session)}},
|
|
420
436
|
"label": "codex",
|
|
437
|
+
"sensitive_args": [initial_prompt] if initial_prompt else [],
|
|
421
438
|
}
|
|
422
439
|
|
|
423
440
|
return _build_launch_spec(
|
|
@@ -468,7 +485,7 @@ def _headless_run_info(paths, spec, start_time, returncode):
|
|
|
468
485
|
"ended_at": end_time.isoformat().replace("+00:00", "Z"),
|
|
469
486
|
"duration_ms": int((end_time - start_time).total_seconds() * 1000),
|
|
470
487
|
"command": spec.get("command"),
|
|
471
|
-
"args":
|
|
488
|
+
"args": _redact_sensitive_args(spec),
|
|
472
489
|
"label": spec.get("label"),
|
|
473
490
|
"pid": None,
|
|
474
491
|
"returncode": returncode,
|
|
@@ -476,6 +493,14 @@ def _headless_run_info(paths, spec, start_time, returncode):
|
|
|
476
493
|
}
|
|
477
494
|
|
|
478
495
|
|
|
496
|
+
def _redact_sensitive_args(spec):
|
|
497
|
+
args = list(spec.get("args") or [])
|
|
498
|
+
sensitive = {value for value in (spec.get("sensitive_args") or []) if value}
|
|
499
|
+
if not sensitive:
|
|
500
|
+
return args
|
|
501
|
+
return [REDACTED_PROMPT_ARG if arg in sensitive else arg for arg in args]
|
|
502
|
+
|
|
503
|
+
|
|
479
504
|
def _run_headless_provider_command(session, cwd=None, env_override=None, initial_prompt=None,
|
|
480
505
|
timeout_seconds=None, spawn=None, run_id=None):
|
|
481
506
|
spawn = spawn or subprocess.Popen
|
|
@@ -545,7 +570,7 @@ def _run_headless_provider_command(session, cwd=None, env_override=None, initial
|
|
|
545
570
|
"ended_at": end_time.isoformat().replace("+00:00", "Z"),
|
|
546
571
|
"duration_ms": int((end_time - start_time).total_seconds() * 1000),
|
|
547
572
|
"command": spec.get("command"),
|
|
548
|
-
"args":
|
|
573
|
+
"args": _redact_sensitive_args(spec),
|
|
549
574
|
"label": spec.get("label"),
|
|
550
575
|
"pid": getattr(child, "pid", None),
|
|
551
576
|
"returncode": returncode,
|
|
@@ -558,7 +583,7 @@ def _build_login_status_spec(session, env_override=None):
|
|
|
558
583
|
if session["provider"] == PROVIDER_CLAUDE:
|
|
559
584
|
auth_home = _get_auth_home(session)
|
|
560
585
|
env = _claude_env(env, auth_home)
|
|
561
|
-
oauth_token =
|
|
586
|
+
oauth_token = _read_claude_launch_oauth_token(auth_home)
|
|
562
587
|
if oauth_token:
|
|
563
588
|
env["CLAUDE_CODE_OAUTH_TOKEN"] = oauth_token
|
|
564
589
|
|
|
@@ -639,13 +664,14 @@ def _resolve_command(command, env=None):
|
|
|
639
664
|
return shutil.which(command, path=env.get("PATH")) or command
|
|
640
665
|
|
|
641
666
|
|
|
642
|
-
def _probe_provider_auth(session, spawn_sync=None, env_override=None):
|
|
667
|
+
def _probe_provider_auth(session, spawn_sync=None, env_override=None, trust_local_credentials=True):
|
|
643
668
|
spawn_sync = spawn_sync or subprocess.run
|
|
644
669
|
spec = _build_login_status_spec(session, env_override)
|
|
645
|
-
if
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
670
|
+
if trust_local_credentials:
|
|
671
|
+
if session.get("provider") == PROVIDER_CLAUDE and _has_local_claude_auth(_get_auth_home(session)):
|
|
672
|
+
return True
|
|
673
|
+
if session.get("provider") == PROVIDER_CODEX and _has_local_codex_auth(_get_auth_home(session)):
|
|
674
|
+
return True
|
|
649
675
|
try:
|
|
650
676
|
if spawn_sync is subprocess.run:
|
|
651
677
|
command = _resolve_command(spec["command"], spec["env"])
|
|
@@ -830,8 +856,17 @@ def _should_retry_without_transcript(spec):
|
|
|
830
856
|
|
|
831
857
|
def _ensure_session_authentication(session, service, spawn=None, spawn_sync=None,
|
|
832
858
|
stdin_is_tty=True, env_override=None, behavior="launch",
|
|
833
|
-
signal_emitter=None):
|
|
834
|
-
|
|
859
|
+
signal_emitter=None, trust_local_credentials=True):
|
|
860
|
+
if behavior == "launch" and (session.get("auth") or {}).get("status") == "logged_out":
|
|
861
|
+
raise CdxError(
|
|
862
|
+
f"Session {session['name']} is not authenticated. Run: cdx login {session['name']}"
|
|
863
|
+
)
|
|
864
|
+
is_authenticated = _probe_provider_auth(
|
|
865
|
+
session,
|
|
866
|
+
spawn_sync=spawn_sync,
|
|
867
|
+
env_override=env_override,
|
|
868
|
+
trust_local_credentials=trust_local_credentials,
|
|
869
|
+
)
|
|
835
870
|
if is_authenticated:
|
|
836
871
|
return {"authenticated": True, "checked": True}
|
|
837
872
|
if behavior == "probe-only":
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from .errors import CdxError
|
|
4
|
+
from .run_usage import empty_usage, extract_run_usage
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def read_run_prompt(parsed):
|
|
8
|
+
if parsed.get("prompt") is not None:
|
|
9
|
+
return parsed["prompt"]
|
|
10
|
+
try:
|
|
11
|
+
with open(parsed["prompt_file"], "r", encoding="utf-8") as handle:
|
|
12
|
+
return handle.read()
|
|
13
|
+
except OSError as error:
|
|
14
|
+
raise CdxError(f"Unable to read prompt file: {parsed['prompt_file']}") from error
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def run_cdx_error_code(error):
|
|
18
|
+
message = str(error)
|
|
19
|
+
if message.startswith("Usage:"):
|
|
20
|
+
return "invalid_request"
|
|
21
|
+
if message.startswith("Invalid cwd:"):
|
|
22
|
+
return "invalid_cwd"
|
|
23
|
+
if message.startswith("Session is disabled:"):
|
|
24
|
+
return "session_disabled"
|
|
25
|
+
if "CLI not found on PATH" in message:
|
|
26
|
+
return "provider_cli_not_found"
|
|
27
|
+
if message.startswith("Failed to start "):
|
|
28
|
+
return "provider_start_failed"
|
|
29
|
+
if (
|
|
30
|
+
message.startswith("Unsupported reasoning effort:")
|
|
31
|
+
or message.startswith("Unsupported power:")
|
|
32
|
+
or "--reasoning-effort and --power must match" in message
|
|
33
|
+
):
|
|
34
|
+
return "invalid_reasoning_effort"
|
|
35
|
+
return "cdx_error"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def run_payload_reasoning_effort(parsed, session):
|
|
39
|
+
launch = (session.get("launch") or {}) if session else {}
|
|
40
|
+
return (
|
|
41
|
+
parsed.get("reasoning_effort")
|
|
42
|
+
or parsed.get("power")
|
|
43
|
+
or launch.get("reasoning_effort")
|
|
44
|
+
or launch.get("reasoningEffort")
|
|
45
|
+
or launch.get("power")
|
|
46
|
+
or ("low" if launch.get("fast") is True else None)
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def run_result_payload(api_schema_version, ok, parsed, session, run_info=None,
|
|
51
|
+
error=None, error_source=None, error_code=None):
|
|
52
|
+
run_info = run_info or {}
|
|
53
|
+
usage = run_info.get("usage") if isinstance(run_info.get("usage"), dict) else (
|
|
54
|
+
extract_run_usage(session.get("provider"), run_info.get("stdout_path"))
|
|
55
|
+
if session else
|
|
56
|
+
empty_usage()
|
|
57
|
+
)
|
|
58
|
+
return {
|
|
59
|
+
"schema_version": api_schema_version,
|
|
60
|
+
"ok": bool(ok),
|
|
61
|
+
"action": "run",
|
|
62
|
+
"launcher": "cdx",
|
|
63
|
+
"session": session.get("name") if session else None,
|
|
64
|
+
"provider": session.get("provider") if session else parsed.get("provider"),
|
|
65
|
+
"model": parsed.get("model") or ((session.get("launch") or {}).get("model") if session else None),
|
|
66
|
+
"reasoning_effort": run_payload_reasoning_effort(parsed, session),
|
|
67
|
+
"power": parsed.get("power") or ((session.get("launch") or {}).get("power") if session else None),
|
|
68
|
+
"cwd": os.path.abspath(parsed.get("cwd") or os.getcwd()),
|
|
69
|
+
"run_id": run_info.get("run_id"),
|
|
70
|
+
"exit_code": run_info.get("returncode"),
|
|
71
|
+
"duration_seconds": (run_info.get("duration_ms") / 1000.0) if run_info.get("duration_ms") is not None else None,
|
|
72
|
+
"transcript_path": run_info.get("transcript_path"),
|
|
73
|
+
"stdout_path": run_info.get("stdout_path"),
|
|
74
|
+
"stderr_path": run_info.get("stderr_path"),
|
|
75
|
+
"usage": usage,
|
|
76
|
+
"warnings": [],
|
|
77
|
+
"error": None if ok else {
|
|
78
|
+
"source": error_source or "cdx",
|
|
79
|
+
"code": error_code or "cdx_error",
|
|
80
|
+
"message": str(error) if error else "Run failed.",
|
|
81
|
+
"provider_code": run_info.get("returncode") if error_source == "provider" else None,
|
|
82
|
+
},
|
|
83
|
+
}
|
package/src/session_service.py
CHANGED
|
@@ -23,19 +23,26 @@ RESERVED_SESSION_NAMES = {
|
|
|
23
23
|
"add",
|
|
24
24
|
"clean",
|
|
25
25
|
"context",
|
|
26
|
+
"configs",
|
|
26
27
|
"cp",
|
|
27
28
|
"disable",
|
|
28
29
|
"doctor",
|
|
29
30
|
"enable",
|
|
30
31
|
"export",
|
|
32
|
+
"fast",
|
|
31
33
|
"help",
|
|
32
34
|
"handoff",
|
|
33
35
|
"history",
|
|
34
36
|
"import",
|
|
37
|
+
"last",
|
|
35
38
|
"login",
|
|
36
39
|
"logout",
|
|
40
|
+
"model",
|
|
37
41
|
"mv",
|
|
42
|
+
"next",
|
|
38
43
|
"notify",
|
|
44
|
+
"perm",
|
|
45
|
+
"power",
|
|
39
46
|
"ready",
|
|
40
47
|
"repair",
|
|
41
48
|
"ren",
|
|
@@ -45,6 +52,7 @@ RESERVED_SESSION_NAMES = {
|
|
|
45
52
|
"select",
|
|
46
53
|
"config",
|
|
47
54
|
"set",
|
|
55
|
+
"stats",
|
|
48
56
|
"status",
|
|
49
57
|
"unset",
|
|
50
58
|
"update",
|
|
@@ -63,6 +71,10 @@ LAUNCH_PERMISSION_VALUES = {"review", "default", "auto", "full"}
|
|
|
63
71
|
MAX_LAUNCH_MODEL_LENGTH = 128
|
|
64
72
|
MIN_LAUNCH_PRIORITY = 0
|
|
65
73
|
MAX_LAUNCH_PRIORITY = 100
|
|
74
|
+
DEFAULT_LAUNCH_SETTINGS = {
|
|
75
|
+
"power": "medium",
|
|
76
|
+
"fast": False,
|
|
77
|
+
}
|
|
66
78
|
|
|
67
79
|
|
|
68
80
|
def _encode(name):
|
|
@@ -450,6 +462,8 @@ def create_session_service(options=None):
|
|
|
450
462
|
raise CdxError("Session name is required")
|
|
451
463
|
if str(name) != str(name).strip():
|
|
452
464
|
raise CdxError("Session name cannot start or end with whitespace")
|
|
465
|
+
if str(name) in (".", ".."):
|
|
466
|
+
raise CdxError("Session name cannot be . or ..")
|
|
453
467
|
if len(str(name)) > MAX_SESSION_NAME_LENGTH:
|
|
454
468
|
raise CdxError(f"Session name is too long (max {MAX_SESSION_NAME_LENGTH} characters)")
|
|
455
469
|
if any(ord(ch) < 32 or ord(ch) == 127 for ch in str(name)):
|
|
@@ -544,6 +558,7 @@ def create_session_service(options=None):
|
|
|
544
558
|
"lastLaunchedAt": None,
|
|
545
559
|
"lastStatusAt": None,
|
|
546
560
|
"lastStatus": None,
|
|
561
|
+
"launch": dict(DEFAULT_LAUNCH_SETTINGS),
|
|
547
562
|
"auth": {
|
|
548
563
|
"status": "unknown",
|
|
549
564
|
"lastCheckedAt": None,
|
|
@@ -801,6 +816,16 @@ def create_session_service(options=None):
|
|
|
801
816
|
raise CdxError("At least one launch setting is required.")
|
|
802
817
|
current = _normalize_launch_settings(session.get("launch") or {})
|
|
803
818
|
launch = {**current, **updates}
|
|
819
|
+
explicit_power = "power" in updates or "reasoning_effort" in updates
|
|
820
|
+
if explicit_power and "fast" not in updates:
|
|
821
|
+
launch["fast"] = False
|
|
822
|
+
if "fast" in updates and not explicit_power:
|
|
823
|
+
if updates["fast"] is True:
|
|
824
|
+
launch.pop("power", None)
|
|
825
|
+
launch.pop("reasoning_effort", None)
|
|
826
|
+
launch.pop("reasoningEffort", None)
|
|
827
|
+
elif not any(key in launch for key in ("power", "reasoning_effort", "reasoningEffort")):
|
|
828
|
+
launch["power"] = DEFAULT_LAUNCH_SETTINGS["power"]
|
|
804
829
|
now = _local_now_iso()
|
|
805
830
|
return store["update_session"](name, lambda s: {
|
|
806
831
|
**s,
|
|
@@ -1117,7 +1142,7 @@ def create_session_service(options=None):
|
|
|
1117
1142
|
bundle_bytes = encode_bundle(payload, include_auth=include_auth, passphrase=passphrase)
|
|
1118
1143
|
if progress_callback:
|
|
1119
1144
|
progress_callback({"event": "writing_started", "path": file_path, "bundle_size_bytes": len(bundle_bytes)})
|
|
1120
|
-
|
|
1145
|
+
os.makedirs(os.path.dirname(os.path.abspath(file_path)) or ".", exist_ok=True)
|
|
1121
1146
|
with open(file_path, "wb") as handle:
|
|
1122
1147
|
handle.write(bundle_bytes)
|
|
1123
1148
|
if sys.platform != "win32":
|
package/src/status_view.py
CHANGED
|
@@ -87,15 +87,11 @@ def _format_status_rows(rows, use_color=False, small=False):
|
|
|
87
87
|
return "SESSION STATUS OK 5H WEEK BLOCK CR RESET 5H RESET WEEK UPDATED\nNo saved sessions yet."
|
|
88
88
|
headers = [_style(header, "1", use_color) for header in headers]
|
|
89
89
|
active_rows = [r for r in rows if r.get("enabled", True) is not False]
|
|
90
|
-
|
|
91
|
-
r for r in active_rows
|
|
92
|
-
if _format_auth_status(r) != "logged out"
|
|
93
|
-
]
|
|
90
|
+
priority = recommend_priority_rows(rows)
|
|
94
91
|
disabled_rows = sorted(
|
|
95
92
|
[r for r in rows if r.get("enabled", True) is False],
|
|
96
93
|
key=lambda r: r.get("session_name") or "",
|
|
97
94
|
)
|
|
98
|
-
priority = _recommend_priority_sessions(priority_candidates)
|
|
99
95
|
priority_names = {r.get("session_name") for r in priority}
|
|
100
96
|
non_priority_active = [
|
|
101
97
|
r for r in active_rows
|
|
@@ -180,6 +176,19 @@ def _format_auth_status(row):
|
|
|
180
176
|
return "unknown"
|
|
181
177
|
|
|
182
178
|
|
|
179
|
+
def recommend_priority_rows(rows):
|
|
180
|
+
active_rows = [r for r in rows if r.get("enabled", True) is not False]
|
|
181
|
+
priority_candidates = [
|
|
182
|
+
r for r in active_rows
|
|
183
|
+
if _format_auth_status(r) != "logged out"
|
|
184
|
+
]
|
|
185
|
+
return _recommend_priority_sessions(priority_candidates)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def format_priority_instruction(row, position="first"):
|
|
189
|
+
return _priority_instruction(row, position)
|
|
190
|
+
|
|
191
|
+
|
|
183
192
|
def _recommend_priority_sessions(rows):
|
|
184
193
|
if not rows:
|
|
185
194
|
return []
|