cctally 1.68.0 → 1.69.1

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.
Files changed (40) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/bin/_cctally_alerts.py +54 -2
  3. package/bin/_cctally_cache.py +644 -107
  4. package/bin/_cctally_cache_report.py +41 -17
  5. package/bin/_cctally_codex.py +109 -4
  6. package/bin/_cctally_config.py +368 -2
  7. package/bin/_cctally_core.py +168 -5
  8. package/bin/_cctally_dashboard.py +679 -5
  9. package/bin/_cctally_dashboard_conversation.py +9 -4
  10. package/bin/_cctally_dashboard_envelope.py +110 -1
  11. package/bin/_cctally_dashboard_share.py +557 -79
  12. package/bin/_cctally_db.py +303 -17
  13. package/bin/_cctally_diff.py +49 -16
  14. package/bin/_cctally_doctor.py +118 -0
  15. package/bin/_cctally_forecast.py +12 -4
  16. package/bin/_cctally_parser.py +298 -92
  17. package/bin/_cctally_project.py +24 -8
  18. package/bin/_cctally_quota.py +1381 -0
  19. package/bin/_cctally_record.py +319 -14
  20. package/bin/_cctally_refresh.py +105 -3
  21. package/bin/_cctally_reporting.py +7 -1
  22. package/bin/_cctally_setup.py +343 -113
  23. package/bin/_cctally_statusline.py +228 -0
  24. package/bin/_cctally_tui.py +787 -7
  25. package/bin/_lib_alert_axes.py +11 -0
  26. package/bin/_lib_codex_hooks.py +367 -0
  27. package/bin/_lib_conversation_query.py +156 -67
  28. package/bin/_lib_doctor.py +202 -0
  29. package/bin/_lib_jsonl.py +529 -162
  30. package/bin/_lib_quota.py +566 -0
  31. package/bin/_lib_share.py +152 -34
  32. package/bin/_lib_snapshot_cache.py +26 -0
  33. package/bin/_lib_source_identity.py +74 -0
  34. package/bin/cctally +324 -10
  35. package/dashboard/static/assets/index-CBbErI-P.js +80 -0
  36. package/dashboard/static/assets/index-kDDVOLa_.css +1 -0
  37. package/dashboard/static/dashboard.html +2 -2
  38. package/package.json +5 -1
  39. package/dashboard/static/assets/index-BybNp_Di.js +0 -80
  40. package/dashboard/static/assets/index-DgAmLK65.css +0 -1
@@ -281,7 +281,30 @@ def _add_codex_shared_args(parser: argparse.ArgumentParser) -> None:
281
281
  )
282
282
 
283
283
 
