cdx-manager 0.5.3 → 0.5.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 +4 -4
- package/changelogs/CHANGELOGS_0_5_4.md +49 -0
- package/changelogs/CHANGELOGS_0_5_5.md +48 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/claude_refresh.py +36 -19
- package/src/claude_usage.py +33 -1
- package/src/cli.py +1 -1
- package/src/cli_commands.py +185 -156
- package/src/codex_usage.py +36 -7
- package/src/config.py +4 -0
- package/src/notify.py +25 -1
- package/src/provider_runtime.py +17 -10
- package/src/session_service.py +99 -28
- package/src/status_source.py +22 -20
- package/src/status_view.py +22 -4
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# CDX Manager
|
|
2
2
|
|
|
3
|
-
[](LICENSE) ](LICENSE)  
|
|
4
4
|
|
|
5
5
|
**Run multiple Codex and Claude sessions from one terminal. Switch between accounts instantly.**
|
|
6
6
|
|
|
@@ -35,7 +35,7 @@ One command to launch any session. Zero auth juggling.
|
|
|
35
35
|
- **Multiple accounts, one tool.** Register as many Codex or Claude sessions as you need. Each one gets its own isolated auth environment — no cross-contamination between accounts.
|
|
36
36
|
- **Instant launch.** `cdx work` opens your "work" session. `cdx personal` opens another. No config files to edit mid-flow.
|
|
37
37
|
- **Auth guardrails.** `cdx` checks authentication before launching. If a session is not logged in, it tells you exactly what to run — no silent failures.
|
|
38
|
-
- **Usage at a glance.** `cdx status` shows token usage, 5-hour window quota, weekly quota,
|
|
38
|
+
- **Usage at a glance.** `cdx status` shows token usage, 5-hour window quota, weekly quota, last-updated timestamps, priority guidance, and the last launched session in one aligned table.
|
|
39
39
|
- **Session control.** Disable a session without deleting it when an account is temporarily out of credits; disabled sessions remain visible and sort last.
|
|
40
40
|
- **Shared handoff context.** Keep a per-workspace Markdown context, or build one from a source session transcript, and install it into another assistant session before switching providers or accounts.
|
|
41
41
|
- **Passive status resolution.** Codex status is read from the local Codex app-server rate-limit API when available, with legacy transcript/history parsing kept as a fallback.
|
|
@@ -126,7 +126,7 @@ For a specific version:
|
|
|
126
126
|
|
|
127
127
|
```bash
|
|
128
128
|
curl -fsSL https://raw.githubusercontent.com/AlexAgo83/cdx-manager/main/install.sh -o install.sh
|
|
129
|
-
CDX_VERSION=v0.5.
|
|
129
|
+
CDX_VERSION=v0.5.5 sh install.sh
|
|
130
130
|
```
|
|
131
131
|
|
|
132
132
|
From source:
|
|
@@ -349,7 +349,7 @@ cdx import backup-auth.cdx --passphrase-env CDX_BUNDLE_PASSPHRASE
|
|
|
349
349
|
|
|
350
350
|
Notes:
|
|
351
351
|
|
|
352
|
-
- `--include-auth` is encrypted
|
|
352
|
+
- `--include-auth` is encrypted, requires a passphrase, and exports only provider credential files rather than full profile caches or logs.
|
|
353
353
|
- Without `--passphrase-env`, `cdx` prompts in an interactive terminal.
|
|
354
354
|
- `--sessions work,perso` exports or imports only a subset.
|
|
355
355
|
- `--force` allows overwriting existing destination sessions during import or replacing an existing bundle file during export.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Changelog (`0.5.3 -> 0.5.4`)
|
|
2
|
+
|
|
3
|
+
Release date: 2026-05-23
|
|
4
|
+
|
|
5
|
+
## Major Highlights
|
|
6
|
+
|
|
7
|
+
- Added a Logics ADR for the code quality, security, performance, and test coverage improvements completed after `0.5.3`.
|
|
8
|
+
- Hardened bundle import path handling and launch prompt validation without changing CLI contracts or on-disk formats.
|
|
9
|
+
- Improved status parsing and provider handling internals for safer future maintenance.
|
|
10
|
+
- Added structured Codex app-server probe diagnostics while keeping the existing status API behavior compatible.
|
|
11
|
+
|
|
12
|
+
## Security and Runtime Hardening
|
|
13
|
+
|
|
14
|
+
- Hardened bundle relative path validation against traversal, absolute paths, and Windows drive paths.
|
|
15
|
+
- Validated provider launch `initial_prompt` type and length before it is passed into subprocess arguments.
|
|
16
|
+
- Replaced mutable signal-capture state with explicit `nonlocal` signal forwarding.
|
|
17
|
+
- Added Linux `notify-send` support with backend timeout handling and shell-free notification arguments.
|
|
18
|
+
|
|
19
|
+
## Status and Provider Internals
|
|
20
|
+
|
|
21
|
+
- Moved status extraction regex compilation to module-level constants.
|
|
22
|
+
- Added shared provider constants for Codex and Claude identity checks.
|
|
23
|
+
- Improved Claude status refresh handling for sync and async refresh functions without per-thread event loops.
|
|
24
|
+
- Added `fetch_codex_rate_limit_diagnostic()` to expose structured failure reasons such as missing auth home, initialize failure, app-server read failure, missing rate limits, and missing Codex CLI.
|
|
25
|
+
|
|
26
|
+
## CLI Parser Maintenance
|
|
27
|
+
|
|
28
|
+
- Factorized more CLI flag parsing through a shared flat-flag parser.
|
|
29
|
+
- Added `--flag=value` support where appropriate, including `cdx notify --poll=<seconds>`.
|
|
30
|
+
- Preserved existing usage errors and JSON/non-JSON output contracts.
|
|
31
|
+
|
|
32
|
+
## Documentation
|
|
33
|
+
|
|
34
|
+
- Added `logics/architecture/adr_001_code_quality_and_security_improvements.md` documenting the security, test, quality, and performance decisions.
|
|
35
|
+
- Updated package metadata, CLI version output, README badge, and pinned installer example to `v0.5.4`.
|
|
36
|
+
|
|
37
|
+
## Validation and Regression Coverage
|
|
38
|
+
|
|
39
|
+
- Added direct session-store rollback tests for add, remove, rename, and replace failure paths.
|
|
40
|
+
- Added status-source tests for key-value, Codex block, Claude block, artifact selection, reset timestamp parsing, and safe relative paths.
|
|
41
|
+
- Added notification tests for macOS, Linux, backend fallback, timeout, and argument handling.
|
|
42
|
+
- Added CLI parser tests for update, status, repair, export/import subsets, corrupt bundles, and clean behavior with and without logs.
|
|
43
|
+
- Added Codex app-server diagnostic tests for success and failure cases.
|
|
44
|
+
|
|
45
|
+
## Validation and Regression Evidence
|
|
46
|
+
|
|
47
|
+
- `npm test`
|
|
48
|
+
- `npm run lint`
|
|
49
|
+
- `python logics/skills/logics.py lint --require-status`
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Changelog (`0.5.4 -> 0.5.5`)
|
|
2
|
+
|
|
3
|
+
Release date: 2026-05-24
|
|
4
|
+
|
|
5
|
+
## Major Highlights
|
|
6
|
+
|
|
7
|
+
- Fixed Claude usage probing for OAuth credentials by using bearer-token authentication.
|
|
8
|
+
- Improved `cdx status` guidance with progress output, conservative reset timing, near-empty account handling, and last-launched session context.
|
|
9
|
+
- Made encrypted auth exports lightweight by backing up provider credential files instead of full profile caches, logs, and runtime artifacts.
|
|
10
|
+
|
|
11
|
+
## Usage and Status
|
|
12
|
+
|
|
13
|
+
- Added progress output while `cdx status` resolves session statuses in text mode, while keeping `--json` output clean.
|
|
14
|
+
- Added a `Current:` line under the priority recommendation showing the last launched session, useful before running a handoff.
|
|
15
|
+
- Removed the older status tip line now that status output carries actionable priority and current-session context.
|
|
16
|
+
- Treated sessions with `5%` or less availability as empty for priority recommendations, while preserving the true percentage in the table.
|
|
17
|
+
- Added a conservative one-minute reset countdown margin so users are less likely to switch back before a reset is usable.
|
|
18
|
+
|
|
19
|
+
## Claude Usage Handling
|
|
20
|
+
|
|
21
|
+
- Fixed the Claude rate-limit probe to send OAuth access tokens as `Authorization: Bearer ...`.
|
|
22
|
+
- Surfaced Claude HTTP failures without rate-limit headers as structured refresh warnings, including API error text when available.
|
|
23
|
+
- Preserved existing fallback behavior for unavailable network probes and historical status data.
|
|
24
|
+
|
|
25
|
+
## Export and Backup
|
|
26
|
+
|
|
27
|
+
- Limited `cdx export --include-auth` to provider credential files:
|
|
28
|
+
- Codex: `auth.json`
|
|
29
|
+
- Claude: `claude-home/.claude/.credentials.json`, `claude-home/.claude.json`, and legacy `claude-home/auth.json`
|
|
30
|
+
- Excluded profile caches, logs, SQLite usage databases, temporary files, and provider runtime artifacts from auth bundles.
|
|
31
|
+
- Added export progress output in text mode and a final summary with bundle size, auth file count, and auth data size.
|
|
32
|
+
- Kept `cdx export --json` clean and added bundle metrics to its JSON payload.
|
|
33
|
+
|
|
34
|
+
## Documentation
|
|
35
|
+
|
|
36
|
+
- Updated package metadata, CLI version output, README badge, and pinned installer example to `v0.5.5`.
|
|
37
|
+
- Clarified that encrypted auth exports include credential files rather than full profile caches or logs.
|
|
38
|
+
|
|
39
|
+
## Validation and Regression Coverage
|
|
40
|
+
|
|
41
|
+
- Added regression coverage for Claude HTTP failures without usage headers.
|
|
42
|
+
- Added status rendering tests for progress output, last-launched context, near-empty priority handling, and removed tip text.
|
|
43
|
+
- Added export/import tests proving auth bundles exclude non-auth profile files such as `logs_2.sqlite`.
|
|
44
|
+
|
|
45
|
+
## Validation and Regression Evidence
|
|
46
|
+
|
|
47
|
+
- `python3 -m pytest test/test_cli_py.py test/test_session_service_py.py`
|
|
48
|
+
- `python3 -m pytest test/test_runtime_py.py test/test_cli_py.py test/test_status_source_py.py`
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
package/src/claude_refresh.py
CHANGED
|
@@ -3,6 +3,7 @@ import threading
|
|
|
3
3
|
from datetime import datetime, timezone
|
|
4
4
|
|
|
5
5
|
from .claude_usage import refresh_claude_session_status
|
|
6
|
+
from .config import PROVIDER_CLAUDE
|
|
6
7
|
from .errors import CdxError
|
|
7
8
|
|
|
8
9
|
CLAUDE_REFRESH_TTL_SECONDS = 10 * 60
|
|
@@ -35,7 +36,7 @@ def _refresh_claude_sessions(service, refresh_fn=None, target_names=None, force=
|
|
|
35
36
|
sessions = service["list_sessions"]()
|
|
36
37
|
claude_sessions = [
|
|
37
38
|
s for s in sessions
|
|
38
|
-
if s["provider"] ==
|
|
39
|
+
if s["provider"] == PROVIDER_CLAUDE
|
|
39
40
|
and s.get("enabled", True) is not False
|
|
40
41
|
and (not target_names or s["name"] in target_names)
|
|
41
42
|
and (force or _is_stale(s, ttl_seconds=ttl_seconds))
|
|
@@ -45,25 +46,41 @@ def _refresh_claude_sessions(service, refresh_fn=None, target_names=None, force=
|
|
|
45
46
|
|
|
46
47
|
errors = []
|
|
47
48
|
results = {}
|
|
48
|
-
threads = []
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
50
|
+
if inspect.iscoroutinefunction(refresh_fn):
|
|
51
|
+
import asyncio
|
|
52
|
+
|
|
53
|
+
async def fetch_all():
|
|
54
|
+
async def fetch_async(s):
|
|
55
|
+
try:
|
|
56
|
+
usage = await refresh_fn(s)
|
|
57
|
+
if usage:
|
|
58
|
+
results[s["name"]] = usage
|
|
59
|
+
except Exception as e:
|
|
60
|
+
errors.append({"session": s["name"], "error": e})
|
|
61
|
+
|
|
62
|
+
await asyncio.gather(*(fetch_async(s) for s in claude_sessions))
|
|
63
|
+
|
|
64
|
+
asyncio.run(fetch_all())
|
|
65
|
+
else:
|
|
66
|
+
threads = []
|
|
67
|
+
|
|
68
|
+
def fetch(s):
|
|
69
|
+
try:
|
|
70
|
+
usage = refresh_fn(s)
|
|
71
|
+
if inspect.isawaitable(usage):
|
|
72
|
+
raise CdxError("Claude refresh function returned an awaitable from a sync callable.")
|
|
73
|
+
if usage:
|
|
74
|
+
results[s["name"]] = usage
|
|
75
|
+
except Exception as e:
|
|
76
|
+
errors.append({"session": s["name"], "error": e})
|
|
77
|
+
|
|
78
|
+
for s in claude_sessions:
|
|
79
|
+
t = threading.Thread(target=fetch, args=(s,), daemon=True)
|
|
80
|
+
threads.append(t)
|
|
81
|
+
t.start()
|
|
82
|
+
for t in threads:
|
|
83
|
+
t.join(timeout=10)
|
|
67
84
|
|
|
68
85
|
for name, usage in results.items():
|
|
69
86
|
try:
|
package/src/claude_usage.py
CHANGED
|
@@ -4,6 +4,8 @@ import urllib.request
|
|
|
4
4
|
import urllib.error
|
|
5
5
|
from datetime import datetime, timezone
|
|
6
6
|
|
|
7
|
+
from .errors import CdxError
|
|
8
|
+
|
|
7
9
|
MONTH_ABBR = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
|
8
10
|
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
|
|
9
11
|
CLAUDE_STATUS_PROBE_MODEL = os.environ.get("CDX_CLAUDE_STATUS_MODEL", "claude-haiku-4-5-20251001")
|
|
@@ -24,6 +26,29 @@ def _format_reset_date(unix_seconds):
|
|
|
24
26
|
return f"{MONTH_ABBR[dt.month - 1]} {dt.day} {str(dt.hour).zfill(2)}:{str(dt.minute).zfill(2)}"
|
|
25
27
|
|
|
26
28
|
|
|
29
|
+
def _read_http_error_message(error):
|
|
30
|
+
try:
|
|
31
|
+
body = error.read().decode("utf-8", errors="replace")
|
|
32
|
+
except (AttributeError, OSError, UnicodeDecodeError):
|
|
33
|
+
body = ""
|
|
34
|
+
if not body:
|
|
35
|
+
return ""
|
|
36
|
+
try:
|
|
37
|
+
payload = json.loads(body)
|
|
38
|
+
except json.JSONDecodeError:
|
|
39
|
+
return body.strip()
|
|
40
|
+
detail = payload.get("error") if isinstance(payload, dict) else None
|
|
41
|
+
if isinstance(detail, dict):
|
|
42
|
+
message = detail.get("message") or detail.get("type")
|
|
43
|
+
if message:
|
|
44
|
+
return str(message)
|
|
45
|
+
if isinstance(payload, dict):
|
|
46
|
+
message = payload.get("message") or payload.get("detail")
|
|
47
|
+
if message:
|
|
48
|
+
return str(message)
|
|
49
|
+
return body.strip()
|
|
50
|
+
|
|
51
|
+
|
|
27
52
|
def fetch_claude_rate_limit_headers(access_token):
|
|
28
53
|
body = json.dumps({
|
|
29
54
|
"model": CLAUDE_STATUS_PROBE_MODEL,
|
|
@@ -35,7 +60,7 @@ def fetch_claude_rate_limit_headers(access_token):
|
|
|
35
60
|
"https://api.anthropic.com/v1/messages",
|
|
36
61
|
data=body,
|
|
37
62
|
headers={
|
|
38
|
-
"
|
|
63
|
+
"Authorization": f"Bearer {access_token}",
|
|
39
64
|
"anthropic-version": "2023-06-01",
|
|
40
65
|
"content-type": "application/json",
|
|
41
66
|
},
|
|
@@ -47,6 +72,13 @@ def fetch_claude_rate_limit_headers(access_token):
|
|
|
47
72
|
headers = {k.lower(): v for k, v in resp.getheaders()}
|
|
48
73
|
except urllib.error.HTTPError as e:
|
|
49
74
|
headers = {k.lower(): v for k, v in e.headers.items()}
|
|
75
|
+
if (
|
|
76
|
+
"anthropic-ratelimit-unified-5h-utilization" not in headers
|
|
77
|
+
and "anthropic-ratelimit-unified-7d-utilization" not in headers
|
|
78
|
+
):
|
|
79
|
+
message = _read_http_error_message(e)
|
|
80
|
+
suffix = f": {message}" if message else ""
|
|
81
|
+
raise CdxError(f"Claude usage unavailable (HTTP {e.code}{suffix})") from e
|
|
50
82
|
except urllib.error.URLError:
|
|
51
83
|
return None
|
|
52
84
|
|