cdx-manager 0.9.13 → 0.9.15
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/bin/python-runner.js +4 -7
- package/changelogs/CHANGELOGS_0_9_14.md +27 -0
- package/changelogs/CHANGELOGS_0_9_15.md +39 -0
- package/checksums/release-archives.json +8 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/backup_bundle.py +2 -0
- package/src/claude_refresh.py +9 -2
- package/src/claude_usage.py +36 -5
- package/src/cli.py +7 -6
- package/src/cli_args.py +23 -7
- package/src/cli_commands.py +37 -23
- package/src/cli_render.py +1 -1
- package/src/codex_usage.py +3 -1
- package/src/context_store.py +3 -4
- package/src/fs_utils.py +32 -0
- package/src/provider_runtime.py +45 -13
- package/src/run_registry.py +47 -14
- package/src/run_usage.py +3 -1
- package/src/session_service.py +137 -100
- package/src/session_store.py +11 -0
- package/src/status_source.py +101 -25
- package/src/status_view.py +19 -6
- package/src/update_check.py +4 -0
- package/src/update_manager.py +3 -1
package/src/status_source.py
CHANGED
|
@@ -19,7 +19,7 @@ _KEY_VALUE_PATTERNS = [
|
|
|
19
19
|
("usage_pct", re.compile(r"usage_pct\s*[:=]\s*(\d{1,3})%?", re.I)),
|
|
20
20
|
("remaining_5h_pct", re.compile(r"remaining_?5h_pct\s*[:=]\s*(\d{1,3})%?", re.I)),
|
|
21
21
|
("remaining_week_pct", re.compile(r"remaining_?week_pct\s*[:=]\s*(\d{1,3})%?", re.I)),
|
|
22
|
-
("credits", re.compile(r"credits?\s*[:=]\s*(
|
|
22
|
+
("credits", re.compile(r"credits?\s*[:=]\s*(\d[\d, ]*(?:\.\d+)?)\s*(?:credits?)?", re.I)),
|
|
23
23
|
("remaining_5h_pct", re.compile(r"5h\s+limit\s*:\s*\[[^\]]*\]\s*(\d{1,3})%\s*left", re.I)),
|
|
24
24
|
("remaining_week_pct", re.compile(r"weekly\s+limit\s*:\s*\[[^\]]*\]\s*(\d{1,3})%\s*left", re.I)),
|
|
25
25
|
("remaining_5h_pct", re.compile(r"5h\s+limit\s*:\s*\[[^\]]*\]\s*(\d{1,3})(?:%|\b)", re.I)),
|
|
@@ -94,6 +94,15 @@ def _coerce_percentage(value):
|
|
|
94
94
|
return None
|
|
95
95
|
|
|
96
96
|
|
|
97
|
+
def _is_zero_credit_balance(value):
|
|
98
|
+
if value in (None, ""):
|
|
99
|
+
return True
|
|
100
|
+
try:
|
|
101
|
+
return float(str(value).replace(",", "").strip()) == 0
|
|
102
|
+
except (TypeError, ValueError):
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
|
|
97
106
|
def _extract_structured_rate_limits(record):
|
|
98
107
|
if not isinstance(record, dict):
|
|
99
108
|
return None
|
|
@@ -123,7 +132,11 @@ def _extract_structured_rate_limits(record):
|
|
|
123
132
|
}
|
|
124
133
|
|
|
125
134
|
balance = credits.get("balance")
|
|
126
|
-
if balance not in (None, "")
|
|
135
|
+
if balance not in (None, "") and not (
|
|
136
|
+
_is_zero_credit_balance(balance)
|
|
137
|
+
and not credits.get("hasCredits")
|
|
138
|
+
and not credits.get("unlimited")
|
|
139
|
+
):
|
|
127
140
|
result["credits"] = str(balance).strip()
|
|
128
141
|
|
|
129
142
|
result["reset_at"] = result["reset_week_at"] or result["reset_5h_at"]
|
|
@@ -277,6 +290,8 @@ def _extract_jsonl_texts(file_path, provider=None):
|
|
|
277
290
|
"timestamp": record.get("timestamp"),
|
|
278
291
|
"text": structured["raw_status_text"],
|
|
279
292
|
"structured": structured,
|
|
293
|
+
# Exact API data, as trustworthy as a /status screen.
|
|
294
|
+
"trusted": True,
|
|
280
295
|
})
|
|
281
296
|
payload_texts = _collect_text_values(record.get("payload") or {})
|
|
282
297
|
for candidate in payload_texts:
|
|
@@ -296,7 +311,11 @@ def _extract_log_block(file_path, provider=None):
|
|
|
296
311
|
text = _safe_read_text(file_path)
|
|
297
312
|
if not text:
|
|
298
313
|
return []
|
|
299
|
-
|
|
314
|
+
items = _extract_status_blocks_from_text(text, provider=provider, source_ref=file_path, timestamp=None)
|
|
315
|
+
for item in items:
|
|
316
|
+
# A /status screen captured in a terminal log is authoritative.
|
|
317
|
+
item["trusted"] = True
|
|
318
|
+
return items
|
|
300
319
|
|
|
301
320
|
|
|
302
321
|
def _parse_month_index(name):
|
|
@@ -422,7 +441,14 @@ def extract_named_statuses_from_text(text):
|
|
|
422
441
|
for field, pattern in _KEY_VALUE_PATTERNS:
|
|
423
442
|
if field not in result:
|
|
424
443
|
m = pattern.search(normalized)
|
|
425
|
-
if m:
|
|
444
|
+
if not m:
|
|
445
|
+
continue
|
|
446
|
+
if field == "credits":
|
|
447
|
+
# Keep the decimal part; a zero balance is no credit at all.
|
|
448
|
+
value = m[1].replace(",", "").replace(" ", "")
|
|
449
|
+
if not _is_zero_credit_balance(value):
|
|
450
|
+
result[field] = value
|
|
451
|
+
else:
|
|
426
452
|
result[field] = int(re.sub(r"\D", "", m[1]))
|
|
427
453
|
|
|
428
454
|
# Claude "Current session / Current week" block
|
|
@@ -596,12 +622,49 @@ def _sort_recent(paths):
|
|
|
596
622
|
}
|
|
597
623
|
return sorted(
|
|
598
624
|
candidate_stats,
|
|
599
|
-
key=lambda fp: candidate_stats[fp].st_mtime,
|
|
625
|
+
key=lambda fp: (candidate_stats[fp].st_mtime, fp),
|
|
600
626
|
reverse=True,
|
|
601
627
|
)
|
|
602
628
|
|
|
603
629
|
|
|
604
|
-
|
|
630
|
+
_STATUS_VALUE_FIELDS = (
|
|
631
|
+
"usage_pct",
|
|
632
|
+
"remaining_5h_pct",
|
|
633
|
+
"remaining_week_pct",
|
|
634
|
+
"credits",
|
|
635
|
+
"reset_5h_at",
|
|
636
|
+
"reset_week_at",
|
|
637
|
+
"reset_at",
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def _backfill_missing_fields(target, source):
|
|
642
|
+
for field in _STATUS_VALUE_FIELDS:
|
|
643
|
+
if target.get(field) is None and source.get(field) is not None:
|
|
644
|
+
target[field] = source[field]
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
def _record_timestamp_epoch(value):
|
|
648
|
+
"""Best-effort epoch seconds from a record timestamp (ISO string, epoch s or ms)."""
|
|
649
|
+
if value in (None, ""):
|
|
650
|
+
return None
|
|
651
|
+
try:
|
|
652
|
+
number = float(value)
|
|
653
|
+
except (TypeError, ValueError):
|
|
654
|
+
try:
|
|
655
|
+
return datetime.fromisoformat(str(value).replace("Z", "+00:00")).timestamp()
|
|
656
|
+
except (TypeError, ValueError):
|
|
657
|
+
return None
|
|
658
|
+
# ponytail: >1e11 means epoch milliseconds (that's year 5138 in seconds)
|
|
659
|
+
return number / 1000 if number > 1e11 else number
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
def find_latest_status_artifact(
|
|
663
|
+
root_dir,
|
|
664
|
+
provider=None,
|
|
665
|
+
expected_account_email=None,
|
|
666
|
+
trust_unattributed_structured=True,
|
|
667
|
+
):
|
|
605
668
|
priority_candidates, history_candidates = _collect_candidate_files(root_dir)
|
|
606
669
|
candidates = (
|
|
607
670
|
_sort_recent(priority_candidates)
|
|
@@ -634,35 +697,48 @@ def find_latest_status_artifact(root_dir, provider=None, expected_account_email=
|
|
|
634
697
|
}
|
|
635
698
|
if not parsed:
|
|
636
699
|
continue
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
score = 0
|
|
700
|
+
# Score on the record's own time so sources stay comparable: falling
|
|
701
|
+
# back to file mtime would let a stale block in an actively-appended
|
|
702
|
+
# jsonl outrank a genuinely fresher terminal log.
|
|
703
|
+
epoch = _record_timestamp_epoch(candidate.get("timestamp"))
|
|
642
704
|
src_file = re.sub(r":\d+$", "", candidate["source_ref"])
|
|
643
705
|
stat = _safe_stat(src_file)
|
|
644
|
-
if
|
|
706
|
+
if epoch is not None:
|
|
707
|
+
score = epoch
|
|
708
|
+
elif stat:
|
|
645
709
|
score = stat.st_mtime
|
|
646
|
-
|
|
710
|
+
else:
|
|
711
|
+
score = 0
|
|
712
|
+
# Each record producer stamps "trusted" where it knows the source kind.
|
|
713
|
+
# Untrusted records are text scraped from jsonl payloads: they can be
|
|
714
|
+
# conversational noise quoting an old status block.
|
|
715
|
+
trusted = bool(candidate.get("trusted"))
|
|
716
|
+
# Structured payloads carry no account identity, so the email guard
|
|
717
|
+
# above cannot filter them. In roots shared by several accounts the
|
|
718
|
+
# caller demotes them below account-verified /status screens.
|
|
719
|
+
if trusted and candidate.get("structured") and not trust_unattributed_structured:
|
|
720
|
+
trusted = False
|
|
721
|
+
priority = 2 if trusted else 1
|
|
647
722
|
|
|
648
723
|
if best is None or (priority, score) >= (best["priority"], best["score"]):
|
|
724
|
+
previous = best
|
|
649
725
|
best = {
|
|
650
726
|
"priority": priority,
|
|
651
727
|
"score": score,
|
|
652
728
|
"source_ref": candidate["source_ref"],
|
|
729
|
+
"structured": bool(candidate.get("structured")),
|
|
653
730
|
**parsed,
|
|
654
731
|
}
|
|
655
|
-
if
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
if "
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
).astimezone().isoformat()
|
|
732
|
+
if score:
|
|
733
|
+
best["updated_at"] = datetime.fromtimestamp(
|
|
734
|
+
score, tz=timezone.utc
|
|
735
|
+
).astimezone().isoformat()
|
|
736
|
+
# A fresher-but-partial winner (e.g. rate_limits with only the
|
|
737
|
+
# primary window) must not erase fields an equally trusted
|
|
738
|
+
# candidate provided.
|
|
739
|
+
if previous and previous["priority"] == priority:
|
|
740
|
+
_backfill_missing_fields(best, previous)
|
|
741
|
+
elif priority == best["priority"]:
|
|
742
|
+
_backfill_missing_fields(best, parsed)
|
|
667
743
|
|
|
668
744
|
return best
|
package/src/status_view.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from datetime import datetime
|
|
1
|
+
from datetime import datetime, timedelta
|
|
2
2
|
from decimal import ROUND_HALF_UP, Decimal, InvalidOperation
|
|
3
3
|
|
|
4
4
|
from .cli_render import (
|
|
@@ -328,12 +328,25 @@ def _parse_reset_timestamp(value):
|
|
|
328
328
|
return parsed.timestamp()
|
|
329
329
|
except (TypeError, ValueError):
|
|
330
330
|
pass
|
|
331
|
-
try:
|
|
332
|
-
parsed = datetime.strptime(text, "%b %d %H:%M")
|
|
333
|
-
except (TypeError, ValueError):
|
|
334
|
-
return None
|
|
335
331
|
now = datetime.now().astimezone()
|
|
336
|
-
parsed =
|
|
332
|
+
parsed = None
|
|
333
|
+
# Parse with an explicit year: strptime's implicit 1900 rejects "Feb 29",
|
|
334
|
+
# and the current year may not hold the date at all around new year.
|
|
335
|
+
for year in (now.year, now.year + 1):
|
|
336
|
+
try:
|
|
337
|
+
parsed = datetime.strptime(f"{year} {text}", "%Y %b %d %H:%M").replace(tzinfo=now.tzinfo)
|
|
338
|
+
break
|
|
339
|
+
except (TypeError, ValueError):
|
|
340
|
+
continue
|
|
341
|
+
if parsed is None:
|
|
342
|
+
return None
|
|
343
|
+
# A year-less date nearly a year in the past is a year wrap ("Jan 2"
|
|
344
|
+
# rendered on Dec 31), not a reset that passed months ago.
|
|
345
|
+
if parsed < now - timedelta(days=182):
|
|
346
|
+
try:
|
|
347
|
+
parsed = parsed.replace(year=parsed.year + 1)
|
|
348
|
+
except ValueError:
|
|
349
|
+
pass
|
|
337
350
|
return parsed.timestamp()
|
|
338
351
|
|
|
339
352
|
|
package/src/update_check.py
CHANGED
|
@@ -236,6 +236,10 @@ def check_logics_manager_for_update(base_dir, env=None, now_fn=None, runner=None
|
|
|
236
236
|
return None
|
|
237
237
|
|
|
238
238
|
latest_version = fetch_latest_logics_manager_version(env=env)
|
|
239
|
+
if not latest_version:
|
|
240
|
+
# Don't cache a failed fetch: a None latest_version would hide a
|
|
241
|
+
# real update for the whole TTL after the registry recovers.
|
|
242
|
+
return None
|
|
239
243
|
payload = {
|
|
240
244
|
"checked_at": now_ts,
|
|
241
245
|
"latest_version": latest_version,
|
package/src/update_manager.py
CHANGED
|
@@ -204,7 +204,9 @@ def run_update_plan(plan, runner=None, env=None):
|
|
|
204
204
|
"stdout": _result_text(result, "stdout"),
|
|
205
205
|
"stderr": _result_text(result, "stderr"),
|
|
206
206
|
})
|
|
207
|
-
|
|
207
|
+
# A runner that reports no return code is a failure, not a success:
|
|
208
|
+
# treating None as ok would let a failed step continue the plan.
|
|
209
|
+
if code != 0:
|
|
208
210
|
break
|
|
209
211
|
return results
|
|
210
212
|
|