284
- def _add_share_args(parser, *, has_status_line: bool = False) -> None:
284
+ def _add_source_args(
285
+ parser: argparse.ArgumentParser, *, fixed_source: str | None = None,
286
+ speed: bool = False,
287
+ ) -> None:
288
+ """Attach the source selector or pin a nested provider alias."""
289
+ if fixed_source is None:
290
+ parser.add_argument(
291
+ "--source", choices=("claude", "codex", "all"), default="claude",
292
+ help="Analytics provider: claude (default), codex, or all.",
293
+ )
294
+ else:
295
+ if fixed_source not in {"claude", "codex"}:
296
+ raise ValueError(f"unsupported fixed source {fixed_source!r}")
297
+ parser.set_defaults(source=fixed_source)
298
+ if speed:
299
+ parser.add_argument(
300
+ "--speed", choices=("auto", "standard", "fast"), default="auto",
301
+ help="Codex pricing tier for Codex and all-source requests.",
302
+ )
303
+
304
+
305
+ def _add_share_args(
306
+ parser, *, has_status_line: bool = False, json_dest: str = "json",
307
+ ) -> None:
285
308
  """Attach shareable-reports flags + format/json mutex to a subparser.
286
309
 
287
310
  Idempotent — call exactly once per subparser. Caller MUST remove any
@@ -310,7 +333,7 @@ def _add_share_args(parser, *, has_status_line: bool = False) -> None:
310
333
  help="Render output as shareable markdown, self-contained HTML, or SVG. "
311
334
  "Default destination: md->stdout, html/svg->~/Downloads file.")
312
335
  output_group.add_argument(
313
- "--json", action="store_true",
336
+ "--json", action="store_true", dest=json_dest,
314
337
  help="Emit machine-readable JSON; suppresses terminal render.")
315
338
  if has_status_line:
316
339
  output_group.add_argument(
@@ -926,9 +949,10 @@ def _build_codex_daily_parser(subparsers, name, *, help_text, xref):
926
949
  help="Show per-model cost breakdown sub-rows.")
927
950
  p.add_argument("-o", "--order", choices=("asc", "desc"), default="asc",
928
951
  help="Sort direction by date (default: asc).")
929
- p.add_argument("--json", action="store_true", dest="json",
930
- help="Output JSON matching upstream ccusage-codex daily format.")
931
952
  _add_codex_shared_args(p)
953
+ p.add_argument("--config", default=None, metavar="PATH",
954
+ help="Read configuration from PATH without writing it.")
955
+ _add_share_args(p)
932
956
  p.set_defaults(func=c.cmd_codex_daily)
933
957
  return p
934
958
 
@@ -957,9 +981,10 @@ def _build_codex_monthly_parser(subparsers, name, *, help_text, xref):
957
981
  help="Show per-model cost breakdown sub-rows.")
958
982
  p.add_argument("-o", "--order", choices=("asc", "desc"), default="asc",
959
983
  help="Sort direction by month (default: asc).")
960
- p.add_argument("--json", action="store_true", dest="json",
961
- help="Output JSON matching upstream ccusage-codex monthly format.")
962
984
  _add_codex_shared_args(p)
985
+ p.add_argument("--config", default=None, metavar="PATH",
986
+ help="Read configuration from PATH without writing it.")
987
+ _add_share_args(p)
963
988
  p.set_defaults(func=c.cmd_codex_monthly)
964
989
  return p
965
990
 
@@ -992,9 +1017,10 @@ def _build_codex_weekly_parser(subparsers, name, *, help_text, xref):
992
1017
  help="Show per-model cost breakdown sub-rows.")
993
1018
  p.add_argument("-o", "--order", choices=("asc", "desc"), default="asc",
994
1019
  help="Sort direction by week (default: asc).")
995
- p.add_argument("--json", action="store_true", dest="json",
996
- help="Output JSON.")
997
1020
  _add_codex_shared_args(p)
1021
+ p.add_argument("--config", default=None, metavar="PATH",
1022
+ help="Read configuration from PATH without writing it.")
1023
+ _add_share_args(p)
998
1024
  p.set_defaults(func=c.cmd_codex_weekly)
999
1025
  return p
1000
1026
 
@@ -1021,18 +1047,109 @@ def _build_codex_session_parser(subparsers, name, *, help_text, xref):
1021
1047
  help_until="Filter until date (inclusive; accepts YYYY-MM-DD or YYYYMMDD).")
1022
1048
  p.add_argument("-o", "--order", choices=("asc", "desc"), default="asc",
1023
1049
  help="Sort direction by last activity (default: asc — earliest first).")
1024
- p.add_argument("--json", action="store_true", dest="json",
1025
- help="Output JSON matching upstream ccusage-codex session format.")
1026
1050
  _add_codex_shared_args(p)
1051
+ p.add_argument("--config", default=None, metavar="PATH",
1052
+ help="Read configuration from PATH without writing it.")
1053
+ _add_share_args(p)
1027
1054
  p.set_defaults(func=c.cmd_codex_session)
1028
1055
  return p
1029
1056
 
1030
1057
 
1058
+ def _add_codex_quota_common_args(parser) -> None:
1059
+ """Attach the shared exact-selector/reporting surface for native quotas."""
1060
+ parser.add_argument(
1061
+ "--root-key", dest="root_key", metavar="FULL_SOURCE_ROOT_KEY",
1062
+ help="Exact case-sensitive sourceRootKey selector (no prefix matching).",
1063
+ )
1064
+ parser.add_argument(
1065
+ "--limit-key", dest="limit_key", metavar="FULL_LOGICAL_LIMIT_KEY",
1066
+ help="Exact case-sensitive logicalLimitKey selector (no prefix matching).",
1067
+ )
1068
+ parser.add_argument(
1069
+ "--no-sync", action="store_true",
1070
+ help="Read retained local-rollout quota evidence without a Codex cache sync.",
1071
+ )
1072
+ parser.add_argument(
1073
+ "--config", default=None, metavar="PATH",
1074
+ help="Read display settings from PATH for this invocation only.",
1075
+ )
1076
+ parser.add_argument(
1077
+ "--json", action="store_true",
1078
+ help="Emit stamped schemaVersion:1 JSON.",
1079
+ )
1080
+
1081
+
1082
+ def _build_codex_quota_parser(subparsers, name, *, help_text, xref=None):
1083
+ """Build canonical `cctally codex quota …` leaves (issue #294 S2)."""
1084
+ c = _cctally()
1085
+ quota = subparsers.add_parser(
1086
+ name, help=help_text, formatter_class=CLIHelpFormatter,
1087
+ description=(
1088
+ "Native Codex quota history, current status, forecast, reset blocks, "
1089
+ "and percent-crossing breakdowns. Each window remains root-qualified; "
1090
+ "independent percentages are never combined."
1091
+ ),
1092
+ )
1093
+ leaves = quota.add_subparsers(dest="quota_command", required=True, metavar="<command>")
1094
+
1095
+ history = leaves.add_parser("history", help="Show physical local-rollout quota history",
1096
+ formatter_class=CLIHelpFormatter)
1097
+ _add_since_until_args(
1098
+ history, metavar_since="DATE_OR_ISO", metavar_until="DATE_OR_ISO",
1099
+ help_since="Inclusive start; date-only is interpreted in display.tz.",
1100
+ help_until="Exclusive end; date-only is interpreted in display.tz.",
1101
+ )
1102
+ _add_codex_quota_common_args(history)
1103
+ history.set_defaults(func=c.cmd_codex_quota_history)
1104
+
1105
+ statusline = leaves.add_parser("statusline", help="Show one native status segment per quota identity",
1106
+ formatter_class=CLIHelpFormatter)
1107
+ statusline.add_argument(
1108
+ "--as-of", default=None, metavar="ISO-8601",
1109
+ help="Interpret retained local evidence at this instant (naive means UTC).",
1110
+ )
1111
+ _add_codex_quota_common_args(statusline)
1112
+ statusline.set_defaults(func=c.cmd_codex_quota_statusline)
1113
+
1114
+ forecast = leaves.add_parser("forecast", help="Forecast each native quota reset window",
1115
+ formatter_class=CLIHelpFormatter)
1116
+ forecast.add_argument(
1117
+ "--as-of", default=None, metavar="ISO-8601",
1118
+ help="Forecast at this instant (naive means UTC).",
1119
+ )
1120
+ _add_codex_quota_common_args(forecast)
1121
+ forecast.set_defaults(func=c.cmd_codex_quota_forecast)
1122
+
1123
+ blocks = leaves.add_parser("blocks", help="Show native reset blocks",
1124
+ formatter_class=CLIHelpFormatter)
1125
+ _add_since_until_args(
1126
+ blocks, metavar_since="DATE_OR_ISO", metavar_until="DATE_OR_ISO",
1127
+ help_since="Inclusive start; date-only is interpreted in display.tz.",
1128
+ help_until="Exclusive end; date-only is interpreted in display.tz.",
1129
+ )
1130
+ _add_codex_quota_common_args(blocks)
1131
+ blocks.set_defaults(func=c.cmd_codex_quota_blocks)
1132
+
1133
+ breakdown = leaves.add_parser("breakdown", help="Correlate one native block's percent crossings",
1134
+ formatter_class=CLIHelpFormatter)
1135
+ breakdown.add_argument(
1136
+ "--reset-at", required=True, metavar="ISO-8601",
1137
+ help="Exact block reset timestamp; date-only is rejected and naive means UTC.",
1138
+ )
1139
+ breakdown.add_argument(
1140
+ "--speed", choices=("auto", "standard", "fast"), default="auto",
1141
+ help="Codex pricing tier for query-time cost correlation (default: auto).",
1142
+ )
1143
+ _add_codex_quota_common_args(breakdown)
1144
+ breakdown.set_defaults(func=c.cmd_codex_quota_breakdown)
1145
+ return quota
1146
+
1147
+
1031
1148
  def _build_sync_week_parser(subparsers, name, *, help_text, xref=None):
1032
1149
  """Build the `sync-week` parser (registered via _REGISTRATION; #279 S6 W3).
1033
1150
 
1034
- Move-only extraction of the former inline build_parser() block;
1035
- call-time `c = _cctally()` binding, --help bytes unchanged.
1151
+ Move-only extraction of the former inline build_parser() block with
1152
+ call-time `c = _cctally()` binding.
1036
1153
  """
1037
1154
  c = _cctally()
1038
1155
  py = subparsers.add_parser(
@@ -1104,11 +1221,11 @@ def _build_sync_week_parser(subparsers, name, *, help_text, xref=None):
1104
1221
  )
1105
1222
  py.set_defaults(func=c.cmd_sync_week)
1106
1223
 
1107
- def _build_report_parser(subparsers, name, *, help_text, xref=None):
1224
+ def _build_report_parser(subparsers, name, *, help_text, xref=None, fixed_source=None):
1108
1225
  """Build the `report` parser (registered via _REGISTRATION; #279 S6 W3).
1109
1226
 
1110
- Move-only extraction of the former inline build_parser() block;
1111
- call-time `c = _cctally()` binding, --help bytes unchanged.
1227
+ Move-only extraction of the former inline build_parser() block with
1228
+ call-time `c = _cctally()` binding.
1112
1229
  """
1113
1230
  c = _cctally()
1114
1231
  pr = subparsers.add_parser(
@@ -1116,6 +1233,14 @@ def _build_report_parser(subparsers, name, *, help_text, xref=None):
1116
1233
  help=help_text,
1117
1234
  formatter_class=CLIHelpFormatter,
1118
1235
  description=textwrap.dedent(
1236
+ (
1237
+ """\
1238
+ Report Codex quota-window dollars per percent.
1239
+
1240
+ Each native quota window retains its own reset identity; the
1241
+ report never invents Claude subscription weeks.
1242
+ """
1243
+ if fixed_source == "codex" else
1119
1244
  """\
1120
1245
  Report current and historical dollars per 1% weekly usage.
1121
1246
 
@@ -1124,49 +1249,64 @@ def _build_report_parser(subparsers, name, *, help_text, xref=None):
1124
1249
  - latest cost snapshot (USD)
1125
1250
  then computes USD / percent.
1126
1251
  """
1127
- ),
1252
+ )
1253
+ ),
1128
1254
  epilog=textwrap.dedent(
1255
+ (
1256
+ """\
1257
+ Examples:
1258
+ cctally codex report
1259
+ cctally codex report --sync-current
1260
+ cctally codex report --weeks 12 --json
1261
+ """
1262
+ if fixed_source == "codex" else
1129
1263
  """\
1130
1264
  Examples:
1131
1265
  cctally report
1132
1266
  cctally report --sync-current
1133
1267
  cctally report --weeks 12 --json
1134
1268
  """
1135
- ),
1269
+ )
1270
+ ),
1136
1271
  )
