cctally 1.14.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 CHANGED
@@ -5,6 +5,19 @@ 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
+
16
+ ## [1.15.0] - 2026-05-26
17
+
18
+ ### Added
19
+ - **`cctally claude <cmd>` and `cctally codex <cmd>` subgroup commands** let you paste ccusage's hierarchical syntax verbatim — `cctally claude {daily,monthly,weekly,session,blocks}` is a drop-in for `ccusage claude <cmd>`, and `cctally codex {daily,monthly,session,weekly}` for `ccusage codex <cmd>` (`codex weekly` is a cctally extension; upstream has none). Each subgroup leaf routes to the exact same engine as its flat form, so the table / `--json` / exit code are byte-identical — only `--help` adds a one-line alias/canonical cross-reference. The flat forms (`cctally daily`, `cctally codex-daily`, …) remain fully supported as back-compat aliases, with no deprecation warning. (#86)
20
+
8
21
  ## [1.14.0] - 2026-05-26
9
22
 
10
23
  ### Added
package/README.md CHANGED
@@ -113,7 +113,7 @@ For status-line integration, alerts, and configuration, see [docs/installation.m
113
113
 
114
114
  ## What cctally adds
115
115
 
116
- `cctally` started as a local-first replacement for [`ccusage`](https://github.com/ryoppippi/ccusage), and it stays compatible at the level of common CLI flows (`daily`, `monthly`, `weekly`, `session`, `blocks`). Beyond that, it adds:
116
+ `cctally` started as a local-first replacement for [`ccusage`](https://github.com/ryoppippi/ccusage), and it stays compatible at the level of common CLI flows (`daily`, `monthly`, `weekly`, `session`, `blocks`). Paste from ccusage verbatim: `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. Beyond that, it adds:
117
117
 
118
118
  - **Live web dashboard.** Nine-panel SSE-driven view at `localhost:8789` (current week, forecast, trend, sessions, weekly, monthly, blocks, daily, recent alerts), with per-panel modals, a mobile layout, threshold alerts, and a settings drawer.
119
119
  - **TUI live mode.** The same data inside your terminal (`cctally tui`; requires the optional `rich` package).
@@ -123,7 +123,7 @@ For status-line integration, alerts, and configuration, see [docs/installation.m
123
123
  - **5-hour block analytics.** Per-block usage with model and project breakdowns (`cctally five-hour-blocks --breakdown=model`).
124
124
  - **Time-window diff.** Compare two windows with model and project decomposition (`cctally diff`).
125
125
  - **Project rollup.** Usage by Git project (`cctally project`).
126
- - **Codex parity.** Drop-in replacements for `ccusage-codex daily / monthly / session`, plus an added `cctally codex-weekly` rollup (upstream has no `codex weekly`).
126
+ - **Codex parity.** `cctally codex daily / monthly / session` are drop-ins for `ccusage codex daily / monthly / session`; the flat `codex-*` forms (drop-ins for the standalone `ccusage-codex` binary) remain as aliases, plus an added `cctally codex weekly` / `cctally codex-weekly` rollup (upstream has no `codex weekly`).
127
127
  - **Persistent SQLite.** Week-over-week comparisons survive across runs.
128
128
 
129
129
  **On speed.** Pricing is embedded and computed at query time from a delta-tail SQLite cache (`~/.local/share/cctally/cache.db`), with no shell-outs. First-table latency on 30 days of session data: **~2.6s (cctally) vs ~31s (ccusage)**, about 12× faster. Measured by `bench/cctally-vs-ccusage.sh` on macOS arm64, 2026-05-05; your numbers will vary.[^bench]
@@ -2371,6 +2371,10 @@ def _recompute_banner_should_emit(
2371
2371
  confuses scripted pipelines. Banner still lands on the
2372
2372
  next interactive non-report command (``report``,
2373
2373
  ``weekly``, ``percent-breakdown``, etc.) once on upgrade.
2374
+ Subgroup forms (``cctally claude/codex <cmd>``, issue #86
2375
+ Session B) carry the source group in ``argv[1]`` and the
2376
+ leaf in ``argv[2]``; we resolve the leaf so suppression is
2377
+ byte-identical to the flat alias.
2374
2378
 
2375
2379
  Returns True iff the banner should be printed. Defensive: any
2376
2380
  error reading ``sys.argv`` falls back to "don't print" — silence
@@ -2388,6 +2392,14 @@ def _recompute_banner_should_emit(
2388
2392
  return False
2389
2393
  try:
2390
2394
  argv1 = sys.argv[1] if len(sys.argv) > 1 else None
2395
+ # Subgroup forms (`cctally claude <cmd>` / `cctally codex <cmd>`) carry
2396
+ # the source group in argv[1]; the suppression key is the leaf command
2397
+ # in argv[2]. Resolve it so the recompute banner suppresses identically
2398
+ # to the flat alias (issue #86 Session B; matches the args.command leaf
2399
+ # resolution used by the error-sentinel banner). Purely additive — flat
2400
+ # invocations (argv1 not in {claude,codex}) are byte-identical to before.
2401
+ if argv1 in ("claude", "codex") and len(sys.argv) > 2:
2402
+ argv1 = sys.argv[2]
2391
2403
  except Exception:
2392
2404
  argv1 = None
2393
2405
  if argv1 in _BANNER_SUPPRESSED_COMMANDS:
@@ -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
@@ -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="auto", tz=display_tz)
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="auto", tz=display_tz)
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="auto",
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`` (mode="auto" matches
4363
- # ``_group_entries_into_blocks``'s default) so every total stays in
4364
- # one code path no field-by-field assignment that could drift if
4365
- # the dataclass grows new fields.
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
- "auto",
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
 
@@ -9887,6 +9919,70 @@ def _add_ccusage_alias_args(parser, *, ansi_emit: bool) -> None:
9887
9919
  )
9888
9920
 
9889
9921
 
9922
+ def _add_codex_shared_args(parser: argparse.ArgumentParser) -> None:
9923
+ """Register upstream `ccusage-codex sharedArgs` on a codex subparser.
9924
+
9925
+ Upstream sharedArgs (node_modules/@ccusage/codex/dist/index.js):
9926
+ --timezone/-z, --locale/-l, --compact, --color, --noColor,
9927
+ --offline/--no-offline.
9928
+
9929
+ Honored here: --timezone (dates + aggregation buckets) and
9930
+ --compact (table layout). Accepted-but-no-op (stored on the
9931
+ namespace for drop-in parity with upstream scripts): --locale
9932
+ (we don't locale-format dates), --color / --noColor (we don't
9933
+ emit ANSI codes today). --offline is accepted as a no-op too
9934
+ (we are always offline); it uses BooleanOptionalAction so
9935
+ `--no-offline` also parses cleanly. `-O` is kept as the short
9936
+ form for offline for backward compat with earlier builds.
9937
+ """
9938
+ parser.add_argument(
9939
+ "-z", "--timezone", default=None, metavar="TZ",
9940
+ help="IANA timezone for date bucketing and Date/Last Activity cells.",
9941
+ )
9942
+ parser.add_argument(
9943
+ "-l", "--locale", default=None, metavar="LOCALE",
9944
+ help="Accepted for drop-in compat; no-op (dates are not locale-formatted).",
9945
+ )
9946
+ parser.add_argument(
9947
+ "--compact", action="store_true",
9948
+ help="Force compact table layout regardless of terminal width.",
9949
+ )
9950
+ parser.add_argument(
9951
+ "--color", action="store_true",
9952
+ help="Accepted for drop-in compat; no-op today (no ANSI escapes are emitted).",
9953
+ )
9954
+ parser.add_argument(
9955
+ "--noColor", action="store_true", dest="no_color",
9956
+ help="Accepted for drop-in compat; no-op today (no ANSI escapes are emitted).",
9957
+ )
9958
+ parser.add_argument(
9959
+ "-O", "--offline", action=argparse.BooleanOptionalAction, default=False,
9960
+ help="Accepted for drop-in compat with ccusage-codex; we are always offline.",
9961
+ )
9962
+ parser.add_argument(
9963
+ "--tz", default=None, type=_argparse_tz, metavar="TZ",
9964
+ help="Display timezone: local, utc, or IANA name. Overrides "
9965
+ "config display.tz for this call. Takes precedence over "
9966
+ "upstream's --timezone for drop-in parity.",
9967
+ )
9968
+ # Issue #92: codex parity for the #89 --debug surface. Codex JSONL
9969
+ # has no recorded costUSD to diff against, so the report is the
9970
+ # codex variant ("Codex Pricing Debug Report": totals + top-N
9971
+ # highest computed-cost entries), wired via
9972
+ # _emit_codex_debug_samples_if_set in each cmd_codex_* body.
9973
+ parser.add_argument(
9974
+ "-d", "--debug", action="store_true",
9975
+ help="Emit a stderr 'Codex Pricing Debug Report' (totals + "
9976
+ "the N highest computed-cost sample entries).",
9977
+ )
9978
+ parser.add_argument(
9979
+ "--debug-samples", type=_nonneg_int, default=5, metavar="N",
9980
+ help="Cap on top-entry sample rows in the --debug report "
9981
+ "(default 5; N=0 suppresses the sample block; "
9982
+ "negatives rejected at parse time).",
9983
+ )
9984
+
9985
+
9890
9986
  def _add_share_args(parser, *, has_status_line: bool = False) -> None:
9891
9987
  """Attach shareable-reports flags + format/json mutex to a subparser.
9892
9988
 
@@ -10015,6 +10111,399 @@ def _share_validate_args(args) -> None:
10015
10111
  sys.exit(2)
10016
10112
 
10017
10113
 
10114
+ def _build_daily_parser(subparsers, name, *, help_text, xref):
10115
+ """Build the `daily` leaf parser (issue #86 Session B; routes to cmd_daily).
10116
+
10117
+ Build-once, register-twice: this body is the verbatim former inline `daily`
10118
+ construction, parameterized only by `name`, the parent-list `help_text`, and
10119
+ the `xref` appended to `description` (renders on `cctally <name> --help`).
10120
+ """
10121
+ p = subparsers.add_parser(
10122
+ name,
10123
+ help=help_text,
10124
+ formatter_class=CLIHelpFormatter,
10125
+ description="Show usage grouped by date, matching upstream ccusage daily output."
10126
+ "\n\n" + xref,
10127
+ epilog=textwrap.dedent("""\
10128
+ Examples:
10129
+ cctally daily --since 20260414
10130
+ cctally daily --since 20260410 --until 20260416
10131
+ cctally daily --since 20260414 --breakdown
10132
+ cctally daily --since 20260414 --json
10133
+ cctally daily --order desc
10134
+ """),
10135
+ )
10136
+ p.add_argument(
10137
+ "-s", "--since",
10138
+ default=None,
10139
+ metavar="YYYYMMDD",
10140
+ help="Filter from date (inclusive).",
10141
+ )
10142
+ p.add_argument(
10143
+ "-u", "--until",
10144
+ default=None,
10145
+ metavar="YYYYMMDD",
10146
+ help="Filter until date (inclusive).",
10147
+ )
10148
+ p.add_argument(
10149
+ "-b", "--breakdown",
10150
+ action="store_true",
10151
+ help="Show per-model cost breakdown sub-rows.",
10152
+ )
10153
+ p.add_argument(
10154
+ "-o", "--order",
10155
+ choices=("asc", "desc"),
10156
+ default="asc",
10157
+ help="Sort direction by date (default: asc).",
10158
+ )
10159
+ p.add_argument(
10160
+ "--reveal-projects",
10161
+ action="store_true",
10162
+ dest="reveal_projects",
10163
+ help="In --format output, show real project basenames instead of "
10164
+ "the default project-1, project-2, ... anonymization.",
10165
+ )
10166
+ p.add_argument(
10167
+ "--tz", default=None, type=_argparse_tz, metavar="TZ",
10168
+ help="Display timezone: local, utc, or IANA name. "
10169
+ "Overrides config display.tz for this call.",
10170
+ )
10171
+ _add_ccusage_alias_args(p, ansi_emit=False)
10172
+ _add_mode_arg(p)
10173
+ _add_share_args(p)
10174
+ p.set_defaults(func=cmd_daily)
10175
+ return p
10176
+
10177
+
10178
+ def _build_monthly_parser(subparsers, name, *, help_text, xref):
10179
+ """Build the `monthly` leaf parser (issue #86 Session B; routes to cmd_monthly)."""
10180
+ p = subparsers.add_parser(
10181
+ name,
10182
+ help=help_text,
10183
+ formatter_class=CLIHelpFormatter,
10184
+ description="Show usage grouped by calendar month, matching upstream ccusage monthly output."
10185
+ "\n\n" + xref,
10186
+ epilog=textwrap.dedent("""\
10187
+ Examples:
10188
+ cctally monthly --since 20260101
10189
+ cctally monthly --since 20260101 --until 20260331
10190
+ cctally monthly --since 20260101 --breakdown
10191
+ cctally monthly --since 20260101 --json
10192
+ cctally monthly --order desc
10193
+ """),
10194
+ )
10195
+ p.add_argument(
10196
+ "-s", "--since",
10197
+ default=None,
10198
+ metavar="YYYYMMDD",
10199
+ help="Filter from date (inclusive).",
10200
+ )
10201
+ p.add_argument(
10202
+ "-u", "--until",
10203
+ default=None,
10204
+ metavar="YYYYMMDD",
10205
+ help="Filter until date (inclusive).",
10206
+ )
10207
+ p.add_argument(
10208
+ "-b", "--breakdown",
10209
+ action="store_true",
10210
+ help="Show per-model cost breakdown sub-rows.",
10211
+ )
10212
+ p.add_argument(
10213
+ "-o", "--order",
10214
+ choices=("asc", "desc"),
10215
+ default="asc",
10216
+ help="Sort direction by month (default: asc).",
10217
+ )
10218
+ p.add_argument(
10219
+ "--reveal-projects",
10220
+ action="store_true",
10221
+ dest="reveal_projects",
10222
+ help="In --format output, show real project basenames instead of "
10223
+ "the default project-1, project-2, ... anonymization.",
10224
+ )
10225
+ p.add_argument(
10226
+ "--tz", default=None, type=_argparse_tz, metavar="TZ",
10227
+ help="Display timezone: local, utc, or IANA name. "
10228
+ "Overrides config display.tz for this call.",
10229
+ )
10230
+ _add_ccusage_alias_args(p, ansi_emit=False)
10231
+ _add_mode_arg(p)
10232
+ _add_share_args(p)
10233
+ p.set_defaults(func=cmd_monthly)
10234
+ return p
10235
+
10236
+
10237
+ def _build_weekly_parser(subparsers, name, *, help_text, xref):
10238
+ """Build the `weekly` leaf parser (issue #86 Session B; routes to cmd_weekly)."""
10239
+ p = subparsers.add_parser(
10240
+ name,
10241
+ help=help_text,
10242
+ formatter_class=CLIHelpFormatter,
10243
+ description="Show Claude usage grouped by subscription week. Boundaries are anchored "
10244
+ "to weekly_usage_snapshots.week_start_at with 7-day-cadence extrapolation "
10245
+ "for pre-snapshot history. Columns extend daily/monthly's set with Used % "
10246
+ "and $/1%."
10247
+ "\n\n" + xref,
10248
+ epilog=textwrap.dedent("""\
10249
+ Examples:
10250
+ cctally weekly
10251
+ cctally weekly --since 20260101
10252
+ cctally weekly --breakdown
10253
+ cctally weekly --json
10254
+ cctally weekly --order desc
10255
+ """),
10256
+ )
10257
+ p.add_argument("-s", "--since", default=None, metavar="YYYYMMDD",
10258
+ help="Filter from date (inclusive).")
10259
+ p.add_argument("-u", "--until", default=None, metavar="YYYYMMDD",
10260
+ help="Filter until date (inclusive).")
10261
+ p.add_argument("-b", "--breakdown", action="store_true",
10262
+ help="Show per-model cost breakdown sub-rows.")
10263
+ p.add_argument("-o", "--order", choices=("asc", "desc"), default="asc",
10264
+ help="Sort direction by week (default: asc).")
10265
+ p.add_argument("--reveal-projects", action="store_true", dest="reveal_projects",
10266
+ help="In --format output, show real project basenames instead of "
10267
+ "the default project-1, project-2, ... anonymization.")
10268
+ p.add_argument("--tz", default=None, type=_argparse_tz, metavar="TZ",
10269
+ help="Display timezone: local, utc, or IANA name. "
10270
+ "Overrides config display.tz for this call.")
10271
+ _add_ccusage_alias_args(p, ansi_emit=False)
10272
+ _add_mode_arg(p)
10273
+ _add_share_args(p)
10274
+ p.set_defaults(func=cmd_weekly)
10275
+ return p
10276
+
10277
+
10278
+ def _build_session_parser(subparsers, name, *, help_text, xref):
10279
+ """Build the `session` leaf parser (issue #86 Session B; routes to cmd_session)."""
10280
+ p = subparsers.add_parser(
10281
+ name,
10282
+ help=help_text,
10283
+ formatter_class=CLIHelpFormatter,
10284
+ description="Show Claude usage grouped by JSONL sessionId. Resumed sessions (same "
10285
+ "sessionId across multiple files) collapse into one row. 11-column "
10286
+ "layout paralleling codex-session."
10287
+ "\n\n" + xref,
10288
+ epilog=textwrap.dedent("""\
10289
+ Examples:
10290
+ cctally session
10291
+ cctally session --since 20260401
10292
+ cctally session --since 20260401 --breakdown
10293
+ cctally session --json
10294
+ cctally session --order desc
10295
+ """),
10296
+ )
10297
+ p.add_argument("-s", "--since", default=None, metavar="YYYYMMDD",
10298
+ help="Filter from date (inclusive).")
10299
+ p.add_argument("-u", "--until", default=None, metavar="YYYYMMDD",
10300
+ help="Filter until date (inclusive).")
10301
+ p.add_argument("-b", "--breakdown", action="store_true",
10302
+ help="Show per-model cost breakdown sub-rows.")
10303
+ p.add_argument("-o", "--order", choices=("asc", "desc"), default="asc",
10304
+ help="Sort direction by last activity (default: asc — earliest first).")
10305
+ p.add_argument("--reveal-projects", action="store_true", dest="reveal_projects",
10306
+ help="In --format output, show real project basenames instead of "
10307
+ "the default project-1, project-2, ... anonymization.")
10308
+ p.add_argument("--top-n", type=int, default=15, dest="top_n",
10309
+ metavar="N",
10310
+ help="In --format output, cap rows to top N by cost (default: 15). "
10311
+ "Must be >= 1; values above 50 emit a readability warning. "
10312
+ "Has no effect on terminal/JSON output.")
10313
+ p.add_argument("--tz", default=None, type=_argparse_tz, metavar="TZ",
10314
+ help="Display timezone: local, utc, or IANA name. "
10315
+ "Overrides config display.tz for this call.")
10316
+ p.add_argument(
10317
+ "-i", "--id", default=None, metavar="SESSION_ID", dest="id",
10318
+ help="Filter to a single session by exact-string sessionId. "
10319
+ "Match is against the post-resume-merge id (sessions "
10320
+ "resumed across multiple JSONL files collapse to one id). "
10321
+ "Unknown id → exit 0 with the empty-render branch.",
10322
+ )
10323
+ _add_ccusage_alias_args(p, ansi_emit=False)
10324
+ _add_mode_arg(p)
10325
+ _add_share_args(p)
10326
+ p.set_defaults(func=cmd_session)
10327
+ return p
10328
+
10329
+
10330
+ def _build_blocks_parser(subparsers, name, *, help_text, xref):
10331
+ """Build the `blocks` leaf parser (issue #86 Session B; routes to cmd_blocks).
10332
+
10333
+ Note: `blocks` intentionally has NO `_add_share_args` (matches the former
10334
+ inline block — it is not part of the shareable-output flag surface).
10335
+ """
10336
+ p = subparsers.add_parser(
10337
+ name,
10338
+ help=help_text,
10339
+ formatter_class=CLIHelpFormatter,
10340
+ description="Show usage grouped by 5-hour session blocks, matching upstream ccusage blocks output."
10341
+ "\n\n" + xref,
10342
+ epilog=textwrap.dedent("""\
10343
+ Examples:
10344
+ cctally blocks --since 20260414
10345
+ cctally blocks --since 20260410 --until 20260416
10346
+ cctally blocks --since 20260414 --breakdown
10347
+ cctally blocks --since 20260414 --json
10348
+ """),
10349
+ )
10350
+ p.add_argument(
10351
+ "-s", "--since",
10352
+ default=None,
10353
+ metavar="YYYYMMDD",
10354
+ help="Filter from date (inclusive).",
10355
+ )
10356
+ p.add_argument(
10357
+ "-u", "--until",
10358
+ default=None,
10359
+ metavar="YYYYMMDD",
10360
+ help="Filter until date (inclusive).",
10361
+ )
10362
+ p.add_argument(
10363
+ "-b", "--breakdown",
10364
+ action="store_true",
10365
+ help="Show per-model cost breakdown.",
10366
+ )
10367
+ p.add_argument(
10368
+ "--json",
10369
+ action="store_true",
10370
+ dest="json",
10371
+ help="Output JSON matching upstream ccusage blocks format.",
10372
+ )
10373
+ p.add_argument(
10374
+ "--tz", default=None, type=_argparse_tz, metavar="TZ",
10375
+ help="Display timezone: local, utc, or IANA name. "
10376
+ "Overrides config display.tz for this call.",
10377
+ )
10378
+ _add_ccusage_alias_args(p, ansi_emit=False)
10379
+ _add_mode_arg(p)
10380
+ p.set_defaults(func=cmd_blocks)
10381
+ return p
10382
+
10383
+
10384
+ def _build_codex_daily_parser(subparsers, name, *, help_text, xref):
10385
+ """Build the `codex-daily` leaf parser (issue #86 Session B; routes to cmd_codex_daily)."""
10386
+ p = subparsers.add_parser(
10387
+ name,
10388
+ help=help_text,
10389
+ formatter_class=CLIHelpFormatter,
10390
+ description="Show Codex usage grouped by date, matching upstream ccusage-codex daily output."
10391
+ "\n\n" + xref,
10392
+ epilog=textwrap.dedent("""\
10393
+ Examples:
10394
+ cctally codex-daily --since 20260401
10395
+ cctally codex-daily --since 20260401 --breakdown
10396
+ cctally codex-daily --since 20260401 --json
10397
+ cctally codex-daily --order desc
10398
+ """),
10399
+ )
10400
+ p.add_argument("-s", "--since", default=None, metavar="YYYY-MM-DD",
10401
+ help="Filter from date (inclusive; accepts YYYY-MM-DD or YYYYMMDD).")
10402
+ p.add_argument("-u", "--until", default=None, metavar="YYYY-MM-DD",
10403
+ help="Filter until date (inclusive; accepts YYYY-MM-DD or YYYYMMDD).")
10404
+ p.add_argument("-b", "--breakdown", action="store_true",
10405
+ help="Show per-model cost breakdown sub-rows.")
10406
+ p.add_argument("-o", "--order", choices=("asc", "desc"), default="asc",
10407
+ help="Sort direction by date (default: asc).")
10408
+ p.add_argument("--json", action="store_true", dest="json",
10409
+ help="Output JSON matching upstream ccusage-codex daily format.")
10410
+ _add_codex_shared_args(p)
10411
+ p.set_defaults(func=cmd_codex_daily)
10412
+ return p
10413
+
10414
+
10415
+ def _build_codex_monthly_parser(subparsers, name, *, help_text, xref):
10416
+ """Build the `codex-monthly` leaf parser (issue #86 Session B; routes to cmd_codex_monthly)."""
10417
+ p = subparsers.add_parser(
10418
+ name,
10419
+ help=help_text,
10420
+ formatter_class=CLIHelpFormatter,
10421
+ description="Show Codex usage grouped by calendar month, matching upstream ccusage-codex monthly output."
10422
+ "\n\n" + xref,
10423
+ epilog=textwrap.dedent("""\
10424
+ Examples:
10425
+ cctally codex-monthly --since 20260101
10426
+ cctally codex-monthly --breakdown
10427
+ cctally codex-monthly --json
10428
+ """),
10429
+ )
10430
+ p.add_argument("-s", "--since", default=None, metavar="YYYY-MM-DD",
10431
+ help="Filter from date (inclusive; accepts YYYY-MM-DD or YYYYMMDD).")
10432
+ p.add_argument("-u", "--until", default=None, metavar="YYYY-MM-DD",
10433
+ help="Filter until date (inclusive; accepts YYYY-MM-DD or YYYYMMDD).")
10434
+ p.add_argument("-b", "--breakdown", action="store_true",
10435
+ help="Show per-model cost breakdown sub-rows.")
10436
+ p.add_argument("-o", "--order", choices=("asc", "desc"), default="asc",
10437
+ help="Sort direction by month (default: asc).")
10438
+ p.add_argument("--json", action="store_true", dest="json",
10439
+ help="Output JSON matching upstream ccusage-codex monthly format.")
10440
+ _add_codex_shared_args(p)
10441
+ p.set_defaults(func=cmd_codex_monthly)
10442
+ return p
10443
+
10444
+
10445
+ def _build_codex_weekly_parser(subparsers, name, *, help_text, xref):
10446
+ """Build the `codex-weekly` leaf parser (issue #86 Session B; routes to cmd_codex_weekly)."""
10447
+ p = subparsers.add_parser(
10448
+ name,
10449
+ help=help_text,
10450
+ formatter_class=CLIHelpFormatter,
10451
+ description="Show Codex usage grouped by week. Week-start day is read from config.json "
10452
+ "(collector.week_start, Monday default). Not a ccusage-codex drop-in — "
10453
+ "upstream has no `codex weekly` command."
10454
+ "\n\n" + xref,
10455
+ epilog=textwrap.dedent("""\
10456
+ Examples:
10457
+ cctally codex-weekly
10458
+ cctally codex-weekly --since 20260301
10459
+ cctally codex-weekly --breakdown
10460
+ cctally codex-weekly --json
10461
+ cctally codex-weekly --order desc
10462
+ """),
10463
+ )
10464
+ p.add_argument("-s", "--since", default=None, metavar="YYYY-MM-DD",
10465
+ help="Filter from date (inclusive; accepts YYYY-MM-DD or YYYYMMDD).")
10466
+ p.add_argument("-u", "--until", default=None, metavar="YYYY-MM-DD",
10467
+ help="Filter until date (inclusive; accepts YYYY-MM-DD or YYYYMMDD).")
10468
+ p.add_argument("-b", "--breakdown", action="store_true",
10469
+ help="Show per-model cost breakdown sub-rows.")
10470
+ p.add_argument("-o", "--order", choices=("asc", "desc"), default="asc",
10471
+ help="Sort direction by week (default: asc).")
10472
+ p.add_argument("--json", action="store_true", dest="json",
10473
+ help="Output JSON.")
10474
+ _add_codex_shared_args(p)
10475
+ p.set_defaults(func=cmd_codex_weekly)
10476
+ return p
10477
+
10478
+
10479
+ def _build_codex_session_parser(subparsers, name, *, help_text, xref):
10480
+ """Build the `codex-session` leaf parser (issue #86 Session B; routes to cmd_codex_session)."""
10481
+ p = subparsers.add_parser(
10482
+ name,
10483
+ help=help_text,
10484
+ formatter_class=CLIHelpFormatter,
10485
+ description="Show Codex usage grouped by session, matching upstream ccusage-codex session output."
10486
+ "\n\n" + xref,
10487
+ epilog=textwrap.dedent("""\
10488
+ Examples:
10489
+ cctally codex-session
10490
+ cctally codex-session --since 20260401
10491
+ cctally codex-session --json
10492
+ """),
10493
+ )
10494
+ p.add_argument("-s", "--since", default=None, metavar="YYYY-MM-DD",
10495
+ help="Filter from date (inclusive; accepts YYYY-MM-DD or YYYYMMDD).")
10496
+ p.add_argument("-u", "--until", default=None, metavar="YYYY-MM-DD",
10497
+ help="Filter until date (inclusive; accepts YYYY-MM-DD or YYYYMMDD).")
10498
+ p.add_argument("-o", "--order", choices=("asc", "desc"), default="asc",
10499
+ help="Sort direction by last activity (default: asc — earliest first).")
10500
+ p.add_argument("--json", action="store_true", dest="json",
10501
+ help="Output JSON matching upstream ccusage-codex session format.")
10502
+ _add_codex_shared_args(p)
10503
+ p.set_defaults(func=cmd_codex_session)
10504
+ return p
10505
+
10506
+
10018
10507
  def build_parser() -> argparse.ArgumentParser:
10019
10508
  p = argparse.ArgumentParser(
10020
10509
  prog="cctally",
@@ -10747,49 +11236,10 @@ def build_parser() -> argparse.ArgumentParser:
10747
11236
  rc.set_defaults(func=cmd_range_cost)
10748
11237
 
10749
11238
  # -- blocks --
10750
- bl = sub.add_parser(
10751
- "blocks",
10752
- help="Show usage report grouped by 5-hour session blocks",
10753
- formatter_class=CLIHelpFormatter,
10754
- description="Show usage grouped by 5-hour session blocks, matching upstream ccusage blocks output.",
10755
- epilog=textwrap.dedent("""\
10756
- Examples:
10757
- cctally blocks --since 20260414
10758
- cctally blocks --since 20260410 --until 20260416
10759
- cctally blocks --since 20260414 --breakdown
10760
- cctally blocks --since 20260414 --json
10761
- """),
10762
- )
10763
- bl.add_argument(
10764
- "-s", "--since",
10765
- default=None,
10766
- metavar="YYYYMMDD",
10767
- help="Filter from date (inclusive).",
10768
- )
10769
- bl.add_argument(
10770
- "-u", "--until",
10771
- default=None,
10772
- metavar="YYYYMMDD",
10773
- help="Filter until date (inclusive).",
10774
- )
10775
- bl.add_argument(
10776
- "-b", "--breakdown",
10777
- action="store_true",
10778
- help="Show per-model cost breakdown.",
10779
- )
10780
- bl.add_argument(
10781
- "--json",
10782
- action="store_true",
10783
- dest="json",
10784
- help="Output JSON matching upstream ccusage blocks format.",
10785
- )
10786
- bl.add_argument(
10787
- "--tz", default=None, type=_argparse_tz, metavar="TZ",
10788
- help="Display timezone: local, utc, or IANA name. "
10789
- "Overrides config display.tz for this call.",
10790
- )
10791
- _add_ccusage_alias_args(bl, ansi_emit=False)
10792
- bl.set_defaults(func=cmd_blocks)
11239
+ _build_blocks_parser(
11240
+ sub, "blocks",
11241
+ help_text="Show usage report grouped by 5-hour session blocks",
11242
+ xref="Alias of `cctally claude blocks` (the canonical form).")
10793
11243
 
10794
11244
  # -- five-hour-blocks --
10795
11245
  fhb = sub.add_parser(
@@ -10853,6 +11303,7 @@ def build_parser() -> argparse.ArgumentParser:
10853
11303
  # guard. The helper's --color add lands as a parsed-and-ignored
10854
11304
  # no-op (the renderer emits plain text).
10855
11305
  _add_ccusage_alias_args(fhb, ansi_emit=False)
11306
+ _add_mode_arg(fhb, noop=True)
10856
11307
  _add_share_args(fhb)
10857
11308
  fhb.set_defaults(func=cmd_five_hour_blocks)
10858
11309
 
@@ -10875,319 +11326,46 @@ def build_parser() -> argparse.ArgumentParser:
10875
11326
  p_cache_sync.set_defaults(func=cmd_cache_sync)
10876
11327
 
10877
11328
  # -- daily --
10878
- dy = sub.add_parser(
10879
- "daily",
10880
- help="Show usage report grouped by date",
10881
- formatter_class=CLIHelpFormatter,
10882
- description="Show usage grouped by date, matching upstream ccusage daily output.",
10883
- epilog=textwrap.dedent("""\
10884
- Examples:
10885
- cctally daily --since 20260414
10886
- cctally daily --since 20260410 --until 20260416
10887
- cctally daily --since 20260414 --breakdown
10888
- cctally daily --since 20260414 --json
10889
- cctally daily --order desc
10890
- """),
10891
- )
10892
- dy.add_argument(
10893
- "-s", "--since",
10894
- default=None,
10895
- metavar="YYYYMMDD",
10896
- help="Filter from date (inclusive).",
10897
- )
10898
- dy.add_argument(
10899
- "-u", "--until",
10900
- default=None,
10901
- metavar="YYYYMMDD",
10902
- help="Filter until date (inclusive).",
10903
- )
10904
- dy.add_argument(
10905
- "-b", "--breakdown",
10906
- action="store_true",
10907
- help="Show per-model cost breakdown sub-rows.",
10908
- )
10909
- dy.add_argument(
10910
- "-o", "--order",
10911
- choices=("asc", "desc"),
10912
- default="asc",
10913
- help="Sort direction by date (default: asc).",
10914
- )
10915
- dy.add_argument(
10916
- "--reveal-projects",
10917
- action="store_true",
10918
- dest="reveal_projects",
10919
- help="In --format output, show real project basenames instead of "
10920
- "the default project-1, project-2, ... anonymization.",
10921
- )
10922
- dy.add_argument(
10923
- "--tz", default=None, type=_argparse_tz, metavar="TZ",
10924
- help="Display timezone: local, utc, or IANA name. "
10925
- "Overrides config display.tz for this call.",
10926
- )
10927
- _add_ccusage_alias_args(dy, ansi_emit=False)
10928
- _add_share_args(dy)
10929
- dy.set_defaults(func=cmd_daily)
11329
+ _build_daily_parser(
11330
+ sub, "daily",
11331
+ help_text="Show usage report grouped by date",
11332
+ xref="Alias of `cctally claude daily` (the canonical form).")
10930
11333
 
10931
11334
  # -- monthly --
10932
- mo = sub.add_parser(
10933
- "monthly",
10934
- help="Show usage report grouped by month",
10935
- formatter_class=CLIHelpFormatter,
10936
- description="Show usage grouped by calendar month, matching upstream ccusage monthly output.",
10937
- epilog=textwrap.dedent("""\
10938
- Examples:
10939
- cctally monthly --since 20260101
10940
- cctally monthly --since 20260101 --until 20260331
10941
- cctally monthly --since 20260101 --breakdown
10942
- cctally monthly --since 20260101 --json
10943
- cctally monthly --order desc
10944
- """),
10945
- )
10946
- mo.add_argument(
10947
- "-s", "--since",
10948
- default=None,
10949
- metavar="YYYYMMDD",
10950
- help="Filter from date (inclusive).",
10951
- )
10952
- mo.add_argument(
10953
- "-u", "--until",
10954
- default=None,
10955
- metavar="YYYYMMDD",
10956
- help="Filter until date (inclusive).",
10957
- )
10958
- mo.add_argument(
10959
- "-b", "--breakdown",
10960
- action="store_true",
10961
- help="Show per-model cost breakdown sub-rows.",
10962
- )
10963
- mo.add_argument(
10964
- "-o", "--order",
10965
- choices=("asc", "desc"),
10966
- default="asc",
10967
- help="Sort direction by month (default: asc).",
10968
- )
10969
- mo.add_argument(
10970
- "--reveal-projects",
10971
- action="store_true",
10972
- dest="reveal_projects",
10973
- help="In --format output, show real project basenames instead of "
10974
- "the default project-1, project-2, ... anonymization.",
10975
- )
10976
- mo.add_argument(
10977
- "--tz", default=None, type=_argparse_tz, metavar="TZ",
10978
- help="Display timezone: local, utc, or IANA name. "
10979
- "Overrides config display.tz for this call.",
10980
- )
10981
- _add_ccusage_alias_args(mo, ansi_emit=False)
10982
- _add_share_args(mo)
10983
- mo.set_defaults(func=cmd_monthly)
11335
+ _build_monthly_parser(
11336
+ sub, "monthly",
11337
+ help_text="Show usage report grouped by month",
11338
+ xref="Alias of `cctally claude monthly` (the canonical form).")
10984
11339
 
10985
11340
  # -- weekly --
10986
- we = sub.add_parser(
10987
- "weekly",
10988
- help="Show usage grouped by subscription week (with Used %% and $/1%%)",
10989
- formatter_class=CLIHelpFormatter,
10990
- description="Show Claude usage grouped by subscription week. Boundaries are anchored "
10991
- "to weekly_usage_snapshots.week_start_at with 7-day-cadence extrapolation "
10992
- "for pre-snapshot history. Columns extend daily/monthly's set with Used % "
10993
- "and $/1%.",
10994
- epilog=textwrap.dedent("""\
10995
- Examples:
10996
- cctally weekly
10997
- cctally weekly --since 20260101
10998
- cctally weekly --breakdown
10999
- cctally weekly --json
11000
- cctally weekly --order desc
11001
- """),
11002
- )
11003
- we.add_argument("-s", "--since", default=None, metavar="YYYYMMDD",
11004
- help="Filter from date (inclusive).")
11005
- we.add_argument("-u", "--until", default=None, metavar="YYYYMMDD",
11006
- help="Filter until date (inclusive).")
11007
- we.add_argument("-b", "--breakdown", action="store_true",
11008
- help="Show per-model cost breakdown sub-rows.")
11009
- we.add_argument("-o", "--order", choices=("asc", "desc"), default="asc",
11010
- help="Sort direction by week (default: asc).")
11011
- we.add_argument("--reveal-projects", action="store_true", dest="reveal_projects",
11012
- help="In --format output, show real project basenames instead of "
11013
- "the default project-1, project-2, ... anonymization.")
11014
- we.add_argument("--tz", default=None, type=_argparse_tz, metavar="TZ",
11015
- help="Display timezone: local, utc, or IANA name. "
11016
- "Overrides config display.tz for this call.")
11017
- _add_ccusage_alias_args(we, ansi_emit=False)
11018
- _add_share_args(we)
11019
- we.set_defaults(func=cmd_weekly)
11020
-
11021
- # -- codex shared args helper --
11022
- def _add_codex_shared_args(parser: argparse.ArgumentParser) -> None:
11023
- """Register upstream `ccusage-codex sharedArgs` on a codex subparser.
11024
-
11025
- Upstream sharedArgs (node_modules/@ccusage/codex/dist/index.js):
11026
- --timezone/-z, --locale/-l, --compact, --color, --noColor,
11027
- --offline/--no-offline.
11028
-
11029
- Honored here: --timezone (dates + aggregation buckets) and
11030
- --compact (table layout). Accepted-but-no-op (stored on the
11031
- namespace for drop-in parity with upstream scripts): --locale
11032
- (we don't locale-format dates), --color / --noColor (we don't
11033
- emit ANSI codes today). --offline is accepted as a no-op too
11034
- (we are always offline); it uses BooleanOptionalAction so
11035
- `--no-offline` also parses cleanly. `-O` is kept as the short
11036
- form for offline for backward compat with earlier builds.
11037
- """
11038
- parser.add_argument(
11039
- "-z", "--timezone", default=None, metavar="TZ",
11040
- help="IANA timezone for date bucketing and Date/Last Activity cells.",
11041
- )
11042
- parser.add_argument(
11043
- "-l", "--locale", default=None, metavar="LOCALE",
11044
- help="Accepted for drop-in compat; no-op (dates are not locale-formatted).",
11045
- )
11046
- parser.add_argument(
11047
- "--compact", action="store_true",
11048
- help="Force compact table layout regardless of terminal width.",
11049
- )
11050
- parser.add_argument(
11051
- "--color", action="store_true",
11052
- help="Accepted for drop-in compat; no-op today (no ANSI escapes are emitted).",
11053
- )
11054
- parser.add_argument(
11055
- "--noColor", action="store_true", dest="no_color",
11056
- help="Accepted for drop-in compat; no-op today (no ANSI escapes are emitted).",
11057
- )
11058
- parser.add_argument(
11059
- "-O", "--offline", action=argparse.BooleanOptionalAction, default=False,
11060
- help="Accepted for drop-in compat with ccusage-codex; we are always offline.",
11061
- )
11062
- parser.add_argument(
11063
- "--tz", default=None, type=_argparse_tz, metavar="TZ",
11064
- help="Display timezone: local, utc, or IANA name. Overrides "
11065
- "config display.tz for this call. Takes precedence over "
11066
- "upstream's --timezone for drop-in parity.",
11067
- )
11068
- # Issue #92: codex parity for the #89 --debug surface. Codex JSONL
11069
- # has no recorded costUSD to diff against, so the report is the
11070
- # codex variant ("Codex Pricing Debug Report": totals + top-N
11071
- # highest computed-cost entries), wired via
11072
- # _emit_codex_debug_samples_if_set in each cmd_codex_* body.
11073
- parser.add_argument(
11074
- "-d", "--debug", action="store_true",
11075
- help="Emit a stderr 'Codex Pricing Debug Report' (totals + "
11076
- "the N highest computed-cost sample entries).",
11077
- )
11078
- parser.add_argument(
11079
- "--debug-samples", type=_nonneg_int, default=5, metavar="N",
11080
- help="Cap on top-entry sample rows in the --debug report "
11081
- "(default 5; N=0 suppresses the sample block; "
11082
- "negatives rejected at parse time).",
11083
- )
11341
+ _build_weekly_parser(
11342
+ sub, "weekly",
11343
+ help_text="Show usage grouped by subscription week (with Used %% and $/1%%)",
11344
+ xref="Alias of `cctally claude weekly` (the canonical form).")
11084
11345
 
11085
11346
  # -- codex-daily --
11086
- cd = sub.add_parser(
11087
- "codex-daily",
11088
- help="Show Codex usage report grouped by date (drop-in for `ccusage-codex daily`)",
11089
- formatter_class=CLIHelpFormatter,
11090
- description="Show Codex usage grouped by date, matching upstream ccusage-codex daily output.",
11091
- epilog=textwrap.dedent("""\
11092
- Examples:
11093
- cctally codex-daily --since 20260401
11094
- cctally codex-daily --since 20260401 --breakdown
11095
- cctally codex-daily --since 20260401 --json
11096
- cctally codex-daily --order desc
11097
- """),
11098
- )
11099
- cd.add_argument("-s", "--since", default=None, metavar="YYYY-MM-DD",
11100
- help="Filter from date (inclusive; accepts YYYY-MM-DD or YYYYMMDD).")
11101
- cd.add_argument("-u", "--until", default=None, metavar="YYYY-MM-DD",
11102
- help="Filter until date (inclusive; accepts YYYY-MM-DD or YYYYMMDD).")
11103
- cd.add_argument("-b", "--breakdown", action="store_true",
11104
- help="Show per-model cost breakdown sub-rows.")
11105
- cd.add_argument("-o", "--order", choices=("asc", "desc"), default="asc",
11106
- help="Sort direction by date (default: asc).")
11107
- cd.add_argument("--json", action="store_true", dest="json",
11108
- help="Output JSON matching upstream ccusage-codex daily format.")
11109
- _add_codex_shared_args(cd)
11110
- cd.set_defaults(func=cmd_codex_daily)
11347
+ _build_codex_daily_parser(
11348
+ sub, "codex-daily",
11349
+ help_text="Show Codex usage report grouped by date (drop-in for `ccusage-codex daily`)",
11350
+ xref="Alias of `cctally codex daily` (the canonical form).")
11111
11351
 
11112
11352
  # -- codex-monthly --
11113
- cmn = sub.add_parser(
11114
- "codex-monthly",
11115
- help="Show Codex usage grouped by month (drop-in for `ccusage-codex monthly`)",
11116
- formatter_class=CLIHelpFormatter,
11117
- description="Show Codex usage grouped by calendar month, matching upstream ccusage-codex monthly output.",
11118
- epilog=textwrap.dedent("""\
11119
- Examples:
11120
- cctally codex-monthly --since 20260101
11121
- cctally codex-monthly --breakdown
11122
- cctally codex-monthly --json
11123
- """),
11124
- )
11125
- cmn.add_argument("-s", "--since", default=None, metavar="YYYY-MM-DD",
11126
- help="Filter from date (inclusive; accepts YYYY-MM-DD or YYYYMMDD).")
11127
- cmn.add_argument("-u", "--until", default=None, metavar="YYYY-MM-DD",
11128
- help="Filter until date (inclusive; accepts YYYY-MM-DD or YYYYMMDD).")
11129
- cmn.add_argument("-b", "--breakdown", action="store_true",
11130
- help="Show per-model cost breakdown sub-rows.")
11131
- cmn.add_argument("-o", "--order", choices=("asc", "desc"), default="asc",
11132
- help="Sort direction by month (default: asc).")
11133
- cmn.add_argument("--json", action="store_true", dest="json",
11134
- help="Output JSON matching upstream ccusage-codex monthly format.")
11135
- _add_codex_shared_args(cmn)
11136
- cmn.set_defaults(func=cmd_codex_monthly)
11353
+ _build_codex_monthly_parser(
11354
+ sub, "codex-monthly",
11355
+ help_text="Show Codex usage grouped by month (drop-in for `ccusage-codex monthly`)",
11356
+ xref="Alias of `cctally codex monthly` (the canonical form).")
11137
11357
 
11138
11358
  # -- codex-weekly --
11139
- cw = sub.add_parser(
11140
- "codex-weekly",
11141
- help="Show Codex usage grouped by week (week-start from config.json)",
11142
- formatter_class=CLIHelpFormatter,
11143
- description="Show Codex usage grouped by week. Week-start day is read from config.json "
11144
- "(collector.week_start, Monday default). Not a ccusage-codex drop-in — "
11145
- "upstream has no `codex weekly` command.",
11146
- epilog=textwrap.dedent("""\
11147
- Examples:
11148
- cctally codex-weekly
11149
- cctally codex-weekly --since 20260301
11150
- cctally codex-weekly --breakdown
11151
- cctally codex-weekly --json
11152
- cctally codex-weekly --order desc
11153
- """),
11154
- )
11155
- cw.add_argument("-s", "--since", default=None, metavar="YYYY-MM-DD",
11156
- help="Filter from date (inclusive; accepts YYYY-MM-DD or YYYYMMDD).")
11157
- cw.add_argument("-u", "--until", default=None, metavar="YYYY-MM-DD",
11158
- help="Filter until date (inclusive; accepts YYYY-MM-DD or YYYYMMDD).")
11159
- cw.add_argument("-b", "--breakdown", action="store_true",
11160
- help="Show per-model cost breakdown sub-rows.")
11161
- cw.add_argument("-o", "--order", choices=("asc", "desc"), default="asc",
11162
- help="Sort direction by week (default: asc).")
11163
- cw.add_argument("--json", action="store_true", dest="json",
11164
- help="Output JSON.")
11165
- _add_codex_shared_args(cw)
11166
- cw.set_defaults(func=cmd_codex_weekly)
11359
+ _build_codex_weekly_parser(
11360
+ sub, "codex-weekly",
11361
+ help_text="Show Codex usage grouped by week (week-start from config.json)",
11362
+ xref="Alias of `cctally codex weekly` (the canonical form).")
11167
11363
 
11168
11364
  # -- codex-session --
11169
- cs = sub.add_parser(
11170
- "codex-session",
11171
- help="Show Codex usage grouped by session (drop-in for `ccusage-codex session`)",
11172
- formatter_class=CLIHelpFormatter,
11173
- description="Show Codex usage grouped by session, matching upstream ccusage-codex session output.",
11174
- epilog=textwrap.dedent("""\
11175
- Examples:
11176
- cctally codex-session
11177
- cctally codex-session --since 20260401
11178
- cctally codex-session --json
11179
- """),
11180
- )
11181
- cs.add_argument("-s", "--since", default=None, metavar="YYYY-MM-DD",
11182
- help="Filter from date (inclusive; accepts YYYY-MM-DD or YYYYMMDD).")
11183
- cs.add_argument("-u", "--until", default=None, metavar="YYYY-MM-DD",
11184
- help="Filter until date (inclusive; accepts YYYY-MM-DD or YYYYMMDD).")
11185
- cs.add_argument("-o", "--order", choices=("asc", "desc"), default="asc",
11186
- help="Sort direction by last activity (default: asc — earliest first).")
11187
- cs.add_argument("--json", action="store_true", dest="json",
11188
- help="Output JSON matching upstream ccusage-codex session format.")
11189
- _add_codex_shared_args(cs)
11190
- cs.set_defaults(func=cmd_codex_session)
11365
+ _build_codex_session_parser(
11366
+ sub, "codex-session",
11367
+ help_text="Show Codex usage grouped by session (drop-in for `ccusage-codex session`)",
11368
+ xref="Alias of `cctally codex session` (the canonical form).")
11191
11369
 
11192
11370
  # -- project --
11193
11371
  p_project = sub.add_parser(
@@ -11285,51 +11463,62 @@ def build_parser() -> argparse.ArgumentParser:
11285
11463
  diff_p.set_defaults(func=cmd_diff)
11286
11464
 
11287
11465
  # -- session --
11288
- se = sub.add_parser(
11289
- "session",
11290
- help="Show Claude usage grouped by sessionId (merges resumed-across-files sessions)",
11466
+ _build_session_parser(
11467
+ sub, "session",
11468
+ help_text="Show Claude usage grouped by sessionId (merges resumed-across-files sessions)",
11469
+ xref="Alias of `cctally claude session` (the canonical form).")
11470
+
11471
+ # --- `claude` subgroup (drop-in for `ccusage claude …`); issue #86 Session B ---
11472
+ # Build-once, register-twice: these reuse the same nine builders as the flat
11473
+ # forms above. Nested subparsers reuse dest="command" so args.command resolves
11474
+ # to the leaf name (e.g. "blocks"), keeping banner suppression byte-identical
11475
+ # to the flat form with zero hook-path changes.
11476
+ claude_p = sub.add_parser(
11477
+ "claude",
11478
+ help="Claude-source reports (drop-in for `ccusage claude …`)",
11291
11479
  formatter_class=CLIHelpFormatter,
11292
- description="Show Claude usage grouped by JSONL sessionId. Resumed sessions (same "
11293
- "sessionId across multiple files) collapse into one row. 11-column "
11294
- "layout paralleling codex-session.",
11295
- epilog=textwrap.dedent("""\
11296
- Examples:
11297
- cctally session
11298
- cctally session --since 20260401
11299
- cctally session --since 20260401 --breakdown
11300
- cctally session --json
11301
- cctally session --order desc
11302
- """),
11303
- )
11304
- se.add_argument("-s", "--since", default=None, metavar="YYYYMMDD",
11305
- help="Filter from date (inclusive).")
11306
- se.add_argument("-u", "--until", default=None, metavar="YYYYMMDD",
11307
- help="Filter until date (inclusive).")
11308
- se.add_argument("-b", "--breakdown", action="store_true",
11309
- help="Show per-model cost breakdown sub-rows.")
11310
- se.add_argument("-o", "--order", choices=("asc", "desc"), default="asc",
11311
- help="Sort direction by last activity (default: asc — earliest first).")
11312
- se.add_argument("--reveal-projects", action="store_true", dest="reveal_projects",
11313
- help="In --format output, show real project basenames instead of "
11314
- "the default project-1, project-2, ... anonymization.")
11315
- se.add_argument("--top-n", type=int, default=15, dest="top_n",
11316
- metavar="N",
11317
- help="In --format output, cap rows to top N by cost (default: 15). "
11318
- "Must be >= 1; values above 50 emit a readability warning. "
11319
- "Has no effect on terminal/JSON output.")
11320
- se.add_argument("--tz", default=None, type=_argparse_tz, metavar="TZ",
11321
- help="Display timezone: local, utc, or IANA name. "
11322
- "Overrides config display.tz for this call.")
11323
- se.add_argument(
11324
- "-i", "--id", default=None, metavar="SESSION_ID", dest="id",
11325
- help="Filter to a single session by exact-string sessionId. "
11326
- "Match is against the post-resume-merge id (sessions "
11327
- "resumed across multiple JSONL files collapse to one id). "
11328
- "Unknown id exit 0 with the empty-render branch.",
11329
- )
11330
- _add_ccusage_alias_args(se, ansi_emit=False)
11331
- _add_share_args(se)
11332
- se.set_defaults(func=cmd_session)
11480
+ description="Claude-source usage reports. Each subcommand is a drop-in for the "
11481
+ "matching `ccusage claude <cmd>` and shares its engine with the "
11482
+ "top-level `cctally <cmd>` alias.")
11483
+ claude_sub = claude_p.add_subparsers(dest="command", required=True, metavar="<command>")
11484
+ _build_daily_parser(claude_sub, "daily",
11485
+ help_text="Show usage grouped by date",
11486
+ xref="Drop-in for `ccusage claude daily`. Same engine as `cctally daily`.")
11487
+ _build_monthly_parser(claude_sub, "monthly",
11488
+ help_text="Show usage grouped by month",
11489
+ xref="Drop-in for `ccusage claude monthly`. Same engine as `cctally monthly`.")
11490
+ _build_weekly_parser(claude_sub, "weekly",
11491
+ help_text="Show usage grouped by subscription week",
11492
+ xref="Drop-in for `ccusage claude weekly`. Same engine as `cctally weekly`.")
11493
+ _build_session_parser(claude_sub, "session",
11494
+ help_text="Show usage grouped by session",
11495
+ xref="Drop-in for `ccusage claude session`. Same engine as `cctally session`.")
11496
+ _build_blocks_parser(claude_sub, "blocks",
11497
+ help_text="Show usage grouped by 5-hour session blocks",
11498
+ xref="Drop-in for `ccusage claude blocks`. Same engine as `cctally blocks`.")
11499
+
11500
+ # --- `codex` subgroup (drop-in for `ccusage codex …`); issue #86 Session B ---
11501
+ codex_p = sub.add_parser(
11502
+ "codex",
11503
+ help="Codex-source reports (drop-in for `ccusage codex …`)",
11504
+ formatter_class=CLIHelpFormatter,
11505
+ description="Codex-source usage reports. daily/monthly/session are drop-ins for "
11506
+ "`ccusage codex <cmd>`; weekly is a cctally extension. Each shares its "
11507
+ "engine with the matching `cctally codex-<cmd>` alias.")
11508
+ codex_sub = codex_p.add_subparsers(dest="command", required=True, metavar="<command>")
11509
+ _build_codex_daily_parser(codex_sub, "daily",
11510
+ help_text="Show Codex usage grouped by date",
11511
+ xref="Drop-in for `ccusage codex daily`. Same engine as `cctally codex-daily`.")
11512
+ _build_codex_monthly_parser(codex_sub, "monthly",
11513
+ help_text="Show Codex usage grouped by month",
11514
+ xref="Drop-in for `ccusage codex monthly`. Same engine as `cctally codex-monthly`.")
11515
+ _build_codex_session_parser(codex_sub, "session",
11516
+ help_text="Show Codex usage grouped by session",
11517
+ xref="Drop-in for `ccusage codex session`. Same engine as `cctally codex-session`.")
11518
+ _build_codex_weekly_parser(codex_sub, "weekly",
11519
+ help_text="Show Codex usage grouped by week",
11520
+ xref="cctally extension (no upstream `ccusage codex weekly`). Same engine as "
11521
+ "`cctally codex-weekly`.")
11333
11522
 
11334
11523
  # ---- config (persisted user preferences) ----
11335
11524
  cfg_p = sub.add_parser(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cctally",
3
- "version": "1.14.0",
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": {