claude-usage-limits 1.2.0 → 1.3.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.2.0",
4
+ "version": "1.3.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,21 @@ node skills/usage-limits/scripts/usage.js
167
167
  node skills/usage-limits/scripts/usage.js --json
168
168
  ```
169
169
 
170
+ ## Which limit it watches
171
+
172
+ Two windows run at once and the 5-hour one is usually what actually stops you,
173
+ so it gets picked whenever it is tighter, and it wins a tie against the weekly
174
+ window because the shorter window is the one hit first in practice.
175
+
176
+ It is not forced, though. When the weekly window is genuinely the wall, at 99
177
+ percent with minutes left, that is what gets reported. Forcing the 5-hour there
178
+ would hide the limit about to stop the work, which is the same failure as
179
+ ignoring it.
180
+
181
+ A window with no recent spend to measure is ranked by how full it is rather
182
+ than being skipped, so a 5-hour window sitting at 95 percent is never passed
183
+ over just because nothing has gone through it in the last few minutes.
184
+
170
185
  ## When you keep typing
171
186
 
172
187
  Every message sent while work is already running starts another turn, and each
@@ -296,6 +311,7 @@ minute, so it costs about 400ms cold and 120ms warm.
296
311
  | `USAGE_LIMITS_FLOOR` | 40 | Below this, pace is ignored. |
297
312
  | `USAGE_LIMITS_AHEAD` | 15 | Points ahead of pace that count as burning fast. |
298
313
  | `USAGE_LIMITS_CACHE` | 60 | Seconds the measured half stays good for. |
314
+ | `USAGE_LIMITS_FEW_TURNS` | 20 | Turn count at or below which the budget counts as tight. |
299
315
 
300
316
  ## What would this job cost
301
317
 
@@ -448,7 +464,7 @@ test/ node --test, no dependencies
448
464
  node --test
449
465
  ```
450
466
 
451
- 130 tests over the pricing, the window arithmetic, plan and credit detection,
467
+ 142 tests over the pricing, the window arithmetic, plan and credit detection,
452
468
  the status line, the before-prompt line, job forecasting, per-project
453
469
  attribution, the CLI, packaging, and the settings save/restore.
454
470
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-usage-limits",
3
- "version": "1.2.0",
3
+ "version": "1.3.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",
@@ -28,6 +28,8 @@ const DEFAULTS = {
28
28
  // How long the measured part stays good for. Prompts often arrive in
29
29
  // bursts, and a transcript scan per prompt would be wasteful.
30
30
  cacheSeconds: 60,
31
+ // Few enough turns that the count itself is the warning.
32
+ fewTurns: 20,
31
33
  };
32
34
 
33
35
  function configDir() {
@@ -104,6 +106,7 @@ function settings() {
104
106
  floor: number(env.USAGE_LIMITS_FLOOR, DEFAULTS.floor),
105
107
  ahead: number(env.USAGE_LIMITS_AHEAD, DEFAULTS.ahead),
106
108
  cacheSeconds: number(env.USAGE_LIMITS_CACHE, DEFAULTS.cacheSeconds),
109
+ fewTurns: number(env.USAGE_LIMITS_FEW_TURNS, DEFAULTS.fewTurns),
107
110
  };
108
111
  }
109
112
 
@@ -117,16 +120,32 @@ function aheadOfPace(window, now) {
117
120
  }
118
121
 
119
122
  // Not whether to speak, which is always, but how hard to lean on it.
120
- function pressure(window, now, config) {
123
+ // Not whether to speak, which is always, but how hard to lean on it.
124
+ function pressure(window, now, config, turnsLeft) {
121
125
  if (!window || window.percentUsed === null || window.stale) return 'unknown';
122
- if (window.verdict === 'exhausted') return 'gone';
126
+ if (window.verdict === 'exhausted' || window.percentUsed >= 100) return 'gone';
123
127
  if (window.verdict === 'runs-out') return 'tight';
124
- if (window.percentUsed >= config.near) return 'tight';
125
128
 
126
- const lead = aheadOfPace(window, now);
127
- if (window.percentUsed >= config.floor && lead !== null && lead >= config.ahead) {
129
+ // A rebuilt figure only counts this machine, so it reads low. React to it
130
+ // sooner than to a figure the API actually reported.
131
+ const near = window.estimated ? Math.min(config.near, 70) : config.near;
132
+ if (window.percentUsed >= near) return 'tight';
133
+
134
+ // Turns are the number the work is planned in, so a short count is tight
135
+ // whatever the percentage says.
136
+ if (Number.isFinite(turnsLeft) && turnsLeft <= config.fewTurns) {
128
137
  return 'tight';
129
138
  }
139
+
140
+ // Pace only means something for a window with a real start. A rebuilt one
141
+ // is anchored at now minus its span, so it is always "fully elapsed" and
142
+ // the comparison can never fire.
143
+ if (!window.estimated) {
144
+ const lead = aheadOfPace(window, now);
145
+ if (window.percentUsed >= config.floor && lead !== null && lead >= config.ahead) {
146
+ return 'tight';
147
+ }
148
+ }
130
149
  return 'roomy';
131
150
  }
132
151
 
@@ -292,7 +311,7 @@ async function run(now, hookInput) {
292
311
  session: view.session,
293
312
  rebuilt: Boolean(binding && binding.estimated),
294
313
  snapshotAge: usage.formatDuration(base.snapshotAgeMs),
295
- pressure: pressure(binding, now, config),
314
+ pressure: pressure(binding, now, config, view.turnsLeft),
296
315
  });
297
316
  }
298
317
 
@@ -571,11 +571,27 @@ function bindingWindow(windows) {
571
571
  const live = fresh.length ? fresh : known;
572
572
  if (!live.length) return null;
573
573
 
574
- const measured = live.filter((w) => w.headroomMs !== null);
575
- if (measured.length) {
576
- return measured.reduce((worst, w) => (w.headroomMs < worst.headroomMs ? w : worst));
577
- }
578
- return live.reduce((worst, w) => (w.percentUsed > worst.percentUsed ? w : worst));
574
+ // How soon this window stops the work. A window with no pace estimate is
575
+ // ranked by how full it is instead, because a nearly full window must never
576
+ // be passed over merely because nothing has been spent in it lately.
577
+ const soonest = (w) => {
578
+ if (Number.isFinite(w.headroomMs)) return w.headroomMs;
579
+ return w.percentUsed >= 90 ? 0 : Infinity;
580
+ };
581
+
582
+ return live.reduce((best, w) => {
583
+ const mine = soonest(w);
584
+ const theirs = soonest(best);
585
+ if (mine !== theirs) return mine < theirs ? w : best;
586
+
587
+ // Equally urgent: the shorter window is the one hit first in practice, so
588
+ // the 5-hour limit wins a tie against the weekly one.
589
+ const myspan = Number.isFinite(w.spanMs) ? w.spanMs : Infinity;
590
+ const theirspan = Number.isFinite(best.spanMs) ? best.spanMs : Infinity;
591
+ if (myspan !== theirspan) return myspan < theirspan ? w : best;
592
+
593
+ return w.percentUsed > best.percentUsed ? w : best;
594
+ });
579
595
  }
580
596
 
581
597
  function formatDuration(ms) {