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.
|
|
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.
|
|
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
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
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
|
-
|
|
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.
|
|
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', '
|
|
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', '
|
|
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=
|
|
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.
|
|
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({
|
|
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;
|