1137
1272
  pr.add_argument(
1138
1273
  "--weeks",
1139
1274
  type=int,
1140
1275
  default=8,
1141
- help="How many recent week windows to include in the trend.",
1276
+ help=("How many recent native Codex quota windows to include in the trend."
1277
+ if fixed_source == "codex" else
1278
+ "How many recent week windows to include in the trend."),
1142
1279
  )
1143
1280
  pr.add_argument(
1144
1281
  "--sync-current",
1145
1282
  action="store_true",
1146
- help="Run sync-week first, then generate the report.",
1147
- )
1148
- pr.add_argument(
1149
- "--week-start-name",
1150
- default=None,
1151
- choices=list(WEEKDAY_MAP.keys()),
1152
- help="Week-start day used if report falls back to date-only week logic.",
1153
- )
1154
- pr.add_argument(
1155
- "--mode",
1156
- default="auto",
1157
- choices=["auto", "calculate", "display"],
1158
- help="Mode passed to sync-week when --sync-current is used.",
1159
- )
1160
- pr.add_argument(
1161
- "--offline",
1162
- action="store_true",
1163
- help="Pass --offline to sync-week when --sync-current is used.",
1164
- )
1165
- pr.add_argument(
1166
- "--project",
1167
- default=None,
1168
- help="Project filter passed to sync-week when --sync-current is used.",
1169
- )
1283
+ help=("Sync Codex accounting and reconcile native quota state first, then "
1284
+ "generate the report." if fixed_source == "codex" else
1285
+ "Run sync-week first, then generate the report."),
1286
+ )
1287
+ if fixed_source != "codex":
1288
+ pr.add_argument(
1289
+ "--week-start-name",
1290
+ default=None,
1291
+ choices=list(WEEKDAY_MAP.keys()),
1292
+ help="Week-start day used if report falls back to date-only week logic.",
1293
+ )
1294
+ pr.add_argument(
1295
+ "--mode",
1296
+ default="auto",
1297
+ choices=["auto", "calculate", "display"],
1298
+ help="Mode passed to sync-week when --sync-current is used.",
1299
+ )
1300
+ pr.add_argument(
1301
+ "--offline",
1302
+ action="store_true",
1303
+ help="Pass --offline to sync-week when --sync-current is used.",
1304
+ )
1305
+ pr.add_argument(
1306
+ "--project",
1307
+ default=None,
1308
+ help="Project filter passed to sync-week when --sync-current is used.",
1309
+ )
1170
1310
  pr.add_argument(
1171
1311
  "--reveal-projects",
1172
1312
  action="store_true",
@@ -1177,21 +1317,24 @@ def _build_report_parser(subparsers, name, *, help_text, xref=None):
1177
1317
  pr.add_argument(
1178
1318
  "--detail",
1179
1319
  action="store_true",
1180
- help="Include per-percent cost milestones for the current week.",
1320
+ help=("Include native quota-window attribution detail."
1321
+ if fixed_source == "codex" else
1322
+ "Include per-percent cost milestones for the current week."),
1181
1323
  )
1182
1324
  pr.add_argument(
1183
1325
  "--tz", default=None, type=_argparse_tz, metavar="TZ",
1184
1326
  help="Display timezone: local, utc, or IANA name. "
1185
1327
  "Overrides config display.tz for this call.",
1186
1328
  )
1329
+ _add_source_args(pr, fixed_source=fixed_source, speed=True)
1187
1330
  _add_share_args(pr)
1188
1331
  pr.set_defaults(func=c.cmd_report)
1189
1332
 
1190
1333
  def _build_forecast_parser(subparsers, name, *, help_text, xref=None):
1191
1334
  """Build the `forecast` parser (registered via _REGISTRATION; #279 S6 W3).
1192
1335
 
1193
- Move-only extraction of the former inline build_parser() block;
1194
- call-time `c = _cctally()` binding, --help bytes unchanged.
1336
+ Move-only extraction of the former inline build_parser() block with
1337
+ call-time `c = _cctally()` binding.
1195
1338
  """
1196
1339
  c = _cctally()
1197
1340
  fc = subparsers.add_parser(
@@ -1727,26 +1870,23 @@ def _build_refresh_usage_parser(subparsers, name, *, help_text, xref=None):
1727
1870
  help="HTTP timeout in seconds (default: 5.0).")
1728
1871
  rfu.set_defaults(func=c.cmd_refresh_usage)
1729
1872
 
1730
- def _build_cache_report_parser(subparsers, name, *, help_text, xref=None):
1873
+ def _build_cache_report_parser(subparsers, name, *, help_text, xref=None, fixed_source=None):
1731
1874
  """Build the `cache-report` parser (registered via _REGISTRATION; #279 S6 W3).
1732
1875
 
1733
1876
  Move-only extraction of the former inline build_parser() block;
1734
1877
  call-time `c = _cctally()` binding, --help bytes unchanged.
1735
1878
  """
1736
1879
  c = _cctally()
1880
+ cache_description = {
1881
+ None: "Report Claude cache diagnostics or Codex cached-input/token reuse.",
1882
+ "claude": "Report Claude cache diagnostics.",
1883
+ "codex": "Report Codex cached-input/token reuse.",
1884
+ }[fixed_source]
1737
1885
  pc = subparsers.add_parser(
1738
1886
  name,
1739
1887
  help=help_text,
1740
1888
  formatter_class=CLIHelpFormatter,
1741
- description=textwrap.dedent(
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
- ),
1889
+ description=cache_description,
1750
1890
  epilog=textwrap.dedent(
1751
1891
  """\
1752
1892
  Examples:
@@ -1780,8 +1920,8 @@ def _build_cache_report_parser(subparsers, name, *, help_text, xref=None):
1780
1920
  "--by-session",
1781
1921
  action="store_true",
1782
1922
  dest="by_session",
1783
- help="Group by Claude sessionId (resumed-merged) instead of by date. "
1784
- "Adds SessionId, Last Activity, and Project identity columns.",
1923
+ help="Group by source-native session identity instead of by date. "
1924
+ "Adds identity, Last Activity, and Project columns.",
1785
1925
  )
1786
1926
  pc.add_argument(
1787
1927
  "-O", "--offline",
@@ -1798,18 +1938,13 @@ def _build_cache_report_parser(subparsers, name, *, help_text, xref=None):
1798
1938
  default=None,
1799
1939
  help="Filter to a specific project.",
1800
1940
  )
1801
- pc.add_argument(
1802
- "--json",
1803
- action="store_true",
1804
- help="Emit machine-readable JSON output.",
1805
- )
1806
1941
  pc.add_argument(
1807
1942
  "--anomaly-threshold-pp",
1808
1943
  type=int,
1809
1944
  default=15,
1810
1945
  dest="anomaly_threshold_pp",
1811
- help="Cache%% drop threshold (percentage points) vs. trailing-median "
1812
- "baseline for the cache_drop anomaly trigger. Default: 15.",
1946
+ help="Claude cache %% drop threshold (percentage points) vs. a trailing "
1947
+ "median. Default: 15.",
1813
1948
  )
1814
1949
  pc.add_argument(
1815
1950
  "--anomaly-window-days",
@@ -1823,36 +1958,47 @@ def _build_cache_report_parser(subparsers, name, *, help_text, xref=None):
1823
1958
  "--no-anomaly",
1824
1959
  action="store_true",
1825
1960
  dest="no_anomaly",
1826
- help="Disable all anomaly triggers (both cache_drop and net_negative).",
1961
+ help="Disable Claude cache anomaly triggers.",
1827
1962
  )
1828
1963
  pc.add_argument(
1829
1964
  "--sort",
1830
- choices=["date", "net", "cache", "recent", "cost", "anomaly"],
1965
+ choices=["date", "net", "cache", "recent", "cost", "anomaly", "reuse"],
1831
1966
  default=None,
1832
1967
  dest="sort",
1833
- help="Override sort order. Defaults: 'date' in daily mode, 'net' in "
1834
- "--by-session mode.",
1968
+ help="Override sort order; valid values depend on the selected source.",
1835
1969
  )
1836
1970
  pc.add_argument(
1837
1971
  "--tz", default=None, type=_argparse_tz, metavar="TZ",
1838
1972
  help="Display timezone: local, utc, or IANA name. "
1839
1973
  "Overrides config display.tz for this call.",
1840
1974
  )
1975
+ pc.add_argument(
1976
+ "--reveal-projects", action="store_true", dest="reveal_projects",
1977
+ help="In --format output, show real project basenames instead of "
1978
+ "the default project-1, project-2, ... anonymization.",
1979
+ )
1841
1980
  _add_ccusage_alias_args(pc, ansi_emit=False)
1981
+ _add_source_args(pc, fixed_source=fixed_source, speed=True)
1982
+ _add_share_args(pc)
1842
1983
  pc.set_defaults(func=c.cmd_cache_report)
1843
1984
 
1844
- def _build_range_cost_parser(subparsers, name, *, help_text, xref=None):
1985
+ def _build_range_cost_parser(subparsers, name, *, help_text, xref=None, fixed_source=None):
1845
1986
  """Build the `range-cost` parser (registered via _REGISTRATION; #279 S6 W3).
1846
1987
 
1847
1988
  Move-only extraction of the former inline build_parser() block;
1848
1989
  call-time `c = _cctally()` binding, --help bytes unchanged.
1849
1990
  """
1850
1991
  c = _cctally()
1992
+ range_description = {
1993
+ None: "Compute USD cost for an absolute time range from Claude, Codex, or both providers.",
1994
+ "claude": "Compute USD cost for a Claude absolute time range.",
1995
+ "codex": "Compute USD cost for a Codex absolute time range.",
1996
+ }[fixed_source]
1851
1997
  rc = subparsers.add_parser(
1852
1998
  name,
1853
1999
  help=help_text,
1854
2000
  formatter_class=CLIHelpFormatter,
1855
- description="Compute USD cost for Claude Code usage between start/end timestamps.",
2001
+ description=range_description,
1856
2002
  epilog=textwrap.dedent("""\
1857
2003
  Examples:
1858
2004
  cctally range-cost -s "2026-04-10T10:00:00+03:00"
@@ -1887,19 +2033,20 @@ def _build_range_cost_parser(subparsers, name, *, help_text, xref=None):
1887
2033
  action="store_true",
1888
2034
  help="Show per-model usage and cost breakdown.",
1889
2035
  )
1890
- rc.add_argument(
1891
- "--json",
1892
- action="store_true",
1893
- dest="json",
1894
- help="Output JSON.",
1895
- )
1896
2036
  rc.add_argument(
1897
2037
  "--total-only",
1898
2038
  action="store_true",
1899
2039
  dest="total_only",
1900
2040
  help="Print numeric USD total only.",
1901
2041
  )
2042
+ rc.add_argument(
2043
+ "--reveal-projects", action="store_true", dest="reveal_projects",
2044
+ help="In --format output, show real project basenames instead of "
2045
+ "the default project-1, project-2, ... anonymization.",
2046
+ )
1902
2047
  _add_ccusage_alias_args(rc, ansi_emit=False)
2048
+ _add_source_args(rc, fixed_source=fixed_source, speed=True)
2049
+ _add_share_args(rc)
1903
2050
  rc.set_defaults(func=c.cmd_range_cost)
1904
2051
 
1905
2052
  def _build_five_hour_blocks_parser(subparsers, name, *, help_text, xref=None):
@@ -1989,20 +2136,34 @@ def _build_cache_sync_parser(subparsers, name, *, help_text, xref=None):
1989
2136
  )
1990
2137
  p_cache_sync.set_defaults(func=c.cmd_cache_sync)
1991
2138
 
1992
- def _build_project_parser(subparsers, name, *, help_text, xref=None):
2139
+ def _build_project_parser(subparsers, name, *, help_text, xref=None, fixed_source=None):
1993
2140
  """Build the `project` parser (registered via _REGISTRATION; #279 S6 W3).
1994
2141
 
1995
2142
  Move-only extraction of the former inline build_parser() block;
1996
2143
  call-time `c = _cctally()` binding, --help bytes unchanged.
1997
2144
  """
1998
2145
  c = _cctally()
2146
+ project_description = {
2147
+ None: (
2148
+ "Aggregate project usage. Claude uses subscription weeks; Codex uses "
2149
+ "calendar weeks; --source all uses an absolute calendar range."
2150
+ ),
2151
+ "claude": "Aggregate Claude project usage for subscription weeks.",
2152
+ "codex": "Aggregate Codex project usage for calendar weeks.",
2153
+ }[fixed_source]
1999
2154
  p_project = subparsers.add_parser(
2000
2155
  name,
2001
2156
  help=help_text,
2002
2157
  formatter_class=CLIHelpFormatter,
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.",
2005
- epilog=textwrap.dedent("""\
2158
+ description=project_description,
2159
+ epilog=textwrap.dedent(("""\
2160
+ Examples:
2161
+ cctally codex project
2162
+ cctally codex project --weeks 4
2163
+ cctally codex project --project project:0123456789abcdef01234567
2164
+ cctally codex project --breakdown --sort cost --order desc
2165
+ cctally codex project --group full-path --json
2166
+ """ if fixed_source == "codex" else """\
2006
2167
  Examples:
2007
2168
  cctally project
2008
2169
  cctally project --weeks 4
@@ -2010,25 +2171,34 @@ def _build_project_parser(subparsers, name, *, help_text, xref=None):
2010
2171
  cctally project --project ccusage --model sonnet
2011
2172
  cctally project --breakdown --sort used --order desc
2012
2173
  cctally project --group full-path --json
2013
- """),
2174
+ """)),
2014
2175
  )
2015
2176
  _add_since_until_args(
2016
2177
  p_project, metavar_since="YYYYMMDD", metavar_until="YYYYMMDD",
2017
2178
  help_since="Inclusive start date (YYYY-MM-DD or YYYYMMDD).",
2018
2179
  help_until="Inclusive end date (YYYY-MM-DD or YYYYMMDD).")
2019
2180
  p_project.add_argument("--weeks", type=int, default=None,
2020
- help="Last N subscription weeks ending now.")
2181
+ help=("Last N Codex calendar weeks ending now."
2182
+ if fixed_source == "codex" else
2183
+ "Last N weeks ending now: Claude subscription, Codex calendar; "
2184
+ "all uses an absolute calendar range."))
2021
2185
  p_project.add_argument("--project", action="append", default=[], metavar="PATTERN",
2022
- help="Substring filter on project display key (repeatable, OR).")
2186
+ help=("Filter by an exact opaque project key or collision-safe display "
2187
+ "label (repeatable, OR)." if fixed_source == "codex" else
2188
+ "Substring filter on project display key (repeatable, OR)."))
2023
2189
  p_project.add_argument("--model", action="append", default=[], metavar="PATTERN",
2024
2190
  help="Substring filter on model name (repeatable, OR).")
