cctally 1.64.0 → 1.66.0
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 +50 -0
- package/bin/_cctally_alerts.py +1 -1
- package/bin/_cctally_cache.py +262 -40
- package/bin/_cctally_cache_report.py +7 -5
- package/bin/_cctally_core.py +49 -14
- package/bin/_cctally_dashboard.py +827 -4911
- package/bin/_cctally_dashboard_cache_report.py +714 -0
- package/bin/_cctally_dashboard_conversation.py +771 -0
- package/bin/_cctally_dashboard_envelope.py +1520 -0
- package/bin/_cctally_dashboard_share.py +2012 -0
- package/bin/_cctally_db.py +127 -5
- package/bin/_cctally_doctor.py +104 -3
- package/bin/_cctally_five_hour.py +2 -1
- package/bin/_cctally_forecast.py +78 -135
- package/bin/_cctally_parser.py +919 -784
- package/bin/_cctally_percent_breakdown.py +1 -1
- package/bin/_cctally_pricing_check.py +39 -0
- package/bin/_cctally_project.py +8 -5
- package/bin/_cctally_record.py +279 -274
- package/bin/_cctally_reporting.py +1 -1
- package/bin/_cctally_sync_week.py +1 -1
- package/bin/_cctally_telemetry.py +3 -5
- package/bin/_cctally_tui.py +487 -254
- package/bin/_cctally_update.py +5 -0
- package/bin/_lib_alert_dispatch.py +1 -1
- package/bin/_lib_blocks.py +6 -1
- package/bin/_lib_conversation_query.py +349 -36
- package/bin/_lib_credit.py +133 -0
- package/bin/_lib_doctor.py +150 -1
- package/bin/_lib_five_hour.py +34 -0
- package/bin/_lib_forecast.py +144 -0
- package/bin/_lib_json_envelope.py +38 -0
- package/bin/_lib_jsonl.py +115 -73
- package/bin/_lib_log.py +96 -0
- package/bin/_lib_perf.py +180 -0
- package/bin/_lib_pricing.py +47 -1
- package/bin/_lib_pricing_check.py +25 -3
- package/bin/_lib_record.py +179 -0
- package/bin/_lib_render.py +18 -10
- package/bin/_lib_snapshot_cache.py +61 -0
- package/bin/_lib_transcript_access.py +23 -0
- package/bin/cctally +51 -7
- package/bin/cctally-dashboard +2 -2
- package/bin/cctally-statusline +1 -0
- package/bin/cctally-tui +2 -2
- package/dashboard/static/assets/index-CJTUCpKt.js +80 -0
- package/dashboard/static/assets/index-DQWNrIqu.css +1 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +11 -1
- package/dashboard/static/assets/index-0jzYm75p.css +0 -1
- package/dashboard/static/assets/index-D_Ylyqsf.js +0 -80
package/bin/_cctally_parser.py
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
"""cctally CLI argument-parser construction (eager sibling).
|
|
2
2
|
|
|
3
|
-
Holds the full argparse tree: build_parser()
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
Holds the full argparse tree: build_parser() loops over the ordered
|
|
4
|
+
_REGISTRATION table (`_Reg` rows) of per-command `_build_*_parser` builders
|
|
5
|
+
(#279 S6 W3 — table order is registration order is --help order), the
|
|
6
|
+
_add_*_args helpers incl. _add_since_until_args, _share_validate_args, the
|
|
7
|
+
_nonneg_int type validator, and CLIHelpFormatter. Loaded eagerly by bin/cctally;
|
|
8
|
+
every symbol is re-exported into the cctally namespace. cmd_* handlers and other
|
|
9
|
+
bin/cctally-staying globals are reached via the call-time _cctally() accessor;
|
|
10
|
+
the table stores builder callables + literal help_text/xref + the __preview
|
|
11
|
+
predicate lambda — nothing is resolved at import time.
|
|
8
12
|
|
|
9
13
|
Spec: docs/superpowers/specs/2026-05-30-parser-share-extraction-design.md
|
|
10
14
|
"""
|
|
@@ -13,6 +17,7 @@ from __future__ import annotations
|
|
|
13
17
|
import argparse
|
|
14
18
|
import sys
|
|
15
19
|
import textwrap
|
|
20
|
+
from typing import NamedTuple
|
|
16
21
|
|
|
17
22
|
from _cctally_core import WEEKDAY_MAP
|
|
18
23
|
from _lib_display_tz import _argparse_tz
|
|
@@ -91,6 +96,17 @@ def _add_mode_arg(parser, *, noop: bool = False) -> None:
|
|
|
91
96
|
)
|
|
92
97
|
|
|
93
98
|
|
|
99
|
+
def _add_since_until_args(parser, *, metavar_since, metavar_until,
|
|
100
|
+
help_since, help_until) -> None:
|
|
101
|
+
"""Shared -s/--since -u/--until pair; metavars/help passed verbatim
|
|
102
|
+
per command (the YYYYMMDD vs YYYY-MM-DD drift is data, not unified —
|
|
103
|
+
#279 S6 W3; --help bytes must not move)."""
|
|
104
|
+
parser.add_argument("-s", "--since", default=None,
|
|
105
|
+
metavar=metavar_since, help=help_since)
|
|
106
|
+
parser.add_argument("-u", "--until", default=None,
|
|
107
|
+
metavar=metavar_until, help=help_until)
|
|
108
|
+
|
|
109
|
+
|
|
94
110
|
def _add_ccusage_alias_args(parser, *, ansi_emit: bool) -> None:
|
|
95
111
|
"""Attach the Session A ccusage alias surface to a Claude-cmd subparser.
|
|
96
112
|
|
|
@@ -418,18 +434,10 @@ def _build_daily_parser(subparsers, name, *, help_text, xref):
|
|
|
418
434
|
cctally daily -i --project-aliases repos=Repos
|
|
419
435
|
"""),
|
|
420
436
|
)
|
|
421
|
-
|
|
422
|
-
"
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
help="Filter from date (inclusive).",
|
|
426
|
-
)
|
|
427
|
-
p.add_argument(
|
|
428
|
-
"-u", "--until",
|
|
429
|
-
default=None,
|
|
430
|
-
metavar="YYYYMMDD",
|
|
431
|
-
help="Filter until date (inclusive).",
|
|
432
|
-
)
|
|
437
|
+
_add_since_until_args(
|
|
438
|
+
p, metavar_since="YYYYMMDD", metavar_until="YYYYMMDD",
|
|
439
|
+
help_since="Filter from date (inclusive).",
|
|
440
|
+
help_until="Filter until date (inclusive).")
|
|
433
441
|
p.add_argument(
|
|
434
442
|
"-b", "--breakdown",
|
|
435
443
|
action="store_true",
|
|
@@ -500,18 +508,10 @@ def _build_monthly_parser(subparsers, name, *, help_text, xref):
|
|
|
500
508
|
cctally monthly --order desc
|
|
501
509
|
"""),
|
|
502
510
|
)
|
|
503
|
-
|
|
504
|
-
"
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
help="Filter from date (inclusive).",
|
|
508
|
-
)
|
|
509
|
-
p.add_argument(
|
|
510
|
-
"-u", "--until",
|
|
511
|
-
default=None,
|
|
512
|
-
metavar="YYYYMMDD",
|
|
513
|
-
help="Filter until date (inclusive).",
|
|
514
|
-
)
|
|
511
|
+
_add_since_until_args(
|
|
512
|
+
p, metavar_since="YYYYMMDD", metavar_until="YYYYMMDD",
|
|
513
|
+
help_since="Filter from date (inclusive).",
|
|
514
|
+
help_until="Filter until date (inclusive).")
|
|
515
515
|
p.add_argument(
|
|
516
516
|
"-b", "--breakdown",
|
|
517
517
|
action="store_true",
|
|
@@ -563,10 +563,10 @@ def _build_weekly_parser(subparsers, name, *, help_text, xref):
|
|
|
563
563
|
cctally weekly --order desc
|
|
564
564
|
"""),
|
|
565
565
|
)
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
566
|
+
_add_since_until_args(
|
|
567
|
+
p, metavar_since="YYYYMMDD", metavar_until="YYYYMMDD",
|
|
568
|
+
help_since="Filter from date (inclusive).",
|
|
569
|
+
help_until="Filter until date (inclusive).")
|
|
570
570
|
p.add_argument("-b", "--breakdown", action="store_true",
|
|
571
571
|
help="Show per-model cost breakdown sub-rows.")
|
|
572
572
|
p.add_argument("-o", "--order", choices=("asc", "desc"), default="asc",
|
|
@@ -604,10 +604,10 @@ def _build_session_parser(subparsers, name, *, help_text, xref):
|
|
|
604
604
|
cctally session --order desc
|
|
605
605
|
"""),
|
|
606
606
|
)
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
607
|
+
_add_since_until_args(
|
|
608
|
+
p, metavar_since="YYYYMMDD", metavar_until="YYYYMMDD",
|
|
609
|
+
help_since="Filter from date (inclusive).",
|
|
610
|
+
help_until="Filter until date (inclusive).")
|
|
611
611
|
p.add_argument("-b", "--breakdown", action="store_true",
|
|
612
612
|
help="Show per-model cost breakdown sub-rows.")
|
|
613
613
|
p.add_argument("-o", "--order", choices=("asc", "desc"), default="asc",
|
|
@@ -658,18 +658,10 @@ def _build_blocks_parser(subparsers, name, *, help_text, xref):
|
|
|
658
658
|
cctally blocks --since 20260414 --json
|
|
659
659
|
"""),
|
|
660
660
|
)
|
|
661
|
-
|
|
662
|
-
"
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
help="Filter from date (inclusive).",
|
|
666
|
-
)
|
|
667
|
-
p.add_argument(
|
|
668
|
-
"-u", "--until",
|
|
669
|
-
default=None,
|
|
670
|
-
metavar="YYYYMMDD",
|
|
671
|
-
help="Filter until date (inclusive).",
|
|
672
|
-
)
|
|
661
|
+
_add_since_until_args(
|
|
662
|
+
p, metavar_since="YYYYMMDD", metavar_until="YYYYMMDD",
|
|
663
|
+
help_since="Filter from date (inclusive).",
|
|
664
|
+
help_until="Filter until date (inclusive).")
|
|
673
665
|
p.add_argument(
|
|
674
666
|
"-b", "--breakdown",
|
|
675
667
|
action="store_true",
|
|
@@ -926,10 +918,10 @@ def _build_codex_daily_parser(subparsers, name, *, help_text, xref):
|
|
|
926
918
|
cctally codex-daily --order desc
|
|
927
919
|
"""),
|
|
928
920
|
)
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
921
|
+
_add_since_until_args(
|
|
922
|
+
p, metavar_since="YYYY-MM-DD", metavar_until="YYYY-MM-DD",
|
|
923
|
+
help_since="Filter from date (inclusive; accepts YYYY-MM-DD or YYYYMMDD).",
|
|
924
|
+
help_until="Filter until date (inclusive; accepts YYYY-MM-DD or YYYYMMDD).")
|
|
933
925
|
p.add_argument("-b", "--breakdown", action="store_true",
|
|
934
926
|
help="Show per-model cost breakdown sub-rows.")
|
|
935
927
|
p.add_argument("-o", "--order", choices=("asc", "desc"), default="asc",
|
|
@@ -957,10 +949,10 @@ def _build_codex_monthly_parser(subparsers, name, *, help_text, xref):
|
|
|
957
949
|
cctally codex-monthly --json
|
|
958
950
|
"""),
|
|
959
951
|
)
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
952
|
+
_add_since_until_args(
|
|
953
|
+
p, metavar_since="YYYY-MM-DD", metavar_until="YYYY-MM-DD",
|
|
954
|
+
help_since="Filter from date (inclusive; accepts YYYY-MM-DD or YYYYMMDD).",
|
|
955
|
+
help_until="Filter until date (inclusive; accepts YYYY-MM-DD or YYYYMMDD).")
|
|
964
956
|
p.add_argument("-b", "--breakdown", action="store_true",
|
|
965
957
|
help="Show per-model cost breakdown sub-rows.")
|
|
966
958
|
p.add_argument("-o", "--order", choices=("asc", "desc"), default="asc",
|
|
@@ -992,10 +984,10 @@ def _build_codex_weekly_parser(subparsers, name, *, help_text, xref):
|
|
|
992
984
|
cctally codex-weekly --order desc
|
|
993
985
|
"""),
|
|
994
986
|
)
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
987
|
+
_add_since_until_args(
|
|
988
|
+
p, metavar_since="YYYY-MM-DD", metavar_until="YYYY-MM-DD",
|
|
989
|
+
help_since="Filter from date (inclusive; accepts YYYY-MM-DD or YYYYMMDD).",
|
|
990
|
+
help_until="Filter until date (inclusive; accepts YYYY-MM-DD or YYYYMMDD).")
|
|
999
991
|
p.add_argument("-b", "--breakdown", action="store_true",
|
|
1000
992
|
help="Show per-model cost breakdown sub-rows.")
|
|
1001
993
|
p.add_argument("-o", "--order", choices=("asc", "desc"), default="asc",
|
|
@@ -1023,10 +1015,10 @@ def _build_codex_session_parser(subparsers, name, *, help_text, xref):
|
|
|
1023
1015
|
cctally codex-session --json
|
|
1024
1016
|
"""),
|
|
1025
1017
|
)
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1018
|
+
_add_since_until_args(
|
|
1019
|
+
p, metavar_since="YYYY-MM-DD", metavar_until="YYYY-MM-DD",
|
|
1020
|
+
help_since="Filter from date (inclusive; accepts YYYY-MM-DD or YYYYMMDD).",
|
|
1021
|
+
help_until="Filter until date (inclusive; accepts YYYY-MM-DD or YYYYMMDD).")
|
|
1030
1022
|
p.add_argument("-o", "--order", choices=("asc", "desc"), default="asc",
|
|
1031
1023
|
help="Sort direction by last activity (default: asc — earliest first).")
|
|
1032
1024
|
p.add_argument("--json", action="store_true", dest="json",
|
|
@@ -1036,67 +1028,35 @@ def _build_codex_session_parser(subparsers, name, *, help_text, xref):
|
|
|
1036
1028
|
return p
|
|
1037
1029
|
|
|
1038
1030
|
|
|
1039
|
-
def
|
|
1040
|
-
|
|
1041
|
-
p = argparse.ArgumentParser(
|
|
1042
|
-
prog="cctally",
|
|
1043
|
-
formatter_class=CLIHelpFormatter,
|
|
1044
|
-
description=textwrap.dedent(
|
|
1045
|
-
"""\
|
|
1046
|
-
Track Claude subscription weekly usage percent and weekly cost
|
|
1047
|
-
in a local SQLite database.
|
|
1031
|
+
def _build_sync_week_parser(subparsers, name, *, help_text, xref=None):
|
|
1032
|
+
"""Build the `sync-week` parser (registered via _REGISTRATION; #279 S6 W3).
|
|
1048
1033
|
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
epilog=textwrap.dedent(
|
|
1057
|
-
"""\
|
|
1058
|
-
Quick start:
|
|
1059
|
-
# Add record-usage call to ~/.claude/statusline-command.sh (see record-usage --help)
|
|
1060
|
-
cctally sync-week
|
|
1061
|
-
cctally report
|
|
1062
|
-
"""
|
|
1063
|
-
),
|
|
1064
|
-
)
|
|
1065
|
-
p.add_argument(
|
|
1066
|
-
"-v", "--version",
|
|
1067
|
-
action="store_true",
|
|
1068
|
-
default=argparse.SUPPRESS,
|
|
1069
|
-
help="Print cctally version (from CHANGELOG.md latest release header) and exit",
|
|
1070
|
-
)
|
|
1071
|
-
sub = p.add_subparsers(
|
|
1072
|
-
dest="command",
|
|
1073
|
-
required=False,
|
|
1074
|
-
title="commands",
|
|
1075
|
-
metavar="<command>",
|
|
1076
|
-
)
|
|
1077
|
-
|
|
1078
|
-
py = sub.add_parser(
|
|
1079
|
-
"sync-week",
|
|
1080
|
-
help="Compute weekly cost from session data and store in SQLite",
|
|
1034
|
+
Move-only extraction of the former inline build_parser() block;
|
|
1035
|
+
call-time `c = _cctally()` binding, --help bytes unchanged.
|
|
1036
|
+
"""
|
|
1037
|
+
c = _cctally()
|
|
1038
|
+
py = subparsers.add_parser(
|
|
1039
|
+
name,
|
|
1040
|
+
help=help_text,
|
|
1081
1041
|
formatter_class=CLIHelpFormatter,
|
|
1082
1042
|
description=textwrap.dedent(
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1043
|
+
"""\
|
|
1044
|
+
Compute and store weekly cost (USD) for a selected week window.
|
|
1045
|
+
|
|
1046
|
+
Week selection priority:
|
|
1047
|
+
1) Explicit --week-start/--week-end (date based)
|
|
1048
|
+
2) Latest usage snapshot weekStartAt/weekEndAt (hour-accurate)
|
|
1049
|
+
3) Current week from configured week-start rule
|
|
1050
|
+
"""
|
|
1051
|
+
),
|
|
1092
1052
|
epilog=textwrap.dedent(
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1053
|
+
"""\
|
|
1054
|
+
Examples:
|
|
1055
|
+
cctally sync-week
|
|
1056
|
+
cctally sync-week --week-start 2026-02-05 --week-end 2026-02-12
|
|
1057
|
+
cctally sync-week --mode calculate --offline --json
|
|
1058
|
+
"""
|
|
1059
|
+
),
|
|
1100
1060
|
)
|
|
1101
1061
|
py.add_argument(
|
|
1102
1062
|
"--week-start",
|
|
@@ -1144,28 +1104,35 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1144
1104
|
)
|
|
1145
1105
|
py.set_defaults(func=c.cmd_sync_week)
|
|
1146
1106
|
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1107
|
+
def _build_report_parser(subparsers, name, *, help_text, xref=None):
|
|
1108
|
+
"""Build the `report` parser (registered via _REGISTRATION; #279 S6 W3).
|
|
1109
|
+
|
|
1110
|
+
Move-only extraction of the former inline build_parser() block;
|
|
1111
|
+
call-time `c = _cctally()` binding, --help bytes unchanged.
|
|
1112
|
+
"""
|
|
1113
|
+
c = _cctally()
|
|
1114
|
+
pr = subparsers.add_parser(
|
|
1115
|
+
name,
|
|
1116
|
+
help=help_text,
|
|
1150
1117
|
formatter_class=CLIHelpFormatter,
|
|
1151
1118
|
description=textwrap.dedent(
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1119
|
+
"""\
|
|
1120
|
+
Report current and historical dollars per 1% weekly usage.
|
|
1121
|
+
|
|
1122
|
+
For each week, report joins:
|
|
1123
|
+
- latest usage snapshot (%)
|
|
1124
|
+
- latest cost snapshot (USD)
|
|
1125
|
+
then computes USD / percent.
|
|
1126
|
+
"""
|
|
1127
|
+
),
|
|
1161
1128
|
epilog=textwrap.dedent(
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1129
|
+
"""\
|
|
1130
|
+
Examples:
|
|
1131
|
+
cctally report
|
|
1132
|
+
cctally report --sync-current
|
|
1133
|
+
cctally report --weeks 12 --json
|
|
1134
|
+
"""
|
|
1135
|
+
),
|
|
1169
1136
|
)
|
|
1170
1137
|
pr.add_argument(
|
|
1171
1138
|
"--weeks",
|
|
@@ -1220,30 +1187,37 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1220
1187
|
_add_share_args(pr)
|
|
1221
1188
|
pr.set_defaults(func=c.cmd_report)
|
|
1222
1189
|
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1190
|
+
def _build_forecast_parser(subparsers, name, *, help_text, xref=None):
|
|
1191
|
+
"""Build the `forecast` parser (registered via _REGISTRATION; #279 S6 W3).
|
|
1192
|
+
|
|
1193
|
+
Move-only extraction of the former inline build_parser() block;
|
|
1194
|
+
call-time `c = _cctally()` binding, --help bytes unchanged.
|
|
1195
|
+
"""
|
|
1196
|
+
c = _cctally()
|
|
1197
|
+
fc = subparsers.add_parser(
|
|
1198
|
+
name,
|
|
1199
|
+
help=help_text,
|
|
1226
1200
|
formatter_class=CLIHelpFormatter,
|
|
1227
1201
|
description=textwrap.dedent(
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1202
|
+
"""\
|
|
1203
|
+
Forecast end-of-week usage % and daily $ / % budgets to stay under
|
|
1204
|
+
target ceilings (default 100% and 90%). Reads current-week
|
|
1205
|
+
`weekly_usage_snapshots` + `session_entries`; never writes.
|
|
1206
|
+
"""
|
|
1207
|
+
),
|
|
1234
1208
|
epilog=textwrap.dedent(
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1209
|
+
"""\
|
|
1210
|
+
Examples:
|
|
1211
|
+
cctally forecast
|
|
1212
|
+
cctally forecast --json
|
|
1213
|
+
cctally forecast --status-line --no-sync
|
|
1214
|
+
cctally forecast --targets 100,95,85
|
|
1215
|
+
|
|
1216
|
+
Status-line integration (add to ~/.claude/statusline-command.sh):
|
|
1217
|
+
forecast_seg=$(cctally forecast --status-line --no-sync 2>/dev/null)
|
|
1218
|
+
# ...then include "$forecast_seg" in your prompt composition.
|
|
1219
|
+
"""
|
|
1220
|
+
),
|
|
1247
1221
|
)
|
|
1248
1222
|
fc.add_argument("--reveal-projects", action="store_true", dest="reveal_projects",
|
|
1249
1223
|
help="In --format output, show real project basenames instead of "
|
|
@@ -1259,40 +1233,41 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1259
1233
|
help="Skip sync_cache(); recommended for status-line use.")
|
|
1260
1234
|
fc.add_argument("--color", choices=("auto", "always", "never"), default="auto",
|
|
1261
1235
|
help="Color output control (also honors NO_COLOR).")
|
|
1262
|
-
# Dev-only: override "now" for deterministic fixture tests. Hidden from --help.
|
|
1263
1236
|
fc.add_argument("--as-of", dest="as_of", default=None, help=argparse.SUPPRESS)
|
|
1264
1237
|
_add_share_args(fc, has_status_line=True)
|
|
1265
1238
|
fc.set_defaults(func=c.cmd_forecast)
|
|
1266
1239
|
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1240
|
+
def _build_budget_parser(subparsers, name, *, help_text, xref=None):
|
|
1241
|
+
"""Build the `budget` parser (registered via _REGISTRATION; #279 S6 W3).
|
|
1242
|
+
|
|
1243
|
+
Move-only extraction of the former inline build_parser() block;
|
|
1244
|
+
call-time `c = _cctally()` binding, --help bytes unchanged.
|
|
1245
|
+
"""
|
|
1246
|
+
c = _cctally()
|
|
1247
|
+
bg = subparsers.add_parser(
|
|
1248
|
+
name,
|
|
1249
|
+
help=help_text,
|
|
1275
1250
|
formatter_class=CLIHelpFormatter,
|
|
1276
1251
|
description=textwrap.dedent(
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1252
|
+
"""\
|
|
1253
|
+
Track Claude equivalent-$ spend for the current subscription week
|
|
1254
|
+
against a weekly budget. Shows spend, pace, projected end-of-week,
|
|
1255
|
+
and a verdict (ok / warn / over). `budget set <amount>` and
|
|
1256
|
+
`budget unset` manage the budget; spend-crossing alerts fire from
|
|
1257
|
+
record-usage (see `cctally alerts`).
|
|
1258
|
+
"""
|
|
1259
|
+
),
|
|
1285
1260
|
epilog=textwrap.dedent(
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1261
|
+
"""\
|
|
1262
|
+
Examples:
|
|
1263
|
+
cctally budget
|
|
1264
|
+
cctally budget set 300
|
|
1265
|
+
cctally budget unset
|
|
1266
|
+
cctally budget set 25 --project
|
|
1267
|
+
cctally budget --json
|
|
1268
|
+
cctally budget --format md
|
|
1269
|
+
"""
|
|
1270
|
+
),
|
|
1296
1271
|
)
|
|
1297
1272
|
bg.add_argument("action", nargs="?", choices=["set", "unset"], default=None,
|
|
1298
1273
|
help="`set <amount>` to set the weekly budget, `unset` to clear it.")
|
|
@@ -1332,25 +1307,32 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1332
1307
|
_add_share_args(bg)
|
|
1333
1308
|
bg.set_defaults(func=c.cmd_budget)
|
|
1334
1309
|
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1310
|
+
def _build_percent_breakdown_parser(subparsers, name, *, help_text, xref=None):
|
|
1311
|
+
"""Build the `percent-breakdown` parser (registered via _REGISTRATION; #279 S6 W3).
|
|
1312
|
+
|
|
1313
|
+
Move-only extraction of the former inline build_parser() block;
|
|
1314
|
+
call-time `c = _cctally()` binding, --help bytes unchanged.
|
|
1315
|
+
"""
|
|
1316
|
+
c = _cctally()
|
|
1317
|
+
pb = subparsers.add_parser(
|
|
1318
|
+
name,
|
|
1319
|
+
help=help_text,
|
|
1338
1320
|
formatter_class=CLIHelpFormatter,
|
|
1339
1321
|
description=textwrap.dedent(
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1322
|
+
"""\
|
|
1323
|
+
Show the cumulative and marginal cost at each integer percent threshold
|
|
1324
|
+
for a given week. Milestones are recorded automatically when
|
|
1325
|
+
record-usage stores a snapshot crossing a new integer percent.
|
|
1326
|
+
"""
|
|
1327
|
+
),
|
|
1346
1328
|
epilog=textwrap.dedent(
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1329
|
+
"""\
|
|
1330
|
+
Examples:
|
|
1331
|
+
cctally percent-breakdown
|
|
1332
|
+
cctally percent-breakdown --week-start 2026-03-20
|
|
1333
|
+
cctally percent-breakdown --json
|
|
1334
|
+
"""
|
|
1335
|
+
),
|
|
1354
1336
|
)
|
|
1355
1337
|
pb.add_argument(
|
|
1356
1338
|
"--week-start",
|
|
@@ -1376,25 +1358,32 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1376
1358
|
)
|
|
1377
1359
|
pb.set_defaults(func=c.cmd_percent_breakdown)
|
|
1378
1360
|
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1361
|
+
def _build_five_hour_breakdown_parser(subparsers, name, *, help_text, xref=None):
|
|
1362
|
+
"""Build the `five-hour-breakdown` parser (registered via _REGISTRATION; #279 S6 W3).
|
|
1363
|
+
|
|
1364
|
+
Move-only extraction of the former inline build_parser() block;
|
|
1365
|
+
call-time `c = _cctally()` binding, --help bytes unchanged.
|
|
1366
|
+
"""
|
|
1367
|
+
c = _cctally()
|
|
1368
|
+
fhbd = subparsers.add_parser(
|
|
1369
|
+
name,
|
|
1370
|
+
help=help_text,
|
|
1382
1371
|
formatter_class=CLIHelpFormatter,
|
|
1383
1372
|
description=textwrap.dedent(
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1373
|
+
"""\
|
|
1374
|
+
Show cumulative + marginal cost at each integer percent threshold
|
|
1375
|
+
inside one 5h block. Mirrors percent-breakdown for the 5h axis.
|
|
1376
|
+
"""
|
|
1377
|
+
),
|
|
1389
1378
|
epilog=textwrap.dedent(
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1379
|
+
"""\
|
|
1380
|
+
Examples:
|
|
1381
|
+
cctally five-hour-breakdown
|
|
1382
|
+
cctally five-hour-breakdown --block-start 2026-04-30T19:30
|
|
1383
|
+
cctally five-hour-breakdown --ago 1
|
|
1384
|
+
cctally five-hour-breakdown --json
|
|
1385
|
+
"""
|
|
1386
|
+
),
|
|
1398
1387
|
)
|
|
1399
1388
|
fhbd.add_argument(
|
|
1400
1389
|
"--block-start",
|
|
@@ -1430,33 +1419,40 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1430
1419
|
)
|
|
1431
1420
|
fhbd.set_defaults(func=c.cmd_five_hour_breakdown)
|
|
1432
1421
|
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1422
|
+
def _build_tui_parser(subparsers, name, *, help_text, xref=None):
|
|
1423
|
+
"""Build the `tui` parser (registered via _REGISTRATION; #279 S6 W3).
|
|
1424
|
+
|
|
1425
|
+
Move-only extraction of the former inline build_parser() block;
|
|
1426
|
+
call-time `c = _cctally()` binding, --help bytes unchanged.
|
|
1427
|
+
"""
|
|
1428
|
+
c = _cctally()
|
|
1429
|
+
tp = subparsers.add_parser(
|
|
1430
|
+
name,
|
|
1431
|
+
help=help_text,
|
|
1436
1432
|
formatter_class=CLIHelpFormatter,
|
|
1437
1433
|
description=textwrap.dedent(
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1434
|
+
"""\
|
|
1435
|
+
Live terminal dashboard with four refreshing panels:
|
|
1436
|
+
- Current week % and 5-hour window
|
|
1437
|
+
- Forecast verdict + projections + daily $ budgets
|
|
1438
|
+
- $/1% trend over the last 8 weeks (with sparkline)
|
|
1439
|
+
- Recent Claude sessions (last 100, scrollable)
|
|
1440
|
+
|
|
1441
|
+
Two visual variants — conventional 2x2 grid and expressive
|
|
1442
|
+
hero layout — toggleable at runtime with `v`.
|
|
1443
|
+
|
|
1444
|
+
Requires the `rich` Python package.
|
|
1445
|
+
"""
|
|
1446
|
+
),
|
|
1451
1447
|
epilog=textwrap.dedent(
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1448
|
+
"""\
|
|
1449
|
+
Examples:
|
|
1450
|
+
cctally tui
|
|
1451
|
+
cctally tui --expressive
|
|
1452
|
+
cctally tui --refresh 2 --sync-interval 30
|
|
1453
|
+
cctally tui --no-sync
|
|
1454
|
+
"""
|
|
1455
|
+
),
|
|
1460
1456
|
)
|
|
1461
1457
|
tp.add_argument(
|
|
1462
1458
|
"--variant",
|
|
@@ -1506,34 +1502,33 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1506
1502
|
help="Display timezone: local, utc, or IANA name. "
|
|
1507
1503
|
"Overrides config display.tz for this call.",
|
|
1508
1504
|
)
|
|
1509
|
-
# Dev-only: pin "now" for deterministic fixture tests.
|
|
1510
1505
|
tp.add_argument("--as-of", dest="as_of", default=None, help=argparse.SUPPRESS)
|
|
1511
|
-
# Dev-only: fixture injection — render one frame from a Python module that
|
|
1512
|
-
# exposes `SNAPSHOT` (DataSnapshot) and exits.
|
|
1513
1506
|
tp.add_argument(
|
|
1514
1507
|
"--snapshot-module", dest="snapshot_module", default=None, help=argparse.SUPPRESS
|
|
1515
1508
|
)
|
|
1516
|
-
# Dev-only: one-shot render for golden capture.
|
|
1517
1509
|
tp.add_argument(
|
|
1518
1510
|
"--render-once", action="store_true", dest="render_once", help=argparse.SUPPRESS
|
|
1519
1511
|
)
|
|
1520
|
-
# Dev-only: force terminal size for --render-once.
|
|
1521
1512
|
tp.add_argument(
|
|
1522
1513
|
"--force-size", dest="force_size", default=None, metavar="WxH",
|
|
1523
1514
|
help=argparse.SUPPRESS
|
|
1524
1515
|
)
|
|
1525
1516
|
tp.set_defaults(func=c.cmd_tui)
|
|
1526
1517
|
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1518
|
+
def _build_dashboard_parser(subparsers, name, *, help_text, xref=None):
|
|
1519
|
+
"""Build the `dashboard` parser (registered via _REGISTRATION; #279 S6 W3).
|
|
1520
|
+
|
|
1521
|
+
Move-only extraction of the former inline build_parser() block;
|
|
1522
|
+
call-time `c = _cctally()` binding, --help bytes unchanged.
|
|
1523
|
+
"""
|
|
1524
|
+
c = _cctally()
|
|
1525
|
+
dp = subparsers.add_parser(
|
|
1526
|
+
name,
|
|
1527
|
+
help=help_text,
|
|
1528
|
+
description="Start a local web server rendering a live dashboard of your "
|
|
1529
|
+
"subscription usage, weekly cost trend, and recent sessions. "
|
|
1530
|
+
"Press Ctrl-C to stop. Two variants are served by the companion "
|
|
1531
|
+
"'tui' subcommand for terminal-only use.",
|
|
1537
1532
|
)
|
|
1538
1533
|
dp.add_argument(
|
|
1539
1534
|
"--port",
|
|
@@ -1573,33 +1568,40 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1573
1568
|
)
|
|
1574
1569
|
dp.set_defaults(func=c.cmd_dashboard)
|
|
1575
1570
|
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1571
|
+
def _build_record_usage_parser(subparsers, name, *, help_text, xref=None):
|
|
1572
|
+
"""Build the `record-usage` parser (registered via _REGISTRATION; #279 S6 W3).
|
|
1573
|
+
|
|
1574
|
+
Move-only extraction of the former inline build_parser() block;
|
|
1575
|
+
call-time `c = _cctally()` binding, --help bytes unchanged.
|
|
1576
|
+
"""
|
|
1577
|
+
c = _cctally()
|
|
1578
|
+
ru = subparsers.add_parser(
|
|
1579
|
+
name,
|
|
1580
|
+
help=help_text,
|
|
1579
1581
|
formatter_class=CLIHelpFormatter,
|
|
1580
1582
|
description=textwrap.dedent(
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1583
|
+
"""\
|
|
1584
|
+
Record usage percentage from Claude Code status line rate_limits data.
|
|
1585
|
+
Called automatically by the status line script after each assistant message.
|
|
1586
|
+
"""
|
|
1587
|
+
),
|
|
1586
1588
|
epilog=textwrap.dedent(
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1589
|
+
"""\
|
|
1590
|
+
Examples:
|
|
1591
|
+
cctally record-usage --percent 14.2 --resets-at 1744531200
|
|
1592
|
+
cctally record-usage --percent 14.2 --resets-at 1744531200 \\
|
|
1593
|
+
--five-hour-percent 38.5 --five-hour-resets-at 1744502400
|
|
1594
|
+
|
|
1595
|
+
Status line integration (add to ~/.claude/statusline-command.sh):
|
|
1596
|
+
if [ -n "$week_pct" ] && [ -n "$week_resets" ]; then
|
|
1597
|
+
record_args="--percent $week_pct --resets-at ${week_resets%.*}"
|
|
1598
|
+
if [ -n "$five_pct" ] && [ -n "$five_resets" ]; then
|
|
1599
|
+
record_args="$record_args --five-hour-percent $five_pct --five-hour-resets-at ${five_resets%.*}"
|
|
1600
|
+
fi
|
|
1601
|
+
cctally record-usage $record_args &
|
|
1602
|
+
fi
|
|
1603
|
+
"""
|
|
1604
|
+
),
|
|
1603
1605
|
)
|
|
1604
1606
|
ru.add_argument(
|
|
1605
1607
|
"--percent",
|
|
@@ -1625,32 +1627,39 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1625
1627
|
)
|
|
1626
1628
|
ru.set_defaults(func=c.cmd_record_usage)
|
|
1627
1629
|
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1630
|
+
def _build_record_credit_parser(subparsers, name, *, help_text, xref=None):
|
|
1631
|
+
"""Build the `record-credit` parser (registered via _REGISTRATION; #279 S6 W3).
|
|
1632
|
+
|
|
1633
|
+
Move-only extraction of the former inline build_parser() block;
|
|
1634
|
+
call-time `c = _cctally()` binding, --help bytes unchanged.
|
|
1635
|
+
"""
|
|
1636
|
+
c = _cctally()
|
|
1637
|
+
rc = subparsers.add_parser(
|
|
1638
|
+
name,
|
|
1639
|
+
help=help_text,
|
|
1631
1640
|
formatter_class=CLIHelpFormatter,
|
|
1632
1641
|
description=textwrap.dedent(
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1642
|
+
"""\
|
|
1643
|
+
Record an in-place weekly (7d) credit that the auto-detector
|
|
1644
|
+
misses (a sub-25pp, non-zero drop — e.g. Anthropic lowered your
|
|
1645
|
+
7d % from 46 to 31 without a clean reset). Writes a
|
|
1646
|
+
weekly_credit_floors clamp row (no week re-anchor — the same week
|
|
1647
|
+
continues), lowers hwm-7d, and inserts a post-credit snapshot so
|
|
1648
|
+
reports and the statusline read the credited value.
|
|
1649
|
+
Preview + confirm by default.
|
|
1650
|
+
"""
|
|
1651
|
+
),
|
|
1643
1652
|
epilog=textwrap.dedent(
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1653
|
+
"""\
|
|
1654
|
+
Examples:
|
|
1655
|
+
cctally record-credit --to 31 # baseline auto-read from HWM
|
|
1656
|
+
cctally record-credit --to 31 --dry-run # preview, write nothing
|
|
1657
|
+
cctally record-credit --to 31 --yes # apply without prompting
|
|
1649
1658
|
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1659
|
+
Exit codes: 0 success (incl. --dry-run and an interactive decline),
|
|
1660
|
+
2 validation/refuse, 3 on a database error.
|
|
1661
|
+
"""
|
|
1662
|
+
),
|
|
1654
1663
|
)
|
|
1655
1664
|
rc.add_argument("--to", required=True, type=float,
|
|
1656
1665
|
help="New post-credit weekly %% (0-100).")
|
|
@@ -1670,36 +1679,43 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1670
1679
|
help="Machine output (schemaVersion 1).")
|
|
1671
1680
|
rc.set_defaults(func=c.cmd_record_credit)
|
|
1672
1681
|
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1682
|
+
def _build_refresh_usage_parser(subparsers, name, *, help_text, xref=None):
|
|
1683
|
+
"""Build the `refresh-usage` parser (registered via _REGISTRATION; #279 S6 W3).
|
|
1684
|
+
|
|
1685
|
+
Move-only extraction of the former inline build_parser() block;
|
|
1686
|
+
call-time `c = _cctally()` binding, --help bytes unchanged.
|
|
1687
|
+
"""
|
|
1688
|
+
c = _cctally()
|
|
1689
|
+
rfu = subparsers.add_parser(
|
|
1690
|
+
name,
|
|
1691
|
+
help=help_text,
|
|
1676
1692
|
formatter_class=CLIHelpFormatter,
|
|
1677
1693
|
description=textwrap.dedent(
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1694
|
+
"""\
|
|
1695
|
+
Force a fresh fetch of seven_day.utilization and five_hour.utilization
|
|
1696
|
+
from Anthropic's OAuth usage API, persist it via the same path
|
|
1697
|
+
record-usage uses (HWM, percent_milestones, weekly_usage_snapshots),
|
|
1698
|
+
and bust the statusline OAuth cache file at
|
|
1699
|
+
/tmp/claude-statusline-usage-cache.json so the next status-line tick
|
|
1700
|
+
also gets fresh data.
|
|
1701
|
+
|
|
1702
|
+
Use this when the displayed 7d percent is stale (e.g., you've
|
|
1703
|
+
been away from Claude Code and the status-line hasn't fired
|
|
1704
|
+
recently). Otherwise the status-line script handles refresh
|
|
1705
|
+
automatically every minute.
|
|
1706
|
+
"""
|
|
1707
|
+
),
|
|
1692
1708
|
epilog=textwrap.dedent(
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1709
|
+
"""\
|
|
1710
|
+
Examples:
|
|
1711
|
+
ccusage-refresh-usage # one-liner output
|
|
1712
|
+
ccusage-refresh-usage --json | jq . # scriptable
|
|
1713
|
+
ccusage-refresh-usage --quiet # silent (exit code only)
|
|
1698
1714
|
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1715
|
+
Exit codes: 0 success / 2 no OAuth token / 3 network failure
|
|
1716
|
+
/ 4 malformed API response / 5 record-usage internal failure.
|
|
1717
|
+
"""
|
|
1718
|
+
),
|
|
1703
1719
|
)
|
|
1704
1720
|
rfu.add_argument("--json", action="store_true",
|
|
1705
1721
|
help="Emit schema_version=1 JSON to stdout instead of one-liner.")
|
|
@@ -1711,30 +1727,37 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1711
1727
|
help="HTTP timeout in seconds (default: 5.0).")
|
|
1712
1728
|
rfu.set_defaults(func=c.cmd_refresh_usage)
|
|
1713
1729
|
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1730
|
+
def _build_cache_report_parser(subparsers, name, *, help_text, xref=None):
|
|
1731
|
+
"""Build the `cache-report` parser (registered via _REGISTRATION; #279 S6 W3).
|
|
1732
|
+
|
|
1733
|
+
Move-only extraction of the former inline build_parser() block;
|
|
1734
|
+
call-time `c = _cctally()` binding, --help bytes unchanged.
|
|
1735
|
+
"""
|
|
1736
|
+
c = _cctally()
|
|
1737
|
+
pc = subparsers.add_parser(
|
|
1738
|
+
name,
|
|
1739
|
+
help=help_text,
|
|
1717
1740
|
formatter_class=CLIHelpFormatter,
|
|
1718
1741
|
description=textwrap.dedent(
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1742
|
+
"""\
|
|
1743
|
+
Query ccusage for daily token breakdown and display cache hit
|
|
1744
|
+
percentages per model. Useful for spotting caching regressions
|
|
1745
|
+
after Claude Code updates.
|
|
1746
|
+
|
|
1747
|
+
Cache hit % = cacheReadTokens / (input + cacheCreate + cacheRead)
|
|
1748
|
+
"""
|
|
1749
|
+
),
|
|
1727
1750
|
epilog=textwrap.dedent(
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1751
|
+
"""\
|
|
1752
|
+
Examples:
|
|
1753
|
+
cctally cache-report
|
|
1754
|
+
cctally cache-report --days 14
|
|
1755
|
+
cctally cache-report --since 2026-04-10 --until 2026-04-18
|
|
1756
|
+
cctally cache-report --by-session --days 14
|
|
1757
|
+
cctally cache-report --by-session --sort cache
|
|
1758
|
+
cctally cache-report --json
|
|
1759
|
+
"""
|
|
1760
|
+
),
|
|
1738
1761
|
)
|
|
1739
1762
|
pc.add_argument(
|
|
1740
1763
|
"--days",
|
|
@@ -1815,25 +1838,28 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1815
1838
|
help="Display timezone: local, utc, or IANA name. "
|
|
1816
1839
|
"Overrides config display.tz for this call.",
|
|
1817
1840
|
)
|
|
1818
|
-
# Session A (spec §7.6): ansi_emit=False; existing `--offline` is
|
|
1819
|
-
# skipped by the helper's `_argparse_has_arg` guard (the collision
|
|
1820
|
-
# case spec §7.1.2 calls out explicitly).
|
|
1821
1841
|
_add_ccusage_alias_args(pc, ansi_emit=False)
|
|
1822
1842
|
pc.set_defaults(func=c.cmd_cache_report)
|
|
1823
1843
|
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1844
|
+
def _build_range_cost_parser(subparsers, name, *, help_text, xref=None):
|
|
1845
|
+
"""Build the `range-cost` parser (registered via _REGISTRATION; #279 S6 W3).
|
|
1846
|
+
|
|
1847
|
+
Move-only extraction of the former inline build_parser() block;
|
|
1848
|
+
call-time `c = _cctally()` binding, --help bytes unchanged.
|
|
1849
|
+
"""
|
|
1850
|
+
c = _cctally()
|
|
1851
|
+
rc = subparsers.add_parser(
|
|
1852
|
+
name,
|
|
1853
|
+
help=help_text,
|
|
1828
1854
|
formatter_class=CLIHelpFormatter,
|
|
1829
1855
|
description="Compute USD cost for Claude Code usage between start/end timestamps.",
|
|
1830
1856
|
epilog=textwrap.dedent("""\
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1857
|
+
Examples:
|
|
1858
|
+
cctally range-cost -s "2026-04-10T10:00:00+03:00"
|
|
1859
|
+
cctally range-cost -s "2026-04-10T10:00:00Z" -e "2026-04-12T10:00:00Z" --breakdown
|
|
1860
|
+
cctally range-cost -s "2026-04-10T10:00:00Z" --json
|
|
1861
|
+
cctally range-cost -s "2026-04-10T10:00:00Z" --total-only
|
|
1862
|
+
"""),
|
|
1837
1863
|
)
|
|
1838
1864
|
rc.add_argument(
|
|
1839
1865
|
"-s", "--start",
|
|
@@ -1873,55 +1899,34 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1873
1899
|
dest="total_only",
|
|
1874
1900
|
help="Print numeric USD total only.",
|
|
1875
1901
|
)
|
|
1876
|
-
# Session A (spec §7.6): ansi_emit=False. range-cost has no --tz of
|
|
1877
|
-
# its own (ISO timestamps carry zone info), but the helper-added
|
|
1878
|
-
# -z/--timezone still lands on the namespace; the bridge promotes
|
|
1879
|
-
# it onto args.tz where the rest of the pipeline treats it as a
|
|
1880
|
-
# documented no-op (cmd_range_cost does not consume args.tz).
|
|
1881
1902
|
_add_ccusage_alias_args(rc, ansi_emit=False)
|
|
1882
1903
|
rc.set_defaults(func=c.cmd_range_cost)
|
|
1883
1904
|
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
sub, "blocks",
|
|
1887
|
-
help_text="Show usage report grouped by 5-hour session blocks",
|
|
1888
|
-
xref="Alias of `cctally claude blocks` (the canonical form).")
|
|
1905
|
+
def _build_five_hour_blocks_parser(subparsers, name, *, help_text, xref=None):
|
|
1906
|
+
"""Build the `five-hour-blocks` parser (registered via _REGISTRATION; #279 S6 W3).
|
|
1889
1907
|
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
fhb = sub.add_parser(
|
|
1898
|
-
"five-hour-blocks",
|
|
1899
|
-
help="List API-anchored 5h blocks with rollup totals + 7d-drift columns",
|
|
1908
|
+
Move-only extraction of the former inline build_parser() block;
|
|
1909
|
+
call-time `c = _cctally()` binding, --help bytes unchanged.
|
|
1910
|
+
"""
|
|
1911
|
+
c = _cctally()
|
|
1912
|
+
fhb = subparsers.add_parser(
|
|
1913
|
+
name,
|
|
1914
|
+
help=help_text,
|
|
1900
1915
|
formatter_class=CLIHelpFormatter,
|
|
1901
|
-
description=(
|
|
1902
|
-
|
|
1903
|
-
"distinct from `cctally blocks` upstream-parity drop-in)."
|
|
1904
|
-
),
|
|
1916
|
+
description="Show usage grouped by API-anchored 5-hour blocks (analytics view, "
|
|
1917
|
+
"distinct from `cctally blocks` upstream-parity drop-in).",
|
|
1905
1918
|
epilog=textwrap.dedent("""\
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
)
|
|
1913
|
-
|
|
1914
|
-
"
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
help="Filter from date (inclusive).",
|
|
1918
|
-
)
|
|
1919
|
-
fhb.add_argument(
|
|
1920
|
-
"-u", "--until",
|
|
1921
|
-
default=None,
|
|
1922
|
-
metavar="YYYYMMDD",
|
|
1923
|
-
help="Filter until date (inclusive).",
|
|
1924
|
-
)
|
|
1919
|
+
Examples:
|
|
1920
|
+
cctally five-hour-blocks
|
|
1921
|
+
cctally five-hour-blocks --since 20260420
|
|
1922
|
+
cctally five-hour-blocks --breakdown model
|
|
1923
|
+
cctally five-hour-blocks --breakdown project --json
|
|
1924
|
+
"""),
|
|
1925
|
+
)
|
|
1926
|
+
_add_since_until_args(
|
|
1927
|
+
fhb, metavar_since="YYYYMMDD", metavar_until="YYYYMMDD",
|
|
1928
|
+
help_since="Filter from date (inclusive).",
|
|
1929
|
+
help_until="Filter until date (inclusive).")
|
|
1925
1930
|
fhb.add_argument(
|
|
1926
1931
|
"--breakdown",
|
|
1927
1932
|
choices=("model", "project"),
|
|
@@ -1949,20 +1954,21 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1949
1954
|
help="Display timezone: local, utc, or IANA name. "
|
|
1950
1955
|
"Overrides config display.tz for this call.",
|
|
1951
1956
|
)
|
|
1952
|
-
# Session A (spec §7.6 / §7.6.3): ansi_emit=False. fhb already
|
|
1953
|
-
# declares --no-color (refreshed to no-op text in spec §7.6.3); the
|
|
1954
|
-
# helper's --no-color add is short-circuited by the existing-arg
|
|
1955
|
-
# guard. The helper's --color add lands as a parsed-and-ignored
|
|
1956
|
-
# no-op (the renderer emits plain text).
|
|
1957
1957
|
_add_ccusage_alias_args(fhb, ansi_emit=False)
|
|
1958
1958
|
_add_mode_arg(fhb, noop=True)
|
|
1959
1959
|
_add_share_args(fhb)
|
|
1960
1960
|
fhb.set_defaults(func=c.cmd_five_hour_blocks)
|
|
1961
1961
|
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1962
|
+
def _build_cache_sync_parser(subparsers, name, *, help_text, xref=None):
|
|
1963
|
+
"""Build the `cache-sync` parser (registered via _REGISTRATION; #279 S6 W3).
|
|
1964
|
+
|
|
1965
|
+
Move-only extraction of the former inline build_parser() block;
|
|
1966
|
+
call-time `c = _cctally()` binding, --help bytes unchanged.
|
|
1967
|
+
"""
|
|
1968
|
+
c = _cctally()
|
|
1969
|
+
p_cache_sync = subparsers.add_parser(
|
|
1970
|
+
name,
|
|
1971
|
+
help=help_text,
|
|
1966
1972
|
)
|
|
1967
1973
|
p_cache_sync.add_argument(
|
|
1968
1974
|
"--rebuild",
|
|
@@ -1983,71 +1989,33 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1983
1989
|
)
|
|
1984
1990
|
p_cache_sync.set_defaults(func=c.cmd_cache_sync)
|
|
1985
1991
|
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
help_text
|
|
1996
|
-
xref="Alias of `cctally claude monthly` (the canonical form).")
|
|
1997
|
-
|
|
1998
|
-
# -- weekly --
|
|
1999
|
-
_build_weekly_parser(
|
|
2000
|
-
sub, "weekly",
|
|
2001
|
-
help_text="Show usage grouped by subscription week (with Used %% and $/1%%)",
|
|
2002
|
-
xref="Alias of `cctally claude weekly` (the canonical form).")
|
|
2003
|
-
|
|
2004
|
-
# -- codex-daily --
|
|
2005
|
-
_build_codex_daily_parser(
|
|
2006
|
-
sub, "codex-daily",
|
|
2007
|
-
help_text="Show Codex usage report grouped by date (drop-in for `ccusage-codex daily`)",
|
|
2008
|
-
xref="Alias of `cctally codex daily` (the canonical form).")
|
|
2009
|
-
|
|
2010
|
-
# -- codex-monthly --
|
|
2011
|
-
_build_codex_monthly_parser(
|
|
2012
|
-
sub, "codex-monthly",
|
|
2013
|
-
help_text="Show Codex usage grouped by month (drop-in for `ccusage-codex monthly`)",
|
|
2014
|
-
xref="Alias of `cctally codex monthly` (the canonical form).")
|
|
2015
|
-
|
|
2016
|
-
# -- codex-weekly --
|
|
2017
|
-
_build_codex_weekly_parser(
|
|
2018
|
-
sub, "codex-weekly",
|
|
2019
|
-
help_text="Show Codex usage grouped by week (week-start from config.json)",
|
|
2020
|
-
xref="Alias of `cctally codex weekly` (the canonical form).")
|
|
2021
|
-
|
|
2022
|
-
# -- codex-session --
|
|
2023
|
-
_build_codex_session_parser(
|
|
2024
|
-
sub, "codex-session",
|
|
2025
|
-
help_text="Show Codex usage grouped by session (drop-in for `ccusage-codex session`)",
|
|
2026
|
-
xref="Alias of `cctally codex session` (the canonical form).")
|
|
2027
|
-
|
|
2028
|
-
# -- project --
|
|
2029
|
-
p_project = sub.add_parser(
|
|
2030
|
-
"project",
|
|
2031
|
-
help="Roll usage up by project (git-root), with per-project Used %% attribution",
|
|
1992
|
+
def _build_project_parser(subparsers, name, *, help_text, xref=None):
|
|
1993
|
+
"""Build the `project` parser (registered via _REGISTRATION; #279 S6 W3).
|
|
1994
|
+
|
|
1995
|
+
Move-only extraction of the former inline build_parser() block;
|
|
1996
|
+
call-time `c = _cctally()` binding, --help bytes unchanged.
|
|
1997
|
+
"""
|
|
1998
|
+
c = _cctally()
|
|
1999
|
+
p_project = subparsers.add_parser(
|
|
2000
|
+
name,
|
|
2001
|
+
help=help_text,
|
|
2032
2002
|
formatter_class=CLIHelpFormatter,
|
|
2033
|
-
description=(
|
|
2034
|
-
|
|
2035
|
-
"the current subscription week; use --since/--until or --weeks N to extend."
|
|
2036
|
-
),
|
|
2003
|
+
description="Aggregate Claude usage by project (git-root resolved). Default range is "
|
|
2004
|
+
"the current subscription week; use --since/--until or --weeks N to extend.",
|
|
2037
2005
|
epilog=textwrap.dedent("""\
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
)
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2006
|
+
Examples:
|
|
2007
|
+
cctally project
|
|
2008
|
+
cctally project --weeks 4
|
|
2009
|
+
cctally project --since 20260401 --until 20260414
|
|
2010
|
+
cctally project --project ccusage --model sonnet
|
|
2011
|
+
cctally project --breakdown --sort used --order desc
|
|
2012
|
+
cctally project --group full-path --json
|
|
2013
|
+
"""),
|
|
2014
|
+
)
|
|
2015
|
+
_add_since_until_args(
|
|
2016
|
+
p_project, metavar_since="YYYYMMDD", metavar_until="YYYYMMDD",
|
|
2017
|
+
help_since="Inclusive start date (YYYY-MM-DD or YYYYMMDD).",
|
|
2018
|
+
help_until="Inclusive end date (YYYY-MM-DD or YYYYMMDD).")
|
|
2051
2019
|
p_project.add_argument("--weeks", type=int, default=None,
|
|
2052
2020
|
help="Last N subscription weeks ending now.")
|
|
2053
2021
|
p_project.add_argument("--project", action="append", default=[], metavar="PATTERN",
|
|
@@ -2071,18 +2039,20 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
2071
2039
|
p_project.add_argument("--tz", default=None, type=_argparse_tz, metavar="TZ",
|
|
2072
2040
|
help="Display timezone: local, utc, or IANA name. "
|
|
2073
2041
|
"Overrides config display.tz for this call.")
|
|
2074
|
-
# Session A (spec §7.6): ansi_emit=True. project is one of the two
|
|
2075
|
-
# real ANSI emitters. The helper skips its --no-color add (already
|
|
2076
|
-
# declared at p_project above) and adds the new bool --color flag
|
|
2077
|
-
# whose precedence flows through _resolve_color_enabled (§7.3).
|
|
2078
2042
|
_add_ccusage_alias_args(p_project, ansi_emit=True)
|
|
2079
2043
|
_add_share_args(p_project)
|
|
2080
2044
|
p_project.set_defaults(func=c.cmd_project)
|
|
2081
2045
|
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2046
|
+
def _build_diff_parser(subparsers, name, *, help_text, xref=None):
|
|
2047
|
+
"""Build the `diff` parser (registered via _REGISTRATION; #279 S6 W3).
|
|
2048
|
+
|
|
2049
|
+
Move-only extraction of the former inline build_parser() block;
|
|
2050
|
+
call-time `c = _cctally()` binding, --help bytes unchanged.
|
|
2051
|
+
"""
|
|
2052
|
+
c = _cctally()
|
|
2053
|
+
diff_p = subparsers.add_parser(
|
|
2054
|
+
name,
|
|
2055
|
+
help=help_text,
|
|
2086
2056
|
)
|
|
2087
2057
|
diff_p.add_argument("--a", required=True,
|
|
2088
2058
|
help="Window A token (this-week | last-week | Nw-ago | this-month | last-month | Nm-ago | last-Nd | prev-Nd | YYYY-MM-DD..YYYY-MM-DD)")
|
|
@@ -2110,34 +2080,34 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
2110
2080
|
diff_p.add_argument("--json", dest="emit_json", action="store_true")
|
|
2111
2081
|
diff_p.add_argument("--width", type=int, help=argparse.SUPPRESS)
|
|
2112
2082
|
diff_p.add_argument("--debug-now", action="store_true", help=argparse.SUPPRESS)
|
|
2113
|
-
# Session A (spec §7.6): ansi_emit=True. diff is the other real
|
|
2114
|
-
# ANSI emitter. The helper skips its --no-color add (declared
|
|
2115
|
-
# above) and adds the new bool --color flag wired through
|
|
2116
|
-
# _resolve_color_enabled (§7.3). Note: --debug here collides with
|
|
2117
|
-
# diff's existing `--debug-now` (SUPPRESS'd internal flag) but
|
|
2118
|
-
# `--debug-now` is a different option string; the helper still
|
|
2119
|
-
# adds plain `--debug` cleanly.
|
|
2120
2083
|
_add_ccusage_alias_args(diff_p, ansi_emit=True)
|
|
2121
2084
|
diff_p.set_defaults(func=c.cmd_diff)
|
|
2122
2085
|
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2086
|
+
def _build_claude_parser(subparsers, name, *, help_text, xref=None):
|
|
2087
|
+
"""Build the `claude` parser (registered via _REGISTRATION; #279 S6 W3).
|
|
2088
|
+
|
|
2089
|
+
Move-only extraction of the former inline build_parser() block;
|
|
2090
|
+
call-time `c = _cctally()` binding, --help bytes unchanged.
|
|
2091
|
+
|
|
2092
|
+
Build-once, register-twice: reuses the same leaf builders as the flat
|
|
2093
|
+
forms. The nested subparsers deliberately reuse dest="command" so
|
|
2094
|
+
args.command resolves to the LEAF name (e.g. "blocks") — this
|
|
2095
|
+
leaf-collapse keeps `_recompute_banner_should_emit` (_cctally_db.py,
|
|
2096
|
+
raw sys.argv routing that hardcodes claude/codex as the only
|
|
2097
|
+
subgroups) and `_post_command_update_hooks` (bin/cctally)
|
|
2098
|
+
byte-identical between flat and nested forms. Pinned by
|
|
2099
|
+
tests/test_subgroup_routing.py; adding a new subgroup means updating
|
|
2100
|
+
that raw-argv routing too.
|
|
2101
|
+
"""
|
|
2102
|
+
c = _cctally()
|
|
2103
|
+
claude_p = subparsers.add_parser(
|
|
2104
|
+
name,
|
|
2105
|
+
help=help_text,
|
|
2137
2106
|
formatter_class=CLIHelpFormatter,
|
|
2138
2107
|
description="Claude-source usage reports. Each subcommand is a drop-in for the "
|
|
2139
|
-
|
|
2140
|
-
|
|
2108
|
+
"matching `ccusage claude <cmd>` and shares its engine with the "
|
|
2109
|
+
"top-level `cctally <cmd>` alias.",
|
|
2110
|
+
)
|
|
2141
2111
|
claude_sub = claude_p.add_subparsers(dest="command", required=True, metavar="<command>")
|
|
2142
2112
|
_build_daily_parser(claude_sub, "daily",
|
|
2143
2113
|
help_text="Show usage grouped by date",
|
|
@@ -2159,14 +2129,26 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
2159
2129
|
xref="Canonical `cctally claude statusline` (flat alias: `cctally statusline`). "
|
|
2160
2130
|
"Drop-in for `ccusage statusline` plus cctally extension segments.")
|
|
2161
2131
|
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2132
|
+
def _build_codex_parser(subparsers, name, *, help_text, xref=None):
|
|
2133
|
+
"""Build the `codex` parser (registered via _REGISTRATION; #279 S6 W3).
|
|
2134
|
+
|
|
2135
|
+
Move-only extraction of the former inline build_parser() block;
|
|
2136
|
+
call-time `c = _cctally()` binding, --help bytes unchanged.
|
|
2137
|
+
|
|
2138
|
+
Same build-once/register-twice + dest="command" leaf-collapse coupling
|
|
2139
|
+
as `_build_claude_parser` — see its docstring for the
|
|
2140
|
+
`_recompute_banner_should_emit` / `_post_command_update_hooks`
|
|
2141
|
+
cross-reference.
|
|
2142
|
+
"""
|
|
2143
|
+
c = _cctally()
|
|
2144
|
+
codex_p = subparsers.add_parser(
|
|
2145
|
+
name,
|
|
2146
|
+
help=help_text,
|
|
2166
2147
|
formatter_class=CLIHelpFormatter,
|
|
2167
2148
|
description="Codex-source usage reports. daily/monthly/session are drop-ins for "
|
|
2168
|
-
|
|
2169
|
-
|
|
2149
|
+
"`ccusage codex <cmd>`; weekly is a cctally extension. Each shares its "
|
|
2150
|
+
"engine with the matching `cctally codex-<cmd>` alias.",
|
|
2151
|
+
)
|
|
2170
2152
|
codex_sub = codex_p.add_subparsers(dest="command", required=True, metavar="<command>")
|
|
2171
2153
|
_build_codex_daily_parser(codex_sub, "daily",
|
|
2172
2154
|
help_text="Show Codex usage grouped by date",
|
|
@@ -2182,32 +2164,38 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
2182
2164
|
xref="cctally extension (no upstream `ccusage codex weekly`). Same engine as "
|
|
2183
2165
|
"`cctally codex-weekly`.")
|
|
2184
2166
|
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2167
|
+
def _build_config_parser(subparsers, name, *, help_text, xref=None):
|
|
2168
|
+
"""Build the `config` parser (registered via _REGISTRATION; #279 S6 W3).
|
|
2169
|
+
|
|
2170
|
+
Move-only extraction of the former inline build_parser() block;
|
|
2171
|
+
call-time `c = _cctally()` binding, --help bytes unchanged.
|
|
2172
|
+
"""
|
|
2173
|
+
c = _cctally()
|
|
2174
|
+
cfg_p = subparsers.add_parser(
|
|
2175
|
+
name,
|
|
2176
|
+
help=help_text,
|
|
2189
2177
|
formatter_class=CLIHelpFormatter,
|
|
2190
2178
|
description=textwrap.dedent("""\
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2179
|
+
Manage cctally user preferences in ~/.local/share/cctally/config.json.
|
|
2180
|
+
|
|
2181
|
+
Currently supported keys:
|
|
2182
|
+
display.tz Display timezone. Values: 'local' (default; host
|
|
2183
|
+
zone via the OS locale), 'utc', or any IANA name
|
|
2184
|
+
like 'America/New_York'. Per-call --tz flag on
|
|
2185
|
+
any subcommand still wins over the persisted value.
|
|
2186
|
+
alerts.enabled Enable/disable threshold alerts (true/false).
|
|
2187
|
+
dashboard.bind Host the `dashboard` subcommand binds. Values:
|
|
2188
|
+
'loopback' (default; binds 127.0.0.1 —
|
|
2189
|
+
loopback-only), 'lan' (binds 0.0.0.0 —
|
|
2190
|
+
LAN-accessible), or any literal IP / hostname.
|
|
2191
|
+
|
|
2192
|
+
Examples:
|
|
2193
|
+
cctally config get
|
|
2194
|
+
cctally config get display.tz
|
|
2195
|
+
cctally config set display.tz America/New_York
|
|
2196
|
+
cctally config set dashboard.bind lan
|
|
2197
|
+
cctally config unset dashboard.bind
|
|
2198
|
+
"""),
|
|
2211
2199
|
)
|
|
2212
2200
|
cfg_sub = cfg_p.add_subparsers(dest="action", required=True)
|
|
2213
2201
|
cfg_get = cfg_sub.add_parser("get", help="Print current value(s)")
|
|
@@ -2225,36 +2213,42 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
2225
2213
|
cfg_unset.add_argument("key", help="Config key")
|
|
2226
2214
|
cfg_unset.set_defaults(func=c.cmd_config)
|
|
2227
2215
|
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2216
|
+
def _build_telemetry_parser(subparsers, name, *, help_text, xref=None):
|
|
2217
|
+
"""Build the `telemetry` parser (registered via _REGISTRATION; #279 S6 W3).
|
|
2218
|
+
|
|
2219
|
+
Move-only extraction of the former inline build_parser() block;
|
|
2220
|
+
call-time `c = _cctally()` binding, --help bytes unchanged.
|
|
2221
|
+
"""
|
|
2222
|
+
c = _cctally()
|
|
2223
|
+
tele_p = subparsers.add_parser(
|
|
2224
|
+
name,
|
|
2225
|
+
help=help_text,
|
|
2232
2226
|
formatter_class=CLIHelpFormatter,
|
|
2233
2227
|
description=textwrap.dedent("""\
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2228
|
+
Anonymous install-count telemetry: what it sends, and how to opt out.
|
|
2229
|
+
|
|
2230
|
+
cctally sends, at most once a day, a minimal beat: a one-way
|
|
2231
|
+
month-rotating token (never your install id), the client version,
|
|
2232
|
+
and a coarse OS family (macos/linux/windows/other). No IP, no
|
|
2233
|
+
username, no paths, no session content ever leaves the machine.
|
|
2234
|
+
|
|
2235
|
+
Actions:
|
|
2236
|
+
(none) Show the current state, resolved reason, what gets sent,
|
|
2237
|
+
and the token that would be used this month.
|
|
2238
|
+
on Enable telemetry (sets telemetry.enabled = true).
|
|
2239
|
+
off Disable telemetry (sets telemetry.enabled = false).
|
|
2240
|
+
reset Discard the local install id and mint a fresh one.
|
|
2241
|
+
|
|
2242
|
+
It is also disabled by CCTALLY_DISABLE_TELEMETRY=1, the DO_NOT_TRACK
|
|
2243
|
+
convention, and in dev checkouts.
|
|
2244
|
+
|
|
2245
|
+
Examples:
|
|
2246
|
+
cctally telemetry
|
|
2247
|
+
cctally telemetry --json
|
|
2248
|
+
cctally telemetry off
|
|
2249
|
+
cctally telemetry on
|
|
2250
|
+
cctally telemetry reset
|
|
2251
|
+
"""),
|
|
2258
2252
|
)
|
|
2259
2253
|
tele_p.add_argument(
|
|
2260
2254
|
"action", nargs="?", choices=("on", "off", "reset"),
|
|
@@ -2266,24 +2260,30 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
2266
2260
|
)
|
|
2267
2261
|
tele_p.set_defaults(func=c.cmd_telemetry)
|
|
2268
2262
|
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2263
|
+
def _build_alerts_parser(subparsers, name, *, help_text, xref=None):
|
|
2264
|
+
"""Build the `alerts` parser (registered via _REGISTRATION; #279 S6 W3).
|
|
2265
|
+
|
|
2266
|
+
Move-only extraction of the former inline build_parser() block;
|
|
2267
|
+
call-time `c = _cctally()` binding, --help bytes unchanged.
|
|
2268
|
+
"""
|
|
2269
|
+
c = _cctally()
|
|
2270
|
+
p_alerts = subparsers.add_parser(
|
|
2271
|
+
name,
|
|
2272
|
+
help=help_text,
|
|
2273
2273
|
formatter_class=CLIHelpFormatter,
|
|
2274
2274
|
description=textwrap.dedent("""\
|
|
2275
|
-
|
|
2275
|
+
Manage cctally threshold alerts.
|
|
2276
2276
|
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2277
|
+
Subcommands:
|
|
2278
|
+
test Send a synthetic test alert through the dispatch
|
|
2279
|
+
pipeline (osascript spawn + alerts.log line). Logs
|
|
2280
|
+
with mode=test so it doesn't pollute real-alert
|
|
2281
|
+
history.
|
|
2282
2282
|
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2283
|
+
Examples:
|
|
2284
|
+
cctally alerts test
|
|
2285
|
+
cctally alerts test --axis five-hour --threshold 95
|
|
2286
|
+
"""),
|
|
2287
2287
|
)
|
|
2288
2288
|
alerts_sub = p_alerts.add_subparsers(dest="alerts_command", required=True)
|
|
2289
2289
|
p_alerts_test = alerts_sub.add_parser(
|
|
@@ -2348,25 +2348,31 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
2348
2348
|
)
|
|
2349
2349
|
p_alerts_test.set_defaults(func=c.cmd_alerts_test)
|
|
2350
2350
|
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2351
|
+
def _build_setup_parser(subparsers, name, *, help_text, xref=None):
|
|
2352
|
+
"""Build the `setup` parser (registered via _REGISTRATION; #279 S6 W3).
|
|
2353
|
+
|
|
2354
|
+
Move-only extraction of the former inline build_parser() block;
|
|
2355
|
+
call-time `c = _cctally()` binding, --help bytes unchanged.
|
|
2356
|
+
"""
|
|
2357
|
+
c = _cctally()
|
|
2358
|
+
sp = subparsers.add_parser(
|
|
2359
|
+
name,
|
|
2360
|
+
help=help_text,
|
|
2355
2361
|
formatter_class=CLIHelpFormatter,
|
|
2356
2362
|
description=textwrap.dedent(
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2363
|
+
"""\
|
|
2364
|
+
Install cctally into Claude Code by adding hook entries to
|
|
2365
|
+
~/.claude/settings.json (additive, idempotent) and creating
|
|
2366
|
+
user-facing symlinks under ~/.local/bin/.
|
|
2367
|
+
|
|
2368
|
+
Modes (mutually exclusive):
|
|
2369
|
+
cctally setup # install (default)
|
|
2370
|
+
cctally setup --dry-run # show planned changes, change nothing
|
|
2371
|
+
cctally setup --status # report current install state
|
|
2372
|
+
cctally setup --uninstall # remove hooks + symlinks (keep data)
|
|
2373
|
+
cctally setup --uninstall --purge # also wipe ~/.local/share/cctally/
|
|
2374
|
+
"""
|
|
2375
|
+
),
|
|
2370
2376
|
)
|
|
2371
2377
|
mode = sp.add_mutually_exclusive_group()
|
|
2372
2378
|
mode.add_argument("--status", action="store_true", help="Report current install state")
|
|
@@ -2383,8 +2389,6 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
2383
2389
|
sp.add_argument("--force-dev", action="store_true", dest="force_dev",
|
|
2384
2390
|
help="Allow setup to run from a dev checkout (writes "
|
|
2385
2391
|
"dev-pointing hooks into ~/.claude/settings.json)")
|
|
2386
|
-
# Legacy bespoke-hook migration flags (install-mode only — see cmd_setup
|
|
2387
|
-
# post-parse validation). Spec Section 2 mode×flag matrix.
|
|
2388
2392
|
mig_group = sp.add_mutually_exclusive_group()
|
|
2389
2393
|
mig_group.add_argument(
|
|
2390
2394
|
"--migrate-legacy-hooks", action="store_true", dest="migrate_legacy_hooks",
|
|
@@ -2396,39 +2400,44 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
2396
2400
|
)
|
|
2397
2401
|
sp.set_defaults(func=c.cmd_setup)
|
|
2398
2402
|
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
+
def _build_db_parser(subparsers, name, *, help_text, xref=None):
|
|
2404
|
+
"""Build the `db` parser (registered via _REGISTRATION; #279 S6 W3).
|
|
2405
|
+
|
|
2406
|
+
Move-only extraction of the former inline build_parser() block;
|
|
2407
|
+
call-time `c = _cctally()` binding, --help bytes unchanged.
|
|
2408
|
+
"""
|
|
2409
|
+
c = _cctally()
|
|
2410
|
+
db_parser = subparsers.add_parser(
|
|
2411
|
+
name,
|
|
2412
|
+
help=help_text,
|
|
2403
2413
|
formatter_class=CLIHelpFormatter,
|
|
2404
2414
|
description=textwrap.dedent(
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2415
|
+
"""\
|
|
2416
|
+
Inspect and manage cctally's SQLite migration state.
|
|
2417
|
+
|
|
2418
|
+
Subcommands:
|
|
2419
|
+
status List migrations + applied/pending/failed/skipped
|
|
2420
|
+
state across stats.db and cache.db. Glyphs:
|
|
2421
|
+
✓ applied ✗ failed · pending ~ skipped
|
|
2422
|
+
skip Mark a migration as skipped (manual poison-pill
|
|
2423
|
+
escape — bypass an offending migration).
|
|
2424
|
+
unskip Remove a skip mark; the migration runs on next
|
|
2425
|
+
open.
|
|
2426
|
+
|
|
2427
|
+
Migration names accept either bare ("003_…") or qualified
|
|
2428
|
+
("stats.db:003_…" / "cache.db:003_…") forms. Bare names are
|
|
2429
|
+
rejected with exit 2 if the same NNN_… exists in both
|
|
2430
|
+
registries.
|
|
2431
|
+
|
|
2432
|
+
Examples:
|
|
2433
|
+
cctally db status
|
|
2434
|
+
cctally db status --json
|
|
2435
|
+
cctally db skip 003_merge_5h_block_duplicates_v1 --reason "perf hot"
|
|
2436
|
+
cctally db unskip stats.db:003_merge_5h_block_duplicates_v1
|
|
2437
|
+
"""
|
|
2438
|
+
),
|
|
2429
2439
|
)
|
|
2430
2440
|
db_sub = db_parser.add_subparsers(dest="db_action", required=True)
|
|
2431
|
-
|
|
2432
2441
|
db_status = db_sub.add_parser(
|
|
2433
2442
|
"status",
|
|
2434
2443
|
help="List migrations + applied/pending/failed/skipped state",
|
|
@@ -2439,7 +2448,6 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
2439
2448
|
help="Emit JSON to stdout",
|
|
2440
2449
|
)
|
|
2441
2450
|
db_status.set_defaults(func=c.cmd_db_status)
|
|
2442
|
-
|
|
2443
2451
|
db_skip = db_sub.add_parser(
|
|
2444
2452
|
"skip",
|
|
2445
2453
|
help="Mark a migration as skipped",
|
|
@@ -2453,7 +2461,6 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
2453
2461
|
help="Free-text reason (shown in db status)",
|
|
2454
2462
|
)
|
|
2455
2463
|
db_skip.set_defaults(func=c.cmd_db_skip)
|
|
2456
|
-
|
|
2457
2464
|
db_unskip = db_sub.add_parser(
|
|
2458
2465
|
"unskip",
|
|
2459
2466
|
help="Remove a skip mark; migration runs on next open",
|
|
@@ -2463,7 +2470,6 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
2463
2470
|
help="Migration name (NNN_… or qualified)",
|
|
2464
2471
|
)
|
|
2465
2472
|
db_unskip.set_defaults(func=c.cmd_db_unskip)
|
|
2466
|
-
|
|
2467
2473
|
db_recover = db_sub.add_parser(
|
|
2468
2474
|
"recover",
|
|
2469
2475
|
help="Revert a version-ahead DB to the known schema head (#145)",
|
|
@@ -2481,27 +2487,33 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
2481
2487
|
)
|
|
2482
2488
|
db_recover.set_defaults(func=c.cmd_db_recover)
|
|
2483
2489
|
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2490
|
+
def _build_doctor_parser(subparsers, name, *, help_text, xref=None):
|
|
2491
|
+
"""Build the `doctor` parser (registered via _REGISTRATION; #279 S6 W3).
|
|
2492
|
+
|
|
2493
|
+
Move-only extraction of the former inline build_parser() block;
|
|
2494
|
+
call-time `c = _cctally()` binding, --help bytes unchanged.
|
|
2495
|
+
"""
|
|
2496
|
+
c = _cctally()
|
|
2497
|
+
doctor_p = subparsers.add_parser(
|
|
2498
|
+
name,
|
|
2499
|
+
help=help_text,
|
|
2488
2500
|
formatter_class=CLIHelpFormatter,
|
|
2489
2501
|
description=textwrap.dedent(
|
|
2490
|
-
|
|
2491
|
-
|
|
2502
|
+
"""\
|
|
2503
|
+
Run all read-only diagnostic checks and emit a report.
|
|
2492
2504
|
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2505
|
+
Categories: install, hooks, auth, db, data, safety. Each
|
|
2506
|
+
category renders a severity (✓ ok / ⚠ warn / ✗ fail) and
|
|
2507
|
+
actionable remediation guidance for non-OK rows.
|
|
2496
2508
|
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2509
|
+
Exit code: 0 unless any check is FAIL (then exit 2). WARN
|
|
2510
|
+
rows do not change the exit code — doctor is a read-only
|
|
2511
|
+
diagnostic and warn-class findings are advisories.
|
|
2500
2512
|
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2513
|
+
See docs/commands/doctor.md for the full check inventory
|
|
2514
|
+
and JSON schema reference.
|
|
2515
|
+
"""
|
|
2516
|
+
),
|
|
2505
2517
|
)
|
|
2506
2518
|
doctor_p.add_argument(
|
|
2507
2519
|
"--json", action="store_true",
|
|
@@ -2518,40 +2530,46 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
2518
2530
|
)
|
|
2519
2531
|
doctor_p.set_defaults(func=c.cmd_doctor)
|
|
2520
2532
|
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2533
|
+
def _build_pricing_check_parser(subparsers, name, *, help_text, xref=None):
|
|
2534
|
+
"""Build the `pricing-check` parser (registered via _REGISTRATION; #279 S6 W3).
|
|
2535
|
+
|
|
2536
|
+
Move-only extraction of the former inline build_parser() block;
|
|
2537
|
+
call-time `c = _cctally()` binding, --help bytes unchanged.
|
|
2538
|
+
"""
|
|
2539
|
+
c = _cctally()
|
|
2540
|
+
pc_p = subparsers.add_parser(
|
|
2541
|
+
name,
|
|
2542
|
+
help=help_text,
|
|
2525
2543
|
formatter_class=CLIHelpFormatter,
|
|
2526
2544
|
description=textwrap.dedent(
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2545
|
+
"""\
|
|
2546
|
+
Check whether cctally's embedded model pricing is stale or
|
|
2547
|
+
missing, across three independently-degrading legs:
|
|
2548
|
+
|
|
2549
|
+
• coverage (offline, all-history) — models in your cached
|
|
2550
|
+
session data that cctally cannot price (Claude $0) or only
|
|
2551
|
+
approximates (Codex gpt-5 fallback).
|
|
2552
|
+
• drift (network, LiteLLM) — embedded price values vs the
|
|
2553
|
+
LiteLLM snapshot (direction-aware; allowlist-suppressed).
|
|
2554
|
+
• existence (network, Anthropic /v1/models) — vendor models the
|
|
2555
|
+
API offers that our table lacks. Maintainer-local (needs
|
|
2556
|
+
OAuth); degrades to skipped/degraded otherwise.
|
|
2557
|
+
|
|
2558
|
+
Exit codes:
|
|
2559
|
+
0 — no actionable findings (fully clean, OR partially/fully
|
|
2560
|
+
network-degraded but nothing actionable; --json still
|
|
2561
|
+
carries "status":"degraded").
|
|
2562
|
+
1 — any actionable finding (a coverage gap, value drift,
|
|
2563
|
+
missing-from-us, or an existence gap) — EVEN IF a network
|
|
2564
|
+
leg degraded. Findings always win over degradation.
|
|
2565
|
+
2 — argument/usage error.
|
|
2566
|
+
|
|
2567
|
+
"status" (ok|degraded) reports check completeness; the exit code
|
|
2568
|
+
reports whether you must act. They are orthogonal.
|
|
2569
|
+
|
|
2570
|
+
See docs/commands/pricing-check.md for the JSON schema.
|
|
2571
|
+
"""
|
|
2572
|
+
),
|
|
2555
2573
|
)
|
|
2556
2574
|
pc_p.add_argument(
|
|
2557
2575
|
"--json", action="store_true",
|
|
@@ -2563,24 +2581,26 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
2563
2581
|
)
|
|
2564
2582
|
pc_p.set_defaults(func=c.cmd_pricing_check)
|
|
2565
2583
|
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
# See docs/RELEASE.md.
|
|
2584
|
+
def _build_hook_tick_parser(subparsers, name, *, help_text, xref=None):
|
|
2585
|
+
"""Build the `hook-tick` parser (registered via _REGISTRATION; #279 S6 W3).
|
|
2569
2586
|
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2587
|
+
Move-only extraction of the former inline build_parser() block;
|
|
2588
|
+
call-time `c = _cctally()` binding, --help bytes unchanged.
|
|
2589
|
+
"""
|
|
2590
|
+
c = _cctally()
|
|
2591
|
+
ht = subparsers.add_parser(
|
|
2592
|
+
name,
|
|
2593
|
+
help=help_text,
|
|
2574
2594
|
formatter_class=CLIHelpFormatter,
|
|
2575
2595
|
description=textwrap.dedent(
|
|
2576
|
-
|
|
2577
|
-
|
|
2596
|
+
"""\
|
|
2597
|
+
Internal subcommand invoked by Claude Code hooks.
|
|
2578
2598
|
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2599
|
+
Reads CC's hook payload from stdin, runs sync_cache, and
|
|
2600
|
+
conditionally refreshes the OAuth usage cache (throttled).
|
|
2601
|
+
Returns 0 unconditionally in normal mode.
|
|
2602
|
+
"""
|
|
2603
|
+
),
|
|
2584
2604
|
)
|
|
2585
2605
|
ht.add_argument("--explain", action="store_true",
|
|
2586
2606
|
help="Run synchronously, print decision tree, exit informative code")
|
|
@@ -2595,42 +2615,52 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
2595
2615
|
help=argparse.SUPPRESS) # JSON string fed to mock fetch (tests only)
|
|
2596
2616
|
ht.set_defaults(func=c.cmd_hook_tick)
|
|
2597
2617
|
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2618
|
+
def _build_preview_parser(subparsers, name, *, help_text, xref=None):
|
|
2619
|
+
"""Build the `__preview` parser (registered via _REGISTRATION; #279 S6 W3).
|
|
2620
|
+
|
|
2621
|
+
Move-only extraction of the former inline build_parser() block;
|
|
2622
|
+
call-time `c = _cctally()` binding, --help bytes unchanged.
|
|
2623
|
+
"""
|
|
2624
|
+
c = _cctally()
|
|
2625
|
+
pv = subparsers.add_parser(
|
|
2626
|
+
name,
|
|
2627
|
+
help=help_text,
|
|
2628
|
+
formatter_class=CLIHelpFormatter,
|
|
2629
|
+
description="Internal: provision/manage the preview-channel data dir.",
|
|
2630
|
+
)
|
|
2631
|
+
pv_sub = pv.add_subparsers(dest="action", required=True)
|
|
2632
|
+
pv_ensure = pv_sub.add_parser("ensure", help=argparse.SUPPRESS)
|
|
2633
|
+
pv_ensure.add_argument("--no-refresh", action="store_true")
|
|
2634
|
+
pv_ensure.add_argument("--reseed", action="store_true")
|
|
2635
|
+
pv_ensure.set_defaults(func=c.cmd_preview)
|
|
2636
|
+
pv_clean = pv_sub.add_parser("clean", help=argparse.SUPPRESS)
|
|
2637
|
+
pv_clean.add_argument("--dry-run", action="store_true")
|
|
2638
|
+
pv_clean.set_defaults(func=c.cmd_preview)
|
|
2639
|
+
pv_status = pv_sub.add_parser("status", help=argparse.SUPPRESS)
|
|
2640
|
+
pv_status.set_defaults(func=c.cmd_preview)
|
|
2641
|
+
|
|
2642
|
+
def _build_update_parser(subparsers, name, *, help_text, xref=None):
|
|
2643
|
+
"""Build the `update` parser (registered via _REGISTRATION; #279 S6 W3).
|
|
2644
|
+
|
|
2645
|
+
Move-only extraction of the former inline build_parser() block;
|
|
2646
|
+
call-time `c = _cctally()` binding, --help bytes unchanged.
|
|
2647
|
+
"""
|
|
2648
|
+
c = _cctally()
|
|
2649
|
+
sub_update = subparsers.add_parser(
|
|
2650
|
+
name,
|
|
2651
|
+
help=help_text,
|
|
2622
2652
|
formatter_class=CLIHelpFormatter,
|
|
2623
2653
|
description=textwrap.dedent(
|
|
2624
|
-
|
|
2625
|
-
|
|
2654
|
+
"""\
|
|
2655
|
+
Update cctally to the latest version (npm/brew installs only).
|
|
2626
2656
|
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2657
|
+
Modes:
|
|
2658
|
+
cctally update install the latest version
|
|
2659
|
+
cctally update --check show update info without installing
|
|
2660
|
+
cctally update --skip [VER] don't remind about VER (default: latest)
|
|
2661
|
+
cctally update --remind-later [DAYS] defer the banner (default: 7)
|
|
2662
|
+
"""
|
|
2663
|
+
),
|
|
2634
2664
|
)
|
|
2635
2665
|
update_modes = sub_update.add_mutually_exclusive_group()
|
|
2636
2666
|
update_modes.add_argument(
|
|
@@ -2647,14 +2677,6 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
2647
2677
|
default=None,
|
|
2648
2678
|
help="Defer reminders by N days (default: 7)",
|
|
2649
2679
|
)
|
|
2650
|
-
# `--version` here is local to the update subparser. The subparser's
|
|
2651
|
-
# value is bound to `args.install_version` (NOT `args.version`) to
|
|
2652
|
-
# avoid a namespace collision with the top-level `--version`
|
|
2653
|
-
# (store_true) flag handled in `main()` before subcommand dispatch:
|
|
2654
|
-
# if both used `dest="version"`, `cctally update --version 1.2.3`
|
|
2655
|
-
# would set `args.version="1.2.3"`, which `main()`'s truthy check
|
|
2656
|
-
# would treat as "global --version requested" and short-circuit to
|
|
2657
|
-
# print the version banner before `cmd_update` ever ran.
|
|
2658
2680
|
sub_update.add_argument(
|
|
2659
2681
|
"--version", metavar="X.Y.Z", default=None, dest="install_version",
|
|
2660
2682
|
help="Install a specific version (npm only; brew has no versioned formulae)",
|
|
@@ -2673,71 +2695,184 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
2673
2695
|
)
|
|
2674
2696
|
sub_update.set_defaults(func=c.cmd_update)
|
|
2675
2697
|
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2698
|
+
def _build_update_check_parser(subparsers, name, *, help_text, xref=None):
|
|
2699
|
+
"""Build the `_update-check` parser (registered via _REGISTRATION; #279 S6 W3).
|
|
2700
|
+
|
|
2701
|
+
Move-only extraction of the former inline build_parser() block;
|
|
2702
|
+
call-time `c = _cctally()` binding, --help bytes unchanged.
|
|
2703
|
+
"""
|
|
2704
|
+
c = _cctally()
|
|
2705
|
+
uc = subparsers.add_parser(
|
|
2706
|
+
name,
|
|
2707
|
+
help=help_text,
|
|
2680
2708
|
formatter_class=CLIHelpFormatter,
|
|
2681
2709
|
description=textwrap.dedent(
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2710
|
+
"""\
|
|
2711
|
+
Internal subcommand: detached version-check worker spawned
|
|
2712
|
+
by `cctally update` (spec §3.6). Touches the throttle
|
|
2713
|
+
marker, fetches the latest version from npm or homebrew
|
|
2714
|
+
depending on install method, and writes update-state.json.
|
|
2715
|
+
Always returns 0; failures are logged to update.log.
|
|
2716
|
+
"""
|
|
2717
|
+
),
|
|
2690
2718
|
)
|
|
2691
2719
|
uc.set_defaults(func=c.cmd_update_check_internal)
|
|
2692
2720
|
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2721
|
+
def _build_telemetry_beat_parser(subparsers, name, *, help_text, xref=None):
|
|
2722
|
+
"""Build the `_telemetry-beat` parser (registered via _REGISTRATION; #279 S6 W3).
|
|
2723
|
+
|
|
2724
|
+
Move-only extraction of the former inline build_parser() block;
|
|
2725
|
+
call-time `c = _cctally()` binding, --help bytes unchanged.
|
|
2726
|
+
"""
|
|
2727
|
+
c = _cctally()
|
|
2728
|
+
tb = subparsers.add_parser(
|
|
2729
|
+
name,
|
|
2730
|
+
help=help_text,
|
|
2697
2731
|
formatter_class=CLIHelpFormatter,
|
|
2698
2732
|
description=textwrap.dedent(
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2733
|
+
"""\
|
|
2734
|
+
Internal subcommand: detached anonymous install-count beat
|
|
2735
|
+
worker, spawned broad-but-throttled from
|
|
2736
|
+
`_post_command_update_hooks` (spec 2026-07-07). A dedicated
|
|
2737
|
+
worker, decoupled from `_update-check` — it touches only the
|
|
2738
|
+
telemetry markers, never update-check state. Honours every
|
|
2739
|
+
opt-out (CCTALLY_DISABLE_TELEMETRY / DO_NOT_TRACK / config /
|
|
2740
|
+
dev checkout) via `resolve_telemetry_state`. Always returns 0;
|
|
2741
|
+
failures are swallowed.
|
|
2742
|
+
"""
|
|
2743
|
+
),
|
|
2710
2744
|
)
|
|
2711
2745
|
tb.set_defaults(func=c.cmd_telemetry_beat_internal)
|
|
2712
2746
|
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2747
|
+
def _build_repair_symlinks_parser(subparsers, name, *, help_text, xref=None):
|
|
2748
|
+
"""Build the `repair-symlinks` parser (registered via _REGISTRATION; #279 S6 W3).
|
|
2749
|
+
|
|
2750
|
+
Move-only extraction of the former inline build_parser() block;
|
|
2751
|
+
call-time `c = _cctally()` binding, --help bytes unchanged.
|
|
2752
|
+
"""
|
|
2753
|
+
c = _cctally()
|
|
2754
|
+
rs = subparsers.add_parser(
|
|
2755
|
+
name,
|
|
2756
|
+
help=help_text,
|
|
2757
|
+
formatter_class=CLIHelpFormatter,
|
|
2758
|
+
description=textwrap.dedent(
|
|
2759
|
+
"""\
|
|
2760
|
+
Internal subcommand: additively create any missing
|
|
2761
|
+
~/.local/bin/ symlinks for cctally subcommands (issue #114).
|
|
2762
|
+
|
|
2763
|
+
Invoked best-effort by the npm postinstall on upgrade so new
|
|
2764
|
+
cctally-* binaries become reachable without re-running
|
|
2765
|
+
`cctally setup`. Gated to existing installs (>=1 symlink
|
|
2766
|
+
already present); a fresh install is a silent no-op. Touches
|
|
2767
|
+
only symlinks — no hooks, settings.json, or cache. Refuses
|
|
2768
|
+
from a dev checkout.
|
|
2769
|
+
"""
|
|
2770
|
+
),
|
|
2771
|
+
)
|
|
2772
|
+
rs.set_defaults(func=c.cmd_repair_symlinks)
|
|
2773
|
+
|
|
2774
|
+
|
|
2775
|
+
class _Reg(NamedTuple):
|
|
2776
|
+
name: str
|
|
2777
|
+
builder: object
|
|
2778
|
+
help_text: object
|
|
2779
|
+
xref: object
|
|
2780
|
+
predicate: object
|
|
2781
|
+
|
|
2782
|
+
|
|
2783
|
+
_REGISTRATION = (
|
|
2784
|
+
_Reg('sync-week', _build_sync_week_parser, "Compute weekly cost from session data and store in SQLite", None, None),
|
|
2785
|
+
_Reg('report', _build_report_parser, "Show current and trend dollars-per-1%% statistics", None, None),
|
|
2786
|
+
_Reg('forecast', _build_forecast_parser, "Project current-week usage to reset; show daily budgets", None, None),
|
|
2787
|
+
_Reg('budget', _build_budget_parser, "Weekly equivalent-$ budget + pace + spend alerts", None, None),
|
|
2788
|
+
_Reg('percent-breakdown', _build_percent_breakdown_parser, "Show per-percent cost milestones for a week", None, None),
|
|
2789
|
+
_Reg('five-hour-breakdown', _build_five_hour_breakdown_parser, "Per-percent milestones inside one 5h block (mirror of percent-breakdown)", None, None),
|
|
2790
|
+
_Reg('tui', _build_tui_parser, "Live refreshing dashboard (current week, forecast, trend, sessions)", None, None),
|
|
2791
|
+
_Reg('dashboard', _build_dashboard_parser, "Launch the live web dashboard on http://localhost:8789", None, None),
|
|
2792
|
+
_Reg('record-usage', _build_record_usage_parser, "Record usage data from Claude Code status line", None, None),
|
|
2793
|
+
_Reg('record-credit', _build_record_credit_parser, "Record an in-place weekly credit the auto-detector misses", None, None),
|
|
2794
|
+
_Reg('refresh-usage', _build_refresh_usage_parser, "Force-fetch 7d/5h percent from OAuth API and record it", None, None),
|
|
2795
|
+
_Reg('cache-report', _build_cache_report_parser, "Show daily cache hit rates per model from ccusage data", None, None),
|
|
2796
|
+
_Reg('range-cost', _build_range_cost_parser, "Compute USD cost for a time range from session data", None, None),
|
|
2797
|
+
_Reg('blocks', _build_blocks_parser, "Show usage report grouped by 5-hour session blocks", "Alias of `cctally claude blocks` (the canonical form).", None),
|
|
2798
|
+
_Reg('statusline', _build_statusline_parser, "Compact one-line status for Claude Code hooks", "Alias of `cctally claude statusline` (the canonical form).", None),
|
|
2799
|
+
_Reg('five-hour-blocks', _build_five_hour_blocks_parser, "List API-anchored 5h blocks with rollup totals + 7d-drift columns", None, None),
|
|
2800
|
+
_Reg('cache-sync', _build_cache_sync_parser, "Sync (or rebuild) the session-entry cache", None, None),
|
|
2801
|
+
_Reg('daily', _build_daily_parser, "Show usage report grouped by date", "Alias of `cctally claude daily` (the canonical form).", None),
|
|
2802
|
+
_Reg('monthly', _build_monthly_parser, "Show usage report grouped by month", "Alias of `cctally claude monthly` (the canonical form).", None),
|
|
2803
|
+
_Reg('weekly', _build_weekly_parser, "Show usage grouped by subscription week (with Used %% and $/1%%)", "Alias of `cctally claude weekly` (the canonical form).", None),
|
|
2804
|
+
_Reg('codex-daily', _build_codex_daily_parser, "Show Codex usage report grouped by date (drop-in for `ccusage-codex daily`)", "Alias of `cctally codex daily` (the canonical form).", None),
|
|
2805
|
+
_Reg('codex-monthly', _build_codex_monthly_parser, "Show Codex usage grouped by month (drop-in for `ccusage-codex monthly`)", "Alias of `cctally codex monthly` (the canonical form).", None),
|
|
2806
|
+
_Reg('codex-weekly', _build_codex_weekly_parser, "Show Codex usage grouped by week (week-start from config.json)", "Alias of `cctally codex weekly` (the canonical form).", None),
|
|
2807
|
+
_Reg('codex-session', _build_codex_session_parser, "Show Codex usage grouped by session (drop-in for `ccusage-codex session`)", "Alias of `cctally codex session` (the canonical form).", None),
|
|
2808
|
+
_Reg('project', _build_project_parser, "Roll usage up by project (git-root), with per-project Used %% attribution", None, None),
|
|
2809
|
+
_Reg('diff', _build_diff_parser, "Compare Claude usage between two windows.", None, None),
|
|
2810
|
+
_Reg('session', _build_session_parser, "Show Claude usage grouped by sessionId (merges resumed-across-files sessions)", "Alias of `cctally claude session` (the canonical form).", None),
|
|
2811
|
+
_Reg('claude', _build_claude_parser, "Claude-source reports (drop-in for `ccusage claude …`)", None, None),
|
|
2812
|
+
_Reg('codex', _build_codex_parser, "Codex-source reports (drop-in for `ccusage codex …`)", None, None),
|
|
2813
|
+
_Reg('config', _build_config_parser, "Get / set / unset persisted user preferences", None, None),
|
|
2814
|
+
_Reg('telemetry', _build_telemetry_parser, "Show or change anonymous install-count telemetry", None, None),
|
|
2815
|
+
_Reg('alerts', _build_alerts_parser, "Manage threshold alerts", None, None),
|
|
2816
|
+
_Reg('setup', _build_setup_parser, "Install cctally into Claude Code (hooks + symlinks)", None, None),
|
|
2817
|
+
_Reg('db', _build_db_parser, "Migration / DB management (status, skip, unskip)", None, None),
|
|
2818
|
+
_Reg('doctor', _build_doctor_parser, "Diagnose data freshness and install state", None, None),
|
|
2819
|
+
_Reg('pricing-check', _build_pricing_check_parser, "Detect stale or missing embedded model pricing", None, None),
|
|
2820
|
+
_Reg('hook-tick', _build_hook_tick_parser, argparse.SUPPRESS, None, None),
|
|
2821
|
+
_Reg('__preview', _build_preview_parser, argparse.SUPPRESS, None, lambda c: getattr(c, "cmd_preview", None) is not None),
|
|
2822
|
+
_Reg('update', _build_update_parser, "Update cctally to the latest version", None, None),
|
|
2823
|
+
_Reg('_update-check', _build_update_check_parser, argparse.SUPPRESS, None, None),
|
|
2824
|
+
_Reg('_telemetry-beat', _build_telemetry_beat_parser, argparse.SUPPRESS, None, None),
|
|
2825
|
+
_Reg('repair-symlinks', _build_repair_symlinks_parser, argparse.SUPPRESS, None, None),
|
|
2826
|
+
)
|
|
2827
|
+
|
|
2828
|
+
|
|
2829
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
2830
|
+
c = _cctally()
|
|
2831
|
+
p = argparse.ArgumentParser(
|
|
2832
|
+
prog="cctally",
|
|
2717
2833
|
formatter_class=CLIHelpFormatter,
|
|
2718
2834
|
description=textwrap.dedent(
|
|
2719
2835
|
"""\
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2836
|
+
Track Claude subscription weekly usage percent and weekly cost
|
|
2837
|
+
in a local SQLite database.
|
|
2838
|
+
|
|
2839
|
+
Data flow:
|
|
2840
|
+
1) Claude Code status line captures rate limit data after each API call.
|
|
2841
|
+
2) record-usage stores usage snapshots and triggers percent milestones.
|
|
2842
|
+
3) sync-week computes weekly USD cost from Claude Code session data.
|
|
2843
|
+
4) report computes dollars per 1% and shows trend history.
|
|
2844
|
+
"""
|
|
2845
|
+
),
|
|
2846
|
+
epilog=textwrap.dedent(
|
|
2847
|
+
"""\
|
|
2848
|
+
Quick start:
|
|
2849
|
+
# Add record-usage call to ~/.claude/statusline-command.sh (see record-usage --help)
|
|
2850
|
+
cctally sync-week
|
|
2851
|
+
cctally report
|
|
2729
2852
|
"""
|
|
2730
2853
|
),
|
|
2731
2854
|
)
|
|
2732
|
-
|
|
2855
|
+
p.add_argument(
|
|
2856
|
+
"-v", "--version",
|
|
2857
|
+
action="store_true",
|
|
2858
|
+
default=argparse.SUPPRESS,
|
|
2859
|
+
help="Print cctally version (from CHANGELOG.md latest release header) and exit",
|
|
2860
|
+
)
|
|
2861
|
+
sub = p.add_subparsers(
|
|
2862
|
+
dest="command",
|
|
2863
|
+
required=False,
|
|
2864
|
+
title="commands",
|
|
2865
|
+
metavar="<command>",
|
|
2866
|
+
)
|
|
2867
|
+
|
|
2868
|
+
for _reg in _REGISTRATION:
|
|
2869
|
+
if _reg.predicate is not None and not _reg.predicate(c):
|
|
2870
|
+
continue
|
|
2871
|
+
_reg.builder(sub, _reg.name, help_text=_reg.help_text, xref=_reg.xref)
|
|
2733
2872
|
|
|
2734
|
-
# Python 3.14 leaks `==SUPPRESS==` for hidden subparsers in --help; strip
|
|
2735
|
-
# the pseudo-action so the row disappears entirely. (The choice still
|
|
2736
|
-
# appears in the `{...}` choices header — there's no clean way to hide
|
|
2737
|
-
# that without a custom formatter, and it's harmless.)
|
|
2738
2873
|
sub._choices_actions = [
|
|
2739
2874
|
a for a in getattr(sub, "_choices_actions", [])
|
|
2740
2875
|
if getattr(a, "help", None) is not argparse.SUPPRESS
|
|
2741
2876
|
]
|
|
2742
|
-
|
|
2743
2877
|
return p
|
|
2878
|
+
|