@tigorhutasuhut/claude-retry 0.1.9 → 0.1.11
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/cli.js +5 -4
- package/dist/format.d.ts +15 -0
- package/dist/format.js +36 -0
- package/dist/monitor.d.ts +3 -3
- package/dist/monitor.js +13 -13
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3,23 +3,24 @@ import { capturePane, inject, listPaneTargets, captureTarget, injectTarget, } fr
|
|
|
3
3
|
import { runMonitor, runMultiMonitor } from "./monitor.js";
|
|
4
4
|
import { readAccessToken, fetchUsage } from "./usage.js";
|
|
5
5
|
import { discoverAccountDirs, resolvePaneConfigDir } from "./accounts.js";
|
|
6
|
+
import { formatClock } from "./format.js";
|
|
6
7
|
const USAGE = `claude-retry — Auto-inject 'continue' when Claude hits a rate limit in zellij
|
|
7
8
|
|
|
8
9
|
Usage:
|
|
9
|
-
claude-retry start Watch ALL
|
|
10
|
+
claude-retry start Watch ALL panes across ALL sessions (acts on the ones showing a rate-limit banner)
|
|
10
11
|
claude-retry monitor <pane-id> Watch one pane by ID in the current session
|
|
11
12
|
claude-retry help Show this help
|
|
12
13
|
|
|
13
14
|
Run as a foreground daemon in any zellij pane (a dedicated session is ideal).
|
|
14
15
|
'start' polls every 60s, walks every live zellij session and every pane, and
|
|
15
|
-
auto-injects 'continue' after
|
|
16
|
-
on detached sessions and skips its own session. New
|
|
16
|
+
auto-injects 'continue' after a pane's rate-limit reset time. It works
|
|
17
|
+
on detached sessions and skips its own session. New panes are picked up
|
|
17
18
|
automatically; closed ones are dropped. Logs go to stderr.
|
|
18
19
|
|
|
19
20
|
'monitor <pane-id>' is the legacy single-pane mode (current session only).`;
|
|
20
21
|
/** Timestamped stderr logger — chatty so the daemon shows clear signs of life. */
|
|
21
22
|
function log(msg) {
|
|
22
|
-
const ts =
|
|
23
|
+
const ts = formatClock(); // local HH:MM:SS
|
|
23
24
|
process.stderr.write(`[${ts}] ${msg}\n`);
|
|
24
25
|
}
|
|
25
26
|
const now = () => Date.now();
|
package/dist/format.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local-time formatting helpers.
|
|
3
|
+
* All output uses the machine's local timezone (not UTC).
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Local wall-clock "HH:MM:SS" (24h).
|
|
7
|
+
* @param ms - epoch ms; defaults to Date.now() when omitted.
|
|
8
|
+
*/
|
|
9
|
+
export declare function formatClock(ms?: number): string;
|
|
10
|
+
/**
|
|
11
|
+
* Local "YYYY-MM-DD HH:MM:SS TZ" (e.g. "2026-06-07 14:05:09 GMT+7").
|
|
12
|
+
* The TZ suffix is derived from Intl.DateTimeFormat; omitted gracefully if unavailable.
|
|
13
|
+
*/
|
|
14
|
+
export declare function formatLocalDateTime(ms: number): string;
|
|
15
|
+
//# sourceMappingURL=format.d.ts.map
|
package/dist/format.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local-time formatting helpers.
|
|
3
|
+
* All output uses the machine's local timezone (not UTC).
|
|
4
|
+
*/
|
|
5
|
+
function pad2(n) {
|
|
6
|
+
return String(n).padStart(2, '0');
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Local wall-clock "HH:MM:SS" (24h).
|
|
10
|
+
* @param ms - epoch ms; defaults to Date.now() when omitted.
|
|
11
|
+
*/
|
|
12
|
+
export function formatClock(ms) {
|
|
13
|
+
const d = new Date(ms ?? Date.now());
|
|
14
|
+
return `${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Local "YYYY-MM-DD HH:MM:SS TZ" (e.g. "2026-06-07 14:05:09 GMT+7").
|
|
18
|
+
* The TZ suffix is derived from Intl.DateTimeFormat; omitted gracefully if unavailable.
|
|
19
|
+
*/
|
|
20
|
+
export function formatLocalDateTime(ms) {
|
|
21
|
+
const d = new Date(ms);
|
|
22
|
+
const date = `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
|
|
23
|
+
const time = `${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`;
|
|
24
|
+
let tz = '';
|
|
25
|
+
try {
|
|
26
|
+
const parts = Intl.DateTimeFormat(undefined, { timeZoneName: 'short' }).formatToParts(d);
|
|
27
|
+
const tzPart = parts.find((p) => p.type === 'timeZoneName');
|
|
28
|
+
if (tzPart?.value)
|
|
29
|
+
tz = ` ${tzPart.value}`;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Intl unavailable or failed — omit TZ suffix
|
|
33
|
+
}
|
|
34
|
+
return `${date} ${time}${tz}`;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=format.js.map
|
package/dist/monitor.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export interface MonitorDeps {
|
|
|
6
6
|
now: () => number;
|
|
7
7
|
sleep: (ms: number) => Promise<void>;
|
|
8
8
|
}
|
|
9
|
-
/** Deps for watching many
|
|
9
|
+
/** Deps for watching many panes across sessions. Capture/inject are
|
|
10
10
|
* addressed by PaneTarget rather than a bare pane id. */
|
|
11
11
|
export interface MultiMonitorDeps {
|
|
12
12
|
listTargets: () => Promise<PaneTarget[]>;
|
|
@@ -32,9 +32,9 @@ export declare function createState(): MonitorState;
|
|
|
32
32
|
export declare function tick(paneId: string, state: MonitorState, deps: MonitorDeps, marginSeconds?: number, fallbackHours?: number): Promise<MonitorStatus>;
|
|
33
33
|
export declare function runMonitor(paneId: string, deps: MonitorDeps, pollIntervalMs?: number, marginSeconds?: number, fallbackHours?: number): Promise<void>;
|
|
34
34
|
/**
|
|
35
|
-
* One discovery+monitor pass over every
|
|
35
|
+
* One discovery+monitor pass over every pane in every live session.
|
|
36
36
|
*
|
|
37
|
-
* Re-discovers targets each call so new
|
|
37
|
+
* Re-discovers targets each call so new sessions/panes are picked up and
|
|
38
38
|
* closed ones are pruned. Per-pane state lives in `states`, keyed by the
|
|
39
39
|
* target's label (session:paneId), and persists across calls. A failed
|
|
40
40
|
* discovery or a single pane's capture/inject error is swallowed so one bad
|
package/dist/monitor.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { match, isBlockedAtBanner } from "./patterns.js";
|
|
2
2
|
import { parseResetTime, calculateWaitMs } from "./time-parser.js";
|
|
3
|
+
import { formatLocalDateTime } from "./format.js";
|
|
3
4
|
const MAX_MISSES = 3;
|
|
4
5
|
export function createState() {
|
|
5
6
|
return { status: 'monitoring', waitUntil: 0, missCount: 0 };
|
|
@@ -65,7 +66,7 @@ async function stepState(state, screenText, now, injectContinue, marginSeconds,
|
|
|
65
66
|
await injectContinue();
|
|
66
67
|
state.status = 'monitoring';
|
|
67
68
|
state.waitUntil = 0;
|
|
68
|
-
logger(`${label} reset reached
|
|
69
|
+
logger(`${label} — reset reached, sent 'continue'`);
|
|
69
70
|
return 'retried';
|
|
70
71
|
}
|
|
71
72
|
// Still limited, before reset → keep waiting.
|
|
@@ -83,7 +84,7 @@ async function stepState(state, screenText, now, injectContinue, marginSeconds,
|
|
|
83
84
|
// limit banner whose quota just reset (restart-after-reset / reopened-idle).
|
|
84
85
|
if (isBlockedAtBanner(screenText)) {
|
|
85
86
|
await injectContinue();
|
|
86
|
-
logger(`${label} cleared-limit banner at bottom —
|
|
87
|
+
logger(`${label} cleared-limit banner at bottom — sent 'continue'`);
|
|
87
88
|
return 'retried';
|
|
88
89
|
}
|
|
89
90
|
logger(`${label} stale banner ignored (account not limited)`);
|
|
@@ -94,7 +95,7 @@ async function stepState(state, screenText, now, injectContinue, marginSeconds,
|
|
|
94
95
|
if (usage.resetsAtMs !== null) {
|
|
95
96
|
state.waitUntil = usage.resetsAtMs + marginMs;
|
|
96
97
|
state.status = 'waiting';
|
|
97
|
-
logger(`${label} account ${accountDir} limited,
|
|
98
|
+
logger(`${label} account ${accountDir} limited, 'continue' at ${formatLocalDateTime(state.waitUntil)}`);
|
|
98
99
|
return 'rate-limited';
|
|
99
100
|
}
|
|
100
101
|
// resetsAtMs null — fall through to text parse for the time, but
|
|
@@ -111,7 +112,7 @@ async function stepState(state, screenText, now, injectContinue, marginSeconds,
|
|
|
111
112
|
// Reset time already passed → limit is over (no roll-to-tomorrow).
|
|
112
113
|
if (isBlockedAtBanner(screenText)) {
|
|
113
114
|
await injectContinue();
|
|
114
|
-
logger(`${label} reset already passed —
|
|
115
|
+
logger(`${label} reset already passed — sent 'continue'`);
|
|
115
116
|
return 'retried';
|
|
116
117
|
}
|
|
117
118
|
logger(`${label} stale banner ignored (reset already passed)`);
|
|
@@ -138,9 +139,9 @@ export async function runMonitor(paneId, deps, pollIntervalMs, marginSeconds, fa
|
|
|
138
139
|
}
|
|
139
140
|
}
|
|
140
141
|
/**
|
|
141
|
-
* One discovery+monitor pass over every
|
|
142
|
+
* One discovery+monitor pass over every pane in every live session.
|
|
142
143
|
*
|
|
143
|
-
* Re-discovers targets each call so new
|
|
144
|
+
* Re-discovers targets each call so new sessions/panes are picked up and
|
|
144
145
|
* closed ones are pruned. Per-pane state lives in `states`, keyed by the
|
|
145
146
|
* target's label (session:paneId), and persists across calls. A failed
|
|
146
147
|
* discovery or a single pane's capture/inject error is swallowed so one bad
|
|
@@ -178,8 +179,8 @@ export async function multiTick(states, deps, marginSeconds, fallbackHours) {
|
|
|
178
179
|
}
|
|
179
180
|
}
|
|
180
181
|
log(targets.length === 0
|
|
181
|
-
? 'scan: no
|
|
182
|
-
: `scan: watching ${targets.length}
|
|
182
|
+
? 'scan: no panes found'
|
|
183
|
+
: `scan: watching ${targets.length} pane(s) [${targets.map((t) => t.label).join(', ')}]`);
|
|
183
184
|
// Capture each target's screen once, collecting successes into a map.
|
|
184
185
|
// Capture failures are logged and that pane is skipped this round.
|
|
185
186
|
const screens = new Map();
|
|
@@ -216,7 +217,7 @@ export async function multiTick(states, deps, marginSeconds, fallbackHours) {
|
|
|
216
217
|
if (!state) {
|
|
217
218
|
state = createState();
|
|
218
219
|
states.set(target.label, state);
|
|
219
|
-
log(`${target.label} — new
|
|
220
|
+
log(`${target.label} — new pane, now watching`);
|
|
220
221
|
}
|
|
221
222
|
// Reset miss counter for panes present this pass
|
|
222
223
|
state.missCount = 0;
|
|
@@ -233,14 +234,13 @@ export async function multiTick(states, deps, marginSeconds, fallbackHours) {
|
|
|
233
234
|
}
|
|
234
235
|
function logPaneStatus(log, label, before, state, status) {
|
|
235
236
|
if (status === 'rate-limited' && before === 'monitoring') {
|
|
236
|
-
|
|
237
|
-
log(`${label} — RATE LIMITED, waiting until ${until}`);
|
|
237
|
+
log(`${label} — rate limited, will send 'continue' at ${formatLocalDateTime(state.waitUntil)}`);
|
|
238
238
|
}
|
|
239
239
|
else if (status === 'rate-limited') {
|
|
240
|
-
log(`${label} —
|
|
240
|
+
log(`${label} — waiting, 'continue' at ${formatLocalDateTime(state.waitUntil)}`);
|
|
241
241
|
}
|
|
242
242
|
else if (status === 'retried') {
|
|
243
|
-
log(`${label} — reset reached,
|
|
243
|
+
log(`${label} — reset reached, sent 'continue'`);
|
|
244
244
|
}
|
|
245
245
|
else {
|
|
246
246
|
log(`${label} — ok`);
|