2025
2191
  p_project.add_argument("-b", "--breakdown", action="store_true",
2026
2192
  help="Add per-model child rows under each project.")
2027
2193
  p_project.add_argument("-o", "--order", choices=("asc", "desc"), default="desc",
2028
2194
  help="Sort direction (default: desc).")
2029
- p_project.add_argument("--sort", choices=("cost", "used", "name", "last-seen"),
2195
+ p_project.add_argument("--sort", choices=(("cost", "name", "last-seen")
2196
+ if fixed_source == "codex"
2197
+ else ("cost", "used", "name", "last-seen")),
2030
2198
  default="cost",
2031
- help="Sort key (default: cost).")
2199
+ help=("Sort key (default: cost; Claude-only used-percent ordering is "
2200
+ "unavailable)." if fixed_source == "codex" else
2201
+ "Sort key (default: cost)."))
2032
2202
  p_project.add_argument("--group", choices=("git-root", "full-path"), default="git-root",
2033
2203
  help="Bucket by resolved git-root (default) or raw project_path.")
2034
2204
  p_project.add_argument("--reveal-projects", action="store_true", dest="reveal_projects",
@@ -2040,10 +2210,11 @@ def _build_project_parser(subparsers, name, *, help_text, xref=None):
2040
2210
  help="Display timezone: local, utc, or IANA name. "
2041
2211
  "Overrides config display.tz for this call.")
