cctally 1.13.0 → 1.15.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/bin/cctally CHANGED
@@ -909,6 +909,17 @@ def _migrate_legacy_data_dir() -> None:
909
909
  Removable in a future major version once early users have been on
910
910
  cctally long enough that the legacy dir is gone everywhere.
911
911
  """
912
+ # Dev-instance isolation (F2): the legacy ccusage-subscription rename is a
913
+ # PROD-only concern. Skip it whenever the data dir was relocated away from
914
+ # the canonical prod path — i.e. dev-checkout auto-detect (DEV_MODE) or an
915
+ # explicit CCTALLY_DATA_DIR override — so a dev run (APP_DIR = cctally-dev)
916
+ # or a per-branch override never hijacks the one-shot move into the wrong
917
+ # dir. "not DEV_MODE and not CCTALLY_DATA_DIR" is exactly the prod-default
918
+ # resolution branch, i.e. APP_DIR == ~/.local/share/cctally. Under the test
919
+ # suppressor DEV_MODE is False and the existing migration tests (which pin
920
+ # APP_DIR directly, no override) still exercise the move.
921
+ if _cctally_core.DEV_MODE or os.environ.get("CCTALLY_DATA_DIR", "").strip():
922
+ return
912
923
  if _cctally_core.APP_DIR.exists():
913
924
  return # already migrated, or fresh install at the new path
914
925
  if not _cctally_core.LEGACY_APP_DIR.exists():
@@ -3369,7 +3380,16 @@ def _backfill_five_hour_blocks(conn: sqlite3.Connection) -> int:
3369
3380
  now_iso = now_utc_iso()
3370
3381
  now_dt = parse_iso_datetime(now_iso, "now")
3371
3382
 
3372
- conn.execute("BEGIN")
3383
+ # BEGIN IMMEDIATE (not deferred): this transaction's first DML is a
3384
+ # READ (min_row/max_row below), so a plain deferred BEGIN takes a read
3385
+ # snapshot and only tries to upgrade to the write lock at the first
3386
+ # INSERT OR IGNORE. Under concurrent first-run openers, a competing
3387
+ # commit landing between that read and the first write makes the upgrade
3388
+ # fail with SQLITE_BUSY_SNAPSHOT *immediately* — busy_timeout cannot
3389
+ # absorb it, and the whole backfill rolls back. Acquiring the write lock
3390
+ # up front serializes the backfill cleanly behind busy_timeout instead.
3391
+ # See cctally-dev#87.
3392
+ conn.execute("BEGIN IMMEDIATE")
3373
3393
  try:
3374
3394
  for key in keys:
3375
3395
  # MIN-captured row defines the immutable block boundary
@@ -9715,6 +9735,10 @@ def doctor_gather_state(
9715
9735
  effective_update_reason=effective_update_reason,
9716
9736
  now_utc=now_utc,
9717
9737
  cctally_version=cctally_version,
9738
+ # Dev-instance isolation (§4): which data dir resolved + how.
9739
+ dev_mode=_cctally_core.DEV_MODE,
9740
+ app_dir=str(_cctally_core.APP_DIR),
9741
+ is_dev_checkout=_cctally_core._is_dev_checkout(),
9718
9742
  )
9719
9743
 
9720
9744
 
@@ -9863,6 +9887,70 @@ def _add_ccusage_alias_args(parser, *, ansi_emit: bool) -> None:
9863
9887
  )
9864
9888
 
9865
9889
 
9890
+ def _add_codex_shared_args(parser: argparse.ArgumentParser) -> None:
9891
+ """Register upstream `ccusage-codex sharedArgs` on a codex subparser.
9892
+
9893
+ Upstream sharedArgs (node_modules/@ccusage/codex/dist/index.js):
9894
+ --timezone/-z, --locale/-l, --compact, --color, --noColor,
9895
+ --offline/--no-offline.
9896
+
9897
+ Honored here: --timezone (dates + aggregation buckets) and
9898
+ --compact (table layout). Accepted-but-no-op (stored on the
9899
+ namespace for drop-in parity with upstream scripts): --locale
9900
+ (we don't locale-format dates), --color / --noColor (we don't
9901
+ emit ANSI codes today). --offline is accepted as a no-op too
9902
+ (we are always offline); it uses BooleanOptionalAction so
9903
+ `--no-offline` also parses cleanly. `-O` is kept as the short
9904
+ form for offline for backward compat with earlier builds.
9905
+ """
9906
+ parser.add_argument(
9907
+ "-z", "--timezone", default=None, metavar="TZ",
9908
+ help="IANA timezone for date bucketing and Date/Last Activity cells.",
9909
+ )
9910
+ parser.add_argument(
9911
+ "-l", "--locale", default=None, metavar="LOCALE",
9912
+ help="Accepted for drop-in compat; no-op (dates are not locale-formatted).",
9913
+ )
9914
+ parser.add_argument(
9915
+ "--compact", action="store_true",
9916
+ help="Force compact table layout regardless of terminal width.",
9917
+ )
9918
+ parser.add_argument(
9919
+ "--color", action="store_true",
9920
+ help="Accepted for drop-in compat; no-op today (no ANSI escapes are emitted).",
9921
+ )
9922
+ parser.add_argument(
9923
+ "--noColor", action="store_true", dest="no_color",
9924
+ help="Accepted for drop-in compat; no-op today (no ANSI escapes are emitted).",
9925
+ )
9926
+ parser.add_argument(
9927
+ "-O", "--offline", action=argparse.BooleanOptionalAction, default=False,
9928
+ help="Accepted for drop-in compat with ccusage-codex; we are always offline.",
9929
+ )
9930
+ parser.add_argument(
9931
+ "--tz", default=None, type=_argparse_tz, metavar="TZ",
9932
+ help="Display timezone: local, utc, or IANA name. Overrides "
9933
+ "config display.tz for this call. Takes precedence over "
9934
+ "upstream's --timezone for drop-in parity.",
9935
+ )
9936
+ # Issue #92: codex parity for the #89 --debug surface. Codex JSONL
9937
+ # has no recorded costUSD to diff against, so the report is the
9938
+ # codex variant ("Codex Pricing Debug Report": totals + top-N
9939
+ # highest computed-cost entries), wired via
9940
+ # _emit_codex_debug_samples_if_set in each cmd_codex_* body.
9941
+ parser.add_argument(
9942
+ "-d", "--debug", action="store_true",
9943
+ help="Emit a stderr 'Codex Pricing Debug Report' (totals + "
9944
+ "the N highest computed-cost sample entries).",
9945
+ )
9946
+ parser.add_argument(
9947
+ "--debug-samples", type=_nonneg_int, default=5, metavar="N",
9948
+ help="Cap on top-entry sample rows in the --debug report "
9949
+ "(default 5; N=0 suppresses the sample block; "
9950
+ "negatives rejected at parse time).",
9951
+ )
9952
+
9953
+
9866
9954
  def _add_share_args(parser, *, has_status_line: bool = False) -> None:
9867
9955
  """Attach shareable-reports flags + format/json mutex to a subparser.
