claude-usage-limits 1.15.0 → 1.16.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.15.0",
4
+ "version": "1.16.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",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "usage-limits",
3
- "version": "1.15.0",
3
+ "version": "1.16.1",
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.15.0",
3
+ "version": "1.16.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",
@@ -717,7 +717,15 @@ function briefText(parts) {
717
717
  // that is a real session: the Fable weekly hit 89 per cent, the line said
718
718
  // the budget was nearly gone, and the work ended with the five-hour window
719
719
  // at 46 and every other model untouched.
720
- escapeText && (parts.pressure === 'tight' || parts.pressure === 'gone')
720
+ // Only a MODEL switch survives to 'gone'. It retires a scoped window
721
+ // outright, so the budget really is still there. Effort does not: a
722
+ // cheaper turn against an exhausted window is still a turn you cannot
723
+ // take, and telling a session at 100 per cent that it is "not out of
724
+ // budget" because medium is cheaper than high would be this plugin
725
+ // producing the exact failure it exists to prevent, in reverse - refusing
726
+ // to stop at the one moment stopping is right.
727
+ escapeText &&
728
+ (parts.pressure === 'tight' || (parts.pressure === 'gone' && escape && escape.kind === 'model'))
721
729
  ? 'This window is nearly gone, but you are not out of budget and you must ' +
722
730
  'not stop as though you were. ' + escapeText + ' Do that, say in one ' +
723
731
  'line that you switched and why, and carry on with the whole request 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 = [];
@@ -3714,6 +3783,8 @@ module.exports = {
3714
3783
  preferLive,
3715
3784
  accountUuid,
3716
3785
  subagentTranscripts,
3786
+ liveAgents,
3787
+ AGENT_LIVE_MS,
3717
3788
  claudeTranscriptFiles,
3718
3789
  readClaudeEvents,
3719
3790
  SCAN_VERSION,
@@ -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,