claude-overnight 1.19.1 → 1.23.1

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.
@@ -1 +1 @@
1
- export declare const VERSION = "1.19.1";
1
+ export declare const VERSION = "1.23.1";
package/dist/_version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // Auto-generated by build — do not edit manually.
2
- export const VERSION = "1.19.1";
2
+ export const VERSION = "1.23.1";
@@ -58,6 +58,7 @@ export declare const PROXY_DEFAULT_URL = "http://127.0.0.1:8765";
58
58
  export declare function isCursorProxyProvider(p: ProviderConfig): boolean;
59
59
  /**
60
60
  * Health check: GET /health on the proxy. Returns true if proxy is reachable.
61
+ * Passes the stored API key so the /health endpoint doesn't return 401.
61
62
  */
62
63
  export declare function healthCheckCursorProxy(baseUrl?: string): Promise<boolean>;
63
64
  /**
@@ -68,22 +69,21 @@ export declare function fetchCursorModels(baseUrl?: string): Promise<string[]>;
68
69
  /**
69
70
  * Auto-start the cursor-api-proxy as a detached background process.
70
71
  *
71
- * When the proxy is started, we also configure it to use system Node.js
72
- * for spawning the cursor-agent subprocess. The agent's bundled Node.js
73
- * segfaults with --list-models on macOS (exit 139), so we resolve the
74
- * system `node` binary and the agent's index.js, patch the proxy's env.js
75
- * to respect CURSOR_AGENT_NODE/SCRIPT on Unix, and pass those env vars.
72
+ * Passes CURSOR_AGENT_NODE/SCRIPT so the fork uses system Node.js for the
73
+ * agent subprocess (avoids segfaults with --list-models on macOS).
76
74
  *
77
75
  * Handles:
78
76
  * - Proxy already running and verified → returns true immediately
79
- * - Something on the port but not our proxy → warns, skips spawn
80
- * - Port in use by nothing responsive returns true (something bound it)
81
- * - Proxy not running spawns `npx cursor-api-proxy` detached, waits for health
82
- * - Spawn fails (not installed) → returns false, caller falls back to manual instructions
77
+ * - Something on the port but not our proxy → warns, kills, restarts
78
+ * - Proxy not runningspawns detached, waits for health
79
+ * - Spawn failsreturns false, caller falls back to manual instructions
80
+ *
81
+ * When `forceRestart` is true and a stale process is on the port, it will be
82
+ * killed and the proxy restarted.
83
83
  *
84
84
  * Returns true when the proxy is reachable at PROXY_DEFAULT_URL.
85
85
  */
86
- export declare function ensureCursorProxyRunning(baseUrl?: string): Promise<boolean>;
86
+ export declare function ensureCursorProxyRunning(baseUrl?: string, forceRestart?: boolean): Promise<boolean>;
87
87
  /**
88
88
  * Full install + configure flow for cursor-api-proxy.
89
89
  * Walks through CLI install, API key config, and proxy start.
package/dist/providers.js CHANGED
@@ -1,4 +1,4 @@
1
- import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, realpathSync, readdirSync } from "fs";
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, realpathSync } from "fs";
2
2
  import { homedir } from "os";
3
3
  import { join, dirname } from "path";
4
4
  import { execSync, spawn } from "child_process";
@@ -262,13 +262,28 @@ export const PROXY_DEFAULT_URL = "http://127.0.0.1:8765";
262
262
  export function isCursorProxyProvider(p) {
263
263
  return p.cursorProxy === true || p.baseURL === PROXY_DEFAULT_URL;
264
264
  }
265
+ /** Resolve the cursor-api-proxy API key from env or providers.json. */
266
+ function resolveCursorProxyKey() {
267
+ if (process.env.CURSOR_BRIDGE_API_KEY?.trim())
268
+ return process.env.CURSOR_BRIDGE_API_KEY.trim();
269
+ const saved = loadProviders().find(p => p.cursorProxy);
270
+ if (saved?.cursorApiKey?.trim())
271
+ return saved.cursorApiKey.trim();
272
+ return null;
273
+ }
274
+ /** Build fetch options with the cursor proxy auth header if a key is available. */
275
+ function cursorProxyFetchOpts() {
276
+ const key = resolveCursorProxyKey();
277
+ return key ? { headers: { Authorization: `Bearer ${key}` } } : {};
278
+ }
265
279
  /**
266
280
  * Health check: GET /health on the proxy. Returns true if proxy is reachable.
281
+ * Passes the stored API key so the /health endpoint doesn't return 401.
267
282
  */
