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_core.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
917
|
-
|
|
918
|
-
#
|
|
919
|
-
#
|
|
920
|
-
#
|
|
921
|
-
#
|
|
922
|
-
#
|
|
923
|
-
#
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
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 (
|