2042
2212
  _add_ccusage_alias_args(p_project, ansi_emit=True)
2213
+ _add_source_args(p_project, fixed_source=fixed_source, speed=True)
2043
2214
  _add_share_args(p_project)
2044
2215
  p_project.set_defaults(func=c.cmd_project)
2045
2216
 
2046
- def _build_diff_parser(subparsers, name, *, help_text, xref=None):
2217
+ def _build_diff_parser(subparsers, name, *, help_text, xref=None, fixed_source=None):
2047
2218
  """Build the `diff` parser (registered via _REGISTRATION; #279 S6 W3).
2048
2219
 
2049
2220
  Move-only extraction of the former inline build_parser() block;
@@ -2059,7 +2230,11 @@ def _build_diff_parser(subparsers, name, *, help_text, xref=None):
2059
2230
  diff_p.add_argument("--b", required=True, help="Window B token (same grammar as --a)")
2060
2231
  diff_p.add_argument("--allow-mismatch", action="store_true",
2061
2232
  help="Permit mismatched window lengths (deltas normalized per-day)")
2062
- diff_p.add_argument("--only", help="Comma-separated section list (overall,models,projects,cache)")
2233
+ diff_p.add_argument(
2234
+ "--only",
2235
+ help=("Comma-separated section list: overall,models,projects; "
2236
+ "cache (Claude only); token-reuse (Codex only)"),
2237
+ )
2063
2238
  diff_p.add_argument("--with", dest="with_extra",
2064
2239
  help="Comma-separated opt-in sections (trend,time)")
2065
2240
  diff_p.add_argument("--all", dest="show_all", action="store_true",
@@ -2077,10 +2252,16 @@ def _build_diff_parser(subparsers, name, *, help_text, xref=None):
2077
2252
  help="Display timezone: local, utc, or IANA name. "
2078
2253
  "Overrides config display.tz for this call.")
2079
2254
  diff_p.add_argument("--no-color", action="store_true")
2080
- diff_p.add_argument("--json", dest="emit_json", action="store_true")
2081
2255
  diff_p.add_argument("--width", type=int, help=argparse.SUPPRESS)
2082
2256
  diff_p.add_argument("--debug-now", action="store_true", help=argparse.SUPPRESS)
2257
+ diff_p.add_argument(
2258
+ "--reveal-projects", action="store_true", dest="reveal_projects",
2259
+ help="In --format output, show real project basenames instead of "
2260
+ "the default project-1, project-2, ... anonymization.",
2261
+ )
2083
2262
  _add_ccusage_alias_args(diff_p, ansi_emit=True)
2263
+ _add_source_args(diff_p, fixed_source=fixed_source, speed=True)
2264
+ _add_share_args(diff_p, json_dest="emit_json")
2084
2265
  diff_p.set_defaults(func=c.cmd_diff)
2085
2266
 
2086
2267
  def _build_claude_parser(subparsers, name, *, help_text, xref=None):
@@ -2128,6 +2309,16 @@ def _build_claude_parser(subparsers, name, *, help_text, xref=None):
2128
2309
  help_text="Compact one-line status for Claude Code hooks",
2129
2310
  xref="Canonical `cctally claude statusline` (flat alias: `cctally statusline`). "
2130
2311
  "Drop-in for `ccusage statusline` plus cctally extension segments.")
2312
+ _build_project_parser(claude_sub, "project",
2313
+ help_text="Roll usage up by project", fixed_source="claude")
2314
+ _build_diff_parser(claude_sub, "diff",
2315
+ help_text="Compare usage between two windows", fixed_source="claude")
2316
+ _build_range_cost_parser(claude_sub, "range-cost",
2317
+ help_text="Compute USD cost for a time range", fixed_source="claude")
2318
+ _build_cache_report_parser(claude_sub, "cache-report",
2319
+ help_text="Show cache analytics", fixed_source="claude")
2320
+ _build_report_parser(claude_sub, "report",
2321
+ help_text="Show dollars-per-percent report", fixed_source="claude")
2131
2322
 
2132
2323
  def _build_codex_parser(subparsers, name, *, help_text, xref=None):
2133
2324
  """Build the `codex` parser (registered via _REGISTRATION; #279 S6 W3).
@@ -2163,6 +2354,18 @@ def _build_codex_parser(subparsers, name, *, help_text, xref=None):
2163
2354
  help_text="Show Codex usage grouped by week",
2164
2355
  xref="cctally extension (no upstream `ccusage codex weekly`). Same engine as "
2165
2356
  "`cctally codex-weekly`.")
