From 0652a232747c8c742aad05d15dc80aad33a32671 Mon Sep 17 00:00:00 2001 From: CheapestInference Date: Sun, 29 Mar 2026 03:52:16 +0200 Subject: [PATCH] fix: improve foreground process detection for macOS compatibility Expand default foreground command list beyond node/claude to include npx, tsx, bun, deno. Log actual detected process name for debugging. Add configurable foregroundCommands in ~/.claude-auto-retry.json. Co-Authored-By: Claude Opus 4.6 (1M context) --- package.json | 2 +- src/config.js | 5 +++++ src/monitor.js | 8 +++++--- test/monitor.test.js | 15 +++++++++++++++ 4 files changed, 26 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 9d4ac17..d99fb3b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "claude-auto-retry", - "version": "0.2.0", + "version": "0.2.1", "description": "Automatically retry Claude Code sessions when hitting Anthropic subscription rate limits", "type": "module", "bin": { diff --git a/src/config.js b/src/config.js index a8c6a68..054056a 100644 --- a/src/config.js +++ b/src/config.js @@ -28,6 +28,11 @@ function validate(cfg) { if (!Array.isArray(cfg.customPatterns)) { cfg.customPatterns = DEFAULT_CONFIG.customPatterns; } + if (cfg.foregroundCommands !== undefined) { + if (!Array.isArray(cfg.foregroundCommands) || cfg.foregroundCommands.length === 0) { + delete cfg.foregroundCommands; + } + } return cfg; } diff --git a/src/monitor.js b/src/monitor.js index 377871f..72e54b1 100644 --- a/src/monitor.js +++ b/src/monitor.js @@ -4,7 +4,7 @@ import { capturePane, sendKeys, getPaneCommand } from './tmux.js'; import { loadConfig } from './config.js'; import { createLogger } from './logger.js'; -const CLAUDE_COMMANDS = ['node', 'claude']; +const DEFAULT_FOREGROUND_COMMANDS = ['node', 'claude', 'npx', 'tsx', 'bun', 'deno']; export function createMonitorState() { return { status: 'monitoring', waitUntil: 0, attempts: 0, lastRateLimitMessage: null }; @@ -35,9 +35,11 @@ export async function processOneTick(state, tmuxAdapter, pane, config, isAlive) } const fg = await tmuxAdapter.getPaneCommand(pane); - if (!CLAUDE_COMMANDS.some(c => fg.toLowerCase().includes(c))) { + const fgCommands = config.foregroundCommands || DEFAULT_FOREGROUND_COMMANDS; + if (!fgCommands.some(c => fg.toLowerCase().includes(c))) { // Push waitUntil forward to avoid tight-loop polling every tick state.waitUntil = Date.now() + (config.pollIntervalSeconds * 1000 * 6); + state._lastForeground = fg; return 'skipped-not-claude'; } @@ -89,7 +91,7 @@ export async function startMonitor(pane, pid) { if (result === 'retried') await logger.info(`Sent retry message (attempt ${state.attempts})`); if (result === 'user-continued') await logger.info('User already continued. Attempt counter reset.'); if (result === 'max-retries') await logger.warn(`Max retries (${config.maxRetries}) reached. Monitor still active but will not send further retries until rate limit clears.`); - if (result === 'skipped-not-claude') await logger.warn('Foreground is not Claude. Skipping send-keys.'); + if (result === 'skipped-not-claude') await logger.warn(`Foreground is "${state._lastForeground}", not Claude. Skipping send-keys. (Add to foregroundCommands in ~/.claude-auto-retry.json if this is wrong)`); } catch (err) { consecutiveErrors++; await logger.error(`Monitor tick error: ${err.message}`).catch(() => {}); diff --git a/test/monitor.test.js b/test/monitor.test.js index b50c828..900db25 100644 --- a/test/monitor.test.js +++ b/test/monitor.test.js @@ -55,6 +55,21 @@ describe('processOneTick', () => { s.waitUntil = Date.now() - 1000; s.status = 'waiting'; assert.equal(await processOneTick(s, t, '%0', DEFAULT_CONFIG, () => true), 'skipped-not-claude'); assert.equal(t._sent.length, 0); + assert.equal(s._lastForeground, 'vim'); + }); + it('accepts custom foregroundCommands from config', async () => { + const t = mockTmux('5-hour limit reached - resets 3pm (UTC)', 'my-claude-wrapper'); + const s = createMonitorState(); + s.waitUntil = Date.now() - 1000; s.status = 'waiting'; + const config = { ...DEFAULT_CONFIG, foregroundCommands: ['my-claude-wrapper'] }; + assert.equal(await processOneTick(s, t, '%0', config, () => true), 'retried'); + assert.equal(t._sent.length, 1); + }); + it('matches npx as default foreground command', async () => { + const t = mockTmux('5-hour limit reached - resets 3pm (UTC)', 'npx'); + const s = createMonitorState(); + s.waitUntil = Date.now() - 1000; s.status = 'waiting'; + assert.equal(await processOneTick(s, t, '%0', DEFAULT_CONFIG, () => true), 'retried'); }); it('resets counter when rate limit disappears', async () => { const t = mockTmux('Claude is working normally');