9868
9956
 
@@ -9991,6 +10079,394 @@ def _share_validate_args(args) -> None:
9991
10079
  sys.exit(2)
9992
10080
 
9993
10081
 
10082
+ def _build_daily_parser(subparsers, name, *, help_text, xref):
10083
+ """Build the `daily` leaf parser (issue #86 Session B; routes to cmd_daily).
10084
+
10085
+ Build-once, register-twice: this body is the verbatim former inline `daily`
10086
+ construction, parameterized only by `name`, the parent-list `help_text`, and
10087
+ the `xref` appended to `description` (renders on `cctally <name> --help`).
10088
+ """
10089
+ p = subparsers.add_parser(
10090
+ name,
10091
+ help=help_text,
10092
+ formatter_class=CLIHelpFormatter,
10093
+ description="Show usage grouped by date, matching upstream ccusage daily output."
10094
+ "\n\n" + xref,
10095
+ epilog=textwrap.dedent("""\
10096
+ Examples:
10097
+ cctally daily --since 20260414
10098
+ cctally daily --since 20260410 --until 20260416
10099
+ cctally daily --since 20260414 --breakdown
10100
+ cctally daily --since 20260414 --json
10101
+ cctally daily --order desc
10102
+ """),
10103
+ )
10104
+ p.add_argument(
10105
+ "-s", "--since",
10106
+ default=None,
10107
+ metavar="YYYYMMDD",
10108
+ help="Filter from date (inclusive).",
10109
+ )
10110
+ p.add_argument(
10111
+ "-u", "--until",
10112
+ default=None,
10113
+ metavar="YYYYMMDD",
10114
+ help="Filter until date (inclusive).",
10115
+ )
10116
+ p.add_argument(
10117
+ "-b", "--breakdown",
10118
+ action="store_true",
10119
+ help="Show per-model cost breakdown sub-rows.",
10120
+ )
10121
+ p.add_argument(
10122
+ "-o", "--order",
10123
+ choices=("asc", "desc"),
10124
+ default="asc",
10125
+ help="Sort direction by date (default: asc).",
10126
+ )
10127
+ p.add_argument(
10128
+ "--reveal-projects",
10129
+ action="store_true",
10130
+ dest="reveal_projects",
10131
+ help="In --format output, show real project basenames instead of "
10132
+ "the default project-1, project-2, ... anonymization.",
10133
+ )
10134
+ p.add_argument(
10135
+ "--tz", default=None, type=_argparse_tz, metavar="TZ",
10136
+ help="Display timezone: local, utc, or IANA name. "
10137
+ "Overrides config display.tz for this call.",
10138
+ )
10139
+ _add_ccusage_alias_args(p, ansi_emit=False)
10140
+ _add_share_args(p)
10141
+ p.set_defaults(func=cmd_daily)
10142
+ return p
10143
+
10144
+
10145
+ def _build_monthly_parser(subparsers, name, *, help_text, xref):
10146
+ """Build the `monthly` leaf parser (issue #86 Session B; routes to cmd_monthly)."""
10147
+ p = subparsers.add_parser(
10148
+ name,
10149
+ help=help_text,
10150
+ formatter_class=CLIHelpFormatter,
10151
+ description="Show usage grouped by calendar month, matching upstream ccusage monthly output."
10152
+ "\n\n" + xref,
10153
+ epilog=textwrap.dedent("""\
10154
+ Examples:
10155
+ cctally monthly --since 20260101
10156
+ cctally monthly --since 20260101 --until 20260331
10157
+ cctally monthly --since 20260101 --breakdown
10158
+ cctally monthly --since 20260101 --json
10159
+ cctally monthly --order desc
10160
+ """),
10161
+ )
10162
+ p.add_argument(
10163
+ "-s", "--since",
10164
+ default=None,
10165
+ metavar="YYYYMMDD",
10166
+ help="Filter from date (inclusive).",
10167
+ )
10168
+ p.add_argument(
10169
+ "-u", "--until",
10170
+ default=None,
10171
+ metavar="YYYYMMDD",
10172
+ help="Filter until date (inclusive).",
10173
+ )
10174
+ p.add_argument(
10175
+ "-b", "--breakdown",
10176
+ action="store_true",
10177
+ help="Show per-model cost breakdown sub-rows.",
10178
+ )
10179
+ p.add_argument(
10180
+ "-o", "--order",
10181
+ choices=("asc", "desc"),
10182
+ default="asc",
10183
+ help="Sort direction by month (default: asc).",
10184
+ )
10185
+ p.add_argument(
10186
+ "--reveal-projects",
10187
+ action="store_true",
10188
+ dest="reveal_projects",
10189
+ help="In --format output, show real project basenames instead of "
10190
+ "the default project-1, project-2, ... anonymization.",
10191
+ )
10192
+ p.add_argument(
10193
+ "--tz", default=None, type=_argparse_tz, metavar="TZ",
10194
+ help="Display timezone: local, utc, or IANA name. "
10195
+ "Overrides config display.tz for this call.",
10196
+ )
10197
+ _add_ccusage_alias_args(p, ansi_emit=False)
10198
+ _add_share_args(p)
10199
+ p.set_defaults(func=cmd_monthly)
10200
+ return p
10201
+
10202
+
10203
+ def _build_weekly_parser(subparsers, name, *, help_text, xref):
10204
+ """Build the `weekly` leaf parser (issue #86 Session B; routes to cmd_weekly)."""
10205
+ p = subparsers.add_parser(
10206
+ name,
10207
+ help=help_text,
10208
+ formatter_class=CLIHelpFormatter,
10209
+ description="Show Claude usage grouped by subscription week. Boundaries are anchored "
10210
+ "to weekly_usage_snapshots.week_start_at with 7-day-cadence extrapolation "
10211
+ "for pre-snapshot history. Columns extend daily/monthly's set with Used % "
10212
+ "and $/1%."
10213
+ "\n\n" + xref,
10214
+ epilog=textwrap.dedent("""\
10215
+ Examples:
10216
+ cctally weekly
10217
+ cctally weekly --since 20260101
10218
+ cctally weekly --breakdown
10219
+ cctally weekly --json
10220
+ cctally weekly --order desc
10221
+ """),
10222
+ )
10223
+ p.add_argument("-s", "--since", default=None, metavar="YYYYMMDD",
10224
+ help="Filter from date (inclusive).")
10225
+ p.add_argument("-u", "--until", default=None, metavar="YYYYMMDD",
10226
+ help="Filter until date (inclusive).")
10227
+ p.add_argument("-b", "--breakdown", action="store_true",
10228
+ help="Show per-model cost breakdown sub-rows.")
10229
+ p.add_argument("-o", "--order", choices=("asc", "desc"), default="asc",
10230
+ help="Sort direction by week (default: asc).")
10231
+ p.add_argument("--reveal-projects", action="store_true", dest="reveal_projects",
10232
+ help="In --format output, show real project basenames instead of "
10233
+ "the default project-1, project-2, ... anonymization.")
10234
+ p.add_argument("--tz", default=None, type=_argparse_tz, metavar="TZ",
10235
+ help="Display timezone: local, utc, or IANA name. "
10236
+ "Overrides config display.tz for this call.")
10237
+ _add_ccusage_alias_args(p, ansi_emit=False)
10238
+ _add_share_args(p)
10239
+ p.set_defaults(func=cmd_weekly)
10240
+ return p
10241
+
10242
+
10243
+ def _build_session_parser(subparsers, name, *, help_text, xref):
10244
+ """Build the `session` leaf parser (issue #86 Session B; routes to cmd_session)."""
10245
+ p = subparsers.add_parser(
10246
+ name,
10247
+ help=help_text,
10248
+ formatter_class=CLIHelpFormatter,
10249
+ description="Show Claude usage grouped by JSONL sessionId. Resumed sessions (same "
10250
+ "sessionId across multiple files) collapse into one row. 11-column "
10251
+ "layout paralleling codex-session."
10252
+ "\n\n" + xref,
10253
+ epilog=textwrap.dedent("""\
10254
+ Examples:
10255
+ cctally session
10256
+ cctally session --since 20260401
10257
+ cctally session --since 20260401 --breakdown
10258
+ cctally session --json
10259
+ cctally session --order desc
10260
+ """),
10261
+ )
10262
+ p.add_argument("-s", "--since", default=None, metavar="YYYYMMDD",
10263
+ help="Filter from date (inclusive).")
10264
+ p.add_argument("-u", "--until", default=None, metavar="YYYYMMDD",
10265
+ help="Filter until date (inclusive).")
10266
+ p.add_argument("-b", "--breakdown", action="store_true",
10267
+ help="Show per-model cost breakdown sub-rows.")
10268
+ p.add_argument("-o", "--order", choices=("asc", "desc"), default="asc",
10269
+ help="Sort direction by last activity (default: asc — earliest first).")
10270
+ p.add_argument("--reveal-projects", action="store_true", dest="reveal_projects",
10271
+ help="In --format output, show real project basenames instead of "
10272
+ "the default project-1, project-2, ... anonymization.")
10273
+ p.add_argument("--top-n", type=int, default=15, dest="top_n",
10274
+ metavar="N",
10275
+ help="In --format output, cap rows to top N by cost (default: 15). "
10276
+ "Must be >= 1; values above 50 emit a readability warning. "
10277
+ "Has no effect on terminal/JSON output.")
10278
+ p.add_argument("--tz", default=None, type=_argparse_tz, metavar="TZ",
10279
+ help="Display timezone: local, utc, or IANA name. "
10280
+ "Overrides config display.tz for this call.")
10281
+ p.add_argument(
10282
+ "-i", "--id", default=None, metavar="SESSION_ID", dest="id",
10283
+ help="Filter to a single session by exact-string sessionId. "
10284
+ "Match is against the post-resume-merge id (sessions "
10285
+ "resumed across multiple JSONL files collapse to one id). "
10286
+ "Unknown id → exit 0 with the empty-render branch.",
10287
+ )
10288
+ _add_ccusage_alias_args(p, ansi_emit=False)
10289
+ _add_share_args(p)
10290
+ p.set_defaults(func=cmd_session)
10291
+ return p
10292
+
10293
+
10294
+ def _build_blocks_parser(subparsers, name, *, help_text, xref):
10295
+ """Build the `blocks` leaf parser (issue #86 Session B; routes to cmd_blocks).
10296
+
10297
+ Note: `blocks` intentionally has NO `_add_share_args` (matches the former
10298
+ inline block — it is not part of the shareable-output flag surface).
10299
+ """
10300
+ p = subparsers.add_parser(
10301
+ name,
10302
+ help=help_text,
10303
+ formatter_class=CLIHelpFormatter,
10304
+ description="Show usage grouped by 5-hour session blocks, matching upstream ccusage blocks output."
10305
+ "\n\n" + xref,
10306
+ epilog=textwrap.dedent("""\
10307
+ Examples:
10308
+ cctally blocks --since 20260414
10309
+ cctally blocks --since 20260410 --until 20260416
10310
+ cctally blocks --since 20260414 --breakdown
10311
+ cctally blocks --since 20260414 --json
10312
+ """),
10313
+ )
10314
+ p.add_argument(
10315
+ "-s", "--since",
10316
+ default=None,
10317
+ metavar="YYYYMMDD",
10318
+ help="Filter from date (inclusive).",
10319
+ )
10320
+ p.add_argument(
10321
+ "-u", "--until",
10322
+ default=None,
10323
+ metavar="YYYYMMDD",
10324
+ help="Filter until date (inclusive).",
10325
+ )
10326
+ p.add_argument(
10327
+ "-b", "--breakdown",
10328
+ action="store_true",
10329
+ help="Show per-model cost breakdown.",
10330
+ )
10331
+ p.add_argument(
10332
+ "--json",
10333
+ action="store_true",
10334
+ dest="json",
10335
+ help="Output JSON matching upstream ccusage blocks format.",
10336
+ )
10337
+ p.add_argument(
10338
+ "--tz", default=None, type=_argparse_tz, metavar="TZ",
10339
+ help="Display timezone: local, utc, or IANA name. "
10340
+ "Overrides config display.tz for this call.",
10341
+ )
10342
+ _add_ccusage_alias_args(p, ansi_emit=False)
10343
+ p.set_defaults(func=cmd_blocks)
10344
+ return p
10345
+
10346
+
10347
+ def _build_codex_daily_parser(subparsers, name, *, help_text, xref):
10348
+ """Build the `codex-daily` leaf parser (issue #86 Session B; routes to cmd_codex_daily)."""
10349
+ p = subparsers.add_parser(
10350
+ name,
10351
+ help=help_text,
10352
+ formatter_class=CLIHelpFormatter,
10353
+ description="Show Codex usage grouped by date, matching upstream ccusage-codex daily output."
10354
+ "\n\n" + xref,
10355
+ epilog=textwrap.dedent("""\
10356
+ Examples:
10357
+ cctally codex-daily --since 20260401
10358
+ cctally codex-daily --since 20260401 --breakdown
10359
+ cctally codex-daily --since 20260401 --json
10360
+ cctally codex-daily --order desc
10361
+ """),
10362
+ )
10363
+ p.add_argument("-s", "--since", default=None, metavar="YYYY-MM-DD",
10364
+ help="Filter from date (inclusive; accepts YYYY-MM-DD or YYYYMMDD).")
10365
+ p.add_argument("-u", "--until", default=None, metavar="YYYY-MM-DD",
10366
+ help="Filter until date (inclusive; accepts YYYY-MM-DD or YYYYMMDD).")
10367
+ p.add_argument("-b", "--breakdown", action="store_true",
10368
+ help="Show per-model cost breakdown sub-rows.")
10369
+ p.add_argument("-o", "--order", choices=("asc", "desc"), default="asc",
10370
+ help="Sort direction by date (default: asc).")
10371
+ p.add_argument("--json", action="store_true", dest="json",
10372
+ help="Output JSON matching upstream ccusage-codex daily format.")
10373
+ _add_codex_shared_args(p)
10374
+ p.set_defaults(func=cmd_codex_daily)
10375
+ return p
10376
+
10377
+
10378
+ def _build_codex_monthly_parser(subparsers, name, *, help_text, xref):
10379
+ """Build the `codex-monthly` leaf parser (issue #86 Session B; routes to cmd_codex_monthly)."""
10380
+ p = subparsers.add_parser(
10381
+ name,
10382
+ help=help_text,
10383
+ formatter_class=CLIHelpFormatter,
10384
+ description="Show Codex usage grouped by calendar month, matching upstream ccusage-codex monthly output."
10385
+ "\n\n" + xref,
10386
+ epilog=textwrap.dedent("""\
10387
+ Examples:
10388
+ cctally codex-monthly --since 20260101
10389
+ cctally codex-monthly --breakdown
10390
+ cctally codex-monthly --json
10391
+ """),
10392
+ )
10393
+ p.add_argument("-s", "--since", default=None, metavar="YYYY-MM-DD",
10394
+ help="Filter from date (inclusive; accepts YYYY-MM-DD or YYYYMMDD).")
10395
+ p.add_argument("-u", "--until", default=None, metavar="YYYY-MM-DD",
10396
+ help="Filter until date (inclusive; accepts YYYY-MM-DD or YYYYMMDD).")
10397
+ p.add_argument("-b", "--breakdown", action="store_true",
10398
+ help="Show per-model cost breakdown sub-rows.")
10399
+ p.add_argument("-o", "--order", choices=("asc", "desc"), default="asc",
10400
+ help="Sort direction by month (default: asc).")
10401
+ p.add_argument("--json", action="store_true", dest="json",
10402
+ help="Output JSON matching upstream ccusage-codex monthly format.")
10403
+ _add_codex_shared_args(p)
10404
+ p.set_defaults(func=cmd_codex_monthly)
10405
+ return p
10406
+
10407
+
10408
+ def _build_codex_weekly_parser(subparsers, name, *, help_text, xref):
10409
+ """Build the `codex-weekly` leaf parser (issue #86 Session B; routes to cmd_codex_weekly)."""
10410
+ p = subparsers.add_parser(
10411
+ name,
10412
+ help=help_text,
10413
+ formatter_class=CLIHelpFormatter,
10414
+ description="Show Codex usage grouped by week. Week-start day is read from config.json "
10415
+ "(collector.week_start, Monday default). Not a ccusage-codex drop-in — "
10416
+ "upstream has no `codex weekly` command."
10417
+ "\n\n" + xref,
10418
+ epilog=textwrap.dedent("""\
10419
+ Examples:
10420
+ cctally codex-weekly
10421
+ cctally codex-weekly --since 20260301
10422
+ cctally codex-weekly --breakdown
10423
+ cctally codex-weekly --json
10424
+ cctally codex-weekly --order desc
10425
+ """),
10426
+ )
10427
+ p.add_argument("-s", "--since", default=None, metavar="YYYY-MM-DD",
10428
+ help="Filter from date (inclusive; accepts YYYY-MM-DD or YYYYMMDD).")
10429
+ p.add_argument("-u", "--until", default=None, metavar="YYYY-MM-DD",
10430
+ help="Filter until date (inclusive; accepts YYYY-MM-DD or YYYYMMDD).")
10431
+ p.add_argument("-b", "--breakdown", action="store_true",
10432
+ help="Show per-model cost breakdown sub-rows.")
10433
+ p.add_argument("-o", "--order", choices=("asc", "desc"), default="asc",
10434
+ help="Sort direction by week (default: asc).")
10435
+ p.add_argument("--json", action="store_true", dest="json",
10436
+ help="Output JSON.")
10437
+ _add_codex_shared_args(p)
10438
+ p.set_defaults(func=cmd_codex_weekly)
10439
+ return p
10440
+
10441
+
10442
+ def _build_codex_session_parser(subparsers, name, *, help_text, xref):
10443
+ """Build the `codex-session` leaf parser (issue #86 Session B; routes to cmd_codex_session)."""
10444
+ p = subparsers.add_parser(
10445
+ name,
10446
+ help=help_text,
10447
+ formatter_class=CLIHelpFormatter,
10448
+ description="Show Codex usage grouped by session, matching upstream ccusage-codex session output."
10449
+ "\n\n" + xref,
10450
+ epilog=textwrap.dedent("""\
10451
+ Examples:
10452
+ cctally codex-session
10453
+ cctally codex-session --since 20260401
10454
+ cctally codex-session --json
10455
+ """),
10456
+ )
10457
+ p.add_argument("-s", "--since", default=None, metavar="YYYY-MM-DD",
10458
+ help="Filter from date (inclusive; accepts YYYY-MM-DD or YYYYMMDD).")
10459
+ p.add_argument("-u", "--until", default=None, metavar="YYYY-MM-DD",
10460
+ help="Filter until date (inclusive; accepts YYYY-MM-DD or YYYYMMDD).")
10461
+ p.add_argument("-o", "--order", choices=("asc", "desc"), default="asc",
10462
+ help="Sort direction by last activity (default: asc — earliest first).")
10463
+ p.add_argument("--json", action="store_true", dest="json",
10464
+ help="Output JSON matching upstream ccusage-codex session format.")
10465
+ _add_codex_shared_args(p)
10466
+ p.set_defaults(func=cmd_codex_session)
10467
+ return p
10468
+
10469
+
9994
10470
  def build_parser() -> argparse.ArgumentParser:
