claude-usage-limits 1.5.5 → 1.6.1
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.
|
|
4
|
+
"version": "1.6.1",
|
|
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",
|
package/README.md
CHANGED
|
@@ -207,6 +207,12 @@ A window with no recent spend to measure is ranked by how full it is rather
|
|
|
207
207
|
than being skipped, so a 5-hour window sitting at 95 percent is never passed
|
|
208
208
|
over just because nothing has gone through it in the last few minutes.
|
|
209
209
|
|
|
210
|
+
Binding is about what stops you soonest, though, not what stopping costs, and
|
|
211
|
+
those are different: a 5-hour window returns in hours, the weekly one in days.
|
|
212
|
+
So a window above 85 percent gets called out even when something shorter binds,
|
|
213
|
+
with its own reset time, because spending the weekly window to save a few turns
|
|
214
|
+
of the 5-hour one is a bad trade.
|
|
215
|
+
|
|
210
216
|
## When you keep typing
|
|
211
217
|
|
|
212
218
|
Every message sent while work is already running starts another turn, and each
|
|
@@ -467,6 +473,11 @@ Good enough to plan with, not a bill. The honest caveats:
|
|
|
467
473
|
to the meter rather than from an assumption about the clock.
|
|
468
474
|
- The turn cost behind "turns of headroom" is a median over at least five
|
|
469
475
|
turns, so one compaction cannot define your pace.
|
|
476
|
+
- What a point of a window costs is learned once from the best sample seen and
|
|
477
|
+
remembered, rather than re-derived each time from whatever slice is to hand.
|
|
478
|
+
A thin baseline prices a point badly and every correction built on it
|
|
479
|
+
inherits the error, which is how a window truly at 70 percent once came out
|
|
480
|
+
at 82.
|
|
470
481
|
- The cache only refreshes when Claude Code talks to the API, so after a gap it
|
|
471
482
|
can be hours old and its 5-hour window long since rolled over. Dropping that
|
|
472
483
|
window would hide the limit that actually stops short work, so it gets rebuilt
|
|
@@ -509,7 +520,7 @@ test/ node --test, no dependencies
|
|
|
509
520
|
node --test
|
|
510
521
|
```
|
|
511
522
|
|
|
512
|
-
|
|
523
|
+
190 tests over the pricing, the window arithmetic, plan and credit detection,
|
|
513
524
|
the status line, the before-prompt line, job forecasting, per-project
|
|
514
525
|
attribution, the CLI, packaging, and the settings save/restore.
|
|
515
526
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-usage-limits",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.1",
|
|
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",
|
|
@@ -171,6 +171,12 @@ Keep it to one line unless the work genuinely does not fit. The budget note is
|
|
|
171
171
|
a header, not a section, and it must never push the actual answer down the
|
|
172
172
|
page.
|
|
173
173
|
|
|
174
|
+
Which window binds is about what stops you soonest, not what stopping costs.
|
|
175
|
+
Those differ: a 5-hour window comes back in hours, the weekly one in days. So
|
|
176
|
+
when a window that is not binding sits near its wall, say so and weigh it. The
|
|
177
|
+
hook flags those. Running the weekly out to save a few turns of the 5-hour
|
|
178
|
+
window is a bad trade even though the 5-hour is what runs out first.
|
|
179
|
+
|
|
174
180
|
One thing to get right: **quote the binding window, not the roomiest one.**
|
|
175
181
|
Two windows run at once and they are rarely in the same place. The turns of
|
|
176
182
|
headroom and the reset time belong to whichever runs out first. Putting the
|
|
@@ -242,6 +242,17 @@ function briefText(parts) {
|
|
|
242
242
|
);
|
|
243
243
|
}
|
|
244
244
|
if (parts.othersSummary) sentences.push('Other windows: ' + parts.othersSummary + '.');
|
|
245
|
+
|
|
246
|
+
// A window that is not binding can still be the expensive one to exhaust.
|
|
247
|
+
if (parts.critical && parts.critical.length) {
|
|
248
|
+
for (const other of parts.critical) {
|
|
249
|
+
sentences.push(
|
|
250
|
+
'Note that ' + other.label + ' is at ' + other.percentUsed + '% and resets in ' +
|
|
251
|
+
other.resetsIn + ', so running that one out stops work for far longer than the ' +
|
|
252
|
+
'binding window would. Weigh it even though it is not what runs out first.'
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
245
256
|
if (parts.session) {
|
|
246
257
|
sentences.push(
|
|
247
258
|
'This session: ' + parts.session.turns + ' turns, ' +
|
|
@@ -295,6 +306,11 @@ async function run(now, hookInput) {
|
|
|
295
306
|
othersSummary: summariseOthers(data.windows, binding && binding.key),
|
|
296
307
|
sessions: data.sessions,
|
|
297
308
|
staleWindows: data.staleWindows,
|
|
309
|
+
critical: usage.criticalOthers(data.windows, binding && binding.key).map((w) => ({
|
|
310
|
+
label: w.label,
|
|
311
|
+
percentUsed: w.percentUsed,
|
|
312
|
+
resetsIn: Number.isFinite(w.msToReset) ? usage.formatDuration(w.msToReset) : 'an unknown time',
|
|
313
|
+
})),
|
|
298
314
|
snapshotAge: usage.formatDuration(data.snapshotAgeMs),
|
|
299
315
|
binding: binding
|
|
300
316
|
? {
|
|
@@ -334,6 +350,7 @@ async function run(now, hookInput) {
|
|
|
334
350
|
session: view.session,
|
|
335
351
|
rebuilt: Boolean(binding && binding.estimated),
|
|
336
352
|
staleWindows: view.staleWindows || 0,
|
|
353
|
+
critical: view.critical || [],
|
|
337
354
|
pointsSinceSnapshot: (binding && binding.pointsSinceSnapshot) || 0,
|
|
338
355
|
snapshotAge: view.snapshotAge,
|
|
339
356
|
pressure: pressure(binding, now, config, view.turnsLeft),
|
|
@@ -532,6 +532,43 @@ function shareOf(sessions, sessionId) {
|
|
|
532
532
|
return mine.share > 0 ? mine.share : 1 / sessions.length;
|
|
533
533
|
}
|
|
534
534
|
|
|
535
|
+
// What a point of a window costs is a property of the plan, not of the moment,
|
|
536
|
+
// so it should be learned once from a good sample rather than re-derived from
|
|
537
|
+
// whatever slice happens to be to hand. A thin baseline prices a point badly
|
|
538
|
+
// and every correction built on it inherits the error: a 24 minute old
|
|
539
|
+
// snapshot once turned a window truly at 70 per cent into a confident 82.
|
|
540
|
+
function calibrationFile() {
|
|
541
|
+
return path.join(configDir(), 'usage-limits-calibration.json');
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function readCalibration() {
|
|
545
|
+
try {
|
|
546
|
+
const parsed = JSON.parse(fs.readFileSync(calibrationFile(), 'utf8'));
|
|
547
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
548
|
+
} catch (err) {
|
|
549
|
+
return {};
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function writeCalibration(all) {
|
|
554
|
+
try {
|
|
555
|
+
fs.mkdirSync(path.dirname(calibrationFile()), { recursive: true });
|
|
556
|
+
fs.writeFileSync(calibrationFile(), JSON.stringify(all), 'utf8');
|
|
557
|
+
} catch (err) {
|
|
558
|
+
// Losing it costs accuracy on the next thin baseline, nothing more.
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// A sample is better when it rests on more turns. Percentages read in whole
|
|
563
|
+
// numbers, so a bigger percentage also divides more precisely.
|
|
564
|
+
function betterCalibration(current, candidate) {
|
|
565
|
+
if (!candidate || !Number.isFinite(candidate.usdPerPercent) || candidate.usdPerPercent <= 0) {
|
|
566
|
+
return current || null;
|
|
567
|
+
}
|
|
568
|
+
if (!current || !Number.isFinite(current.turns)) return candidate;
|
|
569
|
+
return candidate.turns > current.turns ? candidate : current;
|
|
570
|
+
}
|
|
571
|
+
|
|
535
572
|
// Everything the report needs about one limit window.
|
|
536
573
|
function buildWindow(spec, snapshot, events, now, options) {
|
|
537
574
|
const extra = options || {};
|
|
@@ -584,6 +621,8 @@ function buildWindow(spec, snapshot, events, now, options) {
|
|
|
584
621
|
pointsSinceSnapshot: 0,
|
|
585
622
|
// Set when spend since the snapshot could not be priced sensibly.
|
|
586
623
|
correctionUnreliable: false,
|
|
624
|
+
// The price-per-point this window derived from its own baseline.
|
|
625
|
+
calibration: null,
|
|
587
626
|
verdict: 'unknown',
|
|
588
627
|
};
|
|
589
628
|
|
|
@@ -618,7 +657,13 @@ function buildWindow(spec, snapshot, events, now, options) {
|
|
|
618
657
|
// divided by that, which is how a window truly at 55% got corrected all the
|
|
619
658
|
// way to a confident 100.
|
|
620
659
|
if (upTo.cost > 0 && after.cost > 0 && upTo.turns >= MIN_BASELINE_TURNS) {
|
|
621
|
-
const
|
|
660
|
+
const mine = { usdPerPercent: upTo.cost / rawPercent, turns: upTo.turns, percent: rawPercent };
|
|
661
|
+
const known = extra.knownCalibration;
|
|
662
|
+
// Trust the better-sampled of the two, whichever that is.
|
|
663
|
+
const chosen =
|
|
664
|
+
known && Number.isFinite(known.turns) && known.turns > upTo.turns ? known : mine;
|
|
665
|
+
window.calibration = mine;
|
|
666
|
+
const pricePerPoint = chosen.usdPerPercent;
|
|
622
667
|
sinceSnapshot = after.cost / pricePerPoint;
|
|
623
668
|
|
|
624
669
|
// Same rule as a rebuild: past this it is the calibration that is full,
|
|
@@ -685,6 +730,24 @@ function buildWindow(spec, snapshot, events, now, options) {
|
|
|
685
730
|
return window;
|
|
686
731
|
}
|
|
687
732
|
|
|
733
|
+
// Which window binds is about what stops you soonest. It says nothing about
|
|
734
|
+
// what stopping costs. Running out of a 5-hour window waits hours; running out
|
|
735
|
+
// of the weekly one waits days. So a weekly window near the wall is worth
|
|
736
|
+
// hearing about even while a shorter window binds.
|
|
737
|
+
const CRITICAL_PERCENT = 85;
|
|
738
|
+
|
|
739
|
+
function criticalOthers(windows, bindingKey, threshold) {
|
|
740
|
+
const limit = Number.isFinite(threshold) ? threshold : CRITICAL_PERCENT;
|
|
741
|
+
return (windows || []).filter(
|
|
742
|
+
(w) =>
|
|
743
|
+
w &&
|
|
744
|
+
w.key !== bindingKey &&
|
|
745
|
+
!w.stale &&
|
|
746
|
+
w.percentUsed !== null &&
|
|
747
|
+
w.percentUsed >= limit
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
|
|
688
751
|
// The window that will stop the work first.
|
|
689
752
|
function bindingWindow(windows) {
|
|
690
753
|
const known = windows.filter((w) => w.percentUsed !== null);
|
|
@@ -869,14 +932,17 @@ function reconstructWindow(spec, snapshot, events, now) {
|
|
|
869
932
|
|
|
870
933
|
// No snapshot at all means no windows, which is what tells the report to
|
|
871
934
|
// explain itself rather than print a table of dashes.
|
|
872
|
-
function buildWindows(utilization, events, now, fetchedAt) {
|
|
935
|
+
function buildWindows(utilization, events, now, fetchedAt, learned) {
|
|
873
936
|
if (!utilization) return [];
|
|
874
937
|
return WINDOWS.map((spec) => {
|
|
875
938
|
const snapshot = utilization[spec.key];
|
|
876
939
|
// The per-model weekly windows only exist on some plans.
|
|
877
940
|
if (spec.key !== 'five_hour' && spec.key !== 'seven_day' && !snapshot) return null;
|
|
878
941
|
|
|
879
|
-
const window = buildWindow(spec, snapshot, events, now, {
|
|
942
|
+
const window = buildWindow(spec, snapshot, events, now, {
|
|
943
|
+
fetchedAt,
|
|
944
|
+
knownCalibration: learned ? learned[spec.key] : null,
|
|
945
|
+
});
|
|
880
946
|
if (!window.stale) return window;
|
|
881
947
|
|
|
882
948
|
// Rolled over. Rebuild from local history rather than going blind on it.
|
|
@@ -916,7 +982,27 @@ async function report(now, options) {
|
|
|
916
982
|
const earliest = now - 8 * DAY;
|
|
917
983
|
const events = await readEvents(earliest);
|
|
918
984
|
|
|
919
|
-
const
|
|
985
|
+
const learned = readCalibration();
|
|
986
|
+
const windows = buildWindows(
|
|
987
|
+
base.utilization,
|
|
988
|
+
events,
|
|
989
|
+
now,
|
|
990
|
+
base.snapshotFetchedAt,
|
|
991
|
+
learned
|
|
992
|
+
);
|
|
993
|
+
|
|
994
|
+
// Keep the best sample seen so far, so a thin baseline never has to guess.
|
|
995
|
+
const updated = Object.assign({}, learned);
|
|
996
|
+
let changed = false;
|
|
997
|
+
for (const window of windows) {
|
|
998
|
+
if (!window.calibration) continue;
|
|
999
|
+
const best = betterCalibration(learned[window.key], window.calibration);
|
|
1000
|
+
if (best && best !== learned[window.key]) {
|
|
1001
|
+
updated[window.key] = best;
|
|
1002
|
+
changed = true;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
if (changed) writeCalibration(updated);
|
|
920
1006
|
|
|
921
1007
|
const recentEvents = events.filter((event) => event.at >= now - HOUR);
|
|
922
1008
|
const recent = totals(recentEvents);
|
|
@@ -1340,6 +1426,10 @@ module.exports = {
|
|
|
1340
1426
|
MIN_BASELINE_TURNS,
|
|
1341
1427
|
buildWindows,
|
|
1342
1428
|
bindingWindow,
|
|
1429
|
+
criticalOthers,
|
|
1430
|
+
betterCalibration,
|
|
1431
|
+
calibrationFile,
|
|
1432
|
+
CRITICAL_PERCENT,
|
|
1343
1433
|
dominantEffort,
|
|
1344
1434
|
typicalTurnCost,
|
|
1345
1435
|
activeSessions,
|