@tigorhutasuhut/claude-retry 0.1.8 → 0.1.10
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/README.md +26 -12
- package/dist/cli.js +3 -3
- package/dist/monitor.d.ts +3 -3
- package/dist/monitor.js +7 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# claude-retry
|
|
2
2
|
|
|
3
|
-
Watches every pane across **all** your [zellij](https://zellij.dev/) sessions. When a pane hits Anthropic's usage/session limit, it detects the on-screen rate-limit banner
|
|
3
|
+
Watches every pane across **all** your [zellij](https://zellij.dev/) sessions. When a pane hits Anthropic's usage/session limit, it detects the on-screen rate-limit banner and cross-checks against Anthropic's usage API to get the **exact** reset time and discard stale or incidental banners. Once the reset elapses it clears the input and injects `continue` to resume automatically. One daemon covers every session at once — even detached ones.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -55,20 +55,34 @@ source "$(npm root -g)/claude-retry/shell/wrapper.bash"
|
|
|
55
55
|
|
|
56
56
|
## How it works
|
|
57
57
|
|
|
58
|
-
Every pass (60s for `start
|
|
58
|
+
Every pass (60s for `start`):
|
|
59
59
|
|
|
60
|
-
1. **Discover** — `zellij list-sessions` enumerates live sessions (EXITED ones and the daemon's own `$ZELLIJ_SESSION_NAME` are skipped). For each, `zellij --session <name> action list-panes -j` lists its panes; plugins and exited panes are dropped. New panes are added
|
|
61
|
-
2. **Capture** — each pane's visible screen is dumped with `zellij --session <name> action dump-screen --pane-id <id>` (ANSI stripped).
|
|
62
|
-
3. **
|
|
63
|
-
|
|
64
|
-
- **
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
5. **Retry** — once the resolved reset time elapses, the daemon sends **Ctrl+C** (clears any half-typed input — a single Ctrl+C in Claude Code doesn't quit), then types `continue` and Enter via `write-chars` / `write`.
|
|
60
|
+
1. **Discover** — `zellij list-sessions` enumerates live sessions (EXITED ones and the daemon's own `$ZELLIJ_SESSION_NAME` are skipped). For each, `zellij --session <name> action list-panes -j` lists its panes; plugins and exited panes are dropped. New panes are added; gone panes are tracked via a miss-counter and dropped only after **3 consecutive absent passes**, so a transient `list-panes` hiccup never loses a pane mid-wait.
|
|
61
|
+
2. **Capture** — each pane's visible screen is dumped with `zellij --session <name> action dump-screen --pane-id <id>` (ANSI stripped). Works on detached sessions — no attached client required.
|
|
62
|
+
3. **Signal check** — the screen is checked for two signals:
|
|
63
|
+
- **Loose banner match** — any rate-limit text present anywhere on screen (candidate trigger).
|
|
64
|
+
- **Canonical banner** (`isBlockedAtBanner`) — a high-confidence match anchored to the **bottom** of the screen, meaning Claude is parked at the limit right above its input box. This distinguishes an active block from incidental banner text in scrollback.
|
|
65
|
+
4. **API call (conditional)** — the usage API (`GET https://api.anthropic.com/api/oauth/usage`) is called **only** when at least one pane shows a banner or is already waiting. Zero API calls when nothing is limited. Account is resolved as: the sole account on the machine, else the sole limited account, else via the Linux `/proc` bridge (pane → pts → `CLAUDE_CONFIG_DIR`), else unknown.
|
|
66
|
+
5. **State machine per pane:**
|
|
68
67
|
|
|
69
|
-
|
|
68
|
+
**MONITORING:**
|
|
69
|
+
- No banner → idle, nothing to do.
|
|
70
|
+
- Banner + account **LIMITED** → enter WAITING until `resets_at`.
|
|
71
|
+
- Banner + account **CLEARED** → if a canonical banner sits at the bottom (Claude restarted after reset, or a reopened `claude --continue` left idle) → inject `continue`; otherwise ignore (stale or scrollback text — no false triggers).
|
|
72
|
+
- Banner + account **UNKNOWN** (API down) → parse reset time from on-screen text; future → enter WAITING; already-passed → inject `continue` if canonical banner at bottom, else ignore. A bare past time means the limit already reset — it is never rolled to tomorrow.
|
|
70
73
|
|
|
71
|
-
|
|
74
|
+
**WAITING:**
|
|
75
|
+
- Banner gone → abandon (Claude exited, user continued, or pane ID reused).
|
|
76
|
+
- Account cleared **or** timer elapsed → inject `continue`.
|
|
77
|
+
- Account still limited → keep waiting; `resets_at` refreshed live each pass.
|
|
78
|
+
|
|
79
|
+
6. **Inject** — Ctrl+C (clears any half-typed input; one Ctrl+C does not quit Claude Code), then `continue` + Enter via `write-chars` / `write`.
|
|
80
|
+
|
|
81
|
+
Per-pane state (keyed by `session:paneId`) persists across passes. Runs as a plain foreground process — the zellij pane is the daemon.
|
|
82
|
+
|
|
83
|
+
> **Single-pane `monitor <id>` mode** is text-only: no account API, just screen scraping against the current session.
|
|
84
|
+
|
|
85
|
+
> **Multi-account (Linux).** Account discovery reads `CLAUDE_CONFIG_DIR` from every live Claude process via `/proc` and polls usage for each account. On non-Linux the daemon uses the default account (`~/.claude`) and falls back to on-screen time parsing when the API is unavailable.
|
|
72
86
|
|
|
73
87
|
## Requirements
|
|
74
88
|
|
package/dist/cli.js
CHANGED
|
@@ -6,14 +6,14 @@ import { discoverAccountDirs, resolvePaneConfigDir } from "./accounts.js";
|
|
|
6
6
|
const USAGE = `claude-retry — Auto-inject 'continue' when Claude hits a rate limit in zellij
|
|
7
7
|
|
|
8
8
|
Usage:
|
|
9
|
-
claude-retry start Watch ALL
|
|
9
|
+
claude-retry start Watch ALL panes across ALL sessions (acts on the ones showing a rate-limit banner)
|
|
10
10
|
claude-retry monitor <pane-id> Watch one pane by ID in the current session
|
|
11
11
|
claude-retry help Show this help
|
|
12
12
|
|
|
13
13
|
Run as a foreground daemon in any zellij pane (a dedicated session is ideal).
|
|
14
14
|
'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
|
|
15
|
+
auto-injects 'continue' after a pane's rate-limit reset time. It works
|
|
16
|
+
on detached sessions and skips its own session. New panes are picked up
|
|
17
17
|
automatically; closed ones are dropped. Logs go to stderr.
|
|
18
18
|
|
|
19
19
|
'monitor <pane-id>' is the legacy single-pane mode (current session only).`;
|
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
|
@@ -133,14 +133,14 @@ async function tickTarget(target, state, screenText, deps, marginSeconds, fallba
|
|
|
133
133
|
export async function runMonitor(paneId, deps, pollIntervalMs, marginSeconds, fallbackHours) {
|
|
134
134
|
const state = createState();
|
|
135
135
|
for (;;) {
|
|
136
|
-
await deps.sleep(pollIntervalMs ?? 5000);
|
|
137
136
|
await tick(paneId, state, deps, marginSeconds, fallbackHours);
|
|
137
|
+
await deps.sleep(pollIntervalMs ?? 5000);
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
140
|
/**
|
|
141
|
-
* One discovery+monitor pass over every
|
|
141
|
+
* One discovery+monitor pass over every pane in every live session.
|
|
142
142
|
*
|
|
143
|
-
* Re-discovers targets each call so new
|
|
143
|
+
* Re-discovers targets each call so new sessions/panes are picked up and
|
|
144
144
|
* closed ones are pruned. Per-pane state lives in `states`, keyed by the
|
|
145
145
|
* target's label (session:paneId), and persists across calls. A failed
|
|
146
146
|
* discovery or a single pane's capture/inject error is swallowed so one bad
|
|
@@ -178,8 +178,8 @@ export async function multiTick(states, deps, marginSeconds, fallbackHours) {
|
|
|
178
178
|
}
|
|
179
179
|
}
|
|
180
180
|
log(targets.length === 0
|
|
181
|
-
? 'scan: no
|
|
182
|
-
: `scan: watching ${targets.length}
|
|
181
|
+
? 'scan: no panes found'
|
|
182
|
+
: `scan: watching ${targets.length} pane(s) [${targets.map((t) => t.label).join(', ')}]`);
|
|
183
183
|
// Capture each target's screen once, collecting successes into a map.
|
|
184
184
|
// Capture failures are logged and that pane is skipped this round.
|
|
185
185
|
const screens = new Map();
|
|
@@ -216,7 +216,7 @@ export async function multiTick(states, deps, marginSeconds, fallbackHours) {
|
|
|
216
216
|
if (!state) {
|
|
217
217
|
state = createState();
|
|
218
218
|
states.set(target.label, state);
|
|
219
|
-
log(`${target.label} — new
|
|
219
|
+
log(`${target.label} — new pane, now watching`);
|
|
220
220
|
}
|
|
221
221
|
// Reset miss counter for panes present this pass
|
|
222
222
|
state.missCount = 0;
|
|
@@ -249,8 +249,8 @@ function logPaneStatus(log, label, before, state, status) {
|
|
|
249
249
|
export async function runMultiMonitor(deps, pollIntervalMs, marginSeconds, fallbackHours) {
|
|
250
250
|
const states = new Map();
|
|
251
251
|
for (;;) {
|
|
252
|
-
await deps.sleep(pollIntervalMs ?? 60000);
|
|
253
252
|
await multiTick(states, deps, marginSeconds, fallbackHours);
|
|
253
|
+
await deps.sleep(pollIntervalMs ?? 60000);
|
|
254
254
|
}
|
|
255
255
|
}
|
|
256
256
|
//# sourceMappingURL=monitor.js.map
|