claude-usage-limits 1.14.1 → 1.16.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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/skills/usage-limits/SKILL.md +17 -0
- package/skills/usage-limits/scripts/brief.js +24 -5
- package/skills/usage-limits/scripts/feed.js +16 -0
- package/skills/usage-limits/scripts/usage.js +99 -1
- package/skills/usage-limits/scripts/view.js +3 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "usage-limits",
|
|
3
3
|
"displayName": "Usage Limits",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.16.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",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "usage-limits",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.0",
|
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-usage-limits",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.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",
|
|
@@ -387,6 +387,23 @@ is. There are two, and only one of them is the account's budget.
|
|
|
387
387
|
this account. `node scripts/usage.js --recommend` names both levers with the
|
|
388
388
|
commands.
|
|
389
389
|
|
|
390
|
+
**The commands differ by host, and the wrong one does nothing.** `/model` and
|
|
391
|
+
`/effort` do not exist in Codex. Under Codex, change the model and effort for
|
|
392
|
+
the task in hand through Codex's own controls; `node scripts/lowpower.js on
|
|
393
|
+
--host codex --effort low --model <id>` only saves defaults for *new* sessions
|
|
394
|
+
and cannot change one already running. The budget line prints whichever
|
|
395
|
+
vocabulary the host it is running in actually understands, so use the command
|
|
396
|
+
it gives you rather than the one you remember.
|
|
397
|
+
|
|
398
|
+
**This is a lever you may pull yourself, not only an emergency exit.** Use it
|
|
399
|
+
whenever the current setting is dearer than the work in front of you needs,
|
|
400
|
+
without being asked and long before any window is tight: a mechanical rename,
|
|
401
|
+
a docs pass or a mass find-and-replace does not need the model and effort a
|
|
402
|
+
hard design decision does. Drop it for that stretch, say in one line that you
|
|
403
|
+
did and why, and put it back when the work gets hard again. An agent that only
|
|
404
|
+
ever reads this as a wall notice runs every trivial turn at the top setting and
|
|
405
|
+
then wonders where the window went.
|
|
406
|
+
|
|
390
407
|
The budget line does this arithmetic for you. Once the binding window is half
|
|
391
408
|
gone it says which lever applies, and at the wall it says outright that you are
|
|
392
409
|
not out of budget and must not stop as though you were. When it does, the
|
|
@@ -548,7 +548,13 @@ function briefText(parts) {
|
|
|
548
548
|
sentences.push(
|
|
549
549
|
'The effort setting is what is spending this: ' + bits.join(', and ') +
|
|
550
550
|
'. Keep it where the work genuinely needs the thinking and drop it where it ' +
|
|
551
|
-
'does not; it changes what every turn costs, not how many you get.'
|
|
551
|
+
'does not; it changes what every turn costs, not how many you get.' +
|
|
552
|
+
// The command, in the host's own vocabulary. /effort does not exist
|
|
553
|
+
// in Codex, and naming it there is telling Codex to do nothing while
|
|
554
|
+
// believing it acted.
|
|
555
|
+
(warning.cheaper && warning.cheaper.effort
|
|
556
|
+
? ' To drop it: ' + usage.levers(usage.currentHost()).effort(warning.cheaper.effort) + '.'
|
|
557
|
+
: '')
|
|
552
558
|
);
|
|
553
559
|
}
|
|
554
560
|
}
|
|
@@ -580,14 +586,27 @@ function briefText(parts) {
|
|
|
580
586
|
(escape.nextLabel
|
|
581
587
|
? 'After a switch the binding window would be ' + escape.nextLabel + ' at ' + escape.nextPercent + ' per cent. '
|
|
582
588
|
: '') +
|
|
583
|
-
|
|
589
|
+
// No command rather than a wrong one: the vocabulary differs by host,
|
|
590
|
+
// and "Use undefined" is worse than saying which lever it is and
|
|
591
|
+
// leaving the reader to reach for it.
|
|
592
|
+
(escape.command ? 'Use ' + escape.command + '.' : '')
|
|
584
593
|
: escape && escape.kind === 'effort'
|
|
585
594
|
? 'A model switch does not free this window - it follows the account - but effort does: ' +
|
|
586
595
|
escape.to + ' measured ' + (escape.multiple ? escape.multiple + 'x ' : '') + 'cheaper a turn than ' +
|
|
587
|
-
(escape.from || 'the current effort') + '.
|
|
596
|
+
(escape.from || 'the current effort') + '.' + (escape.command ? ' Use ' + escape.command + '.' : '')
|
|
588
597
|
: null;
|
|
598
|
+
// Not only an emergency exit. The same lever is the right one whenever the
|
|
599
|
+
// setting is dearer than the work in front of you needs - a mechanical edit
|
|
600
|
+
// does not need the model a hard design decision does. Say so, because an
|
|
601
|
+
// agent that only ever reads this as a wall notice will run every trivial
|
|
602
|
+
// turn at the top setting and then wonder where the window went.
|
|
603
|
+
const chooseText = escapeText
|
|
604
|
+
? ' You may make that change yourself, at any point and without being asked, ' +
|
|
605
|
+
'whenever the current setting is dearer than the work needs rather than only ' +
|
|
606
|
+
'when the window is nearly gone. Say in one line that you changed it and why.'
|
|
607
|
+
: '';
|
|
589
608
|
if (escapeText && parts.binding && Number.isFinite(parts.binding.percentUsed) && parts.binding.percentUsed >= HALF_SPENT) {
|
|
590
|
-
sentences.push(escapeText);
|
|
609
|
+
sentences.push(escapeText + chooseText);
|
|
591
610
|
}
|
|
592
611
|
|
|
593
612
|
// A window that is not binding can still be the expensive one to exhaust.
|
|
@@ -955,7 +974,7 @@ async function run(now, hookInput) {
|
|
|
955
974
|
othersSummary: summariseOthers(data.windows, binding && binding.key),
|
|
956
975
|
// The way out that is not stopping. Cached with the rest of the view
|
|
957
976
|
// because it is derived from the same one pass over the windows.
|
|
958
|
-
escape: usage.escapeRoute(data.windows, binding, data.effortWarning || null),
|
|
977
|
+
escape: usage.escapeRoute(data.windows, binding, data.effortWarning || null, usage.currentHost()),
|
|
959
978
|
// Every window, trimmed to the cacheable fields, so the corrected reading
|
|
960
979
|
// can be recorded for all three columns of the status line on a cache
|
|
961
980
|
// hit too. Recording the binding window alone left the other two at
|
|
@@ -238,10 +238,25 @@ function line(built, options) {
|
|
|
238
238
|
Number.isFinite(built.othersWorking) && built.othersWorking > 0
|
|
239
239
|
? bars.paint('+' + built.othersWorking + ' working', bars.THEME.claude, mode)
|
|
240
240
|
: '';
|
|
241
|
+
// Agents spend the same window and were the one thing on it with no voice.
|
|
242
|
+
// A fan-out can empty half a five-hour window in five minutes while the line
|
|
243
|
+
// shows one session working, so the count goes on the line whenever any are
|
|
244
|
+
// live - and it is dropped first when the terminal is too narrow, because a
|
|
245
|
+
// percentage the user cannot see is worse than a count they cannot see.
|
|
246
|
+
const agents =
|
|
247
|
+
built.agents && built.agents.running > 0
|
|
248
|
+
? bars.paint(
|
|
249
|
+
'+' + built.agents.running + ' agent' + (built.agents.running === 1 ? '' : 's') +
|
|
250
|
+
(built.agents.runs > 1 ? ' (' + built.agents.runs + ' runs)' : ''),
|
|
251
|
+
bars.THEME.claude,
|
|
252
|
+
mode
|
|
253
|
+
)
|
|
254
|
+
: '';
|
|
241
255
|
let text = '';
|
|
242
256
|
for (const attempt of attempts) {
|
|
243
257
|
const parts = built.rows.map((row) => segment(row, attempt.width, attempt.shorter));
|
|
244
258
|
if (others) parts.push(others);
|
|
259
|
+
if (agents && !attempt.shorter) parts.push(agents);
|
|
245
260
|
if (attempt.head) parts.unshift(head);
|
|
246
261
|
text = parts.join(attempt.gap || ' ');
|
|
247
262
|
if (bars.visibleWidth(text) <= columns) return text;
|
|
@@ -445,6 +460,7 @@ async function main(argv) {
|
|
|
445
460
|
.filter((row) => row.state === 'working' && row.sessionId !== mine).length;
|
|
446
461
|
const built = view.build({
|
|
447
462
|
now,
|
|
463
|
+
agents: usage.liveAgents(now),
|
|
448
464
|
utilization: collected.utilization,
|
|
449
465
|
fetchedAtMs: collected.snapshotFetchedAt,
|
|
450
466
|
source: collected.snapshotSource,
|
|
@@ -534,6 +534,75 @@ function subagentTranscripts(dir, since, depth) {
|
|
|
534
534
|
return files;
|
|
535
535
|
}
|
|
536
536
|
|
|
537
|
+
// The agents working right now, and what they have cost.
|
|
538
|
+
//
|
|
539
|
+
// Their spend has always been counted - the scan walks subagents/ and
|
|
540
|
+
// workflows/ and every token lands in the window totals - but nothing ever
|
|
541
|
+
// SHOWED them. So a display could say one session was working while eighteen
|
|
542
|
+
// agents underneath it spent two million tokens, and the only sign was the
|
|
543
|
+
// percentage moving for no visible reason. One workflow in this plugin's own
|
|
544
|
+
// development did exactly that twice in an afternoon.
|
|
545
|
+
//
|
|
546
|
+
// Cheap on purpose: it stats files under the session's own subagent
|
|
547
|
+
// directories and never parses one. The panel redraws every second.
|
|
548
|
+
const AGENT_LIVE_MS = 90 * 1000;
|
|
549
|
+
|
|
550
|
+
// Measured at about 95 ms on a machine with a few hundred agent transcripts.
|
|
551
|
+
// The panel redraws every second, so without this it would spend a tenth of
|
|
552
|
+
// every frame stat-ing files that cannot have changed much.
|
|
553
|
+
let agentMemo = null;
|
|
554
|
+
const AGENT_MEMO_MS = 2000;
|
|
555
|
+
|
|
556
|
+
function liveAgents(now, windowMs) {
|
|
557
|
+
const at = Number.isFinite(now) ? now : Date.now();
|
|
558
|
+
const within = Number.isFinite(windowMs) ? windowMs : AGENT_LIVE_MS;
|
|
559
|
+
if (agentMemo && agentMemo.within === within && at - agentMemo.at < AGENT_MEMO_MS) return agentMemo.value;
|
|
560
|
+
const root = path.join(configDir(), 'projects');
|
|
561
|
+
let projects = [];
|
|
562
|
+
try {
|
|
563
|
+
projects = fs.readdirSync(root, { withFileTypes: true });
|
|
564
|
+
} catch (err) {
|
|
565
|
+
return { running: 0, runs: 0, newestAt: null };
|
|
566
|
+
}
|
|
567
|
+
let running = 0;
|
|
568
|
+
const runs = new Set();
|
|
569
|
+
// projects/<project>/<sessionId>/subagents/[workflows/<run>/]agent-*.jsonl -
|
|
570
|
+
// the session id is a level the first version of this walked straight past,
|
|
571
|
+
// which is why it counted nothing on a machine with a hundred and fifty
|
|
572
|
+
// agent transcripts an hour old.
|
|
573
|
+
for (const project of projects) {
|
|
574
|
+
if (!project.isDirectory()) continue;
|
|
575
|
+
let sessions = [];
|
|
576
|
+
try {
|
|
577
|
+
sessions = fs.readdirSync(path.join(root, project.name), { withFileTypes: true });
|
|
578
|
+
} catch (err) {
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
for (const session of sessions) {
|
|
582
|
+
if (!session.isDirectory()) continue;
|
|
583
|
+
const dir = path.join(root, project.name, session.name, 'subagents');
|
|
584
|
+
for (const file of subagentTranscripts(dir, at - within, 3)) {
|
|
585
|
+
let stat;
|
|
586
|
+
try {
|
|
587
|
+
stat = fs.statSync(file);
|
|
588
|
+
} catch (err) {
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
if (at - stat.mtimeMs > within) continue;
|
|
592
|
+
running += 1;
|
|
593
|
+
// .../workflows/<run>/agent-x.jsonl - the run is what a person
|
|
594
|
+
// recognises, so it is counted as well as the agents. An agent that is
|
|
595
|
+
// not in a workflow sits directly in subagents/ and is its own run.
|
|
596
|
+
const parent = path.basename(path.dirname(file));
|
|
597
|
+
runs.add(parent === 'subagents' ? 'agent:' + path.basename(file) : parent);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
const value = { running, runs: runs.size, newestAt: running ? at : null };
|
|
602
|
+
agentMemo = { at, within, value };
|
|
603
|
+
return value;
|
|
604
|
+
}
|
|
605
|
+
|
|
537
606
|
function claudeTranscriptFiles(since) {
|
|
538
607
|
const root = path.join(configDir(), 'projects');
|
|
539
608
|
let dirs = [];
|
|
@@ -2104,8 +2173,32 @@ function criticalOthers(windows, bindingKey, threshold) {
|
|
|
2104
2173
|
// changes nothing and the lever is effort instead. Either way the point is the
|
|
2105
2174
|
// same - being at the wall is not the same as being out of budget, and an agent
|
|
2106
2175
|
// that cannot tell the difference stops early and calls it caution.
|
|
2107
|
-
|
|
2176
|
+
// The commands differ by host and getting them wrong is worse than saying
|
|
2177
|
+
// nothing: /model and /effort do not exist in Codex, and telling Codex to run
|
|
2178
|
+
// them is telling it to do nothing while believing it acted. Codex changes its
|
|
2179
|
+
// own model and effort through its own controls; the script can only save
|
|
2180
|
+
// defaults for NEW sessions, which is a different promise and is stated as one.
|
|
2181
|
+
function levers(which) {
|
|
2182
|
+
// `host` is the required module here, not a parameter - shadowing it was a
|
|
2183
|
+
// ReferenceError on the constant it owns.
|
|
2184
|
+
const codex = which === host.CODEX;
|
|
2185
|
+
return {
|
|
2186
|
+
model: (family) =>
|
|
2187
|
+
codex
|
|
2188
|
+
? "Codex's own model control (the script only saves defaults for new sessions: " +
|
|
2189
|
+
'node scripts/lowpower.js on --host codex --model <id>)'
|
|
2190
|
+
: '/model ' + (family || '<another model>') + ' (or node scripts/lowpower.js on --model <id>)',
|
|
2191
|
+
effort: (level) =>
|
|
2192
|
+
codex
|
|
2193
|
+
? "Codex's own effort control for this task (node scripts/lowpower.js on --host codex --effort " +
|
|
2194
|
+
(level || '<level>') + ' saves it for new sessions and cannot change one already running)'
|
|
2195
|
+
: '/effort ' + (level || '<level>'),
|
|
2196
|
+
};
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
function escapeRoute(windows, binding, effortNote, host) {
|
|
2108
2200
|
if (!binding || binding.percentUsed === null || binding.percentUsed === undefined) return null;
|
|
2201
|
+
const lever = levers(host || currentHost());
|
|
2109
2202
|
const live = (windows || []).filter(
|
|
2110
2203
|
(w) => w && w.percentUsed !== null && w.percentUsed !== undefined && !w.stale && w.key !== binding.key
|
|
2111
2204
|
);
|
|
@@ -2133,6 +2226,7 @@ function escapeRoute(windows, binding, effortNote) {
|
|
|
2133
2226
|
nextLabel: next ? next.label || next.key : null,
|
|
2134
2227
|
nextPercent: next ? Math.round(next.percentUsed) : null,
|
|
2135
2228
|
suggest: roomier && roomier.family ? roomier.family : null,
|
|
2229
|
+
command: lever.model(roomier && roomier.family ? roomier.family : null),
|
|
2136
2230
|
};
|
|
2137
2231
|
}
|
|
2138
2232
|
|
|
@@ -2144,6 +2238,7 @@ function escapeRoute(windows, binding, effortNote) {
|
|
|
2144
2238
|
from: effortNote.effort || null,
|
|
2145
2239
|
to: effortNote.cheaper.effort,
|
|
2146
2240
|
multiple: Number.isFinite(effortNote.cheaper.multiple) ? effortNote.cheaper.multiple : null,
|
|
2241
|
+
command: lever.effort(effortNote.cheaper.effort),
|
|
2147
2242
|
};
|
|
2148
2243
|
}
|
|
2149
2244
|
return null;
|
|
@@ -3688,6 +3783,8 @@ module.exports = {
|
|
|
3688
3783
|
preferLive,
|
|
3689
3784
|
accountUuid,
|
|
3690
3785
|
subagentTranscripts,
|
|
3786
|
+
liveAgents,
|
|
3787
|
+
AGENT_LIVE_MS,
|
|
3691
3788
|
claudeTranscriptFiles,
|
|
3692
3789
|
readClaudeEvents,
|
|
3693
3790
|
SCAN_VERSION,
|
|
@@ -3740,6 +3837,7 @@ module.exports = {
|
|
|
3740
3837
|
bindingWindow,
|
|
3741
3838
|
criticalOthers,
|
|
3742
3839
|
escapeRoute,
|
|
3840
|
+
levers,
|
|
3743
3841
|
betterCalibration,
|
|
3744
3842
|
calibrationForPlan,
|
|
3745
3843
|
stampPlan,
|
|
@@ -352,6 +352,9 @@ function build(input) {
|
|
|
352
352
|
rows,
|
|
353
353
|
fable,
|
|
354
354
|
hidden,
|
|
355
|
+
// The agents underneath this session. They spend the same window and had
|
|
356
|
+
// no voice on any display until now.
|
|
357
|
+
agents: opts.agents && Number.isFinite(opts.agents.running) ? opts.agents : { running: 0, runs: 0 },
|
|
355
358
|
model,
|
|
356
359
|
modelLabel: opts.modelName || bars.prettyModel(model),
|
|
357
360
|
effort,
|