cctally 1.7.3 → 1.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/CHANGELOG.md +10 -0
- package/bin/_cctally_alerts.py +12 -5
- package/bin/_cctally_cache.py +12 -11
- package/bin/_cctally_config.py +34 -19
- package/bin/_cctally_core.py +890 -0
- package/bin/_cctally_dashboard.py +40 -110
- package/bin/_cctally_db.py +89 -20
- package/bin/_cctally_record.py +76 -75
- package/bin/_cctally_refresh.py +35 -20
- package/bin/_cctally_setup.py +26 -16
- package/bin/_cctally_sync_week.py +21 -6
- package/bin/_cctally_tui.py +28 -51
- package/bin/_cctally_update.py +11 -16
- package/bin/_lib_aggregators.py +7 -1
- package/bin/_lib_diff_kernel.py +19 -21
- package/bin/_lib_subscription_weeks.py +17 -9
- package/bin/cctally +49 -822
- package/package.json +2 -1
package/bin/_cctally_refresh.py
CHANGED
|
@@ -55,6 +55,23 @@ def _cctally():
|
|
|
55
55
|
return sys.modules["cctally"]
|
|
56
56
|
|
|
57
57
|
|
|
58
|
+
# === Honest imports from extracted homes ===================================
|
|
59
|
+
# Spec 2026-05-17-cctally-core-kernel-extraction.md §3.3: kernel symbols
|
|
60
|
+
# import from _cctally_core. `load_config` plus out-of-scope helpers
|
|
61
|
+
# (`_resolve_oauth_token`, `_fetch_oauth_usage`, `_bust_statusline_cache`,
|
|
62
|
+
# `_refresh_usage_inproc`, `_get_oauth_usage_config`,
|
|
63
|
+
# `_seconds_since_iso`, `_select_last_known_snapshot`,
|
|
64
|
+
# `_newest_snapshot_age_seconds`, `_normalize_percent`,
|
|
65
|
+
# `_forecast_color_enabled`, `_discover_cc_version`, `cmd_record_usage`)
|
|
66
|
+
# stay on the _cctally() accessor.
|
|
67
|
+
from _cctally_core import (
|
|
68
|
+
eprint,
|
|
69
|
+
now_utc_iso,
|
|
70
|
+
_iso_to_epoch,
|
|
71
|
+
_format_short_duration,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
58
75
|
# =========================================================================
|
|
59
76
|
# Exception classes
|
|
60
77
|
# =========================================================================
|
|
@@ -289,14 +306,13 @@ def _render_refresh_usage_text(payload: dict, color: bool, now_epoch: int) -> st
|
|
|
289
306
|
The ``5h`` segment is omitted entirely when ``payload["five_hour"]`` is None.
|
|
290
307
|
Color codes are emitted only when ``color`` is True.
|
|
291
308
|
"""
|
|
292
|
-
c = _cctally()
|
|
293
309
|
a = _REFRESH_USAGE_ANSI if color else {k: "" for k in _REFRESH_USAGE_ANSI}
|
|
294
310
|
|
|
295
311
|
seven = payload["seven_day"]
|
|
296
312
|
seven_pct = seven["used_percent"]
|
|
297
313
|
seven_resets = seven.get("resets_at_epoch")
|
|
298
314
|
if seven_resets is not None:
|
|
299
|
-
seven_ttl =
|
|
315
|
+
seven_ttl = _format_short_duration(seven_resets - now_epoch)
|
|
300
316
|
seven_seg = (
|
|
301
317
|
f"{a['yellow']}7d {seven_pct:.0f}%{a['reset']}"
|
|
302
318
|
f" {a['orange']}(in {seven_ttl}){a['reset']}"
|
|
@@ -309,7 +325,7 @@ def _render_refresh_usage_text(payload: dict, color: bool, now_epoch: int) -> st
|
|
|
309
325
|
five_pct = five["used_percent"]
|
|
310
326
|
five_resets = five.get("resets_at_epoch")
|
|
311
327
|
if five_resets is not None:
|
|
312
|
-
five_ttl =
|
|
328
|
+
five_ttl = _format_short_duration(five_resets - now_epoch)
|
|
313
329
|
five_seg = (
|
|
314
330
|
f" | {a['yellow']}5h {five_pct:.0f}%{a['reset']}"
|
|
315
331
|
f" {a['orange']}(in {five_ttl}){a['reset']}"
|
|
@@ -420,14 +436,13 @@ def _bust_statusline_cache(path: str = _STATUSLINE_OAUTH_CACHE) -> str:
|
|
|
420
436
|
``"absent"`` (file did not exist), ``"error"`` (delete failed for
|
|
421
437
|
a non-FileNotFoundError reason — logged via eprint, does NOT raise).
|
|
422
438
|
"""
|
|
423
|
-
c = _cctally()
|
|
424
439
|
try:
|
|
425
440
|
os.remove(path)
|
|
426
441
|
return "busted"
|
|
427
442
|
except FileNotFoundError:
|
|
428
443
|
return "absent"
|
|
429
444
|
except OSError as exc:
|
|
430
|
-
|
|
445
|
+
eprint(f"refresh-usage: cache-bust failed: {exc}")
|
|
431
446
|
return "error"
|
|
432
447
|
|
|
433
448
|
|
|
@@ -450,7 +465,7 @@ def _cmd_refresh_usage_handle_rate_limit(args: argparse.Namespace, exc) -> int:
|
|
|
450
465
|
quiet = bool(getattr(args, "quiet", False))
|
|
451
466
|
|
|
452
467
|
if snap is None:
|
|
453
|
-
|
|
468
|
+
eprint("refresh-usage: rate-limited; no last-known data; "
|
|
454
469
|
"status-line will populate on next CC tick")
|
|
455
470
|
if json_mode:
|
|
456
471
|
print(json.dumps({
|
|
@@ -467,7 +482,7 @@ def _cmd_refresh_usage_handle_rate_limit(args: argparse.Namespace, exc) -> int:
|
|
|
467
482
|
_freshness_label(age_s, cfg) if age_s is not None else "stale"
|
|
468
483
|
)
|
|
469
484
|
|
|
470
|
-
|
|
485
|
+
eprint(f"refresh-usage: rate-limited; using last-known "
|
|
471
486
|
f"(captured {int(age_s) if age_s is not None else '?'}s ago)")
|
|
472
487
|
|
|
473
488
|
if json_mode:
|
|
@@ -550,7 +565,7 @@ def _refresh_usage_inproc(timeout_seconds: float = 5.0) -> _RefreshUsageResult:
|
|
|
550
565
|
# built then a downstream cmd_record_usage call fails).
|
|
551
566
|
seven_pct = c._normalize_percent(float(seven["utilization"]))
|
|
552
567
|
seven_resets_iso = seven["resets_at"]
|
|
553
|
-
seven_resets_epoch =
|
|
568
|
+
seven_resets_epoch = _iso_to_epoch(seven_resets_iso)
|
|
554
569
|
except (TypeError, ValueError, KeyError) as exc:
|
|
555
570
|
return _RefreshUsageResult(
|
|
556
571
|
status="parse_failed",
|
|
@@ -566,7 +581,7 @@ def _refresh_usage_inproc(timeout_seconds: float = 5.0) -> _RefreshUsageResult:
|
|
|
566
581
|
try:
|
|
567
582
|
five_pct = c._normalize_percent(float(five["utilization"]))
|
|
568
583
|
five_resets_iso = five["resets_at"]
|
|
569
|
-
five_resets_epoch =
|
|
584
|
+
five_resets_epoch = _iso_to_epoch(five_resets_iso)
|
|
570
585
|
except (TypeError, ValueError) as exc:
|
|
571
586
|
# 5h is optional - silently degrade rather than fail the command
|
|
572
587
|
# (parity with the previous cmd_refresh_usage behavior; the eprint
|
|
@@ -599,7 +614,7 @@ def _refresh_usage_inproc(timeout_seconds: float = 5.0) -> _RefreshUsageResult:
|
|
|
599
614
|
# (tests/test_refresh_usage_cmd.py:55, test_refresh_usage_inproc.py:18).
|
|
600
615
|
cache_state = c._bust_statusline_cache()
|
|
601
616
|
|
|
602
|
-
fetched_at =
|
|
617
|
+
fetched_at = now_utc_iso()
|
|
603
618
|
fresh_envelope = {
|
|
604
619
|
"label": "fresh",
|
|
605
620
|
"captured_at": fetched_at,
|
|
@@ -652,7 +667,7 @@ def cmd_refresh_usage(args: argparse.Namespace) -> int:
|
|
|
652
667
|
# Emit BEFORE rendering so stderr flushes consistently for harnesses
|
|
653
668
|
# that grep across both streams.
|
|
654
669
|
for warning in result.warnings:
|
|
655
|
-
|
|
670
|
+
eprint(f"refresh-usage: {warning}")
|
|
656
671
|
payload = result.payload or {}
|
|
657
672
|
if getattr(args, "json", False):
|
|
658
673
|
print(_serialize_refresh_usage_json(payload))
|
|
@@ -668,7 +683,7 @@ def cmd_refresh_usage(args: argparse.Namespace) -> int:
|
|
|
668
683
|
args, RefreshUsageRateLimitError(result.reason or "rate limited"))
|
|
669
684
|
|
|
670
685
|
if result.status == "no_oauth_token":
|
|
671
|
-
|
|
686
|
+
eprint("refresh-usage: no OAuth token found "
|
|
672
687
|
"(run 'claude' once to authenticate)")
|
|
673
688
|
return 2
|
|
674
689
|
|
|
@@ -678,9 +693,9 @@ def cmd_refresh_usage(args: argparse.Namespace) -> int:
|
|
|
678
693
|
# config-error reason string verbatim so we can detect it here.
|
|
679
694
|
reason = result.reason or ""
|
|
680
695
|
if reason.startswith("invalid oauth_usage config:"):
|
|
681
|
-
|
|
696
|
+
eprint(f"cctally: {reason}")
|
|
682
697
|
return 2
|
|
683
|
-
|
|
698
|
+
eprint(f"refresh-usage: OAuth fetch failed: {reason}")
|
|
684
699
|
return 3
|
|
685
700
|
|
|
686
701
|
if result.status == "parse_failed":
|
|
@@ -688,7 +703,7 @@ def cmd_refresh_usage(args: argparse.Namespace) -> int:
|
|
|
688
703
|
# only seven_day failures (RefreshUsageMalformedError or unparseable
|
|
689
704
|
# field extraction) propagate here. Preserve the original exact
|
|
690
705
|
# error-line shape for bash harnesses that grep stderr.
|
|
691
|
-
|
|
706
|
+
eprint(f"refresh-usage: {result.reason or ''}")
|
|
692
707
|
return 4
|
|
693
708
|
|
|
694
709
|
if result.status == "record_failed":
|
|
@@ -698,13 +713,13 @@ def cmd_refresh_usage(args: argparse.Namespace) -> int:
|
|
|
698
713
|
rc = int(reason.split()[1])
|
|
699
714
|
except (IndexError, ValueError):
|
|
700
715
|
rc = -1
|
|
701
|
-
|
|
716
|
+
eprint(f"refresh-usage: failed to record usage (exit {rc})")
|
|
702
717
|
else:
|
|
703
|
-
|
|
718
|
+
eprint(f"refresh-usage: failed to record usage: {reason}")
|
|
704
719
|
return 5
|
|
705
720
|
|
|
706
721
|
# Defensive: unknown status from _refresh_usage_inproc -> treat as parse error.
|
|
707
|
-
|
|
722
|
+
eprint(f"refresh-usage: unexpected status {result.status!r}")
|
|
708
723
|
return 4
|
|
709
724
|
|
|
710
725
|
|
|
@@ -757,7 +772,7 @@ def _hook_tick_oauth_refresh(
|
|
|
757
772
|
seven = api["seven_day"]
|
|
758
773
|
try:
|
|
759
774
|
seven_pct = c._normalize_percent(float(seven["utilization"]))
|
|
760
|
-
seven_resets_epoch =
|
|
775
|
+
seven_resets_epoch = _iso_to_epoch(seven["resets_at"])
|
|
761
776
|
except (TypeError, ValueError, KeyError):
|
|
762
777
|
return "err(parse)", None
|
|
763
778
|
five = api.get("five_hour") if isinstance(api.get("five_hour"), dict) else None
|
|
@@ -766,7 +781,7 @@ def _hook_tick_oauth_refresh(
|
|
|
766
781
|
if five is not None and "utilization" in five and "resets_at" in five:
|
|
767
782
|
try:
|
|
768
783
|
five_pct = c._normalize_percent(float(five["utilization"]))
|
|
769
|
-
five_resets_epoch =
|
|
784
|
+
five_resets_epoch = _iso_to_epoch(five["resets_at"])
|
|
770
785
|
except (TypeError, ValueError):
|
|
771
786
|
five_pct = None
|
|
772
787
|
five_resets_epoch = None
|
package/bin/_cctally_setup.py
CHANGED
|
@@ -63,6 +63,18 @@ def _cctally():
|
|
|
63
63
|
return sys.modules["cctally"]
|
|
64
64
|
|
|
65
65
|
|
|
66
|
+
# === Honest imports from extracted homes ===================================
|
|
67
|
+
# Spec 2026-05-17-cctally-core-kernel-extraction.md §3.3: kernel symbols
|
|
68
|
+
# import from _cctally_core. Path constants (`APP_DIR`,
|
|
69
|
+
# `CLAUDE_SETTINGS_PATH`, `HOOK_TICK_LOG_PATH`) plus the extensive
|
|
70
|
+
# out-of-scope setup-specific helpers (legacy migration, hook surgery,
|
|
71
|
+
# OAuth token, sync_cache, …) stay on the _cctally() accessor.
|
|
72
|
+
from _cctally_core import (
|
|
73
|
+
eprint,
|
|
74
|
+
_command_as_of,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
66
78
|
# ── settings.json hook surgery ─────────────────────────────────────────
|
|
67
79
|
|
|
68
80
|
|
|
@@ -430,7 +442,7 @@ def _legacy_resolve_backup_dir() -> pathlib.Path:
|
|
|
430
442
|
so a re-run never overwrites a prior migration's snapshot.
|
|
431
443
|
"""
|
|
432
444
|
c = _cctally()
|
|
433
|
-
now =
|
|
445
|
+
now = _command_as_of()
|
|
434
446
|
stamp = now.strftime("%Y%m%d-%H%M%S")
|
|
435
447
|
base = pathlib.Path.home() / ".claude" / f"{c._LEGACY_BACKUP_DIR_PREFIX}{stamp}"
|
|
436
448
|
base.mkdir(parents=True, exist_ok=True)
|
|
@@ -614,7 +626,6 @@ def _setup_read_legacy_prompt_input(stream, reprompt: str | None = None) -> bool
|
|
|
614
626
|
us). When None (test default), no reprompt is emitted — useful for unit
|
|
615
627
|
tests that drive `stream` from io.StringIO.
|
|
616
628
|
"""
|
|
617
|
-
eprint = _cctally().eprint
|
|
618
629
|
yes_words = {"y", "yes"}
|
|
619
630
|
no_words = {"n", "no"}
|
|
620
631
|
for attempt in range(3):
|
|
@@ -815,7 +826,7 @@ def _setup_status(args: argparse.Namespace) -> int:
|
|
|
815
826
|
try:
|
|
816
827
|
settings = c._load_claude_settings()
|
|
817
828
|
except c.SetupError as exc:
|
|
818
|
-
|
|
829
|
+
eprint(f"setup: warning: {exc}")
|
|
819
830
|
settings = {}
|
|
820
831
|
hook_counts = _setup_count_hook_entries(settings)
|
|
821
832
|
oauth = _setup_oauth_token_present()
|
|
@@ -911,14 +922,14 @@ def _setup_uninstall(args: argparse.Namespace) -> int:
|
|
|
911
922
|
try:
|
|
912
923
|
settings = c._load_claude_settings()
|
|
913
924
|
except c.SetupError as exc:
|
|
914
|
-
|
|
925
|
+
eprint(f"setup: {exc}")
|
|
915
926
|
return 1
|
|
916
927
|
settings, removed = _settings_merge_uninstall(settings)
|
|
917
928
|
if removed:
|
|
918
929
|
try:
|
|
919
930
|
c._write_claude_settings_atomic(settings)
|
|
920
931
|
except OSError as exc:
|
|
921
|
-
|
|
932
|
+
eprint(f"setup: failed to write {c.CLAUDE_SETTINGS_PATH}: {exc}")
|
|
922
933
|
return 2
|
|
923
934
|
out.append(f"Removed {removed} hook entries from {c.CLAUDE_SETTINGS_PATH}")
|
|
924
935
|
|
|
@@ -938,7 +949,7 @@ def _setup_uninstall(args: argparse.Namespace) -> int:
|
|
|
938
949
|
dst.unlink()
|
|
939
950
|
sym_removed += 1
|
|
940
951
|
except OSError as exc:
|
|
941
|
-
|
|
952
|
+
eprint(f"setup: failed to remove {dst}: {exc}")
|
|
942
953
|
out.append(f"Removed {sym_removed} symlinks from {dst_dir}/")
|
|
943
954
|
|
|
944
955
|
legacy = _setup_detect_legacy_snippet()
|
|
@@ -1000,7 +1011,7 @@ def _setup_uninstall(args: argparse.Namespace) -> int:
|
|
|
1000
1011
|
"exit_code": 1,
|
|
1001
1012
|
}, indent=2))
|
|
1002
1013
|
else:
|
|
1003
|
-
|
|
1014
|
+
eprint(f"setup: failed to wipe {c.APP_DIR}: {exc}")
|
|
1004
1015
|
return 1
|
|
1005
1016
|
else:
|
|
1006
1017
|
out.append(
|
|
@@ -1039,7 +1050,7 @@ def _setup_dry_run(args: argparse.Namespace) -> int:
|
|
|
1039
1050
|
# against an empty dict simply yields detected=False for entries (files
|
|
1040
1051
|
# detection is independent of settings). Mirror _setup_status's pattern
|
|
1041
1052
|
# so the user sees the same condition that would fail _setup_install.
|
|
1042
|
-
|
|
1053
|
+
eprint(f"setup: warning: {exc}")
|
|
1043
1054
|
settings = {}
|
|
1044
1055
|
detection = _setup_detect_legacy_bespoke_hooks(settings)
|
|
1045
1056
|
sym_results = []
|
|
@@ -1238,7 +1249,7 @@ def _setup_install(args: argparse.Namespace) -> int:
|
|
|
1238
1249
|
|
|
1239
1250
|
claude_dir = pathlib.Path.home() / ".claude"
|
|
1240
1251
|
if not claude_dir.exists():
|
|
1241
|
-
|
|
1252
|
+
eprint(
|
|
1242
1253
|
f"~/.claude/ does not exist. If Claude Code isn't installed yet, "
|
|
1243
1254
|
f"install it first. If it is installed, run `claude` once to "
|
|
1244
1255
|
f"initialize, then re-run cctally setup."
|
|
@@ -1259,7 +1270,7 @@ def _setup_install(args: argparse.Namespace) -> int:
|
|
|
1259
1270
|
try:
|
|
1260
1271
|
settings = c._load_claude_settings()
|
|
1261
1272
|
except c.SetupError as exc:
|
|
1262
|
-
|
|
1273
|
+
eprint(f"setup: {exc}")
|
|
1263
1274
|
return 1
|
|
1264
1275
|
|
|
1265
1276
|
# ── Legacy bespoke hook detection + migration decision (spec §1, §2) ──
|
|
@@ -1304,7 +1315,7 @@ def _setup_install(args: argparse.Namespace) -> int:
|
|
|
1304
1315
|
# still propagates into this code path post-extraction (§5.6 option C).
|
|
1305
1316
|
backup_dir = c._legacy_resolve_backup_dir()
|
|
1306
1317
|
except OSError as exc:
|
|
1307
|
-
|
|
1318
|
+
eprint(f"setup: cannot create migration backup dir: {exc}")
|
|
1308
1319
|
return 1
|
|
1309
1320
|
# Unwire BEFORE the merge so the same atomic write removes legacy
|
|
1310
1321
|
# entries and adds cctally entries (spec §2 step 6).
|
|
@@ -1322,14 +1333,14 @@ def _setup_install(args: argparse.Namespace) -> int:
|
|
|
1322
1333
|
try:
|
|
1323
1334
|
_settings_merge_install(settings, abs_path)
|
|
1324
1335
|
except c.SetupError as exc:
|
|
1325
|
-
|
|
1336
|
+
eprint(f"setup: {exc}")
|
|
1326
1337
|
return 1
|
|
1327
1338
|
|
|
1328
1339
|
sym_results = _setup_create_symlinks(repo_root, dst_dir)
|
|
1329
1340
|
failed = [r for r in sym_results if r.status == "failed"]
|
|
1330
1341
|
if failed:
|
|
1331
1342
|
for r in failed:
|
|
1332
|
-
|
|
1343
|
+
eprint(f"setup: symlink {r.name} failed: {r.detail}")
|
|
1333
1344
|
return 1
|
|
1334
1345
|
new_count = sum(1 for r in sym_results if r.status == "created")
|
|
1335
1346
|
same_count = sum(1 for r in sym_results if r.status == "already")
|
|
@@ -1356,7 +1367,7 @@ def _setup_install(args: argparse.Namespace) -> int:
|
|
|
1356
1367
|
try:
|
|
1357
1368
|
c._write_claude_settings_atomic(settings)
|
|
1358
1369
|
except OSError as exc:
|
|
1359
|
-
|
|
1370
|
+
eprint(f"setup: failed to write {c.CLAUDE_SETTINGS_PATH}: {exc}")
|
|
1360
1371
|
return 2
|
|
1361
1372
|
|
|
1362
1373
|
# ── Post-write migration apply (spec §2 steps 6a, 6b) ──
|
|
@@ -1548,7 +1559,6 @@ def _setup_install(args: argparse.Namespace) -> int:
|
|
|
1548
1559
|
|
|
1549
1560
|
|
|
1550
1561
|
def cmd_setup(args: argparse.Namespace) -> int:
|
|
1551
|
-
c = _cctally()
|
|
1552
1562
|
# Migration flags are install-mode-only. Reject combinations with
|
|
1553
1563
|
# --status or --uninstall (per spec Section 2 mode×flag matrix). The
|
|
1554
1564
|
# mutex group on the parser already prevents both flags being set
|
|
@@ -1560,7 +1570,7 @@ def cmd_setup(args: argparse.Namespace) -> int:
|
|
|
1560
1570
|
else None
|
|
1561
1571
|
)
|
|
1562
1572
|
if mig_flag and (getattr(args, "status", False) or getattr(args, "uninstall", False)):
|
|
1563
|
-
|
|
1573
|
+
eprint(f"setup: {mig_flag} is install-mode only")
|
|
1564
1574
|
return 2
|
|
1565
1575
|
if getattr(args, "uninstall", False):
|
|
1566
1576
|
return _setup_uninstall(args)
|
|
@@ -30,12 +30,27 @@ def _cctally():
|
|
|
30
30
|
return sys.modules["cctally"]
|
|
31
31
|
|
|
32
32
|
|
|
33
|
+
# === Honest imports from extracted homes ===================================
|
|
34
|
+
# Spec 2026-05-17-cctally-core-kernel-extraction.md §3.3: kernel symbols
|
|
35
|
+
# import from _cctally_core. `load_config` stays on the _cctally()
|
|
36
|
+
# accessor per spec §3.5 monkeypatch carve-out. Z-high helpers
|
|
37
|
+
# (`compute_week_cost`, `insert_cost_snapshot`) and out-of-scope
|
|
38
|
+
# (`pick_week_selection`) stay on the accessor per spec §3.7.
|
|
39
|
+
from _cctally_core import (
|
|
40
|
+
get_week_start_name,
|
|
41
|
+
open_db,
|
|
42
|
+
format_local_iso,
|
|
43
|
+
make_week_ref,
|
|
44
|
+
get_latest_usage_for_week,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
33
48
|
def cmd_sync_week(args: argparse.Namespace) -> int:
|
|
34
49
|
c = _cctally()
|
|
35
50
|
config = c.load_config()
|
|
36
|
-
week_start_name =
|
|
51
|
+
week_start_name = get_week_start_name(config, args.week_start_name)
|
|
37
52
|
|
|
38
|
-
conn =
|
|
53
|
+
conn = open_db()
|
|
39
54
|
try:
|
|
40
55
|
selection = c.pick_week_selection(
|
|
41
56
|
conn,
|
|
@@ -54,8 +69,8 @@ def cmd_sync_week(args: argparse.Namespace) -> int:
|
|
|
54
69
|
start_iso_override=selection.start_iso_override,
|
|
55
70
|
end_iso_override=selection.end_iso_override,
|
|
56
71
|
)
|
|
57
|
-
week_start_at = selection.start_iso_override or
|
|
58
|
-
week_end_at = selection.end_iso_override or
|
|
72
|
+
week_start_at = selection.start_iso_override or format_local_iso(week_start, end_of_day=False)
|
|
73
|
+
week_end_at = selection.end_iso_override or format_local_iso(week_end, end_of_day=True)
|
|
59
74
|
insert_id = c.insert_cost_snapshot(
|
|
60
75
|
conn,
|
|
61
76
|
week_start=week_start,
|
|
@@ -69,13 +84,13 @@ def cmd_sync_week(args: argparse.Namespace) -> int:
|
|
|
69
84
|
project=args.project,
|
|
70
85
|
)
|
|
71
86
|
|
|
72
|
-
week_ref =
|
|
87
|
+
week_ref = make_week_ref(
|
|
73
88
|
week_start_date=week_start.isoformat(),
|
|
74
89
|
week_end_date=week_end.isoformat(),
|
|
75
90
|
week_start_at=week_start_at,
|
|
76
91
|
week_end_at=week_end_at,
|
|
77
92
|
)
|
|
78
|
-
usage_row =
|
|
93
|
+
usage_row = get_latest_usage_for_week(conn, week_ref)
|
|
79
94
|
weekly_percent = float(usage_row["weekly_percent"]) if usage_row else None
|
|
80
95
|
dollars_per_percent = (
|
|
81
96
|
result.cost_usd / weekly_percent if weekly_percent and weekly_percent > 0 else None
|
package/bin/_cctally_tui.py
CHANGED
|
@@ -202,43 +202,40 @@ def _cctally():
|
|
|
202
202
|
return sys.modules["cctally"]
|
|
203
203
|
|
|
204
204
|
|
|
205
|
+
# === Honest imports from extracted homes ===================================
|
|
206
|
+
# Spec 2026-05-17-cctally-core-kernel-extraction.md §3.3.
|
|
207
|
+
from _cctally_core import (
|
|
208
|
+
eprint,
|
|
209
|
+
parse_iso_datetime,
|
|
210
|
+
_now_utc,
|
|
211
|
+
open_db,
|
|
212
|
+
get_latest_usage_for_week,
|
|
213
|
+
_canonicalize_optional_iso,
|
|
214
|
+
make_week_ref,
|
|
215
|
+
)
|
|
216
|
+
from _lib_display_tz import (
|
|
217
|
+
format_display_dt,
|
|
218
|
+
resolve_display_tz,
|
|
219
|
+
normalize_display_tz_value,
|
|
220
|
+
_compute_display_block,
|
|
221
|
+
)
|
|
222
|
+
from _lib_aggregators import _aggregate_monthly
|
|
223
|
+
|
|
224
|
+
|
|
205
225
|
# === Module-level back-ref shims for helpers that STAY in bin/cctally ======
|
|
206
226
|
# Each shim resolves ``sys.modules['cctally'].X`` at CALL TIME (not bind
|
|
207
227
|
# time), so monkeypatches on cctally's namespace propagate into the moved
|
|
208
|
-
# code unchanged.
|
|
209
|
-
#
|
|
210
|
-
#
|
|
211
|
-
#
|
|
212
|
-
|
|
213
|
-
return sys.modules["cctally"].eprint(*args, **kwargs)
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
def parse_iso_datetime(*args, **kwargs):
|
|
217
|
-
return sys.modules["cctally"].parse_iso_datetime(*args, **kwargs)
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
def _now_utc(*args, **kwargs):
|
|
221
|
-
return sys.modules["cctally"]._now_utc(*args, **kwargs)
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
def open_db(*args, **kwargs):
|
|
225
|
-
return sys.modules["cctally"].open_db(*args, **kwargs)
|
|
226
|
-
|
|
227
|
-
|
|
228
|
+
# code unchanged. `load_config` and `get_claude_session_entries` STAY as
|
|
229
|
+
# shims even though their natural homes are decentralized (_cctally_config
|
|
230
|
+
# / _cctally_cache) — tests monkeypatch them via `ns["X"]` (21 sites total,
|
|
231
|
+
# audited 2026-05-17); direct imports would silently bypass the patches.
|
|
232
|
+
# See spec §3.5 (carve-out) and §3.7 (stays-on-shim allowlist).
|
|
228
233
|
def load_config(*args, **kwargs):
|
|
229
234
|
return sys.modules["cctally"].load_config(*args, **kwargs)
|
|
230
235
|
|
|
231
236
|
|
|
232
|
-
def
|
|
233
|
-
return sys.modules["cctally"].
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
def resolve_display_tz(*args, **kwargs):
|
|
237
|
-
return sys.modules["cctally"].resolve_display_tz(*args, **kwargs)
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
def normalize_display_tz_value(*args, **kwargs):
|
|
241
|
-
return sys.modules["cctally"].normalize_display_tz_value(*args, **kwargs)
|
|
237
|
+
def get_claude_session_entries(*args, **kwargs):
|
|
238
|
+
return sys.modules["cctally"].get_claude_session_entries(*args, **kwargs)
|
|
242
239
|
|
|
243
240
|
|
|
244
241
|
def _resolve_display_tz_obj(*args, **kwargs):
|
|
@@ -253,10 +250,6 @@ def _apply_midweek_reset_override(*args, **kwargs):
|
|
|
253
250
|
return sys.modules["cctally"]._apply_midweek_reset_override(*args, **kwargs)
|
|
254
251
|
|
|
255
252
|
|
|
256
|
-
def _compute_display_block(*args, **kwargs):
|
|
257
|
-
return sys.modules["cctally"]._compute_display_block(*args, **kwargs)
|
|
258
|
-
|
|
259
|
-
|
|
260
253
|
def _compute_forecast(*args, **kwargs):
|
|
261
254
|
return sys.modules["cctally"]._compute_forecast(*args, **kwargs)
|
|
262
255
|
|
|
@@ -297,18 +290,6 @@ def _aggregate_claude_sessions(*args, **kwargs):
|
|
|
297
290
|
return sys.modules["cctally"]._aggregate_claude_sessions(*args, **kwargs)
|
|
298
291
|
|
|
299
292
|
|
|
300
|
-
def _aggregate_monthly(*args, **kwargs):
|
|
301
|
-
return sys.modules["cctally"]._aggregate_monthly(*args, **kwargs)
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
def get_claude_session_entries(*args, **kwargs):
|
|
305
|
-
return sys.modules["cctally"].get_claude_session_entries(*args, **kwargs)
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
def get_latest_usage_for_week(*args, **kwargs):
|
|
309
|
-
return sys.modules["cctally"].get_latest_usage_for_week(*args, **kwargs)
|
|
310
|
-
|
|
311
|
-
|
|
312
293
|
def get_latest_cost_for_week(*args, **kwargs):
|
|
313
294
|
return sys.modules["cctally"].get_latest_cost_for_week(*args, **kwargs)
|
|
314
295
|
|
|
@@ -317,10 +298,6 @@ def get_milestones_for_week(*args, **kwargs):
|
|
|
317
298
|
return sys.modules["cctally"].get_milestones_for_week(*args, **kwargs)
|
|
318
299
|
|
|
319
300
|
|
|
320
|
-
def _canonicalize_optional_iso(*args, **kwargs):
|
|
321
|
-
return sys.modules["cctally"]._canonicalize_optional_iso(*args, **kwargs)
|
|
322
|
-
|
|
323
|
-
|
|
324
301
|
def get_recent_weeks(*args, **kwargs):
|
|
325
302
|
return sys.modules["cctally"].get_recent_weeks(*args, **kwargs)
|
|
326
303
|
|
|
@@ -1555,7 +1532,7 @@ def _tui_build_trend(
|
|
|
1555
1532
|
canon_start, canon_end = c._get_canonical_boundary_for_date(
|
|
1556
1533
|
conn, latest_usage["week_start_date"]
|
|
1557
1534
|
)
|
|
1558
|
-
current_ref =
|
|
1535
|
+
current_ref = make_week_ref(
|
|
1559
1536
|
week_start_date=latest_usage["week_start_date"],
|
|
1560
1537
|
week_end_date=latest_usage["week_end_date"],
|
|
1561
1538
|
week_start_at=canon_start,
|
package/bin/_cctally_update.py
CHANGED
|
@@ -192,28 +192,23 @@ def _cctally():
|
|
|
192
192
|
return sys.modules["cctally"]
|
|
193
193
|
|
|
194
194
|
|
|
195
|
+
# === Honest imports from extracted homes ===================================
|
|
196
|
+
# Spec 2026-05-17-cctally-core-kernel-extraction.md §3.3.
|
|
197
|
+
from _cctally_core import eprint, _now_utc
|
|
198
|
+
from _cctally_config import save_config
|
|
199
|
+
|
|
200
|
+
|
|
195
201
|
# === Module-level back-ref shims for helpers that STAY in bin/cctally ======
|
|
196
202
|
# Each shim resolves ``sys.modules['cctally'].X`` at CALL TIME (not bind
|
|
197
203
|
# time), so monkeypatches on cctally's namespace propagate into the moved
|
|
198
|
-
# code unchanged.
|
|
199
|
-
#
|
|
200
|
-
# (
|
|
201
|
-
|
|
202
|
-
return sys.modules["cctally"].eprint(*args, **kwargs)
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
def _now_utc(*args, **kwargs):
|
|
206
|
-
return sys.modules["cctally"]._now_utc(*args, **kwargs)
|
|
207
|
-
|
|
208
|
-
|
|
204
|
+
# code unchanged. `load_config` STAYS as a shim even though its natural
|
|
205
|
+
# home is _cctally_config — tests monkeypatch it via `ns["load_config"]`
|
|
206
|
+
# (16 sites, audited 2026-05-17); direct import would silently bypass.
|
|
207
|
+
# See spec §3.5 (carve-out) and §3.7 (stays-on-shim allowlist).
|
|
209
208
|
def load_config(*args, **kwargs):
|
|
210
209
|
return sys.modules["cctally"].load_config(*args, **kwargs)
|
|
211
210
|
|
|
212
211
|
|
|
213
|
-
def save_config(*args, **kwargs):
|
|
214
|
-
return sys.modules["cctally"].save_config(*args, **kwargs)
|
|
215
|
-
|
|
216
|
-
|
|
217
212
|
def _release_read_latest_release_version(*args, **kwargs):
|
|
218
213
|
return sys.modules["cctally"]._release_read_latest_release_version(
|
|
219
214
|
*args, **kwargs
|
|
@@ -2114,7 +2109,7 @@ def _should_show_update_banner(
|
|
|
2114
2109
|
return False
|
|
2115
2110
|
if not config.get("update", {}).get("check", {}).get("enabled", True):
|
|
2116
2111
|
return False
|
|
2117
|
-
available, _ = c._compute_effective_update_available(state, suppress,
|
|
2112
|
+
available, _ = c._compute_effective_update_available(state, suppress, _now_utc())
|
|
2118
2113
|
return available
|
|
2119
2114
|
|
|
2120
2115
|
|
package/bin/_lib_aggregators.py
CHANGED
|
@@ -79,6 +79,13 @@ _lib_subscription_weeks = _load_lib("_lib_subscription_weeks")
|
|
|
79
79
|
SubWeek = _lib_subscription_weeks.SubWeek
|
|
80
80
|
|
|
81
81
|
|
|
82
|
+
# === Honest imports from extracted homes ===================================
|
|
83
|
+
# Spec 2026-05-17-cctally-core-kernel-extraction.md §3.3: kernel symbols
|
|
84
|
+
# import from _cctally_core. `CODEX_SESSIONS_DIR` (path constant) and
|
|
85
|
+
# `_decode_escaped_cwd` (out-of-scope) stay on the _cctally() accessor.
|
|
86
|
+
from _cctally_core import parse_iso_datetime
|
|
87
|
+
|
|
88
|
+
|
|
82
89
|
@dataclass
|
|
83
90
|
class BucketUsage:
|
|
84
91
|
"""Aggregated usage for one time bucket.
|
|
@@ -247,7 +254,6 @@ def _aggregate_weekly(
|
|
|
247
254
|
# candidate week in O(log W) per entry rather than the linear
|
|
248
255
|
# scan that previously ran ~130k x ~54 = 7M comparisons.
|
|
249
256
|
import bisect
|
|
250
|
-
parse_iso_datetime = _cctally().parse_iso_datetime
|
|
251
257
|
parsed_bounds: list[tuple[dt.datetime, dt.datetime, str]] = []
|
|
252
258
|
for w in weeks:
|
|
253
259
|
start_dt = parse_iso_datetime(w.start_ts, "week.start_ts")
|
package/bin/_lib_diff_kernel.py
CHANGED
|
@@ -116,11 +116,25 @@ _resolve_tz = _lib_display_tz._resolve_tz
|
|
|
116
116
|
format_display_dt = _lib_display_tz.format_display_dt
|
|
117
117
|
|
|
118
118
|
|
|
119
|
-
#
|
|
120
|
-
#
|
|
121
|
-
#
|
|
122
|
-
#
|
|
123
|
-
|
|
119
|
+
# === Honest imports from extracted homes ===================================
|
|
120
|
+
# Spec 2026-05-17-cctally-core-kernel-extraction.md §3.3: kernel symbols
|
|
121
|
+
# (Z-leaf + Z-mid) import from _cctally_core. The legacy shim functions
|
|
122
|
+
# for these names are deleted.
|
|
123
|
+
from _cctally_core import (
|
|
124
|
+
open_db,
|
|
125
|
+
_command_as_of,
|
|
126
|
+
_canonicalize_optional_iso,
|
|
127
|
+
parse_iso_datetime,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# === Module-level back-ref shims for helpers that STAY in bin/cctally ======
|
|
132
|
+
# Each shim resolves ``sys.modules['cctally'].X`` at CALL TIME (not bind
|
|
133
|
+
# time), so monkeypatches on cctally's namespace propagate into the moved
|
|
134
|
+
# code unchanged. `get_claude_session_entries` STAYS as a shim even though
|
|
135
|
+
# its natural home is _cctally_cache — tests monkeypatch it via ``ns["X"]``
|
|
136
|
+
# (audited 2026-05-17); a direct import would silently bypass the patches.
|
|
137
|
+
# See spec §3.5 (carve-out) and §3.7 (stays-on-shim allowlist).
|
|
124
138
|
def get_claude_session_entries(*args, **kwargs):
|
|
125
139
|
return sys.modules["cctally"].get_claude_session_entries(*args, **kwargs)
|
|
126
140
|
|
|
@@ -129,10 +143,6 @@ def _resolve_project_key(*args, **kwargs):
|
|
|
129
143
|
return sys.modules["cctally"]._resolve_project_key(*args, **kwargs)
|
|
130
144
|
|
|
131
145
|
|
|
132
|
-
def open_db(*args, **kwargs):
|
|
133
|
-
return sys.modules["cctally"].open_db(*args, **kwargs)
|
|
134
|
-
|
|
135
|
-
|
|
136
146
|
def _iso_z(*args, **kwargs):
|
|
137
147
|
return sys.modules["cctally"]._iso_z(*args, **kwargs)
|
|
138
148
|
|
|
@@ -145,18 +155,6 @@ def _style_ansi(*args, **kwargs):
|
|
|
145
155
|
return sys.modules["cctally"]._style_ansi(*args, **kwargs)
|
|
146
156
|
|
|
147
157
|
|
|
148
|
-
def _command_as_of(*args, **kwargs):
|
|
149
|
-
return sys.modules["cctally"]._command_as_of(*args, **kwargs)
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
def _canonicalize_optional_iso(*args, **kwargs):
|
|
153
|
-
return sys.modules["cctally"]._canonicalize_optional_iso(*args, **kwargs)
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
def parse_iso_datetime(*args, **kwargs):
|
|
157
|
-
return sys.modules["cctally"].parse_iso_datetime(*args, **kwargs)
|
|
158
|
-
|
|
159
|
-
|
|
160
158
|
# Private eprint shim per spec §5.3 (pure layer does not back-import
|
|
161
159
|
# cctally for ubiquitous helpers; eprint isn't actually called by the
|
|
162
160
|
# moved code, but kept here as the canonical pure-layer pattern so
|