cctally 1.15.0 → 1.16.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 +8 -0
- package/bin/_lib_aggregators.py +2 -1
- package/bin/_lib_view_models.py +8 -8
- package/bin/cctally +48 -10
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,14 @@ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [1.16.0] - 2026-05-26
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **`-m/--mode {auto,calculate,display}` cost-source selector on `cctally daily`, `monthly`, `weekly`, `session`, and `blocks`** — a drop-in for `ccusage <cmd> --mode`. `auto` (the default) uses the recorded `costUSD` from JSONL when present and otherwise computes from embedded pricing; `calculate` always computes from pricing, ignoring any recorded `costUSD`; `display` shows the recorded `costUSD` only and renders `$0.00` when an entry has none (ccusage-faithful — and because most modern Claude Code JSONL omits `costUSD`, `display` reports `$0` for nearly everything). On `blocks` the mode is honored on both the main grouping and the active canonical-swapped block. `cctally five-hour-blocks` also accepts `--mode` but as a documented no-op, since its cost is the authoritative per-block value materialized at record-time. The default-`auto` output is byte-identical to before for `daily`, `monthly`, `weekly`, and `blocks`, and no `mode` key is added to any JSON shape (see the Changed note below for the one `session` exception). (#86)
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- **`cctally session` (and the TUI session views) now prefer a session's recorded `costUSD` over a recomputed cost by default, for the historical sessions whose JSONL still carries it.** `session` was the one report that always recomputed cost from embedded pricing and ignored any recorded `costUSD`, even though `daily`, `monthly`, `weekly`, and the dashboard already preferred the recorded value — so on `costUSD`-bearing windows `cctally session` totals silently disagreed with `cctally daily`. Wiring the new `--mode auto` default through the session aggregator closes that gap: default totals now match the other reports (and the shipped reconcile invariant `Σ session.totalCost == Σ daily.totalCost`). Only the ~3.9% of historical files that still carry `costUSD` are affected (modern Claude Code omits it); everything else is unchanged. The TUI session panel/detail follow the same default and have no `--mode` flag; pass `cctally session --mode calculate` on the CLI to force the previous always-recompute behavior. (#86)
|
|
15
|
+
|
|
8
16
|
## [1.15.0] - 2026-05-26
|
|
9
17
|
|
|
10
18
|
### Added
|
package/bin/_lib_aggregators.py
CHANGED
|
@@ -608,6 +608,7 @@ def _aggregate_codex_sessions(entries: list[CodexEntry]) -> list[CodexSessionUsa
|
|
|
608
608
|
|
|
609
609
|
def _aggregate_claude_sessions(
|
|
610
610
|
entries: list["_JoinedClaudeEntry"],
|
|
611
|
+
mode: str = "auto",
|
|
611
612
|
) -> list[ClaudeSessionUsage]:
|
|
612
613
|
"""Group entries by session_id, collapsing resumed-across-files sessions.
|
|
613
614
|
|
|
@@ -666,7 +667,7 @@ def _aggregate_claude_sessions(
|
|
|
666
667
|
"cache_creation_input_tokens": entry.cache_creation_tokens,
|
|
667
668
|
"cache_read_input_tokens": entry.cache_read_tokens,
|
|
668
669
|
}
|
|
669
|
-
cost = _calculate_entry_cost(entry.model, usage)
|
|
670
|
+
cost = _calculate_entry_cost(entry.model, usage, mode=mode, cost_usd=entry.cost_usd)
|
|
670
671
|
|
|
671
672
|
sess["input"] += entry.input_tokens
|
|
672
673
|
sess["cache_create"] += entry.cache_creation_tokens
|
package/bin/_lib_view_models.py
CHANGED
|
@@ -318,7 +318,7 @@ class DailyView:
|
|
|
318
318
|
display_tz_label: str = ""
|
|
319
319
|
|
|
320
320
|
|
|
321
|
-
def build_daily_view(entries, *, now_utc, display_tz=None):
|
|
321
|
+
def build_daily_view(entries, *, now_utc, display_tz=None, mode="auto"):
|
|
322
322
|
"""Build a ``DailyView`` from raw ``UsageEntry`` list (spec §5.1).
|
|
323
323
|
|
|
324
324
|
Gap-free: only days with entries appear in ``view.rows`` /
|
|
@@ -336,7 +336,7 @@ def build_daily_view(entries, *, now_utc, display_tz=None):
|
|
|
336
336
|
share consumers ignore them and read ``view.aggregated`` instead.
|
|
337
337
|
"""
|
|
338
338
|
_agg = _load_lib("_lib_aggregators")
|
|
339
|
-
buckets = _agg._aggregate_daily(entries, mode=
|
|
339
|
+
buckets = _agg._aggregate_daily(entries, mode=mode, tz=display_tz)
|
|
340
340
|
if not buckets:
|
|
341
341
|
return DailyView(
|
|
342
342
|
rows=(),
|
|
@@ -427,7 +427,7 @@ class MonthlyView:
|
|
|
427
427
|
display_tz_label: str = ""
|
|
428
428
|
|
|
429
429
|
|
|
430
|
-
def build_monthly_view(entries, *, now_utc, n=12, display_tz=None):
|
|
430
|
+
def build_monthly_view(entries, *, now_utc, n=12, display_tz=None, mode="auto"):
|
|
431
431
|
"""Build a ``MonthlyView`` for the trailing ``n`` calendar months
|
|
432
432
|
(spec §5.2).
|
|
433
433
|
|
|
@@ -440,7 +440,7 @@ def build_monthly_view(entries, *, now_utc, n=12, display_tz=None):
|
|
|
440
440
|
CLI table footer would.
|
|
441
441
|
"""
|
|
442
442
|
_agg = _load_lib("_lib_aggregators")
|
|
443
|
-
buckets = _agg._aggregate_monthly(entries, mode=
|
|
443
|
+
buckets = _agg._aggregate_monthly(entries, mode=mode, tz=display_tz)
|
|
444
444
|
if not buckets:
|
|
445
445
|
return MonthlyView(
|
|
446
446
|
rows=(), aggregated=(),
|
|
@@ -528,7 +528,7 @@ class WeeklyView:
|
|
|
528
528
|
|
|
529
529
|
|
|
530
530
|
def build_weekly_view(conn, entries, *, weeks, now_utc, display_tz=None,
|
|
531
|
-
as_of_utc=None):
|
|
531
|
+
as_of_utc=None, mode="auto"):
|
|
532
532
|
"""Build a ``WeeklyView`` from subscription-week boundaries
|
|
533
533
|
(spec §5.3).
|
|
534
534
|
|
|
@@ -548,7 +548,7 @@ def build_weekly_view(conn, entries, *, weeks, now_utc, display_tz=None,
|
|
|
548
548
|
"""
|
|
549
549
|
_agg = _load_lib("_lib_aggregators")
|
|
550
550
|
_cct_core = _load_lib("_cctally_core")
|
|
551
|
-
buckets_asc = _agg._aggregate_weekly(entries, weeks)
|
|
551
|
+
buckets_asc = _agg._aggregate_weekly(entries, weeks, mode=mode)
|
|
552
552
|
if not buckets_asc:
|
|
553
553
|
return WeeklyView(
|
|
554
554
|
rows=(), aggregated=(), overlay=(),
|
|
@@ -1022,7 +1022,7 @@ class SessionsView:
|
|
|
1022
1022
|
display_tz_label: str = ""
|
|
1023
1023
|
|
|
1024
1024
|
|
|
1025
|
-
def build_sessions_view(entries, *, now_utc, limit=None, display_tz=None):
|
|
1025
|
+
def build_sessions_view(entries, *, now_utc, limit=None, display_tz=None, mode="auto"):
|
|
1026
1026
|
"""Build a ``SessionsView`` from joined Claude session entries
|
|
1027
1027
|
(spec §5.5).
|
|
1028
1028
|
|
|
@@ -1053,7 +1053,7 @@ def build_sessions_view(entries, *, now_utc, limit=None, display_tz=None):
|
|
|
1053
1053
|
"""
|
|
1054
1054
|
import os as _os # late: keep top-level imports lean.
|
|
1055
1055
|
_agg = _load_lib("_lib_aggregators")
|
|
1056
|
-
aggregated = _agg._aggregate_claude_sessions(entries)
|
|
1056
|
+
aggregated = _agg._aggregate_claude_sessions(entries, mode=mode)
|
|
1057
1057
|
# Apply limit truncation up front so `rows` and `aggregated` stay
|
|
1058
1058
|
# in lockstep (spec §4.3 invariant: `total_sessions == len(rows)
|
|
1059
1059
|
# == len(aggregated)`). limit=None → keep everything.
|
package/bin/cctally
CHANGED
|
@@ -4238,7 +4238,7 @@ def cmd_blocks(args: argparse.Namespace) -> int:
|
|
|
4238
4238
|
range_start=range_start,
|
|
4239
4239
|
range_end=range_end,
|
|
4240
4240
|
display_tz=tz,
|
|
4241
|
-
mode=
|
|
4241
|
+
mode=args.mode,
|
|
4242
4242
|
skip_rows=True,
|
|
4243
4243
|
)
|
|
4244
4244
|
blocks = list(view.aggregated)
|
|
@@ -4263,7 +4263,7 @@ def cmd_blocks(args: argparse.Namespace) -> int:
|
|
|
4263
4263
|
# additional entries. Without re-aggregation the displayed window
|
|
4264
4264
|
# said one thing and the cost said another (live data: window
|
|
4265
4265
|
# 20:50→01:50 with $45 cost vs the real $128).
|
|
4266
|
-
_maybe_swap_active_block_to_canonical(blocks, all_entries, now=now_utc)
|
|
4266
|
+
_maybe_swap_active_block_to_canonical(blocks, all_entries, now=now_utc, mode=args.mode)
|
|
4267
4267
|
|
|
4268
4268
|
if args.json:
|
|
4269
4269
|
print(_blocks_to_json(blocks))
|
|
@@ -4284,6 +4284,7 @@ def _maybe_swap_active_block_to_canonical(
|
|
|
4284
4284
|
all_entries: list[Any],
|
|
4285
4285
|
*,
|
|
4286
4286
|
now: dt.datetime,
|
|
4287
|
+
mode: str = "auto",
|
|
4287
4288
|
) -> None:
|
|
4288
4289
|
"""In-place swap of an ACTIVE heuristic block to its API-anchored
|
|
4289
4290
|
canonical window — timestamps AND token/cost totals.
|
|
@@ -4359,10 +4360,10 @@ def _maybe_swap_active_block_to_canonical(
|
|
|
4359
4360
|
if block_end_utc <= now.astimezone(dt.timezone.utc):
|
|
4360
4361
|
return
|
|
4361
4362
|
# Re-aggregate entries over the canonical interval. Build a fresh
|
|
4362
|
-
# Block via ``_build_activity_block``
|
|
4363
|
-
#
|
|
4364
|
-
#
|
|
4365
|
-
# the
|
|
4363
|
+
# Block via ``_build_activity_block`` so every total stays in one code
|
|
4364
|
+
# path — no field-by-field assignment that could drift if the dataclass
|
|
4365
|
+
# grows new fields. Thread the caller's ``mode`` so the active block's
|
|
4366
|
+
# cost honors --mode like the main grouping (Session C / Codex F1).
|
|
4366
4367
|
canonical_entries = [
|
|
4367
4368
|
e for e in all_entries
|
|
4368
4369
|
if block_start_utc <= e.timestamp < block_end_utc
|
|
@@ -4372,7 +4373,7 @@ def _maybe_swap_active_block_to_canonical(
|
|
|
4372
4373
|
block_start_utc,
|
|
4373
4374
|
block_end_utc,
|
|
4374
4375
|
now.astimezone(dt.timezone.utc),
|
|
4375
|
-
|
|
4376
|
+
mode,
|
|
4376
4377
|
anchor="recorded",
|
|
4377
4378
|
)
|
|
4378
4379
|
blocks[active_idx] = rebuilt
|
|
@@ -4541,7 +4542,7 @@ def cmd_daily(args: argparse.Namespace) -> int:
|
|
|
4541
4542
|
# fields live on BucketUsage, not on DailyPanelRow. The builder's
|
|
4542
4543
|
# `_aggregate_daily` call is the same one we used inline.
|
|
4543
4544
|
view = build_daily_view(all_entries, now_utc=_command_as_of(),
|
|
4544
|
-
display_tz=tz)
|
|
4545
|
+
display_tz=tz, mode=args.mode)
|
|
4545
4546
|
# `_aggregate_daily` returned ascending order; build_daily_view stores
|
|
4546
4547
|
# `aggregated` newest-first. CLI's default order is ascending, so
|
|
4547
4548
|
# re-reverse to match the prior on-the-wire shape.
|
|
@@ -4628,7 +4629,7 @@ def cmd_monthly(args: argparse.Namespace) -> int:
|
|
|
4628
4629
|
# Pass a large `n` so the CLI's `--since`/`--until` window controls
|
|
4629
4630
|
# how many months render (the dashboard caps at n=12; CLI doesn't).
|
|
4630
4631
|
view = build_monthly_view(all_entries, now_utc=_command_as_of(),
|
|
4631
|
-
n=10**6, display_tz=tz)
|
|
4632
|
+
n=10**6, display_tz=tz, mode=args.mode)
|
|
4632
4633
|
# The view stores `aggregated` newest-first; CLI default is asc.
|
|
4633
4634
|
months = list(reversed(view.aggregated))
|
|
4634
4635
|
|
|
@@ -4745,7 +4746,7 @@ def cmd_weekly(args: argparse.Namespace) -> int:
|
|
|
4745
4746
|
# assertions stay aligned.
|
|
4746
4747
|
view = build_weekly_view(
|
|
4747
4748
|
conn, all_entries, weeks=weeks, now_utc=now_utc,
|
|
4748
|
-
display_tz=args._resolved_tz, as_of_utc=as_of_utc,
|
|
4749
|
+
display_tz=args._resolved_tz, as_of_utc=as_of_utc, mode=args.mode,
|
|
4749
4750
|
)
|
|
4750
4751
|
buckets = list(reversed(view.aggregated))
|
|
4751
4752
|
overlay = list(reversed(view.overlay))
|
|
@@ -5973,6 +5974,7 @@ def cmd_session(args: argparse.Namespace) -> int:
|
|
|
5973
5974
|
# files collapses to ONE entry in BOTH tuples).
|
|
5974
5975
|
view = build_sessions_view(
|
|
5975
5976
|
entries, now_utc=_command_as_of(), limit=None, display_tz=tz,
|
|
5977
|
+
mode=args.mode,
|
|
5976
5978
|
)
|
|
5977
5979
|
sessions = list(view.aggregated)
|
|
5978
5980
|
|
|
@@ -9783,6 +9785,36 @@ def _argparse_has_arg(parser, option_string: str) -> bool:
|
|
|
9783
9785
|
return False
|
|
9784
9786
|
|
|
9785
9787
|
|
|
9788
|
+
def _add_mode_arg(parser, *, noop: bool = False) -> None:
|
|
9789
|
+
"""Add ccusage's -m/--mode {auto,calculate,display} cost-source flag.
|
|
9790
|
+
|
|
9791
|
+
Standalone (not folded into _add_ccusage_alias_args) so it lands only
|
|
9792
|
+
on the six Session-C reporting commands and never collides with
|
|
9793
|
+
range-cost, which defines its own -m/--mode.
|
|
9794
|
+
|
|
9795
|
+
noop=True (five-hour-blocks only): the flag is accepted for surface
|
|
9796
|
+
parity with `blocks` but does not alter numbers — that command's cost
|
|
9797
|
+
is the authoritative materialized five_hour_blocks.total_cost_usd
|
|
9798
|
+
computed at record-time (always auto semantics).
|
|
9799
|
+
"""
|
|
9800
|
+
help_real = (
|
|
9801
|
+
"Cost source: auto (recorded costUSD when present, else computed), "
|
|
9802
|
+
"calculate (always compute from embedded pricing), display "
|
|
9803
|
+
"(recorded costUSD only; $0 when absent). Default: auto."
|
|
9804
|
+
)
|
|
9805
|
+
help_noop = (
|
|
9806
|
+
"Accepted for ccusage drop-in compat; no-op here — five-hour-blocks "
|
|
9807
|
+
"cost is the authoritative materialized per-block value computed at "
|
|
9808
|
+
"record-time. Default: auto."
|
|
9809
|
+
)
|
|
9810
|
+
parser.add_argument(
|
|
9811
|
+
"-m", "--mode",
|
|
9812
|
+
default="auto",
|
|
9813
|
+
choices=["auto", "calculate", "display"],
|
|
9814
|
+
help=help_noop if noop else help_real,
|
|
9815
|
+
)
|
|
9816
|
+
|
|
9817
|
+
|
|
9786
9818
|
def _add_ccusage_alias_args(parser, *, ansi_emit: bool) -> None:
|
|
9787
9819
|
"""Attach the Session A ccusage alias surface to a Claude-cmd subparser.
|
|
9788
9820
|
|
|
@@ -10137,6 +10169,7 @@ def _build_daily_parser(subparsers, name, *, help_text, xref):
|
|
|
10137
10169
|
"Overrides config display.tz for this call.",
|
|
10138
10170
|
)
|
|
10139
10171
|
_add_ccusage_alias_args(p, ansi_emit=False)
|
|
10172
|
+
_add_mode_arg(p)
|
|
10140
10173
|
_add_share_args(p)
|
|
10141
10174
|
p.set_defaults(func=cmd_daily)
|
|
10142
10175
|
return p
|
|
@@ -10195,6 +10228,7 @@ def _build_monthly_parser(subparsers, name, *, help_text, xref):
|
|
|
10195
10228
|
"Overrides config display.tz for this call.",
|
|
10196
10229
|
)
|
|
10197
10230
|
_add_ccusage_alias_args(p, ansi_emit=False)
|
|
10231
|
+
_add_mode_arg(p)
|
|
10198
10232
|
_add_share_args(p)
|
|
10199
10233
|
p.set_defaults(func=cmd_monthly)
|
|
10200
10234
|
return p
|
|
@@ -10235,6 +10269,7 @@ def _build_weekly_parser(subparsers, name, *, help_text, xref):
|
|
|
10235
10269
|
help="Display timezone: local, utc, or IANA name. "
|
|
10236
10270
|
"Overrides config display.tz for this call.")
|
|
10237
10271
|
_add_ccusage_alias_args(p, ansi_emit=False)
|
|
10272
|
+
_add_mode_arg(p)
|
|
10238
10273
|
_add_share_args(p)
|
|
10239
10274
|
p.set_defaults(func=cmd_weekly)
|
|
10240
10275
|
return p
|
|
@@ -10286,6 +10321,7 @@ def _build_session_parser(subparsers, name, *, help_text, xref):
|
|
|
10286
10321
|
"Unknown id → exit 0 with the empty-render branch.",
|
|
10287
10322
|
)
|
|
10288
10323
|
_add_ccusage_alias_args(p, ansi_emit=False)
|
|
10324
|
+
_add_mode_arg(p)
|
|
10289
10325
|
_add_share_args(p)
|
|
10290
10326
|
p.set_defaults(func=cmd_session)
|
|
10291
10327
|
return p
|
|
@@ -10340,6 +10376,7 @@ def _build_blocks_parser(subparsers, name, *, help_text, xref):
|
|
|
10340
10376
|
"Overrides config display.tz for this call.",
|
|
10341
10377
|
)
|
|
10342
10378
|
_add_ccusage_alias_args(p, ansi_emit=False)
|
|
10379
|
+
_add_mode_arg(p)
|
|
10343
10380
|
p.set_defaults(func=cmd_blocks)
|
|
10344
10381
|
return p
|
|
10345
10382
|
|
|
@@ -11266,6 +11303,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
11266
11303
|
# guard. The helper's --color add lands as a parsed-and-ignored
|
|
11267
11304
|
# no-op (the renderer emits plain text).
|
|
11268
11305
|
_add_ccusage_alias_args(fhb, ansi_emit=False)
|
|
11306
|
+
_add_mode_arg(fhb, noop=True)
|
|
11269
11307
|
_add_share_args(fhb)
|
|
11270
11308
|
fhb.set_defaults(func=cmd_five_hour_blocks)
|
|
11271
11309
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cctally",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.0",
|
|
4
4
|
"description": "Claude Code usage tracker and local dashboard for Pro/Max subscription limits - weekly cost-per-percent trend, quota forecasts, threshold alerts. ccusage-compatible.",
|
|
5
5
|
"homepage": "https://github.com/omrikais/cctally",
|
|
6
6
|
"repository": {
|