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.
Files changed (51) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/bin/_cctally_alerts.py +1 -1
  3. package/bin/_cctally_cache.py +262 -40
  4. package/bin/_cctally_cache_report.py +7 -5
  5. package/bin/_cctally_core.py +49 -14
  6. package/bin/_cctally_dashboard.py +827 -4911
  7. package/bin/_cctally_dashboard_cache_report.py +714 -0
  8. package/bin/_cctally_dashboard_conversation.py +771 -0
  9. package/bin/_cctally_dashboard_envelope.py +1520 -0
  10. package/bin/_cctally_dashboard_share.py +2012 -0
  11. package/bin/_cctally_db.py +127 -5
  12. package/bin/_cctally_doctor.py +104 -3
  13. package/bin/_cctally_five_hour.py +2 -1
  14. package/bin/_cctally_forecast.py +78 -135
  15. package/bin/_cctally_parser.py +919 -784
  16. package/bin/_cctally_percent_breakdown.py +1 -1
  17. package/bin/_cctally_pricing_check.py +39 -0
  18. package/bin/_cctally_project.py +8 -5
  19. package/bin/_cctally_record.py +279 -274
  20. package/bin/_cctally_reporting.py +1 -1
  21. package/bin/_cctally_sync_week.py +1 -1
  22. package/bin/_cctally_telemetry.py +3 -5
  23. package/bin/_cctally_tui.py +487 -254
  24. package/bin/_cctally_update.py +5 -0
  25. package/bin/_lib_alert_dispatch.py +1 -1
  26. package/bin/_lib_blocks.py +6 -1
  27. package/bin/_lib_conversation_query.py +349 -36
  28. package/bin/_lib_credit.py +133 -0
  29. package/bin/_lib_doctor.py +150 -1
  30. package/bin/_lib_five_hour.py +34 -0
  31. package/bin/_lib_forecast.py +144 -0
  32. package/bin/_lib_json_envelope.py +38 -0
  33. package/bin/_lib_jsonl.py +115 -73
  34. package/bin/_lib_log.py +96 -0
  35. package/bin/_lib_perf.py +180 -0
  36. package/bin/_lib_pricing.py +47 -1
  37. package/bin/_lib_pricing_check.py +25 -3
  38. package/bin/_lib_record.py +179 -0
  39. package/bin/_lib_render.py +18 -10
  40. package/bin/_lib_snapshot_cache.py +61 -0
  41. package/bin/_lib_transcript_access.py +23 -0
  42. package/bin/cctally +51 -7
  43. package/bin/cctally-dashboard +2 -2
  44. package/bin/cctally-statusline +1 -0
  45. package/bin/cctally-tui +2 -2
  46. package/dashboard/static/assets/index-CJTUCpKt.js +80 -0
  47. package/dashboard/static/assets/index-DQWNrIqu.css +1 -0
  48. package/dashboard/static/dashboard.html +2 -2
  49. package/package.json +11 -1
  50. package/dashboard/static/assets/index-0jzYm75p.css +0 -1
  51. package/dashboard/static/assets/index-D_Ylyqsf.js +0 -80
@@ -149,6 +149,20 @@ def _init_paths_from_env() -> None:
149
149
  CLAUDE_PROJECTS_DIR = home / ".claude" / "projects"
150
150
 
151
151
 
152
+ def _truthy_env(name: str) -> bool:
153
+ """A ``1``/``true``/``yes``/any-other-non-empty env value is truthy;
154
+ unset, empty, ``0``, ``false``, ``no`` are falsey (case-insensitive,
155
+ whitespace-stripped).
156
+
157
+ Canonical home for boolean env-flag parsing (#279 S1 F1) — presence-only
158
+ ``os.environ.get(...)`` checks made ``FLAG=0`` mean *enabled*, which for
159
+ ``CCTALLY_ALLOW_PROD_MIGRATION`` / ``CCTALLY_DISABLE_DEV_AUTODETECT`` was
160
+ the exact opposite of intent. ``_cctally_telemetry._truthy_env`` delegates
161
+ here."""
162
+ v = os.environ.get(name)
163
+ return v is not None and v.strip().lower() not in ("", "0", "false", "no")
164
+
165
+
152
166
  def _repo_root() -> pathlib.Path:
