cctally 1.27.0 → 1.28.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 +27 -0
- package/bin/_cctally_alerts.py +26 -1
- package/bin/_cctally_cache.py +278 -6
- package/bin/_cctally_config.py +153 -11
- package/bin/_cctally_core.py +230 -41
- package/bin/_cctally_dashboard.py +399 -37
- package/bin/_cctally_db.py +594 -163
- package/bin/_cctally_doctor.py +11 -0
- package/bin/_cctally_forecast.py +700 -57
- package/bin/_cctally_milestones.py +273 -28
- package/bin/_cctally_parser.py +44 -4
- package/bin/_cctally_record.py +328 -50
- package/bin/_cctally_setup.py +7 -3
- package/bin/_cctally_statusline.py +8 -0
- package/bin/_cctally_update.py +3 -3
- package/bin/_cctally_weekrefs.py +30 -6
- package/bin/_lib_alert_axes.py +8 -1
- package/bin/_lib_alerts_payload.py +95 -3
- package/bin/_lib_budget.py +48 -0
- package/bin/_lib_conversation.py +162 -0
- package/bin/_lib_conversation_query.py +524 -0
- package/bin/_lib_doctor.py +60 -1
- package/bin/_lib_transcript_access.py +80 -0
- package/bin/cctally +40 -1
- package/dashboard/static/assets/{index-D34qf0LE.css → index-Bj5ckRUE.css} +1 -1
- package/dashboard/static/assets/index-Dw4G5FD9.js +18 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +4 -1
- package/dashboard/static/assets/index-C2F1_Mxt.js +0 -18
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Pure loopback / Host gate predicates for the conversation-viewer transcript
|
|
2
|
+
endpoints (Plan 2, spec §5).
|
|
3
|
+
|
|
4
|
+
No I/O, no globals — directly unit-testable. The dashboard's
|
|
5
|
+
``_require_transcripts_allowed`` composes ``transcripts_allowed`` (is this bind
|
|
6
|
+
served at all?) with ``host_allowed_for_transcripts`` (anti-DNS-rebinding on the
|
|
7
|
+
request's Host header). Loopback-by-default kills rebinding (it needs a
|
|
8
|
+
hostname that resolves to 127.0.0.1); under the explicit ``expose`` opt-in the
|
|
9
|
+
LAN device reaches the dashboard at its IP literal (allowed), while an
|
|
10
|
+
attacker's rebinding *domain* (a hostname) is rejected.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
import ipaddress
|
|
14
|
+
|
|
15
|
+
# Public surface (Plan 2): shipped in the npm tarball + brew formula + public
|
|
16
|
+
# mirror — imported by the dashboard's transcript gate at runtime.
|
|
17
|
+
|
|
18
|
+
_LOOPBACK_NAMES = {"localhost", "::1"}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def authority_host(host_header) -> str:
|
|
22
|
+
"""Extract the lowercased host from an HTTP authority (``Host`` header or a
|
|
23
|
+
bind string). Strips the port, the ``[...]`` IPv6 brackets, and any
|
|
24
|
+
``%zone`` id. IPv6-safe (does NOT ``split(':')``). Empty string on missing
|
|
25
|
+
input."""
|
|
26
|
+
if not host_header:
|
|
27
|
+
return ""
|
|
28
|
+
h = str(host_header).strip()
|
|
29
|
+
if h.startswith("["):
|
|
30
|
+
# [IPv6]:port -> IPv6
|
|
31
|
+
end = h.find("]")
|
|
32
|
+
if end != -1:
|
|
33
|
+
h = h[1:end]
|
|
34
|
+
else:
|
|
35
|
+
h = h[1:]
|
|
36
|
+
elif h.count(":") == 1:
|
|
37
|
+
# host:port (a single colon can't be a bare IPv6 literal)
|
|
38
|
+
h = h.split(":", 1)[0]
|
|
39
|
+
# else: bare IPv6 literal (multiple colons) or bare host — leave as-is
|
|
40
|
+
h = h.split("%", 1)[0] # drop IPv6 zone id
|
|
41
|
+
return h.lower()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def is_loopback(host: str) -> bool:
|
|
45
|
+
"""True for the loopback names plus any IPv4/IPv6 loopback literal
|
|
46
|
+
(127.0.0.0/8, ::1)."""
|
|
47
|
+
if not host:
|
|
48
|
+
return False
|
|
49
|
+
h = host.lower()
|
|
50
|
+
if h in _LOOPBACK_NAMES:
|
|
51
|
+
return True
|
|
52
|
+
try:
|
|
53
|
+
return ipaddress.ip_address(h).is_loopback
|
|
54
|
+
except ValueError:
|
|
55
|
+
return False
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def transcripts_allowed(bind_host, expose: bool) -> bool:
|
|
59
|
+
"""Are transcripts served AT ALL on this bind? Loopback bind always; a
|
|
60
|
+
non-loopback (LAN) bind only under the explicit ``expose`` opt-in."""
|
|
61
|
+
return is_loopback(authority_host(bind_host)) or bool(expose)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def host_allowed_for_transcripts(host_header, expose: bool) -> bool:
|
|
65
|
+
"""Anti-DNS-rebinding Host allowlist. Loopback Host always OK. Otherwise
|
|
66
|
+
only an IP-literal Host under ``expose`` (the LAN IP the dashboard is
|
|
67
|
+
reached at) — a rebinding *domain* (any hostname) is rejected. Fail closed
|
|
68
|
+
on missing/empty Host."""
|
|
69
|
+
h = authority_host(host_header)
|
|
70
|
+
if not h:
|
|
71
|
+
return False
|
|
72
|
+
if is_loopback(h):
|
|
73
|
+
return True
|
|
74
|
+
if not expose:
|
|
75
|
+
return False
|
|
76
|
+
try:
|
|
77
|
+
ipaddress.ip_address(h) # IP literal can't be DNS-rebound
|
|
78
|
+
return True
|
|
79
|
+
except ValueError:
|
|
80
|
+
return False # hostname (rebinding vector) — reject
|
package/bin/cctally
CHANGED
|
@@ -306,6 +306,8 @@ BudgetInputs = _lib_budget.BudgetInputs
|
|
|
306
306
|
BudgetStatus = _lib_budget.BudgetStatus
|
|
307
307
|
compute_budget_status = _lib_budget.compute_budget_status
|
|
308
308
|
project_linear = _lib_budget.project_linear
|
|
309
|
+
calendar_month_window = _lib_budget.calendar_month_window
|
|
310
|
+
calendar_week_window = _lib_budget.calendar_week_window
|
|
309
311
|
|
|
310
312
|
# CLAUDE_MODEL_CONTEXT_WINDOWS / …_DEFAULT_FAMILY moved to _cctally_statusline.py (re-exported below).
|
|
311
313
|
|
|
@@ -377,12 +379,14 @@ _alert_text_weekly = _lib_alerts_payload._alert_text_weekly
|
|
|
377
379
|
_alert_text_five_hour = _lib_alerts_payload._alert_text_five_hour
|
|
378
380
|
_alert_text_budget = _lib_alerts_payload._alert_text_budget
|
|
379
381
|
_alert_text_project_budget = _lib_alerts_payload._alert_text_project_budget
|
|
382
|
+
_alert_text_codex_budget = _lib_alerts_payload._alert_text_codex_budget
|
|
380
383
|
_alert_text_projected = _lib_alerts_payload._alert_text_projected
|
|
381
384
|
_escape_applescript_string = _lib_alerts_payload._escape_applescript_string
|
|
382
385
|
_build_alert_payload_weekly = _lib_alerts_payload._build_alert_payload_weekly
|
|
383
386
|
_build_alert_payload_five_hour = _lib_alerts_payload._build_alert_payload_five_hour
|
|
384
387
|
_build_alert_payload_budget = _lib_alerts_payload._build_alert_payload_budget
|
|
385
388
|
_build_alert_payload_project_budget = _lib_alerts_payload._build_alert_payload_project_budget
|
|
389
|
+
_build_alert_payload_codex_budget = _lib_alerts_payload._build_alert_payload_codex_budget
|
|
386
390
|
_build_alert_payload_projected = _lib_alerts_payload._build_alert_payload_projected
|
|
387
391
|
|
|
388
392
|
_lib_alert_dispatch = _load_sibling("_lib_alert_dispatch")
|
|
@@ -736,6 +740,7 @@ add_column_if_missing = _cctally_db.add_column_if_missing
|
|
|
736
740
|
_MIGRATION_NAME_RE = _cctally_db._MIGRATION_NAME_RE
|
|
737
741
|
Migration = _cctally_db.Migration
|
|
738
742
|
DowngradeDetected = _cctally_db.DowngradeDetected
|
|
743
|
+
ProdMigrationRefused = _cctally_db.ProdMigrationRefused
|
|
739
744
|
_STATS_MIGRATIONS = _cctally_db._STATS_MIGRATIONS
|
|
740
745
|
_CACHE_MIGRATIONS = _cctally_db._CACHE_MIGRATIONS
|
|
741
746
|
_make_migration_decorator = _cctally_db._make_migration_decorator
|
|
@@ -743,7 +748,9 @@ stats_migration = _cctally_db.stats_migration
|
|
|
743
748
|
cache_migration = _cctally_db.cache_migration
|
|
744
749
|
_LEGACY_MARKER_ALIASES_BY_DB = _cctally_db._LEGACY_MARKER_ALIASES_BY_DB
|
|
745
750
|
_bootstrap_rename_legacy_markers = _cctally_db._bootstrap_rename_legacy_markers
|
|
751
|
+
_stamp_applied = _cctally_db._stamp_applied
|
|
746
752
|
_run_pending_migrations = _cctally_db._run_pending_migrations
|
|
753
|
+
_recover_version_ahead = _cctally_db._recover_version_ahead
|
|
747
754
|
_log_migration_error = _cctally_db._log_migration_error
|
|
748
755
|
_clear_migration_error_log_entries = _cctally_db._clear_migration_error_log_entries
|
|
749
756
|
_render_migration_error_banner = _cctally_db._render_migration_error_banner
|
|
@@ -757,6 +764,7 @@ _db_resolve_migration_name = _cctally_db._db_resolve_migration_name
|
|
|
757
764
|
_db_path_for_label = _cctally_db._db_path_for_label
|
|
758
765
|
cmd_db_skip = _cctally_db.cmd_db_skip
|
|
759
766
|
cmd_db_unskip = _cctally_db.cmd_db_unskip
|
|
767
|
+
cmd_db_recover = _cctally_db.cmd_db_recover
|
|
760
768
|
|
|
761
769
|
|
|
762
770
|
# Session-entry cache subsystem (Claude + Codex) — read-through delta
|
|
@@ -782,6 +790,7 @@ IngestStats = _cctally_cache.IngestStats
|
|
|
782
790
|
_progress_stderr = _cctally_cache._progress_stderr
|
|
783
791
|
_ensure_session_files_row = _cctally_cache._ensure_session_files_row
|
|
784
792
|
sync_cache = _cctally_cache.sync_cache
|
|
793
|
+
backfill_conversation_messages = _cctally_cache.backfill_conversation_messages
|
|
785
794
|
iter_entries = _cctally_cache.iter_entries
|
|
786
795
|
_collect_entries_direct = _cctally_cache._collect_entries_direct
|
|
787
796
|
_JoinedClaudeEntry = _cctally_cache._JoinedClaudeEntry
|
|
@@ -793,6 +802,7 @@ sync_codex_cache = _cctally_cache.sync_codex_cache
|
|
|
793
802
|
iter_codex_entries = _cctally_cache.iter_codex_entries
|
|
794
803
|
_collect_codex_entries_direct = _cctally_cache._collect_codex_entries_direct
|
|
795
804
|
get_codex_entries = _cctally_cache.get_codex_entries
|
|
805
|
+
_sum_codex_cost_for_range = _cctally_cache._sum_codex_cost_for_range
|
|
796
806
|
get_entries = _cctally_cache.get_entries
|
|
797
807
|
open_cache_db = _cctally_cache.open_cache_db
|
|
798
808
|
cmd_cache_sync = _cctally_cache.cmd_cache_sync
|
|
@@ -822,6 +832,7 @@ _normalize_percent = _cctally_record._normalize_percent
|
|
|
822
832
|
maybe_record_milestone = _cctally_record.maybe_record_milestone
|
|
823
833
|
maybe_record_budget_milestone = _cctally_record.maybe_record_budget_milestone
|
|
824
834
|
maybe_record_project_budget_milestone = _cctally_record.maybe_record_project_budget_milestone
|
|
835
|
+
maybe_record_codex_budget_milestone = _cctally_record.maybe_record_codex_budget_milestone
|
|
825
836
|
maybe_record_projected_alert = _cctally_record.maybe_record_projected_alert
|
|
826
837
|
_weekly_pct_week_avg_projection = _cctally_record._weekly_pct_week_avg_projection
|
|
827
838
|
_compute_block_totals = _cctally_record._compute_block_totals
|
|
@@ -1101,6 +1112,7 @@ _fetch_current_week_snapshots = _cctally_forecast._fetch_current_week_snapshots
|
|
|
1101
1112
|
_apply_midweek_reset_override = _cctally_forecast._apply_midweek_reset_override
|
|
1102
1113
|
_resolve_current_budget_window = _cctally_forecast._resolve_current_budget_window
|
|
1103
1114
|
_build_budget_status_inputs = _cctally_forecast._build_budget_status_inputs
|
|
1115
|
+
_build_vendor_budget_inputs = _cctally_forecast._build_vendor_budget_inputs
|
|
1104
1116
|
_select_dollars_per_percent = _cctally_forecast._select_dollars_per_percent
|
|
1105
1117
|
_assess_forecast_confidence = _cctally_forecast._assess_forecast_confidence
|
|
1106
1118
|
_pick_p_24h_ago = _cctally_forecast._pick_p_24h_ago
|
|
@@ -1115,6 +1127,12 @@ _forecast_color_enabled = _cctally_forecast._forecast_color_enabled
|
|
|
1115
1127
|
_render_forecast_progress_bar = _cctally_forecast._render_forecast_progress_bar
|
|
1116
1128
|
_render_forecast_terminal = _cctally_forecast._render_forecast_terminal
|
|
1117
1129
|
_BUDGET_JSON_SCHEMA_VERSION = _cctally_forecast._BUDGET_JSON_SCHEMA_VERSION
|
|
1130
|
+
# Single-sourced `--period` spellings (code-review #5): the parser's `choices=`
|
|
1131
|
+
# derives from the same map the handler normalizer uses, so they can't drift.
|
|
1132
|
+
_BUDGET_PERIOD_ALIASES = _cctally_forecast._BUDGET_PERIOD_ALIASES
|
|
1133
|
+
_BUDGET_PERIOD_CHOICES = _cctally_forecast._BUDGET_PERIOD_CHOICES
|
|
1134
|
+
_normalize_budget_period = _cctally_forecast._normalize_budget_period
|
|
1135
|
+
_resolve_calendar_window = _cctally_forecast._resolve_calendar_window
|
|
1118
1136
|
_cmd_budget_set = _cctally_forecast._cmd_budget_set
|
|
1119
1137
|
_cmd_budget_unset = _cctally_forecast._cmd_budget_unset
|
|
1120
1138
|
# Per-project budget set/unset (#19/#121, spec §4.3 / §7).
|
|
@@ -2084,6 +2102,12 @@ get_milestones_for_week = _cctally_milestones.get_milestones_for_wee
|
|
|
2084
2102
|
insert_percent_milestone = _cctally_milestones.insert_percent_milestone # record shim; idempotency-test mod.
|
|
2085
2103
|
insert_budget_milestone = _cctally_milestones.insert_budget_milestone # record shim
|
|
2086
2104
|
insert_project_budget_milestone = _cctally_milestones.insert_project_budget_milestone # record shim; project-budget-config-test ns[]
|
|
2105
|
+
insert_codex_budget_milestone = _cctally_milestones.insert_codex_budget_milestone # record shim; test_codex_budget_alerts ns[]
|
|
2106
|
+
_codex_budget_crossings = _cctally_milestones._codex_budget_crossings # record shim (shared INSERT-and-arm core for the codex_budget axis)
|
|
2107
|
+
_resolve_codex_budget_period_window = _cctally_milestones._resolve_codex_budget_period_window # record shim; milestones c. (codex period window)
|
|
2108
|
+
_reconcile_codex_budget_milestones_on_set = _cctally_milestones._reconcile_codex_budget_milestones_on_set # test_codex_budget_alerts ns[]; forecast set/reconcile
|
|
2109
|
+
_reconcile_codex_budget_on_config_write = _cctally_milestones._reconcile_codex_budget_on_config_write # forecast/config c. (forward-only codex-budget reconcile)
|
|
2110
|
+
_resolve_claude_budget_window = _cctally_milestones._resolve_claude_budget_window # record shim; milestones c. (period-aware Claude budget window)
|
|
2087
2111
|
_project_crossings = _cctally_milestones._project_crossings # record shim; milestones c. (#130 firing/reconcile shared crossing arithmetic)
|
|
2088
2112
|
insert_projected_milestone = _cctally_milestones.insert_projected_milestone # record shim
|
|
2089
2113
|
_projected_levels_already_latched = _cctally_milestones._projected_levels_already_latched # record shim
|
|
@@ -2672,6 +2696,9 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
2672
2696
|
except DowngradeDetected as exc:
|
|
2673
2697
|
eprint(f"cctally: {exc}")
|
|
2674
2698
|
return 2
|
|
2699
|
+
except ProdMigrationRefused as exc:
|
|
2700
|
+
eprint(f"{exc}") # message already carries the 'cctally:' prefix
|
|
2701
|
+
return 2
|
|
2675
2702
|
except Exception as exc: # pragma: no cover
|
|
2676
2703
|
eprint(f"Error: {exc}")
|
|
2677
2704
|
rc = 1
|
|
@@ -2720,7 +2747,19 @@ def _post_command_update_hooks(command: str | None, args) -> None:
|
|
|
2720
2747
|
``_spawn_background_update_check`` here would create ``config.json``,
|
|
2721
2748
|
``update-state.json``, and ``update.log`` on a fresh install where
|
|
2722
2749
|
the existing-install gate already makes the symlink work a no-op.
|
|
2723
|
-
Same rationale as doctor.
|
|
2750
|
+
Same rationale as doctor.
|
|
2751
|
+
|
|
2752
|
+
Test/CI seam: ``CCTALLY_DISABLE_UPDATE_CHECK`` short-circuits the whole
|
|
2753
|
+
hook (mirrors ``CCTALLY_DISABLE_DEV_AUTODETECT``). The background
|
|
2754
|
+
``_spawn_background_update_check`` is a DETACHED process that
|
|
2755
|
+
``mkdir``s APP_DIR to write ``update-state.json`` / ``update.log``; a
|
|
2756
|
+
fixture harness that runs a command and then asserts on APP_DIR (e.g.
|
|
2757
|
+
``cctally-setup-test``'s uninstall-purge "data dir is gone" check)
|
|
2758
|
+
otherwise races that detached writer re-creating the dir after the
|
|
2759
|
+
command returns. Setting this env var makes such harnesses
|
|
2760
|
+
deterministic without disabling the feature for real users."""
|
|
2761
|
+
if os.environ.get("CCTALLY_DISABLE_UPDATE_CHECK"):
|
|
2762
|
+
return
|
|
2724
2763
|
if command == "setup" and getattr(args, "uninstall", False):
|
|
2725
2764
|
return
|
|
2726
2765
|
if command == "doctor":
|