cctally 1.82.0 → 1.83.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/README.md +52 -74
  3. package/bin/_cctally_alerts.py +8 -1
  4. package/bin/_cctally_cache.py +963 -149
  5. package/bin/_cctally_config.py +43 -4
  6. package/bin/_cctally_core.py +933 -759
  7. package/bin/_cctally_dashboard.py +157 -47
  8. package/bin/_cctally_dashboard_cache_report.py +13 -6
  9. package/bin/_cctally_dashboard_conversation.py +1 -0
  10. package/bin/_cctally_dashboard_envelope.py +186 -8
  11. package/bin/_cctally_dashboard_share.py +60 -20
  12. package/bin/_cctally_dashboard_sources.py +427 -128
  13. package/bin/_cctally_db.py +605 -128
  14. package/bin/_cctally_doctor.py +413 -28
  15. package/bin/_cctally_five_hour.py +12 -5
  16. package/bin/_cctally_journal.py +2050 -156
  17. package/bin/_cctally_journal_repair.py +519 -0
  18. package/bin/_cctally_milestone_history.py +142 -56
  19. package/bin/_cctally_milestones.py +179 -111
  20. package/bin/_cctally_parser.py +42 -0
  21. package/bin/_cctally_project.py +24 -18
  22. package/bin/_cctally_quota.py +139 -25
  23. package/bin/_cctally_record.py +279 -108
  24. package/bin/_cctally_rederive.py +1052 -0
  25. package/bin/_cctally_reporting.py +58 -53
  26. package/bin/_cctally_setup.py +1 -0
  27. package/bin/_cctally_source_analytics.py +4 -1
  28. package/bin/_cctally_statusline.py +11 -11
  29. package/bin/_cctally_store.py +1039 -31
  30. package/bin/_cctally_sync_week.py +17 -8
  31. package/bin/_cctally_tui.py +421 -54
  32. package/bin/_cctally_update.py +133 -8
  33. package/bin/_cctally_weekrefs.py +14 -0
  34. package/bin/_lib_aggregators.py +10 -6
  35. package/bin/_lib_cache_report.py +101 -9
  36. package/bin/_lib_codex_pools.py +82 -0
  37. package/bin/_lib_conversation_query.py +126 -33
  38. package/bin/_lib_dashboard_sources.py +126 -1
  39. package/bin/_lib_diff_kernel.py +28 -15
  40. package/bin/_lib_doctor.py +342 -4
  41. package/bin/_lib_journal.py +924 -2
  42. package/bin/_lib_jsonl.py +43 -14
  43. package/bin/_lib_pricing.py +140 -21
  44. package/bin/_lib_readme_refresh.py +401 -0
  45. package/bin/_lib_rederive.py +395 -0
  46. package/bin/_lib_share.py +58 -2
  47. package/bin/cctally +56 -8
  48. package/dashboard/static/assets/{index-DJP4gEB7.js → index-3bgCMVHb.js} +52 -52
  49. package/dashboard/static/assets/index-D27EIHEI.css +1 -0
  50. package/dashboard/static/dashboard.html +2 -2
  51. package/package.json +6 -1
  52. package/dashboard/static/assets/index-Dk1nplOz.css +0 -1
@@ -376,64 +376,69 @@ def cmd_weekly(args: argparse.Namespace) -> int:
376
376
 
377
377
  conn = open_db()
378
378
 
379
- # Build the subscription-week list spanning the range. Boundaries are
380
- # anchored in `weekly_usage_snapshots` when available and otherwise
381
- # extrapolated (see `_compute_subscription_weeks`). Pass the
382
- # `--config`-honoring resolved config (issue #88) so the no-snapshot
383
- # calendar-week fallback uses the explicit override's `week_start`.
384
- weeks = c._compute_subscription_weeks(
385
- conn, range_start, range_end, config=config,
386
- account_key=acct_key, # #341: None = merged (all-accounts) read
387
- )
379
+ try:
380
+ # Build the subscription-week list spanning the range. Boundaries are
381
+ # anchored in `weekly_usage_snapshots` when available and otherwise
382
+ # extrapolated (see `_compute_subscription_weeks`). Pass the
383
+ # `--config`-honoring resolved config (issue #88) so the no-snapshot
384
+ # calendar-week fallback uses the explicit override's `week_start`.
385
+ weeks = c._compute_subscription_weeks(
386
+ conn, range_start, range_end, config=config,
387
+ account_key=acct_key, # #341: None = merged (all-accounts) read
388
+ )
388
389
 