9995
10471
  p = argparse.ArgumentParser(
9996
10472
  prog="cctally",
@@ -10723,49 +11199,10 @@ def build_parser() -> argparse.ArgumentParser:
10723
11199
  rc.set_defaults(func=cmd_range_cost)
10724
11200
 
10725
11201
  # -- blocks --
10726
- bl = sub.add_parser(
10727
- "blocks",
10728
- help="Show usage report grouped by 5-hour session blocks",
10729
- formatter_class=CLIHelpFormatter,
10730
- description="Show usage grouped by 5-hour session blocks, matching upstream ccusage blocks output.",
10731
- epilog=textwrap.dedent("""\
10732
- Examples:
10733
- cctally blocks --since 20260414
10734
- cctally blocks --since 20260410 --until 20260416
10735
- cctally blocks --since 20260414 --breakdown
10736
- cctally blocks --since 20260414 --json
10737
- """),
10738
- )
10739
- bl.add_argument(
10740
- "-s", "--since",
10741
- default=None,
10742
- metavar="YYYYMMDD",
10743
- help="Filter from date (inclusive).",
10744
- )
10745
- bl.add_argument(
10746
- "-u", "--until",
10747
- default=None,
10748
- metavar="YYYYMMDD",
10749
- help="Filter until date (inclusive).",
10750
- )
10751
- bl.add_argument(
10752
- "-b", "--breakdown",
10753
- action="store_true",
10754
- help="Show per-model cost breakdown.",
10755
- )
10756
- bl.add_argument(
10757
- "--json",
10758
- action="store_true",
10759
- dest="json",
10760
- help="Output JSON matching upstream ccusage blocks format.",
10761
- )
10762
- bl.add_argument(
10763
- "--tz", default=None, type=_argparse_tz, metavar="TZ",
10764
- help="Display timezone: local, utc, or IANA name. "
10765
- "Overrides config display.tz for this call.",
10766
- )
10767
- _add_ccusage_alias_args(bl, ansi_emit=False)
10768
- bl.set_defaults(func=cmd_blocks)
11202
+ _build_blocks_parser(
11203
+ sub, "blocks",
11204
+ help_text="Show usage report grouped by 5-hour session blocks",
11205
+ xref="Alias of `cctally claude blocks` (the canonical form).")
10769
11206
 
10770
11207
  # -- five-hour-blocks --
10771
11208
  fhb = sub.add_parser(
@@ -10851,319 +11288,46 @@ def build_parser() -> argparse.ArgumentParser:
10851
11288
  p_cache_sync.set_defaults(func=cmd_cache_sync)
10852
11289
 
10853
11290
  # -- daily --
10854
- dy = sub.add_parser(
10855
- "daily",
10856
- help="Show usage report grouped by date",
10857
- formatter_class=CLIHelpFormatter,
10858
- description="Show usage grouped by date, matching upstream ccusage daily output.",
10859
- epilog=textwrap.dedent("""\
10860
- Examples:
10861
- cctally daily --since 20260414
10862
- cctally daily --since 20260410 --until 20260416
10863
- cctally daily --since 20260414 --breakdown
10864
- cctally daily --since 20260414 --json
10865
- cctally daily --order desc
10866
- """),
10867
- )
10868
- dy.add_argument(
10869
- "-s", "--since",
10870
- default=None,
10871
- metavar="YYYYMMDD",
10872
- help="Filter from date (inclusive).",
10873
- )
10874
- dy.add_argument(
10875
- "-u", "--until",
10876
- default=None,
10877
- metavar="YYYYMMDD",
10878
- help="Filter until date (inclusive).",
10879
- )
10880
- dy.add_argument(
10881
- "-b", "--breakdown",
10882
- action="store_true",
10883
- help="Show per-model cost breakdown sub-rows.",
10884
- )
10885
- dy.add_argument(
10886
- "-o", "--order",
10887
- choices=("asc", "desc"),
10888
- default="asc",
10889
- help="Sort direction by date (default: asc).",
10890
- )
10891
- dy.add_argument(
10892
- "--reveal-projects",
10893
- action="store_true",
10894
- dest="reveal_projects",
10895
- help="In --format output, show real project basenames instead of "
10896
- "the default project-1, project-2, ... anonymization.",
10897
- )
10898
- dy.add_argument(
10899
- "--tz", default=None, type=_argparse_tz, metavar="TZ",
10900
- help="Display timezone: local, utc, or IANA name. "
10901
- "Overrides config display.tz for this call.",
10902
- )
10903
- _add_ccusage_alias_args(dy, ansi_emit=False)
10904
- _add_share_args(dy)
10905
- dy.set_defaults(func=cmd_daily)
11291
+ _build_daily_parser(
11292
+ sub, "daily",
11293
+ help_text="Show usage report grouped by date",
11294
+ xref="Alias of `cctally claude daily` (the canonical form).")
10906
11295
 
10907
11296
  # -- monthly --
10908
- mo = sub.add_parser(
10909
- "monthly",
10910
- help="Show usage report grouped by month",
10911
- formatter_class=CLIHelpFormatter,
10912
- description="Show usage grouped by calendar month, matching upstream ccusage monthly output.",
10913
- epilog=textwrap.dedent("""\
10914
- Examples:
10915
- cctally monthly --since 20260101
10916
- cctally monthly --since 20260101 --until 20260331
10917
- cctally monthly --since 20260101 --breakdown
10918
- cctally monthly --since 20260101 --json
10919
- cctally monthly --order desc
10920
- """),
10921
- )
10922
- mo.add_argument(
10923
- "-s", "--since",
10924
- default=None,
10925
- metavar="YYYYMMDD",
10926
- help="Filter from date (inclusive).",
10927
- )
10928
- mo.add_argument(
10929
- "-u", "--until",
10930
- default=None,
10931
- metavar="YYYYMMDD",
10932
- help="Filter until date (inclusive).",
10933
- )
10934
- mo.add_argument(
10935
- "-b", "--breakdown",
10936
- action="store_true",
10937
- help="Show per-model cost breakdown sub-rows.",
10938
- )
10939
- mo.add_argument(
10940
- "-o", "--order",
10941
- choices=("asc", "desc"),
10942
- default="asc",
10943
- help="Sort direction by month (default: asc).",
10944
- )
10945
- mo.add_argument(
10946
- "--reveal-projects",
10947
- action="store_true",
10948
- dest="reveal_projects",
10949
- help="In --format output, show real project basenames instead of "
10950
- "the default project-1, project-2, ... anonymization.",
10951
- )
10952
- mo.add_argument(
10953
- "--tz", default=None, type=_argparse_tz, metavar="TZ",
10954
- help="Display timezone: local, utc, or IANA name. "
10955
- "Overrides config display.tz for this call.",
10956
- )
10957
- _add_ccusage_alias_args(mo, ansi_emit=False)
10958
- _add_share_args(mo)
10959
- mo.set_defaults(func=cmd_monthly)
11297
+ _build_monthly_parser(
11298
+ sub, "monthly",
11299
+ help_text="Show usage report grouped by month",
11300
+ xref="Alias of `cctally claude monthly` (the canonical form).")
10960
11301
 
10961
11302
  # -- weekly --
10962
- we = sub.add_parser(
10963
- "weekly",
10964
- help="Show usage grouped by subscription week (with Used %% and $/1%%)",
10965
- formatter_class=CLIHelpFormatter,
10966
- description="Show Claude usage grouped by subscription week. Boundaries are anchored "
10967
- "to weekly_usage_snapshots.week_start_at with 7-day-cadence extrapolation "
10968
- "for pre-snapshot history. Columns extend daily/monthly's set with Used % "
10969
- "and $/1%.",
10970
- epilog=textwrap.dedent("""\
10971
- Examples:
10972
- cctally weekly
10973
- cctally weekly --since 20260101
10974
- cctally weekly --breakdown
10975
- cctally weekly --json
10976
- cctally weekly --order desc
10977
- """),
10978
- )
10979
- we.add_argument("-s", "--since", default=None, metavar="YYYYMMDD",
10980
- help="Filter from date (inclusive).")
10981
- we.add_argument("-u", "--until", default=None, metavar="YYYYMMDD",
10982
- help="Filter until date (inclusive).")
10983
- we.add_argument("-b", "--breakdown", action="store_true",
10984
- help="Show per-model cost breakdown sub-rows.")
10985
- we.add_argument("-o", "--order", choices=("asc", "desc"), default="asc",
10986
- help="Sort direction by week (default: asc).")
10987
- we.add_argument("--reveal-projects", action="store_true", dest="reveal_projects",
10988
- help="In --format output, show real project basenames instead of "
10989
- "the default project-1, project-2, ... anonymization.")
10990
- we.add_argument("--tz", default=None, type=_argparse_tz, metavar="TZ",
10991
- help="Display timezone: local, utc, or IANA name. "
10992
- "Overrides config display.tz for this call.")
10993
- _add_ccusage_alias_args(we, ansi_emit=False)
10994
- _add_share_args(we)
10995
- we.set_defaults(func=cmd_weekly)
10996
-
10997
- # -- codex shared args helper --
10998
- def _add_codex_shared_args(parser: argparse.ArgumentParser) -> None:
10999
- """Register upstream `ccusage-codex sharedArgs` on a codex subparser.
11000
-
11001
- Upstream sharedArgs (node_modules/@ccusage/codex/dist/index.js):
11002
- --timezone/-z, --locale/-l, --compact, --color, --noColor,
11003
- --offline/--no-offline.
11004
-
11005
- Honored here: --timezone (dates + aggregation buckets) and
11006
- --compact (table layout). Accepted-but-no-op (stored on the
11007
- namespace for drop-in parity with upstream scripts): --locale
11008
- (we don't locale-format dates), --color / --noColor (we don't
11009
- emit ANSI codes today). --offline is accepted as a no-op too
11010
- (we are always offline); it uses BooleanOptionalAction so
11011
- `--no-offline` also parses cleanly. `-O` is kept as the short
11012
- form for offline for backward compat with earlier builds.
11013
- """
11014
- parser.add_argument(
11015
- "-z", "--timezone", default=None, metavar="TZ",
11016
- help="IANA timezone for date bucketing and Date/Last Activity cells.",
11017
- )
11018
- parser.add_argument(
11019
- "-l", "--locale", default=None, metavar="LOCALE",
11020
- help="Accepted for drop-in compat; no-op (dates are not locale-formatted).",
11021
- )
11022
- parser.add_argument(
11023
- "--compact", action="store_true",
11024
- help="Force compact table layout regardless of terminal width.",
11025
- )
11026
- parser.add_argument(
11027
- "--color", action="store_true",
11028
- help="Accepted for drop-in compat; no-op today (no ANSI escapes are emitted).",
11029
- )
11030
- parser.add_argument(
11031
- "--noColor", action="store_true", dest="no_color",
11032
- help="Accepted for drop-in compat; no-op today (no ANSI escapes are emitted).",
11033
- )
11034
- parser.add_argument(
11035
- "-O", "--offline", action=argparse.BooleanOptionalAction, default=False,
11036
- help="Accepted for drop-in compat with ccusage-codex; we are always offline.",
11037
- )
11038
- parser.add_argument(
11039
- "--tz", default=None, type=_argparse_tz, metavar="TZ",
11040
- help="Display timezone: local, utc, or IANA name. Overrides "
11041
- "config display.tz for this call. Takes precedence over "
11042
- "upstream's --timezone for drop-in parity.",
11043
- )
11044
- # Issue #92: codex parity for the #89 --debug surface. Codex JSONL
11045
- # has no recorded costUSD to diff against, so the report is the
11046
- # codex variant ("Codex Pricing Debug Report": totals + top-N
11047
- # highest computed-cost entries), wired via
11048
- # _emit_codex_debug_samples_if_set in each cmd_codex_* body.
11049
- parser.add_argument(
11050
- "-d", "--debug", action="store_true",
11051
- help="Emit a stderr 'Codex Pricing Debug Report' (totals + "
11052
- "the N highest computed-cost sample entries).",
11053
- )
11054
- parser.add_argument(
11055
- "--debug-samples", type=_nonneg_int, default=5, metavar="N",
11056
- help="Cap on top-entry sample rows in the --debug report "
11057
- "(default 5; N=0 suppresses the sample block; "
11058
- "negatives rejected at parse time).",
11059
- )
11303
+ _build_weekly_parser(
11304
+ sub, "weekly",
11305
+ help_text="Show usage grouped by subscription week (with Used %% and $/1%%)",
11306
+ xref="Alias of `cctally claude weekly` (the canonical form).")
11060
11307
 
11061
11308
  # -- codex-daily --
11062
- cd = sub.add_parser(
11063
- "codex-daily",
11064
- help="Show Codex usage report grouped by date (drop-in for `ccusage-codex daily`)",
11065
- formatter_class=CLIHelpFormatter,
11066
- description="Show Codex usage grouped by date, matching upstream ccusage-codex daily output.",
11067
- epilog=textwrap.dedent("""\
11068
- Examples:
11069
- cctally codex-daily --since 20260401
11070
- cctally codex-daily --since 20260401 --breakdown
11071
- cctally codex-daily --since 20260401 --json
11072
- cctally codex-daily --order desc
11073
- """),
11074
- )
11075
- cd.add_argument("-s", "--since", default=None, metavar="YYYY-MM-DD",
11076
- help="Filter from date (inclusive; accepts YYYY-MM-DD or YYYYMMDD).")
11077
- cd.add_argument("-u", "--until", default=None, metavar="YYYY-MM-DD",
11078
- help="Filter until date (inclusive; accepts YYYY-MM-DD or YYYYMMDD).")
11079
- cd.add_argument("-b", "--breakdown", action="store_true",
11080
- help="Show per-model cost breakdown sub-rows.")
11081
- cd.add_argument("-o", "--order", choices=("asc", "desc"), default="asc",
11082
- help="Sort direction by date (default: asc).")
11083
- cd.add_argument("--json", action="store_true", dest="json",
11084
- help="Output JSON matching upstream ccusage-codex daily format.")
11085
- _add_codex_shared_args(cd)
11086
- cd.set_defaults(func=cmd_codex_daily)
11309
+ _build_codex_daily_parser(
11310
+ sub, "codex-daily",
11311
+ help_text="Show Codex usage report grouped by date (drop-in for `ccusage-codex daily`)",
11312
+ xref="Alias of `cctally codex daily` (the canonical form).")
11087
11313
 
11088
11314
  # -- codex-monthly --
11089
- cmn = sub.add_parser(
11090
- "codex-monthly",
11091
- help="Show Codex usage grouped by month (drop-in for `ccusage-codex monthly`)",
11092
- formatter_class=CLIHelpFormatter,
11093
- description="Show Codex usage grouped by calendar month, matching upstream ccusage-codex monthly output.",
11094
- epilog=textwrap.dedent("""\
11095
- Examples:
11096
- cctally codex-monthly --since 20260101
11097
- cctally codex-monthly --breakdown
11098
- cctally codex-monthly --json
11099
- """),
11100
- )
11101
- cmn.add_argument("-s", "--since", default=None, metavar="YYYY-MM-DD",
11102
- help="Filter from date (inclusive; accepts YYYY-MM-DD or YYYYMMDD).")
11103
- cmn.add_argument("-u", "--until", default=None, metavar="YYYY-MM-DD",
11104
- help="Filter until date (inclusive; accepts YYYY-MM-DD or YYYYMMDD).")
11105
- cmn.add_argument("-b", "--breakdown", action="store_true",
11106
- help="Show per-model cost breakdown sub-rows.")
11107
- cmn.add_argument("-o", "--order", choices=("asc", "desc"), default="asc",
11108
- help="Sort direction by month (default: asc).")
11109
- cmn.add_argument("--json", action="store_true", dest="json",
11110
- help="Output JSON matching upstream ccusage-codex monthly format.")
11111
- _add_codex_shared_args(cmn)
11112
- cmn.set_defaults(func=cmd_codex_monthly)
11315
+ _build_codex_monthly_parser(
11316
+ sub, "codex-monthly",
11317
+ help_text="Show Codex usage grouped by month (drop-in for `ccusage-codex monthly`)",
11318
+ xref="Alias of `cctally codex monthly` (the canonical form).")
11113
11319
 
11114
11320
  # -- codex-weekly --
11115
- cw = sub.add_parser(
11116
- "codex-weekly",
11117
- help="Show Codex usage grouped by week (week-start from config.json)",
11118
- formatter_class=CLIHelpFormatter,
11119
- description="Show Codex usage grouped by week. Week-start day is read from config.json "
11120
- "(collector.week_start, Monday default). Not a ccusage-codex drop-in — "
11121
- "upstream has no `codex weekly` command.",
11122
- epilog=textwrap.dedent("""\
11123
- Examples:
11124
- cctally codex-weekly
11125
- cctally codex-weekly --since 20260301
11126
- cctally codex-weekly --breakdown
11127
- cctally codex-weekly --json
11128
- cctally codex-weekly --order desc
11129
- """),
11130
- )
11131
- cw.add_argument("-s", "--since", default=None, metavar="YYYY-MM-DD",
11132
- help="Filter from date (inclusive; accepts YYYY-MM-DD or YYYYMMDD).")
11133
- cw.add_argument("-u", "--until", default=None, metavar="YYYY-MM-DD",
11134
- help="Filter until date (inclusive; accepts YYYY-MM-DD or YYYYMMDD).")
11135
- cw.add_argument("-b", "--breakdown", action="store_true",
11136
- help="Show per-model cost breakdown sub-rows.")
11137
- cw.add_argument("-o", "--order", choices=("asc", "desc"), default="asc",
11138
- help="Sort direction by week (default: asc).")
11139
- cw.add_argument("--json", action="store_true", dest="json",
11140
- help="Output JSON.")
11141
- _add_codex_shared_args(cw)
11142
- cw.set_defaults(func=cmd_codex_weekly)
11321
+ _build_codex_weekly_parser(
11322
+ sub, "codex-weekly",
11323
+ help_text="Show Codex usage grouped by week (week-start from config.json)",
11324
+ xref="Alias of `cctally codex weekly` (the canonical form).")
11143
11325
 
11144
11326
  # -- codex-session --
11145
- cs = sub.add_parser(
11146
- "codex-session",
11147
- help="Show Codex usage grouped by session (drop-in for `ccusage-codex session`)",
11148
- formatter_class=CLIHelpFormatter,
11149
- description="Show Codex usage grouped by session, matching upstream ccusage-codex session output.",
11150
- epilog=textwrap.dedent("""\
11151
- Examples:
11152
- cctally codex-session
11153
- cctally codex-session --since 20260401
11154
- cctally codex-session --json
11155
- """),
11156
- )
11157
- cs.add_argument("-s", "--since", default=None, metavar="YYYY-MM-DD",
11158
- help="Filter from date (inclusive; accepts YYYY-MM-DD or YYYYMMDD).")
11159
- cs.add_argument("-u", "--until", default=None, metavar="YYYY-MM-DD",
11160
- help="Filter until date (inclusive; accepts YYYY-MM-DD or YYYYMMDD).")
11161
- cs.add_argument("-o", "--order", choices=("asc", "desc"), default="asc",
11162
- help="Sort direction by last activity (default: asc — earliest first).")
11163
- cs.add_argument("--json", action="store_true", dest="json",
11164
- help="Output JSON matching upstream ccusage-codex session format.")
11165
- _add_codex_shared_args(cs)
11166
- cs.set_defaults(func=cmd_codex_session)
11327
+ _build_codex_session_parser(
11328
+ sub, "codex-session",
11329
+ help_text="Show Codex usage grouped by session (drop-in for `ccusage-codex session`)",
11330
+ xref="Alias of `cctally codex session` (the canonical form).")
11167
11331
 
11168
11332
  # -- project --
11169
11333
  p_project = sub.add_parser(
@@ -11261,51 +11425,62 @@ def build_parser() -> argparse.ArgumentParser:
11261
11425
  diff_p.set_defaults(func=cmd_diff)
11262
11426
 
11263
11427
  # -- session --
11264
- se = sub.add_parser(
11265
- "session",
11266
- help="Show Claude usage grouped by sessionId (merges resumed-across-files sessions)",
11428
+ _build_session_parser(
11429
+ sub, "session",
11430
+ help_text="Show Claude usage grouped by sessionId (merges resumed-across-files sessions)",
11431
+ xref="Alias of `cctally claude session` (the canonical form).")
11432
+
11433
+ # --- `claude` subgroup (drop-in for `ccusage claude …`); issue #86 Session B ---
11434
+ # Build-once, register-twice: these reuse the same nine builders as the flat
11435
+ # forms above. Nested subparsers reuse dest="command" so args.command resolves
11436
+ # to the leaf name (e.g. "blocks"), keeping banner suppression byte-identical
11437
+ # to the flat form with zero hook-path changes.
11438
+ claude_p = sub.add_parser(
11439
+ "claude",
11440
+ help="Claude-source reports (drop-in for `ccusage claude …`)",
11267
11441
  formatter_class=CLIHelpFormatter,
11268
- description="Show Claude usage grouped by JSONL sessionId. Resumed sessions (same "
11269
- "sessionId across multiple files) collapse into one row. 11-column "
11270
- "layout paralleling codex-session.",
11271
- epilog=textwrap.dedent("""\
11272
- Examples:
11273
- cctally session
11274
- cctally session --since 20260401
11275
- cctally session --since 20260401 --breakdown
11276
- cctally session --json
11277
- cctally session --order desc
11278
- """),
11279
- )
11280
- se.add_argument("-s", "--since", default=None, metavar="YYYYMMDD",
11281
- help="Filter from date (inclusive).")
11282
- se.add_argument("-u", "--until", default=None, metavar="YYYYMMDD",
11283
- help="Filter until date (inclusive).")
11284
- se.add_argument("-b", "--breakdown", action="store_true",
11285
- help="Show per-model cost breakdown sub-rows.")
11286
- se.add_argument("-o", "--order", choices=("asc", "desc"), default="asc",
11287
- help="Sort direction by last activity (default: asc — earliest first).")
11288
- se.add_argument("--reveal-projects", action="store_true", dest="reveal_projects",
11289
- help="In --format output, show real project basenames instead of "
11290
- "the default project-1, project-2, ... anonymization.")
11291
- se.add_argument("--top-n", type=int, default=15, dest="top_n",
11292
- metavar="N",
11293
- help="In --format output, cap rows to top N by cost (default: 15). "
11294
- "Must be >= 1; values above 50 emit a readability warning. "
11295
- "Has no effect on terminal/JSON output.")
11296
- se.add_argument("--tz", default=None, type=_argparse_tz, metavar="TZ",
11297
- help="Display timezone: local, utc, or IANA name. "
11298
- "Overrides config display.tz for this call.")
11299
- se.add_argument(
11300
- "-i", "--id", default=None, metavar="SESSION_ID", dest="id",
11301
- help="Filter to a single session by exact-string sessionId. "
11302
- "Match is against the post-resume-merge id (sessions "
11303
- "resumed across multiple JSONL files collapse to one id). "
11304
- "Unknown id exit 0 with the empty-render branch.",
11305
- )
11306
- _add_ccusage_alias_args(se, ansi_emit=False)
11307
- _add_share_args(se)
11308
- se.set_defaults(func=cmd_session)
11442
+ description="Claude-source usage reports. Each subcommand is a drop-in for the "
11443
+ "matching `ccusage claude <cmd>` and shares its engine with the "
11444
+ "top-level `cctally <cmd>` alias.")
11445
+ claude_sub = claude_p.add_subparsers(dest="command", required=True, metavar="<command>")
11446
+ _build_daily_parser(claude_sub, "daily",
11447
+ help_text="Show usage grouped by date",
11448
+ xref="Drop-in for `ccusage claude daily`. Same engine as `cctally daily`.")
11449
+ _build_monthly_parser(claude_sub, "monthly",
11450
+ help_text="Show usage grouped by month",
11451
+ xref="Drop-in for `ccusage claude monthly`. Same engine as `cctally monthly`.")
11452
+ _build_weekly_parser(claude_sub, "weekly",
11453
+ help_text="Show usage grouped by subscription week",
11454
+ xref="Drop-in for `ccusage claude weekly`. Same engine as `cctally weekly`.")
11455
+ _build_session_parser(claude_sub, "session",
11456
+ help_text="Show usage grouped by session",
11457
+ xref="Drop-in for `ccusage claude session`. Same engine as `cctally session`.")
11458
+ _build_blocks_parser(claude_sub, "blocks",
11459
+ help_text="Show usage grouped by 5-hour session blocks",
11460
+ xref="Drop-in for `ccusage claude blocks`. Same engine as `cctally blocks`.")
11461
+
11462
+ # --- `codex` subgroup (drop-in for `ccusage codex …`); issue #86 Session B ---
11463
+ codex_p = sub.add_parser(
11464
+ "codex",
11465
+ help="Codex-source reports (drop-in for `ccusage codex …`)",
11466
+ formatter_class=CLIHelpFormatter,
11467
+ description="Codex-source usage reports. daily/monthly/session are drop-ins for "
11468
+ "`ccusage codex <cmd>`; weekly is a cctally extension. Each shares its "
11469
+ "engine with the matching `cctally codex-<cmd>` alias.")
11470
+ codex_sub = codex_p.add_subparsers(dest="command", required=True, metavar="<command>")
11471
+ _build_codex_daily_parser(codex_sub, "daily",
11472
+ help_text="Show Codex usage grouped by date",
11473
+ xref="Drop-in for `ccusage codex daily`. Same engine as `cctally codex-daily`.")
11474
+ _build_codex_monthly_parser(codex_sub, "monthly",
11475
+ help_text="Show Codex usage grouped by month",
11476
+ xref="Drop-in for `ccusage codex monthly`. Same engine as `cctally codex-monthly`.")
11477
+ _build_codex_session_parser(codex_sub, "session",
11478
+ help_text="Show Codex usage grouped by session",
11479
+ xref="Drop-in for `ccusage codex session`. Same engine as `cctally codex-session`.")
11480
+ _build_codex_weekly_parser(codex_sub, "weekly",
11481
+ help_text="Show Codex usage grouped by week",
11482
+ xref="cctally extension (no upstream `ccusage codex weekly`). Same engine as "
11483
+ "`cctally codex-weekly`.")
11309
11484
 
11310
11485
  # ---- config (persisted user preferences) ----
11311
11486
  cfg_p = sub.add_parser(
@@ -11448,6 +11623,9 @@ def build_parser() -> argparse.ArgumentParser:
11448
11623
  help="Skip confirmations")
11449
11624
  sp.add_argument("--json", action="store_true",
11450
11625
  help="Emit machine-readable output")
11626
+ sp.add_argument("--force-dev", action="store_true", dest="force_dev",
11627
+ help="Allow setup to run from a dev checkout (writes "
11628
+ "dev-pointing hooks into ~/.claude/settings.json)")
11451
11629
  # Legacy bespoke-hook migration flags (install-mode only — see cmd_setup
11452
11630
  # post-parse validation). Spec Section 2 mode×flag matrix.
11453
11631
  mig_group = sp.add_mutually_exclusive_group()
@@ -13495,10 +13673,19 @@ def main(argv: list[str] | None = None) -> int:
13495
13673
  # works without a subcommand (`cctally --version`).
13496
13674
  if getattr(args, "version", False):
13497
13675
  v = _lib_changelog._read_latest_changelog_version()
13498
- if v is None:
13499
- print("cctally unknown")
13500
- else:
13501
- print(f"cctally {v[0]}")
13676
+ base = "cctally unknown" if v is None else f"cctally {v[0]}"
13677
+ # Dev-instance isolation (§4, P3): append the dev marker + resolved
13678
+ # data dir whenever running from a checkout — keyed on
13679
+ # _is_dev_checkout(), NOT DEV_MODE, so the CCTALLY_DATA_DIR
13680
+ # override-on-checkout case (DEV_MODE False) still shows the marker
13681
+ # instead of masquerading as the installed binary. Prod (no .git)
13682
+ # output is unchanged. The override case is labelled distinctly so
13683
+ # the user can tell auto-detect (cctally-dev) from an explicit dir.
13684
+ if _cctally_core.DEV_MODE:
13685
+ base += f" (dev — {_cctally_core.APP_DIR})"
13686
+ elif _cctally_core._is_dev_checkout():
13687
+ base += f" (dev checkout, custom data dir — {_cctally_core.APP_DIR})"
13688
+ print(base)
13502
13689
  return 0
13503
13690
  if not getattr(args, "func", None):
13504
13691
  parser.error("a subcommand is required")