153
167
  """Repo root when running from a source checkout: this file lives at
154
168
  ``<repo>/bin/_cctally_core.py``, so the root is two parents up. Factored
@@ -165,7 +179,7 @@ def _is_dev_checkout() -> bool:
165
179
  the ``setup`` guard (which protects WHICH BINARY gets wired into
166
180
  ~/.claude/settings.json), not the data-dir relocation. The npm/brew
167
181
  install copies ship without ``.git`` so they never read True."""
168
- if os.environ.get("CCTALLY_DISABLE_DEV_AUTODETECT"):
182
+ if _truthy_env("CCTALLY_DISABLE_DEV_AUTODETECT"):
169
183
  return False
170
184
  return (_repo_root() / ".git").exists()
171
185
 
@@ -913,19 +927,40 @@ def open_db() -> sqlite3.Connection:
913
927
  ensure_dirs()
914
928
  conn = sqlite3.connect(DB_PATH)
915
929
  conn.row_factory = sqlite3.Row
916
- conn.execute("PRAGMA journal_mode=WAL")
917
- conn.execute("PRAGMA synchronous=NORMAL")
918
- # Explicit for intent + symmetry with open_cache_db (bin/_cctally_cache.py).
919
- # sqlite3.connect()'s default timeout=5.0 ALREADY maps to busy_timeout=5000,
920
- # so this is not a behavior change it makes the multi-writer retry window
921
- # an explicit contract beside the WAL pragmas instead of an inherited
922
- # default a reader has to know about. NOTE: busy_timeout does NOT absorb
923
- # SQLITE_BUSY_SNAPSHOT (a WAL read-then-write transaction whose snapshot is
924
- # invalidated by a competing commit raises "database is locked" instantly,
925
- # bypassing the busy handler). The write paths defend against that by taking
926
- # the write lock up front — BEGIN IMMEDIATE, or a write as the transaction's
927
- # first DML. See cctally-dev#87.
928
- conn.execute("PRAGMA busy_timeout=5000")
930
+ # #279 S1 F4: probe connect + initial PRAGMAs so a corrupt stats.db (the
931
+ # non-re-derivable DB) surfaces as a one-line diagnosis + exit 2 instead of
932
+ # a raw traceback. The catch boundary is DELIBERATELY narrow — ONLY the
933
+ # connect/PRAGMA/probe below. The DDL + `_run_pending_migrations` region
934
+ # further down is NOT wrapped: migration-handler failures have their own
935
+ # logging/suppression contract and must not be mislabeled as corruption
936
+ # (corruption that only surfaces mid-DDL stays a raw error; the probe
937
+ # catches the common case).
938
+ try:
939
+ conn.execute("PRAGMA journal_mode=WAL")
940
+ conn.execute("PRAGMA synchronous=NORMAL")
941
+ # Explicit for intent + symmetry with open_cache_db (bin/_cctally_cache.py).
942
+ # sqlite3.connect()'s default timeout=5.0 ALREADY maps to busy_timeout=5000,
943
+ # so this is not a behavior change — it makes the multi-writer retry window
944
+ # an explicit contract beside the WAL pragmas instead of an inherited
945
+ # default a reader has to know about. NOTE: busy_timeout does NOT absorb
946
+ # SQLITE_BUSY_SNAPSHOT (a WAL read-then-write transaction whose snapshot is
947
+ # invalidated by a competing commit raises "database is locked" instantly,
948
+ # bypassing the busy handler). The write paths defend against that by taking
949
+ # the write lock up front — BEGIN IMMEDIATE, or a write as the transaction's
950
+ # first DML. See cctally-dev#87.
951
+ conn.execute("PRAGMA busy_timeout=5000")
952
+ conn.execute("SELECT 1").fetchone()
953
+ except sqlite3.DatabaseError as exc:
954
+ try:
955
+ conn.close()
956
+ except Exception:
957
+ pass
958
+ raise c.StatsDbCorruptError(
959
+ f"stats.db appears corrupt or unreadable ({exc}). path: {DB_PATH}. "
960
+ f"Not auto-recreated — it holds your recorded usage history. "
961
+ f'Recovery: back up the file, then try `sqlite3 "{DB_PATH}" ".recover"`, '
962
+ f"or move it aside to start fresh. If this recurs, please file an issue."
963
+ ) from exc
929
964
  conn.execute(
930
965
  """
931
966
  CREATE TABLE IF NOT EXISTS weekly_usage_snapshots (