cdx-manager 0.26.6 → 0.26.8
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 +82 -1
- package/VERSION +1 -1
- package/changelogs/CHANGELOGS_0_26_7.md +20 -0
- package/changelogs/CHANGELOGS_0_26_8.md +19 -0
- package/changelogs/CHANGELOGS_UNRELEASED.md +18 -0
- package/checksums/release-archives.json +22 -0
- package/docs/cli.md +73 -1
- package/package.json +1 -1
- package/pyproject.toml +4 -1
- package/src/claude_refresh.py +17 -4
- package/src/claude_status_line.py +139 -0
- package/src/claude_usage.py +10 -7
- package/src/cli.py +2 -2
- package/src/cli_args.py +16 -10
- package/src/cli_helpers.py +7 -2
- package/src/codex_usage.py +12 -4
- package/src/commands/backup.py +3 -0
- package/src/commands/conversation.py +10 -3
- package/src/commands/launch.py +7 -0
- package/src/commands/lifecycle.py +9 -1
- package/src/commands/runs.py +15 -14
- package/src/commands/settings.py +9 -0
- package/src/commands/status.py +8 -0
- package/src/conversation.py +41 -6
- package/src/process_conversation.py +81 -0
- package/src/quota_gauge.txt +2 -0
- package/src/quota_provenance.py +45 -0
- package/src/session_backup.py +4 -0
- package/src/session_service.py +27 -8
- package/src/session_status.py +26 -2
- package/src/status_source.py +7 -8
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Additive quota evidence; never infer a measurement from a missing value."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
|
|
5
|
+
SOURCES = {"codex_rate_limits", "claude_probe", "claude_statusline", "cache", "unknown"}
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def quota_windows(status, provider, reason=None, now=None):
|
|
9
|
+
status = status or {}
|
|
10
|
+
now = now or datetime.now(timezone.utc)
|
|
11
|
+
ttl = {"codex": 300, "claude": 600}.get(provider, 60)
|
|
12
|
+
source = {
|
|
13
|
+
"api:codex-app-server-rate-limits": "codex_rate_limits",
|
|
14
|
+
"api:anthropic-ratelimit-headers": "claude_probe",
|
|
15
|
+
"claude:statusline": "claude_statusline",
|
|
16
|
+
}.get(status.get("source_ref"), "cache")
|
|
17
|
+
windows = {}
|
|
18
|
+
for name, field in (("5h", "remaining_5h_pct"), ("weekly", "remaining_week_pct")):
|
|
19
|
+
evidence = (status.get("quota_windows") or {}).get(name, {})
|
|
20
|
+
value = status.get(field)
|
|
21
|
+
observed_at = evidence.get("observed_at", status.get("updated_at")) if value is not None else None
|
|
22
|
+
try:
|
|
23
|
+
observed = datetime.fromisoformat(str(observed_at).replace("Z", "+00:00"))
|
|
24
|
+
if observed.tzinfo is None:
|
|
25
|
+
observed = observed.replace(tzinfo=timezone.utc)
|
|
26
|
+
observed_at = observed.isoformat()
|
|
27
|
+
except (TypeError, ValueError):
|
|
28
|
+
observed, observed_at = None, None
|
|
29
|
+
window_reason = reason or ("missing_window" if value is None else None)
|
|
30
|
+
availability = "unavailable" if window_reason else "measured"
|
|
31
|
+
if not window_reason and (observed is None or (now - observed).total_seconds() >= ttl):
|
|
32
|
+
availability = "stale"
|
|
33
|
+
window_source = evidence.get("source", source) if value is not None else "unknown"
|
|
34
|
+
windows[name] = {
|
|
35
|
+
"source": window_source if window_source in SOURCES else "unknown",
|
|
36
|
+
"observed_at": observed_at,
|
|
37
|
+
"availability": availability,
|
|
38
|
+
"reason": window_reason,
|
|
39
|
+
}
|
|
40
|
+
return windows
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def mark_quota_unavailable(row, reason):
|
|
44
|
+
for window in row.get("quota_windows", {}).values():
|
|
45
|
+
window.update(availability="unavailable", reason=reason)
|
package/src/session_backup.py
CHANGED
|
@@ -441,7 +441,11 @@ def import_bundle(base_dir, store, file_path,
|
|
|
441
441
|
if recovery_dir:
|
|
442
442
|
remove_tree(recovery_dir, ignore_errors=True)
|
|
443
443
|
|
|
444
|
+
from .claude_status_line import provision
|
|
445
|
+
|
|
446
|
+
status_line_warnings = [notice for name in names if (notice := provision(store["get_session"](name), base_dir))]
|
|
444
447
|
return {
|
|
448
|
+
"status_line_warnings": status_line_warnings,
|
|
445
449
|
"path": file_path,
|
|
446
450
|
"session_names": names,
|
|
447
451
|
"include_auth": bool(decoded["meta"].get("include_auth")),
|
package/src/session_service.py
CHANGED
|
@@ -149,7 +149,7 @@ def _seed_codex_auth_from_global(auth_home, env=None):
|
|
|
149
149
|
return True
|
|
150
150
|
|
|
151
151
|
|
|
152
|
-
_ON_OFF_SETTINGS = {"fast": "fast", "rtk": "rtk", "logics": "logics", "notify": "notify", "agent_state": "agent state", "notify_preview": "notify preview", "scrub_env": "scrub env", "shared_memory": "shared memory", "retro": "retro"}
|
|
152
|
+
_ON_OFF_SETTINGS = {"fast": "fast", "rtk": "rtk", "logics": "logics", "notify": "notify", "agent_state": "agent state", "status_line": "status line", "notify_preview": "notify preview", "scrub_env": "scrub env", "shared_memory": "shared memory", "retro": "retro"}
|
|
153
153
|
|
|
154
154
|
|
|
155
155
|
def _has_control_chars(text):
|
|
@@ -284,7 +284,7 @@ def _normalize_session_label(label):
|
|
|
284
284
|
return text
|
|
285
285
|
|
|
286
286
|
|
|
287
|
-
def set_launch_settings(store, name, settings):
|
|
287
|
+
def set_launch_settings(store, name, settings, base_dir=None):
|
|
288
288
|
session = store["get_session"](name)
|
|
289
289
|
if not session:
|
|
290
290
|
raise CdxError(f"Unknown session: {name}")
|
|
@@ -317,16 +317,21 @@ def set_launch_settings(store, name, settings):
|
|
|
317
317
|
"updatedAt": now,
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
-
|
|
320
|
+
updated = store["update_session"](name, updater)
|
|
321
|
+
if base_dir is not None:
|
|
322
|
+
from .claude_status_line import provision
|
|
323
|
+
|
|
324
|
+
provision(updated, base_dir)
|
|
325
|
+
return updated
|
|
321
326
|
|
|
322
|
-
def unset_launch_settings(store, name, keys):
|
|
327
|
+
def unset_launch_settings(store, name, keys, base_dir=None):
|
|
323
328
|
session = store["get_session"](name)
|
|
324
329
|
if not session:
|
|
325
330
|
raise CdxError(f"Unknown session: {name}")
|
|
326
331
|
if not keys:
|
|
327
332
|
raise CdxError("At least one launch setting is required.")
|
|
328
333
|
allowed = {
|
|
329
|
-
"power", "reasoning_effort", "reasoningEffort", "permission", "fast", "rtk", "logics", "notify", "agent_state", "notify_preview",
|
|
334
|
+
"power", "reasoning_effort", "reasoningEffort", "permission", "fast", "rtk", "logics", "notify", "agent_state", "status_line", "notify_preview",
|
|
330
335
|
"scrub_env", "shared_memory", "retro", "model", "fallback_model", "budget", "extra_args", "priority", "policy", "max_parallel",
|
|
331
336
|
}
|
|
332
337
|
unknown = [key for key in keys if key not in allowed]
|
|
@@ -345,7 +350,12 @@ def unset_launch_settings(store, name, keys):
|
|
|
345
350
|
updated.pop("launch", None)
|
|
346
351
|
return updated
|
|
347
352
|
|
|
348
|
-
|
|
353
|
+
updated = store["update_session"](name, updater)
|
|
354
|
+
if base_dir is not None:
|
|
355
|
+
from .claude_status_line import provision
|
|
356
|
+
|
|
357
|
+
provision(updated, base_dir)
|
|
358
|
+
return updated
|
|
349
359
|
|
|
350
360
|
def update_auth_state(store, name, updater):
|
|
351
361
|
now = _local_now_iso()
|
|
@@ -555,6 +565,9 @@ def copy_session(base_dir, store, source_name, dest_name):
|
|
|
555
565
|
remove_tree(backup_root, ignore_errors=True)
|
|
556
566
|
finally:
|
|
557
567
|
remove_tree(temp_parent, ignore_errors=True)
|
|
568
|
+
from .claude_status_line import provision
|
|
569
|
+
|
|
570
|
+
provision(result["session"], base_dir)
|
|
558
571
|
return {"session": result["session"], "overwritten": overwritten}
|
|
559
572
|
|
|
560
573
|
|
|
@@ -604,6 +617,9 @@ def rename_session(base_dir, store, source_name, dest_name):
|
|
|
604
617
|
delete_keychain_credentials(source_auth_home)
|
|
605
618
|
except CdxError:
|
|
606
619
|
raise CdxError(f"Renamed session to {dest_name}, but old keychain credential cleanup failed; the new session is usable and the old entry was retained.") from None
|
|
620
|
+
from .claude_status_line import provision
|
|
621
|
+
|
|
622
|
+
provision(result["session"], base_dir)
|
|
607
623
|
return result["session"]
|
|
608
624
|
|
|
609
625
|
|
|
@@ -647,6 +663,9 @@ def create_session(base_dir, env, store, name, provider=DEFAULT_PROVIDER):
|
|
|
647
663
|
_seed_codex_auth_from_global(auth_home, env=env)
|
|
648
664
|
if normalized_provider == PROVIDER_CLAUDE:
|
|
649
665
|
_ensure_claude_attribution_disabled(auth_home)
|
|
666
|
+
from .claude_status_line import provision
|
|
667
|
+
|
|
668
|
+
provision({"provider": normalized_provider, "authHome": auth_home, "name": name}, base_dir)
|
|
650
669
|
if normalized_provider == "antigravity":
|
|
651
670
|
from .antigravity_status import install
|
|
652
671
|
|
|
@@ -1034,8 +1053,8 @@ def create_session_service(options=None):
|
|
|
1034
1053
|
"set_session_enabled": partial(set_session_enabled, store),
|
|
1035
1054
|
"set_session_label": partial(set_session_label, store),
|
|
1036
1055
|
"clear_session_label": partial(clear_session_label, store),
|
|
1037
|
-
"set_launch_settings": partial(set_launch_settings, store),
|
|
1038
|
-
"unset_launch_settings": partial(unset_launch_settings, store),
|
|
1056
|
+
"set_launch_settings": partial(set_launch_settings, store, base_dir=base_dir),
|
|
1057
|
+
"unset_launch_settings": partial(unset_launch_settings, store, base_dir=base_dir),
|
|
1039
1058
|
"record_status": partial(record_status, store),
|
|
1040
1059
|
"record_launch_history": partial(record_launch_history, store),
|
|
1041
1060
|
"get_launch_history": partial(get_launch_history, store),
|
package/src/session_status.py
CHANGED
|
@@ -13,6 +13,7 @@ from .claude_credentials import claude_auth_type, setup_token_days_left
|
|
|
13
13
|
from .claude_usage import _decode_jwt_claims
|
|
14
14
|
from .config import PROVIDER_CLAUDE, PROVIDER_CODEX, get_cdx_home
|
|
15
15
|
from .errors import CdxError
|
|
16
|
+
from .quota_provenance import quota_windows
|
|
16
17
|
from .session_helpers import (
|
|
17
18
|
_get_global_codex_home,
|
|
18
19
|
_get_session_auth_home,
|
|
@@ -158,6 +159,7 @@ def _normalize_status_payload(payload=None):
|
|
|
158
159
|
"raw_status_text": payload.get("raw_status_text"),
|
|
159
160
|
"source_ref": payload.get("source_ref"),
|
|
160
161
|
"structured": bool(payload.get("structured")),
|
|
162
|
+
"quota_windows": payload.get("quota_windows"),
|
|
161
163
|
}
|
|
162
164
|
|
|
163
165
|
def _parse_status_timestamp(value):
|
|
@@ -240,6 +242,11 @@ def _merge_status_payload(current, candidate):
|
|
|
240
242
|
return current
|
|
241
243
|
|
|
242
244
|
merged = dict(current)
|
|
245
|
+
merged["quota_windows"] = dict(current.get("quota_windows") or quota_windows(current, "unknown"))
|
|
246
|
+
candidate_windows = candidate.get("quota_windows") or quota_windows(candidate, "unknown")
|
|
247
|
+
for name, field in (("5h", "remaining_5h_pct"), ("weekly", "remaining_week_pct")):
|
|
248
|
+
if current.get(field) is None and candidate.get(field) is not None:
|
|
249
|
+
merged["quota_windows"][name] = candidate_windows[name]
|
|
243
250
|
for field in [
|
|
244
251
|
"usage_pct",
|
|
245
252
|
"remaining_5h_pct",
|
|
@@ -336,7 +343,7 @@ def _fetch_codex_status(codex_status_fetcher, custom_codex_status_fetcher, defau
|
|
|
336
343
|
if custom_codex_status_fetcher:
|
|
337
344
|
return custom_codex_status_fetcher(session)
|
|
338
345
|
timeout = timeout_seconds or default_status_timeout_seconds
|
|
339
|
-
return codex_status_fetcher(session, timeout=timeout)
|
|
346
|
+
return codex_status_fetcher(session, timeout=timeout, diagnostic=True)
|
|
340
347
|
|
|
341
348
|
def session_paths(session):
|
|
342
349
|
"""Where a session's files live, so a caller never rebuilds cdx's layout.
|
|
@@ -415,6 +422,12 @@ def _status_row_from_session(store, s):
|
|
|
415
422
|
)})
|
|
416
423
|
if enabled and antigravity["quota_stale"]:
|
|
417
424
|
row["status"] = "stale"
|
|
425
|
+
reason = s.get("_quota_error")
|
|
426
|
+
if not enabled:
|
|
427
|
+
reason = "profile_disabled"
|
|
428
|
+
elif row["auth_status"] == "logged_out":
|
|
429
|
+
reason = "login_required"
|
|
430
|
+
row["quota_windows"] = quota_windows(row_status, s["provider"], reason=reason)
|
|
418
431
|
row["runs_out_at_iso"] = _runs_out(row)
|
|
419
432
|
return row
|
|
420
433
|
|
|
@@ -465,9 +478,13 @@ def _resolve_session_status(store, base_dir, env, fetch_codex_status, session,
|
|
|
465
478
|
{**session, "authHome": source_root},
|
|
466
479
|
timeout_seconds=status_timeout_seconds,
|
|
467
480
|
)
|
|
468
|
-
if live_status:
|
|
481
|
+
if live_status and live_status.get("_quota_error"):
|
|
482
|
+
session["_quota_error"] = live_status["_quota_error"]
|
|
483
|
+
elif live_status:
|
|
469
484
|
record_status(store, session["name"], live_status)
|
|
470
485
|
return live_status
|
|
486
|
+
else:
|
|
487
|
+
session["_quota_error"] = "quota_read_failed"
|
|
471
488
|
|
|
472
489
|
expected_account_email = (
|
|
473
490
|
_read_expected_account_email(source_root)
|
|
@@ -494,6 +511,12 @@ def _resolve_session_status(store, base_dir, env, fetch_codex_status, session,
|
|
|
494
511
|
# structured payloads there cannot be attributed to ours.
|
|
495
512
|
trust_unattributed_structured=False,
|
|
496
513
|
)
|
|
514
|
+
if artifact and (artifact.get("structured") or not expected_account_email):
|
|
515
|
+
# Shared artifacts without verified identity cannot date this profile.
|
|
516
|
+
artifact["quota_windows"] = {
|
|
517
|
+
name: {"source": "cache", "observed_at": None}
|
|
518
|
+
for name in ("5h", "weekly")
|
|
519
|
+
}
|
|
497
520
|
if not artifact:
|
|
498
521
|
if _is_low_confidence_status_source(current_status):
|
|
499
522
|
return None
|
|
@@ -511,6 +534,7 @@ def _resolve_session_status(store, base_dir, env, fetch_codex_status, session,
|
|
|
511
534
|
"raw_status_text": artifact.get("raw_status_text"),
|
|
512
535
|
"source_ref": artifact.get("source_ref"),
|
|
513
536
|
"structured": artifact.get("structured"),
|
|
537
|
+
"quota_windows": artifact.get("quota_windows"),
|
|
514
538
|
})
|
|
515
539
|
if _is_low_confidence_status_source(current_status) and not _is_low_confidence_status_source(resolved):
|
|
516
540
|
record_status(store, session["name"], resolved)
|
package/src/status_source.py
CHANGED
|
@@ -4,6 +4,7 @@ import re
|
|
|
4
4
|
from datetime import datetime, timezone
|
|
5
5
|
|
|
6
6
|
from .config import PROVIDER_CLAUDE, PROVIDER_CODEX
|
|
7
|
+
from .quota_provenance import quota_windows
|
|
7
8
|
|
|
8
9
|
_ANSI_ESCAPE = re.compile(r"\x1b\[[0-9;]*m")
|
|
9
10
|
_ANSI_TERMINAL_CONTROL = re.compile(r"\x1b\[[0-9;?]*[ -/]*[@-~]")
|
|
@@ -513,11 +514,6 @@ def extract_named_statuses_from_text(text):
|
|
|
513
514
|
if "remaining_week_pct" not in result and week_used is not None:
|
|
514
515
|
result["remaining_week_pct"] = max(0, 100 - week_used)
|
|
515
516
|
|
|
516
|
-
if re.search(r"No stats available yet\.?\s+Start using Claude Code", normalized, re.I):
|
|
517
|
-
result.setdefault("usage_pct", 0)
|
|
518
|
-
result.setdefault("remaining_5h_pct", 100)
|
|
519
|
-
result.setdefault("remaining_week_pct", 100)
|
|
520
|
-
|
|
521
517
|
# Table header row
|
|
522
518
|
header_idx = next(
|
|
523
519
|
(i for i, l in enumerate(lines)
|
|
@@ -722,6 +718,9 @@ def _backfill_missing_fields(target, source):
|
|
|
722
718
|
continue
|
|
723
719
|
if target.get(field) is None and source.get(field) is not None:
|
|
724
720
|
target[field] = source[field]
|
|
721
|
+
name = {"remaining_5h_pct": "5h", "remaining_week_pct": "weekly"}.get(field)
|
|
722
|
+
if name:
|
|
723
|
+
target.setdefault("quota_windows", {})[name] = (source.get("quota_windows") or quota_windows(source, "unknown"))[name]
|
|
725
724
|
|
|
726
725
|
|
|
727
726
|
def _record_timestamp_epoch(value):
|
|
@@ -792,9 +791,9 @@ def find_latest_status_artifact(
|
|
|
792
791
|
score = stat.st_mtime
|
|
793
792
|
else:
|
|
794
793
|
score = 0
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
#
|
|
794
|
+
parsed["updated_at"] = datetime.fromtimestamp(score, tz=timezone.utc).isoformat() if score else None
|
|
795
|
+
parsed["quota_windows"] = quota_windows(parsed, provider)
|
|
796
|
+
# Trust comes from the record producer, not conversational text.
|
|
798
797
|
trusted = bool(candidate.get("trusted"))
|
|
799
798
|
# Structured payloads carry no account identity, so the email guard
|
|
800
799
|
# above cannot filter them. In roots shared by several accounts the
|