@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 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 Claude panes across ALL sessions
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 each Claude pane's rate-limit reset time. It works
16
- on detached sessions and skips its own session. New Claude panes are picked up
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 = new Date().toISOString().slice(11, 19); // HH:MM:SS
23
+ const ts = formatClock(); // local HH:MM:SS
23
24
  process.stderr.write(`[${ts}] ${msg}\n`);
24
25
  }
25
26
  const now = () => Date.now();
@@ -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 Claude panes across sessions. Capture/inject are
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 Claude pane in every live session.
35
+ * One discovery+monitor pass over every pane in every live session.
36
36
  *
37
- * Re-discovers targets each call so new Claude sessions/panes are picked up and
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 injected continue`);
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 — injected continue`);
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, reset ${new Date(usage.resetsAtMs).toISOString()}`);
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 — injected continue`);
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 Claude pane in every live session.
142
+ * One discovery+monitor pass over every pane in every live session.
142
143
  *
143
- * Re-discovers targets each call so new Claude sessions/panes are picked up and
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 Claude panes found'
182
- : `scan: watching ${targets.length} Claude pane(s) [${targets.map((t) => t.label).join(', ')}]`);
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 Claude pane, now watching`);
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
- const until = new Date(state.waitUntil).toISOString();
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} — still waiting for reset`);
240
+ log(`${label} — waiting, 'continue' at ${formatLocalDateTime(state.waitUntil)}`);
241
241
  }
242
242
  else if (status === 'retried') {
243
- log(`${label} — reset reached, cleared input + injected 'continue'`);
243
+ log(`${label} — reset reached, sent 'continue'`);
244
244
  }
245
245
  else {
246
246
  log(`${label} — ok`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tigorhutasuhut/claude-retry",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "Monitor Claude CLI in a zellij pane, auto-inject continue on rate-limit",
5
5
  "type": "module",
6
6
  "license": "MIT",