claude-overnight 1.25.14 → 1.25.19
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 +3 -3
- package/dist/_version.d.ts +1 -1
- package/dist/_version.js +1 -1
- package/dist/index.js +13 -9
- package/dist/planner-query.js +24 -13
- package/dist/planner.js +9 -1
- package/dist/providers.d.ts +25 -0
- package/dist/providers.js +124 -9
- package/dist/swarm.js +10 -3
- package/docs/CURSOR_PROXY_MACOS_DISCOVERY.md +64 -68
- package/package.json +1 -1
- package/plugins/claude-overnight/.claude-plugin/plugin.json +1 -1
package/README.md
CHANGED
|
@@ -97,11 +97,11 @@ security unlock-keychain ~/Library/Keychains/login.keychain-db
|
|
|
97
97
|
|
|
98
98
|
**Advanced:** If something else must share port `8765` and you manage the proxy yourself, set `CURSOR_OVERNIGHT_NO_PROXY_RESTART=1` to skip the automatic “replace listener” step when a Cursor API token is present.
|
|
99
99
|
|
|
100
|
-
**How headless Cursor + macOS Keychain actually works (discovery):** We documented the full investigation: why ACP
|
|
100
|
+
**How headless Cursor + macOS Keychain actually works (discovery):** We documented the full investigation: why ACP was the wrong path for opus/sonnet `*-thinking-*` variants (model-name mismatch → silent `exit 1`), how **chat-only workspace** (default in cursor-composer) fakes `HOME` and triggers **Keychain timeouts** despite a User API key, and how a cloned **account pool** makes parallel cursor-agent spawns race-free. See **[docs/CURSOR_PROXY_MACOS_DISCOVERY.md](docs/CURSOR_PROXY_MACOS_DISCOVERY.md)**.
|
|
101
101
|
|
|
102
|
-
**Quick reference — bundled proxy env:** `
|
|
102
|
+
**Quick reference — bundled proxy env:** `CURSOR_BRIDGE_USE_ACP=0` (CLI streaming path accepts all friendly model names), `CURSOR_BRIDGE_CHAT_ONLY_WORKSPACE=false`, `CURSOR_CONFIG_DIRS=<5 cloned pool dirs>` (parallel-safe), plus `CURSOR_API_KEY` / `CURSOR_AUTH_TOKEN` / `CURSOR_BRIDGE_API_KEY` and `CURSOR_SKIP_KEYCHAIN=1` / `CI=true`. Details and tables are in the doc above.
|
|
103
103
|
|
|
104
|
-
**Regression / stress test:** `npm run matrix:cursor-proxy` (optional `--quick`, `--include-danger`). Use `MATRIX_MODELS=composer-2,
|
|
104
|
+
**Regression / stress test:** `npm run matrix:cursor-proxy` (optional `--quick`, `--include-danger`). Use `MATRIX_MODELS=composer-2,claude-opus-4-7-thinking-high` to compare models; override `MATRIX_PORT_BASE`, `MATRIX_MODEL`, `MATRIX_MSG_TIMEOUT_MS` as needed.
|
|
105
105
|
|
|
106
106
|
## Install
|
|
107
107
|
|
package/dist/_version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "1.25.
|
|
1
|
+
export declare const VERSION = "1.25.19";
|
package/dist/_version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated by build — do not edit manually.
|
|
2
|
-
export const VERSION = "1.25.
|
|
2
|
+
export const VERSION = "1.25.19";
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import { Swarm } from "./swarm.js";
|
|
|
9
9
|
import { planTasks, refinePlan, identifyThemes, buildThinkingTasks, orchestrate, salvageFromFile } from "./planner.js";
|
|
10
10
|
import { modelDisplayName, formatContextWindow, DEFAULT_MODEL } from "./models.js";
|
|
11
11
|
import { setPlannerEnvResolver } from "./planner-query.js";
|
|
12
|
-
import { pickModel, loadProviders, preflightProvider, buildEnvResolver, healthCheckCursorProxy, PROXY_DEFAULT_URL, isCursorProxyProvider, ensureCursorProxyRunning, bundledComposerProxyShellCommand, warnMacCursorAgentShellPatchIfNeeded, hasCursorAgentToken, } from "./providers.js";
|
|
12
|
+
import { pickModel, loadProviders, preflightProvider, buildEnvResolver, healthCheckCursorProxy, PROXY_DEFAULT_URL, isCursorProxyProvider, readCursorProxyLogTail, ensureCursorProxyRunning, bundledComposerProxyShellCommand, warnMacCursorAgentShellPatchIfNeeded, hasCursorAgentToken, } from "./providers.js";
|
|
13
13
|
import { RunDisplay } from "./ui.js";
|
|
14
14
|
import { renderSummary } from "./render.js";
|
|
15
15
|
import { executeRun } from "./run.js";
|
|
@@ -806,11 +806,11 @@ async function main() {
|
|
|
806
806
|
}
|
|
807
807
|
}
|
|
808
808
|
process.stdout.write(` ${chalk.dim(`◆ Pinging ${pending.map(([r, p]) => `${r} (${p.displayName})`).join(", ")}…`)}\n`);
|
|
809
|
-
//
|
|
810
|
-
//
|
|
811
|
-
//
|
|
812
|
-
//
|
|
813
|
-
//
|
|
809
|
+
// Preflight strategy: all providers run fully in parallel. Cursor proxy
|
|
810
|
+
// providers used to race on the shared `~/.cursor/cli-config.json`, but the
|
|
811
|
+
// proxy now uses an account pool (`CURSOR_CONFIG_DIRS`) — each parallel
|
|
812
|
+
// cursor-agent subprocess gets its own config dir, eliminating the race.
|
|
813
|
+
// See ensureCursorAccountPool() in providers.ts.
|
|
814
814
|
const progress = (msg) => process.stdout.write(chalk.dim(` ${msg}\n`));
|
|
815
815
|
/** Cursor agent cold start + thinking-variant model latency can exceed 20s; API providers stay tight. */
|
|
816
816
|
const preflightMs = (p) => isCursorProxyProvider(p) ? 60_000 : 20_000;
|
|
@@ -823,10 +823,14 @@ async function main() {
|
|
|
823
823
|
if (!result.ok) {
|
|
824
824
|
console.error(chalk.red(` ✗ ${role} preflight failed: ${chalk.dim(result.error)}`));
|
|
825
825
|
if (isCursorProxyProvider(provider)) {
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
console.error(chalk.yellow(`
|
|
826
|
+
const tail = readCursorProxyLogTail(25);
|
|
827
|
+
if (tail) {
|
|
828
|
+
console.error(chalk.yellow(` ── proxy log tail (agent stderr + sessions) ──`));
|
|
829
|
+
for (const line of tail.split("\n"))
|
|
830
|
+
console.error(chalk.dim(` ${line}`));
|
|
829
831
|
}
|
|
832
|
+
const cmd = bundledComposerProxyShellCommand();
|
|
833
|
+
console.error(chalk.yellow(` The proxy at ${PROXY_DEFAULT_URL} may have crashed or timed out (e.g. keychain/UI). Retry, or start the bundled proxy: ${cmd ?? "npm install in the claude-overnight package, then re-run"}`));
|
|
830
834
|
}
|
|
831
835
|
else {
|
|
832
836
|
console.error(chalk.red(` Fix the provider at ~/.claude/claude-overnight/providers.json and retry.`));
|
package/dist/planner-query.js
CHANGED
|
@@ -174,23 +174,34 @@ async function runPlannerQueryOnce(prompt, opts, onLog) {
|
|
|
174
174
|
sessionId = msg.session_id;
|
|
175
175
|
if (msg.type === "stream_event") {
|
|
176
176
|
const ev = msg.event;
|
|
177
|
-
if (ev?.type === "content_block_start"
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
177
|
+
if (ev?.type === "content_block_start") {
|
|
178
|
+
const cb = ev.content_block;
|
|
179
|
+
if (cb?.type === "tool_use") {
|
|
180
|
+
toolCount++;
|
|
181
|
+
const toolName = cb.name;
|
|
182
|
+
const input = cb.input;
|
|
183
|
+
// Enrich event with target file/path for readability
|
|
184
|
+
const target = input?.path ?? input?.file_path ?? input?.command
|
|
185
|
+
? (typeof input?.command === "string" ? input.command.split(" ").slice(0, 3).join(" ") : "")
|
|
186
|
+
: "";
|
|
187
|
+
lastLogText = target ? `${toolName} ${target}` : toolName;
|
|
188
|
+
onLog(target ? `${toolName} → ${target}` : toolName, "event");
|
|
189
|
+
}
|
|
190
|
+
else if (cb?.type === "thinking" || cb?.type === "redacted_thinking") {
|
|
191
|
+
lastLogText = "thinking…";
|
|
192
|
+
}
|
|
187
193
|
}
|
|
188
194
|
if (ev?.type === "content_block_delta") {
|
|
189
195
|
const delta = ev.delta;
|
|
190
|
-
|
|
191
|
-
|
|
196
|
+
// thinking_delta carries reasoning text under `delta.thinking`;
|
|
197
|
+
// text_delta carries final-answer text under `delta.text`.
|
|
198
|
+
const raw = delta?.type === "text_delta" ? delta.text
|
|
199
|
+
: delta?.type === "thinking_delta" ? delta.thinking
|
|
200
|
+
: undefined;
|
|
201
|
+
if (typeof raw === "string" && raw) {
|
|
202
|
+
const snippet = raw.trim().replace(/[{}"\\,[\]]+/g, " ").replace(/\s+/g, " ").trim();
|
|
192
203
|
if (snippet.length > 5)
|
|
193
|
-
lastLogText = snippet.slice(
|
|
204
|
+
lastLogText = snippet.slice(-60);
|
|
194
205
|
}
|
|
195
206
|
}
|
|
196
207
|
}
|
package/dist/planner.js
CHANGED
|
@@ -180,7 +180,15 @@ export async function planTasks(objective, cwd, plannerModel, workerModel, permi
|
|
|
180
180
|
return tasks;
|
|
181
181
|
}
|
|
182
182
|
export async function identifyThemes(objective, count, cwd, model, permissionMode, onLog = () => { }) {
|
|
183
|
-
const resultText = await runPlannerQuery(`
|
|
183
|
+
const resultText = await runPlannerQuery(`You are picking ${count} research angles for architects who will deeply explore a codebase next.
|
|
184
|
+
|
|
185
|
+
First do a BRIEF recon (3-6 tool calls max, don't go deep): read package.json and README if present, glob the top-level directory, peek at one or two config files that reveal the stack. You are learning what this codebase actually IS -- not solving anything.
|
|
186
|
+
|
|
187
|
+
Then pick ${count} angles that carve up THIS specific codebase orthogonally. Prefer concrete subsystems you saw (e.g. "authentication + session handling", "time-tracking mutation paths") over generic buckets ("data layer", "UX").
|
|
188
|
+
|
|
189
|
+
Objective: ${objective}
|
|
190
|
+
|
|
191
|
+
Return ONLY a JSON object: {"themes": ["angle description", ...]}`, { cwd, model, permissionMode, outputFormat: THEMES_SCHEMA }, onLog);
|
|
184
192
|
const parsed = attemptJsonParse(resultText);
|
|
185
193
|
if (parsed?.themes && Array.isArray(parsed.themes))
|
|
186
194
|
return parsed.themes.slice(0, count);
|
package/dist/providers.d.ts
CHANGED
|
@@ -60,8 +60,33 @@ export declare function preflightProvider(p: ProviderConfig, cwd: string, timeou
|
|
|
60
60
|
error: string;
|
|
61
61
|
}>;
|
|
62
62
|
export declare const PROXY_DEFAULT_URL = "http://127.0.0.1:8765";
|
|
63
|
+
/** File we write the proxy child's stdout+stderr to (so agent errors aren't lost to stdio:ignore). */
|
|
64
|
+
export declare function cursorProxyOutLogPath(): string;
|
|
65
|
+
/** cursor-composer-in-claude's default sessions log (request trace + ERROR lines). */
|
|
66
|
+
export declare function cursorProxySessionsLogPath(): string;
|
|
67
|
+
/**
|
|
68
|
+
* Read the tail of both proxy logs for diagnostics. Returns a human-readable
|
|
69
|
+
* block with file paths + last lines, or null if neither log exists.
|
|
70
|
+
*/
|
|
71
|
+
export declare function readCursorProxyLogTail(linesPerFile?: number): string | null;
|
|
63
72
|
/** Check if a provider routes through cursor-composer-in-claude. */
|
|
64
73
|
export declare function isCursorProxyProvider(p: ProviderConfig): boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Ensure an "account pool" of cloned config dirs exists under
|
|
76
|
+
* `~/.cursor-api-proxy/accounts/pool-{1..N}`. Each clone is just a copy of the
|
|
77
|
+
* user's `~/.cursor/cli-config.json` (has `authInfo.email` so cursor-composer
|
|
78
|
+
* auto-discovers it as an authenticated account).
|
|
79
|
+
*
|
|
80
|
+
* Purpose: cursor-agent subprocesses write their own cli-config.json on every
|
|
81
|
+
* startup via atomic tmp+rename. When N siblings all write to the same file in
|
|
82
|
+
* parallel, rename can lose the race and raise ENOENT. Giving each spawned
|
|
83
|
+
* agent its own CURSOR_CONFIG_DIR (one per pool entry) lets cursor-composer's
|
|
84
|
+
* AccountPool round-robin between them — zero shared writes, zero race.
|
|
85
|
+
*
|
|
86
|
+
* Refreshed every startup so token rotations in ~/.cursor flow through.
|
|
87
|
+
* Returns the list of pool dir paths, or null if the source config is missing.
|
|
88
|
+
*/
|
|
89
|
+
export declare function ensureCursorAccountPool(poolSize?: number): string[] | null;
|
|
65
90
|
/** True if ~/.zshrc / ~/.zprofile contain the `run_cursor_agent` workaround (see README). */
|
|
66
91
|
export declare function hasCursorMacAgentZshPatch(): boolean;
|
|
67
92
|
/**
|
package/dist/providers.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, realpathSync } from "fs";
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, realpathSync, openSync, statSync, readSync, closeSync } from "fs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { homedir } from "os";
|
|
4
4
|
import { join, dirname } from "path";
|
|
@@ -385,10 +385,104 @@ async function preflightCursorProxyViaHttp(p, timeoutMs, opts) {
|
|
|
385
385
|
}
|
|
386
386
|
// ── Cursor API Proxy ──
|
|
387
387
|
export const PROXY_DEFAULT_URL = "http://127.0.0.1:8765";
|
|
388
|
+
/** Directory cursor-composer-in-claude uses for its own sessions.log (see env.js). */
|
|
389
|
+
function cursorProxyLogDir() {
|
|
390
|
+
return join(homedir(), ".cursor-api-proxy");
|
|
391
|
+
}
|
|
392
|
+
/** File we write the proxy child's stdout+stderr to (so agent errors aren't lost to stdio:ignore). */
|
|
393
|
+
export function cursorProxyOutLogPath() {
|
|
394
|
+
return join(cursorProxyLogDir(), "proxy.out.log");
|
|
395
|
+
}
|
|
396
|
+
/** cursor-composer-in-claude's default sessions log (request trace + ERROR lines). */
|
|
397
|
+
export function cursorProxySessionsLogPath() {
|
|
398
|
+
return join(cursorProxyLogDir(), "sessions.log");
|
|
399
|
+
}
|
|
400
|
+
function tailFile(path, maxLines, maxBytes = 32_768) {
|
|
401
|
+
try {
|
|
402
|
+
const st = statSync(path);
|
|
403
|
+
const size = st.size;
|
|
404
|
+
const start = size > maxBytes ? size - maxBytes : 0;
|
|
405
|
+
const buf = Buffer.alloc(size - start);
|
|
406
|
+
const fd = openSync(path, "r");
|
|
407
|
+
try {
|
|
408
|
+
readSync(fd, buf, 0, buf.length, start);
|
|
409
|
+
}
|
|
410
|
+
finally {
|
|
411
|
+
try {
|
|
412
|
+
closeSync(fd);
|
|
413
|
+
}
|
|
414
|
+
catch { }
|
|
415
|
+
}
|
|
416
|
+
const text = buf.toString("utf8");
|
|
417
|
+
const lines = text.split("\n").filter(Boolean);
|
|
418
|
+
return lines.slice(-maxLines).join("\n");
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Read the tail of both proxy logs for diagnostics. Returns a human-readable
|
|
426
|
+
* block with file paths + last lines, or null if neither log exists.
|
|
427
|
+
*/
|
|
428
|
+
export function readCursorProxyLogTail(linesPerFile = 20) {
|
|
429
|
+
const out = cursorProxyOutLogPath();
|
|
430
|
+
const sess = cursorProxySessionsLogPath();
|
|
431
|
+
const parts = [];
|
|
432
|
+
const outTail = tailFile(out, linesPerFile);
|
|
433
|
+
if (outTail)
|
|
434
|
+
parts.push(`── ${out} (last ${linesPerFile} lines) ──\n${outTail}`);
|
|
435
|
+
const sessTail = tailFile(sess, linesPerFile);
|
|
436
|
+
if (sessTail)
|
|
437
|
+
parts.push(`── ${sess} (last ${linesPerFile} lines) ──\n${sessTail}`);
|
|
438
|
+
return parts.length ? parts.join("\n\n") : null;
|
|
439
|
+
}
|
|
388
440
|
/** Check if a provider routes through cursor-composer-in-claude. */
|
|
389
441
|
export function isCursorProxyProvider(p) {
|
|
390
442
|
return p.cursorProxy === true || p.baseURL === PROXY_DEFAULT_URL;
|
|
391
443
|
}
|
|
444
|
+
/**
|
|
445
|
+
* Ensure an "account pool" of cloned config dirs exists under
|
|
446
|
+
* `~/.cursor-api-proxy/accounts/pool-{1..N}`. Each clone is just a copy of the
|
|
447
|
+
* user's `~/.cursor/cli-config.json` (has `authInfo.email` so cursor-composer
|
|
448
|
+
* auto-discovers it as an authenticated account).
|
|
449
|
+
*
|
|
450
|
+
* Purpose: cursor-agent subprocesses write their own cli-config.json on every
|
|
451
|
+
* startup via atomic tmp+rename. When N siblings all write to the same file in
|
|
452
|
+
* parallel, rename can lose the race and raise ENOENT. Giving each spawned
|
|
453
|
+
* agent its own CURSOR_CONFIG_DIR (one per pool entry) lets cursor-composer's
|
|
454
|
+
* AccountPool round-robin between them — zero shared writes, zero race.
|
|
455
|
+
*
|
|
456
|
+
* Refreshed every startup so token rotations in ~/.cursor flow through.
|
|
457
|
+
* Returns the list of pool dir paths, or null if the source config is missing.
|
|
458
|
+
*/
|
|
459
|
+
export function ensureCursorAccountPool(poolSize = 5) {
|
|
460
|
+
if (poolSize <= 0)
|
|
461
|
+
return null;
|
|
462
|
+
const source = join(homedir(), ".cursor", "cli-config.json");
|
|
463
|
+
if (!existsSync(source))
|
|
464
|
+
return null;
|
|
465
|
+
let sourceBuf;
|
|
466
|
+
try {
|
|
467
|
+
sourceBuf = readFileSync(source);
|
|
468
|
+
}
|
|
469
|
+
catch {
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
472
|
+
const dirs = [];
|
|
473
|
+
for (let i = 1; i <= poolSize; i++) {
|
|
474
|
+
const dir = join(homedir(), ".cursor-api-proxy", "accounts", `pool-${i}`);
|
|
475
|
+
try {
|
|
476
|
+
mkdirSync(dir, { recursive: true });
|
|
477
|
+
writeFileSync(join(dir, "cli-config.json"), sourceBuf);
|
|
478
|
+
dirs.push(dir);
|
|
479
|
+
}
|
|
480
|
+
catch {
|
|
481
|
+
// skip this slot; pool still works with fewer dirs
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return dirs.length > 0 ? dirs : null;
|
|
485
|
+
}
|
|
392
486
|
/** True if ~/.zshrc / ~/.zprofile contain the `run_cursor_agent` workaround (see README). */
|
|
393
487
|
export function hasCursorMacAgentZshPatch() {
|
|
394
488
|
let combined = "";
|
|
@@ -772,12 +866,18 @@ async function startProxyProcess(baseUrl, url, port) {
|
|
|
772
866
|
// if the shell omitted CURSOR_API_KEY (GUI launches, etc.).
|
|
773
867
|
CURSOR_API_KEY: agentToken,
|
|
774
868
|
CURSOR_AUTH_TOKEN: agentToken,
|
|
775
|
-
//
|
|
776
|
-
//
|
|
777
|
-
|
|
778
|
-
//
|
|
779
|
-
//
|
|
780
|
-
|
|
869
|
+
// Use the CLI streaming path (runStreaming), NOT ACP. The ACP path is broken for
|
|
870
|
+
// opus/sonnet *-thinking-*/effort-variant friendly names: cursor-composer's
|
|
871
|
+
// resolveAcpModelConfigValue only matches against ACP `name` fields (e.g.
|
|
872
|
+
// `claude-opus-4-7`), while friendly IDs like `claude-opus-4-7-thinking-high`
|
|
873
|
+
// come from `agent --list-models`. The ACP agent then replies
|
|
874
|
+
// `{error: "Invalid model value: claude-opus-4-7-thinking-high"}` and
|
|
875
|
+
// cursor-composer's acp-client swallows the error to a silent exit-1.
|
|
876
|
+
// The CLI path accepts all friendly names (verified with opus-thinking-high,
|
|
877
|
+
// gemini-3.1-pro, composer-2 via HTTP preflight). Keychain safety is preserved:
|
|
878
|
+
// the CLI path injects keychain-shim-inject.js via NODE_OPTIONS which no-ops
|
|
879
|
+
// /usr/bin/security calls on macOS (cursor-composer/dist/lib/process.js).
|
|
880
|
+
CURSOR_BRIDGE_USE_ACP: "0",
|
|
781
881
|
// cursor-composer chat-only mode fakes HOME to a temp dir; on macOS the agent still waits on
|
|
782
882
|
// Keychain (~30s) for `cursor-user` despite CURSOR_API_KEY. Use the real workspace profile.
|
|
783
883
|
CURSOR_BRIDGE_CHAT_ONLY_WORKSPACE: "false",
|
|
@@ -786,6 +886,12 @@ async function startProxyProcess(baseUrl, url, port) {
|
|
|
786
886
|
proxyEnv.CURSOR_AGENT_NODE = sysNode;
|
|
787
887
|
proxyEnv.CURSOR_AGENT_SCRIPT = agentJs;
|
|
788
888
|
}
|
|
889
|
+
// Enable the account pool so parallel cursor-agent subprocesses get
|
|
890
|
+
// separate CURSOR_CONFIG_DIRs — no more cli-config.json write race.
|
|
891
|
+
const pool = ensureCursorAccountPool(5);
|
|
892
|
+
if (pool && !proxyEnv.CURSOR_CONFIG_DIRS) {
|
|
893
|
+
proxyEnv.CURSOR_CONFIG_DIRS = pool.join(",");
|
|
894
|
+
}
|
|
789
895
|
console.log(chalk.dim(JSON.stringify({
|
|
790
896
|
claudeOvernight: VERSION,
|
|
791
897
|
spawnProxy: {
|
|
@@ -802,14 +908,23 @@ async function startProxyProcess(baseUrl, url, port) {
|
|
|
802
908
|
CURSOR_BRIDGE_USE_ACP: proxyEnv.CURSOR_BRIDGE_USE_ACP,
|
|
803
909
|
CURSOR_BRIDGE_CHAT_ONLY_WORKSPACE: proxyEnv.CURSOR_BRIDGE_CHAT_ONLY_WORKSPACE,
|
|
804
910
|
CURSOR_API_KEY: "(set)",
|
|
911
|
+
accountPool: proxyEnv.CURSOR_CONFIG_DIRS ? `${proxyEnv.CURSOR_CONFIG_DIRS.split(",").length} dirs` : "disabled",
|
|
805
912
|
},
|
|
806
913
|
},
|
|
807
914
|
})));
|
|
808
915
|
try {
|
|
809
|
-
|
|
916
|
+
// Capture proxy stdout+stderr to a log file — stdio:"ignore" was hiding
|
|
917
|
+
// agent stderr so "cursor_cli_error" responses had no actionable context.
|
|
918
|
+
const logPath = cursorProxyOutLogPath();
|
|
919
|
+
try {
|
|
920
|
+
mkdirSync(dirname(logPath), { recursive: true });
|
|
921
|
+
}
|
|
922
|
+
catch { }
|
|
923
|
+
const logFd = openSync(logPath, "a");
|
|
924
|
+
console.log(chalk.dim(` Spawning proxy… ${chalk.dim(`(logs: ${logPath})`)}`));
|
|
810
925
|
const child = spawn(process.execPath, [composerCli], {
|
|
811
926
|
detached: true,
|
|
812
|
-
stdio: "ignore",
|
|
927
|
+
stdio: ["ignore", logFd, logFd],
|
|
813
928
|
env: proxyEnv,
|
|
814
929
|
});
|
|
815
930
|
child.unref(); // let it outlive this process
|
package/dist/swarm.js
CHANGED
|
@@ -734,13 +734,20 @@ export class Swarm {
|
|
|
734
734
|
const target = input?.path ?? input?.file_path ?? (typeof input?.command === "string" ? input.command.split(" ").slice(0, 3).join(" ") : "");
|
|
735
735
|
this.log(agent.id, target ? `${cb.name} \u2192 ${target}` : cb.name);
|
|
736
736
|
}
|
|
737
|
+
else if (cb?.type === "thinking" || cb?.type === "redacted_thinking") {
|
|
738
|
+
agent.lastText = "thinking…";
|
|
739
|
+
}
|
|
737
740
|
}
|
|
738
741
|
else if (ev.type === "content_block_delta") {
|
|
739
742
|
const delta = ev.delta;
|
|
740
|
-
|
|
741
|
-
|
|
743
|
+
// thinking_delta: `delta.thinking`; text_delta: `delta.text`.
|
|
744
|
+
const raw = delta?.type === "text_delta" ? delta.text
|
|
745
|
+
: delta?.type === "thinking_delta" ? delta.thinking
|
|
746
|
+
: undefined;
|
|
747
|
+
if (typeof raw === "string") {
|
|
748
|
+
const t = raw.trim();
|
|
742
749
|
if (t)
|
|
743
|
-
agent.lastText = t.slice(
|
|
750
|
+
agent.lastText = t.slice(-80);
|
|
744
751
|
}
|
|
745
752
|
}
|
|
746
753
|
break;
|
|
@@ -1,116 +1,112 @@
|
|
|
1
|
-
# Cursor bundled proxy on macOS: Keychain, ACP,
|
|
1
|
+
# Cursor bundled proxy on macOS: Keychain, ACP, parallel safety
|
|
2
2
|
|
|
3
|
-
This document records **why** the Cursor API proxy (`cursor-composer-in-claude`)
|
|
3
|
+
This document records **why** the Cursor API proxy (`cursor-composer-in-claude`) is tricky to run headlessly (macOS Keychain dialogs, parallel crashes, model-name mismatches), **what did not fix it**, and the **env vars + account-pool setup** `claude-overnight` now ships as defaults.
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
## Context
|
|
8
8
|
|
|
9
|
-
- **claude-overnight**
|
|
10
|
-
- Headless use is supposed to rely on a **[User API key](https://cursor.com/docs/cli/headless)** (`CURSOR_API_KEY`
|
|
11
|
-
- Despite setting `CURSOR_SKIP_KEYCHAIN=1`, `CI=true`, and API keys, macOS could still show Keychain UI or block for ~30s with errors like **`Keychain operation timed out after 30000ms`** in the proxy log (`~/.cursor-api-proxy/sessions.log` or stderr).
|
|
9
|
+
- **claude-overnight** bundles **cursor-composer-in-claude**, an Anthropic-compatible HTTP server that forwards requests to the Cursor **`agent`** CLI. cursor-composer has **two agent paths**: **CLI streaming** (default, `useAcp=false`) and **ACP** (JSON-RPC over stdio, `useAcp=true`).
|
|
10
|
+
- Headless use is supposed to rely on a **[User API key](https://cursor.com/docs/cli/headless)** (`CURSOR_API_KEY`), not on interactive login stored as **`cursor-user`** in the login keychain.
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
## Symptoms we saw
|
|
16
|
-
|
|
17
|
-
1. **GUI:** System Keychain prompts, or “Keychain Not Found” style dialogs for `cursor-user`.
|
|
18
|
-
2. **Proxy logs:** `Agent error: Cursor CLI failed (exit 1): Error: Keychain operation timed out after 30000ms`.
|
|
19
|
-
3. **Stress tests:** Every matrix row returning **HTTP 500** looked like one bug; in reality **two different failure modes** were mixed (see below).
|
|
12
|
+
Three independent failure modes were mixed together in early reports: **Keychain contention**, **ACP model-name mismatch**, and a **`cli-config.json` write race** under parallel load.
|
|
20
13
|
|
|
21
14
|
---
|
|
22
15
|
|
|
23
|
-
##
|
|
24
|
-
|
|
25
|
-
These are still **correct** to set; they address real issues, but they did **not** alone stop Keychain contention on macOS.
|
|
16
|
+
## Failure mode 1: Keychain contention (chat-only workspace + temp `HOME`)
|
|
26
17
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
| **`CURSOR_BRIDGE_API_KEY`** | HTTP bearer for the proxy’s `/health` and `/v1/*` routes; often mirrored from the same token. |
|
|
32
|
-
| **`CURSOR_BRIDGE_ACP_SKIP_AUTHENTICATE=1`** | In `cursor-composer-in-claude`, `loadBridgeConfig` sets `acpSkipAuthenticate` when this is on **or** when an API key is present. Skips the ACP **`authenticate` / `cursor_login`** step that can touch Keychain. |
|
|
33
|
-
| **`CURSOR_BRIDGE_USE_ACP=1`** | Default bridge config has **`useAcp: false`**. Without ACP, traffic used **`runStreaming`** instead of **`runAcpStream`**; skip-authenticate only applies on the **ACP** path. Forcing ACP keeps behavior aligned with the intended headless/ACP pipeline. |
|
|
34
|
-
|
|
35
|
-
Without **`CURSOR_BRIDGE_USE_ACP=1`**, skip-authenticate did not apply to the code path that handled streaming requests.
|
|
18
|
+
- cursor-composer defaults **`CURSOR_BRIDGE_CHAT_ONLY_WORKSPACE=true`**. For each request it creates a temp dir and sets **`HOME`** (and related profile vars) to that temp dir so rules from the real `~/.cursor` are not loaded.
|
|
19
|
+
- With a valid User API key in env, `composer-2` could still hit **`Keychain operation timed out after 30000ms`** under chat-only. Setting **`CURSOR_BRIDGE_CHAT_ONLY_WORKSPACE=false`** made the same call succeed — the Cursor CLI was probing Keychain for `cursor-user` when its profile view was empty, even though API key auth was set.
|
|
20
|
+
- **Fix shipped:** spawn the proxy with `CURSOR_BRIDGE_CHAT_ONLY_WORKSPACE=false`. Trade-off: the agent no longer runs with a disposable fake `HOME` per request.
|
|
21
|
+
- Orthogonally, cursor-composer injects **`keychain-shim-inject.js`** via `NODE_OPTIONS` on macOS (see `node_modules/cursor-composer-in-claude/dist/lib/keychain-shim-inject.js`). It no-ops `/usr/bin/security` at the Node level for spawned agents — Keychain safety is preserved on the CLI streaming path without needing ACP's `skipAuthenticate`.
|
|
36
22
|
|
|
37
23
|
---
|
|
38
24
|
|
|
39
|
-
##
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
**Trade-off:** You lose the strictest isolation (the agent no longer runs with a disposable fake `HOME` for every request). You gain reliable headless behavior on macOS with API keys. For many automation setups this is the right default.
|
|
55
|
-
|
|
56
|
-
**How to see it in tests:** The matrix script includes a row **`12-chat-workspace-isolated`** (`CURSOR_BRIDGE_CHAT_ONLY_WORKSPACE=true`). With **`composer-2`**, that row tends to **fail** while **`01-overnight-parity`** passes, reproducing the regression.
|
|
25
|
+
## Failure mode 2: ACP model-name mismatch (opus/sonnet `*-thinking-*`)
|
|
26
|
+
|
|
27
|
+
- `agent --list-models` returns friendly IDs like `claude-opus-4-7-thinking-high`, `gemini-3.1-pro`.
|
|
28
|
+
- The ACP model catalog only exposes **bracketed** IDs keyed by stripped `name` fields: `claude-opus-4-7` with `modelId: claude-opus-4-7[thinking=true,effort=high]`. cursor-composer's `resolveAcpModelConfigValue` has no mapping from `claude-opus-4-7-thinking-high` to the bracketed form.
|
|
29
|
+
- When `USE_ACP=1`, the ACP agent replies:
|
|
30
|
+
```
|
|
31
|
+
{"error":{"code":-32602,"message":"Invalid params","data":{"message":"Invalid model value: claude-opus-4-7-thinking-high"}}}
|
|
32
|
+
```
|
|
33
|
+
`acp-client.js` swallows this RPC error to a silent `exit 1`, which the proxy surfaces as:
|
|
34
|
+
```
|
|
35
|
+
HTTP 500: The Cursor agent process exited with code 1. See server logs for details.
|
|
36
|
+
```
|
|
37
|
+
- **Fix shipped:** force **`CURSOR_BRIDGE_USE_ACP=0`**. The CLI streaming path accepts every `agent --list-models` friendly name (verified: `claude-opus-4-7-thinking-high`, `gemini-3.1-pro`, `composer-2`). Keychain safety is preserved by the `NODE_OPTIONS` shim above.
|
|
38
|
+
- `CURSOR_BRIDGE_ACP_SKIP_AUTHENTICATE` is no longer needed and no longer set: the CLI path never calls `cursor_login`.
|
|
57
39
|
|
|
58
40
|
---
|
|
59
41
|
|
|
60
|
-
##
|
|
42
|
+
## Failure mode 3: `cli-config.json` write race (parallel spawns)
|
|
61
43
|
|
|
62
|
-
|
|
44
|
+
- Every `cursor-agent` subprocess rewrites `~/.cursor/cli-config.json` on startup using atomic tmp+rename. N sibling spawns race on the `rename(*.tmp → cli-config.json)` step and intermittently raise:
|
|
45
|
+
```
|
|
46
|
+
ERROR POST /v1/messages 127.0.0.1 agent_exit_1
|
|
47
|
+
Error: ENOENT: no such file or directory,
|
|
48
|
+
rename '/Users/x/.cursor/cli-config.json.tmp' -> '/Users/x/.cursor/cli-config.json'
|
|
49
|
+
```
|
|
50
|
+
- Observed hit rate: ~20% (3/15 queries) at swarm concurrency 5 with shared config. Preflights with 3 cursor providers hit it too.
|
|
51
|
+
- cursor-composer has a built-in **AccountPool**: when `CURSOR_CONFIG_DIRS=<dir1,dir2,…>` is set (or `~/.cursor-api-proxy/accounts/<name>/cli-config.json` contains `authInfo.email`), it round-robins spawns across the dirs, each exported to the agent as its own `CURSOR_CONFIG_DIR`. Separate files, no shared rename target, no race.
|
|
52
|
+
- **Fix shipped:** `ensureCursorAccountPool()` in `src/providers.ts` clones `~/.cursor/cli-config.json` into `~/.cursor-api-proxy/accounts/pool-{1..5}` on startup and exports `CURSOR_CONFIG_DIRS` to the proxy. Pool is refreshed every startup so token rotations in `~/.cursor` propagate.
|
|
53
|
+
- **Verified:** 25/25 across 5 rounds × 5 parallel `composer-2` requests, zero `agent_exit_1` / `cli-config.json.tmp` entries in `~/.cursor-api-proxy/sessions.log`.
|
|
54
|
+
- **Preflight impact:** 3 cursor providers in parallel drop from ~21s sequential to ~8s.
|
|
63
55
|
|
|
64
56
|
---
|
|
65
57
|
|
|
66
58
|
## What claude-overnight sets when it auto-starts the proxy
|
|
67
59
|
|
|
68
|
-
|
|
60
|
+
`startProxyProcess` in `src/providers.ts` builds a `proxyEnv` that always includes:
|
|
69
61
|
|
|
70
|
-
| Variable | Purpose |
|
|
71
|
-
|
|
72
|
-
| `CI` | `"true"`
|
|
73
|
-
| `CURSOR_SKIP_KEYCHAIN` | `"1"`
|
|
74
|
-
| `CURSOR_API_KEY` / `CURSOR_AUTH_TOKEN` |
|
|
75
|
-
| `CURSOR_BRIDGE_API_KEY` | HTTP
|
|
76
|
-
|
|
|
77
|
-
|
|
|
78
|
-
| **`
|
|
79
|
-
| `CURSOR_AGENT_NODE` / `CURSOR_AGENT_SCRIPT` | When detected
|
|
62
|
+
| Variable | Value | Purpose |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| `CI` | `"true"` | Forced; prevents a parent shell from re-enabling interactive probes. |
|
|
65
|
+
| `CURSOR_SKIP_KEYCHAIN` | `"1"` | Cursor's own CI convention. |
|
|
66
|
+
| `CURSOR_API_KEY` / `CURSOR_AUTH_TOKEN` | User API key | Headless auth for the native agent; mirrored into the proxy spawn env because GUI launches can omit them. |
|
|
67
|
+
| `CURSOR_BRIDGE_API_KEY` | Same token | HTTP bearer for the proxy's `/health` and `/v1/*`. |
|
|
68
|
+
| **`CURSOR_BRIDGE_USE_ACP`** | **`"0"`** | CLI streaming path; avoids the ACP model-name mismatch for `*-thinking-*` variants. |
|
|
69
|
+
| **`CURSOR_BRIDGE_CHAT_ONLY_WORKSPACE`** | **`"false"`** | Avoids temp `HOME` → Keychain waits on macOS. |
|
|
70
|
+
| **`CURSOR_CONFIG_DIRS`** | **`pool-1,…,pool-5`** | Cloned from `~/.cursor/cli-config.json`; eliminates the write race under parallel spawns. |
|
|
71
|
+
| `CURSOR_AGENT_NODE` / `CURSOR_AGENT_SCRIPT` | When detected | System Node + `agent/index.js` (avoids known issues with the bundled Node on some macOS installs). |
|
|
80
72
|
|
|
81
|
-
|
|
73
|
+
Startup logs print an `accountPool` field in `spawnProxy.childEnv` showing how many pool dirs are active.
|
|
82
74
|
|
|
83
75
|
---
|
|
84
76
|
|
|
85
77
|
## How to verify
|
|
86
78
|
|
|
87
|
-
1. **Matrix (recommended):**
|
|
88
|
-
|
|
89
|
-
|
|
79
|
+
1. **Matrix (recommended):**
|
|
80
|
+
```
|
|
81
|
+
MATRIX_MODELS=composer-2,claude-opus-4-7-thinking-high npm run matrix:cursor-proxy
|
|
82
|
+
```
|
|
83
|
+
All rows (including thinking variants) should return **HTTP 200**.
|
|
84
|
+
|
|
85
|
+
2. **Parallel smoke (manual):** fire 5 concurrent `POST /v1/messages` at `composer-2` through the running proxy. With the account pool enabled, expect 5/5 200s and zero `agent_exit_1` in `~/.cursor-api-proxy/sessions.log`.
|
|
90
86
|
|
|
91
|
-
|
|
87
|
+
3. **Logs:** claude-overnight redirects proxy stdout+stderr to `~/.cursor-api-proxy/proxy.out.log` and prints a tail on preflight failure. cursor-composer's own request trace is `~/.cursor-api-proxy/sessions.log`.
|
|
92
88
|
|
|
93
|
-
|
|
89
|
+
4. **Preflight:** claude-overnight runs provider preflights fully in parallel (HTTP `POST /v1/messages` with `max_tokens: 4096`, not a claude-CLI spawn). Cursor proxy providers ride the account pool.
|
|
94
90
|
|
|
95
91
|
---
|
|
96
92
|
|
|
97
93
|
## When the OS keychain itself is broken
|
|
98
94
|
|
|
99
|
-
If **`login.keychain`** is missing or damaged, macOS can still show dialogs unrelated to Cursor. Keychain Access → First Aid, or `security unlock-keychain ~/Library/Keychains/login.keychain-db`, may help. That is **orthogonal** to the chat-only /
|
|
95
|
+
If **`login.keychain`** is missing or damaged, macOS can still show dialogs unrelated to Cursor. Keychain Access → First Aid, or `security unlock-keychain ~/Library/Keychains/login.keychain-db`, may help. That is **orthogonal** to the chat-only / ACP / pool discoveries above.
|
|
100
96
|
|
|
101
97
|
---
|
|
102
98
|
|
|
103
99
|
## References in this repo
|
|
104
100
|
|
|
105
|
-
- Implementation: `src/providers.ts` (`startProxyProcess`, `
|
|
101
|
+
- Implementation: `src/providers.ts` (`startProxyProcess`, `ensureCursorAccountPool`, `preflightCursorProxyViaHttp`, `readCursorProxyLogTail`).
|
|
102
|
+
- Preflight strategy: `src/index.ts` (all providers parallel via `Promise.all`).
|
|
106
103
|
- Stress harness: `scripts/cursor-proxy-keychain-matrix.mjs`, `npm run matrix:cursor-proxy`.
|
|
107
|
-
- Upstream behavior: `node_modules/cursor-composer-in-claude/dist/lib
|
|
104
|
+
- Upstream behavior: `node_modules/cursor-composer-in-claude/dist/lib/` — `env.js` (`configDirs` discovery), `account-pool.js` (round-robin), `process.js` (sets `CURSOR_CONFIG_DIR` from pool), `workspace.js` (`getChatOnlyEnvOverrides`), `acp-client.js` (`resolveAcpModelConfigValue` and the error-swallowing `exit 1`), `keychain-shim-inject.js` (`/usr/bin/security` no-op).
|
|
108
105
|
|
|
109
106
|
---
|
|
110
107
|
|
|
111
108
|
## Summary
|
|
112
109
|
|
|
113
|
-
1.
|
|
114
|
-
2. **`CURSOR_BRIDGE_CHAT_ONLY_WORKSPACE=false`**
|
|
115
|
-
3.
|
|
116
|
-
4. Use **`composer-2`** as the model name — `composer-2-fast` was never a real model in the ACP catalog.
|
|
110
|
+
1. **`CURSOR_BRIDGE_USE_ACP=0`** routes requests through the CLI streaming path, which accepts every friendly model name including opus/sonnet `*-thinking-*`.
|
|
111
|
+
2. **`CURSOR_BRIDGE_CHAT_ONLY_WORKSPACE=false`** stops temp-`HOME` from driving Keychain waits on macOS. The `NODE_OPTIONS` shim (`keychain-shim-inject.js`) keeps `/usr/bin/security` from reaching macOS.
|
|
112
|
+
3. **`CURSOR_CONFIG_DIRS=<pool-1,…,pool-5>`** gives each parallel cursor-agent subprocess its own `cli-config.json` — no more `rename(.tmp→cli-config.json)` race under swarm concurrency or parallel preflights.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-overnight",
|
|
3
|
-
"version": "1.25.
|
|
3
|
+
"version": "1.25.19",
|
|
4
4
|
"description": "Parallel Claude agents in git worktrees with a usage cap that reserves headroom for your interactive Claude Code. Crash-safe resume. Provider-agnostic model catalog (Anthropic, Cursor, OpenAI, Gemini, DeepSeek, Llama, Qwen) with capability-based task scoping.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-overnight",
|
|
3
|
-
"version": "1.25.
|
|
3
|
+
"version": "1.25.19",
|
|
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"
|