389
- # Fetch entries and aggregate.
390
- # Cover each SubWeek's full [start_ts, end_ts) on the range_start side —
391
- # `_compute_subscription_weeks` can emit weeks whose start_ts precedes
392
- # range_start (any week overlapping the range). Without widening, boundary
393
- # weeks get tail-only cost divided by full-week usedPct → understated
394
- # totalCost and $/1%. range_end stays as the upper bound so historical
395
- # `--until <past>` queries still clip the tail week (paired with the
396
- # as_of_utc bound on get_latest_usage_for_week below).
397
- if weeks:
398
- fetch_start = min(
399
- range_start,
400
- parse_iso_datetime(weeks[0].start_ts, "week_start_at"),
390
+ # Fetch entries and aggregate.
391
+ # Cover each SubWeek's full [start_ts, end_ts) on the range_start side —
392
+ # `_compute_subscription_weeks` can emit weeks whose start_ts precedes
393
+ # range_start (any week overlapping the range). Without widening,
394
+ # boundary weeks get tail-only cost divided by full-week usedPct →
395
+ # understated totalCost and $/1%. range_end stays as the upper bound so
396
+ # historical `--until <past>` queries still clip the tail week (paired
397
+ # with the as_of_utc bound on get_latest_usage_for_week below).
398
+ if weeks:
399
+ fetch_start = min(
400
+ range_start,
401
+ parse_iso_datetime(weeks[0].start_ts, "week_start_at"),
402
+ )
403
+ else:
404
+ fetch_start = range_start
405
+ all_entries = c.get_entries(
406
+ fetch_start, range_end, account_key=acct_key
401
407
  )
402
- else:
403
- fetch_start = range_start
404
- all_entries = c.get_entries(fetch_start, range_end, account_key=acct_key)
405
408
 
406
- c._emit_debug_samples_if_set(
407
- args, all_entries, command_label="weekly",
408
- )
409
+ c._emit_debug_samples_if_set(
410
+ args, all_entries, command_label="weekly",
411
+ )
409
412
 
410
- # Bound the usage-snapshot lookup to `<= range_end` so historical
411
- # `--until <past date>` queries pick the usage% that was current at
412
- # the end of the requested window rather than the globally latest
413
- # snapshot for the week. Cost is already truncated to `range_end` by
414
- # `_aggregate_weekly`, so using a later usedPct would produce a
415
- # silently wrong $/1%. Match the stored `captured_at_utc` format
416
- # (see now_utc_iso): UTC, seconds precision, `Z` suffix — otherwise
417
- # lexicographic string compare inside SQLite would misorder `+00:00`
418
- # vs. `Z` at the same instant.
419
- as_of_utc = (
420
- range_end.astimezone(dt.timezone.utc)
421
- .replace(microsecond=0)
422
- .isoformat()
423
- .replace("+00:00", "Z")
424
- )
413
+ # Bound the usage-snapshot lookup to `<= range_end` so historical
414
+ # `--until <past date>` queries pick the usage% that was current at
415
+ # the end of the requested window rather than the globally latest
416
+ # snapshot for the week. Cost is already truncated to `range_end` by
417
+ # `_aggregate_weekly`, so using a later usedPct would produce a
418
+ # silently wrong $/1%. Match the stored `captured_at_utc` format
419
+ # (see now_utc_iso): UTC, seconds precision, `Z` suffix — otherwise
420
+ # lexicographic string compare inside SQLite would misorder `+00:00`
421
+ # vs. `Z` at the same instant.
422
+ as_of_utc = (
423
+ range_end.astimezone(dt.timezone.utc)
424
+ .replace(microsecond=0)
425
+ .isoformat()
426
+ .replace("+00:00", "Z")
427
+ )
425
428
 
426
- # Build the unified weekly view (spec §5.3): runs _aggregate_weekly,
427
- # overlays weekly_usage_snapshots per WeekRef. view.aggregated is
428
- # the BucketUsage tuple newest-first; view.overlay is the parallel
429
- # (used_pct, dollar_per_pct) tuple. We reverse both for CLI's
430
- # default asc rendering so the existing renderer's len-equality
431
- # assertions stay aligned.
432
- view = c.build_weekly_view(
433
- conn, all_entries, weeks=weeks, now_utc=now_utc,
434
- display_tz=args._resolved_tz, as_of_utc=as_of_utc, mode=args.mode,
435
- account_key=acct_key,
436
- )
429
+ # Build the unified weekly view (spec §5.3): runs _aggregate_weekly,
430
+ # overlays weekly_usage_snapshots per WeekRef. view.aggregated is
431
+ # the BucketUsage tuple newest-first; view.overlay is the parallel
432
+ # (used_pct, dollar_per_pct) tuple. We reverse both for CLI's
433
+ # default asc rendering so the existing renderer's len-equality
434
+ # assertions stay aligned.
435
+ view = c.build_weekly_view(
436
+ conn, all_entries, weeks=weeks, now_utc=now_utc,
437
+ display_tz=args._resolved_tz, as_of_utc=as_of_utc, mode=args.mode,
438
+ account_key=acct_key,
439
+ )
440
+ finally:
441
+ conn.close()
437
442
  buckets = list(reversed(view.aggregated))
438
443
  overlay = list(reversed(view.overlay))
439
444
 
@@ -2733,6 +2733,7 @@ def _setup_install(args: argparse.Namespace) -> int:
2733
2733
  lambda active_conn: c.sync_cache(
2734
2734
  active_conn, progress=progress.sync_callback
2735
2735
  ),
2736
+ origin="setup.bootstrap.claude_sync",
2736
2737
  )
