claude-usage-limits 1.5.1 → 1.5.2

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.5.1",
4
+ "version": "1.5.2",
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-usage-limits",
3
- "version": "1.5.1",
3
+ "version": "1.5.2",
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",
@@ -195,7 +195,7 @@ function sessionSpend(events, sessionId) {
195
195
  function describeWindow(window) {
196
196
  if (!window) return null;
197
197
  if (window.stale) return window.label + ' rolling over';
198
- const about = window.estimated ? ' about ' : ' ';
198
+ const about = window.estimated || window.adjusted ? ' about ' : ' ';
199
199
  return window.label + about + window.percentUsed + '%';
200
200
  }
201
201
 
@@ -237,6 +237,11 @@ function briefText(parts) {
237
237
  'That figure was rebuilt from local history because the ' +
238
238
  'snapshot is ' + parts.snapshotAge + ' old; run /usage to refresh it.'
239
239
  );
240
+ } else if (parts.pointsSinceSnapshot) {
241
+ sentences.push(
242
+ 'That includes about ' + parts.pointsSinceSnapshot + ' points spent since the ' +
243
+ 'snapshot was taken ' + parts.snapshotAge + ' ago, which it does not know about yet.'
244
+ );
240
245
  } else if (parts.staleWindows) {
241
246
  // Do not quietly carry on with a window we know is wrong and could not
242
247
  // rebuild. Say it is unknown and point at the one command that fixes it.
@@ -306,6 +311,8 @@ async function run(now, hookInput) {
306
311
  percentUsed: binding.percentUsed,
307
312
  stale: binding.stale,
308
313
  estimated: binding.estimated,
314
+ adjusted: binding.adjusted,
315
+ pointsSinceSnapshot: binding.pointsSinceSnapshot,
309
316
  resetsAt: binding.resetsAt,
310
317
  verdict: binding.verdict,
311
318
  windowStart: binding.windowStart,
@@ -334,6 +341,7 @@ async function run(now, hookInput) {
334
341
  session: view.session,
335
342
  rebuilt: Boolean(binding && binding.estimated),
336
343
  staleWindows: view.staleWindows || 0,
344
+ pointsSinceSnapshot: (binding && binding.pointsSinceSnapshot) || 0,
337
345
  snapshotAge: usage.formatDuration(base.snapshotAgeMs),
338
346
  pressure: pressure(binding, now, config, view.turnsLeft),
339
347
  });
@@ -535,7 +535,7 @@ function shareOf(sessions, sessionId) {
535
535
  // Everything the report needs about one limit window.
536
536
  function buildWindow(spec, snapshot, events, now, options) {
537
537
  const extra = options || {};
538
- const percent =
538
+ const rawPercent =
539
539
  snapshot && typeof snapshot.utilization === 'number' ? snapshot.utilization : null;
540
540
  const resetsAt = snapshot && snapshot.resets_at ? Date.parse(snapshot.resets_at) : null;
541
541
  const hasReset = Number.isFinite(resetsAt);
@@ -556,8 +556,8 @@ function buildWindow(spec, snapshot, events, now, options) {
556
556
  const window = {
557
557
  key: spec.key,
558
558
  label: spec.label,
559
- percentUsed: percent,
560
- percentLeft: percent === null ? null : Math.max(0, 100 - percent),
559
+ percentUsed: rawPercent,
560
+ percentLeft: rawPercent === null ? null : Math.max(0, 100 - rawPercent),
561
561
  resetsAt: hasReset ? resetsAt : null,
562
562
  msToReset: hasReset ? resetsAt - now : null,
563
563
  windowStart: start,
@@ -579,13 +579,50 @@ function buildWindow(spec, snapshot, events, now, options) {
579
579
  // True when the percentage was rebuilt from local history because the
580
580
  // snapshot had gone stale, rather than read from the snapshot itself.
581
581
  estimated: Boolean(extra.estimated),
582
+ // True when spend since the snapshot was added to its reading.
583
+ adjusted: false,
584
+ pointsSinceSnapshot: 0,
582
585
  verdict: 'unknown',
583
586
  };
584
587
 
588
+ // Worked out before anything reads it: the adjustment below and the verdict
589
+ // chain both branch on whether this window has already rolled over.
590
+ window.stale = hasReset && resetsAt <= now;
591
+
585
592
  // Calibrate against this account: how many dollars of measured traffic
586
593
  // moved the meter one point. A rebuilt window hands its own figure in,
587
594
  // because rounding to 0% would otherwise leave it unpriced and drop it out
588
595
  // of the binding choice just after a reset.
596
+ // The snapshot is a reading from a moment in the past, not from now. Spend
597
+ // since then is real and uncounted, and with several sessions running it adds
598
+ // up fast: forty points went missing in nine minutes once, so a window that
599
+ // was truly at 88% was reported at 49%. Calibrate on spend up to the reading
600
+ // only, or the very spend being accounted for inflates the price per point
601
+ // and shrinks its own correction.
602
+ let percent = rawPercent;
603
+ let sinceSnapshot = 0;
604
+ if (
605
+ rawPercent !== null &&
606
+ rawPercent > 0 &&
607
+ !window.stale &&
608
+ Number.isFinite(extra.fetchedAt) &&
609
+ extra.fetchedAt > start
610
+ ) {
611
+ const upTo = totals(inWindow.filter((e) => e.at <= extra.fetchedAt));
612
+ const after = totals(inWindow.filter((e) => e.at > extra.fetchedAt));
613
+ if (upTo.cost > 0 && after.cost > 0) {
614
+ const pricePerPoint = upTo.cost / rawPercent;
615
+ sinceSnapshot = after.cost / pricePerPoint;
616
+ if (sinceSnapshot >= 1) {
617
+ percent = Math.min(100, Math.round(rawPercent + sinceSnapshot));
618
+ window.adjusted = true;
619
+ window.pointsSinceSnapshot = Math.round(sinceSnapshot);
620
+ window.percentUsed = percent;
621
+ window.percentLeft = Math.max(0, 100 - percent);
622
+ }
623
+ }
624
+ }
625
+
589
626
  const derived = percent !== null && percent > 0 && spent.cost > 0 ? spent.cost / percent : null;
590
627
  const priced =
591
628
  derived !== null
@@ -615,7 +652,6 @@ function buildWindow(spec, snapshot, events, now, options) {
615
652
  // A reset time in the past means the window already turned over and the
616
653
  // cached percentage describes a window that no longer exists. Reporting it
617
654
  // as current would claim the budget is gone when it has just come back.
618
- window.stale = hasReset && resetsAt <= now;
619
655
  if (window.stale) {
620
656
  window.remainingUSD = null;
621
657
  window.turnsLeft = null;
@@ -754,6 +790,7 @@ function collect(now) {
754
790
  planTier: plan.tier,
755
791
  planAdvice: plan.advice,
756
792
  snapshotAgeMs: cache && cache.fetchedAtMs ? now - cache.fetchedAtMs : null,
793
+ snapshotFetchedAt: cache && cache.fetchedAtMs ? cache.fetchedAtMs : null,
757
794
  utilization,
758
795
  settings: {
759
796
  model: settings.model || 'default',
@@ -787,7 +824,12 @@ function reconstructWindow(spec, snapshot, events, now) {
787
824
  if (past.cost <= 0) return null;
788
825
 
789
826
  const usdPerPercent = past.cost / snapshot.utilization;
790
- const liveStart = now - spec.span;
827
+
828
+ // The window running now began when the old one reset, not five hours ago.
829
+ // Summing a rolling span sweeps in the window that already expired: three
830
+ // minutes after a reset that meant counting 103 turns instead of 6, and
831
+ // reporting a fresh window as completely full.
832
+ const liveStart = Math.max(now - spec.span, resetsAt);
791
833
  const live = totals(events.filter((e) => e.at >= liveStart && e.at <= now));
792
834
  const raw = live.cost / usdPerPercent;
793
835
 
@@ -810,14 +852,14 @@ function reconstructWindow(spec, snapshot, events, now) {
810
852
 
811
853
  // No snapshot at all means no windows, which is what tells the report to
812
854
  // explain itself rather than print a table of dashes.
813
- function buildWindows(utilization, events, now) {
855
+ function buildWindows(utilization, events, now, fetchedAt) {
814
856
  if (!utilization) return [];
815
857
  return WINDOWS.map((spec) => {
816
858
  const snapshot = utilization[spec.key];
817
859
  // The per-model weekly windows only exist on some plans.
818
860
  if (spec.key !== 'five_hour' && spec.key !== 'seven_day' && !snapshot) return null;
819
861
 
820
- const window = buildWindow(spec, snapshot, events, now);
862
+ const window = buildWindow(spec, snapshot, events, now, { fetchedAt });
821
863
  if (!window.stale) return window;
822
864
 
823
865
  // Rolled over. Rebuild from local history rather than going blind on it.
@@ -844,7 +886,7 @@ async function report(now) {
844
886
  const earliest = now - 8 * DAY;
845
887
  const events = await readEvents(earliest);
846
888
 
847
- const windows = buildWindows(base.utilization, events, now);
889
+ const windows = buildWindows(base.utilization, events, now, base.snapshotFetchedAt);
848
890
 
849
891
  const recentEvents = events.filter((event) => event.at >= now - HOUR);
850
892
  const recent = totals(recentEvents);
@@ -1005,7 +1047,7 @@ function render(data) {
1005
1047
  ? 'stale'
1006
1048
  : window.percentUsed === null
1007
1049
  ? '-'
1008
- : (window.estimated ? '~' : '') + window.percentUsed + '%',
1050
+ : (window.estimated || window.adjusted ? '~' : '') + window.percentUsed + '%',
1009
1051
  6
1010
1052
  ) +
1011
1053
  padLeft(formatDuration(window.msToReset), 12) +
@@ -1083,6 +1125,15 @@ function render(data) {
1083
1125
  }
1084
1126
  lines.push(' Measured ' + formatCount(data.measuredTurns) + ' turns of local transcript');
1085
1127
 
1128
+ if (data.windows.some((window) => window.adjusted)) {
1129
+ lines.push(
1130
+ ' Note ~ includes spend since the snapshot was taken, which its own'
1131
+ );
1132
+ lines.push(
1133
+ ' reading does not cover yet. Run /usage for a fresh one.'
1134
+ );
1135
+ }
1136
+
1086
1137
  if (data.windows.some((window) => window.estimated)) {
1087
1138
  lines.push(
1088
1139
  ' Note ~ means the snapshot had gone stale and that window was rebuilt'