2357
+ _build_project_parser(codex_sub, "project",
2358
+ help_text="Roll Codex usage up by qualified project", fixed_source="codex")
2359
+ _build_diff_parser(codex_sub, "diff",
2360
+ help_text="Compare Codex usage between two windows", fixed_source="codex")
2361
+ _build_range_cost_parser(codex_sub, "range-cost",
2362
+ help_text="Compute Codex USD cost for a time range", fixed_source="codex")
2363
+ _build_cache_report_parser(codex_sub, "cache-report",
2364
+ help_text="Show Codex token-reuse analytics", fixed_source="codex")
2365
+ _build_report_parser(codex_sub, "report",
2366
+ help_text="Show Codex quota-window report", fixed_source="codex")
2367
+ _build_codex_quota_parser(codex_sub, "quota",
2368
+ help_text="Native root-qualified Codex quota reports")
2166
2369
 
2167
2370
  def _build_config_parser(subparsers, name, *, help_text, xref=None):
2168
2371
  """Build the `config` parser (registered via _REGISTRATION; #279 S6 W3).
@@ -2726,6 +2929,10 @@ def _build_hook_tick_parser(subparsers, name, *, help_text, xref=None):
2726
2929
  "(used by --explain and tests)")
2727
2930
  ht.add_argument("--mock-oauth-response", type=str, default=None,
2728
2931
  help=argparse.SUPPRESS) # JSON string fed to mock fetch (tests only)
2932
+ ht.add_argument("--foreground", action="store_true",
2933
+ help=argparse.SUPPRESS) # Codex native hook wrapper
2934
+ ht.add_argument("--source", choices=("claude", "codex"), default="claude",
2935
+ help=argparse.SUPPRESS) # setup-managed native Codex hook
2729
2936
  ht.set_defaults(func=c.cmd_hook_tick)
2730
2937
 
2731
2938
  def _build_preview_parser(subparsers, name, *, help_text, xref=None):
@@ -2905,8 +3112,8 @@ _REGISTRATION = (
2905
3112
  _Reg('record-usage', _build_record_usage_parser, "Record usage data from Claude Code status line", None, None),
2906
3113
  _Reg('record-credit', _build_record_credit_parser, "Record an in-place weekly credit the auto-detector misses", None, None),
2907
3114
  _Reg('refresh-usage', _build_refresh_usage_parser, "Force-fetch 7d/5h percent from OAuth API and record it", None, None),
2908
- _Reg('cache-report', _build_cache_report_parser, "Show daily cache hit rates per model from ccusage data", None, None),
2909
- _Reg('range-cost', _build_range_cost_parser, "Compute USD cost for a time range from session data", None, None),
3115
+ _Reg('cache-report', _build_cache_report_parser, "Show Claude cache diagnostics or Codex token reuse", None, None),
3116
+ _Reg('range-cost', _build_range_cost_parser, "Compute USD cost for a provider-aware time range", None, None),
2910
3117
  _Reg('blocks', _build_blocks_parser, "Show usage report grouped by 5-hour session blocks", "Alias of `cctally claude blocks` (the canonical form).", None),
2911
3118
  _Reg('statusline', _build_statusline_parser, "Compact one-line status for Claude Code hooks", "Alias of `cctally claude statusline` (the canonical form).", None),
2912
3119
  _Reg('five-hour-blocks', _build_five_hour_blocks_parser, "List API-anchored 5h blocks with rollup totals + 7d-drift columns", None, None),
@@ -2918,7 +3125,7 @@ _REGISTRATION = (
2918
3125
  _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),
2919
3126
  _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),
2920
3127
  _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),
2921
- _Reg('project', _build_project_parser, "Roll usage up by project (git-root), with per-project Used %% attribution", None, None),
3128
+ _Reg('project', _build_project_parser, "Roll Claude/Codex usage up by project", None, None),
2922
3129
  _Reg('diff', _build_diff_parser, "Compare Claude usage between two windows.", None, None),
2923
3130
  _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),
2924
3131
  _Reg('transcript', _build_transcript_parser, "Export or search conversation transcripts (anonymized export by default)", None, None),
@@ -2989,4 +3196,3 @@ def build_parser() -> argparse.ArgumentParser:
2989
3196
  if getattr(a, "help", None) is not argparse.SUPPRESS
2990
3197
  ]
2991
3198
  return p
2992
-