2737
2738
  )
2738
2739
  finally:
@@ -489,7 +489,9 @@ def load_qualified_codex_entries(
489
489
  if sync:
490
490
  cache_mod = c._load_sibling("_cctally_cache")
491
491
  stats, conn = cache_mod._run_cache_operation_with_recovery(
492
- conn, lambda active_conn: c.sync_codex_cache(active_conn)
492
+ conn,
493
+ lambda active_conn: c.sync_codex_cache(active_conn),
494
+ origin="source.analytics.codex_sync",
493
495
  )
494
496
  if stats.lock_contended:
495
497
  raise QualifiedMetadataUnavailable("Codex qualified project metadata is unavailable")
@@ -2334,6 +2336,7 @@ def cmd_source_report(args: object) -> int:
2334
2336
  cache_mod._run_cache_operation_with_recovery(
2335
2337
  cache,
2336
2338
  lambda active_conn: c.sync_codex_cache(active_conn),
2339
+ origin="source.report.codex_sync",
2337
2340
  )
2338
2341
  )
2339
2342
  finally:
@@ -1497,7 +1497,7 @@ def _build_statusline_injections(warn_once):
1497
1497
  "SELECT se.timestamp_utc, se.model, "
1498
1498
  " se.input_tokens, se.output_tokens, "
1499
1499
  " se.cache_create_tokens, se.cache_read_tokens, "
1500
- " se.cost_usd_raw "
1500
+ " se.cost_usd_raw, se.cache_create_1h_tokens, se.speed "
1501
1501
  "FROM session_entries se "
1502
1502
  "LEFT JOIN session_files sf ON sf.path = se.source_path "
1503
1503
  "WHERE sf.session_id = ?"
@@ -1514,16 +1514,16 @@ def _build_statusline_injections(warn_once):
1514
1514
  return None
1515
1515
  total = 0.0
1516
1516
  for r in rows:
1517
- usage = {
1518
- "input_tokens": r[2] or 0,
1519
- "output_tokens": r[3] or 0,
1520
- "cache_creation_input_tokens": r[4] or 0,
1521
- "cache_read_input_tokens": r[5] or 0,
1522
- }
1523
- # #181: cost is token-only (_calculate_entry_cost ignores `speed`),
1524
- # so the statusline cost path no longer selects or json.loads the
1525
- # usage_extra_json blob — output is byte-identical. r[6] is still
1526
- # cost_usd_raw (the dropped column was the trailing slot).
1517
+ usage = c.claude_usage_dict( # #195 chokepoint
1518
+ input_tokens=r[2],
1519
+ output_tokens=r[3],
1520
+ cache_creation_tokens=r[4],
1521
+ cache_read_tokens=r[5],
1522
+ cache_1h_tokens=r[7],
1523
+ speed=r[8],
1524
+ )
1525
+ # #413: speed is materialized, so the hot statusline path keeps the
1526
+ # effective tier without parsing the legacy usage_extra_json blob.
1527
1527
  try:
1528
1528
  total += c._calculate_entry_cost(
1529
1529
  r[1], usage, mode="auto", cost_usd=r[6],