cctally 1.80.4 → 1.82.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 +32 -0
- package/bin/_cctally_account.py +397 -0
- package/bin/_cctally_alerts.py +58 -2
- package/bin/_cctally_cache.py +696 -166
- package/bin/_cctally_cache_report.py +21 -3
- package/bin/_cctally_config.py +119 -8
- package/bin/_cctally_core.py +343 -48
- package/bin/_cctally_dashboard.py +32 -7
- package/bin/_cctally_dashboard_conversation.py +6 -2
- package/bin/_cctally_dashboard_envelope.py +49 -0
- package/bin/_cctally_dashboard_share.py +78 -1
- package/bin/_cctally_dashboard_sources.py +434 -48
- package/bin/_cctally_db.py +659 -152
- package/bin/_cctally_diff.py +9 -0
- package/bin/_cctally_doctor.py +201 -26
- package/bin/_cctally_five_hour.py +110 -69
- package/bin/_cctally_forecast.py +112 -43
- package/bin/_cctally_journal.py +591 -80
- package/bin/_cctally_milestones.py +180 -67
- package/bin/_cctally_parser.py +87 -0
- package/bin/_cctally_percent_breakdown.py +24 -15
- package/bin/_cctally_project.py +12 -1
- package/bin/_cctally_quota.py +150 -39
- package/bin/_cctally_record.py +491 -141
- package/bin/_cctally_reporting.py +66 -14
- package/bin/_cctally_setup.py +9 -1
- package/bin/_cctally_source_analytics.py +56 -7
- package/bin/_cctally_statusline.py +56 -8
- package/bin/_cctally_store.py +20 -8
- package/bin/_cctally_sync_week.py +30 -10
- package/bin/_cctally_tui.py +99 -18
- package/bin/_cctally_weekrefs.py +38 -15
- package/bin/_lib_accounts.py +325 -0
- package/bin/_lib_alerts_payload.py +25 -4
- package/bin/_lib_cache_writer_lock.py +77 -0
- package/bin/_lib_codex_hooks.py +40 -1
- package/bin/_lib_diff_kernel.py +28 -14
- package/bin/_lib_doctor.py +160 -0
- package/bin/_lib_journal.py +65 -2
- package/bin/_lib_pricing.py +27 -3
- package/bin/_lib_quota.py +81 -4
- package/bin/_lib_render.py +19 -5
- package/bin/_lib_share.py +25 -0
- package/bin/_lib_snapshot_cache.py +8 -0
- package/bin/_lib_subscription_weeks.py +16 -2
- package/bin/_lib_view_models.py +18 -9
- package/bin/cctally +43 -4
- package/dashboard/static/assets/index-DJP4gEB7.js +92 -0
- package/dashboard/static/assets/index-Dk1nplOz.css +1 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +4 -1
- package/dashboard/static/assets/index-BzWujBS3.js +0 -92
- package/dashboard/static/assets/index-Cc2ykqF_.css +0 -1
package/bin/_cctally_parser.py
CHANGED
|
@@ -302,6 +302,20 @@ def _add_source_args(
|
|
|
302
302
|
)
|
|
303
303
|
|
|
304
304
|
|
|
305
|
+
def _add_account_arg(parser: argparse.ArgumentParser) -> None:
|
|
306
|
+
"""Attach the ``--account <ref>`` render filter (#341, spec §3).
|
|
307
|
+
|
|
308
|
+
Default ``None`` -> the merged view, byte-identical to today (R8). The ref is
|
|
309
|
+
resolved at command time via ``_cctally_account.resolve_account_filter``
|
|
310
|
+
(case-insensitive label -> email -> unique key-prefix; ``unattributed``
|
|
311
|
+
accepted literally; ambiguous/unknown -> exit 2). Idempotent per parser."""
|
|
312
|
+
parser.add_argument(
|
|
313
|
+
"--account", metavar="REF", default=None,
|
|
314
|
+
help="Scope output to one account (label / email / account-key prefix / "
|
|
315
|
+
"'unattributed'). Ambiguous or unknown ref exits 2.",
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
|
|
305
319
|
def _add_share_args(
|
|
306
320
|
parser, *, has_status_line: bool = False, json_dest: str = "json",
|
|
307
321
|
) -> None:
|
|
@@ -509,6 +523,7 @@ def _build_daily_parser(subparsers, name, *, help_text, xref):
|
|
|
509
523
|
_add_ccusage_alias_args(p, ansi_emit=False)
|
|
510
524
|
_add_mode_arg(p)
|
|
511
525
|
_add_share_args(p)
|
|
526
|
+
_add_account_arg(p)
|
|
512
527
|
p.set_defaults(func=c.cmd_daily)
|
|
513
528
|
return p
|
|
514
529
|
|
|
@@ -561,6 +576,7 @@ def _build_monthly_parser(subparsers, name, *, help_text, xref):
|
|
|
561
576
|
_add_ccusage_alias_args(p, ansi_emit=False)
|
|
562
577
|
_add_mode_arg(p)
|
|
563
578
|
_add_share_args(p)
|
|
579
|
+
_add_account_arg(p)
|
|
564
580
|
p.set_defaults(func=c.cmd_monthly)
|
|
565
581
|
return p
|
|
566
582
|
|
|
@@ -603,6 +619,7 @@ def _build_weekly_parser(subparsers, name, *, help_text, xref):
|
|
|
603
619
|
_add_ccusage_alias_args(p, ansi_emit=False)
|
|
604
620
|
_add_mode_arg(p)
|
|
605
621
|
_add_share_args(p)
|
|
622
|
+
_add_account_arg(p)
|
|
606
623
|
p.set_defaults(func=c.cmd_weekly)
|
|
607
624
|
return p
|
|
608
625
|
|
|
@@ -656,6 +673,7 @@ def _build_session_parser(subparsers, name, *, help_text, xref):
|
|
|
656
673
|
_add_ccusage_alias_args(p, ansi_emit=False)
|
|
657
674
|
_add_mode_arg(p)
|
|
658
675
|
_add_share_args(p)
|
|
676
|
+
_add_account_arg(p)
|
|
659
677
|
p.set_defaults(func=c.cmd_session)
|
|
660
678
|
return p
|
|
661
679
|
|
|
@@ -1083,6 +1101,9 @@ def _add_codex_quota_common_args(parser, *, sync_by_default: bool = True) -> Non
|
|
|
1083
1101
|
"--json", action="store_true",
|
|
1084
1102
|
help="Emit stamped schemaVersion:1 JSON.",
|
|
1085
1103
|
)
|
|
1104
|
+
# #341 --account: scope the native quota views to one Codex account
|
|
1105
|
+
# (provider="codex"). Shared by all 5 quota leaves via this chokepoint.
|
|
1106
|
+
_add_account_arg(parser)
|
|
1086
1107
|
|
|
1087
1108
|
|
|
1088
1109
|
def _build_codex_quota_parser(subparsers, name, *, help_text, xref=None):
|
|
@@ -1374,6 +1395,7 @@ def _build_report_parser(subparsers, name, *, help_text, xref=None, fixed_source
|
|
|
1374
1395
|
)
|
|
1375
1396
|
_add_source_args(pr, fixed_source=fixed_source, speed=True)
|
|
1376
1397
|
_add_share_args(pr)
|
|
1398
|
+
_add_account_arg(pr)
|
|
1377
1399
|
pr.set_defaults(func=c.cmd_report)
|
|
1378
1400
|
|
|
1379
1401
|
def _build_forecast_parser(subparsers, name, *, help_text, xref=None):
|
|
@@ -1424,6 +1446,7 @@ def _build_forecast_parser(subparsers, name, *, help_text, xref=None):
|
|
|
1424
1446
|
help="Color output control (also honors NO_COLOR).")
|
|
1425
1447
|
fc.add_argument("--as-of", dest="as_of", default=None, help=argparse.SUPPRESS)
|
|
1426
1448
|
_add_share_args(fc, has_status_line=True)
|
|
1449
|
+
_add_account_arg(fc)
|
|
1427
1450
|
fc.set_defaults(func=c.cmd_forecast)
|
|
1428
1451
|
|
|
1429
1452
|
def _build_budget_parser(subparsers, name, *, help_text, xref=None):
|
|
@@ -1545,6 +1568,7 @@ def _build_percent_breakdown_parser(subparsers, name, *, help_text, xref=None):
|
|
|
1545
1568
|
help="Display timezone: local, utc, or IANA name. "
|
|
1546
1569
|
"Overrides config display.tz for this call.",
|
|
1547
1570
|
)
|
|
1571
|
+
_add_account_arg(pb)
|
|
1548
1572
|
pb.set_defaults(func=c.cmd_percent_breakdown)
|
|
1549
1573
|
|
|
1550
1574
|
def _build_five_hour_breakdown_parser(subparsers, name, *, help_text, xref=None):
|
|
@@ -1606,6 +1630,7 @@ def _build_five_hour_breakdown_parser(subparsers, name, *, help_text, xref=None)
|
|
|
1606
1630
|
help="Display timezone: local, utc, or IANA name. "
|
|
1607
1631
|
"Overrides config display.tz for this call.",
|
|
1608
1632
|
)
|
|
1633
|
+
_add_account_arg(fhbd)
|
|
1609
1634
|
fhbd.set_defaults(func=c.cmd_five_hour_breakdown)
|
|
1610
1635
|
|
|
1611
1636
|
def _build_tui_parser(subparsers, name, *, help_text, xref=None):
|
|
@@ -2026,6 +2051,7 @@ def _build_cache_report_parser(subparsers, name, *, help_text, xref=None, fixed_
|
|
|
2026
2051
|
_add_ccusage_alias_args(pc, ansi_emit=False)
|
|
2027
2052
|
_add_source_args(pc, fixed_source=fixed_source, speed=True)
|
|
2028
2053
|
_add_share_args(pc)
|
|
2054
|
+
_add_account_arg(pc)
|
|
2029
2055
|
pc.set_defaults(func=c.cmd_cache_report)
|
|
2030
2056
|
|
|
2031
2057
|
def _build_range_cost_parser(subparsers, name, *, help_text, xref=None, fixed_source=None):
|
|
@@ -2093,6 +2119,7 @@ def _build_range_cost_parser(subparsers, name, *, help_text, xref=None, fixed_so
|
|
|
2093
2119
|
_add_ccusage_alias_args(rc, ansi_emit=False)
|
|
2094
2120
|
_add_source_args(rc, fixed_source=fixed_source, speed=True)
|
|
2095
2121
|
_add_share_args(rc)
|
|
2122
|
+
_add_account_arg(rc)
|
|
2096
2123
|
rc.set_defaults(func=c.cmd_range_cost)
|
|
2097
2124
|
|
|
2098
2125
|
def _build_five_hour_blocks_parser(subparsers, name, *, help_text, xref=None):
|
|
@@ -2150,6 +2177,7 @@ def _build_five_hour_blocks_parser(subparsers, name, *, help_text, xref=None):
|
|
|
2150
2177
|
_add_ccusage_alias_args(fhb, ansi_emit=False)
|
|
2151
2178
|
_add_mode_arg(fhb, noop=True)
|
|
2152
2179
|
_add_share_args(fhb)
|
|
2180
|
+
_add_account_arg(fhb)
|
|
2153
2181
|
fhb.set_defaults(func=c.cmd_five_hour_blocks)
|
|
2154
2182
|
|
|
2155
2183
|
def _build_cache_sync_parser(subparsers, name, *, help_text, xref=None):
|
|
@@ -2266,6 +2294,7 @@ def _build_project_parser(subparsers, name, *, help_text, xref=None, fixed_sourc
|
|
|
2266
2294
|
_add_ccusage_alias_args(p_project, ansi_emit=True)
|
|
2267
2295
|
_add_source_args(p_project, fixed_source=fixed_source, speed=True)
|
|
2268
2296
|
_add_share_args(p_project)
|
|
2297
|
+
_add_account_arg(p_project)
|
|
2269
2298
|
p_project.set_defaults(func=c.cmd_project)
|
|
2270
2299
|
|
|
2271
2300
|
def _build_diff_parser(subparsers, name, *, help_text, xref=None, fixed_source=None):
|
|
@@ -2316,6 +2345,7 @@ def _build_diff_parser(subparsers, name, *, help_text, xref=None, fixed_source=N
|
|
|
2316
2345
|
_add_ccusage_alias_args(diff_p, ansi_emit=True)
|
|
2317
2346
|
_add_source_args(diff_p, fixed_source=fixed_source, speed=True)
|
|
2318
2347
|
_add_share_args(diff_p, json_dest="emit_json")
|
|
2348
|
+
_add_account_arg(diff_p)
|
|
2319
2349
|
diff_p.set_defaults(func=c.cmd_diff)
|
|
2320
2350
|
|
|
2321
2351
|
def _build_claude_parser(subparsers, name, *, help_text, xref=None):
|
|
@@ -2519,6 +2549,62 @@ def _build_telemetry_parser(subparsers, name, *, help_text, xref=None):
|
|
|
2519
2549
|
)
|
|
2520
2550
|
tele_p.set_defaults(func=c.cmd_telemetry)
|
|
2521
2551
|
|
|
2552
|
+
def _build_account_parser(subparsers, name, *, help_text, xref=None):
|
|
2553
|
+
"""Build the `account` parser (registered via _REGISTRATION; #341 Task 3).
|
|
2554
|
+
|
|
2555
|
+
``list`` / ``show <ref>`` / ``label <ref> <name>`` over the accounts
|
|
2556
|
+
registry. Call-time ``c = _cctally()`` binding, mirroring the config/db
|
|
2557
|
+
nested-subcommand shape.
|
|
2558
|
+
"""
|
|
2559
|
+
c = _cctally()
|
|
2560
|
+
acct_p = subparsers.add_parser(
|
|
2561
|
+
name,
|
|
2562
|
+
help=help_text,
|
|
2563
|
+
formatter_class=CLIHelpFormatter,
|
|
2564
|
+
description=textwrap.dedent("""\
|
|
2565
|
+
Inspect the per-provider account registry (multi-account, #341).
|
|
2566
|
+
|
|
2567
|
+
cctally observes which Claude/Codex account each usage sample was
|
|
2568
|
+
written under (from the provider's own on-disk credential state)
|
|
2569
|
+
and records milestones/quota/alerts per account. This subcommand
|
|
2570
|
+
lists the observed accounts, shows one account's identity +
|
|
2571
|
+
attribution summary, and sets a durable friendly label.
|
|
2572
|
+
|
|
2573
|
+
A <ref> is resolved case-insensitively by label, then email, then
|
|
2574
|
+
a unique account-key prefix; the literal 'unattributed' is accepted
|
|
2575
|
+
for the pre-feature / unresolved bucket. An ambiguous or unknown ref
|
|
2576
|
+
exits 2 with the candidate keys on stderr.
|
|
2577
|
+
|
|
2578
|
+
Actions:
|
|
2579
|
+
list List every observed account (add --json for the
|
|
2580
|
+
stamped-first camelCase envelope).
|
|
2581
|
+
show <ref> Show one account's identity + attribution summary.
|
|
2582
|
+
label <ref> <name>
|
|
2583
|
+
Set a durable user label (survives db rebuild).
|
|
2584
|
+
|
|
2585
|
+
Examples:
|
|
2586
|
+
cctally account list
|
|
2587
|
+
cctally account list --json
|
|
2588
|
+
cctally account show work@example.com
|
|
2589
|
+
cctally account label 3f2a1b work
|
|
2590
|
+
"""),
|
|
2591
|
+
)
|
|
2592
|
+
acct_sub = acct_p.add_subparsers(dest="account_action", required=True)
|
|
2593
|
+
acct_list = acct_sub.add_parser("list", help="List observed accounts")
|
|
2594
|
+
acct_list.add_argument("--json", dest="emit_json", action="store_true",
|
|
2595
|
+
help="Emit the stamped-first camelCase JSON envelope.")
|
|
2596
|
+
acct_list.set_defaults(func=c.cmd_account)
|
|
2597
|
+
acct_show = acct_sub.add_parser("show", help="Show one account's detail")
|
|
2598
|
+
acct_show.add_argument("ref", help="Account ref (label / email / key prefix)")
|
|
2599
|
+
acct_show.add_argument("--json", dest="emit_json", action="store_true",
|
|
2600
|
+
help="Emit the stamped-first camelCase JSON envelope.")
|
|
2601
|
+
acct_show.set_defaults(func=c.cmd_account)
|
|
2602
|
+
acct_label = acct_sub.add_parser("label", help="Set a durable account label")
|
|
2603
|
+
acct_label.add_argument("ref", help="Account ref (label / email / key prefix)")
|
|
2604
|
+
acct_label.add_argument("label", help="New label")
|
|
2605
|
+
acct_label.set_defaults(func=c.cmd_account)
|
|
2606
|
+
|
|
2607
|
+
|
|
2522
2608
|
def _build_alerts_parser(subparsers, name, *, help_text, xref=None):
|
|
2523
2609
|
"""Build the `alerts` parser (registered via _REGISTRATION; #279 S6 W3).
|
|
2524
2610
|
|
|
@@ -3277,6 +3363,7 @@ _REGISTRATION = (
|
|
|
3277
3363
|
_Reg('claude', _build_claude_parser, "Claude-source reports (drop-in for `ccusage claude …`)", None, None),
|
|
3278
3364
|
_Reg('codex', _build_codex_parser, "Codex-source reports (drop-in for `ccusage codex …`)", None, None),
|
|
3279
3365
|
_Reg('config', _build_config_parser, "Get / set / unset persisted user preferences", None, None),
|
|
3366
|
+
_Reg('account', _build_account_parser, "Inspect per-provider account registry (list / show / label)", None, None),
|
|
3280
3367
|
_Reg('telemetry', _build_telemetry_parser, "Show or change anonymous install-count telemetry", None, None),
|
|
3281
3368
|
_Reg('alerts', _build_alerts_parser, "Manage threshold alerts", None, None),
|
|
3282
3369
|
_Reg('setup', _build_setup_parser, "Install cctally into Claude Code (hooks + symlinks)", None, None),
|
|
@@ -80,6 +80,14 @@ def _render_percent_breakdown_terminal(
|
|
|
80
80
|
|
|
81
81
|
def cmd_percent_breakdown(args: argparse.Namespace) -> int:
|
|
82
82
|
c = _cctally()
|
|
83
|
+
# #341 --account: resolve the render filter (provider=claude). This command
|
|
84
|
+
# reads only stats tables (percent_milestones / weekly_usage_snapshots), not
|
|
85
|
+
# the entry cache, so needs_cache=False. None = merged / byte-stable.
|
|
86
|
+
acct_key, acct_exit = c.resolve_account_filter(args, "claude", needs_cache=False)
|
|
87
|
+
if acct_exit is not None:
|
|
88
|
+
return acct_exit
|
|
89
|
+
_acct_pred = "" if acct_key is None else " AND account_key = ?"
|
|
90
|
+
_acct_p = () if acct_key is None else (acct_key,)
|
|
83
91
|
config = c.load_config()
|
|
84
92
|
tz = c.resolve_display_tz(args, config)
|
|
85
93
|
args._resolved_tz = tz
|
|
@@ -91,12 +99,10 @@ def cmd_percent_breakdown(args: argparse.Namespace) -> int:
|
|
|
91
99
|
week_start = parse_date_str(args.week_start, "--week-start")
|
|
92
100
|
else:
|
|
93
101
|
latest_usage = conn.execute(
|
|
94
|
-
""
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
LIMIT 1
|
|
99
|
-
"""
|
|
102
|
+
"SELECT week_start_date FROM weekly_usage_snapshots"
|
|
103
|
+
+ (" WHERE 1=1" + _acct_pred if acct_key is not None else "")
|
|
104
|
+
+ " ORDER BY captured_at_utc DESC, id DESC LIMIT 1",
|
|
105
|
+
_acct_p,
|
|
100
106
|
).fetchone()
|
|
101
107
|
if latest_usage is not None:
|
|
102
108
|
week_start = dt.date.fromisoformat(latest_usage["week_start_date"])
|
|
@@ -109,17 +115,18 @@ def cmd_percent_breakdown(args: argparse.Namespace) -> int:
|
|
|
109
115
|
|
|
110
116
|
# Get week_end from any snapshot for this week
|
|
111
117
|
end_row = conn.execute(
|
|
112
|
-
"""
|
|
118
|
+
f"""
|
|
113
119
|
SELECT MAX(week_end_date) AS week_end_date
|
|
114
120
|
FROM (
|
|
115
|
-
SELECT week_end_date FROM weekly_usage_snapshots WHERE week_start_date = ?
|
|
121
|
+
SELECT week_end_date FROM weekly_usage_snapshots WHERE week_start_date = ?{_acct_pred}
|
|
116
122
|
UNION ALL
|
|
117
|
-
SELECT week_end_date FROM weekly_cost_snapshots WHERE week_start_date = ?
|
|
123
|
+
SELECT week_end_date FROM weekly_cost_snapshots WHERE week_start_date = ?{_acct_pred}
|
|
118
124
|
UNION ALL
|
|
119
|
-
SELECT week_end_date FROM percent_milestones WHERE week_start_date = ?
|
|
125
|
+
SELECT week_end_date FROM percent_milestones WHERE week_start_date = ?{_acct_pred}
|
|
120
126
|
)
|
|
121
127
|
""",
|
|
122
|
-
(week_start_date, week_start_date,
|
|
128
|
+
(week_start_date,) + _acct_p + (week_start_date,) + _acct_p
|
|
129
|
+
+ (week_start_date,) + _acct_p,
|
|
123
130
|
).fetchone()
|
|
124
131
|
week_end_date = end_row["week_end_date"] if end_row and end_row["week_end_date"] else (
|
|
125
132
|
(week_start + dt.timedelta(days=6)).isoformat()
|
|
@@ -158,9 +165,9 @@ def cmd_percent_breakdown(args: argparse.Namespace) -> int:
|
|
|
158
165
|
canon_end_for_lookup = None
|
|
159
166
|
latest_end_row = conn.execute(
|
|
160
167
|
"SELECT week_end_at FROM weekly_usage_snapshots "
|
|
161
|
-
"WHERE week_start_date = ? AND week_end_at IS NOT NULL
|
|
162
|
-
"ORDER BY captured_at_utc DESC, id DESC LIMIT 1",
|
|
163
|
-
(week_start_date,),
|
|
168
|
+
"WHERE week_start_date = ? AND week_end_at IS NOT NULL" + _acct_pred +
|
|
169
|
+
" ORDER BY captured_at_utc DESC, id DESC LIMIT 1",
|
|
170
|
+
(week_start_date,) + _acct_p,
|
|
164
171
|
).fetchone()
|
|
165
172
|
if latest_end_row is not None:
|
|
166
173
|
canon_end_for_lookup = _canonicalize_optional_iso(
|
|
@@ -177,7 +184,8 @@ def cmd_percent_breakdown(args: argparse.Namespace) -> int:
|
|
|
177
184
|
active_segment = int(seg_row["id"])
|
|
178
185
|
|
|
179
186
|
milestones = [
|
|
180
|
-
m for m in c.get_milestones_for_week(
|
|
187
|
+
m for m in c.get_milestones_for_week(
|
|
188
|
+
conn, week_start_date, account_key=acct_key)
|
|
181
189
|
if int(m["reset_event_id"] or 0) == active_segment
|
|
182
190
|
]
|
|
183
191
|
|
|
@@ -201,6 +209,7 @@ def cmd_percent_breakdown(args: argparse.Namespace) -> int:
|
|
|
201
209
|
}
|
|
202
210
|
|
|
203
211
|
if args.json:
|
|
212
|
+
output.update(c.account_json_fields(acct_key)) # #341 R8 decoration
|
|
204
213
|
print(json.dumps(c.stamp_schema_version(output), indent=2))
|
|
205
214
|
return 0
|
|
206
215
|
|
package/bin/_cctally_project.py
CHANGED
|
@@ -423,6 +423,7 @@ def cmd_project(args: argparse.Namespace) -> int:
|
|
|
423
423
|
# fallback for non-Monday-reset accounts).
|
|
424
424
|
current_weeks = c._compute_subscription_weeks(
|
|
425
425
|
conn, now, now + dt.timedelta(microseconds=1), config=config,
|
|
426
|
+
account_key=None, # #341: merged (all-accounts) analytics read
|
|
426
427
|
)
|
|
427
428
|
if current_weeks:
|
|
428
429
|
cw_start = parse_iso_datetime(
|
|
@@ -444,6 +445,7 @@ def cmd_project(args: argparse.Namespace) -> int:
|
|
|
444
445
|
# `_aggregate_weekly`'s bisect pattern (first-match-wins on overlap).
|
|
445
446
|
subweeks = c._compute_subscription_weeks(
|
|
446
447
|
conn, since_dt, until_dt, config=config,
|
|
448
|
+
account_key=None, # #341: merged (all-accounts) analytics read
|
|
447
449
|
)
|
|
448
450
|
parsed_bounds: list[tuple[dt.datetime, dt.datetime]] = []
|
|
449
451
|
for sw in subweeks:
|
|
@@ -489,6 +491,13 @@ def cmd_project(args: argparse.Namespace) -> int:
|
|
|
489
491
|
else:
|
|
490
492
|
scan_start, scan_end = since_dt, until_dt
|
|
491
493
|
|
|
494
|
+
# #341 --account: resolve the render filter (provider=claude; fail closed
|
|
495
|
+
# with exit 3 when the entry cache is unavailable). None = merged.
|
|
496
|
+
acct_key, acct_exit = c.resolve_account_filter(
|
|
497
|
+
args, "claude", needs_cache=True)
|
|
498
|
+
if acct_exit is not None:
|
|
499
|
+
return acct_exit
|
|
500
|
+
|
|
492
501
|
resolver_cache: dict[str, ProjectKey] = {}
|
|
493
502
|
buckets: dict[tuple[ProjectKey, dt.datetime], dict] = {}
|
|
494
503
|
total_cost_by_week: dict[dt.datetime, float] = {}
|
|
@@ -502,7 +511,8 @@ def cmd_project(args: argparse.Namespace) -> int:
|
|
|
502
511
|
# aggregation semantics (denominator widened to ALL entries; visible
|
|
503
512
|
# rows only the post-filter subset). The list is small enough to
|
|
504
513
|
# hold (entries already in memory via the cache row factory).
|
|
505
|
-
joined_entries_all = list(c.get_claude_session_entries(
|
|
514
|
+
joined_entries_all = list(c.get_claude_session_entries(
|
|
515
|
+
scan_start, scan_end, account_key=acct_key))
|
|
506
516
|
|
|
507
517
|
# Build the --debug report dataset: skip synthetic + out-of-window
|
|
508
518
|
# entries, then apply --model and --project filters (mirroring the
|
|
@@ -788,6 +798,7 @@ def cmd_project(args: argparse.Namespace) -> int:
|
|
|
788
798
|
include_breakdown=args.breakdown,
|
|
789
799
|
week_snapshots=week_snapshots,
|
|
790
800
|
)
|
|
801
|
+
payload.update(c.account_json_fields(acct_key)) # #341 R8 decoration
|
|
791
802
|
sink = getattr(args, "_source_result_sink", None)
|
|
792
803
|
if sink is not None:
|
|
793
804
|
sink(payload)
|