claude-usage-limits 1.1.3 → 1.2.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.
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "usage-limits",
|
|
3
3
|
"displayName": "Usage Limits",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.2.0",
|
|
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
|
@@ -167,6 +167,28 @@ node skills/usage-limits/scripts/usage.js
|
|
|
167
167
|
node skills/usage-limits/scripts/usage.js --json
|
|
168
168
|
```
|
|
169
169
|
|
|
170
|
+
## When you keep typing
|
|
171
|
+
|
|
172
|
+
Every message sent while work is already running starts another turn, and each
|
|
173
|
+
turn re-sends the whole conversation. Three follow-ups during one task can cost
|
|
174
|
+
more than the task did.
|
|
175
|
+
|
|
176
|
+
So when several additions arrive mid-task and the binding window is tight,
|
|
177
|
+
Claude says so once and keeps working:
|
|
178
|
+
|
|
179
|
+
> I have got all three. While the weekly window is this tight, sending them
|
|
180
|
+
> together costs a good deal less than one at a time, so I will fold these in
|
|
181
|
+
> and carry on.
|
|
182
|
+
|
|
183
|
+
It asks once, never repeatedly, and only when the budget is actually tight.
|
|
184
|
+
Asking someone to hold their thoughts when there is room to spare is rude for
|
|
185
|
+
no gain.
|
|
186
|
+
|
|
187
|
+
The important exclusion: it never discourages a correction, a stop, or a bug
|
|
188
|
+
report. Those are the messages that save the most work, and a rule that trains
|
|
189
|
+
people out of interrupting to say "that is wrong" costs far more than the turns
|
|
190
|
+
it saves. Only additive scope is worth batching.
|
|
191
|
+
|
|
170
192
|
## Credits, and what happens at the wall
|
|
171
193
|
|
|
172
194
|
The report says which of two things happens when the plan allowance runs out,
|
|
@@ -390,10 +412,17 @@ Good enough to plan with, not a bill. The honest caveats:
|
|
|
390
412
|
- A model released after this table was written is priced at its family's
|
|
391
413
|
average rate, and the report marks those rows with an asterisk rather than
|
|
392
414
|
passing the guess off as a published price.
|
|
393
|
-
- The cache only refreshes when Claude Code talks to the API, so after
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
415
|
+
- The cache only refreshes when Claude Code talks to the API, so after a gap it
|
|
416
|
+
can be hours old and its 5-hour window long since rolled over. Dropping that
|
|
417
|
+
window would hide the limit that actually stops short work, so it gets rebuilt
|
|
418
|
+
from your transcripts instead: whatever was spent inside the window the stale
|
|
419
|
+
reading describes equalled its percentage, and that price per point still
|
|
420
|
+
values the window running now. Rebuilt figures are written `~41%` in the
|
|
421
|
+
report and "about 41%" in the before-prompt line, and they say how old the
|
|
422
|
+
snapshot is so you can run `/usage` and replace the estimate with a reading.
|
|
423
|
+
- A rebuilt figure only counts what this machine did. If you also worked on
|
|
424
|
+
another device it reads low, which is the dangerous direction, so treat it as
|
|
425
|
+
a floor until you refresh.
|
|
397
426
|
|
|
398
427
|
[how-it-works.md](skills/usage-limits/references/how-it-works.md) has the field
|
|
399
428
|
names, the formulas, and the rest of it.
|
|
@@ -419,7 +448,7 @@ test/ node --test, no dependencies
|
|
|
419
448
|
node --test
|
|
420
449
|
```
|
|
421
450
|
|
|
422
|
-
|
|
451
|
+
130 tests over the pricing, the window arithmetic, plan and credit detection,
|
|
423
452
|
the status line, the before-prompt line, job forecasting, per-project
|
|
424
453
|
attribution, the CLI, packaging, and the settings save/restore.
|
|
425
454
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-usage-limits",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
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",
|
|
@@ -78,6 +78,37 @@ abandoned mid-edit.
|
|
|
78
78
|
**The window resets first.** If the reset lands before the budget runs out,
|
|
79
79
|
the limit is not the constraint. Say that and stop optimising for it.
|
|
80
80
|
|
|
81
|
+
## When messages stack up
|
|
82
|
+
|
|
83
|
+
Every message sent while work is already running starts another turn, and every
|
|
84
|
+
turn re-sends the whole conversation. Three follow-ups during one task can cost
|
|
85
|
+
more than the task did.
|
|
86
|
+
|
|
87
|
+
So when several additions arrive while you are still working and the binding
|
|
88
|
+
window is tight, say it once, then keep going:
|
|
89
|
+
|
|
90
|
+
> I have got all three. While the weekly window is this tight, sending them
|
|
91
|
+
> together costs a good deal less than one at a time, so I will fold these in
|
|
92
|
+
> and carry on. Send the rest in one go if you can.
|
|
93
|
+
|
|
94
|
+
Four rules keep that from being obnoxious, and they matter more than the saving.
|
|
95
|
+
|
|
96
|
+
**Ask once per stretch of work.** A second reminder costs more goodwill than
|
|
97
|
+
the tokens it saves.
|
|
98
|
+
|
|
99
|
+
**Only when the budget is actually tight.** With room to spare, asking someone
|
|
100
|
+
to hold their thoughts is rude for no gain. Say nothing.
|
|
101
|
+
|
|
102
|
+
**Never discourage a correction, a stop, or a bug report.** Those are the
|
|
103
|
+
messages that save the most work. Someone interrupting to say the approach is
|
|
104
|
+
wrong has just paid for their own interruption many times over, and a rule that
|
|
105
|
+
trains people out of that is far more expensive than the turns it saves. Only
|
|
106
|
+
additive scope, the "also do X" and "and can you Y", is worth batching.
|
|
107
|
+
|
|
108
|
+
**Never make it about you.** The cost lands on their limit, not yours. Frame it
|
|
109
|
+
as their budget, offer the saving, and let them decide. Then work. Do not stop
|
|
110
|
+
to negotiate about whether to stop.
|
|
111
|
+
|
|
81
112
|
## Credits
|
|
82
113
|
|
|
83
114
|
The `Credits` line in the report says what actually happens at the limit, and
|
|
@@ -43,10 +43,25 @@ function cacheFile() {
|
|
|
43
43
|
// ever got a hit and both paid for a full scan each time.
|
|
44
44
|
const KEEP_SESSIONS = 5;
|
|
45
45
|
|
|
46
|
+
// A cache written before slots were keyed by session keeps its fields at the
|
|
47
|
+
// top level, so upgrading would carry "at", "turnsLeft", "session" and
|
|
48
|
+
// "sessionId" forward as if each were a session, crowding out real slots and
|
|
49
|
+
// quietly undoing the per-session caching. Anything that is not a slot goes.
|
|
50
|
+
function keepSlots(parsed) {
|
|
51
|
+
if (!parsed || typeof parsed !== 'object') return {};
|
|
52
|
+
const slots = {};
|
|
53
|
+
for (const key of Object.keys(parsed)) {
|
|
54
|
+
const value = parsed[key];
|
|
55
|
+
if (value && typeof value === 'object' && Number.isFinite(value.at)) {
|
|
56
|
+
slots[key] = value;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return slots;
|
|
60
|
+
}
|
|
61
|
+
|
|
46
62
|
function readCache() {
|
|
47
63
|
try {
|
|
48
|
-
|
|
49
|
-
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
64
|
+
return keepSlots(JSON.parse(fs.readFileSync(cacheFile(), 'utf8')));
|
|
50
65
|
} catch (err) {
|
|
51
66
|
return {};
|
|
52
67
|
}
|
|
@@ -160,9 +175,9 @@ function sessionSpend(events, sessionId) {
|
|
|
160
175
|
|
|
161
176
|
function describeWindow(window) {
|
|
162
177
|
if (!window) return null;
|
|
163
|
-
return window.
|
|
164
|
-
|
|
165
|
-
|
|
178
|
+
if (window.stale) return window.label + ' rolling over';
|
|
179
|
+
const about = window.estimated ? ' about ' : ' ';
|
|
180
|
+
return window.label + about + window.percentUsed + '%';
|
|
166
181
|
}
|
|
167
182
|
|
|
168
183
|
// Everything except the window that will actually stop the work.
|
|
@@ -191,6 +206,12 @@ function briefText(parts) {
|
|
|
191
206
|
? '[usage-limits] binding window is ' + bound.join(', ') + '.'
|
|
192
207
|
: '[usage-limits] no usable window reading.'
|
|
193
208
|
);
|
|
209
|
+
if (parts.rebuilt) {
|
|
210
|
+
sentences.push(
|
|
211
|
+
'That figure was rebuilt from local history because the ' +
|
|
212
|
+
'snapshot is ' + parts.snapshotAge + ' old; run /usage to refresh it.'
|
|
213
|
+
);
|
|
214
|
+
}
|
|
194
215
|
if (parts.othersSummary) sentences.push('Other windows: ' + parts.othersSummary + '.');
|
|
195
216
|
if (parts.session) {
|
|
196
217
|
sentences.push(
|
|
@@ -203,7 +224,9 @@ function briefText(parts) {
|
|
|
203
224
|
parts.pressure === 'tight' || parts.pressure === 'gone'
|
|
204
225
|
? 'Open your reply with one line on where this leaves the budget, then say ' +
|
|
205
226
|
'what you will do now and what you will leave for after the reset. ' +
|
|
206
|
-
'Do not start work that clearly will not finish.'
|
|
227
|
+
'Do not start work that clearly will not finish. If several additions ' +
|
|
228
|
+
'arrive while you are working, say once that sending them together ' +
|
|
229
|
+
'costs less, then carry on; never say it about a correction or a stop.'
|
|
207
230
|
: 'Open your reply with one short line stating this and confirming the ' +
|
|
208
231
|
'request fits, then get on with the work. Keep it to a single line.';
|
|
209
232
|
|
|
@@ -225,38 +248,50 @@ async function run(now, hookInput) {
|
|
|
225
248
|
if (!base.utilization) return '';
|
|
226
249
|
|
|
227
250
|
const sessionId = hookInput && hookInput.session_id ? hookInput.session_id : null;
|
|
228
|
-
|
|
229
|
-
// The percentages are cheap: one small file, no transcripts.
|
|
230
|
-
const cheap = usage.buildWindows(base.utilization, [], now);
|
|
231
|
-
if (!cheap.length) return '';
|
|
232
|
-
|
|
233
251
|
const all = readCache();
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
let turnsLeft = cached ? cached.turnsLeft : null;
|
|
237
|
-
let session = cached ? cached.session : null;
|
|
238
|
-
let windows = cheap;
|
|
252
|
+
let view = pickCached(all, sessionId, now, config.cacheSeconds * SECOND);
|
|
239
253
|
|
|
240
|
-
|
|
254
|
+
// Everything shown has to come from one pass. Deriving the turns from a
|
|
255
|
+
// full scan and the binding window from somewhere cheaper is how the two
|
|
256
|
+
// end up describing different windows.
|
|
257
|
+
if (!view || !view.binding) {
|
|
241
258
|
const events = await usage.readEvents(now - 8 * DAY);
|
|
242
|
-
windows = usage.buildWindows(base.utilization, events, now);
|
|
259
|
+
const windows = usage.buildWindows(base.utilization, events, now);
|
|
243
260
|
const binding = usage.bindingWindow(windows);
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
261
|
+
view = {
|
|
262
|
+
at: now,
|
|
263
|
+
turnsLeft: binding && Number.isFinite(binding.turnsLeft) ? binding.turnsLeft : null,
|
|
264
|
+
session: sessionSpend(events, sessionId),
|
|
265
|
+
othersSummary: summariseOthers(windows, binding && binding.key),
|
|
266
|
+
binding: binding
|
|
267
|
+
? {
|
|
268
|
+
key: binding.key,
|
|
269
|
+
label: binding.label,
|
|
270
|
+
percentUsed: binding.percentUsed,
|
|
271
|
+
stale: binding.stale,
|
|
272
|
+
estimated: binding.estimated,
|
|
273
|
+
resetsAt: binding.resetsAt,
|
|
274
|
+
verdict: binding.verdict,
|
|
275
|
+
windowStart: binding.windowStart,
|
|
276
|
+
spanMs: binding.spanMs,
|
|
277
|
+
}
|
|
278
|
+
: null,
|
|
279
|
+
};
|
|
280
|
+
writeCache(mergeCache(all, sessionId, view, KEEP_SESSIONS));
|
|
247
281
|
}
|
|
248
282
|
|
|
249
|
-
const binding =
|
|
250
|
-
|
|
283
|
+
const binding = view.binding;
|
|
251
284
|
return briefText({
|
|
252
285
|
binding,
|
|
253
|
-
othersSummary:
|
|
254
|
-
turnsLeft,
|
|
286
|
+
othersSummary: view.othersSummary,
|
|
287
|
+
turnsLeft: view.turnsLeft,
|
|
255
288
|
resetsIn:
|
|
256
|
-
binding && !binding.stale && binding.
|
|
257
|
-
? usage.formatDuration(binding.
|
|
289
|
+
binding && !binding.stale && Number.isFinite(binding.resetsAt)
|
|
290
|
+
? usage.formatDuration(binding.resetsAt - now)
|
|
258
291
|
: null,
|
|
259
|
-
session,
|
|
292
|
+
session: view.session,
|
|
293
|
+
rebuilt: Boolean(binding && binding.estimated),
|
|
294
|
+
snapshotAge: usage.formatDuration(base.snapshotAgeMs),
|
|
260
295
|
pressure: pressure(binding, now, config),
|
|
261
296
|
});
|
|
262
297
|
}
|
|
@@ -285,6 +320,7 @@ module.exports = {
|
|
|
285
320
|
summariseOthers,
|
|
286
321
|
briefText,
|
|
287
322
|
settings,
|
|
323
|
+
keepSlots,
|
|
288
324
|
pickCached,
|
|
289
325
|
mergeCache,
|
|
290
326
|
KEEP_SESSIONS,
|
|
@@ -470,12 +470,17 @@ function forecastWindow(window, turns, rates) {
|
|
|
470
470
|
}
|
|
471
471
|
|
|
472
472
|
// Everything the report needs about one limit window.
|
|
473
|
-
function buildWindow(spec, snapshot, events, now) {
|
|
473
|
+
function buildWindow(spec, snapshot, events, now, options) {
|
|
474
|
+
const extra = options || {};
|
|
474
475
|
const percent =
|
|
475
476
|
snapshot && typeof snapshot.utilization === 'number' ? snapshot.utilization : null;
|
|
476
477
|
const resetsAt = snapshot && snapshot.resets_at ? Date.parse(snapshot.resets_at) : null;
|
|
477
478
|
const hasReset = Number.isFinite(resetsAt);
|
|
478
|
-
const start =
|
|
479
|
+
const start = Number.isFinite(extra.windowStart)
|
|
480
|
+
? extra.windowStart
|
|
481
|
+
: hasReset
|
|
482
|
+
? resetsAt - spec.span
|
|
483
|
+
: now - spec.span;
|
|
479
484
|
|
|
480
485
|
const inWindow = events.filter((event) => event.at >= start && event.at <= now);
|
|
481
486
|
const spent = totals(inWindow);
|
|
@@ -508,6 +513,9 @@ function buildWindow(spec, snapshot, events, now) {
|
|
|
508
513
|
headroomMs: null,
|
|
509
514
|
coarse: false,
|
|
510
515
|
stale: false,
|
|
516
|
+
// True when the percentage was rebuilt from local history because the
|
|
517
|
+
// snapshot had gone stale, rather than read from the snapshot itself.
|
|
518
|
+
estimated: Boolean(extra.estimated),
|
|
511
519
|
verdict: 'unknown',
|
|
512
520
|
};
|
|
513
521
|
|
|
@@ -667,6 +675,39 @@ function collect(now) {
|
|
|
667
675
|
};
|
|
668
676
|
}
|
|
669
677
|
|
|
678
|
+
// A snapshot only refreshes when Claude Code talks to the API, so after a
|
|
679
|
+
// gap it can be hours old and its 5-hour window long since rolled over.
|
|
680
|
+
// Dropping that window loses the limit that actually stops short work, so
|
|
681
|
+
// rebuild it from the transcripts instead.
|
|
682
|
+
//
|
|
683
|
+
// The trick is that the stale reading is still a usable calibration: whatever
|
|
684
|
+
// was spent inside the window it describes equalled its percentage. That
|
|
685
|
+
// dollars-per-point figure is a property of the plan, not of the moment, so it
|
|
686
|
+
// still prices the window running now.
|
|
687
|
+
function reconstructWindow(spec, snapshot, events, now) {
|
|
688
|
+
if (!snapshot || typeof snapshot.utilization !== 'number') return null;
|
|
689
|
+
if (snapshot.utilization <= 0) return null;
|
|
690
|
+
|
|
691
|
+
const resetsAt = snapshot.resets_at ? Date.parse(snapshot.resets_at) : null;
|
|
692
|
+
if (!Number.isFinite(resetsAt) || resetsAt > now) return null;
|
|
693
|
+
|
|
694
|
+
const pastStart = resetsAt - spec.span;
|
|
695
|
+
const past = totals(events.filter((e) => e.at >= pastStart && e.at <= resetsAt));
|
|
696
|
+
if (past.cost <= 0) return null;
|
|
697
|
+
|
|
698
|
+
const usdPerPercent = past.cost / snapshot.utilization;
|
|
699
|
+
const liveStart = now - spec.span;
|
|
700
|
+
const live = totals(events.filter((e) => e.at >= liveStart && e.at <= now));
|
|
701
|
+
|
|
702
|
+
return {
|
|
703
|
+
percentUsed: Math.min(100, Math.round(live.cost / usdPerPercent)),
|
|
704
|
+
usdPerPercent,
|
|
705
|
+
spentUSD: live.cost,
|
|
706
|
+
turns: live.turns,
|
|
707
|
+
windowStart: liveStart,
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
|
|
670
711
|
// No snapshot at all means no windows, which is what tells the report to
|
|
671
712
|
// explain itself rather than print a table of dashes.
|
|
672
713
|
function buildWindows(utilization, events, now) {
|
|
@@ -675,7 +716,20 @@ function buildWindows(utilization, events, now) {
|
|
|
675
716
|
const snapshot = utilization[spec.key];
|
|
676
717
|
// The per-model weekly windows only exist on some plans.
|
|
677
718
|
if (spec.key !== 'five_hour' && spec.key !== 'seven_day' && !snapshot) return null;
|
|
678
|
-
|
|
719
|
+
|
|
720
|
+
const window = buildWindow(spec, snapshot, events, now);
|
|
721
|
+
if (!window.stale) return window;
|
|
722
|
+
|
|
723
|
+
// Rolled over. Rebuild from local history rather than going blind on it.
|
|
724
|
+
const rebuilt = reconstructWindow(spec, snapshot, events, now);
|
|
725
|
+
if (!rebuilt) return window;
|
|
726
|
+
return buildWindow(
|
|
727
|
+
spec,
|
|
728
|
+
{ utilization: rebuilt.percentUsed, resets_at: null },
|
|
729
|
+
events,
|
|
730
|
+
now,
|
|
731
|
+
{ estimated: true, windowStart: rebuilt.windowStart }
|
|
732
|
+
);
|
|
679
733
|
}).filter(Boolean);
|
|
680
734
|
}
|
|
681
735
|
|
|
@@ -842,7 +896,11 @@ function render(data) {
|
|
|
842
896
|
lines.push(
|
|
843
897
|
' ' + pad(window.label, 15) +
|
|
844
898
|
padLeft(
|
|
845
|
-
window.stale
|
|
899
|
+
window.stale
|
|
900
|
+
? 'stale'
|
|
901
|
+
: window.percentUsed === null
|
|
902
|
+
? '-'
|
|
903
|
+
: (window.estimated ? '~' : '') + window.percentUsed + '%',
|
|
846
904
|
6
|
|
847
905
|
) +
|
|
848
906
|
padLeft(formatDuration(window.msToReset), 12) +
|
|
@@ -909,6 +967,15 @@ function render(data) {
|
|
|
909
967
|
}
|
|
910
968
|
lines.push(' Measured ' + formatCount(data.measuredTurns) + ' turns of local transcript');
|
|
911
969
|
|
|
970
|
+
if (data.windows.some((window) => window.estimated)) {
|
|
971
|
+
lines.push(
|
|
972
|
+
' Note ~ means the snapshot had gone stale and that window was rebuilt'
|
|
973
|
+
);
|
|
974
|
+
lines.push(
|
|
975
|
+
' from local history. Run /usage to replace it with a real reading.'
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
|
|
912
979
|
if (data.binding && data.binding.coarse) {
|
|
913
980
|
lines.push(' Note the meter reads in whole percent, so a low reading is a wide bracket');
|
|
914
981
|
}
|
|
@@ -1064,6 +1131,7 @@ module.exports = {
|
|
|
1064
1131
|
eventFrom,
|
|
1065
1132
|
readEvents,
|
|
1066
1133
|
buildWindow,
|
|
1134
|
+
reconstructWindow,
|
|
1067
1135
|
buildWindows,
|
|
1068
1136
|
bindingWindow,
|
|
1069
1137
|
dominantEffort,
|