1aad1e0313
Auto-retry Claude Code sessions when hitting Anthropic subscription rate limits. Uses tmux monitoring + send-keys to detect rate limit messages, wait for reset, and send "continue" automatically. Zero dependencies, zero workflow change. - Shell wrapper intercepts `claude` command transparently - Background monitor polls tmux pane for rate limit patterns - Timezone-aware reset time parsing with DST safety - Safe send-keys with foreground process verification - --print mode: buffers output, retries cleanly for pipes - Config validation prevents bad values from causing crashes - 59 tests, 0 dependencies Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
83 lines
3.1 KiB
JavaScript
83 lines
3.1 KiB
JavaScript
import { describe, it } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { stripAnsi, isRateLimited, findRateLimitMessage } from '../src/patterns.js';
|
|
|
|
describe('stripAnsi', () => {
|
|
it('removes bold codes', () => {
|
|
assert.equal(stripAnsi('\x1b[1mlimit\x1b[0m'), 'limit');
|
|
});
|
|
it('removes color codes', () => {
|
|
assert.equal(stripAnsi('\x1b[31mred\x1b[0m'), 'red');
|
|
});
|
|
it('removes cursor positioning', () => {
|
|
assert.equal(stripAnsi('\x1b[2Jhello\x1b[H'), 'hello');
|
|
});
|
|
it('leaves plain text unchanged', () => {
|
|
assert.equal(stripAnsi('plain text'), 'plain text');
|
|
});
|
|
it('handles mixed content', () => {
|
|
assert.equal(
|
|
stripAnsi('5-hour \x1b[1mlimit\x1b[0m reached - resets 3pm'),
|
|
'5-hour limit reached - resets 3pm'
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('isRateLimited', () => {
|
|
it('detects "5-hour limit reached"', () => {
|
|
assert.equal(isRateLimited('5-hour limit reached - resets 3pm'), true);
|
|
});
|
|
it('detects "usage limit" with reset', () => {
|
|
assert.equal(isRateLimited('Claude usage limit reached. Resets at 2pm'), true);
|
|
});
|
|
it('detects "out of extra usage"', () => {
|
|
assert.equal(isRateLimited("You're out of extra usage · resets 3pm"), true);
|
|
});
|
|
it('detects "try again in 5 hours"', () => {
|
|
assert.equal(isRateLimited('Please try again in 5 hours'), true);
|
|
});
|
|
it('detects "rate limit resets"', () => {
|
|
assert.equal(isRateLimited('Rate limit hit. Resets at 4pm'), true);
|
|
});
|
|
it('returns false for normal output', () => {
|
|
assert.equal(isRateLimited('I can help you with that code'), false);
|
|
});
|
|
it('returns false for empty string', () => {
|
|
assert.equal(isRateLimited(''), false);
|
|
});
|
|
it('detects rate limit with ANSI codes embedded', () => {
|
|
assert.equal(isRateLimited('5-hour \x1b[1mlimit\x1b[0m reached - resets 3pm'), true);
|
|
});
|
|
it('matches custom patterns', () => {
|
|
assert.equal(isRateLimited('custom error xyz', [/custom error/i]), true);
|
|
});
|
|
it('detects "You\'ve hit your limit" (real Claude Code message)', () => {
|
|
assert.equal(isRateLimited("You've hit your limit · resets 3pm (Asia/Tbilisi)"), true);
|
|
});
|
|
it('detects "hit the limit resets"', () => {
|
|
assert.equal(isRateLimited('You hit the limit. Resets at 5pm'), true);
|
|
});
|
|
it('detects "limit resets in: 3 hours"', () => {
|
|
assert.equal(isRateLimited('limit resets in: 3 hours'), true);
|
|
});
|
|
});
|
|
|
|
describe('stripAnsi (private-mode sequences)', () => {
|
|
it('strips cursor hide sequence', () => {
|
|
assert.equal(stripAnsi('\x1b[?25lhello\x1b[?25h'), 'hello');
|
|
});
|
|
it('strips bracketed paste mode', () => {
|
|
assert.equal(stripAnsi('\x1b[?2004htext\x1b[?2004l'), 'text');
|
|
});
|
|
});
|
|
|
|
describe('findRateLimitMessage', () => {
|
|
it('returns the matching line from multiline input', () => {
|
|
const text = 'Some output\n5-hour limit reached - resets 3pm (Europe/Dublin)\nMore output';
|
|
assert.equal(findRateLimitMessage(text), '5-hour limit reached - resets 3pm (Europe/Dublin)');
|
|
});
|
|
it('returns null when no match', () => {
|
|
assert.equal(findRateLimitMessage('normal output\nmore output'), null);
|
|
});
|
|
});
|