268
283
  export async function healthCheckCursorProxy(baseUrl = PROXY_DEFAULT_URL) {
269
284
  const url = `${baseUrl.replace(/\/$/, "")}/health`;
270
285
  try {
271
- const res = await fetch(url, { method: "GET", signal: AbortSignal.timeout(3_000) });
286
+ const res = await fetch(url, { method: "GET", signal: AbortSignal.timeout(3_000), ...cursorProxyFetchOpts() });
272
287
  return res.ok;
273
288
  }
274
289
  catch {
@@ -282,7 +297,7 @@ export async function healthCheckCursorProxy(baseUrl = PROXY_DEFAULT_URL) {
282
297
  export async function fetchCursorModels(baseUrl = PROXY_DEFAULT_URL) {
283
298
  const url = `${baseUrl.replace(/\/$/, "")}/v1/models`;
284
299
  try {
285
- const res = await fetch(url, { method: "GET", signal: AbortSignal.timeout(5_000) });
300
+ const res = await fetch(url, { method: "GET", signal: AbortSignal.timeout(5_000), ...cursorProxyFetchOpts() });
286
301
  if (!res.ok)
287
302
  return [];
288
303
  const json = await res.json();
@@ -336,16 +351,26 @@ async function fetchLiveCursorModels() {
336
351
  }
337
352
  /**
338
353
  * Verify something is actually cursor-api-proxy (not just any HTTP service on the port).
339
- * Calls /v1/models and checks the response shape. Returns true if it looks like the proxy.
354
+ * Tries /health first (proxy identity), then falls back to /v1/models shape check.
355
+ * Returns true if it looks like the proxy.
340
356
  */
341
357
  async function verifyCursorProxy(baseUrl = PROXY_DEFAULT_URL) {
342
- const url = `${baseUrl.replace(/\/$/, "")}/v1/models`;
358
+ const url = baseUrl.replace(/\/$/, "");
359
+ const opts = cursorProxyFetchOpts();
360
+ // /health is the most reliable proxy identity check — works even when
361
+ // /v1/models fails due to agent subprocess crash (macOS segfault).
343
362
  try {
344
- const res = await fetch(url, { method: "GET", signal: AbortSignal.timeout(3_000) });
363
+ const res = await fetch(`${url}/health`, { method: "GET", signal: AbortSignal.timeout(3_000), ...opts });
364
+ if (res.ok)
365
+ return true;
366
+ }
367
+ catch { }
368
+ // Fallback: check /v1/models response shape
369
+ try {
370
+ const res = await fetch(`${url}/v1/models`, { method: "GET", signal: AbortSignal.timeout(3_000), ...opts });
345
371
  if (!res.ok)
346
372
  return false;
347
373
  const json = await res.json();
348
- // cursor-api-proxy returns { data: [{ id: "...", ... }] }
349
374
  return Array.isArray(json?.data);
350
375
  }
351
376
  catch {
@@ -353,143 +378,65 @@ async function verifyCursorProxy(baseUrl = PROXY_DEFAULT_URL) {
353
378
  }
354
379
  }
355
380
  /**
356
- * Check whether something is already listening on the proxy port.
357
- * Returns true if any process bound the port (another instance, Cursor CLI, etc.).
358
- */
359
- async function isPortInUse(port, host = "127.0.0.1") {
360
- try {
361
- const res = await fetch(`http://${host}:${port}/health`, {
362
- method: "GET",
363
- signal: AbortSignal.timeout(2_000),
364
- });
365
- return res.ok || res.status >= 400; // any response means something is listening
366
- }
367
- catch {
368
- return false;
369
- }
370
- }
371
- /**
372
- * Find the system `node` binary path. Uses `which` to bypass any bundled node.
381
+ * Kill whatever process is bound to the given port. Uses `lsof` on macOS /
382
+ * `fuser` on Linux. Returns the PID that was killed, or null if nothing found
383
+ * or permission denied.
373
384
  */
374
- function resolveSystemNode() {
385
+ function killProcessOnPort(port, host = "127.0.0.1") {
375
386
  try {
376
- return execSync("which node 2>/dev/null", {
377
- timeout: 3_000, encoding: "utf-8", shell: "bash",
378
- }).trim() || null;
387
+ // macOS / BSD: lsof -ti :PORT gives just the PID
388
+ const pid = execSync(`lsof -ti :${port} 2>/dev/null`, {
389
+ timeout: 5_000, encoding: "utf-8",
390
+ }).trim().split("\n")[0];
391
+ if (!pid || !/^\d+$/.test(pid))
392
+ return null;
393
+ execSync(`kill -9 ${pid} 2>/dev/null`, { timeout: 5_000 });
394
+ return parseInt(pid, 10);
379
395
  }
380
396
  catch {
381
397
  return null;
382
398
  }
383
399
  }
384
400
  /**
385
- * Find the cursor-agent's index.js. Mirrors the logic in fetchLiveCursorModels:
386
- * resolves the `agent` symlink to find the version directory containing index.js.
401
+ * Check whether something is already listening on the proxy port.
402
+ * Returns true if any process bound the port (another instance, Cursor CLI, etc.).
387
403
  */
388
- function resolveAgentIndexJs() {
404
+ async function isPortInUse(port, host = "127.0.0.1") {
389
405
  try {
390
- const agentPath = execSync("command -v agent 2>/dev/null || command -v cursor-agent 2>/dev/null", {
391
- timeout: 3_000, encoding: "utf-8", shell: "bash",
392
- }).trim();
393
- if (!agentPath)
394
- return null;
395
- const dir = dirname(realpathSync(agentPath));
396
- const indexPath = `${dir}/index.js`;
397
- return existsSync(indexPath) ? indexPath : null;
406
+ const res = await fetch(`http://${host}:${port}/health`, {
407
+ method: "GET",
408
+ signal: AbortSignal.timeout(2_000),
409
+ });
410
+ return res.ok || res.status >= 400; // any response means something is listening
398
411
  }
399
412
  catch {
400
- return null;
401
- }
402
- }
403
- /**
404
- * Patch the proxy's env.js to use CURSOR_AGENT_NODE + CURSOR_AGENT_SCRIPT on Unix.
405
- *
406
- * The proxy already reads these env vars (lines 152-153 of env.js) but only uses
407
- * them on Windows in resolveAgentCommand(). We inject a Unix code path before the
408
- * final `return { command: cmd, args, env }` so that when these vars are set,
409
- * the proxy spawns system node with the agent script instead of the bundled node
410
- * (which segfaults with --list-models on macOS).
411
- *
412
- * Safe to call repeatedly — the patch is idempotent.
413
- */
414
- function patchProxyEnvJs(proxyDir) {
415
- const envJs = join(proxyDir, "dist", "lib", "env.js");
416
- if (!existsSync(envJs))
417
413
  return false;
418
- const src = readFileSync(envJs, "utf-8");
419
- // Check if already patched
420
- if (src.includes("/* claude-overnight patch */"))
421
- return true;
422
- const patch = `\n/* claude-overnight patch: use CURSOR_AGENT_NODE+SCRIPT on unix */\n` +
423
- `if (platform !== "win32" && loaded.agentNode && loaded.agentScript) {\n` +
424
- ` return { command: loaded.agentNode, args: [loaded.agentScript, ...args], env: { ...env, CURSOR_INVOKED_AS: "agent" }, agentScriptPath: loaded.agentScript };\n` +
425
- `}`;
426
- // Insert before the final return in resolveAgentCommand
427
- const target = " return { command: cmd, args, env };\n}";
428
- if (!src.includes(target)) {
429
- // Try minified variant
430
- const target2 = "return{command:cmd,args,env}}";
431
- if (!src.includes(target2))
432
- return false;
433
- writeFileSync(envJs, src.replace(target2, patch + "\n" + target2), "utf-8");
434
- }
435
- else {
436
- writeFileSync(envJs, src.replace(target, patch + "\n" + target), "utf-8");
437
- }
438
- return true;
439
- }
440
- /**
441
- * Find the cursor-api-proxy package directory (npx cache or global install).
442
- */
443
- function findProxyPackageDir() {
444
- try {
445
- // Try npx cache first
446
- const npmCacheRoot = join(homedir(), ".npm", "_npx");
447
- if (existsSync(npmCacheRoot)) {
448
- const dirs = readdirSync(npmCacheRoot);
449
- for (const d of dirs) {
450
- const candidate = join(npmCacheRoot, d, "node_modules", "cursor-api-proxy");
451
- if (existsSync(join(candidate, "dist", "lib", "env.js")))
452
- return candidate;
453
- }
454
- }
455
414
  }
456
- catch { }
457
- // Try global install
458
- try {
459
- const globalDir = execSync("npm root -g 2>/dev/null", {
460
- timeout: 5_000, encoding: "utf-8", shell: "bash",
461
- }).trim();
462
- if (globalDir && existsSync(join(globalDir, "cursor-api-proxy", "dist", "lib", "env.js"))) {
463
- return join(globalDir, "cursor-api-proxy");
464
- }
465
- }
466
- catch { }
467
- return null;
468
415
  }
469
416
  /**
470
417
  * Auto-start the cursor-api-proxy as a detached background process.
471
418
  *
472
- * When the proxy is started, we also configure it to use system Node.js
473
- * for spawning the cursor-agent subprocess. The agent's bundled Node.js
474
- * segfaults with --list-models on macOS (exit 139), so we resolve the
475
- * system `node` binary and the agent's index.js, patch the proxy's env.js
476
- * to respect CURSOR_AGENT_NODE/SCRIPT on Unix, and pass those env vars.
419
+ * Passes CURSOR_AGENT_NODE/SCRIPT so the fork uses system Node.js for the
420
+ * agent subprocess (avoids segfaults with --list-models on macOS).
477
421
  *
478
422
  * Handles:
479
423
  * - Proxy already running and verified → returns true immediately
480
- * - Something on the port but not our proxy → warns, skips spawn
481
- * - Port in use by nothing responsive returns true (something bound it)
482
- * - Proxy not running spawns `npx cursor-api-proxy` detached, waits for health
483
- * - Spawn fails (not installed) → returns false, caller falls back to manual instructions
424
+ * - Something on the port but not our proxy → warns, kills, restarts
425
+ * - Proxy not runningspawns detached, waits for health
426
+ * - Spawn failsreturns false, caller falls back to manual instructions
427
+ *
428
+ * When `forceRestart` is true and a stale process is on the port, it will be
429
+ * killed and the proxy restarted.
484
430
  *
485
431
  * Returns true when the proxy is reachable at PROXY_DEFAULT_URL.
486
432
  */
487
- export async function ensureCursorProxyRunning(baseUrl = PROXY_DEFAULT_URL) {
433
+ export async function ensureCursorProxyRunning(baseUrl = PROXY_DEFAULT_URL, forceRestart = false) {
488
434
  const url = new URL(baseUrl);
489
435
  const port = parseInt(url.port, 10) || 80;
490
436
  // Already healthy?
491
- if (await healthCheckCursorProxy(baseUrl))
437
+ if (await healthCheckCursorProxy(baseUrl)) {
492
438
  return true;
439
+ }
493
440
  // Something bound the port — verify it's actually the cursor proxy
494
441
  if (await isPortInUse(port, url.hostname)) {
495
442
  const isProxy = await verifyCursorProxy(baseUrl);
@@ -497,38 +444,53 @@ export async function ensureCursorProxyRunning(baseUrl = PROXY_DEFAULT_URL) {
497
444
  console.log(chalk.dim(` Proxy verified at port ${port}`));
498
445
  return true;
499
446
  }
500
- console.log(chalk.yellow(` ⚠ Something is on port ${port} but it's not cursor-api-proxy`));
501
- console.log(chalk.dim(` Skip auto-start. If this is unexpected, stop the service or pick a different port.`));
502
- return false;
447
+ // Stale process on the port kill it if forceRestart, or try automatically
448
+ if (!forceRestart) {
449
+ console.log(chalk.yellow(` ⚠ Something is on port ${port} but it's not cursor-api-proxy — killing stale process…`));
450
+ }
451
+ const killedPid = killProcessOnPort(port, url.hostname);
452
+ if (killedPid) {
453
+ console.log(chalk.green(` ✓ Killed stale process PID ${killedPid} on port ${port}`));
454
+ await new Promise(r => setTimeout(r, 500));
455
+ return startProxyProcess(baseUrl, url, port);
456
+ }
457
+ // Couldn't kill (permission denied, already gone) — try starting anyway
458
+ console.log(chalk.yellow(` ⚠ Couldn't kill process on port ${port} — attempting to start proxy anyway…`));
459
+ return startProxyProcess(baseUrl, url, port);
503
460
  }
504
461
  // Port is free — auto-start the proxy
462
+ return startProxyProcess(baseUrl, url, port);
463
+ }
464
+ /** Spawn the proxy process and wait for it to become healthy. */
465
+ async function startProxyProcess(baseUrl, url, port) {
505
466
  console.log(chalk.yellow(`\n Proxy not running at ${baseUrl} — starting it for you…`));
506
- // Resolve system node and agent index.js so the proxy doesn't use the
507
- // agent's bundled node (segfaults with --list-models on macOS).
508
- const sysNode = resolveSystemNode();
509
- const agentJs = resolveAgentIndexJs();
510
- let patchedProxy = false;
511
- if (sysNode && agentJs) {
512
- const proxyDir = findProxyPackageDir();
513
- if (proxyDir) {
514
- patchedProxy = patchProxyEnvJs(proxyDir);
515
- if (patchedProxy) {
516
- console.log(chalk.dim(` Using system node for agent subprocess: ${sysNode}`));
517
- }
518
- else {
519
- console.log(chalk.yellow(` ⚠ Couldn't patch proxy env.js — /v1/models may fail`));
520
- }
467
+ // Resolve system node and agent index.js so the proxy uses system Node.js
468
+ // for the agent subprocess (avoids segfaults with --list-models on macOS).
469
+ let sysNode = null;
470
+ let agentJs = null;
471
+ try {
472
+ sysNode = execSync("which node 2>/dev/null", { timeout: 3_000, encoding: "utf-8", shell: "bash" }).trim() || null;
473
+ const agentPath = execSync("command -v agent 2>/dev/null || command -v cursor-agent 2>/dev/null", {
474
+ timeout: 3_000, encoding: "utf-8", shell: "bash",
475
+ }).trim();
476
+ if (agentPath) {
477
+ const agentDir = dirname(realpathSync(agentPath));
478
+ const indexPath = `${agentDir}/index.js`;
479
+ if (existsSync(indexPath))
480
+ agentJs = indexPath;
521
481
  }
522
482
  }
483
+ catch { }
523
484
  const proxyEnv = {
524
485
  ...Object.fromEntries(Object.entries(process.env).filter(([, v]) => v !== undefined)),
525
486
  CURSOR_BRIDGE_API_KEY: process.env.CURSOR_BRIDGE_API_KEY
526
487
  || loadProviders().find(p => p.cursorProxy)?.cursorApiKey
527
488
  || "unused",
528
489
  };
529
- if (patchedProxy && sysNode && agentJs) {
490
+ if (sysNode && agentJs) {
530
491
  proxyEnv.CURSOR_AGENT_NODE = sysNode;
531
492
  proxyEnv.CURSOR_AGENT_SCRIPT = agentJs;
493
+ console.log(chalk.dim(` Using system node for agent subprocess: ${sysNode}`));
532
494
  }
533
495
  try {
534
496
  const child = spawn("npx", ["cursor-api-proxy"], {
@@ -746,11 +708,11 @@ export async function setupCursorProxy() {
746
708
  console.log(chalk.white(` ${chalk.bold("npx cursor-api-proxy")}`));
747
709
  for (;;) {
748
710
  const choice = await selectKey(` Proxy started?`, [
749
- { key: "r", desc: "etry" },
711
+ { key: "r", desc: "etry (re-attempt auto-start + kill stale)" },
750
712
  { key: "c", desc: "ancel" },
751
713
  ]);
752
714
  if (choice === "r") {
753
- if (await healthCheckCursorProxy()) {
715
+ if (await ensureCursorProxyRunning(PROXY_DEFAULT_URL, true)) {
754
716
  console.log(chalk.green("\n ✓ Proxy is running and healthy"));
755
717
  return true;
756
718
  }
@@ -802,36 +764,38 @@ async function pickCursorModel() {
802
764
  clearInterval(spinner);
803
765
  process.stdout.write("\x1B[2K\r");
804
766
  if (!healthy) {
805
- // Try to auto-start the proxy before bugging the user
767
+ // Try to auto-start the proxy (auto-kills stale processes)
806
768
  const autoStarted = await ensureCursorProxyRunning();
807
769
  if (autoStarted) {
808
770
  // Proxy is up now — proceed to model list
809
771
  }
810
772
  else {
811
773
  console.log(chalk.yellow(" Proxy is not running at " + PROXY_DEFAULT_URL));
812
- const choice = await selectKey(` How to proceed?`, [
813
- { key: "i", desc: "nstall + configure (CLI, API key, server)" },
814
- { key: "r", desc: "etry (I started it manually)" },
815
- { key: "c", desc: "ancel" },
816
- ]);
817
- if (choice === "s") {
818
- const ok = await setupCursorProxy();
819
- if (!ok)
774
+ for (;;) {
775
+ const choice = await selectKey(` How to proceed?`, [
776
+ { key: "r", desc: "etry (re-attempt auto-start + kill stale)" },
777
+ { key: "i", desc: "nstall + configure (CLI, API key, server)" },
778
+ { key: "c", desc: "ancel" },
779
+ ]);
780
+ if (choice === "r") {
781
+ if (await ensureCursorProxyRunning(PROXY_DEFAULT_URL, true)) {
782
+ console.log(chalk.green(" ✓ Proxy started"));
783
+ break;
784
+ }
785
+ console.log(chalk.yellow(` Still not reachable at ${PROXY_DEFAULT_URL}`));
786
+ }
787
+ else if (choice === "i") {
788
+ const ok = await setupCursorProxy();
789
+ if (!ok)
790
+ return null;
791
+ if (await healthCheckCursorProxy())
792
+ break;
820
793
  return null;
821
- }
822
- else if (choice === "r") {
823
- // User manually started it — one more health check
824
- if (await healthCheckCursorProxy()) {
825
- console.log(chalk.green(" ✓ Proxy detected"));
826
794
  }
827
795
  else {
828
- console.log(chalk.yellow(" Still not reachable — try the install flow or cancel."));
829
796
  return null;
830
797
  }
831
798
  }
832
- else {
833
- return null;
834
- }
835
799
  }
836
800
  }
837
801
  const { top, more } = await buildCursorPicker();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-overnight",
3
- "version": "1.19.1",
3
+ "version": "1.23.1",
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. Provider-agnostic model catalog with capability-based planning.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,6 +15,7 @@
15
15
  },
16
16
  "dependencies": {
17
17
  "@anthropic-ai/claude-agent-sdk": "^0.2.92",
18
+ "@claude-overnight/cursor-api-proxy": "file:../cursor-api-proxy",
18
19
  "chalk": "^5.4.1",
19
20
  "jsonwebtoken": "^9.0.2"
20
21
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-overnight",
3
- "version": "1.19.1",
3
+ "version": "1.23.1",
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. Supports Cursor API Proxy, Qwen, OpenRouter.",
5
5
  "author": {
6
6
  "name": "Francesco Fornace"