claude-overnight 1.17.2 → 1.18.0

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
@@ -4,6 +4,8 @@
4
4
 
5
5
  Your Max plan rate limits eat interactive coding time. One deep refactor and the 5-hour window is gone before lunch. `claude-overnight` runs background agent sessions up to the percentage cap you pick (90% is typical), leaving the rest free for your own Claude Code session. Hand it an objective and a session budget, walk away, review the diff when the run ends.
6
6
 
7
+ Cursor API Proxy supported -- route through Cursor's model gateway for Composer-powered execution on `auto`, `composer`, or `composer-2` models. See **Run via Cursor API Proxy** below.
8
+
7
9
  Isolated by default. Every agent runs in its own git worktree on its own branch, so a misbehaving agent can't trash your working tree. You choose what agents can do before the run starts -- no surprise escalation mid-flight. Unmerged branches are preserved for manual review, never discarded. Built on the [Claude Agent SDK](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk) -- not a Claude Code replacement, but a background lane that runs alongside it.
8
10
 
9
11
  Different shape from hosted agent harnesses like [Claude Managed Agents](https://platform.claude.com/docs/en/managed-agents/overview): instead of one agent in one cloud container billed separately, you get many parallel sessions on your own machine, in your real repo, against your own Max plan (or API key). Works with Claude Opus, Sonnet, and Haiku -- or pair an Anthropic planner with a cheaper executor on Qwen, OpenRouter, or any Anthropic-compatible endpoint.
@@ -33,6 +35,39 @@ export ANTHROPIC_MODEL="qwen3.6-plus"
33
35
  claude-overnight
34
36
  ```
35
37
 
38
+ ## Run via Cursor API Proxy
39
+
40
+ Use Cursor's model gateway as an executor -- `auto` (delegates to best available), `composer`, or `composer-2` models. Runs locally through a proxy that speaks the Anthropic Messages API, so it's a drop-in replacement for any other provider.
41
+
42
+ 1. **Install the Cursor CLI and proxy:**
43
+
44
+ ```bash
45
+ curl https://cursor.com/install -fsS | bash
46
+ npm install -g cursor-api-proxy
47
+ ```
48
+
49
+ 2. **Get an API key.** Visit [cursor.com/dashboard/integrations](https://cursor.com/dashboard/integrations) and scroll to the "API Keys" section.
50
+
51
+ 3. **Set up.** Run `claude-overnight` and when prompted to pick a model, choose **Cursor…**. It walks you through a one-time setup: CLI check, API key entry (persisted to `providers.json`), and proxy health check.
52
+
53
+ 4. **Start the proxy** (in a separate terminal):
54
+
55
+ ```bash
56
+ npx cursor-api-proxy
57
+ ```
58
+
59
+ 5. Pick your model (`auto`, `composer`, `composer-2`, etc.). The provider is saved and reappears in every future run.
60
+
61
+ Or configure the key manually:
62
+
63
+ ```bash
64
+ export CURSOR_BRIDGE_API_KEY="sk-..."
65
+ npx cursor-api-proxy &
66
+ claude-overnight
67
+ ```
68
+
69
+ **Tip:** run `claude-overnight` with the `--model=cursor-auto` flag in non-interactive mode to skip the picker. If the proxy isn't running at startup, a warning is shown but Anthropic providers remain available.
70
+
36
71
  ## Install
37
72
 
38
73
  ```bash
@@ -0,0 +1,14 @@
1
+ export declare const CURSOR_PRIORITY_MODELS: Array<{
2
+ id: string;
3
+ label: string;
4
+ hint: string;
5
+ }>;
6
+ export declare const CURSOR_KNOWN_MODELS: Array<{
7
+ id: string;
8
+ label: string;
9
+ hint: string;
10
+ }>;
11
+ /** All known model IDs as a Set for quick membership checks. */
12
+ export declare const KNOWN_CURSOR_MODEL_IDS: Set<string>;
13
+ /** Display hint for a model ID — known ones get a hint, unknowns get a generic label. */
14
+ export declare function cursorModelHint(modelId: string): string;
@@ -0,0 +1,50 @@
1
+ // ── Cursor model constants ──
2
+ //
3
+ // Hardcoded model IDs returned by the Cursor API Proxy. These serve as a
4
+ // fallback when `agent --list-models` crashes (the bundled Node.js binary
5
+ // segfaults on some setups — the proxy inherits this bug).
6
+ //
7
+ // Update this list when Cursor adds/removes models. Run:
8
+ // node ~/.local/share/cursor-agent/versions/*/index.js --list-models
9
+ // to get the current list.
10
+ //
11
+ // The `priority` models always appear at the top of the picker in this order.
12
+ // `known` models appear after them. Anything the proxy returns dynamically
13
+ // that isn't in this list goes into a "more..." sub-menu.
14
+ import { CURSOR_MODEL_HINTS } from "./models.js";
15
+ export const CURSOR_PRIORITY_MODELS = [
16
+ { id: "composer-2", label: "composer-2", hint: "Cursor Composer 2 — latest, strongest Cursor model" },
17
+ { id: "composer-2-fast", label: "composer-2-fast", hint: "Cursor Composer 2 Fast — faster, cheaper variant" },
18
+ { id: "auto", label: "auto", hint: "auto-delegates to the best available model" },
19
+ ];
20
+ export const CURSOR_KNOWN_MODELS = [
21
+ { id: "composer", label: "composer", hint: "Cursor Composer — previous generation" },
22
+ ];
23
+ /** All known model IDs as a Set for quick membership checks. */
24
+ export const KNOWN_CURSOR_MODEL_IDS = new Set([
25
+ ...CURSOR_PRIORITY_MODELS.map(m => m.id),
26
+ ...CURSOR_KNOWN_MODELS.map(m => m.id),
27
+ ]);
28
+ /** Display hint for a model ID — known ones get a hint, unknowns get a generic label. */
29
+ export function cursorModelHint(modelId) {
30
+ const m = modelId.toLowerCase();
31
+ for (const entry of [...CURSOR_PRIORITY_MODELS, ...CURSOR_KNOWN_MODELS]) {
32
+ if (entry.id === m)
33
+ return entry.hint;
34
+ }
35
+ if (m.startsWith("composer"))
36
+ return "Cursor Composer model";
37
+ if (m.includes("opus"))
38
+ return CURSOR_MODEL_HINTS.opus;
39
+ if (m.includes("sonnet"))
40
+ return CURSOR_MODEL_HINTS.sonnet;
41
+ if (m.includes("haiku"))
42
+ return CURSOR_MODEL_HINTS.haiku;
43
+ if (m.startsWith("gpt-5"))
44
+ return "GPT model via Cursor";
45
+ if (m.startsWith("gemini"))
46
+ return "Gemini model via Cursor";
47
+ if (m.startsWith("grok"))
48
+ return "Grok model via Cursor";
49
+ return "Cursor model";
50
+ }
package/dist/index.js CHANGED
@@ -10,6 +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 { DEFAULT_MODEL } from "./models.js";
13
14
  import { pickModel, loadProviders, preflightProvider, buildEnvResolver, healthCheckCursorProxy, PROXY_DEFAULT_URL, isCursorProxyProvider } from "./providers.js";
14
15
  import { RunDisplay } from "./ui.js";
15
16
  import { renderSummary } from "./render.js";
@@ -665,7 +666,7 @@ async function main() {
665
666
  const defaultModel = activeProvider?.model
666
667
  ?? models[0]?.value
667
668
  ?? savedForCLI.find(p => p !== activeProvider)?.model
668
- ?? "claude-sonnet-4-6";
669
+ ?? DEFAULT_MODEL;
669
670
  workerModel = cliFlags.model ?? fileCfg?.model ?? defaultModel;
670
671
  plannerModel = activeProvider?.model ?? models[0]?.value ?? workerModel;
671
672
  // Auto-resolve a saved custom provider if --model matches its id or model id.
@@ -0,0 +1,27 @@
1
+ export declare const MODEL_TIER_OPUS = "opus";
2
+ export declare const MODEL_TIER_SONNET = "sonnet";
3
+ export declare const MODEL_TIER_HAIKU = "haiku";
4
+ export declare const MODEL_TIER_UNKNOWN = "unknown";
5
+ export type ModelTier = typeof MODEL_TIER_OPUS | typeof MODEL_TIER_SONNET | typeof MODEL_TIER_HAIKU | typeof MODEL_TIER_UNKNOWN;
6
+ export interface TierDetectionRule {
7
+ match: (model: string) => boolean;
8
+ tier: ModelTier;
9
+ }
10
+ export declare const TIER_DETECTION_RULES: TierDetectionRule[];
11
+ export declare function detectModelTier(model: string): ModelTier;
12
+ export declare const MODEL_CAPABILITY_DESCRIPTIONS: Record<ModelTier, string>;
13
+ export declare const UNKNOWN_MODEL_CAPABILITIES: Record<string, string>;
14
+ export declare function modelCapabilityBlock(model: string): string;
15
+ export declare const DEFAULT_MODEL = "claude-sonnet-4-6";
16
+ export declare const FALLBACK_MODEL = "claude-opus-4-6";
17
+ export declare const PLANNER_THRESHOLDS: {
18
+ opus: {
19
+ small: number;
20
+ medium: number;
21
+ };
22
+ default: {
23
+ small: number;
24
+ medium: number;
25
+ };
26
+ };
27
+ export declare const CURSOR_MODEL_HINTS: Record<string, string>;
package/dist/models.js ADDED
@@ -0,0 +1,60 @@
1
+ // ── Model tier constants ──
2
+ export const MODEL_TIER_OPUS = "opus";
3
+ export const MODEL_TIER_SONNET = "sonnet";
4
+ export const MODEL_TIER_HAIKU = "haiku";
5
+ export const MODEL_TIER_UNKNOWN = "unknown";
6
+ export const TIER_DETECTION_RULES = [
7
+ { match: m => m === "default" || m.includes("opus"), tier: MODEL_TIER_OPUS },
8
+ { match: m => m.includes("sonnet"), tier: MODEL_TIER_SONNET },
9
+ { match: m => m.includes("haiku"), tier: MODEL_TIER_HAIKU },
10
+ { match: m => m === "auto", tier: MODEL_TIER_UNKNOWN },
11
+ { match: m => m.startsWith("composer"), tier: MODEL_TIER_SONNET },
12
+ { match: m => m.startsWith("gpt-5") || m.startsWith("gemini") || m.startsWith("grok"), tier: MODEL_TIER_SONNET },
13
+ ];
14
+ export function detectModelTier(model) {
15
+ const m = model.toLowerCase();
16
+ for (const rule of TIER_DETECTION_RULES) {
17
+ if (rule.match(m))
18
+ return rule.tier;
19
+ }
20
+ return MODEL_TIER_UNKNOWN;
21
+ }
22
+ // ── Capability descriptions ──
23
+ export const MODEL_CAPABILITY_DESCRIPTIONS = {
24
+ opus: "Each agent runs Claude Opus with 1M context -- a powerhouse. It can own entire epics, do deep codebase research, make architectural decisions, implement complex multi-file systems end-to-end, use browser tools for analysis, and deliver expert-level work. These agents can work for 30+ minutes on the most complex tasks. Do NOT waste them on trivial edits -- give them ownership and autonomy.",
25
+ sonnet: "Each agent runs Claude Sonnet -- capable of substantial implementation, refactoring, testing, and design work. Can work autonomously for 10-20 minutes on complex tasks. Give agents meaningful scope -- not just single-line edits.",
26
+ haiku: "Each agent runs Claude Haiku -- fast and efficient, best for focused, well-specified tasks. Be explicit about files, functions, and expected changes. Keep tasks scoped to a clear, concrete deliverable.",
27
+ unknown: "", // handled by UNKNOWN_MODEL_CAPABILITIES below
28
+ };
29
+ export const UNKNOWN_MODEL_CAPABILITIES = {
30
+ composer: "Each agent runs a Cursor Composer model with full codebase access. Capable of focused implementation work. Be explicit about files, functions, and expected changes.",
31
+ "gpt-5": "Each agent runs a GPT model via Cursor with full codebase access. Capable of focused implementation work. Be explicit about files, functions, and expected changes.",
32
+ gemini: "Each agent runs a Gemini model via Cursor with full codebase access. Be explicit about files, functions, and expected changes.",
33
+ grok: "Each agent runs a Grok model via Cursor with full codebase access. Be explicit about files, functions, and expected changes.",
34
+ };
35
+ export function modelCapabilityBlock(model) {
36
+ const tier = detectModelTier(model);
37
+ const cap = MODEL_CAPABILITY_DESCRIPTIONS[tier];
38
+ if (cap)
39
+ return cap;
40
+ const m = model.toLowerCase();
41
+ for (const [prefix, desc] of Object.entries(UNKNOWN_MODEL_CAPABILITIES)) {
42
+ if (m.startsWith(prefix))
43
+ return desc;
44
+ }
45
+ return `Each agent has full codebase access and can work autonomously.`;
46
+ }
47
+ // ── Default / fallback models ──
48
+ export const DEFAULT_MODEL = "claude-sonnet-4-6";
49
+ export const FALLBACK_MODEL = "claude-opus-4-6"; // used for planner + worker recovery
50
+ // ── Planner thresholds (opus-tuned vs default) ──
51
+ export const PLANNER_THRESHOLDS = {
52
+ opus: { small: 5, medium: 30 },
53
+ default: { small: 15, medium: 50 },
54
+ };
55
+ // ── Cursor model hints ──
56
+ export const CURSOR_MODEL_HINTS = {
57
+ opus: "Opus-tier model via Cursor",
58
+ sonnet: "Sonnet-tier model via Cursor",
59
+ haiku: "Haiku-tier model via Cursor (fast)",
60
+ };
@@ -1,4 +1,6 @@
1
1
  import type { Task, PermMode, RateLimitWindow } from "./types.js";
2
+ import { detectModelTier, modelCapabilityBlock } from "./models.js";
3
+ export { detectModelTier, modelCapabilityBlock };
2
4
  /**
3
5
  * Logging callback used by planner/steering queries.
4
6
  * `kind` distinguishes ephemeral status updates (heartbeat ticker) from
@@ -25,9 +27,6 @@ export interface PlannerOpts {
25
27
  };
26
28
  }
27
29
  export declare function setPlannerEnvResolver(fn: ((model?: string) => Record<string, string> | undefined) | undefined): void;
28
- export type ModelTier = "opus" | "sonnet" | "haiku" | "unknown";
29
- export declare function detectModelTier(model: string): ModelTier;
30
- export declare function modelCapabilityBlock(model: string): string;
31
30
  export declare function getTotalPlannerCost(): number;
32
31
  export declare function getPlannerRateLimitInfo(): PlannerRateLimitInfo;
33
32
  export declare function runPlannerQuery(prompt: string, opts: PlannerOpts, onLog: PlannerLog): Promise<string>;
@@ -1,6 +1,9 @@
1
1
  import { query } from "@anthropic-ai/claude-agent-sdk";
2
2
  import { readFileSync } from "fs";
3
3
  import { NudgeError } from "./types.js";
4
+ import { detectModelTier, modelCapabilityBlock } from "./models.js";
5
+ // Re-export for consumers that import from planner-query (steering.ts, index.ts).
6
+ export { detectModelTier, modelCapabilityBlock };
4
7
  // ── Shared env resolver (set once at run start, used by every planner query) ──
5
8
  //
6
9
  // Swarm and planner calls share a model→env map so a custom provider configured
@@ -10,37 +13,6 @@ let _envResolver;
10
13
  export function setPlannerEnvResolver(fn) {
11
14
  _envResolver = fn;
12
15
  }
13
- export function detectModelTier(model) {
14
- const m = model.toLowerCase();
15
- if (m === "default" || m.includes("opus"))
16
- return "opus";
17
- if (m.includes("sonnet"))
18
- return "sonnet";
19
- if (m.includes("haiku"))
20
- return "haiku";
21
- // Cursor API Proxy models
22
- if (m === "auto")
23
- return "unknown";
24
- if (m.startsWith("composer"))
25
- return "sonnet";
26
- return "unknown";
27
- }
28
- export function modelCapabilityBlock(model) {
29
- switch (detectModelTier(model)) {
30
- case "opus":
31
- return `Each agent runs Claude Opus with 1M context -- a powerhouse. It can own entire epics, do deep codebase research, make architectural decisions, implement complex multi-file systems end-to-end, use browser tools for analysis, and deliver expert-level work. These agents can work for 30+ minutes on the most complex tasks. Do NOT waste them on trivial edits -- give them ownership and autonomy.`;
32
- case "sonnet":
33
- return `Each agent runs Claude Sonnet -- capable of substantial implementation, refactoring, testing, and design work. Can work autonomously for 10-20 minutes on complex tasks. Give agents meaningful scope -- not just single-line edits.`;
34
- case "haiku":
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.`;
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
- }
41
- return `Each agent has full codebase access and can work autonomously.`;
42
- }
43
- }
44
16
  // ── Rate limit tracking ──
45
17
  const RATE_LIMIT_PATTERNS = ["rate", "limit", "overloaded", "429", "hit your limit", "too many"];
46
18
  function isRateLimitError(err) {
package/dist/planner.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { readFileSync } from "fs";
2
- import { runPlannerQuery, extractTaskJson, attemptJsonParse, postProcess, detectModelTier, modelCapabilityBlock } from "./planner-query.js";
2
+ import { runPlannerQuery, extractTaskJson, attemptJsonParse, postProcess } from "./planner-query.js";
3
+ import { detectModelTier, modelCapabilityBlock, MODEL_TIER_HAIKU, MODEL_TIER_OPUS, PLANNER_THRESHOLDS } from "./models.js";
3
4
  // Resilience: if the planner query throws but the agent already wrote valid
4
5
  // tasks to `outFile` (via its Write tool), salvage them instead of discarding
5
6
  // expensive work. Returns salvaged tasks on success, null if nothing usable on
@@ -62,7 +63,7 @@ function plannerPrompt(objective, workerModel, budget, concurrency, flexNote) {
62
63
  ? `\n- ${concurrency} agents run in parallel -- tasks that run concurrently must touch DIFFERENT files to avoid merge conflicts`
63
64
  : "";
64
65
  const flexLine = flexNote ? `\n\n${flexNote}` : "";
65
- if (tier === "haiku") {
66
+ if (tier === MODEL_TIER_HAIKU) {
66
67
  return `You are a task coordinator for a parallel agent system. Analyze this codebase and break the following objective into independent tasks.
67
68
 
68
69
  Objective: ${objective}
@@ -84,8 +85,9 @@ Respond with ONLY a JSON object (no markdown fences):
84
85
  ]
85
86
  }`;
86
87
  }
87
- const smallThreshold = tier === "opus" ? 5 : 15;
88
- const mediumThreshold = tier === "opus" ? 30 : 50;
88
+ const thresholds = tier === MODEL_TIER_OPUS ? PLANNER_THRESHOLDS.opus : PLANNER_THRESHOLDS.default;
89
+ const smallThreshold = thresholds.small;
90
+ const mediumThreshold = thresholds.medium;
89
91
  if (b <= smallThreshold) {
90
92
  return `You are a task coordinator for a parallel agent system. Analyze this codebase and break the following objective into independent tasks.
91
93
 
@@ -17,6 +17,8 @@ export interface ProviderConfig {
17
17
  useJWT?: boolean;
18
18
  /** When true, this provider routes through cursor-api-proxy (special env/health-check handling). */
19
19
  cursorProxy?: boolean;
20
+ /** API key for cursor-api-proxy. Stored in providers.json (0600), used as fallback when CURSOR_BRIDGE_API_KEY env is not set. */
21
+ cursorApiKey?: string;
20
22
  }
21
23
  export declare function getStorePath(): string;
22
24
  export declare function loadProviders(): ProviderConfig[];
@@ -65,7 +67,7 @@ export declare function healthCheckCursorProxy(baseUrl?: string): Promise<boolea
65
67
  export declare function fetchCursorModels(baseUrl?: string): Promise<string[]>;
66
68
  /**
67
69
  * Interactive setup guide for cursor-api-proxy.
68
- * Walks through CLI install, login, and proxy start.
70
+ * Walks through CLI install, API key config, and proxy start.
69
71
  * Returns true when proxy is running and healthy.
70
72
  */
71
73
  export declare function setupCursorProxy(): Promise<boolean>;
package/dist/providers.js CHANGED
@@ -6,6 +6,8 @@ import chalk from "chalk";
6
6
  import { query } from "@anthropic-ai/claude-agent-sdk";
7
7
  import { ask, select, selectKey } from "./cli.js";
8
8
  import { getBearerToken, clearTokenCache } from "./auth.js";
9
+ import { DEFAULT_MODEL } from "./models.js";
10
+ import { CURSOR_PRIORITY_MODELS, CURSOR_KNOWN_MODELS, KNOWN_CURSOR_MODEL_IDS, cursorModelHint, } from "./cursor-models.js";
9
11
  // ── Store ──
10
12
  const STORE_PATH = join(homedir(), ".claude", "claude-overnight", "providers.json");
11
13
  export function getStorePath() { return STORE_PATH; }
@@ -64,9 +66,9 @@ export function envFor(p) {
64
66
  if (v !== undefined)
65
67
  base[k] = v;
66
68
  if (p.cursorProxy) {
67
- // cursor-api-proxy: routes through local proxy, no real API key needed
68
69
  base.ANTHROPIC_BASE_URL = p.baseURL;
69
- base.ANTHROPIC_AUTH_TOKEN = process.env.CURSOR_BRIDGE_API_KEY || "unused";
70
+ const key = process.env.CURSOR_BRIDGE_API_KEY || p.cursorApiKey;
71
+ base.ANTHROPIC_AUTH_TOKEN = key || "unused";
70
72
  delete base.ANTHROPIC_API_KEY;
71
73
  return base;
72
74
  }
@@ -99,8 +101,8 @@ export async function pickModel(label, anthropicModels, currentModelId) {
99
101
  // entry so the user isn't trapped if they cancel the Other… form.
100
102
  if (anthropicModels.length === 0) {
101
103
  items.push({
102
- name: "claude-sonnet-4-6",
103
- value: { kind: "anthropic", model: { value: "claude-sonnet-4-6", displayName: "claude-sonnet-4-6", description: "default (model list unavailable)" } },
104
+ name: DEFAULT_MODEL,
105
+ value: { kind: "anthropic", model: { value: DEFAULT_MODEL, displayName: DEFAULT_MODEL, description: "default (model list unavailable)" } },
104
106
  hint: "default -- Anthropic model list unavailable",
105
107
  });
106
108
  }
@@ -291,24 +293,48 @@ export async function fetchCursorModels(baseUrl = PROXY_DEFAULT_URL) {
291
293
  }
292
294
  }
293
295
  /**
294
- * Known Cursor model recommendations short hints to guide users.
296
+ * Try to fetch live Cursor model IDs. Falls back to an empty array — the
297
+ * caller merges with known constants, so the picker always has content.
298
+ *
299
+ * NOTE: `agent --list-models` segfaults with its bundled Node.js binary
300
+ * (exit 139). We work around this by running with the system `node` instead.
295
301
  */
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";
302
+ async function fetchLiveCursorModels() {
303
+ // Try the proxy first (works when the bundled node doesn't crash)
304
+ const proxyModels = await fetchCursorModels();
305
+ if (proxyModels.length > 0)
306
+ return proxyModels;
307
+ // Fallback: run the cursor-agent CLI with system node
308
+ // Find the agent binary via command -v (alias-safe), then locate its index.js.
309
+ try {
310
+ // command -v handles symlinks and doesn't expand shell aliases
311
+ const agentPath = execSync("command -v agent 2>/dev/null || command -v cursor-agent 2>/dev/null", {
312
+ timeout: 3_000, encoding: "utf-8", shell: "bash",
313
+ }).trim();
314
+ if (!agentPath)
315
+ return [];
316
+ // Resolve the directory (realpath handles symlinks like agent → cursor-agent)
317
+ const dir = execSync(`dirname "$(realpath "${agentPath}")"`, {
318
+ timeout: 3_000, encoding: "utf-8", shell: "bash",
319
+ }).trim();
320
+ // The bundled index.js lives in the same directory as the agent script
321
+ const indexPath = `${dir}/index.js`;
322
+ const raw = execSync(`node "${indexPath}" --list-models 2>/dev/null`, {
323
+ timeout: 10_000, encoding: "utf-8",
324
+ });
325
+ // Strip ANSI escape codes (cursor uses \x1B[2K\r etc.)
326
+ const out = raw.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "");
327
+ // Parse lines like "composer-2-fast - Composer 2 Fast"
328
+ const ids = [];
329
+ for (const line of out.split("\n")) {
330
+ const match = line.match(/^([A-Za-z0-9][A-Za-z0-9._:/-]*)\s+-\s+/);
331
+ if (match)
332
+ ids.push(match[1]);
333
+ }
334
+ return ids;
335
+ }
336
+ catch { }
337
+ return [];
312
338
  }
313
339
  function setupSteps() {
314
340
  return [
@@ -328,19 +354,14 @@ function setupSteps() {
328
354
  successMsg: "Cursor CLI found",
329
355
  },
330
356
  {
331
- label: "Cursor authentication",
357
+ label: "Cursor API key",
332
358
  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
- }
359
+ const key = process.env.CURSOR_BRIDGE_API_KEY;
360
+ return !!key && key.trim().length > 0;
340
361
  },
341
- autoCmd: "agent login",
342
- manualCmd: "agent login",
343
- successMsg: "Cursor authenticated",
362
+ autoCmd: "",
363
+ manualCmd: "",
364
+ successMsg: "Cursor API key configured",
344
365
  },
345
366
  {
346
367
  label: "cursor-api-proxy server",
@@ -359,15 +380,51 @@ function setupSteps() {
359
380
  },
360
381
  ];
361
382
  }
