claude-overnight 1.19.1 → 1.23.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.
@@ -1 +1 @@
1
- export declare const VERSION = "1.19.1";
1
+ export declare const VERSION = "1.23.0";
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.0";
@@ -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
  /**
@@ -81,9 +82,12 @@ export declare function fetchCursorModels(baseUrl?: string): Promise<string[]>;
81
82
  * - Proxy not running → spawns `npx cursor-api-proxy` detached, waits for health
82
83
  * - Spawn fails (not installed) → returns false, caller falls back to manual instructions
83
84
  *
85
+ * When `forceRestart` is true and a stale process is on the port, it will be
86
+ * killed and the proxy restarted.
87
+ *
84
88
  * Returns true when the proxy is reachable at PROXY_DEFAULT_URL.
85
89
  */
86
- export declare function ensureCursorProxyRunning(baseUrl?: string): Promise<boolean>;
90
+ export declare function ensureCursorProxyRunning(baseUrl?: string, forceRestart?: boolean): Promise<boolean>;
87
91
  /**
88
92
  * Full install + configure flow for cursor-api-proxy.
89
93
  * Walks through CLI install, API key config, and proxy start.
package/dist/providers.js CHANGED
@@ -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,22 +351,52 @@ 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 {
352
377
  return false;
353
378
  }
354
379
  }
380
+ /**
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.
384
+ */
385
+ function killProcessOnPort(port, host = "127.0.0.1") {
386
+ try {
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);
395
+ }
396
+ catch {
397
+ return null;
398
+ }
399
+ }
355
400
  /**
356
401
  * Check whether something is already listening on the proxy port.
357
402
  * Returns true if any process bound the port (another instance, Cursor CLI, etc.).
@@ -437,6 +482,35 @@ function patchProxyEnvJs(proxyDir) {
437
482
  }
438
483
  return true;
439
484
  }
485
+ /**
486
+ * Patch the proxy's token-cache.js to skip the macOS keychain read.
487
+ *
488
+ * The proxy calls `security find-generic-password -s "cursor-access-token" -w`
489
+ * after every agent run to cache tokens for its multi-account pool feature.
490
+ * This triggers a macOS keychain popup even though the key is not needed for
491
+ * auth (the cursor-agent subprocess handles its own auth). We neutralize it.
492
+ *
493
+ * Safe to call repeatedly — the patch is idempotent.
494
+ */
495
+ function patchProxyTokenCacheJs(proxyDir) {
496
+ const tcJs = join(proxyDir, "dist", "lib", "token-cache.js");
497
+ if (!existsSync(tcJs))
498
+ return false;
499
+ const src = readFileSync(tcJs, "utf-8");
500
+ // Check if already patched
501
+ if (src.includes("/* claude-overnight patch: skip keychain */"))
502
+ return true;
503
+ const patch = `\n/* claude-overnight patch: skip keychain */\n` +
504
+ `return undefined;`;
505
+ // Replace the entire execSync chain inside readKeychainToken (multi-line)
506
+ const target = `const t = execSync('security find-generic-password -s "cursor-access-token" -w', { stdio: ["pipe", "pipe", "pipe"], timeout: 5000 })
507
+ .toString()
508
+ .trim();`;
509
+ if (!src.includes(target))
510
+ return false;
511
+ writeFileSync(tcJs, src.replace(target, patch + "\n// " + target.replace(/\n/g, "\n// ")), "utf-8");
512
+ return true;
513
+ }
440
514
  /**
441
515
  * Find the cursor-api-proxy package directory (npx cache or global install).
442
516
  */
