@tigorhutasuhut/claude-retry 0.1.7 → 0.1.8

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/dist/monitor.js CHANGED
@@ -1,4 +1,4 @@
1
- import { match } from "./patterns.js";
1
+ import { match, isBlockedAtBanner } from "./patterns.js";
2
2
  import { parseResetTime, calculateWaitMs } from "./time-parser.js";
3
3
  const MAX_MISSES = 3;
4
4
  export function createState() {
@@ -79,7 +79,13 @@ async function stepState(state, screenText, now, injectContinue, marginSeconds,
79
79
  const { dir: accountDir, usage } = await resolveAccountUsage(snapshot, resolvePaneAccount, target);
80
80
  if (accountDir !== null && usage !== undefined) {
81
81
  if (!usage.limited) {
82
- // Staleness gate: account is not limited banner is stale → ignore
82
+ // Account cleared. Either truly stale/incidental, OR a pane parked at a
83
+ // limit banner whose quota just reset (restart-after-reset / reopened-idle).
84
+ if (isBlockedAtBanner(screenText)) {
85
+ await injectContinue();
86
+ logger(`${label} cleared-limit banner at bottom — injected continue`);
87
+ return 'retried';
88
+ }
83
89
  logger(`${label} stale banner ignored (account not limited)`);
84
90
  return 'monitoring';
85
91
  }
@@ -101,6 +107,16 @@ async function stepState(state, screenText, now, injectContinue, marginSeconds,
101
107
  const resetLine = result.resetLine ?? '';
102
108
  const parsed = parseResetTime(resetLine);
103
109
  const waitMs = calculateWaitMs(parsed, marginSeconds, fallbackHours, new Date(now));
110
+ if (waitMs <= 0) {
111
+ // Reset time already passed → limit is over (no roll-to-tomorrow).
112
+ if (isBlockedAtBanner(screenText)) {
113
+ await injectContinue();
114
+ logger(`${label} reset already passed — injected continue`);
115
+ return 'retried';
116
+ }
117
+ logger(`${label} stale banner ignored (reset already passed)`);
118
+ return 'monitoring';
119
+ }
104
120
  state.waitUntil = now + waitMs;
105
121
  state.status = 'waiting';
106
122
  return 'rate-limited';
@@ -4,4 +4,6 @@ export interface MatchResult {
4
4
  resetLine: string | null;
5
5
  }
6
6
  export declare function match(text: string): MatchResult;
7
+ export declare function strictMatch(text: string): boolean;
8
+ export declare function isBlockedAtBanner(text: string, bottomLines?: number): boolean;
7
9
  //# sourceMappingURL=patterns.d.ts.map
package/dist/patterns.js CHANGED
@@ -47,4 +47,37 @@ export function match(text) {
47
47
  }
48
48
  return { limited: false, resetLine: null };
49
49
  }
50
+ // Canonical Claude rate-limit banner phrasings — specific enough not to fire on
51
+ // normal code/output. Used for high-confidence detection.
52
+ const STRICT_PATTERNS = [
53
+ /you(?:'ve|'ve|'ve|\s+have)\s+hit\s+your\s+(?:usage|session)\s+limit/i,
54
+ /\d+-hour\s+limit\s+reached/i,
55
+ /usage\s+limit\s+reached/i,
56
+ /session\s+limit\b.*\bresets/i,
57
+ /upgrade\s+to\s+increase\s+your\s+usage\s+limit/i,
58
+ ];
59
+ export function strictMatch(text) {
60
+ const stripped = stripAnsi(text);
61
+ return STRICT_PATTERNS.some((p) => p.test(stripped));
62
+ }
63
+ // True when a canonical banner sits in the bottom region of the screen — i.e.
64
+ // claude is parked at the limit message right above its input box, not merely
65
+ // displaying banner text somewhere in scrollback/discussion.
66
+ export function isBlockedAtBanner(text, bottomLines = 15) {
67
+ const stripped = stripAnsi(text);
68
+ const lines = stripped.split('\n');
69
+ // Trim trailing blank lines
70
+ let end = lines.length - 1;
71
+ while (end >= 0 && lines[end].trim() === '') {
72
+ end--;
73
+ }
74
+ // Collect last `bottomLines` non-empty lines
75
+ const nonEmpty = [];
76
+ for (let i = end; i >= 0 && nonEmpty.length < bottomLines; i--) {
77
+ if (lines[i].trim() !== '') {
78
+ nonEmpty.unshift(lines[i]);
79
+ }
80
+ }
81
+ return strictMatch(nonEmpty.join('\n'));
82
+ }
50
83
  //# sourceMappingURL=patterns.js.map
@@ -99,17 +99,8 @@ export function calculateWaitMs(parsed, marginSeconds = 60, fallbackHours = 5, n
99
99
  candidateUtcMs = corrected;
100
100
  }
101
101
  if (candidateUtcMs <= nowMs) {
102
- // Tomorrow rolloveradvance local midnight by one day
103
- candidateLocalMs += 86400000;
104
- candidateUtcMs = candidateLocalMs + tzOffset;
105
- for (let i = 0; i < 3; i++) {
106
- const candidateDate = new Date(candidateUtcMs);
107
- const candidateTzOffset = getOffsetMs(timezone, candidateDate);
108
- const corrected = candidateLocalMs + candidateTzOffset;
109
- if (corrected === candidateUtcMs)
110
- break;
111
- candidateUtcMs = corrected;
112
- }
102
+ // Reset time already passed signal "already reset" (non-positive delta)
103
+ return candidateUtcMs - nowMs;
113
104
  }
114
105
  return candidateUtcMs - nowMs + marginSeconds * 1000;
115
106
  }
@@ -118,9 +109,10 @@ export function calculateWaitMs(parsed, marginSeconds = 60, fallbackHours = 5, n
118
109
  const nowMs = now.getTime();
119
110
  const nowDate = now;
120
111
  const midnight = Date.UTC(nowDate.getUTCFullYear(), nowDate.getUTCMonth(), nowDate.getUTCDate());
121
- let candidateMs = midnight + h * 3600000 + minute * 60000;
112
+ const candidateMs = midnight + h * 3600000 + minute * 60000;
122
113
  if (candidateMs <= nowMs) {
123
- candidateMs += 86400000;
114
+ // Reset time already passed — signal "already reset" (non-positive delta)
115
+ return candidateMs - nowMs;
124
116
  }
125
117
  return candidateMs - nowMs + marginSeconds * 1000;
126
118
  }
@@ -131,18 +123,16 @@ export function calculateWaitMs(parsed, marginSeconds = 60, fallbackHours = 5, n
131
123
  const amHour = hour === 12 ? 0 : hour;
132
124
  const waitAm = tryCalculate(amHour);
133
125
  const waitPm = tryCalculate(pmHour);
134
- // Return the sooner positive wait
126
+ // Return the sooner positive wait; if both past, return least-negative (most recent reset)
135
127
  if (waitAm > 0 && waitPm > 0)
136
128
  return Math.min(waitAm, waitPm);
137
129
  if (waitAm > 0)
138
130
  return waitAm;
139
131
  if (waitPm > 0)
140
132
  return waitPm;
141
- return fallbackMs;
133
+ // Both past: return closest-to-zero (most recent reset)
134
+ return Math.max(waitAm, waitPm);
142
135
  }
143
- const wait = tryCalculate(hour);
144
- if (wait <= 0)
145
- return fallbackMs;
146
- return wait;
136
+ return tryCalculate(hour);
147
137
  }
148
138
  //# sourceMappingURL=time-parser.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tigorhutasuhut/claude-retry",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Monitor Claude CLI in a zellij pane, auto-inject continue on rate-limit",
5
5
  "type": "module",
6
6
  "license": "MIT",