383
+ const CURSOR_KEY_PROVIDER_ID = "cursor";
384
+ /** Persist the Cursor API key into providers.json. Updates any existing cursor proxy provider,
385
+ * or creates a sentinel entry so the key survives across sessions. */
386
+ function saveCursorApiKey(key) {
387
+ const existing = loadProviders().filter(p => p.cursorProxy);
388
+ if (existing.length > 0) {
389
+ const p = existing[0];
390
+ p.cursorApiKey = key;
391
+ saveProvider(p);
392
+ }
393
+ else {
394
+ const sentinel = {
395
+ id: CURSOR_KEY_PROVIDER_ID,
396
+ displayName: "Cursor (API key)",
397
+ baseURL: PROXY_DEFAULT_URL,
398
+ model: "auto",
399
+ cursorProxy: true,
400
+ cursorApiKey: key,
401
+ };
402
+ saveProvider(sentinel);
403
+ }
404
+ }
405
+ /** Prompt the user for a Cursor API key and persist it. Returns true if saved. */
406
+ async function promptAndSaveCursorKey() {
407
+ console.log(chalk.dim(` Get your API key from https://cursor.com/dashboard/integrations`));
408
+ console.log(chalk.dim(` (Scroll to the "API Keys" section at the bottom of the page)\n`));
409
+ const key = await ask(` ${chalk.cyan("API key")}: `);
410
+ if (key && key.trim()) {
411
+ const trimmed = key.trim();
412
+ process.env.CURSOR_BRIDGE_API_KEY = trimmed;
413
+ saveCursorApiKey(trimmed);
414
+ return true;
415
+ }
416
+ console.log(chalk.yellow(" No key provided — skipped"));
417
+ return false;
418
+ }
362
419
  /**
363
420
  * Interactive setup guide for cursor-api-proxy.
364
- * Walks through CLI install, login, and proxy start.
421
+ * Walks through CLI install, API key config, and proxy start.
365
422
  * Returns true when proxy is running and healthy.
366
423
  */
