@tigorhutasuhut/claude-retry 0.1.8 → 0.1.9

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 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, then confirms against Anthropic's usage API — the same data the `/usage` command shows — to get the **exact** reset time and to discard stale 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.
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`, 5s for single-pane `monitor`):
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, gone ones pruned.
61
- 2. **Capture** — each pane's visible screen is dumped with `zellij --session <name> action dump-screen --pane-id <id>` (ANSI stripped). This works on detached sessions, no attached client required.
62
- 3. **Match** — the text is checked against the rate-limit patterns. Panes that aren't showing a limit banner are simply left alone.
63
- 4. **Resolve** — on a banner match, the reset time is determined via a three-tier cascade:
64
- - **Tier 1 — usage API (primary).** Once per pass, the daemon discovers every Claude account in use by reading `CLAUDE_CONFIG_DIR` from each Claude process via `/proc` (Linux). For each account it calls `GET https://api.anthropic.com/api/oauth/usage` with the OAuth token from `<CLAUDE_CONFIG_DIR>/.credentials.json`. If the account is **not** limited the banner is stale — it is silently ignored, no wait issued. If the account **is** limited the daemon waits until the API's exact `resets_at` timestamp. Credentials are re-read every pass, so token refreshes are picked up automatically.
65
- - **Tier 2/proc pane→account bridge (planned).** Needed only when two or more accounts are limited simultaneously, so the daemon must map a specific pane to its account. This is a phase-2 stub; until implemented, that case falls through to tier 3.
66
- - **Tier 3 text fallback.** Used when the API is unreachable, the account is unknown, or tier 2 is unresolved. Falls back to parsing the reset time from the on-screen banner text (the original behavior). A banner is never silently ignored when the account is unknown — this ensures a real limit is never missed.
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
- Per-pane state (keyed by `session:paneId`) persists across passes, so a pane mid-wait isn't disturbed by rediscovery. It runs as a plain foreground process — no transparent session wrapping, no external daemon. The zellij pane is the daemon.
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
- > **Multi-account note.** On Linux, account discovery reads `CLAUDE_CONFIG_DIR` from every live Claude process via `/proc`. This means the daemon polls usage for every account in use — not just the default one. On non-Linux systems it falls back to the default account (`~/.claude`) plus tier-3 text parsing.
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/monitor.js CHANGED
@@ -133,8 +133,8 @@ 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
  /**
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tigorhutasuhut/claude-retry",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Monitor Claude CLI in a zellij pane, auto-inject continue on rate-limit",
5
5
  "type": "module",
6
6
  "license": "MIT",