claude-usage-limits 1.11.2 → 1.11.4

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "usage-limits",
3
3
  "displayName": "Usage Limits",
4
- "version": "1.11.2",
4
+ "version": "1.11.4",
5
5
  "description": "Puts your remaining Claude Code usage limit into Claude's context before every prompt, so it opens with what fits in the budget instead of starting work that gets cut off. Reports headroom as turns rather than percentages, prices a job before you start it, and detects your plan tier.",
6
6
  "author": {
7
7
  "name": "Ridelink",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "usage-limits",
3
- "version": "1.11.2",
3
+ "version": "1.11.4",
4
4
  "description": "Reports how much of your Codex usage limit is left as turns of work rather than a percentage, prices a job before you start it, and counts the other agents sharing the same budget.",
5
5
  "author": {
6
6
  "name": "Ridelink",
package/README.md CHANGED
@@ -347,6 +347,12 @@ every tool call keeps it so, the Stop hook marks it idle), so the list is live
347
347
  without anyone polling anything. The status line adds `+1 working` when
348
348
  another session is spending.
349
349
 
350
+ Under the bars it says when the binding window runs out at the pace of the
351
+ last hour, measured from every session's spend, whenever that comes before
352
+ the reset: `at this pace the 5-hour window runs out in 25m, about 12 turns`,
353
+ yellow inside half an hour and red inside ten minutes. The pane rings the
354
+ terminal bell once when a window turns yellow and once more when it turns red.
355
+
350
356
  `panel` alone runs it in the current pane, `--once` prints one frame, `--json`
351
357
  prints the fields, `--no-fetch` (or `USAGE_LIMITS_FETCH=off`) keeps it
352
358
  entirely offline on the reading already on disk, and `--poll N` sets the
@@ -461,6 +467,7 @@ minute, so it costs about 400ms cold and 120ms warm.
461
467
  | `USAGE_LIMITS_CLOCK` | from settings | `12h` or `24h` for reset times; otherwise follows Claude Code's `timeFormat`. |
462
468
  | `USAGE_LIMITS_COLOUR` | detected | `256` or `none` to override colour detection. `NO_COLOR` and `FORCE_COLOR` are honoured. |
463
469
  | `USAGE_LIMITS_ASCII` | off | `1` draws the bars and the spinner with plain characters. |
470
+ | `USAGE_LIMITS_BELL` | on | `off` silences the panel's terminal bell when a window turns yellow or red (`--no-bell` does the same). |
464
471
 
465
472
  ## What a session cost
466
473
 
@@ -666,12 +673,10 @@ allowance and never quotes a price, so the percentages stand alone. And
666
673
 
667
674
  The Claude Code extension for VS Code shows the limits only when you ask with
668
675
  `/usage`, and it does not render a custom status line. So there is an
669
- extension of its own in [vscode/](vscode/): the same bars as a view that sits
670
- directly under the Claude Code chat in the secondary side bar (it contributes
671
- into the Claude Code extension's own view container, so there is no gap and
672
- nothing to arrange), a status bar item with the percentages that turns yellow
673
- and red at the same thresholds, the Sessions list, and the same animations in
674
- CSS. It carries the plugin's scripts inside it, so it has no dependencies and
676
+ extension of its own in [vscode/](vscode/): the same bars as one full-height
677
+ view in the right sidebar, beside the chat, opened for you when VS Code
678
+ starts, with the Sessions list and the same animations in CSS. Nothing at the
679
+ bottom unless you turn `claudeUsageLimits.statusBar` on. It carries the plugin's scripts inside it, so it has no dependencies and
675
680
  reads the same files and takes the same reading as the terminal panel.
676
681
 
677
682
  ```
@@ -859,7 +864,7 @@ test/ node --test, no dependencies
859
864
  node --test
860
865
  ```
861
866
 
862
- 469 tests over the pricing, the window arithmetic, plan and credit detection,
867
+ 477 tests over the pricing, the window arithmetic, plan and credit detection,
863
868
  the status line, the before-prompt line, the mid-turn pulse, the after-reply tally and the session history, job forecasting,
864
869
  per-project attribution, the Codex reader and its installer, the CLI,
865
870
  packaging, and the settings save/restore.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-usage-limits",
3
- "version": "1.11.2",
3
+ "version": "1.11.4",
4
4
  "description": "Puts your remaining Claude Code usage limit into Claude's context before every prompt, so it opens with what fits in the budget instead of starting work that gets cut off. Reports headroom as turns rather than percentages, prices a job before you start it, and detects your plan tier.",
5
5
  "keywords": [
6
6
  "claude",
@@ -47,6 +47,7 @@ const FILE_CHECK_MS = SECOND;
47
47
  const FRAME_MS = 100;
48
48
  const MIN_COLUMNS = 24;
49
49
  const TITLE = 'Claude usage';
50
+ const LEVEL_RANK = { fill: 0, warning: 1, error: 2 };
50
51
 
51
52
  const HELP = `claude-usage-limits panel - live limits in a pane beside the chat
52
53
 
@@ -58,6 +59,7 @@ const HELP = `claude-usage-limits panel - live limits in a pane beside the chat
58
59
  panel --poll 45 seconds between readings (default 30 working, 120 idle)
59
60
  panel --width 40 draw for this many columns instead of the terminal's
60
61
  panel --ascii plain characters instead of block glyphs
62
+ panel --no-bell no terminal bell when a window turns yellow or red
61
63
 
62
64
  Shows the current session (5-hour) window, the current week, and the week for
63
65
  the model in use when the account caps that model on its own. Bars turn yellow
@@ -88,12 +90,14 @@ function parseArgs(argv) {
88
90
  width: null,
89
91
  help: false,
90
92
  ascii: String(process.env.USAGE_LIMITS_ASCII || '') === '1',
93
+ bell: true,
91
94
  hostName: null,
92
95
  };
93
96
  const list = argv || [];
94
97
  for (let i = 0; i < list.length; i += 1) {
95
98
  const arg = list[i];
96
99
  if (arg === '--once') args.once = true;
100
+ else if (arg === '--no-bell') args.bell = false;
97
101
  else if (arg === '--json') args.json = true;
98
102
  else if (arg === '--open') args.open = true;
99
103
  else if (arg === '--no-fetch') args.fetch = false;
@@ -182,6 +186,11 @@ async function snapshot(options) {
182
186
  built.sessionsList = onCodex ? [] : loadSessions(now);
183
187
  built.sessions = onCodex ? 0 : Math.max(built.sessionsList.length, brief.liveSessions(brief.readCache(), now, brief.LIVE_WINDOW_MS, null));
184
188
  built.othersWorking = built.sessionsList.filter((row) => row.state === 'working').length;
189
+ // What the pace says: when the binding window runs out at the current rate,
190
+ // if that comes before its reset. The transcript scan behind it belongs to
191
+ // the report, so it is taken with the readings, not with every frame; the
192
+ // frames in between carry the last answer forward.
193
+ built.pace = opts.pace !== undefined ? opts.pace : await paceOf(now);
185
194
  // Whether the panel is allowed the network at all, which is what the footer
186
195
  // reports. A frame rebuilt from disk between readings is not "network off".
187
196
  built.fetch = opts.network !== undefined ? Boolean(opts.network) : Boolean(opts.fetch);
@@ -189,6 +198,28 @@ async function snapshot(options) {
189
198
  return built;
190
199
  }
191
200
 
201
+ // The report's runway: how long the binding window lasts at the pace of the
202
+ // last hour, measured from every session's spend. The percentage says where
203
+ // you are; this says when you hit the wall, which is the number the rival
204
+ // monitors lead with and the one that matters on a busy afternoon.
205
+ async function paceOf(now) {
206
+ try {
207
+ const data = await usage.report(now, {});
208
+ const binding = data && data.binding;
209
+ if (!binding || !Number.isFinite(binding.headroomMs)) return null;
210
+ const resetsInMs = Number.isFinite(binding.resetsAt) ? binding.resetsAt - now : null;
211
+ return {
212
+ label: binding.label,
213
+ headroomMs: binding.headroomMs,
214
+ resetsInMs,
215
+ turnsLeft: Number.isFinite(binding.turnsLeft) ? binding.turnsLeft : null,
216
+ runsOut: binding.verdict === 'runs-out' || (resetsInMs !== null && binding.headroomMs < resetsInMs),
217
+ };
218
+ } catch (err) {
219
+ return null;
220
+ }
221
+ }
222
+
192
223
  // Codex has no hooks to say when it is working, but it appends to its rollout
193
224
  // file as it goes, so a rollout touched in the last few seconds is a turn in
194
225
  // progress.
@@ -252,6 +283,13 @@ function fit(text, width) {
252
283
  return out + '\x1b[0m';
253
284
  }
254
285
 
286
+ // In a narrow pane the titles are the short ones.
287
+ function shortTitle(row) {
288
+ if (row.key === 'five_hour') return 'Session';
289
+ if (row.key === 'seven_day') return 'Week';
290
+ return String(row.title).replace(/^Current week /, 'Week ').replace(/^Current /, '');
291
+ }
292
+
255
293
  function subline(row, mode, opts) {
256
294
  if (row.stale) return bars.dim('window rolled over, taking a fresh reading', mode);
257
295
  if (row.unreported) return bars.dim('not reported yet, run /usage in Claude Code', mode);
@@ -336,7 +374,7 @@ function render(built, options) {
336
374
  row.level === 'fill' ? row.percentText : bars.paint(row.percentText, bars.levelColour(row.level), mode);
337
375
  body.push({
338
376
  lines: [
339
- bars.bold(row.title, mode),
377
+ bars.bold(columns < 34 ? shortTitle(row) : row.title, mode),
340
378
  (row.percent === null ? bars.paint((ascii ? '-' : '░').repeat(barWidth), bars.THEME.empty, mode) : bars.bar(row.percent, barWidth, { mode, level: row.level, ascii })) +
341
379
  ' ' +
342
380
  percent,
@@ -382,6 +420,21 @@ function render(built, options) {
382
420
  const colour = noteColour(built);
383
421
  footer.push(colour ? bars.paint(built.note, colour, mode) : bars.dim(built.note, mode));
384
422
  }
423
+ if (built.pace && built.pace.runsOut && Number.isFinite(built.pace.headroomMs)) {
424
+ // The longest form that fits: a sentence in a wide pane, a phrase in a
425
+ // narrow one, never a sentence cut off halfway.
426
+ const left = usage.formatDuration(built.pace.headroomMs);
427
+ const turns = Number.isFinite(built.pace.turnsLeft) ? ', about ' + built.pace.turnsLeft + ' turns' : '';
428
+ const forms = [
429
+ 'at this pace the ' + built.pace.label + ' window runs out in ' + left + turns,
430
+ 'runs out in ' + left + ' at this pace' + turns,
431
+ 'runs out in ' + left + ' at this pace',
432
+ 'wall in ' + left,
433
+ ];
434
+ const text = forms.find((form) => form.length <= real) || forms[forms.length - 1];
435
+ const colour = built.pace.headroomMs < 10 * MINUTE ? bars.THEME.error : built.pace.headroomMs < 30 * MINUTE ? bars.THEME.warning : null;
436
+ footer.push(colour ? bars.paint(text, colour, mode) : bars.dim(text, mode));
437
+ }
385
438
  footer.push(stateLine(built, mode));
386
439
  if (opts.interactive !== false) footer.push(bars.dim('q quit · r refresh', mode));
387
440
 
@@ -425,21 +478,21 @@ function openCommand(env, panelPath, nodePath, platform, extraArgs, options) {
425
478
  const old = version && (Number(version[1]) < 3 || (Number(version[1]) === 3 && Number(version[2]) < 1));
426
479
  return {
427
480
  program: 'tmux',
428
- args: ['split-window', '-h', '-d'].concat(old ? ['-p', '32'] : ['-l', '32%'], [cmd]),
481
+ args: ['split-window', '-h', '-d'].concat(old ? ['-p', '24'] : ['-l', '24%'], [cmd]),
429
482
  note: 'opened a pane to the right in tmux',
430
483
  };
431
484
  }
432
485
  if (e.WEZTERM_PANE) {
433
486
  return {
434
487
  program: 'wezterm',
435
- args: ['cli', 'split-pane', '--right', '--percent', '32', '--', node, panel].concat(extra),
488
+ args: ['cli', 'split-pane', '--right', '--percent', '24', '--', node, panel].concat(extra),
436
489
  note: 'opened a pane to the right in WezTerm',
437
490
  };
438
491
  }
439
492
  if (e.KITTY_WINDOW_ID) {
440
493
  return {
441
494
  program: 'kitten',
442
- args: ['@', 'launch', '--location=vsplit', '--bias=32', '--cwd=current', node, panel].concat(extra),
495
+ args: ['@', 'launch', '--location=vsplit', '--bias=24', '--cwd=current', node, panel].concat(extra),
443
496
  note: 'opened a pane to the right in kitty (needs allow_remote_control)',
444
497
  };
445
498
  }
@@ -454,7 +507,7 @@ function openCommand(env, panelPath, nodePath, platform, extraArgs, options) {
454
507
  if (e.WT_SESSION) {
455
508
  return {
456
509
  command:
457
- 'start "" wt.exe -w 0 sp -V --size 0.32 --title "Claude usage" --suppressApplicationTitle ' + cmd,
510
+ 'start "" wt.exe -w 0 sp -V --size 0.24 --title "Claude usage" --suppressApplicationTitle ' + cmd,
458
511
  shell: true,
459
512
  note: 'opened a pane to the right in Windows Terminal',
460
513
  };
@@ -539,8 +592,10 @@ async function interactive(args) {
539
592
  const clock = feed.clockFor(settings, env);
540
593
  const mode = bars.colourMode(env, out.isTTY);
541
594
  const fetch = args.fetch && !live.fetchDisabled(env);
595
+ const bell = args.bell && String(env.USAGE_LIMITS_BELL || '').toLowerCase() !== 'off';
542
596
 
543
597
  const state = {
598
+ levels: {},
544
599
  built: null,
545
600
  outcome: null,
546
601
  lastFetchAt: 0,
@@ -643,10 +698,25 @@ async function interactive(args) {
643
698
  if (!state.built || now - state.lastCheck >= FILE_CHECK_MS) {
644
699
  state.lastCheck = now;
645
700
  try {
646
- state.built = await snapshot({ fetch: false, network: fetch, env, now, outcome: state.outcome });
701
+ state.built = await snapshot({
702
+ fetch: false,
703
+ network: fetch,
704
+ env,
705
+ now,
706
+ outcome: state.outcome,
707
+ pace: state.built ? state.built.pace : null,
708
+ });
647
709
  } catch (err) {
648
710
  // Keep the last frame; a transient read error is not worth a blank.
649
711
  }
712
+ // One bell when a window first turns yellow, another when it turns red.
713
+ if (bell && state.built) {
714
+ for (const row of state.built.rows) {
715
+ const before = state.levels[row.key];
716
+ if (before !== undefined && LEVEL_RANK[row.level] > LEVEL_RANK[before]) out.write('\x07');
717
+ state.levels[row.key] = row.level;
718
+ }
719
+ }
650
720
  // A window that rolled over deserves a reading sooner than the timer.
651
721
  if (fetch && state.built && state.built.rows.some((row) => row.stale) && state.delayMs > 5 * SECOND) {
652
722
  state.delayMs = 5 * SECOND;