cctally 1.68.0 → 1.69.1
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 +61 -0
- package/bin/_cctally_alerts.py +54 -2
- package/bin/_cctally_cache.py +644 -107
- package/bin/_cctally_cache_report.py +41 -17
- package/bin/_cctally_codex.py +109 -4
- package/bin/_cctally_config.py +368 -2
- package/bin/_cctally_core.py +168 -5
- package/bin/_cctally_dashboard.py +679 -5
- package/bin/_cctally_dashboard_conversation.py +9 -4
- package/bin/_cctally_dashboard_envelope.py +110 -1
- package/bin/_cctally_dashboard_share.py +557 -79
- package/bin/_cctally_db.py +303 -17
- package/bin/_cctally_diff.py +49 -16
- package/bin/_cctally_doctor.py +118 -0
- package/bin/_cctally_forecast.py +12 -4
- package/bin/_cctally_parser.py +298 -92
- package/bin/_cctally_project.py +24 -8
- package/bin/_cctally_quota.py +1381 -0
- package/bin/_cctally_record.py +319 -14
- package/bin/_cctally_refresh.py +105 -3
- package/bin/_cctally_reporting.py +7 -1
- package/bin/_cctally_setup.py +343 -113
- package/bin/_cctally_statusline.py +228 -0
- package/bin/_cctally_tui.py +787 -7
- package/bin/_lib_alert_axes.py +11 -0
- package/bin/_lib_codex_hooks.py +367 -0
- package/bin/_lib_conversation_query.py +156 -67
- package/bin/_lib_doctor.py +202 -0
- package/bin/_lib_jsonl.py +529 -162
- package/bin/_lib_quota.py +566 -0
- package/bin/_lib_share.py +152 -34
- package/bin/_lib_snapshot_cache.py +26 -0
- package/bin/_lib_source_identity.py +74 -0
- package/bin/cctally +324 -10
- package/dashboard/static/assets/index-CBbErI-P.js +80 -0
- package/dashboard/static/assets/index-kDDVOLa_.css +1 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +5 -1
- package/dashboard/static/assets/index-BybNp_Di.js +0 -80
- package/dashboard/static/assets/index-DgAmLK65.css +0 -1
package/bin/cctally
CHANGED
|
@@ -86,6 +86,16 @@ def _load_sibling(name: str):
|
|
|
86
86
|
Registers in sys.modules BEFORE exec_module so dataclass(frozen=True)
|
|
87
87
|
paths that inspect cls.__module__'s sys.modules entry work correctly.
|
|
88
88
|
|
|
89
|
+
Thread-safe (#306). That pre-registration opens a window — the module is in
|
|
90
|
+
sys.modules but its top-level names are not yet bound — which, under the
|
|
91
|
+
dashboard's ThreadingHTTPServer, let a second request thread observe the
|
|
92
|
+
half-initialized module and AttributeError on a not-yet-bound name (e.g.
|
|
93
|
+
`_lib_transcript_access.transcripts_allowed`), then self-heal once the body
|
|
94
|
+
finished. `_SIBLING_LOCK` serializes the load so a concurrent caller blocks
|
|
95
|
+
until the body completes; `_LOADED_SIBLINGS` gates a lock-free fast path for
|
|
96
|
+
already-loaded siblings. Fail-closed by construction — a race waits, it never
|
|
97
|
+
returns a partial module early.
|
|
98
|
+
|
|
89
99
|
For `_cctally_*.py` siblings that back-reference `cctally`, the sibling
|
|
90
100
|
module exposes a `_cctally()` accessor that reads `sys.modules['cctally']`
|
|
91
101
|
at call-time (spec §5.5). We re-pin `sys.modules['cctally']` on every
|
|
@@ -104,16 +114,53 @@ def _load_sibling(name: str):
|
|
|
104
114
|
# reference to whichever bin/cctally module instance owns this
|
|
105
115
|
# _load_sibling closure — regardless of pytest re-imports.
|
|
106
116
|
sys.modules["cctally"] = _THIS_MODULE
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
+
# Fast path: a sibling whose body has FULLY executed is returned without
|
|
118
|
+
# contending on the load lock. `_LOADED_SIBLINGS` is only ever added to,
|
|
119
|
+
# after a successful exec_module (below), so its membership is a stable
|
|
120
|
+
# "complete" signal — keeping the hot path lock-free for the ~50 sequential
|
|
121
|
+
# sibling loads at CLI startup and every warm request-thread lazy-load.
|
|
122
|
+
if name in _LOADED_SIBLINGS:
|
|
123
|
+
return sys.modules[name]
|
|
124
|
+
# Slow path — serialize the actual load (#306). Any concurrent caller blocks
|
|
125
|
+
# here until the owning thread finishes exec_module and records the name
|
|
126
|
+
# complete, so it can never observe the half-initialized module.
|
|
127
|
+
with _SIBLING_LOCK:
|
|
128
|
+
# Completed while we waited for the lock.
|
|
129
|
+
if name in _LOADED_SIBLINGS:
|
|
130
|
+
return sys.modules[name]
|
|
131
|
+
cached = sys.modules.get(name)
|
|
132
|
+
if cached is not None:
|
|
133
|
+
# Registered but not yet recorded complete. Under the lock this is
|
|
134
|
+
# NOT another thread mid-load (that thread would hold the lock) — it
|
|
135
|
+
# is either OUR OWN thread re-entering mid-exec_module (a circular
|
|
136
|
+
# import: return the partial module, matching CPython's semantics and
|
|
137
|
+
# the prior behavior) or a module some normal `import` already fully
|
|
138
|
+
# loaded. Either way the existing sys.modules entry is the object to
|
|
139
|
+
# return; do NOT record it complete here — a circular partial must
|
|
140
|
+
# never be published to the lock-free fast path (only the owning
|
|
141
|
+
# exec_module below does that, once the body has finished).
|
|
142
|
+
return cached
|
|
143
|
+
import importlib.util as _ilu
|
|
144
|
+
p = pathlib.Path(__file__).resolve().parent / f"{name}.py"
|
|
145
|
+
# The target itself is located by path, but leaf siblings may use a
|
|
146
|
+
# normal ``from _lib_x import ...`` import. SourceFileLoader / exec
|
|
147
|
+
# callers do not necessarily put bin/ on sys.path, so make the sibling
|
|
148
|
+
# directory available before executing that module.
|
|
149
|
+
bin_dir = str(p.parent)
|
|
150
|
+
if bin_dir not in sys.path:
|
|
151
|
+
sys.path.insert(0, bin_dir)
|
|
152
|
+
spec = _ilu.spec_from_file_location(name, p)
|
|
153
|
+
mod = _ilu.module_from_spec(spec)
|
|
154
|
+
sys.modules[name] = mod
|
|
155
|
+
try:
|
|
156
|
+
spec.loader.exec_module(mod)
|
|
157
|
+
except BaseException:
|
|
158
|
+
# Failed mid-body — unregister so a later call retries cleanly and a
|
|
159
|
+
# broken module is never recorded complete (mirrors importlib).
|
|
160
|
+
sys.modules.pop(name, None)
|
|
161
|
+
raise
|
|
162
|
+
_LOADED_SIBLINGS.add(name)
|
|
163
|
+
return mod
|
|
117
164
|
|
|
118
165
|
|
|
119
166
|
# Stable reference to THIS bin/cctally module instance, used by
|
|
@@ -133,6 +180,19 @@ def _load_sibling(name: str):
|
|
|
133
180
|
_THIS_MODULE = sys.modules.get(__name__) or sys.modules.get("cctally")
|
|
134
181
|
|
|
135
182
|
|
|
183
|
+
# Thread-safety for _load_sibling (#306). The dashboard's ThreadingHTTPServer
|
|
184
|
+
# serves each request on its own thread, and the transcript- / perf- /
|
|
185
|
+
# conversation-path siblings are lazy-loaded on those threads. `_SIBLING_LOCK`
|
|
186
|
+
# serializes the actual load so a concurrent caller can never observe a module
|
|
187
|
+
# that is registered in sys.modules but whose exec_module is still mid-body (the
|
|
188
|
+
# pre-registration window below), and `_LOADED_SIBLINGS` records the names whose
|
|
189
|
+
# bodies have fully executed, gating a lock-free fast path. RLock (not Lock) so a
|
|
190
|
+
# circular import that re-enters _load_sibling on the SAME thread during
|
|
191
|
+
# exec_module cannot self-deadlock.
|
|
192
|
+
_SIBLING_LOCK = threading.RLock()
|
|
193
|
+
_LOADED_SIBLINGS: set[str] = set()
|
|
194
|
+
|
|
195
|
+
|
|
136
196
|
# Hook-tick non-path constants (Section 1 of onboarding spec).
|
|
137
197
|
HOOK_TICK_LOG_ROTATE_BYTES = 1024 * 1024 # 1 MB
|
|
138
198
|
HOOK_TICK_DEFAULT_THROTTLE_SECONDS = 30.0
|
|
@@ -252,6 +312,15 @@ HOOK_TICK_LOG_ROTATED_PATH = _cctally_core.HOOK_TICK_LOG_ROTATED_PATH
|
|
|
252
312
|
HOOK_TICK_THROTTLE_PATH = _cctally_core.HOOK_TICK_THROTTLE_PATH
|
|
253
313
|
HOOK_TICK_THROTTLE_LOCK_PATH = _cctally_core.HOOK_TICK_THROTTLE_LOCK_PATH
|
|
254
314
|
|
|
315
|
+
# Statusline usage-persistence markers + tunables (spec 2026-07-17).
|
|
316
|
+
STATUSLINE_OBSERVE_MARKER_PATH = _cctally_core.STATUSLINE_OBSERVE_MARKER_PATH
|
|
317
|
+
STATUSLINE_PERSIST_LOCK_PATH = _cctally_core.STATUSLINE_PERSIST_LOCK_PATH
|
|
318
|
+
OAUTH_BACKOFF_MARKER_PATH = _cctally_core.OAUTH_BACKOFF_MARKER_PATH
|
|
319
|
+
STATUSLINE_PERSIST_THROTTLE_SECONDS = _cctally_core.STATUSLINE_PERSIST_THROTTLE_SECONDS
|
|
320
|
+
OAUTH_BACKFILL_STALE_SECONDS = _cctally_core.OAUTH_BACKFILL_STALE_SECONDS
|
|
321
|
+
OAUTH_BACKOFF_BASE_SECONDS = _cctally_core.OAUTH_BACKOFF_BASE_SECONDS
|
|
322
|
+
OAUTH_BACKOFF_CAP_SECONDS = _cctally_core.OAUTH_BACKOFF_CAP_SECONDS
|
|
323
|
+
|
|
255
324
|
UPDATE_STATE_PATH = _cctally_core.UPDATE_STATE_PATH
|
|
256
325
|
UPDATE_SUPPRESS_PATH = _cctally_core.UPDATE_SUPPRESS_PATH
|
|
257
326
|
UPDATE_LOCK_PATH = _cctally_core.UPDATE_LOCK_PATH
|
|
@@ -375,6 +444,7 @@ _build_codex_daily_parser = _cctally_parser._build_codex_daily_parser
|
|
|
375
444
|
_build_codex_monthly_parser = _cctally_parser._build_codex_monthly_parser
|
|
376
445
|
_build_codex_weekly_parser = _cctally_parser._build_codex_weekly_parser
|
|
377
446
|
_build_codex_session_parser = _cctally_parser._build_codex_session_parser
|
|
447
|
+
_build_codex_quota_parser = _cctally_parser._build_codex_quota_parser
|
|
378
448
|
|
|
379
449
|
|
|
380
450
|
_lib_alert_axes = _load_sibling("_lib_alert_axes")
|
|
@@ -426,6 +496,37 @@ _CODEX_FILENAME_UUID_RE = _lib_jsonl._CODEX_FILENAME_UUID_RE
|
|
|
426
496
|
_CodexIterState = _lib_jsonl._CodexIterState
|
|
427
497
|
_iter_codex_jsonl_entries_with_offsets = _lib_jsonl._iter_codex_jsonl_entries_with_offsets
|
|
428
498
|
|
|
499
|
+
# Provider-neutral quota interpretation kernel (#294 S2). The module is pure
|
|
500
|
+
# and contains no database, config, filesystem, notifier, parser, or command
|
|
501
|
+
# dependency; adapters added in later stages own those concerns.
|
|
502
|
+
_lib_quota = _load_sibling("_lib_quota")
|
|
503
|
+
QuotaWindowIdentity = _lib_quota.QuotaWindowIdentity
|
|
504
|
+
QuotaObservation = _lib_quota.QuotaObservation
|
|
505
|
+
QuotaHistory = _lib_quota.QuotaHistory
|
|
506
|
+
QuotaBlock = _lib_quota.QuotaBlock
|
|
507
|
+
QuotaPercentMilestone = _lib_quota.QuotaPercentMilestone
|
|
508
|
+
QuotaFreshness = _lib_quota.QuotaFreshness
|
|
509
|
+
QuotaForecast = _lib_quota.QuotaForecast
|
|
510
|
+
QuotaRule = _lib_quota.QuotaRule
|
|
511
|
+
ResolvedQuotaRule = _lib_quota.ResolvedQuotaRule
|
|
512
|
+
QuotaThresholdDecision = _lib_quota.QuotaThresholdDecision
|
|
513
|
+
identity_sort_key = _lib_quota.identity_sort_key
|
|
514
|
+
physical_order_key = _lib_quota.physical_order_key
|
|
515
|
+
logical_value_tuple = _lib_quota.logical_value_tuple
|
|
516
|
+
source_path_key = _lib_quota.source_path_key
|
|
517
|
+
build_history = _lib_quota.build_history
|
|
518
|
+
latest_physical_observation = _lib_quota.latest_physical_observation
|
|
519
|
+
select_baseline = _lib_quota.select_baseline
|
|
520
|
+
build_blocks = _lib_quota.build_blocks
|
|
521
|
+
percent_milestones = _lib_quota.percent_milestones
|
|
522
|
+
stale_after_seconds = _lib_quota.stale_after_seconds
|
|
523
|
+
quota_freshness = _lib_quota.quota_freshness
|
|
524
|
+
forecast_quota = _lib_quota.forecast_quota
|
|
525
|
+
validate_thresholds = _lib_quota.validate_thresholds
|
|
526
|
+
resolve_quota_rule = _lib_quota.resolve_quota_rule
|
|
527
|
+
quota_rule_fingerprint = _lib_quota.quota_rule_fingerprint
|
|
528
|
+
quota_threshold_decisions = _lib_quota.quota_threshold_decisions
|
|
529
|
+
|
|
429
530
|
_lib_blocks = _load_sibling("_lib_blocks")
|
|
430
531
|
BLOCK_DURATION = _lib_blocks.BLOCK_DURATION
|
|
431
532
|
Block = _lib_blocks.Block
|
|
@@ -628,6 +729,12 @@ _setup_data_dir_size_bytes = _cctally_setup._setup_data_dir_size_bytes
|
|
|
628
729
|
_setup_format_bytes = _cctally_setup._setup_format_bytes
|
|
629
730
|
_setup_recent_log_stats = _cctally_setup._setup_recent_log_stats
|
|
630
731
|
_setup_compute_symlink_state = _cctally_setup._setup_compute_symlink_state
|
|
732
|
+
CodexHooksError = _cctally_setup.CodexHooksError
|
|
733
|
+
_codex_hooks_plan_install = _cctally_setup._codex_hooks_plan_install
|
|
734
|
+
_codex_hooks_plan_uninstall = _cctally_setup._codex_hooks_plan_uninstall
|
|
735
|
+
_write_codex_hooks_atomic = _cctally_setup._write_codex_hooks_atomic
|
|
736
|
+
_setup_codex_hook_roots = _cctally_setup._setup_codex_hook_roots
|
|
737
|
+
_setup_manage_codex_hooks = _cctally_setup._setup_manage_codex_hooks
|
|
631
738
|
_setup_status = _cctally_setup._setup_status
|
|
632
739
|
_setup_uninstall = _cctally_setup._setup_uninstall
|
|
633
740
|
_setup_dry_run = _cctally_setup._setup_dry_run
|
|
@@ -660,6 +767,8 @@ _cctally_alerts = _load_sibling("_cctally_alerts")
|
|
|
660
767
|
_alerts_log_path = _cctally_alerts._alerts_log_path
|
|
661
768
|
_dispatch_alert_notification = _cctally_alerts._dispatch_alert_notification
|
|
662
769
|
cmd_alerts_test = _cctally_alerts.cmd_alerts_test
|
|
770
|
+
_build_alert_payload_quota = _cctally_alerts._build_alert_payload_quota
|
|
771
|
+
_alert_text_quota = _cctally_alerts._alert_text_quota
|
|
663
772
|
|
|
664
773
|
# Eager re-export of bin/_cctally_sync_week.py — `cmd_sync_week` is
|
|
665
774
|
# called by bare name from non-extracted `cmd_record_usage` (the
|
|
@@ -746,6 +855,7 @@ _config_known_value = _cctally_config._config_known_value
|
|
|
746
855
|
_cmd_config_get = _cctally_config._cmd_config_get
|
|
747
856
|
_cmd_config_set = _cctally_config._cmd_config_set
|
|
748
857
|
_cmd_config_unset = _cctally_config._cmd_config_unset
|
|
858
|
+
_get_quota_alerts_config = _cctally_config._get_quota_alerts_config
|
|
749
859
|
|
|
750
860
|
|
|
751
861
|
# `cctally refresh-usage` + the OAuth-usage fetch/render/config surface,
|
|
@@ -768,6 +878,7 @@ CLAUDE_CODE_UA_FALLBACK_VERSION = _cctally_refresh.CLAUDE_CODE_UA_FALLBACK_VERSI
|
|
|
768
878
|
_discover_cc_version = _cctally_refresh._discover_cc_version
|
|
769
879
|
_resolve_oauth_usage_user_agent = _cctally_refresh._resolve_oauth_usage_user_agent
|
|
770
880
|
_fetch_oauth_usage = _cctally_refresh._fetch_oauth_usage
|
|
881
|
+
_parse_retry_after = _cctally_refresh._parse_retry_after
|
|
771
882
|
_REFRESH_USAGE_ANSI = _cctally_refresh._REFRESH_USAGE_ANSI
|
|
772
883
|
_render_refresh_usage_text = _cctally_refresh._render_refresh_usage_text
|
|
773
884
|
_serialize_refresh_usage_json = _cctally_refresh._serialize_refresh_usage_json
|
|
@@ -930,9 +1041,20 @@ _hook_tick_log_line = _cctally_record._hook_tick_log_line
|
|
|
930
1041
|
_hook_tick_log_rotate_if_needed = _cctally_record._hook_tick_log_rotate_if_needed
|
|
931
1042
|
_hook_tick_throttle_age_seconds = _cctally_record._hook_tick_throttle_age_seconds
|
|
932
1043
|
_hook_tick_throttle_touch = _cctally_record._hook_tick_throttle_touch
|
|
1044
|
+
# Statusline usage-persistence markers (spec 2026-07-17). Re-exported so
|
|
1045
|
+
# `cctally.X` (the `_cctally()` accessor + test monkeypatch surface) resolves.
|
|
1046
|
+
_statusline_observe_age_seconds = _cctally_record._statusline_observe_age_seconds
|
|
1047
|
+
_statusline_observe_touch = _cctally_record._statusline_observe_touch
|
|
1048
|
+
_oauth_backoff_remaining_seconds = _cctally_record._oauth_backoff_remaining_seconds
|
|
1049
|
+
_oauth_backoff_set = _cctally_record._oauth_backoff_set
|
|
1050
|
+
_oauth_backoff_clear = _cctally_record._oauth_backoff_clear
|
|
1051
|
+
_oauth_backoff_count = _cctally_record._oauth_backoff_count
|
|
1052
|
+
_oauth_backoff_register_429 = _cctally_record._oauth_backoff_register_429
|
|
1053
|
+
_oauth_backoff_reset = _cctally_record._oauth_backoff_reset
|
|
933
1054
|
_hook_tick_read_stdin_event = _cctally_record._hook_tick_read_stdin_event
|
|
934
1055
|
_hook_tick_session_short = _cctally_record._hook_tick_session_short
|
|
935
1056
|
_hook_tick_format_log_line = _cctally_record._hook_tick_format_log_line
|
|
1057
|
+
_cmd_hook_tick_codex = _cctally_record._cmd_hook_tick_codex
|
|
936
1058
|
cmd_hook_tick = _cctally_record.cmd_hook_tick
|
|
937
1059
|
_safe_float = _cctally_record._safe_float
|
|
938
1060
|
_validate_date_optional = _cctally_record._validate_date_optional
|
|
@@ -1182,6 +1304,14 @@ cmd_statusline = _cctally_statusline.cmd_statusline
|
|
|
1182
1304
|
_resolve_context_window = _cctally_statusline._resolve_context_window
|
|
1183
1305
|
_read_last_assistant_usage = _cctally_statusline._read_last_assistant_usage
|
|
1184
1306
|
_build_statusline_injections = _cctally_statusline._build_statusline_injections
|
|
1307
|
+
# Statusline usage-persistence feeder (spec 2026-07-17). Re-exported so
|
|
1308
|
+
# `cctally._statusline_persist` (test surface) + the `_cctally()` accessor
|
|
1309
|
+
# resolve them.
|
|
1310
|
+
_statusline_persist = _cctally_statusline._statusline_persist
|
|
1311
|
+
_fork_persist = _cctally_statusline._fork_persist
|
|
1312
|
+
_record_args = _cctally_statusline._record_args
|
|
1313
|
+
_try_acquire_persist_lock = _cctally_statusline._try_acquire_persist_lock
|
|
1314
|
+
_release_persist_lock = _cctally_statusline._release_persist_lock
|
|
1185
1315
|
|
|
1186
1316
|
|
|
1187
1317
|
# Eager re-export of bin/_cctally_codex.py — the four cmd_codex_* are the
|
|
@@ -1205,6 +1335,50 @@ cmd_codex_monthly = _cctally_codex.cmd_codex_monthly
|
|
|
1205
1335
|
cmd_codex_weekly = _cctally_codex.cmd_codex_weekly
|
|
1206
1336
|
cmd_codex_session = _cctally_codex.cmd_codex_session
|
|
1207
1337
|
|
|
1338
|
+
# Eager re-export of bin/_cctally_quota.py — the five canonical nested
|
|
1339
|
+
# `cctally codex quota` leaves use the durable S2 projection/interpretation
|
|
1340
|
+
# adapter while retaining the existing Codex accounting commands unchanged.
|
|
1341
|
+
_cctally_quota = _load_sibling("_cctally_quota")
|
|
1342
|
+
reconcile_codex_quota_projection = _cctally_quota.reconcile_codex_quota_projection
|
|
1343
|
+
cmd_codex_quota_history = _cctally_quota.cmd_codex_quota_history
|
|
1344
|
+
cmd_codex_quota_statusline = _cctally_quota.cmd_codex_quota_statusline
|
|
1345
|
+
cmd_codex_quota_forecast = _cctally_quota.cmd_codex_quota_forecast
|
|
1346
|
+
cmd_codex_quota_blocks = _cctally_quota.cmd_codex_quota_blocks
|
|
1347
|
+
cmd_codex_quota_breakdown = _cctally_quota.cmd_codex_quota_breakdown
|
|
1348
|
+
|
|
1349
|
+
# S3's qualified Codex accounting adapter is intentionally cache-only: callers
|
|
1350
|
+
# that need project identity must not downgrade to an unqualified rollout parse.
|
|
1351
|
+
try:
|
|
1352
|
+
_cctally_source_analytics = _load_sibling("_cctally_source_analytics")
|
|
1353
|
+
QualifiedMetadataUnavailable = _cctally_source_analytics.QualifiedMetadataUnavailable
|
|
1354
|
+
load_qualified_codex_entries = _cctally_source_analytics.load_qualified_codex_entries
|
|
1355
|
+
cmd_source_project = _cctally_source_analytics.cmd_source_project
|
|
1356
|
+
cmd_source_diff = _cctally_source_analytics.cmd_source_diff
|
|
1357
|
+
cmd_source_range_cost = _cctally_source_analytics.cmd_source_range_cost
|
|
1358
|
+
cmd_source_cache_report = _cctally_source_analytics.cmd_source_cache_report
|
|
1359
|
+
cmd_source_report = _cctally_source_analytics.cmd_source_report
|
|
1360
|
+
validate_source_project_selectors = _cctally_source_analytics.validate_source_project_selectors
|
|
1361
|
+
_lib_source_analytics = _load_sibling("_lib_source_analytics")
|
|
1362
|
+
QualifiedCodexEntry = _lib_source_analytics.QualifiedCodexEntry
|
|
1363
|
+
SourceWarning = _lib_source_analytics.SourceWarning
|
|
1364
|
+
SourceResult = _lib_source_analytics.SourceResult
|
|
1365
|
+
opaque_project_key = _lib_source_analytics.opaque_project_key
|
|
1366
|
+
except Exception:
|
|
1367
|
+
# The Task 1 foundation remains private until its later CLI/share surfaces
|
|
1368
|
+
# are promoted; public mirror builds must retain their current import path.
|
|
1369
|
+
QualifiedMetadataUnavailable = None
|
|
1370
|
+
load_qualified_codex_entries = None
|
|
1371
|
+
QualifiedCodexEntry = None
|
|
1372
|
+
SourceWarning = None
|
|
1373
|
+
SourceResult = None
|
|
1374
|
+
opaque_project_key = None
|
|
1375
|
+
cmd_source_project = None
|
|
1376
|
+
cmd_source_diff = None
|
|
1377
|
+
cmd_source_range_cost = None
|
|
1378
|
+
cmd_source_cache_report = None
|
|
1379
|
+
cmd_source_report = None
|
|
1380
|
+
validate_source_project_selectors = None
|
|
1381
|
+
|
|
1208
1382
|
# Eager re-export of bin/_cctally_reporting.py — the five cmd_* are the
|
|
1209
1383
|
# parser's `set_defaults(func=c.cmd_*)` targets; `cmd_session` + the others
|
|
1210
1384
|
# are retrieved by tests via `ns["…"]`. So EVERY moved symbol must live here.
|
|
@@ -2569,6 +2743,125 @@ _cctally_diff = _load_sibling("_cctally_diff")
|
|
|
2569
2743
|
cmd_diff = _cctally_diff.cmd_diff # parser: c.cmd_diff (_cctally_parser.py:2032)
|
|
2570
2744
|
_emit_diff_debug_samples = _cctally_diff._emit_diff_debug_samples # test_debug_sample_emission mod._emit_diff_debug_samples
|
|
2571
2745
|
|
|
2746
|
+
|
|
2747
|
+
# Provider-aware analytics deliberately wrap the established command entries
|
|
2748
|
+
# here, leaving their Claude implementation modules untouched below the branch.
|
|
2749
|
+
_cmd_project_claude = cmd_project
|
|
2750
|
+
_cmd_diff_claude = cmd_diff
|
|
2751
|
+
_cmd_range_cost_claude = cmd_range_cost
|
|
2752
|
+
_cmd_cache_report_claude = cmd_cache_report
|
|
2753
|
+
_cmd_report_claude = cmd_report
|
|
2754
|
+
|
|
2755
|
+
|
|
2756
|
+
def _dispatch_source_aware(args, claude_command, source_command):
|
|
2757
|
+
"""Preserve ordinary Claude bytes; route share/non-Claude requests to S3."""
|
|
2758
|
+
if getattr(args, "source", "claude") == "claude":
|
|
2759
|
+
# S3 adds share artifacts to three legacy Claude commands. Keep the
|
|
2760
|
+
# established terminal and JSON routes exactly as they were, but route
|
|
2761
|
+
# the new format-only surface through the structured legacy-result
|
|
2762
|
+
# adapter. That adapter never captures or reparses stdout.
|
|
2763
|
+
if (
|
|
2764
|
+
getattr(args, "format", None)
|
|
2765
|
+
and getattr(args, "command", None) in {
|
|
2766
|
+
"diff", "range-cost", "cache-report",
|
|
2767
|
+
}
|
|
2768
|
+
):
|
|
2769
|
+
if source_command is None:
|
|
2770
|
+
raise RuntimeError("provider-aware analytics are unavailable")
|
|
2771
|
+
return source_command(args)
|
|
2772
|
+
return claude_command(args)
|
|
2773
|
+
if source_command is None:
|
|
2774
|
+
raise RuntimeError("provider-aware analytics are unavailable")
|
|
2775
|
+
# The historical diff renderer calls this field ``emit_json`` while the
|
|
2776
|
+
# S3 source adapter uses the shared ``json`` spelling.
|
|
2777
|
+
if hasattr(args, "emit_json"):
|
|
2778
|
+
args.json = bool(args.emit_json)
|
|
2779
|
+
return source_command(args)
|
|
2780
|
+
|
|
2781
|
+
|
|
2782
|
+
def cmd_project(args):
|
|
2783
|
+
return _dispatch_source_aware(args, _cmd_project_claude, cmd_source_project)
|
|
2784
|
+
|
|
2785
|
+
|
|
2786
|
+
def cmd_diff(args):
|
|
2787
|
+
return _dispatch_source_aware(args, _cmd_diff_claude, cmd_source_diff)
|
|
2788
|
+
|
|
2789
|
+
|
|
2790
|
+
def cmd_range_cost(args):
|
|
2791
|
+
return _dispatch_source_aware(args, _cmd_range_cost_claude, cmd_source_range_cost)
|
|
2792
|
+
|
|
2793
|
+
|
|
2794
|
+
def cmd_cache_report(args):
|
|
2795
|
+
return _dispatch_source_aware(args, _cmd_cache_report_claude, cmd_source_cache_report)
|
|
2796
|
+
|
|
2797
|
+
|
|
2798
|
+
def cmd_report(args):
|
|
2799
|
+
return _dispatch_source_aware(args, _cmd_report_claude, cmd_source_report)
|
|
2800
|
+
|
|
2801
|
+
|
|
2802
|
+
def _validate_source_args(args) -> None:
|
|
2803
|
+
"""Fail incompatible provider options before cache sync or rendering."""
|
|
2804
|
+
command = getattr(args, "command", None)
|
|
2805
|
+
if command not in {"project", "diff", "range-cost", "cache-report", "report"}:
|
|
2806
|
+
return
|
|
2807
|
+
source = getattr(args, "source", "claude")
|
|
2808
|
+
speed = getattr(args, "speed", "auto")
|
|
2809
|
+
if source == "claude" and speed != "auto":
|
|
2810
|
+
raise ValueError("--speed is only valid for Codex or all-source requests")
|
|
2811
|
+
if command == "project" and source in {"codex", "all"} and getattr(args, "sort", None) == "used":
|
|
2812
|
+
raise ValueError("project --sort used is only valid for Claude")
|
|
2813
|
+
if command in {"project", "range-cost", "cache-report"} and source in {"codex", "all"}:
|
|
2814
|
+
if validate_source_project_selectors is not None:
|
|
2815
|
+
validate_source_project_selectors(getattr(args, "project", None))
|
|
2816
|
+
if command == "range-cost" and source == "codex" and getattr(args, "mode", "auto") != "auto":
|
|
2817
|
+
raise ValueError("range-cost --mode is only valid for Claude")
|
|
2818
|
+
if command == "report" and source in {"codex", "all"}:
|
|
2819
|
+
weeks = getattr(args, "weeks", None)
|
|
2820
|
+
if not isinstance(weeks, int) or isinstance(weeks, bool) or weeks <= 0:
|
|
2821
|
+
raise ValueError("report --weeks must be a positive integer")
|
|
2822
|
+
if command == "report" and source == "codex":
|
|
2823
|
+
if getattr(args, "week_start_name", None) is not None:
|
|
2824
|
+
raise ValueError("report --week-start-name is only valid for Claude")
|
|
2825
|
+
if getattr(args, "mode", "auto") != "auto":
|
|
2826
|
+
raise ValueError("report --mode is only valid for Claude")
|
|
2827
|
+
if getattr(args, "offline", False):
|
|
2828
|
+
raise ValueError("report --offline is only valid for Claude")
|
|
2829
|
+
if getattr(args, "project", None):
|
|
2830
|
+
raise ValueError("report --project is only valid for Claude")
|
|
2831
|
+
if command == "cache-report":
|
|
2832
|
+
sort = getattr(args, "sort", None)
|
|
2833
|
+
if source == "claude" and sort == "reuse":
|
|
2834
|
+
raise ValueError("cache-report --sort reuse is only valid for Codex")
|
|
2835
|
+
if source == "codex" and sort in {"net", "cache", "anomaly"}:
|
|
2836
|
+
raise ValueError(f"cache-report --sort {sort} is only valid for Claude")
|
|
2837
|
+
if source == "codex" and (
|
|
2838
|
+
getattr(args, "anomaly_threshold_pp", 15) != 15
|
|
2839
|
+
or getattr(args, "anomaly_window_days", 14) != 14
|
|
2840
|
+
or getattr(args, "no_anomaly", False)
|
|
2841
|
+
):
|
|
2842
|
+
raise ValueError("cache-report anomaly options are only valid for Claude")
|
|
2843
|
+
if command == "diff":
|
|
2844
|
+
explicit_only = getattr(args, "only", None)
|
|
2845
|
+
selected = {
|
|
2846
|
+
item.strip() for item in (explicit_only or "").split(",")
|
|
2847
|
+
if item.strip()
|
|
2848
|
+
}
|
|
2849
|
+
if source in {"codex", "all"} and explicit_only is not None and not selected:
|
|
2850
|
+
raise ValueError(
|
|
2851
|
+
"diff --only specified no sections. Supported: "
|
|
2852
|
+
"cache, models, overall, projects, token-reuse"
|
|
2853
|
+
)
|
|
2854
|
+
if source == "claude" and "token-reuse" in selected:
|
|
2855
|
+
raise ValueError("diff --only token-reuse is only valid for Codex")
|
|
2856
|
+
if source == "codex" and "cache" in selected:
|
|
2857
|
+
raise ValueError("diff --only cache is only valid for Claude")
|
|
2858
|
+
supported = {"overall", "models", "projects", "cache", "token-reuse"}
|
|
2859
|
+
unknown = selected - supported
|
|
2860
|
+
if unknown:
|
|
2861
|
+
raise ValueError(
|
|
2862
|
+
"diff --only contains unknown section(s): " + ", ".join(sorted(unknown))
|
|
2863
|
+
)
|
|
2864
|
+
|
|
2572
2865
|
# #281 S4: the `transcript export|search` command module. Eager re-export so the
|
|
2573
2866
|
# parser's set_defaults(func=c.cmd_transcript) resolves at build_parser time.
|
|
2574
2867
|
_cctally_transcript = _load_sibling("_cctally_transcript")
|
|
@@ -2742,6 +3035,7 @@ _tui_build_session_detail = _cctally_tui._tui_build_session_detail
|
|
|
2742
3035
|
_tui_build_session_detail_indexed = _cctally_tui._tui_build_session_detail_indexed
|
|
2743
3036
|
_tui_build_snapshot = _cctally_tui._tui_build_snapshot
|
|
2744
3037
|
_tui_empty_snapshot = _cctally_tui._tui_empty_snapshot
|
|
3038
|
+
_snapshot_data_version = _cctally_tui._snapshot_data_version # #300 change-signal helper
|
|
2745
3039
|
# Key reader + dispatcher + sync thread base class
|
|
2746
3040
|
TuiKeyReader = _cctally_tui.TuiKeyReader
|
|
2747
3041
|
_tui_handle_key = _cctally_tui._tui_handle_key
|
|
@@ -2824,6 +3118,26 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
2824
3118
|
return 0
|
|
2825
3119
|
if not getattr(args, "func", None):
|
|
2826
3120
|
parser.error("a subcommand is required")
|
|
3121
|
+
# Validate share destination shape before the migration banner, config
|
|
3122
|
+
# load, cache sync, quota reconciliation, or any output side effect. The
|
|
3123
|
+
# command handlers repeat this idempotent check for direct-call safety.
|
|
3124
|
+
_share_validate_args(args)
|
|
3125
|
+
# Provider-aware parsers accept the common output destination flags but
|
|
3126
|
+
# defer several presentation defaults to the source share composer. Give
|
|
3127
|
+
# their outer namespace the established share defaults before any handler
|
|
3128
|
+
# can hand it to ``_share_render_and_emit``.
|
|
3129
|
+
for _share_name, _share_default in (
|
|
3130
|
+
("theme", "light"),
|
|
3131
|
+
("no_branding", False),
|
|
3132
|
+
("reveal_projects", False),
|
|
3133
|
+
):
|
|
3134
|
+
if not hasattr(args, _share_name):
|
|
3135
|
+
setattr(args, _share_name, _share_default)
|
|
3136
|
+
try:
|
|
3137
|
+
_validate_source_args(args)
|
|
3138
|
+
except ValueError as exc:
|
|
3139
|
+
eprint(f"cctally: {exc}")
|
|
3140
|
+
return 2
|
|
2827
3141
|
_print_migration_error_banner_if_needed(args)
|
|
2828
3142
|
try:
|
|
2829
3143
|
rc = int(args.func(args))
|