cctally 1.63.0 → 1.65.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 +22 -0
- package/README.md +7 -0
- package/bin/_cctally_cache.py +60 -19
- package/bin/_cctally_config.py +68 -0
- package/bin/_cctally_core.py +29 -0
- package/bin/_cctally_dashboard.py +364 -38
- package/bin/_cctally_doctor.py +24 -0
- package/bin/_cctally_parser.py +61 -0
- package/bin/_cctally_setup.py +22 -0
- package/bin/_cctally_telemetry.py +362 -0
- package/bin/_cctally_tui.py +445 -236
- package/bin/_cctally_update.py +53 -9
- package/bin/_lib_conversation_query.py +349 -36
- package/bin/_lib_doctor.py +32 -0
- package/bin/_lib_perf.py +180 -0
- package/bin/_lib_transcript_access.py +23 -0
- package/bin/cctally +98 -0
- package/bin/cctally-npm-postinstall.js +4 -0
- 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 +3 -1
- package/dashboard/static/assets/index-0jzYm75p.css +0 -1
- package/dashboard/static/assets/index-D_Ylyqsf.js +0 -80
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,28 @@ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [1.65.0] - 2026-07-09
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Opt-in backend performance instrumentation: set `CCTALLY_PERF_TRACE=1` to print a phase-timing trace to stderr on `cache-sync` (and the hidden `tui --render-once` build), and read live backend timings + cache state from the dashboard's loopback-only `/api/debug/backend` endpoint. The trace is off by default and never changes command output — it goes to stderr only, so stdout stays byte-identical. New [docs/backend-performance.md](docs/backend-performance.md) documents the read model, hot paths, and invariants.
|
|
12
|
+
- Reproducible backend benchmark suite (`bin/cctally-bench` + `bin/build-bench-fixtures.py`, a dev/maintainer tool — never shipped): in-process timings for the dashboard snapshot (cold/warm/idle), cache ingest (no-op/delta), the conversation rail/search/find/payload/outline, and the warm reconcile paths, measured against a deterministic synthetic fixture, with a committed advisory baseline and an opt-in `--compare`/`--gate` (never a CI gate) that guard the recent backend performance wins from silent regression. See [bench/README.md](bench/README.md).
|
|
13
|
+
- The opt-in `CCTALLY_PERF_TRACE` backend trace now breaks whole-session conversation assembly into named sub-phases (read, dedup, build, correlate, fold, classify, cost, finalize) under the reader's detail/paginate phases, and the loopback `/api/debug/backend` diagnostic can now surface a conversation-open trace (list/search/outline/find/export/prompts/detail) as well as the data-snapshot build — so a slow transcript open is attributable to a specific stage on a live dashboard; off by default, no command output changes.
|
|
14
|
+
- `cctally-bench --assembly-scan` (dev/maintainer tool): sweeps the cost of assembling a whole conversation across a size ladder against its own isolated fixture and baseline (`bench/baselines/assembly.json`), the measurement behind the recorded decision to defer materializing pre-rendered conversation turns — assembly stays well under a 100 ms visibility budget for all but pathologically large (multi-thousand-turn) sessions, and the reader already caps payload bytes. See [docs/backend-performance.md](docs/backend-performance.md) §5.
|
|
15
|
+
- The conversation browse rail and cross-session search can now be filtered by **model family**: a new "Model" section in the filter popover lists the Opus / Sonnet / Haiku / Fable families that actually appear (each with a session count), and selecting one or more scopes both browsing and search to sessions that used that model — anywhere in the session, including its subagents — with removable chips and "Clear all" just like the other filters (#278).
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- Conversation reader no longer re-processes an open conversation on every background refresh — it now recomputes only when the conversation actually grows, cutting idle CPU/battery use for anyone leaving a transcript open (#278).
|
|
19
|
+
- The dashboard now opens instantly on a heavy-history instance: it binds and paints the current-week and forecast panels almost immediately instead of waiting for the full ~2s data aggregation, and the heavier panels (sessions, projects, weekly, monthly, blocks, daily, trend, cache report) hydrate over the live stream showing a brief loading skeleton (respecting reduced-motion) rather than flashing a misleading "no data" / "restart the dashboard" empty state that then fills in (#278).
|
|
20
|
+
- A first-run or long-gap dashboard now fills in progressively as session history is ingested, instead of sitting blank and then snapping to fully-loaded in one jump; a warm returning session is unaffected, and a slow or backgrounded browser tab always converges to the latest data instead of getting stuck on a stale frame (#278).
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- The conversation filter popover now retries once if its filter options (the Project and Model lists) fail to load on a transient hiccup, instead of coming up empty and staying that way until it is reopened (#278).
|
|
24
|
+
|
|
25
|
+
## [1.64.0] - 2026-07-07
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
- cctally now sends an anonymous, opt-out install-count beat at most once a day so the project can gauge how many people actually use it. It is deliberately minimal and private: the only thing transmitted is a one-way token that rotates every month (derived from a random local id that never leaves your machine and cannot be recovered from the token), plus the cctally version and a coarse operating-system family (macos/linux/windows/other) — no IP, username, file paths, project names, or usage data — and nothing leaves your machine for at least 24 hours after first run, so you always have a window to opt out first. Run `cctally telemetry` to see the current state, why it is on or off, and exactly what would be sent; `cctally telemetry off` disables it (`cctally telemetry on` re-enables), `cctally telemetry reset` rotates your local id, and `cctally doctor` shows a read-only telemetry line. It is also disabled by `cctally config set telemetry.enabled false`, `CCTALLY_DISABLE_TELEMETRY=1`, the `DO_NOT_TRACK` convention, and automatically in development checkouts. Full transparency page — token construction, retention, and an honest threat model — is in [docs/telemetry.md](docs/telemetry.md).
|
|
29
|
+
|
|
8
30
|
## [1.63.0] - 2026-07-07
|
|
9
31
|
|
|
10
32
|
### Added
|
package/README.md
CHANGED
|
@@ -154,6 +154,12 @@ If you also use OpenAI's Codex CLI, `cctally codex daily / monthly / session` ar
|
|
|
154
154
|
|
|
155
155
|
`cctally doctor` gives you a read-only health check - install, hooks, sign-in, database, data freshness, pricing, and safety - and the same status shows up in the dashboard. `cctally pricing-check` warns you when the built-in model pricing is getting stale, `cctally db` manages the local database, and `cctally update` keeps your install current. The local cache is always safe to delete or rebuild.
|
|
156
156
|
|
|
157
|
+
## Privacy & telemetry
|
|
158
|
+
|
|
159
|
+
cctally sends an anonymous, opt-out **install-count beat** — at most once a day — so the project can gauge how many people actually use it (npm and GitHub numbers are drowned in bots and mirrors). The entire payload is a one-way token that rotates every month (derived from a random local id that never leaves your machine and can't be recovered from the token), the cctally version, and a coarse OS family (`macos`/`linux`/`windows`/`other`). No identity, file paths, prompts, usage data, or stored IP — ever. Nothing leaves your machine for at least 24 hours after first run, so you always have a window to opt out first.
|
|
160
|
+
|
|
161
|
+
Turn it off any time with `cctally telemetry off` (or `cctally config set telemetry.enabled false`, `CCTALLY_DISABLE_TELEMETRY=1`, or the community-standard `DO_NOT_TRACK=1`); it's also off automatically in dev checkouts. Run `cctally telemetry` to see the current state and exactly what would be sent. The full transparency page — token construction, retention, and an honest threat model — is [docs/telemetry.md](docs/telemetry.md).
|
|
162
|
+
|
|
157
163
|
## A faster, local-first ccusage
|
|
158
164
|
|
|
159
165
|
`cctally` started as a local replacement for [`ccusage`](https://github.com/ryoppippi/ccusage) and stays compatible at the level of common flows: `cctally claude <cmd>` is a drop-in for `ccusage claude <cmd>` (and `cctally codex <cmd>` for `ccusage codex <cmd>`), with the flat forms (`cctally daily`, `cctally codex-daily`, …) kept as aliases. Paste your ccusage commands verbatim - then reach for the dashboard, forecast, trend, conversation viewer, and alerts that ccusage doesn't have.
|
|
@@ -176,6 +182,7 @@ It's also fast. Pricing is built in and computed in-process from a local SQLite
|
|
|
176
182
|
- [Configuration](docs/configuration.md): `config.json` shape and week-start rules.
|
|
177
183
|
- [Architecture](docs/architecture.md): data flow, caches, week boundaries.
|
|
178
184
|
- [Runtime data](docs/runtime-data.md): what lives in `~/.local/share/cctally/`.
|
|
185
|
+
- [Telemetry](docs/telemetry.md): the anonymous install-count beat, in full — and how to opt out.
|
|
179
186
|
- [Command reference](docs/commands/): one page per subcommand.
|
|
180
187
|
|
|
181
188
|
## License
|
package/bin/_cctally_cache.py
CHANGED
|
@@ -176,6 +176,12 @@ _should_replace = _lib_jsonl._should_replace
|
|
|
176
176
|
_lib_conversation = _load_lib("_lib_conversation")
|
|
177
177
|
_iter_message_rows = _lib_conversation.iter_message_rows
|
|
178
178
|
|
|
179
|
+
# Opt-in backend phase-instrumentation collector (issue #276, Session A). Pure
|
|
180
|
+
# stdlib leaf; near-noop when CCTALLY_PERF_TRACE is unset (phase() returns a
|
|
181
|
+
# shared no-op singleton), so the sync_cache seam wraps below cost nothing on
|
|
182
|
+
# the default path.
|
|
183
|
+
_perf = _load_lib("_lib_perf")
|
|
184
|
+
|
|
179
185
|
# Shared by the fused per-file walk AND backfill_conversation_messages so the
|
|
180
186
|
# column list, placeholders, and tuple order live in ONE place — a column
|
|
181
187
|
# add/reorder can't silently desync the two ingest paths (which would land
|
|
@@ -857,7 +863,10 @@ def sync_cache(
|
|
|
857
863
|
|
|
858
864
|
lock_fh = open(_cctally_core.CACHE_LOCK_PATH, "w")
|
|
859
865
|
try:
|
|
860
|
-
|
|
866
|
+
with _perf.phase("flock") as _p_flock:
|
|
867
|
+
_acquired = _acquire_cache_flock(lock_fh, timeout=lock_timeout)
|
|
868
|
+
_p_flock.set_meta(contended=not _acquired)
|
|
869
|
+
if not _acquired:
|
|
861
870
|
eprint("[cache] sync already in progress; using existing cache")
|
|
862
871
|
stats.lock_contended = True
|
|
863
872
|
return stats
|
|
@@ -1044,6 +1053,12 @@ def sync_cache(
|
|
|
1044
1053
|
# path, which already cleared the flag and repopulates via the normal
|
|
1045
1054
|
# walk). A path-less/:memory: conn has no cache_meta only if the schema
|
|
1046
1055
|
# was never applied; the try/except tolerates that.
|
|
1056
|
+
# #276 perf: bracket the (rare, upgrade-only) backfill/reingest region
|
|
1057
|
+
# as one coarse "backfills" phase. Opened via the context-manager
|
|
1058
|
+
# protocol rather than a ``with`` block so the ~150-line body below is
|
|
1059
|
+
# not reindented. Near-noop when tracing is off (_NULL_PHASE).
|
|
1060
|
+
_p_backfills = _perf.phase("backfills")
|
|
1061
|
+
_p_backfills.__enter__()
|
|
1047
1062
|
if not rebuild and not targeted:
|
|
1048
1063
|
try:
|
|
1049
1064
|
_pending = conn.execute(
|
|
@@ -1191,12 +1206,15 @@ def sync_cache(
|
|
|
1191
1206
|
# IGNORE). Touches ONLY conversation_file_touches, never
|
|
1192
1207
|
# conversation_messages (P1-2).
|
|
1193
1208
|
_consume_file_touches(conn)
|
|
1209
|
+
_p_backfills.__exit__(None, None, None)
|
|
1194
1210
|
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1211
|
+
with _perf.phase("discover") as _p_disc:
|
|
1212
|
+
if targeted:
|
|
1213
|
+
paths = [pathlib.Path(p) for p in only_paths if pathlib.Path(p).is_file()]
|
|
1214
|
+
else:
|
|
1215
|
+
paths = list(_iter_claude_jsonl_files())
|
|
1216
|
+
stats.files_total = len(paths)
|
|
1217
|
+
_p_disc.set_count(len(paths))
|
|
1200
1218
|
|
|
1201
1219
|
# This SELECT does NOT open an implicit transaction (Python's
|
|
1202
1220
|
# sqlite3 module only BEGINs on DML). Do NOT add any INSERT/
|
|
@@ -1406,6 +1424,12 @@ def sync_cache(
|
|
|
1406
1424
|
# zero-write-lock read/parse region, so it adds no DML there.
|
|
1407
1425
|
touched_sessions: set = set()
|
|
1408
1426
|
|
|
1427
|
+
# #276 perf: bracket the fused per-file ingest loop as ONE coarse
|
|
1428
|
+
# "walk" phase (never per-row — Section 2 rule: volume is a count, not
|
|
1429
|
+
# N timed phases). Opened via the context-manager protocol so the hot
|
|
1430
|
+
# loop body below is not reindented; counts recorded after the loop.
|
|
1431
|
+
_p_walk = _perf.phase("walk")
|
|
1432
|
+
_p_walk.__enter__()
|
|
1409
1433
|
for jp in paths:
|
|
1410
1434
|
path_str = str(jp)
|
|
1411
1435
|
# Backfill session_id/project_path for A2 `session` subcommand.
|
|
@@ -1683,6 +1707,13 @@ def sync_cache(
|
|
|
1683
1707
|
|
|
1684
1708
|
if progress is not None:
|
|
1685
1709
|
progress(stats)
|
|
1710
|
+
_p_walk.__exit__(None, None, None)
|
|
1711
|
+
_p_walk.set_count(stats.files_processed)
|
|
1712
|
+
_p_walk.set_meta(
|
|
1713
|
+
skipped=stats.files_skipped_unchanged,
|
|
1714
|
+
failed=stats.files_failed,
|
|
1715
|
+
rows=stats.rows_changed,
|
|
1716
|
+
)
|
|
1686
1717
|
|
|
1687
1718
|
# Browse-rail rollup maintenance (single post-walk recompute, under the
|
|
1688
1719
|
# still-held flock, after every per-file commit and before the
|
|
@@ -1698,16 +1729,17 @@ def sync_cache(
|
|
|
1698
1729
|
# ~1 session/tick). Both recomputes derive COUNT/MIN/MAX from the same
|
|
1699
1730
|
# rows the rail's old live aggregate read, so the rollup stays
|
|
1700
1731
|
# byte-identical to that aggregate.
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1732
|
+
with _perf.phase("recompute.conversation_sessions"):
|
|
1733
|
+
if _conversation_sessions_backfill_pending(conn):
|
|
1734
|
+
_recompute_conversation_sessions(conn)
|
|
1735
|
+
conn.execute(
|
|
1736
|
+
"DELETE FROM cache_meta "
|
|
1737
|
+
"WHERE key='conversation_sessions_backfill_pending'"
|
|
1738
|
+
)
|
|
1739
|
+
conn.commit()
|
|
1740
|
+
elif touched_sessions:
|
|
1741
|
+
_recompute_conversation_sessions(conn, touched_sessions)
|
|
1742
|
+
conn.commit()
|
|
1711
1743
|
|
|
1712
1744
|
# Walk-complete sentinel write (cctally-dev#93, D5a). Still inside the
|
|
1713
1745
|
# held fcntl lock, before the finally-unlock. Only when the entire walk
|
|
@@ -3370,6 +3402,10 @@ def cmd_cache_sync(args: argparse.Namespace) -> int:
|
|
|
3370
3402
|
default is 'all'.
|
|
3371
3403
|
"""
|
|
3372
3404
|
source = getattr(args, "source", "all")
|
|
3405
|
+
# #276 perf: clear any prior tree on this thread so a leaked root can't be
|
|
3406
|
+
# flushed, then (below) time the Claude sync_cache call as the "sync_cache"
|
|
3407
|
+
# root phase and flush the tree to stderr when CCTALLY_PERF_TRACE is set.
|
|
3408
|
+
_perf.reset_thread()
|
|
3373
3409
|
conn = open_cache_db()
|
|
3374
3410
|
|
|
3375
3411
|
# --prune-orphans: fast, targeted cleanup of cache rows whose source
|
|
@@ -3419,9 +3455,10 @@ def cmd_cache_sync(args: argparse.Namespace) -> int:
|
|
|
3419
3455
|
contended = False
|
|
3420
3456
|
|
|
3421
3457
|
if source in ("claude", "all"):
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3458
|
+
with _perf.phase("sync_cache"):
|
|
3459
|
+
stats = sync_cache(
|
|
3460
|
+
conn, progress=_progress_stderr, rebuild=args.rebuild, lock_timeout=lt
|
|
3461
|
+
)
|
|
3425
3462
|
_progress_stderr(stats, force=True)
|
|
3426
3463
|
if stats.lock_contended and args.rebuild:
|
|
3427
3464
|
eprint(
|
|
@@ -3456,4 +3493,8 @@ def cmd_cache_sync(args: argparse.Namespace) -> int:
|
|
|
3456
3493
|
f"{stats.rows_changed} rows changed"
|
|
3457
3494
|
)
|
|
3458
3495
|
|
|
3496
|
+
# #276 perf: when tracing is enabled, flush the completed "sync_cache"
|
|
3497
|
+
# phase tree to stderr (stdout stays byte-identical). No-op when off.
|
|
3498
|
+
if _perf.enabled():
|
|
3499
|
+
_perf.flush_stderr(_perf.current_root())
|
|
3459
3500
|
return 1 if contended else 0
|
package/bin/_cctally_config.py
CHANGED
|
@@ -318,6 +318,7 @@ ALLOWED_CONFIG_KEYS = (
|
|
|
318
318
|
"budget.projects",
|
|
319
319
|
"budget.project_alerts_enabled",
|
|
320
320
|
"budget.codex",
|
|
321
|
+
"telemetry.enabled",
|
|
321
322
|
)
|
|
322
323
|
|
|
323
324
|
|
|
@@ -540,6 +541,25 @@ def _config_known_value(config: dict, key: str) -> "object":
|
|
|
540
541
|
except ValueError:
|
|
541
542
|
return True
|
|
542
543
|
return True
|
|
544
|
+
if key == "telemetry.enabled":
|
|
545
|
+
# Boolean opt-OUT (anonymous install-count telemetry, spec 2026-07-07).
|
|
546
|
+
# Default TRUE — absence is ON. A hand-edited junk value surfaces the
|
|
547
|
+
# True default. Mirrors dashboard.live_tail exactly, under a top-level
|
|
548
|
+
# `telemetry` block instead of `dashboard`.
|
|
549
|
+
block = config.get("telemetry") if isinstance(config, dict) else None
|
|
550
|
+
if not isinstance(block, dict):
|
|
551
|
+
block = {}
|
|
552
|
+
stored = block.get("enabled")
|
|
553
|
+
if stored is None:
|
|
554
|
+
return True
|
|
555
|
+
if isinstance(stored, bool):
|
|
556
|
+
return stored
|
|
557
|
+
if isinstance(stored, str):
|
|
558
|
+
try:
|
|
559
|
+
return c._normalize_alerts_enabled_value(stored)
|
|
560
|
+
except ValueError:
|
|
561
|
+
return True
|
|
562
|
+
return True
|
|
543
563
|
if key in ("update.check.enabled", "update.check.ttl_hours"):
|
|
544
564
|
# Defaults mirror `_is_update_check_due` (True / 24 hours).
|
|
545
565
|
# Hand-edited junk surfaces as the default — matches dashboard.bind.
|
|
@@ -1006,6 +1026,40 @@ def _cmd_config_set(args: argparse.Namespace) -> int:
|
|
|
1006
1026
|
else:
|
|
1007
1027
|
print(f"dashboard.live_tail={'true' if canonical else 'false'}")
|
|
1008
1028
|
return 0
|
|
1029
|
+
if key == "telemetry.enabled":
|
|
1030
|
+
# Anonymous install-count telemetry opt-out (spec 2026-07-07). Mirror
|
|
1031
|
+
# dashboard.live_tail exactly: validate the bool first, then
|
|
1032
|
+
# read-modify-write under config_writer_lock with _load_config_unlocked
|
|
1033
|
+
# (load_config under the lock self-deadlocks). Preserves any sibling
|
|
1034
|
+
# telemetry.* keys. Re-message the shared normalizer's ValueError with
|
|
1035
|
+
# the actual key name.
|
|
1036
|
+
try:
|
|
1037
|
+
canonical = c._normalize_alerts_enabled_value(raw)
|
|
1038
|
+
except ValueError:
|
|
1039
|
+
print(
|
|
1040
|
+
f"cctally: invalid boolean value for telemetry.enabled: "
|
|
1041
|
+
f"{raw!r} (expected true|false|yes|no|1|0|on|off)",
|
|
1042
|
+
file=sys.stderr,
|
|
1043
|
+
)
|
|
1044
|
+
return 2
|
|
1045
|
+
with config_writer_lock():
|
|
1046
|
+
config = _load_config_unlocked()
|
|
1047
|
+
existing = config.get("telemetry")
|
|
1048
|
+
if existing is not None and not isinstance(existing, dict):
|
|
1049
|
+
print(
|
|
1050
|
+
"cctally: telemetry config error: telemetry must be an object",
|
|
1051
|
+
file=sys.stderr,
|
|
1052
|
+
)
|
|
1053
|
+
return 2
|
|
1054
|
+
block = dict(existing or {})
|
|
1055
|
+
block["enabled"] = canonical
|
|
1056
|
+
config["telemetry"] = block
|
|
1057
|
+
save_config(config)
|
|
1058
|
+
if getattr(args, "emit_json", False):
|
|
1059
|
+
print(json.dumps({"telemetry": {"enabled": canonical}}, indent=2))
|
|
1060
|
+
else:
|
|
1061
|
+
print(f"telemetry.enabled={'true' if canonical else 'false'}")
|
|
1062
|
+
return 0
|
|
1009
1063
|
if key in (
|
|
1010
1064
|
"statusline.visual_burn_rate",
|
|
1011
1065
|
"statusline.cost_source",
|
|
@@ -1411,6 +1465,20 @@ def _cmd_config_unset(args: argparse.Namespace) -> int:
|
|
|
1411
1465
|
save_config(config)
|
|
1412
1466
|
# idempotent: silent on missing key
|
|
1413
1467
|
return 0
|
|
1468
|
+
if key == "telemetry.enabled":
|
|
1469
|
+
# Mirror the dashboard.live_tail unset branch: drop only the enabled
|
|
1470
|
+
# leaf; if the telemetry block ends up empty, drop the parent too.
|
|
1471
|
+
# Unsetting restores the True (opt-out) default at read time.
|
|
1472
|
+
with config_writer_lock():
|
|
1473
|
+
config = _load_config_unlocked()
|
|
1474
|
+
block = config.get("telemetry")
|
|
1475
|
+
if isinstance(block, dict) and "enabled" in block:
|
|
1476
|
+
del block["enabled"]
|
|
1477
|
+
if not block:
|
|
1478
|
+
config.pop("telemetry", None)
|
|
1479
|
+
save_config(config)
|
|
1480
|
+
# idempotent: silent on missing key
|
|
1481
|
+
return 0
|
|
1414
1482
|
if key in (
|
|
1415
1483
|
"statusline.visual_burn_rate",
|
|
1416
1484
|
"statusline.cost_source",
|
package/bin/_cctally_core.py
CHANGED
|
@@ -69,6 +69,8 @@ def _init_paths_from_env() -> None:
|
|
|
69
69
|
global UPDATE_LOCK_PATH, UPDATE_LOG_PATH, UPDATE_LOG_ROTATED_PATH
|
|
70
70
|
global UPDATE_CHECK_LAST_FETCH_PATH, CLAUDE_SETTINGS_PATH
|
|
71
71
|
global CLAUDE_PROJECTS_DIR
|
|
72
|
+
global TELEMETRY_INSTALL_ID_PATH, TELEMETRY_LAST_BEAT_PATH
|
|
73
|
+
global TELEMETRY_NOTICE_SHOWN_PATH, TELEMETRY_FIRST_SEEN_PATH
|
|
72
74
|
|
|
73
75
|
home = pathlib.Path.home()
|
|
74
76
|
|
|
@@ -124,6 +126,14 @@ def _init_paths_from_env() -> None:
|
|
|
124
126
|
UPDATE_LOG_ROTATED_PATH = APP_DIR / "update.log.1"
|
|
125
127
|
UPDATE_CHECK_LAST_FETCH_PATH = APP_DIR / "update-check.last-fetch"
|
|
126
128
|
|
|
129
|
+
# Anonymous install-count telemetry markers (see spec 2026-07-07).
|
|
130
|
+
# All four derive from APP_DIR and are re-bound here so a redirected
|
|
131
|
+
# APP_DIR (tests, dev-instance isolation) carries them along.
|
|
132
|
+
TELEMETRY_INSTALL_ID_PATH = APP_DIR / "install_id"
|
|
133
|
+
TELEMETRY_LAST_BEAT_PATH = APP_DIR / "telemetry.last-beat"
|
|
134
|
+
TELEMETRY_NOTICE_SHOWN_PATH = APP_DIR / "telemetry.notice-shown"
|
|
135
|
+
TELEMETRY_FIRST_SEEN_PATH = APP_DIR / "telemetry.first-seen"
|
|
136
|
+
|
|
127
137
|
CLAUDE_SETTINGS_PATH = home / ".claude" / "settings.json"
|
|
128
138
|
|
|
129
139
|
# Claude session JSONL root. Production path is `~/.claude/projects`;
|
|
@@ -189,6 +199,25 @@ def _real_prod_data_dir() -> pathlib.Path:
|
|
|
189
199
|
_init_paths_from_env()
|
|
190
200
|
|
|
191
201
|
|
|
202
|
+
# === Telemetry constants (non-path; see spec 2026-07-07) =============
|
|
203
|
+
#
|
|
204
|
+
# These are static (not APP_DIR-derived) so they live outside
|
|
205
|
+
# `_init_paths_from_env()`. The kernel `bin/_cctally_telemetry.py` reads
|
|
206
|
+
# them at call time via its `_core()` accessor.
|
|
207
|
+
#
|
|
208
|
+
# Public, non-secret domain-separation constant folded into the monthly
|
|
209
|
+
# rotating token (SHA-256, truncated to 32 hex). It only namespaces
|
|
210
|
+
# cctally's token from any other consumer of the same install_id — it is
|
|
211
|
+
# NOT a secret and leaking it discloses nothing about the install.
|
|
212
|
+
TELEMETRY_PEPPER = "cctally-install-count-v1"
|
|
213
|
+
# Default beat endpoint; overridable for tests via CCTALLY_TELEMETRY_ENDPOINT.
|
|
214
|
+
TELEMETRY_ENDPOINT_DEFAULT = "https://cctally-telemetry.cctally.workers.dev/beat"
|
|
215
|
+
# Send at most one beat per this many seconds (mtime-gated on the beat marker).
|
|
216
|
+
TELEMETRY_BEAT_THROTTLE_SECONDS = 24 * 3600
|
|
217
|
+
# Wait this long after first eligibility before sending the first beat.
|
|
218
|
+
TELEMETRY_FIRST_BEAT_GRACE_SECONDS = 24 * 3600
|
|
219
|
+
|
|
220
|
+
|
|
192
221
|
def _resolve_claude_projects_dirs() -> list[pathlib.Path]:
|
|
193
222
|
"""Return Claude Code projects dirs that exist on disk, env-aware.
|
|
194
223
|
|