cctally 1.67.1 → 1.69.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 +68 -0
- package/bin/_cctally_alerts.py +54 -2
- package/bin/_cctally_cache.py +754 -109
- package/bin/_cctally_cache_report.py +41 -17
- package/bin/_cctally_codex.py +109 -4
- package/bin/_cctally_config.py +368 -2
- package/bin/_cctally_core.py +187 -15
- package/bin/_cctally_dashboard.py +679 -5
- package/bin/_cctally_dashboard_conversation.py +9 -4
- package/bin/_cctally_dashboard_envelope.py +110 -1
- package/bin/_cctally_dashboard_share.py +557 -79
- package/bin/_cctally_db.py +366 -17
- package/bin/_cctally_diff.py +49 -16
- package/bin/_cctally_doctor.py +131 -0
- package/bin/_cctally_forecast.py +12 -4
- package/bin/_cctally_parser.py +321 -92
- package/bin/_cctally_project.py +24 -8
- package/bin/_cctally_quota.py +1381 -0
- package/bin/_cctally_record.py +319 -14
- package/bin/_cctally_refresh.py +105 -3
- package/bin/_cctally_reporting.py +7 -1
- package/bin/_cctally_setup.py +343 -113
- package/bin/_cctally_statusline.py +228 -0
- package/bin/_cctally_tui.py +787 -7
- package/bin/_lib_alert_axes.py +11 -0
- package/bin/_lib_codex_hooks.py +367 -0
- package/bin/_lib_conversation_query.py +156 -67
- package/bin/_lib_doctor.py +238 -0
- package/bin/_lib_jsonl.py +529 -162
- package/bin/_lib_quota.py +566 -0
- package/bin/_lib_share.py +152 -34
- package/bin/_lib_snapshot_cache.py +26 -0
- package/bin/_lib_source_identity.py +74 -0
- package/bin/cctally +325 -10
- package/dashboard/static/assets/index-CBbErI-P.js +80 -0
- package/dashboard/static/assets/index-kDDVOLa_.css +1 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +5 -1
- package/dashboard/static/assets/index-BybNp_Di.js +0 -80
- package/dashboard/static/assets/index-DgAmLK65.css +0 -1
|
@@ -918,13 +918,13 @@ def _resolve_cache_report_window(
|
|
|
918
918
|
return since, until
|
|
919
919
|
|
|
920
920
|
|
|
921
|
-
def
|
|
921
|
+
def _cache_report_json_payload(
|
|
922
922
|
rows: list["crk.CacheRow"],
|
|
923
923
|
mode: str,
|
|
924
924
|
*,
|
|
925
925
|
now_utc: dt.datetime | None = None,
|
|
926
|
-
) -> str:
|
|
927
|
-
"""
|
|
926
|
+
) -> dict[str, Any]:
|
|
927
|
+
"""Build rows + totals matching the cache-report JSON schema.
|
|
928
928
|
|
|
929
929
|
Daily mode keeps the top-level `days` key and per-row `totalCost` /
|
|
930
930
|
totals.totalCost aliases for backward compat. Session mode uses
|
|
@@ -1010,7 +1010,19 @@ def _emit_cache_report_json(
|
|
|
1010
1010
|
for row_dict in output[top_key]:
|
|
1011
1011
|
row_dict["totalCost"] = row_dict["cost"]
|
|
1012
1012
|
output["totals"]["totalCost"] = output["totals"]["cost"]
|
|
1013
|
-
return
|
|
1013
|
+
return _cctally().stamp_schema_version(output)
|
|
1014
|
+
|
|
1015
|
+
|
|
1016
|
+
def _emit_cache_report_json(
|
|
1017
|
+
rows: list["crk.CacheRow"],
|
|
1018
|
+
mode: str,
|
|
1019
|
+
*,
|
|
1020
|
+
now_utc: dt.datetime | None = None,
|
|
1021
|
+
) -> str:
|
|
1022
|
+
"""Serialize :func:`_cache_report_json_payload` without changing bytes."""
|
|
1023
|
+
return json.dumps(
|
|
1024
|
+
_cache_report_json_payload(rows, mode, now_utc=now_utc), indent=2,
|
|
1025
|
+
)
|
|
1014
1026
|
|
|
1015
1027
|
|
|
1016
1028
|
def _build_cache_report_title(args: argparse.Namespace, mode: str) -> str:
|
|
@@ -1094,6 +1106,7 @@ def _sort_cache_rows(
|
|
|
1094
1106
|
|
|
1095
1107
|
def cmd_cache_report(args: argparse.Namespace) -> int:
|
|
1096
1108
|
c = _cctally()
|
|
1109
|
+
c._share_validate_args(args)
|
|
1097
1110
|
config = c._load_claude_config_for_args(args)
|
|
1098
1111
|
# Session A (spec §7.2): bridge -z/--timezone into args.tz so the
|
|
1099
1112
|
# existing resolve_display_tz precedence absorbs the new alias. The
|
|
@@ -1140,12 +1153,15 @@ def cmd_cache_report(args: argparse.Namespace) -> int:
|
|
|
1140
1153
|
|
|
1141
1154
|
if since == until:
|
|
1142
1155
|
if args.json:
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1156
|
+
payload = c.stamp_schema_version(
|
|
1157
|
+
{top_key: [], "totals": None,
|
|
1158
|
+
"generatedAt": now_utc_iso(now_utc=now_utc)}
|
|
1159
|
+
)
|
|
1160
|
+
sink = getattr(args, "_source_result_sink", None)
|
|
1161
|
+
if sink is not None:
|
|
1162
|
+
sink(payload)
|
|
1163
|
+
else:
|
|
1164
|
+
print(json.dumps(payload, indent=2))
|
|
1149
1165
|
else:
|
|
1150
1166
|
print("(no cache activity in window)")
|
|
1151
1167
|
return 0
|
|
@@ -1163,12 +1179,15 @@ def cmd_cache_report(args: argparse.Namespace) -> int:
|
|
|
1163
1179
|
|
|
1164
1180
|
if not rows:
|
|
1165
1181
|
if args.json:
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1182
|
+
payload = c.stamp_schema_version(
|
|
1183
|
+
{top_key: [], "totals": None,
|
|
1184
|
+
"generatedAt": now_utc_iso(now_utc=now_utc)}
|
|
1185
|
+
)
|
|
1186
|
+
sink = getattr(args, "_source_result_sink", None)
|
|
1187
|
+
if sink is not None:
|
|
1188
|
+
sink(payload)
|
|
1189
|
+
else:
|
|
1190
|
+
print(json.dumps(payload, indent=2))
|
|
1172
1191
|
else:
|
|
1173
1192
|
print("(no cache activity in window)")
|
|
1174
1193
|
return 0
|
|
@@ -1186,7 +1205,12 @@ def cmd_cache_report(args: argparse.Namespace) -> int:
|
|
|
1186
1205
|
_sort_cache_rows(rows, resolved_sort, mode)
|
|
1187
1206
|
|
|
1188
1207
|
if args.json:
|
|
1189
|
-
|
|
1208
|
+
payload = _cache_report_json_payload(rows, mode, now_utc=now_utc)
|
|
1209
|
+
sink = getattr(args, "_source_result_sink", None)
|
|
1210
|
+
if sink is not None:
|
|
1211
|
+
sink(payload)
|
|
1212
|
+
else:
|
|
1213
|
+
print(json.dumps(payload, indent=2))
|
|
1190
1214
|
return 0
|
|
1191
1215
|
|
|
1192
1216
|
title = _build_cache_report_title(args, mode)
|
package/bin/_cctally_codex.py
CHANGED
|
@@ -37,10 +37,14 @@ import json
|
|
|
37
37
|
import os
|
|
38
38
|
import sys
|
|
39
39
|
from dataclasses import dataclass, field
|
|
40
|
+
from datetime import timezone
|
|
40
41
|
|
|
41
42
|
from _cctally_core import WEEKDAY_MAP, _command_as_of, eprint, get_week_start_name
|
|
42
43
|
|
|
43
44
|
|
|
45
|
+
UTC = timezone.utc
|
|
46
|
+
|
|
47
|
+
|
|
44
48
|
def _cctally():
|
|
45
49
|
"""Resolve the current `cctally` module at call-time (spec §3.1)."""
|
|
46
50
|
return sys.modules["cctally"]
|
|
@@ -279,10 +283,84 @@ def _resolve_codex_speed(requested: str) -> str:
|
|
|
279
283
|
return requested
|
|
280
284
|
|
|
281
285
|
|
|
286
|
+
def _build_codex_share_snapshot(command: str, view, rows):
|
|
287
|
+
"""Build the four established Codex report artifacts without path fields."""
|
|
288
|
+
c = _cctally()
|
|
289
|
+
lib = c._share_load_lib()
|
|
290
|
+
end = getattr(view, "period_end", None) or _command_as_of()
|
|
291
|
+
start = getattr(view, "period_start", None) or end
|
|
292
|
+
if end < start:
|
|
293
|
+
start = end
|
|
294
|
+
display_tz = getattr(view, "display_tz_label", "UTC") or "UTC"
|
|
295
|
+
period_label = f"{start.date().isoformat()} → {end.date().isoformat()} ({display_tz})"
|
|
296
|
+
titles = {
|
|
297
|
+
"codex-daily": "Codex Token Usage — Daily",
|
|
298
|
+
"codex-monthly": "Codex Token Usage — Monthly",
|
|
299
|
+
"codex-weekly": "Codex Token Usage — Weekly",
|
|
300
|
+
"codex-session": "Codex Token Usage — Sessions",
|
|
301
|
+
}
|
|
302
|
+
if command not in titles:
|
|
303
|
+
raise ValueError(f"unknown Codex share command: {command}")
|
|
304
|
+
if command == "codex-session":
|
|
305
|
+
columns = (
|
|
306
|
+
lib.ColumnSpec(key="session", label="Session"),
|
|
307
|
+
lib.ColumnSpec(key="last", label="Last Activity"),
|
|
308
|
+
lib.ColumnSpec(key="tokens", label="Tokens", align="right"),
|
|
309
|
+
lib.ColumnSpec(key="cost", label="$ Cost", align="right"),
|
|
310
|
+
)
|
|
311
|
+
table_rows = tuple(
|
|
312
|
+
lib.Row(cells={
|
|
313
|
+
"session": lib.TextCell(f"Session {index + 1}"),
|
|
314
|
+
"last": lib.TextCell(getattr(row, "last_activity").astimezone(UTC).date().isoformat()),
|
|
315
|
+
"tokens": lib.TextCell(f"{int(getattr(row, 'total_tokens', 0)):,}"),
|
|
316
|
+
"cost": lib.MoneyCell(float(getattr(row, "cost_usd", 0.0))),
|
|
317
|
+
})
|
|
318
|
+
for index, row in enumerate(rows)
|
|
319
|
+
)
|
|
320
|
+
else:
|
|
321
|
+
first_label = {
|
|
322
|
+
"codex-daily": "Date",
|
|
323
|
+
"codex-monthly": "Month",
|
|
324
|
+
"codex-weekly": "Week",
|
|
325
|
+
}[command]
|
|
326
|
+
columns = (
|
|
327
|
+
lib.ColumnSpec(key="bucket", label=first_label),
|
|
328
|
+
lib.ColumnSpec(key="tokens", label="Tokens", align="right"),
|
|
329
|
+
lib.ColumnSpec(key="cost", label="$ Cost", align="right"),
|
|
330
|
+
)
|
|
331
|
+
table_rows = tuple(
|
|
332
|
+
lib.Row(cells={
|
|
333
|
+
"bucket": lib.TextCell(str(getattr(row, "bucket", "—"))),
|
|
334
|
+
"tokens": lib.TextCell(f"{int(getattr(row, 'total_tokens', 0)):,}"),
|
|
335
|
+
"cost": lib.MoneyCell(float(getattr(row, "cost_usd", 0.0))),
|
|
336
|
+
})
|
|
337
|
+
for row in rows
|
|
338
|
+
)
|
|
339
|
+
return lib.ShareSnapshot(
|
|
340
|
+
cmd=command,
|
|
341
|
+
title=titles[command],
|
|
342
|
+
subtitle=period_label,
|
|
343
|
+
period=lib.PeriodSpec(start=start, end=end, display_tz=display_tz, label=period_label),
|
|
344
|
+
columns=columns,
|
|
345
|
+
rows=table_rows,
|
|
346
|
+
chart=None,
|
|
347
|
+
totals=(
|
|
348
|
+
lib.Totalled(label="Total", value=f"${float(getattr(view, 'total_cost_usd', 0.0)):,.2f}"),
|
|
349
|
+
),
|
|
350
|
+
notes=(),
|
|
351
|
+
generated_at=end,
|
|
352
|
+
version=c._share_resolve_version(),
|
|
353
|
+
source="codex",
|
|
354
|
+
source_label="Codex",
|
|
355
|
+
availability="empty" if not rows else "ok",
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
|
|
282
359
|
def cmd_codex_daily(args: argparse.Namespace) -> int:
|
|
283
360
|
"""Show Codex usage report grouped by date (display tz, --tz, or --timezone)."""
|
|
284
361
|
c = _cctally()
|
|
285
|
-
|
|
362
|
+
c._share_validate_args(args)
|
|
363
|
+
config = c.load_config(getattr(args, "config", None))
|
|
286
364
|
tz_obj = c.resolve_display_tz(args, config)
|
|
287
365
|
args._resolved_tz = tz_obj
|
|
288
366
|
# Codex aggregators take a tz_name string. F2 fix: precedence is
|
|
@@ -312,6 +390,12 @@ def cmd_codex_daily(args: argparse.Namespace) -> int:
|
|
|
312
390
|
if args.order == "desc":
|
|
313
391
|
days = list(reversed(days))
|
|
314
392
|
|
|
393
|
+
if getattr(args, "format", None):
|
|
394
|
+
c._share_render_and_emit(
|
|
395
|
+
_build_codex_share_snapshot("codex-daily", view, days), args,
|
|
396
|
+
)
|
|
397
|
+
return 0
|
|
398
|
+
|
|
315
399
|
if not days:
|
|
316
400
|
# Match upstream's no-data sentinel (see _emit_codex_no_data docstring).
|
|
317
401
|
c._emit_codex_no_data(args, "daily")
|
|
@@ -347,7 +431,8 @@ def cmd_codex_daily(args: argparse.Namespace) -> int:
|
|
|
347
431
|
def cmd_codex_monthly(args: argparse.Namespace) -> int:
|
|
348
432
|
"""Show Codex usage report grouped by calendar month (display tz, --tz, or --timezone)."""
|
|
349
433
|
c = _cctally()
|
|
350
|
-
|
|
434
|
+
c._share_validate_args(args)
|
|
435
|
+
config = c.load_config(getattr(args, "config", None))
|
|
351
436
|
tz_obj = c.resolve_display_tz(args, config)
|
|
352
437
|
args._resolved_tz = tz_obj
|
|
353
438
|
# F2 fix: see cmd_codex_daily.
|
|
@@ -371,6 +456,12 @@ def cmd_codex_monthly(args: argparse.Namespace) -> int:
|
|
|
371
456
|
if args.order == "desc":
|
|
372
457
|
months = list(reversed(months))
|
|
373
458
|
|
|
459
|
+
if getattr(args, "format", None):
|
|
460
|
+
c._share_render_and_emit(
|
|
461
|
+
_build_codex_share_snapshot("codex-monthly", view, months), args,
|
|
462
|
+
)
|
|
463
|
+
return 0
|
|
464
|
+
|
|
374
465
|
if not months:
|
|
375
466
|
# Match upstream's no-data sentinel (see _emit_codex_no_data docstring).
|
|
376
467
|
c._emit_codex_no_data(args, "monthly")
|
|
@@ -407,7 +498,8 @@ def cmd_codex_weekly(args: argparse.Namespace) -> int:
|
|
|
407
498
|
"""Show Codex usage grouped by week (display tz, --tz, or --timezone)."""
|
|
408
499
|
c = _cctally()
|
|
409
500
|
now_utc = _command_as_of()
|
|
410
|
-
|
|
501
|
+
c._share_validate_args(args)
|
|
502
|
+
config = c.load_config(getattr(args, "config", None))
|
|
411
503
|
tz_obj = c.resolve_display_tz(args, config)
|
|
412
504
|
args._resolved_tz = tz_obj
|
|
413
505
|
# F2 fix: see cmd_codex_daily.
|
|
@@ -434,6 +526,12 @@ def cmd_codex_weekly(args: argparse.Namespace) -> int:
|
|
|
434
526
|
if args.order == "desc":
|
|
435
527
|
weeks = list(reversed(weeks))
|
|
436
528
|
|
|
529
|
+
if getattr(args, "format", None):
|
|
530
|
+
c._share_render_and_emit(
|
|
531
|
+
_build_codex_share_snapshot("codex-weekly", view, weeks), args,
|
|
532
|
+
)
|
|
533
|
+
return 0
|
|
534
|
+
|
|
437
535
|
if not weeks:
|
|
438
536
|
# Match upstream's no-data sentinel (same string daily/monthly use).
|
|
439
537
|
c._emit_codex_no_data(args, "weekly")
|
|
@@ -472,7 +570,8 @@ def cmd_codex_weekly(args: argparse.Namespace) -> int:
|
|
|
472
570
|
def cmd_codex_session(args: argparse.Namespace) -> int:
|
|
473
571
|
"""Show Codex usage report grouped by session (sorted by last activity)."""
|
|
474
572
|
c = _cctally()
|
|
475
|
-
|
|
573
|
+
c._share_validate_args(args)
|
|
574
|
+
config = c.load_config(getattr(args, "config", None))
|
|
476
575
|
tz_obj = c.resolve_display_tz(args, config)
|
|
477
576
|
args._resolved_tz = tz_obj
|
|
478
577
|
# F2 fix: see cmd_codex_daily.
|
|
@@ -498,6 +597,12 @@ def cmd_codex_session(args: argparse.Namespace) -> int:
|
|
|
498
597
|
if args.order == "asc":
|
|
499
598
|
sessions = list(reversed(sessions))
|
|
500
599
|
|
|
600
|
+
if getattr(args, "format", None):
|
|
601
|
+
c._share_render_and_emit(
|
|
602
|
+
_build_codex_share_snapshot("codex-session", view, sessions), args,
|
|
603
|
+
)
|
|
604
|
+
return 0
|
|
605
|
+
|
|
501
606
|
if not sessions:
|
|
502
607
|
# Match upstream's no-data sentinel (plural "sessions" matches upstream
|
|
503
608
|
# — confirmed in @ccusage/codex@18.0.8 dist/index.js around line 7962).
|