claude-overnight 1.17.0 → 1.17.2
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/auth.d.ts +19 -0
- package/dist/auth.js +82 -0
- package/dist/cli.d.ts +4 -1
- package/dist/cli.js +5 -6
- package/dist/index.js +11 -1
- package/dist/planner-query.js +54 -0
- package/dist/providers.d.ts +22 -0
- package/dist/providers.js +299 -8
- package/dist/run.js +56 -2
- package/dist/swarm.d.ts +2 -0
- package/dist/swarm.js +24 -5
- package/package.json +5 -3
- package/plugins/claude-overnight/.claude-plugin/plugin.json +1 -1
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface JWTPayload {
|
|
2
|
+
sub: string;
|
|
3
|
+
model: string;
|
|
4
|
+
bearer: string;
|
|
5
|
+
aud: string;
|
|
6
|
+
iat: number;
|
|
7
|
+
exp: number;
|
|
8
|
+
}
|
|
9
|
+
export interface TokenRecord {
|
|
10
|
+
signedToken: string;
|
|
11
|
+
payload: JWTPayload;
|
|
12
|
+
}
|
|
13
|
+
export declare function loadSecret(): Buffer;
|
|
14
|
+
export declare function signToken(providerId: string, model: string, bearer: string, baseURL: string): TokenRecord;
|
|
15
|
+
export declare function verifyToken(token: string, providerId: string): JWTPayload | null;
|
|
16
|
+
export declare function refreshToken(oldToken: string, providerId: string): TokenRecord | null;
|
|
17
|
+
export declare function getBearerToken(providerId: string, model: string, bearer: string, baseURL: string): string;
|
|
18
|
+
export declare function clearTokenCache(): void;
|
|
19
|
+
export declare function isJWTAuthError(err: unknown): boolean;
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { homedir } from "os";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { readFileSync, writeFileSync, mkdirSync, chmodSync } from "fs";
|
|
4
|
+
const SECRET_PATH = join(homedir(), ".claude", "claude-overnight", "jwt-secret.key");
|
|
5
|
+
export function loadSecret() {
|
|
6
|
+
try {
|
|
7
|
+
const raw = readFileSync(SECRET_PATH);
|
|
8
|
+
if (raw.length >= 32)
|
|
9
|
+
return raw;
|
|
10
|
+
}
|
|
11
|
+
catch { }
|
|
12
|
+
const secret = cryptoRandomBytes(32);
|
|
13
|
+
mkdirSync(join(homedir(), ".claude", "claude-overnight"), { recursive: true });
|
|
14
|
+
writeFileSync(SECRET_PATH, secret);
|
|
15
|
+
try {
|
|
16
|
+
chmodSync(SECRET_PATH, 0o600);
|
|
17
|
+
}
|
|
18
|
+
catch { }
|
|
19
|
+
return secret;
|
|
20
|
+
}
|
|
21
|
+
function deriveKey(secret, providerId) {
|
|
22
|
+
const crypto = require("crypto");
|
|
23
|
+
return crypto.createHmac("sha256", secret).update(providerId).digest();
|
|
24
|
+
}
|
|
25
|
+
const DEFAULT_TTL_SEC = 300; // 5 minutes
|
|
26
|
+
export function signToken(providerId, model, bearer, baseURL) {
|
|
27
|
+
const jwt = require("jsonwebtoken");
|
|
28
|
+
const secret = loadSecret();
|
|
29
|
+
const key = deriveKey(secret, providerId);
|
|
30
|
+
const now = Math.floor(Date.now() / 1000);
|
|
31
|
+
const payload = { sub: providerId, model, bearer, aud: baseURL, iat: now, exp: now + DEFAULT_TTL_SEC };
|
|
32
|
+
const signedToken = jwt.sign(payload, key, { algorithm: "HS256" });
|
|
33
|
+
return { signedToken, payload };
|
|
34
|
+
}
|
|
35
|
+
export function verifyToken(token, providerId) {
|
|
36
|
+
const jwt = require("jsonwebtoken");
|
|
37
|
+
const secret = loadSecret();
|
|
38
|
+
const key = deriveKey(secret, providerId);
|
|
39
|
+
try {
|
|
40
|
+
return jwt.verify(token, key, { algorithms: ["HS256"] });
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function refreshToken(oldToken, providerId) {
|
|
47
|
+
const payload = verifyToken(oldToken, providerId);
|
|
48
|
+
if (!payload)
|
|
49
|
+
return null;
|
|
50
|
+
const now = Math.floor(Date.now() / 1000);
|
|
51
|
+
if (payload.exp - now > 60)
|
|
52
|
+
return null;
|
|
53
|
+
return signToken(payload.sub, payload.model, payload.bearer, payload.aud);
|
|
54
|
+
}
|
|
55
|
+
const tokenCache = new Map();
|
|
56
|
+
export function getBearerToken(providerId, model, bearer, baseURL) {
|
|
57
|
+
const cached = tokenCache.get(providerId);
|
|
58
|
+
if (cached) {
|
|
59
|
+
const payload = verifyToken(cached.signedToken, providerId);
|
|
60
|
+
if (payload && payload.exp > Math.floor(Date.now() / 1000) + 30) {
|
|
61
|
+
return cached.signedToken;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const fresh = refreshToken(cached?.signedToken ?? "", providerId) ?? signToken(providerId, model, bearer, baseURL);
|
|
65
|
+
tokenCache.set(providerId, fresh);
|
|
66
|
+
return fresh.signedToken;
|
|
67
|
+
}
|
|
68
|
+
export function clearTokenCache() {
|
|
69
|
+
tokenCache.clear();
|
|
70
|
+
}
|
|
71
|
+
function cryptoRandomBytes(length) {
|
|
72
|
+
const crypto = require("crypto");
|
|
73
|
+
return crypto.randomBytes(length);
|
|
74
|
+
}
|
|
75
|
+
export function isJWTAuthError(err) {
|
|
76
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
77
|
+
const lower = msg.toLowerCase();
|
|
78
|
+
return lower.includes("token expired") || lower.includes("invalid_token")
|
|
79
|
+
|| lower.includes("jwt") || lower.includes("signature")
|
|
80
|
+
|| lower.includes("unauthorized") || lower.includes("forbidden")
|
|
81
|
+
|| lower.includes("invalid_api_key") || lower.includes("authentication");
|
|
82
|
+
}
|
package/dist/cli.d.ts
CHANGED
|
@@ -4,7 +4,10 @@ export declare function parseCliFlags(argv: string[]): {
|
|
|
4
4
|
flags: Record<string, string>;
|
|
5
5
|
positional: string[];
|
|
6
6
|
};
|
|
7
|
-
|
|
7
|
+
import { isJWTAuthError } from "./auth.js";
|
|
8
|
+
/** @deprecated Use isJWTAuthError from auth.ts instead. */
|
|
9
|
+
export declare const isAuthError: typeof isJWTAuthError;
|
|
10
|
+
export { isJWTAuthError };
|
|
8
11
|
export declare function fetchModels(timeoutMs?: number): Promise<ModelInfo[]>;
|
|
9
12
|
export declare const PASTE_START = "\u001B[200~";
|
|
10
13
|
export declare const PASTE_END = "\u001B[201~";
|
package/dist/cli.js
CHANGED
|
@@ -29,12 +29,11 @@ export function parseCliFlags(argv) {
|
|
|
29
29
|
}
|
|
30
30
|
return { flags, positional };
|
|
31
31
|
}
|
|
32
|
-
// ── Auth error detection ──
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
32
|
+
// ── Auth error detection (re-exported from auth module for backward compatibility) ──
|
|
33
|
+
import { isJWTAuthError } from "./auth.js";
|
|
34
|
+
/** @deprecated Use isJWTAuthError from auth.ts instead. */
|
|
35
|
+
export const isAuthError = isJWTAuthError;
|
|
36
|
+
export { isJWTAuthError };
|
|
38
37
|
// ── Fetch models via SDK ──
|
|
39
38
|
export async function fetchModels(timeoutMs = 10_000) {
|
|
40
39
|
let q;
|
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
|
10
10
|
import { Swarm } from "./swarm.js";
|
|
11
11
|
import { planTasks, refinePlan, identifyThemes, buildThinkingTasks, orchestrate, salvageFromFile } from "./planner.js";
|
|
12
12
|
import { detectModelTier, setPlannerEnvResolver } from "./planner-query.js";
|
|
13
|
-
import { pickModel, loadProviders, preflightProvider, buildEnvResolver } from "./providers.js";
|
|
13
|
+
import { pickModel, loadProviders, preflightProvider, buildEnvResolver, healthCheckCursorProxy, PROXY_DEFAULT_URL, isCursorProxyProvider } from "./providers.js";
|
|
14
14
|
import { RunDisplay } from "./ui.js";
|
|
15
15
|
import { renderSummary } from "./render.js";
|
|
16
16
|
import { executeRun } from "./run.js";
|
|
@@ -217,6 +217,16 @@ async function main() {
|
|
|
217
217
|
process.exit(1);
|
|
218
218
|
}
|
|
219
219
|
}
|
|
220
|
+
// ── Pre-check: warn if saved Cursor providers exist but proxy is down ──
|
|
221
|
+
const savedCursorProviders = loadProviders().filter(isCursorProxyProvider);
|
|
222
|
+
if (savedCursorProviders.length > 0 && !dryRun) {
|
|
223
|
+
const proxyUp = await healthCheckCursorProxy();
|
|
224
|
+
if (!proxyUp) {
|
|
225
|
+
console.warn(chalk.yellow(`\n ⚠ ${savedCursorProviders.length} Cursor provider(s) saved but proxy is not running at ${PROXY_DEFAULT_URL}`));
|
|
226
|
+
console.warn(chalk.yellow(` Start it: npx cursor-api-proxy`));
|
|
227
|
+
console.warn(chalk.dim(` (Continuing — you can still use Anthropic models)\n`));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
220
230
|
// ── Load tasks ──
|
|
221
231
|
let tasks = [];
|
|
222
232
|
let fileCfg;
|
package/dist/planner-query.js
CHANGED
|
@@ -18,6 +18,11 @@ export function detectModelTier(model) {
|
|
|
18
18
|
return "sonnet";
|
|
19
19
|
if (m.includes("haiku"))
|
|
20
20
|
return "haiku";
|
|
21
|
+
// Cursor API Proxy models
|
|
22
|
+
if (m === "auto")
|
|
23
|
+
return "unknown";
|
|
24
|
+
if (m.startsWith("composer"))
|
|
25
|
+
return "sonnet";
|
|
21
26
|
return "unknown";
|
|
22
27
|
}
|
|
23
28
|
export function modelCapabilityBlock(model) {
|
|
@@ -29,6 +34,10 @@ export function modelCapabilityBlock(model) {
|
|
|
29
34
|
case "haiku":
|
|
30
35
|
return `Each agent runs Claude Haiku -- fast and efficient, best for focused, well-specified tasks. Be explicit about files, functions, and expected changes. Keep each task scoped to a clear, concrete deliverable.`;
|
|
31
36
|
default:
|
|
37
|
+
// Cursor API Proxy or unknown model — generic but mention Cursor context
|
|
38
|
+
if (model.toLowerCase().startsWith("composer") || model.toLowerCase() === "auto") {
|
|
39
|
+
return `Each agent runs a Cursor model with full codebase access. Capable of focused implementation work. Be explicit about files, functions, and expected changes.`;
|
|
40
|
+
}
|
|
32
41
|
return `Each agent has full codebase access and can work autonomously.`;
|
|
33
42
|
}
|
|
34
43
|
}
|
|
@@ -44,6 +53,47 @@ let _plannerRateLimitInfo = {
|
|
|
44
53
|
utilization: 0, status: "", isUsingOverage: false, windows: new Map(), costUsd: 0,
|
|
45
54
|
};
|
|
46
55
|
export function getPlannerRateLimitInfo() { return _plannerRateLimitInfo; }
|
|
56
|
+
// ── Proactive throttle: wait before making API calls when utilization is high ──
|
|
57
|
+
/**
|
|
58
|
+
* Proactive rate-limit gate. Called before each planner/steering query to
|
|
59
|
+
* prevent hammering the API when we're already near a limit.
|
|
60
|
+
*
|
|
61
|
+
* Levels:
|
|
62
|
+
* - rejected -> wait until resetsAt (or 60s fallback)
|
|
63
|
+
* - utilization >= 90% -> wait 30s with exponential backoff
|
|
64
|
+
* - utilization >= 75% -> brief 5s cooldown
|
|
65
|
+
* - utilization < 75% -> pass through immediately
|
|
66
|
+
*/
|
|
67
|
+
async function throttlePlanner(onLog, aborted) {
|
|
68
|
+
const MAX_BACKOFF = 3;
|
|
69
|
+
for (let backoff = 0; backoff <= MAX_BACKOFF; backoff++) {
|
|
70
|
+
if (aborted())
|
|
71
|
+
return;
|
|
72
|
+
const rl = _plannerRateLimitInfo;
|
|
73
|
+
const rejected = rl.resetsAt && rl.resetsAt > Date.now();
|
|
74
|
+
const highUtil = rl.utilization >= 0.9;
|
|
75
|
+
const elevatedUtil = rl.utilization >= 0.75;
|
|
76
|
+
if (!rejected && !highUtil && !elevatedUtil)
|
|
77
|
+
return;
|
|
78
|
+
const waitMs = rejected
|
|
79
|
+
? Math.max(5000, rl.resetsAt - Date.now())
|
|
80
|
+
: highUtil
|
|
81
|
+
? 30_000 * (1 + backoff)
|
|
82
|
+
: 5000;
|
|
83
|
+
const reason = rejected ? "Rate limited" : `Utilization ${Math.round(rl.utilization * 100)}%`;
|
|
84
|
+
onLog(`${reason} -- waiting ${Math.ceil(waitMs / 1000)}s before query${backoff > 0 ? ` (backoff ${backoff})` : ""}`, "event");
|
|
85
|
+
await new Promise((r) => setTimeout(r, waitMs));
|
|
86
|
+
if (aborted())
|
|
87
|
+
return;
|
|
88
|
+
// After a wait, clear the rejected flag so we don't loop forever if
|
|
89
|
+
// the SDK stopped sending updates.
|
|
90
|
+
if (rejected && rl.resetsAt && rl.resetsAt <= Date.now()) {
|
|
91
|
+
rl.resetsAt = undefined;
|
|
92
|
+
rl.utilization = 0;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Exhausted backoffs — proceed anyway, the retry loop will catch a rejection.
|
|
96
|
+
}
|
|
47
97
|
// ── Query execution ──
|
|
48
98
|
const NUDGE_MS = 15 * 60 * 1000;
|
|
49
99
|
const HARD_TIMEOUT_MS = 30 * 60 * 1000;
|
|
@@ -53,8 +103,11 @@ export async function runPlannerQuery(prompt, opts, onLog) {
|
|
|
53
103
|
const BACKOFF = [30_000, 60_000, 120_000];
|
|
54
104
|
let currentPrompt = prompt;
|
|
55
105
|
let currentOpts = opts;
|
|
106
|
+
let aborted = false;
|
|
56
107
|
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
57
108
|
try {
|
|
109
|
+
// Proactive throttle: wait if utilization is already high
|
|
110
|
+
await throttlePlanner(onLog, () => aborted);
|
|
58
111
|
return await runPlannerQueryOnce(currentPrompt, currentOpts, onLog);
|
|
59
112
|
}
|
|
60
113
|
catch (err) {
|
|
@@ -78,6 +131,7 @@ export async function runPlannerQuery(prompt, opts, onLog) {
|
|
|
78
131
|
throw err;
|
|
79
132
|
}
|
|
80
133
|
}
|
|
134
|
+
aborted = true;
|
|
81
135
|
throw new Error("Planner query failed after retries");
|
|
82
136
|
}
|
|
83
137
|
async function runPlannerQueryOnce(prompt, opts, onLog) {
|
package/dist/providers.d.ts
CHANGED
|
@@ -13,6 +13,10 @@ export interface ProviderConfig {
|
|
|
13
13
|
keyEnv?: string;
|
|
14
14
|
/** Inline API key. Stored plaintext in providers.json (mode 0600). */
|
|
15
15
|
key?: string;
|
|
16
|
+
/** When true, use JWT token auth instead of raw API keys. The bearer token is embedded in a short-lived JWT. */
|
|
17
|
+
useJWT?: boolean;
|
|
18
|
+
/** When true, this provider routes through cursor-api-proxy (special env/health-check handling). */
|
|
19
|
+
cursorProxy?: boolean;
|
|
16
20
|
}
|
|
17
21
|
export declare function getStorePath(): string;
|
|
18
22
|
export declare function loadProviders(): ProviderConfig[];
|
|
@@ -47,6 +51,24 @@ export declare function preflightProvider(p: ProviderConfig, cwd: string, timeou
|
|
|
47
51
|
ok: false;
|
|
48
52
|
error: string;
|
|
49
53
|
}>;
|
|
54
|
+
export declare const PROXY_DEFAULT_URL = "http://127.0.0.1:8765";
|
|
55
|
+
/** Check if a provider routes through cursor-api-proxy. */
|
|
56
|
+
export declare function isCursorProxyProvider(p: ProviderConfig): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Health check: GET /health on the proxy. Returns true if proxy is reachable.
|
|
59
|
+
*/
|
|
60
|
+
export declare function healthCheckCursorProxy(baseUrl?: string): Promise<boolean>;
|
|
61
|
+
/**
|
|
62
|
+
* Fetch available Cursor models via GET /v1/models on the proxy.
|
|
63
|
+
* Returns model IDs like ["auto", "composer", "composer-2", "opus-4.6", ...].
|
|
64
|
+
*/
|
|
65
|
+
export declare function fetchCursorModels(baseUrl?: string): Promise<string[]>;
|
|
66
|
+
/**
|
|
67
|
+
* Interactive setup guide for cursor-api-proxy.
|
|
68
|
+
* Walks through CLI install, login, and proxy start.
|
|
69
|
+
* Returns true when proxy is running and healthy.
|
|
70
|
+
*/
|
|
71
|
+
export declare function setupCursorProxy(): Promise<boolean>;
|
|
50
72
|
export type EnvResolver = (model?: string) => Record<string, string> | undefined;
|
|
51
73
|
/**
|
|
52
74
|
* Build a single resolver that swarm.ts and planner-query.ts share. Maps a
|
package/dist/providers.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from "fs";
|
|
2
2
|
import { homedir } from "os";
|
|
3
3
|
import { join } from "path";
|
|
4
|
+
import { execSync } from "child_process";
|
|
4
5
|
import chalk from "chalk";
|
|
5
6
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
6
|
-
import { ask, select } from "./cli.js";
|
|
7
|
+
import { ask, select, selectKey } from "./cli.js";
|
|
8
|
+
import { getBearerToken, clearTokenCache } from "./auth.js";
|
|
7
9
|
// ── Store ──
|
|
8
10
|
const STORE_PATH = join(homedir(), ".claude", "claude-overnight", "providers.json");
|
|
9
11
|
export function getStorePath() { return STORE_PATH; }
|
|
@@ -26,6 +28,7 @@ export function saveProvider(p) {
|
|
|
26
28
|
chmodSync(STORE_PATH, 0o600);
|
|
27
29
|
}
|
|
28
30
|
catch { }
|
|
31
|
+
clearTokenCache();
|
|
29
32
|
}
|
|
30
33
|
export function deleteProvider(id) {
|
|
31
34
|
const all = loadProviders().filter(x => x.id !== id);
|
|
@@ -36,6 +39,7 @@ export function deleteProvider(id) {
|
|
|
36
39
|
chmodSync(STORE_PATH, 0o600);
|
|
37
40
|
}
|
|
38
41
|
catch { }
|
|
42
|
+
clearTokenCache();
|
|
39
43
|
}
|
|
40
44
|
function isValidProvider(p) {
|
|
41
45
|
return p && typeof p.id === "string" && typeof p.baseURL === "string"
|
|
@@ -55,15 +59,27 @@ export function resolveKey(p) {
|
|
|
55
59
|
* you pass `options.env`.
|
|
56
60
|
*/
|
|
57
61
|
export function envFor(p) {
|
|
58
|
-
const key = resolveKey(p);
|
|
59
|
-
if (!key)
|
|
60
|
-
throw new Error(`Provider "${p.id}" has no API key (${p.keyEnv ? `env ${p.keyEnv} is empty` : "inline key missing"})`);
|
|
61
62
|
const base = {};
|
|
62
63
|
for (const [k, v] of Object.entries(process.env))
|
|
63
64
|
if (v !== undefined)
|
|
64
65
|
base[k] = v;
|
|
66
|
+
if (p.cursorProxy) {
|
|
67
|
+
// cursor-api-proxy: routes through local proxy, no real API key needed
|
|
68
|
+
base.ANTHROPIC_BASE_URL = p.baseURL;
|
|
69
|
+
base.ANTHROPIC_AUTH_TOKEN = process.env.CURSOR_BRIDGE_API_KEY || "unused";
|
|
70
|
+
delete base.ANTHROPIC_API_KEY;
|
|
71
|
+
return base;
|
|
72
|
+
}
|
|
73
|
+
const key = resolveKey(p);
|
|
74
|
+
if (!key)
|
|
75
|
+
throw new Error(`Provider "${p.id}" has no API key (${p.keyEnv ? `env ${p.keyEnv} is empty` : "inline key missing"})`);
|
|
65
76
|
base.ANTHROPIC_BASE_URL = p.baseURL;
|
|
66
|
-
|
|
77
|
+
if (p.useJWT) {
|
|
78
|
+
base.ANTHROPIC_AUTH_TOKEN = getBearerToken(p.id, p.model, key, p.baseURL);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
base.ANTHROPIC_AUTH_TOKEN = key;
|
|
82
|
+
}
|
|
67
83
|
delete base.ANTHROPIC_API_KEY;
|
|
68
84
|
return base;
|
|
69
85
|
}
|
|
@@ -90,8 +106,10 @@ export async function pickModel(label, anthropicModels, currentModelId) {
|
|
|
90
106
|
}
|
|
91
107
|
for (const p of saved) {
|
|
92
108
|
const keySrc = p.keyEnv ? `env ${p.keyEnv}` : "stored key";
|
|
93
|
-
|
|
109
|
+
const cursorTag = p.cursorProxy ? chalk.dim(" · cursor") : "";
|
|
110
|
+
items.push({ name: `${p.displayName}${cursorTag}`, value: { kind: "provider", provider: p }, hint: `${p.model} · ${keySrc}` });
|
|
94
111
|
}
|
|
112
|
+
items.push({ name: chalk.green("Cursor…"), value: { kind: "cursor" }, hint: "Cursor API Proxy — composer, composer-2, auto, etc." });
|
|
95
113
|
items.push({ name: chalk.cyan("Other…"), value: { kind: "other" }, hint: "Qwen 3.6 Plus, OpenRouter, or any Anthropic-compatible endpoint" });
|
|
96
114
|
let defaultIdx = 0;
|
|
97
115
|
if (currentModelId) {
|
|
@@ -111,6 +129,13 @@ export async function pickModel(label, anthropicModels, currentModelId) {
|
|
|
111
129
|
if (picked.kind === "provider") {
|
|
112
130
|
return { model: picked.provider.model, providerId: picked.provider.id, provider: picked.provider };
|
|
113
131
|
}
|
|
132
|
+
if (picked.kind === "cursor") {
|
|
133
|
+
const cursorPick = await pickCursorModel();
|
|
134
|
+
if (cursorPick)
|
|
135
|
+
return cursorPick;
|
|
136
|
+
// user cancelled cursor picker — loop back
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
114
139
|
const added = await promptNewProvider();
|
|
115
140
|
if (added) {
|
|
116
141
|
saveProvider(added);
|
|
@@ -144,12 +169,20 @@ async function promptNewProvider() {
|
|
|
144
169
|
if (!process.env[envName]) {
|
|
145
170
|
console.log(chalk.yellow(`\n ⚠ ${envName} is not set in the current shell -- you'll need to export it before running.`));
|
|
146
171
|
}
|
|
147
|
-
|
|
172
|
+
const useJWT = await select(` ${chalk.cyan("Auth method")}:`, [
|
|
173
|
+
{ name: "JWT tokens", value: "jwt", hint: "short-lived tokens, raw keys never passed to agents" },
|
|
174
|
+
{ name: "Raw API key", value: "raw", hint: "key sent directly with every request" },
|
|
175
|
+
]);
|
|
176
|
+
return { id, displayName, baseURL, model, keyEnv: envName, useJWT: useJWT === "jwt" };
|
|
148
177
|
}
|
|
149
178
|
const key = await ask(`\n ${chalk.cyan("API key")}: `);
|
|
150
179
|
if (!key)
|
|
151
180
|
return null;
|
|
152
|
-
|
|
181
|
+
const useJWT = await select(` ${chalk.cyan("Auth method")}:`, [
|
|
182
|
+
{ name: "JWT tokens", value: "jwt", hint: "short-lived tokens, raw keys never passed to agents" },
|
|
183
|
+
{ name: "Raw API key", value: "raw", hint: "key sent directly with every request" },
|
|
184
|
+
]);
|
|
185
|
+
return { id, displayName, baseURL, model, key, useJWT: useJWT === "jwt" };
|
|
153
186
|
}
|
|
154
187
|
function slugify(s) {
|
|
155
188
|
return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 32) || "provider";
|
|
@@ -221,6 +254,264 @@ export async function preflightProvider(p, cwd, timeoutMs = 20_000) {
|
|
|
221
254
|
catch { }
|
|
222
255
|
}
|
|
223
256
|
}
|
|
257
|
+
// ── Cursor API Proxy ──
|
|
258
|
+
export const PROXY_DEFAULT_URL = "http://127.0.0.1:8765";
|
|
259
|
+
/** Check if a provider routes through cursor-api-proxy. */
|
|
260
|
+
export function isCursorProxyProvider(p) {
|
|
261
|
+
return p.cursorProxy === true || p.baseURL === PROXY_DEFAULT_URL;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Health check: GET /health on the proxy. Returns true if proxy is reachable.
|
|
265
|
+
*/
|
|
266
|
+
export async function healthCheckCursorProxy(baseUrl = PROXY_DEFAULT_URL) {
|
|
267
|
+
const url = `${baseUrl.replace(/\/$/, "")}/health`;
|
|
268
|
+
try {
|
|
269
|
+
const res = await fetch(url, { method: "GET", signal: AbortSignal.timeout(3_000) });
|
|
270
|
+
return res.ok;
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Fetch available Cursor models via GET /v1/models on the proxy.
|
|
278
|
+
* Returns model IDs like ["auto", "composer", "composer-2", "opus-4.6", ...].
|
|
279
|
+
*/
|
|
280
|
+
export async function fetchCursorModels(baseUrl = PROXY_DEFAULT_URL) {
|
|
281
|
+
const url = `${baseUrl.replace(/\/$/, "")}/v1/models`;
|
|
282
|
+
try {
|
|
283
|
+
const res = await fetch(url, { method: "GET", signal: AbortSignal.timeout(5_000) });
|
|
284
|
+
if (!res.ok)
|
|
285
|
+
return [];
|
|
286
|
+
const json = await res.json();
|
|
287
|
+
return (json.data || []).map(m => m.id).filter(Boolean);
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
return [];
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Known Cursor model recommendations — short hints to guide users.
|
|
295
|
+
*/
|
|
296
|
+
const CURSOR_MODEL_HINTS = {
|
|
297
|
+
"auto": "fast — delegates to best available model",
|
|
298
|
+
"composer": "Cursor Composer — good for focused tasks",
|
|
299
|
+
"composer-2": "Cursor Composer 2 — latest, strongest Cursor model",
|
|
300
|
+
};
|
|
301
|
+
function cursorModelHint(modelId) {
|
|
302
|
+
const m = modelId.toLowerCase();
|
|
303
|
+
if (CURSOR_MODEL_HINTS[m])
|
|
304
|
+
return CURSOR_MODEL_HINTS[m];
|
|
305
|
+
if (m.includes("opus"))
|
|
306
|
+
return "Opus-tier Cursor model";
|
|
307
|
+
if (m.includes("sonnet"))
|
|
308
|
+
return "Sonnet-tier Cursor model";
|
|
309
|
+
if (m.includes("haiku"))
|
|
310
|
+
return "Haiku-tier Cursor model (fast)";
|
|
311
|
+
return "Cursor model";
|
|
312
|
+
}
|
|
313
|
+
function setupSteps() {
|
|
314
|
+
return [
|
|
315
|
+
{
|
|
316
|
+
label: "Cursor agent CLI",
|
|
317
|
+
check: () => {
|
|
318
|
+
try {
|
|
319
|
+
execSync("which agent", { stdio: "pipe" });
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
},
|
|
326
|
+
autoCmd: "curl https://cursor.com/install -fsS | bash",
|
|
327
|
+
manualCmd: "curl https://cursor.com/install -fsS | bash",
|
|
328
|
+
successMsg: "Cursor CLI found",
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
label: "Cursor authentication",
|
|
332
|
+
check: () => {
|
|
333
|
+
try {
|
|
334
|
+
const out = execSync("agent --list-models", { stdio: "pipe", timeout: 10_000 });
|
|
335
|
+
return out.toString().trim().length > 0;
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
autoCmd: "agent login",
|
|
342
|
+
manualCmd: "agent login",
|
|
343
|
+
successMsg: "Cursor authenticated",
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
label: "cursor-api-proxy server",
|
|
347
|
+
check: () => {
|
|
348
|
+
try {
|
|
349
|
+
execSync("npx cursor-api-proxy --help", { stdio: "pipe", timeout: 10_000 });
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
},
|
|
356
|
+
autoCmd: "npx cursor-api-proxy",
|
|
357
|
+
manualCmd: "npx cursor-api-proxy",
|
|
358
|
+
successMsg: "cursor-api-proxy available",
|
|
359
|
+
},
|
|
360
|
+
];
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Interactive setup guide for cursor-api-proxy.
|
|
364
|
+
* Walks through CLI install, login, and proxy start.
|
|
365
|
+
* Returns true when proxy is running and healthy.
|
|
366
|
+
*/
|
|
367
|
+
export async function setupCursorProxy() {
|
|
368
|
+
console.log(chalk.dim("\n Cursor API Proxy Setup"));
|
|
369
|
+
console.log(chalk.dim(" " + "─".repeat(40)));
|
|
370
|
+
console.log(chalk.dim(" We need three things: Cursor CLI, authentication, and the proxy server.\n"));
|
|
371
|
+
const steps = setupSteps();
|
|
372
|
+
for (const step of steps) {
|
|
373
|
+
if (step.check()) {
|
|
374
|
+
console.log(chalk.green(` ✓ ${step.successMsg}`));
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
console.log(chalk.yellow(`\n ${step.label} not found`));
|
|
378
|
+
const choice = await selectKey(` Set up ${step.label}:`, [
|
|
379
|
+
{ key: "a", desc: "uto (run command)" },
|
|
380
|
+
{ key: "m", desc: "anual (show command)" },
|
|
381
|
+
{ key: "s", desc: "kip (I'll handle it)" },
|
|
382
|
+
]);
|
|
383
|
+
if (choice === "a") {
|
|
384
|
+
if (step.label === "Cursor authentication") {
|
|
385
|
+
// agent login needs interactive browser — run it directly
|
|
386
|
+
console.log(chalk.dim(` Running: ${step.autoCmd}`));
|
|
387
|
+
console.log(chalk.dim(" (A browser window will open for login)\n"));
|
|
388
|
+
try {
|
|
389
|
+
execSync(step.autoCmd, { stdio: "inherit", timeout: 120_000 });
|
|
390
|
+
console.log(chalk.green(` ✓ ${step.successMsg}`));
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
console.log(chalk.yellow(" Login failed — try manual mode"));
|
|
394
|
+
// Fall through to manual display
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
else if (step.label === "cursor-api-proxy server") {
|
|
398
|
+
// Don't auto-start the proxy server here — it blocks. Just verify it's installable.
|
|
399
|
+
console.log(chalk.dim(` Install check: ${step.autoCmd} --help`));
|
|
400
|
+
try {
|
|
401
|
+
execSync("npx cursor-api-proxy --help", { stdio: "pipe", timeout: 30_000 });
|
|
402
|
+
console.log(chalk.green(` ✓ cursor-api-proxy installed`));
|
|
403
|
+
console.log(chalk.yellow(` → Start it in another terminal: ${chalk.bold("npx cursor-api-proxy")}`));
|
|
404
|
+
const ready = await selectKey(` Is the proxy running now?`, [
|
|
405
|
+
{ key: "y", desc: "es" },
|
|
406
|
+
{ key: "n", desc: "ot yet" },
|
|
407
|
+
]);
|
|
408
|
+
if (ready === "y") {
|
|
409
|
+
if (await healthCheckCursorProxy()) {
|
|
410
|
+
console.log(chalk.green(` ✓ Proxy connected`));
|
|
411
|
+
return true;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
console.log(chalk.red(" cursor-api-proxy not installed. Install with: npm install -g cursor-api-proxy"));
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
else {
|
|
420
|
+
console.log(chalk.dim(` Running: ${step.autoCmd}`));
|
|
421
|
+
try {
|
|
422
|
+
execSync(step.autoCmd, { stdio: "inherit", timeout: 60_000 });
|
|
423
|
+
console.log(chalk.green(` ✓ ${step.successMsg}`));
|
|
424
|
+
}
|
|
425
|
+
catch {
|
|
426
|
+
console.log(chalk.yellow(" Command failed — try manual mode"));
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
else if (choice === "m") {
|
|
431
|
+
console.log(chalk.cyan(`\n Run this command:`));
|
|
432
|
+
console.log(chalk.white(` ${step.manualCmd}`));
|
|
433
|
+
if (step.label === "cursor-api-proxy server") {
|
|
434
|
+
console.log(chalk.yellow(` Then start the proxy: ${chalk.bold("npx cursor-api-proxy")}`));
|
|
435
|
+
}
|
|
436
|
+
console.log();
|
|
437
|
+
const done = await selectKey(` Done?`, [
|
|
438
|
+
{ key: "y", desc: "es" },
|
|
439
|
+
{ key: "n", desc: "ot yet" },
|
|
440
|
+
]);
|
|
441
|
+
if (done === "y" && step.label === "cursor-api-proxy server") {
|
|
442
|
+
if (await healthCheckCursorProxy()) {
|
|
443
|
+
console.log(chalk.green(` ✓ Proxy connected`));
|
|
444
|
+
return true;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
console.log(chalk.dim(` Skipped: ${step.label}`));
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
// Final health check
|
|
453
|
+
if (await healthCheckCursorProxy()) {
|
|
454
|
+
console.log(chalk.green("\n ✓ Proxy is running and healthy"));
|
|
455
|
+
return true;
|
|
456
|
+
}
|
|
457
|
+
console.log(chalk.yellow("\n Proxy not reachable yet. You can start it later and add it via 'Cursor' in the model picker."));
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
// ── Cursor model picker sub-flow ──
|
|
461
|
+
async function pickCursorModel() {
|
|
462
|
+
console.log(chalk.dim("\n Cursor API Proxy Models"));
|
|
463
|
+
console.log(chalk.dim(" " + "─".repeat(40)));
|
|
464
|
+
// Quick health check with spinner
|
|
465
|
+
let frame = 0;
|
|
466
|
+
const BRAILLE = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
467
|
+
const spinner = setInterval(() => {
|
|
468
|
+
process.stdout.write(`\x1B[2K\r ${chalk.cyan(BRAILLE[frame++ % BRAILLE.length])} ${chalk.dim("checking proxy...")}`);
|
|
469
|
+
}, 120);
|
|
470
|
+
const healthy = await healthCheckCursorProxy();
|
|
471
|
+
clearInterval(spinner);
|
|
472
|
+
process.stdout.write("\x1B[2K\r");
|
|
473
|
+
if (!healthy) {
|
|
474
|
+
console.log(chalk.yellow(" Proxy is not running at " + PROXY_DEFAULT_URL));
|
|
475
|
+
const choice = await selectKey(` What next?`, [
|
|
476
|
+
{ key: "s", desc: "etup guide" },
|
|
477
|
+
{ key: "r", desc: "etry" },
|
|
478
|
+
{ key: "c", desc: "ancel" },
|
|
479
|
+
]);
|
|
480
|
+
if (choice === "s") {
|
|
481
|
+
const ok = await setupCursorProxy();
|
|
482
|
+
if (!ok)
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
else if (choice === "r") {
|
|
486
|
+
return pickCursorModel();
|
|
487
|
+
}
|
|
488
|
+
else {
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
// Fetch live models
|
|
493
|
+
const modelIds = await fetchCursorModels();
|
|
494
|
+
if (modelIds.length === 0) {
|
|
495
|
+
console.log(chalk.yellow(" No models returned from proxy"));
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
const picked = await select(" Select a Cursor model:", modelIds.map(id => ({
|
|
499
|
+
name: id,
|
|
500
|
+
value: id,
|
|
501
|
+
hint: cursorModelHint(id),
|
|
502
|
+
})), 0);
|
|
503
|
+
// Save as a cursor proxy provider
|
|
504
|
+
const provider = {
|
|
505
|
+
id: `cursor-${picked}`,
|
|
506
|
+
displayName: `Cursor: ${picked}`,
|
|
507
|
+
baseURL: PROXY_DEFAULT_URL,
|
|
508
|
+
model: picked,
|
|
509
|
+
cursorProxy: true,
|
|
510
|
+
};
|
|
511
|
+
saveProvider(provider);
|
|
512
|
+
console.log(chalk.green(` ✓ Saved as provider: ${provider.displayName}`));
|
|
513
|
+
return { model: picked, providerId: provider.id, provider };
|
|
514
|
+
}
|
|
224
515
|
/**
|
|
225
516
|
* Build a single resolver that swarm.ts and planner-query.ts share. Maps a
|
|
226
517
|
* model string to the env overrides that should be passed to `query()`.
|
package/dist/run.js
CHANGED
|
@@ -9,7 +9,8 @@ import { buildEnvResolver } from "./providers.js";
|
|
|
9
9
|
import { RunDisplay } from "./ui.js";
|
|
10
10
|
import { renderSummary } from "./render.js";
|
|
11
11
|
import { fmtTokens } from "./render.js";
|
|
12
|
-
import {
|
|
12
|
+
import { isJWTAuthError } from "./auth.js";
|
|
13
|
+
import { selectKey, ask } from "./cli.js";
|
|
13
14
|
import { readRunMemory, writeStatus, writeGoalUpdate, saveRunState, saveWaveSession, loadWaveHistory, recordBranches, archiveMilestone, writeSteerInbox, consumeSteerInbox, countSteerInbox, appendOvernightLogStart, updateOvernightLogEnd, } from "./state.js";
|
|
14
15
|
export async function executeRun(cfg) {
|
|
15
16
|
const restore = () => { try {
|
|
@@ -353,6 +354,11 @@ export async function executeRun(cfg) {
|
|
|
353
354
|
currentTasks = currentTasks.slice(0, remaining);
|
|
354
355
|
syncRunInfo();
|
|
355
356
|
saveRunState(runDir, buildRunState({ remaining, phase: "steering", currentTasks }));
|
|
357
|
+
// Pre-wave rate limit gate: don't spawn a new wave if the API is already
|
|
358
|
+
// near a limit. This prevents wasting sessions on instant rejections.
|
|
359
|
+
await throttleBeforeWave(() => getPlannerRateLimitInfo(), (text) => display.appendSteeringEvent(text), () => stopping);
|
|
360
|
+
if (stopping)
|
|
361
|
+
break;
|
|
356
362
|
const swarm = new Swarm({
|
|
357
363
|
tasks: currentTasks, concurrency, cwd, model: workerModel, permissionMode, allowedTools,
|
|
358
364
|
useWorktrees, mergeStrategy: waveMerge, agentTimeoutMs: cfg.agentTimeoutMs,
|
|
@@ -366,7 +372,7 @@ export async function executeRun(cfg) {
|
|
|
366
372
|
await swarm.run();
|
|
367
373
|
}
|
|
368
374
|
catch (err) {
|
|
369
|
-
if (
|
|
375
|
+
if (isJWTAuthError(err)) {
|
|
370
376
|
display.stop();
|
|
371
377
|
restore();
|
|
372
378
|
console.error(chalk.red(`\n Authentication failed -- check your API key or run: claude auth\n`));
|
|
@@ -751,3 +757,51 @@ function checkProjectHealth(cwd) {
|
|
|
751
757
|
};
|
|
752
758
|
}
|
|
753
759
|
}
|
|
760
|
+
// ── Pre-wave rate limit gate ──
|
|
761
|
+
function sleep(ms) {
|
|
762
|
+
return new Promise(r => setTimeout(r, ms));
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Proactive rate-limit gate called before spawning a new wave. Prevents
|
|
766
|
+
* starting a batch of agents when the API is already near or at a limit,
|
|
767
|
+
* which would waste sessions on instant rejections.
|
|
768
|
+
*
|
|
769
|
+
* Thresholds:
|
|
770
|
+
* - any window rejected → wait until resetsAt (or 60s fallback)
|
|
771
|
+
* - utilization >= 90% → wait 60s
|
|
772
|
+
* - utilization >= 75% → wait 15s
|
|
773
|
+
*/
|
|
774
|
+
async function throttleBeforeWave(getRL, log, shouldStop) {
|
|
775
|
+
const MAX_ATTEMPTS = 4;
|
|
776
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
777
|
+
if (shouldStop())
|
|
778
|
+
return;
|
|
779
|
+
const rl = getRL();
|
|
780
|
+
// Check for rejected windows
|
|
781
|
+
let rejectedReset;
|
|
782
|
+
for (const w of rl.windows.values()) {
|
|
783
|
+
if (w.status === "rejected" && w.resetsAt && w.resetsAt > Date.now()) {
|
|
784
|
+
if (!rejectedReset || w.resetsAt < rejectedReset)
|
|
785
|
+
rejectedReset = w.resetsAt;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
const highUtil = rl.utilization >= 0.9;
|
|
789
|
+
const elevatedUtil = rl.utilization >= 0.75;
|
|
790
|
+
const explicitRejected = rl.resetsAt && rl.resetsAt > Date.now();
|
|
791
|
+
if (!rejectedReset && !explicitRejected && !highUtil && !elevatedUtil)
|
|
792
|
+
return;
|
|
793
|
+
const waitMs = rejectedReset
|
|
794
|
+
? Math.max(10_000, rejectedReset - Date.now())
|
|
795
|
+
: explicitRejected
|
|
796
|
+
? Math.max(10_000, rl.resetsAt - Date.now())
|
|
797
|
+
: highUtil
|
|
798
|
+
? 60_000 * (1 + attempt)
|
|
799
|
+
: 15_000;
|
|
800
|
+
const reason = rejectedReset ? `Rate limit window blocked`
|
|
801
|
+
: explicitRejected ? "Rate limited"
|
|
802
|
+
: `Utilization ${Math.round(rl.utilization * 100)}%`;
|
|
803
|
+
log(`${reason} -- waiting ${Math.ceil(waitMs / 1000)}s before wave${attempt > 0 ? ` (attempt ${attempt + 1})` : ""}`);
|
|
804
|
+
await sleep(waitMs);
|
|
805
|
+
}
|
|
806
|
+
// Exhausted attempts — proceed anyway, swarm's internal retry will handle rejections.
|
|
807
|
+
}
|
package/dist/swarm.d.ts
CHANGED
|
@@ -110,6 +110,8 @@ export declare class Swarm {
|
|
|
110
110
|
private checkStall;
|
|
111
111
|
private capForOverage;
|
|
112
112
|
private throttle;
|
|
113
|
+
/** Returns the nearest future resetsAt from any rejected window, or undefined. */
|
|
114
|
+
private windowRejectedReset;
|
|
113
115
|
private runAgent;
|
|
114
116
|
private agentSummary;
|
|
115
117
|
private handleMsg;
|
package/dist/swarm.js
CHANGED
|
@@ -377,15 +377,23 @@ export class Swarm {
|
|
|
377
377
|
const cap = this.usageCap;
|
|
378
378
|
const capExceeded = cap != null && cap < 1 && this.rateLimitUtilization >= cap;
|
|
379
379
|
const rejected = this.rateLimitResetsAt && this.rateLimitResetsAt > Date.now();
|
|
380
|
-
|
|
380
|
+
// Proactive: check per-window rejections even when rateLimitResetsAt isn't set
|
|
381
|
+
const windowRejected = this.windowRejectedReset();
|
|
382
|
+
// Proactive: near-critical utilization (no cap set but API is clearly strained)
|
|
383
|
+
const nearCritical = cap == null && this.rateLimitUtilization >= 0.95;
|
|
384
|
+
if (!capExceeded && !rejected && !windowRejected && !nearCritical)
|
|
381
385
|
break;
|
|
382
386
|
const fallbackMs = Math.min(300_000, 60_000 * (1 + consecutiveWaits * 2));
|
|
383
|
-
const waitMs =
|
|
384
|
-
? Math.max(5000, this.rateLimitResetsAt - Date.now())
|
|
385
|
-
:
|
|
387
|
+
const waitMs = (rejected || windowRejected)
|
|
388
|
+
? Math.max(5000, (windowRejected ?? this.rateLimitResetsAt) - Date.now())
|
|
389
|
+
: nearCritical
|
|
390
|
+
? 30_000 * (1 + consecutiveWaits)
|
|
391
|
+
: fallbackMs;
|
|
386
392
|
const reason = capExceeded
|
|
387
393
|
? `Usage at ${Math.round(this.rateLimitUtilization * 100)}% (cap ${Math.round(cap * 100)}%)`
|
|
388
|
-
:
|
|
394
|
+
: nearCritical
|
|
395
|
+
? `Near-critical utilization ${Math.round(this.rateLimitUtilization * 100)}%`
|
|
396
|
+
: `Rate limited${this.windowTag()}`;
|
|
389
397
|
this.log(-1, `${reason} -- waiting ${Math.ceil(waitMs / 1000)}s then retrying ([r] to retry now)`);
|
|
390
398
|
this.rateLimitPaused++;
|
|
391
399
|
await this.rateLimitSleep(waitMs);
|
|
@@ -398,6 +406,17 @@ export class Swarm {
|
|
|
398
406
|
return;
|
|
399
407
|
}
|
|
400
408
|
}
|
|
409
|
+
/** Returns the nearest future resetsAt from any rejected window, or undefined. */
|
|
410
|
+
windowRejectedReset() {
|
|
411
|
+
let nearest;
|
|
412
|
+
for (const w of this.rateLimitWindows.values()) {
|
|
413
|
+
if (w.status === "rejected" && w.resetsAt && w.resetsAt > Date.now()) {
|
|
414
|
+
if (!nearest || w.resetsAt < nearest)
|
|
415
|
+
nearest = w.resetsAt;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return nearest;
|
|
419
|
+
}
|
|
401
420
|
// ── Agent execution ──
|
|
402
421
|
async runAgent(task) {
|
|
403
422
|
const id = this.nextId++;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-overnight",
|
|
3
|
-
"version": "1.17.
|
|
3
|
+
"version": "1.17.2",
|
|
4
4
|
"description": "Background lane for your Claude Max plan. Parallel Claude Agent SDK sessions in git worktrees with a usage cap that reserves headroom for your interactive Claude Code. Crash-safe resume. Opus/Sonnet/Haiku + Qwen/OpenRouter.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,13 +15,15 @@
|
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@anthropic-ai/claude-agent-sdk": "^0.2.92",
|
|
18
|
-
"chalk": "^5.4.1"
|
|
18
|
+
"chalk": "^5.4.1",
|
|
19
|
+
"jsonwebtoken": "^9.0.2"
|
|
19
20
|
},
|
|
20
21
|
"devDependencies": {
|
|
21
22
|
"@types/node": "^22.0.0",
|
|
22
23
|
"node-pty": "^1.1.0",
|
|
23
24
|
"strip-ansi": "^7.1.0",
|
|
24
|
-
"typescript": "^5.7.0"
|
|
25
|
+
"typescript": "^5.7.0",
|
|
26
|
+
"@types/jsonwebtoken": "^9.0.7"
|
|
25
27
|
},
|
|
26
28
|
"license": "MIT",
|
|
27
29
|
"author": "Francesco Fornace",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-overnight",
|
|
3
|
-
"version": "1.17.
|
|
3
|
+
"version": "1.17.2",
|
|
4
4
|
"description": "Claude Code skill for understanding, installing, and inspecting claude-overnight runs -- parallel Claude agents in git worktrees with thinking waves, multi-wave steering, and crash-safe resume.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Francesco Fornace"
|