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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "usage-limits",
3
3
  "displayName": "Usage Limits",
4
- "version": "1.14.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.14.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.14.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
- 'Use /model ' + (escape.suggest || '<another model>') + ' (or scripts/lowpower.js on --model <id>).'
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') + '. Use /effort ' + escape.to + '.'
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
- function escapeRoute(windows, binding, effortNote) {
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,