367
424
  export async function setupCursorProxy() {
368
425
  console.log(chalk.dim("\n Cursor API Proxy Setup"));
369
426
  console.log(chalk.dim(" " + "─".repeat(40)));
370
- console.log(chalk.dim(" We need three things: Cursor CLI, authentication, and the proxy server.\n"));
427
+ console.log(chalk.dim(" We need three things: Cursor CLI, an API key, and the proxy server.\n"));
371
428
  const steps = setupSteps();
372
429
  for (const step of steps) {
373
430
  if (step.check()) {
@@ -381,18 +438,10 @@ export async function setupCursorProxy() {
381
438
  { key: "s", desc: "kip (I'll handle it)" },
382
439
  ]);
383
440
  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 });
441
+ if (step.label === "Cursor API key") {
442
+ if (await promptAndSaveCursorKey()) {
390
443
  console.log(chalk.green(` ✓ ${step.successMsg}`));
391
444
  }
392
- catch {
393
- console.log(chalk.yellow(" Login failed — try manual mode"));
394
- // Fall through to manual display
395
- }
396
445
  }
397
446
  else if (step.label === "cursor-api-proxy server") {
398
447
  // Don't auto-start the proxy server here — it blocks. Just verify it's installable.
@@ -428,10 +477,20 @@ export async function setupCursorProxy() {
428
477
  }
429
478
  }
