cdx-manager 0.7.3 → 0.7.4
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 +15 -6
- package/changelogs/CHANGELOGS_0_7_4.md +45 -0
- package/checksums/release-archives.json +4 -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 +39 -12
- package/src/run_command.py +83 -0
- package/src/session_service.py +16 -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.4 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
|
|
@@ -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`
|
|
@@ -40,6 +40,10 @@
|
|
|
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"
|
|
43
47
|
}
|
|
44
48
|
}
|
|
45
49
|
}
|
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.4"
|
|
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:
|
|
@@ -312,7 +319,7 @@ def _build_launch_spec(session, cwd=None, env_override=None, initial_prompt=None
|
|
|
312
319
|
args.append(initial_prompt)
|
|
313
320
|
auth_home = _get_auth_home(session)
|
|
314
321
|
claude_env = _claude_env(env, auth_home)
|
|
315
|
-
oauth_token =
|
|
322
|
+
oauth_token = _read_claude_launch_oauth_token(auth_home)
|
|
316
323
|
if oauth_token:
|
|
317
324
|
claude_env["CLAUDE_CODE_OAUTH_TOKEN"] = oauth_token
|
|
318
325
|
return _wrap_launch_with_transcript(session, {
|
|
@@ -393,7 +400,7 @@ def _build_headless_launch_spec(session, cwd=None, env_override=None, initial_pr
|
|
|
393
400
|
args.append(initial_prompt)
|
|
394
401
|
auth_home = _get_auth_home(session)
|
|
395
402
|
claude_env = _claude_env(env, auth_home)
|
|
396
|
-
oauth_token =
|
|
403
|
+
oauth_token = _read_claude_launch_oauth_token(auth_home)
|
|
397
404
|
if oauth_token:
|
|
398
405
|
claude_env["CLAUDE_CODE_OAUTH_TOKEN"] = oauth_token
|
|
399
406
|
return {
|
|
@@ -401,6 +408,7 @@ def _build_headless_launch_spec(session, cwd=None, env_override=None, initial_pr
|
|
|
401
408
|
"args": args,
|
|
402
409
|
"options": {"cwd": cwd, "env": claude_env},
|
|
403
410
|
"label": "claude",
|
|
411
|
+
"sensitive_args": [initial_prompt] if initial_prompt else [],
|
|
404
412
|
}
|
|
405
413
|
|
|
406
414
|
if session["provider"] == PROVIDER_CODEX:
|
|
@@ -418,6 +426,7 @@ def _build_headless_launch_spec(session, cwd=None, env_override=None, initial_pr
|
|
|
418
426
|
"args": args,
|
|
419
427
|
"options": {"cwd": cwd, "env": {**env, "CODEX_HOME": _get_auth_home(session)}},
|
|
420
428
|
"label": "codex",
|
|
429
|
+
"sensitive_args": [initial_prompt] if initial_prompt else [],
|
|
421
430
|
}
|
|
422
431
|
|
|
423
432
|
return _build_launch_spec(
|
|
@@ -468,7 +477,7 @@ def _headless_run_info(paths, spec, start_time, returncode):
|
|
|
468
477
|
"ended_at": end_time.isoformat().replace("+00:00", "Z"),
|
|
469
478
|
"duration_ms": int((end_time - start_time).total_seconds() * 1000),
|
|
470
479
|
"command": spec.get("command"),
|
|
471
|
-
"args":
|
|
480
|
+
"args": _redact_sensitive_args(spec),
|
|
472
481
|
"label": spec.get("label"),
|
|
473
482
|
"pid": None,
|
|
474
483
|
"returncode": returncode,
|
|
@@ -476,6 +485,14 @@ def _headless_run_info(paths, spec, start_time, returncode):
|
|
|
476
485
|
}
|
|
477
486
|
|
|
478
487
|
|
|
488
|
+
def _redact_sensitive_args(spec):
|
|
489
|
+
args = list(spec.get("args") or [])
|
|
490
|
+
sensitive = {value for value in (spec.get("sensitive_args") or []) if value}
|
|
491
|
+
if not sensitive:
|
|
492
|
+
return args
|
|
493
|
+
return [REDACTED_PROMPT_ARG if arg in sensitive else arg for arg in args]
|
|
494
|
+
|
|
495
|
+
|
|
479
496
|
def _run_headless_provider_command(session, cwd=None, env_override=None, initial_prompt=None,
|
|
480
497
|
timeout_seconds=None, spawn=None, run_id=None):
|
|
481
498
|
spawn = spawn or subprocess.Popen
|
|
@@ -545,7 +562,7 @@ def _run_headless_provider_command(session, cwd=None, env_override=None, initial
|
|
|
545
562
|
"ended_at": end_time.isoformat().replace("+00:00", "Z"),
|
|
546
563
|
"duration_ms": int((end_time - start_time).total_seconds() * 1000),
|
|
547
564
|
"command": spec.get("command"),
|
|
548
|
-
"args":
|
|
565
|
+
"args": _redact_sensitive_args(spec),
|
|
549
566
|
"label": spec.get("label"),
|
|
550
567
|
"pid": getattr(child, "pid", None),
|
|
551
568
|
"returncode": returncode,
|
|
@@ -558,7 +575,7 @@ def _build_login_status_spec(session, env_override=None):
|
|
|
558
575
|
if session["provider"] == PROVIDER_CLAUDE:
|
|
559
576
|
auth_home = _get_auth_home(session)
|
|
560
577
|
env = _claude_env(env, auth_home)
|
|
561
|
-
oauth_token =
|
|
578
|
+
oauth_token = _read_claude_launch_oauth_token(auth_home)
|
|
562
579
|
if oauth_token:
|
|
563
580
|
env["CLAUDE_CODE_OAUTH_TOKEN"] = oauth_token
|
|
564
581
|
|
|
@@ -639,13 +656,14 @@ def _resolve_command(command, env=None):
|
|
|
639
656
|
return shutil.which(command, path=env.get("PATH")) or command
|
|
640
657
|
|
|
641
658
|
|
|
642
|
-
def _probe_provider_auth(session, spawn_sync=None, env_override=None):
|
|
659
|
+
def _probe_provider_auth(session, spawn_sync=None, env_override=None, trust_local_credentials=True):
|
|
643
660
|
spawn_sync = spawn_sync or subprocess.run
|
|
644
661
|
spec = _build_login_status_spec(session, env_override)
|
|
645
|
-
if
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
662
|
+
if trust_local_credentials:
|
|
663
|
+
if session.get("provider") == PROVIDER_CLAUDE and _has_local_claude_auth(_get_auth_home(session)):
|
|
664
|
+
return True
|
|
665
|
+
if session.get("provider") == PROVIDER_CODEX and _has_local_codex_auth(_get_auth_home(session)):
|
|
666
|
+
return True
|
|
649
667
|
try:
|
|
650
668
|
if spawn_sync is subprocess.run:
|
|
651
669
|
command = _resolve_command(spec["command"], spec["env"])
|
|
@@ -830,8 +848,17 @@ def _should_retry_without_transcript(spec):
|
|
|
830
848
|
|
|
831
849
|
def _ensure_session_authentication(session, service, spawn=None, spawn_sync=None,
|
|
832
850
|
stdin_is_tty=True, env_override=None, behavior="launch",
|
|
833
|
-
signal_emitter=None):
|
|
834
|
-
|
|
851
|
+
signal_emitter=None, trust_local_credentials=True):
|
|
852
|
+
if behavior == "launch" and (session.get("auth") or {}).get("status") == "logged_out":
|
|
853
|
+
raise CdxError(
|
|
854
|
+
f"Session {session['name']} is not authenticated. Run: cdx login {session['name']}"
|
|
855
|
+
)
|
|
856
|
+
is_authenticated = _probe_provider_auth(
|
|
857
|
+
session,
|
|
858
|
+
spawn_sync=spawn_sync,
|
|
859
|
+
env_override=env_override,
|
|
860
|
+
trust_local_credentials=trust_local_credentials,
|
|
861
|
+
)
|
|
835
862
|
if is_authenticated:
|
|
836
863
|
return {"authenticated": True, "checked": True}
|
|
837
864
|
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,
|
|
@@ -1117,7 +1132,7 @@ def create_session_service(options=None):
|
|
|
1117
1132
|
bundle_bytes = encode_bundle(payload, include_auth=include_auth, passphrase=passphrase)
|
|
1118
1133
|
if progress_callback:
|
|
1119
1134
|
progress_callback({"event": "writing_started", "path": file_path, "bundle_size_bytes": len(bundle_bytes)})
|
|
1120
|
-
|
|
1135
|
+
os.makedirs(os.path.dirname(os.path.abspath(file_path)) or ".", exist_ok=True)
|
|
1121
1136
|
with open(file_path, "wb") as handle:
|
|
1122
1137
|
handle.write(bundle_bytes)
|
|
1123
1138
|
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 []
|