@@ -482,14 +556,30 @@ function findProxyPackageDir() {
482
556
  * - Proxy not running → spawns `npx cursor-api-proxy` detached, waits for health
483
557
  * - Spawn fails (not installed) → returns false, caller falls back to manual instructions
484
558
  *
559
+ * When `forceRestart` is true and a stale process is on the port, it will be
560
+ * killed and the proxy restarted.
561
+ *
485
562
  * Returns true when the proxy is reachable at PROXY_DEFAULT_URL.
486
563
  */
487
- export async function ensureCursorProxyRunning(baseUrl = PROXY_DEFAULT_URL) {
564
+ export async function ensureCursorProxyRunning(baseUrl = PROXY_DEFAULT_URL, forceRestart = false) {
488
565
  const url = new URL(baseUrl);
489
566
  const port = parseInt(url.port, 10) || 80;
567
+ // Always patch the npx cache on startup so proxy skips keychain reads.
568
+ // Idempotent — safe to call on every run.
569
+ const proxyDir = findProxyPackageDir();
570
+ if (proxyDir)
571
+ patchProxyTokenCacheJs(proxyDir);
490
572
  // Already healthy?
491
- if (await healthCheckCursorProxy(baseUrl))
573
+ if (await healthCheckCursorProxy(baseUrl)) {
574
+ // Proxy was running before the patch — restart it to load the patched token-cache.js.
575
+ console.log(chalk.dim(` Proxy already running — restarting to pick up keychain patch…`));
576
+ const killedPid = killProcessOnPort(port, url.hostname);
577
+ if (killedPid) {
578
+ await new Promise(r => setTimeout(r, 500));
579
+ return startProxyProcess(baseUrl, url, port);
580
+ }
492
581
  return true;
582
+ }
493
583
  // Something bound the port — verify it's actually the cursor proxy
494
584
  if (await isPortInUse(port, url.hostname)) {
495
585
  const isProxy = await verifyCursorProxy(baseUrl);
@@ -497,11 +587,25 @@ export async function ensureCursorProxyRunning(baseUrl = PROXY_DEFAULT_URL) {
497
587
  console.log(chalk.dim(` Proxy verified at port ${port}`));
498
588
  return true;
499
589
  }
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;
590
+ // Stale process on the port kill it if forceRestart, or try automatically
591
+ if (!forceRestart) {
592
+ console.log(chalk.yellow(` ⚠ Something is on port ${port} but it's not cursor-api-proxy — killing stale process…`));
593
+ }
594
+ const killedPid = killProcessOnPort(port, url.hostname);
595
+ if (killedPid) {
596
+ console.log(chalk.green(` ✓ Killed stale process PID ${killedPid} on port ${port}`));
597
+ await new Promise(r => setTimeout(r, 500));
598
+ return startProxyProcess(baseUrl, url, port);
599
+ }
600
+ // Couldn't kill (permission denied, already gone) — try starting anyway
601
+ console.log(chalk.yellow(` ⚠ Couldn't kill process on port ${port} — attempting to start proxy anyway…`));
602
+ return startProxyProcess(baseUrl, url, port);
503
603
  }
504
604
  // Port is free — auto-start the proxy
605
+ return startProxyProcess(baseUrl, url, port);
606
+ }
607
+ /** Spawn the proxy process and wait for it to become healthy. */
608
+ async function startProxyProcess(baseUrl, url, port) {
505
609
  console.log(chalk.yellow(`\n Proxy not running at ${baseUrl} — starting it for you…`));
506
610
  // Resolve system node and agent index.js so the proxy doesn't use the
507
611
  // agent's bundled node (segfaults with --list-models on macOS).
@@ -746,11 +850,11 @@ export async function setupCursorProxy() {
746
850
  console.log(chalk.white(` ${chalk.bold("npx cursor-api-proxy")}`));
747
851
  for (;;) {
748
852
  const choice = await selectKey(` Proxy started?`, [
749
- { key: "r", desc: "etry" },
853
+ { key: "r", desc: "etry (re-attempt auto-start + kill stale)" },
750
854
  { key: "c", desc: "ancel" },
751
855
  ]);
752
856
  if (choice === "r") {
753
- if (await healthCheckCursorProxy()) {
857
+ if (await ensureCursorProxyRunning(PROXY_DEFAULT_URL, true)) {
754
858
  console.log(chalk.green("\n ✓ Proxy is running and healthy"));
755
859
  return true;
756
860
  }
@@ -802,36 +906,38 @@ async function pickCursorModel() {
802
906
  clearInterval(spinner);
803
907
  process.stdout.write("\x1B[2K\r");
804
908
  if (!healthy) {
805
- // Try to auto-start the proxy before bugging the user
909
+ // Try to auto-start the proxy (auto-kills stale processes)
806
910
  const autoStarted = await ensureCursorProxyRunning();
807
911
  if (autoStarted) {
808
912
  // Proxy is up now — proceed to model list
809
913
  }
810
914
  else {
811
915
  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)
916
+ for (;;) {
917
+ const choice = await selectKey(` How to proceed?`, [
918
+ { key: "r", desc: "etry (re-attempt auto-start + kill stale)" },
919
+ { key: "i", desc: "nstall + configure (CLI, API key, server)" },
920
+ { key: "c", desc: "ancel" },
921
+ ]);
922
+ if (choice === "r") {
923
+ if (await ensureCursorProxyRunning(PROXY_DEFAULT_URL, true)) {
924
+ console.log(chalk.green(" ✓ Proxy started"));
925
+ break;
926
+ }
927
+ console.log(chalk.yellow(` Still not reachable at ${PROXY_DEFAULT_URL}`));
928
+ }
929
+ else if (choice === "i") {
930
+ const ok = await setupCursorProxy();
931
+ if (!ok)
932
+ return null;
933
+ if (await healthCheckCursorProxy())
934
+ break;
820
935
  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
936
  }
827
937
  else {
828
- console.log(chalk.yellow(" Still not reachable — try the install flow or cancel."));
829
938
  return null;
830
939
  }
831
940
  }
832
- else {
833
- return null;
834
- }
835
941
  }
836
942
  }
837
943
  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.0",
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": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-overnight",
3
- "version": "1.19.1",
3
+ "version": "1.23.0",
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"