430
479
  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")}`));
480
+ if (step.label === "Cursor API key") {
481
+ console.log(chalk.cyan(`\n 1. Open: https://cursor.com/dashboard/integrations`));
482
+ console.log(chalk.cyan(` 2. Scroll to "API Keys" at the bottom of the page`));
483
+ console.log(chalk.cyan(` 3. Copy your API key and paste it below\n`));
484
+ if (await promptAndSaveCursorKey()) {
485
+ console.log(chalk.green(` ✓ ${step.successMsg}`));
486
+ }
487
+ }
488
+ else {
489
+ console.log(chalk.cyan(`\n Run this command:`));
490
+ console.log(chalk.white(` ${step.manualCmd}`));
491
+ if (step.label === "cursor-api-proxy server") {
492
+ console.log(chalk.yellow(` Then start the proxy: ${chalk.bold("npx cursor-api-proxy")}`));
493
+ }
435
494
  }
436
495
  console.log();
437
496
  const done = await selectKey(` Done?`, [
@@ -457,7 +516,34 @@ export async function setupCursorProxy() {
457
516
  console.log(chalk.yellow("\n Proxy not reachable yet. You can start it later and add it via 'Cursor' in the model picker."));
458
517
  return false;
459
518
  }
460
- // ── Cursor model picker sub-flow ──
519
+ /**
520
+ * Build the full list of cursor model picker items. Priority models go first,
521
+ * then known models, then any extra live models we fetched. If there are more
522
+ * than a handful extras, they get a "more..." sub-menu.
523
+ */
524
+ async function buildCursorPicker() {
525
+ const liveIds = await fetchLiveCursorModels();
526
+ const extra = new Set();
527
+ for (const id of liveIds) {
528
+ if (!KNOWN_CURSOR_MODEL_IDS.has(id))
529
+ extra.add(id);
530
+ }
531
+ const top = [
532
+ ...CURSOR_PRIORITY_MODELS.map(m => ({ id: m.id, name: m.label, hint: m.hint })),
533
+ ...CURSOR_KNOWN_MODELS.map(m => ({ id: m.id, name: m.label, hint: m.hint })),
534
+ ];
535
+ // Only a few extras? show them inline. Otherwise defer to "more...".
536
+ const MORE_THRESHOLD = 6;
537
+ const more = [...extra].sort().map(id => ({
538
+ id,
539
+ name: id,
540
+ hint: cursorModelHint(id),
541
+ }));
542
+ if (more.length <= MORE_THRESHOLD) {
543
+ return { top: [...top, ...more], more: [] };
544
+ }
545
+ return { top, more };
546
+ }
461
547
  async function pickCursorModel() {
462
548
  console.log(chalk.dim("\n Cursor API Proxy Models"));
463
549
  console.log(chalk.dim(" " + "─".repeat(40)));
@@ -489,28 +575,39 @@ async function pickCursorModel() {
489
575
  return null;
490
576
  }
491
577
  }
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;
578
+ const { top, more } = await buildCursorPicker();
579
+ // If there are more models available, add a "more…" entry
580
+ const items = top.map(m => ({
581
+ name: m.name,
582
+ value: m.id,
583
+ hint: m.hint,
584
+ }));
585
+ let hasMore = more.length > 0;
586
+ if (hasMore) {
587
+ items.push({ name: chalk.gray("more…"), value: "__more__", hint: `${more.length} additional models` });
497
588
  }
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
589
+ const picked = await select(" Select a Cursor model:", items, 0);
590
+ // Handle "more…" sub-menu
591
+ if (picked === "__more__") {
592
+ const moreItems = more.map(m => ({ name: m.name, value: m.id, hint: m.hint }));
593
+ const morePicked = await select(" More Cursor models:", moreItems, 0);
594
+ return saveCursorPick(morePicked);
595
+ }
596
+ return saveCursorPick(picked);
597
+ }
598
+ function saveCursorPick(modelId) {
599
+ const existingKey = loadProviders().find(p => p.id === CURSOR_KEY_PROVIDER_ID)?.cursorApiKey;
504
600
  const provider = {
505
- id: `cursor-${picked}`,
506
- displayName: `Cursor: ${picked}`,
601
+ id: `cursor-${modelId}`,
602
+ displayName: `Cursor: ${modelId}`,
507
603
  baseURL: PROXY_DEFAULT_URL,
508
- model: picked,
604
+ model: modelId,
509
605
  cursorProxy: true,
606
+ ...(existingKey ? { cursorApiKey: existingKey } : {}),
510
607
  };
511
608
  saveProvider(provider);
512
609
  console.log(chalk.green(` ✓ Saved as provider: ${provider.displayName}`));
513
- return { model: picked, providerId: provider.id, provider };
610
+ return { model: modelId, providerId: provider.id, provider };
514
611
  }
515
612
  /**
516
613
  * Build a single resolver that swarm.ts and planner-query.ts share. Maps a
package/dist/state.js CHANGED
@@ -3,6 +3,7 @@ import { execSync } from "child_process";
3
3
  import { join } from "path";
4
4
  import chalk from "chalk";
5
5
  import { forceMergeOverlay } from "./merge.js";
6
+ import { FALLBACK_MODEL } from "./models.js";
6
7
  // ── File I/O helpers ──
7
8
  export function readMdDir(dir) {
8
9
  try {
@@ -267,7 +268,7 @@ export function backfillOrphanedPlans(rootDir, filterCwd) {
267
268
  id: d,
268
269
  objective: `(recovered pre-1.11.7 plan · ${taskCount} tasks)`,
269
270
  budget: taskCount, remaining: taskCount,
270
- workerModel: "claude-opus-4-6", plannerModel: "claude-opus-4-6",
271
+ workerModel: FALLBACK_MODEL, plannerModel: FALLBACK_MODEL,
271
272
  concurrency: 5, permissionMode: "bypassPermissions",
272
273
  flex: false, useWorktrees: true, mergeStrategy: "yolo",
273
274
  allowExtraUsage: false,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "claude-overnight",
3
- "version": "1.17.2",
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.",
3
+ "version": "1.18.0",
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. Provider-agnostic model catalog with capability-based planning.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "claude-overnight": "dist/bin.js"
@@ -60,6 +60,7 @@
60
60
  "code-automation",
61
61
  "refactoring",
62
62
  "migration",
63
+ "cursor",
63
64
  "qwen",
64
65
  "openrouter",
65
66
  "iterative"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "claude-overnight",
3
- "version": "1.17.2",
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.",
3
+ "version": "1.18.0",
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. Supports Cursor API Proxy, Qwen, OpenRouter.",
5
5
  "author": {
6
6
  "name": "Francesco Fornace"
7
7
  },