cctally 1.62.0 → 1.63.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,11 @@ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [1.63.0] - 2026-07-07
9
+
10
+ ### Added
11
+ - `cctally statusline --usage-only` renders just the subscription usage chip (`5h X% · 7d Y%`), dropping the model, cost, burn-rate, context, and reset-countdown segments — for users who want a small quota indicator in their prompt without the cost telemetry. Persist it with `cctally config set statusline.usage_only true`; `--no-usage-only` forces the full statusline when the config key is on. Contributed by @nathanm4 (public PR #3).
12
+
8
13
  ## [1.62.0] - 2026-07-06
9
14
 
10
15
  ### Changed
@@ -309,6 +309,7 @@ ALLOWED_CONFIG_KEYS = (
309
309
  "statusline.visual_burn_rate",
310
310
  "statusline.cost_source",
311
311
  "statusline.cctally_extensions",
312
+ "statusline.usage_only",
312
313
  "budget.weekly_usd",
313
314
  "budget.alerts_enabled",
314
315
  "budget.alert_thresholds",
@@ -379,6 +380,25 @@ def _validate_statusline_cctally_extensions(value):
379
380
  )
380
381
 
381
382
 
383
+ def _validate_statusline_usage_only(value):
384
+ """Validate ``statusline.usage_only``.
385
+
386
+ Accepts booleans (preferred) or canonical truthy/falsy strings
387
+ (``true``/``false``/``yes``/``no``/``on``/``off``/``1``/``0``).
388
+ """
389
+ if isinstance(value, bool):
390
+ return value
391
+ if isinstance(value, str):
392
+ lo = value.strip().lower()
393
+ if lo in ("true", "yes", "on", "1"):
394
+ return True
395
+ if lo in ("false", "no", "off", "0"):
396
+ return False
397
+ raise ValueError(
398
+ f"statusline.usage_only must be boolean (got {value!r})"
399
+ )
400
+
401
+
382
402
  def cmd_config(args: argparse.Namespace) -> int:
383
403
  """Get/set/unset persisted user preferences in config.json.
384
404
 
@@ -544,6 +564,7 @@ def _config_known_value(config: dict, key: str) -> "object":
544
564
  "statusline.visual_burn_rate",
545
565
  "statusline.cost_source",
546
566
  "statusline.cctally_extensions",
567
+ "statusline.usage_only",
547
568
  ):
548
569
  sl_block = config.get("statusline") if isinstance(config, dict) else None
549
570
  if not isinstance(sl_block, dict):
@@ -554,6 +575,7 @@ def _config_known_value(config: dict, key: str) -> "object":
554
575
  "visual_burn_rate": "off",
555
576
  "cost_source": "auto",
556
577
  "cctally_extensions": True,
578
+ "usage_only": False,
557
579
  }
558
580
  if stored is None:
559
581
  return defaults[inner]
@@ -561,6 +583,7 @@ def _config_known_value(config: dict, key: str) -> "object":
561
583
  "visual_burn_rate": _validate_statusline_visual_burn_rate,
562
584
  "cost_source": _validate_statusline_cost_source,
563
585
  "cctally_extensions": _validate_statusline_cctally_extensions,
586
+ "usage_only": _validate_statusline_usage_only,
564
587
  }[inner]
565
588
  try:
566
589
  return validator(stored)
@@ -987,12 +1010,14 @@ def _cmd_config_set(args: argparse.Namespace) -> int:
987
1010
  "statusline.visual_burn_rate",
988
1011
  "statusline.cost_source",
989
1012
  "statusline.cctally_extensions",
1013
+ "statusline.usage_only",
990
1014
  ):
991
1015
  inner_key = key.split(".", 1)[1]
992
1016
  validator = {
993
1017
  "visual_burn_rate": _validate_statusline_visual_burn_rate,
994
1018
  "cost_source": _validate_statusline_cost_source,
995
1019
  "cctally_extensions": _validate_statusline_cctally_extensions,
1020
+ "usage_only": _validate_statusline_usage_only,
996
1021
  }[inner_key]
997
1022
  try:
998
1023
  normalized = validator(raw)
@@ -1390,6 +1415,7 @@ def _cmd_config_unset(args: argparse.Namespace) -> int:
1390
1415
  "statusline.visual_burn_rate",
1391
1416
  "statusline.cost_source",
1392
1417
  "statusline.cctally_extensions",
1418
+ "statusline.usage_only",
1393
1419
  ):
1394
1420
  inner_key = key.split(".", 1)[1]
1395
1421
  with config_writer_lock():
@@ -868,6 +868,21 @@ def _build_statusline_parser(subparsers, name, *, help_text, xref):
868
868
  action="store_false",
869
869
  help="Suppress cctally 5h%%/7d%% segment.",
870
870
  )
871
+ p.add_argument(
872
+ "--usage-only",
873
+ dest="usage_only",
874
+ action="store_true",
875
+ default=None,
876
+ help="Render only subscription usage percentages, e.g. "
877
+ "`5h 36%% · 7d 35%%` (config key statusline.usage_only).",
878
+ )
879
+ p.add_argument(
880
+ "--no-usage-only",
881
+ dest="usage_only",
882
+ action="store_false",
883
+ help="Render the full statusline even when statusline.usage_only "
884
+ "is enabled in config.",
885
+ )
871
886
  p.add_argument(
872
887
  "--config",
873
888
  dest="config",
@@ -218,6 +218,14 @@ def cmd_statusline(args: argparse.Namespace) -> int:
218
218
  )
219
219
  ext_on = True
220
220
 
221
+ usage_only = _resolve(args.usage_only, "usage_only", False)
222
+ if not isinstance(usage_only, bool):
223
+ warn_once(
224
+ f"cctally statusline: invalid statusline.usage_only="
225
+ f"{usage_only!r}; using False"
226
+ )
227
+ usage_only = False
228
+
221
229
  tz_name = _resolve_statusline_tz(getattr(args, "timezone", None), cfg, warn_once)
222
230
 
223
231
  # Color: explicit CLI > NO_COLOR env > TTY detect.
@@ -232,6 +240,7 @@ def cmd_statusline(args: argparse.Namespace) -> int:
232
240
  context_low_threshold=int(args.context_low_threshold),
233
241
  context_medium_threshold=int(args.context_medium_threshold),
234
242
  cctally_extensions=bool(ext_on),
243
+ usage_only=bool(usage_only),
235
244
  color=bool(color),
236
245
  display_tz_name=tz_name,
237
246
  debug=bool(args.debug),
@@ -634,4 +643,3 @@ def _build_statusline_injections(warn_once):
634
643
  warn_once=warn_once,
635
644
  )
636
645
 
637
-
@@ -54,6 +54,7 @@ class StatuslineArgs:
54
54
  context_low_threshold: int
55
55
  context_medium_threshold: int
56
56
  cctally_extensions: bool
57
+ usage_only: bool
57
58
  color: bool # ANSI on/off after auto-detect resolved
58
59
  display_tz_name: str # IANA name; resolved upstream via
59
60
  # get_display_tz_pref(cfg) — defaults to
@@ -372,6 +373,8 @@ def resolve_cctally_extensions(
372
373
  inp: StatuslineInput,
373
374
  now: datetime,
374
375
  inj: StatuslineInjections,
376
+ *,
377
+ include_countdowns: bool = True,
375
378
  ) -> Optional[str]:
376
379
  """Segment 5 — cctally-only `5h X% (...) · 7d Y% (...)`.
377
380
 
@@ -411,12 +414,12 @@ def resolve_cctally_extensions(
411
414
  parts = []
412
415
  if five_pct is not None:
413
416
  s = f"5h {int(round(five_pct))}%"
414
- if five_resets is not None:
417
+ if include_countdowns and five_resets is not None:
415
418
  s += f" ({_fmt_countdown(five_resets - now_epoch)})"
416
419
  parts.append(s)
417
420
  if seven_pct is not None:
418
421
  s = f"7d {int(round(seven_pct))}%"
419
- if seven_resets is not None:
422
+ if include_countdowns and seven_resets is not None:
420
423
  s += f" ({_fmt_countdown(seven_resets - now_epoch)})"
421
424
  parts.append(s)
422
425
  return " · ".join(parts)
@@ -443,6 +446,19 @@ def _wrap_color(text: str, color: Optional[str], enable: bool) -> str:
443
446
  _PERCENT_INT_RE = re.compile(r"(\d+)%")
444
447
 
445
448
 
449
+ def _colorize_usage_segment(ext: str, args: StatuslineArgs) -> str:
450
+ """Color a 5h/7d usage segment using the cctally percent bands."""
451
+ nums = [int(x) for x in _PERCENT_INT_RE.findall(ext)]
452
+ mx = max(nums) if nums else 0
453
+ if mx < 60:
454
+ color = "green"
455
+ elif mx < 85:
456
+ color = "yellow"
457
+ else:
458
+ color = "red"
459
+ return _wrap_color(ext, color, args.color)
460
+
461
+
446
462
  def render_statusline(
447
463
  inp: StatuslineInput,
448
464
  args: StatuslineArgs,
@@ -453,6 +469,12 @@ def render_statusline(
453
469
  None segments (currently only segment 5). See spec §1 for the exact
454
470
  layout and §3 for the data flow.
455
471
  """
472
+ if args.usage_only:
473
+ ext = resolve_cctally_extensions(
474
+ inp, now, inj, include_countdowns=False
475
+ )
476
+ return "" if ext is None else _colorize_usage_segment(ext, args)
477
+
456
478
  seg1 = resolve_model_segment(inp)
457
479
 
458
480
  # Segment 2: 💰 ... session / ... today / ... block (Xh Ym left)
@@ -486,17 +508,7 @@ def render_statusline(
486
508
  if args.cctally_extensions:
487
509
  ext = resolve_cctally_extensions(inp, now, inj)
488
510
  if ext is not None:
489
- # Color by the higher of (5h%, 7d%) using cctally bands:
490
- # <60 green, <85 yellow, >=85 red.
491
- nums = [int(x) for x in _PERCENT_INT_RE.findall(ext)]
492
- mx = max(nums) if nums else 0
493
- if mx < 60:
494
- color = "green"
495
- elif mx < 85:
496
- color = "yellow"
497
- else:
498
- color = "red"
499
- seg5 = _wrap_color(ext, color, args.color)
511
+ seg5 = _colorize_usage_segment(ext, args)
500
512
 
501
513
  segs = [seg1, seg2, seg3, seg4]
502
514
  if seg5 is not None:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cctally",
3
- "version": "1.62.0",
3
+ "version": "1.63.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": {