cursor-agent-bridge 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -1,16 +1,700 @@
1
1
  #!/usr/bin/env node
2
- import { t as startServer } from "./server-CuHDT_fJ.mjs";
3
- //#region src/cli.ts
2
+ import { i as CursorRunner, n as engines, r as version, s as toOpenAIModelList, t as startServer } from "./server-Bk7ol2lA.mjs";
3
+ import { execFile, execFileSync, spawn } from "node:child_process";
4
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
5
+ import { homedir } from "node:os";
6
+ import { dirname, join, resolve } from "node:path";
7
+ import { promisify } from "node:util";
8
+ import { chmodSync, existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
9
+ //#region src/cli-args.ts
4
10
  function readArg(name, fallback) {
5
11
  const index = process.argv.indexOf(name);
6
- return index >= 0 ? process.argv[index + 1] : fallback;
12
+ if (index < 0) return fallback;
13
+ const value = process.argv[index + 1];
14
+ if (!value || value.startsWith("-")) throw new Error(`Missing value for ${name}`);
15
+ return value;
16
+ }
17
+ function parsePort(value, fallback) {
18
+ if (value === void 0) return fallback;
19
+ const port = Number(value);
20
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error(`Invalid port: ${value}`);
21
+ return port;
22
+ }
23
+ function readHostAndPort(defaultHost = "127.0.0.1", defaultPort = 4646) {
24
+ return {
25
+ host: readArg("--host", process.env.HOST) ?? defaultHost,
26
+ port: parsePort(readArg("--port", process.env.PORT), defaultPort)
27
+ };
28
+ }
29
+ //#endregion
30
+ //#region src/codex-config.ts
31
+ const DEFAULT_CODEX_PROFILE = "cursor";
32
+ function resolveCodexConfigPath(profile = DEFAULT_CODEX_PROFILE, homeDir = homedir()) {
33
+ assertValidProfile(profile);
34
+ return join(homeDir, ".codex", `${profile}.config.toml`);
35
+ }
36
+ function buildBaseUrl(host, port) {
37
+ return `http://${host}:${port}/v1`;
38
+ }
39
+ function buildCodexConfigToml(options) {
40
+ const profile = options.profile ?? "cursor";
41
+ assertValidProfile(profile);
42
+ const baseUrl = buildBaseUrl(options.host, options.port);
43
+ return `model_provider = ${formatTomlString(profile)}
44
+ model = "auto"
45
+
46
+ [model_providers.${profile}]
47
+ name = "Cursor Agent Bridge"
48
+ base_url = ${formatTomlString(baseUrl)}
49
+ wire_api = "responses"
50
+ `;
51
+ }
52
+ function parseCodexConfig(content, profile = DEFAULT_CODEX_PROFILE) {
53
+ assertValidProfile(profile);
54
+ const fields = {};
55
+ const providerSection = `model_providers.${profile}`;
56
+ let section = "";
57
+ for (const rawLine of content.split("\n")) {
58
+ const line = rawLine.trim();
59
+ if (!line || line.startsWith("#")) continue;
60
+ const sectionMatch = line.match(/^\[(.+)\]$/);
61
+ if (sectionMatch) {
62
+ section = sectionMatch[1] ?? "";
63
+ continue;
64
+ }
65
+ const assignment = line.match(/^([A-Za-z0-9_.-]+)\s*=\s*(.+)$/);
66
+ if (!assignment) continue;
67
+ const key = assignment[1] ?? "";
68
+ const value = parseTomlValue(assignment[2] ?? "");
69
+ if (section === providerSection) {
70
+ if (key === "name") fields.providerName = value;
71
+ if (key === "base_url") fields.baseUrl = value;
72
+ if (key === "wire_api") fields.wireApi = value;
73
+ continue;
74
+ }
75
+ if (section) continue;
76
+ if (key === "model_provider") fields.modelProvider = value;
77
+ if (key === "model") fields.model = value;
78
+ }
79
+ return fields;
80
+ }
81
+ function checkCodexConfig(content, options) {
82
+ const profile = options.profile ?? "cursor";
83
+ const expectedBaseUrl = buildBaseUrl(options.host, options.port);
84
+ const fields = parseCodexConfig(content, profile);
85
+ const issues = [];
86
+ if (fields.modelProvider !== profile) issues.push(`model_provider should be "${profile}"${fields.modelProvider ? `, found "${fields.modelProvider}"` : ", but it is missing"}`);
87
+ if (fields.baseUrl !== expectedBaseUrl) issues.push(`base_url should be "${expectedBaseUrl}"${fields.baseUrl ? `, found "${fields.baseUrl}"` : ", but it is missing"}`);
88
+ if (fields.wireApi !== "responses") issues.push(`wire_api should be "responses"${fields.wireApi ? `, found "${fields.wireApi}"` : ", but it is missing"}`);
89
+ return {
90
+ ok: issues.length === 0,
91
+ issues
92
+ };
93
+ }
94
+ async function writeCodexConfig(options) {
95
+ let existing;
96
+ try {
97
+ existing = await readFile(options.filePath, "utf8");
98
+ } catch (error) {
99
+ if (error.code !== "ENOENT") throw error;
100
+ }
101
+ if (existing === void 0) {
102
+ await mkdir(dirname(options.filePath), { recursive: true });
103
+ await writeFile(options.filePath, buildCodexConfigToml(options), "utf8");
104
+ return {
105
+ path: options.filePath,
106
+ created: true,
107
+ updated: false
108
+ };
109
+ }
110
+ const merged = mergeCodexConfig(existing, options);
111
+ if ("error" in merged) throw new Error(merged.error);
112
+ if (!merged.changed) return {
113
+ path: options.filePath,
114
+ created: false,
115
+ updated: false
116
+ };
117
+ await writeFile(options.filePath, merged.content, "utf8");
118
+ return {
119
+ path: options.filePath,
120
+ created: false,
121
+ updated: true
122
+ };
123
+ }
124
+ function mergeCodexConfig(existing, options) {
125
+ const profile = options.profile ?? "cursor";
126
+ assertValidProfile(profile);
127
+ const providerSection = `model_providers.${profile}`;
128
+ const expected = buildCodexConfigToml(options);
129
+ const parsed = parseCodexConfig(existing, profile);
130
+ if (parsed.modelProvider && parsed.modelProvider !== profile && !options.force) return { error: `model_provider is "${parsed.modelProvider}". Re-run with --force to switch it to "${profile}".` };
131
+ const lines = existing.split("\n");
132
+ const output = [];
133
+ let section = "";
134
+ let inProviderSection = false;
135
+ let sawModelProvider = false;
136
+ let sawModel = false;
137
+ let sawProviderSection = false;
138
+ const handledProviderKeys = /* @__PURE__ */ new Set();
139
+ for (const rawLine of lines) {
140
+ const trimmed = rawLine.trim();
141
+ const sectionMatch = trimmed.match(/^\[(.+)\]$/);
142
+ if (sectionMatch) {
143
+ if (inProviderSection) appendMissingProviderKeys();
144
+ section = sectionMatch[1] ?? "";
145
+ inProviderSection = section === providerSection;
146
+ if (inProviderSection) sawProviderSection = true;
147
+ output.push(rawLine);
148
+ continue;
149
+ }
150
+ const assignment = trimmed.match(/^([A-Za-z0-9_.-]+)\s*=\s*(.+)$/);
151
+ if (!assignment) {
152
+ output.push(rawLine);
153
+ continue;
154
+ }
155
+ const key = assignment[1] ?? "";
156
+ if (!section) {
157
+ if (key === "model_provider") {
158
+ sawModelProvider = true;
159
+ output.push(`model_provider = ${formatTomlString(profile)}`);
160
+ continue;
161
+ }
162
+ if (key === "model") {
163
+ sawModel = true;
164
+ if (parsed.model === void 0 || options.force) output.push("model = \"auto\"");
165
+ else output.push(rawLine);
166
+ continue;
167
+ }
168
+ }
169
+ if (inProviderSection) {
170
+ if (key === "name") {
171
+ handledProviderKeys.add("name");
172
+ output.push("name = \"Cursor Agent Bridge\"");
173
+ continue;
174
+ }
175
+ if (key === "base_url") {
176
+ handledProviderKeys.add("base_url");
177
+ output.push(`base_url = ${formatTomlString(buildBaseUrl(options.host, options.port))}`);
178
+ continue;
179
+ }
180
+ if (key === "wire_api") {
181
+ handledProviderKeys.add("wire_api");
182
+ output.push("wire_api = \"responses\"");
183
+ continue;
184
+ }
185
+ }
186
+ output.push(rawLine);
187
+ }
188
+ if (inProviderSection) appendMissingProviderKeys();
189
+ const missingTopLevel = [];
190
+ if (!sawModelProvider) missingTopLevel.push(`model_provider = ${formatTomlString(profile)}`);
191
+ if (!sawModel) missingTopLevel.push("model = \"auto\"");
192
+ let content = output.join("\n").trimEnd();
193
+ if (missingTopLevel.length > 0) content = `${missingTopLevel.join("\n")}\n${content}`;
194
+ if (!sawProviderSection) {
195
+ const providerBlock = expected.split("\n").slice(3).join("\n");
196
+ content = `${content}\n\n${providerBlock}\n`;
197
+ }
198
+ const changed = normalizeToml(content) !== normalizeToml(existing);
199
+ return {
200
+ content: `${content.trimEnd()}\n`,
201
+ changed
202
+ };
203
+ function appendMissingProviderKeys() {
204
+ if (!handledProviderKeys.has("name")) output.push("name = \"Cursor Agent Bridge\"");
205
+ if (!handledProviderKeys.has("base_url")) output.push(`base_url = ${formatTomlString(buildBaseUrl(options.host, options.port))}`);
206
+ if (!handledProviderKeys.has("wire_api")) output.push("wire_api = \"responses\"");
207
+ }
208
+ }
209
+ function assertValidProfile(profile) {
210
+ if (/^[A-Za-z0-9_-]+$/.test(profile)) return;
211
+ throw new Error("Invalid Codex profile. Use only letters, numbers, underscores, or hyphens.");
212
+ }
213
+ function formatTomlString(value) {
214
+ return `"${value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"").replaceAll("\b", "\\b").replaceAll(" ", "\\t").replaceAll("\n", "\\n").replaceAll("\f", "\\f").replaceAll("\r", "\\r")}"`;
215
+ }
216
+ function normalizeToml(content) {
217
+ return content.replace(/\r\n/g, "\n").trimEnd();
218
+ }
219
+ function parseTomlValue(raw) {
220
+ const trimmed = stripInlineTomlComment(raw).trim();
221
+ if (trimmed.startsWith("\"") && trimmed.endsWith("\"")) return unescapeTomlString(trimmed.slice(1, -1));
222
+ if (trimmed.startsWith("'") && trimmed.endsWith("'")) return trimmed.slice(1, -1);
223
+ return trimmed;
224
+ }
225
+ function unescapeTomlString(value) {
226
+ return value.replace(/\\(["\\btnfr])/g, (_match, escaped) => ({
227
+ "\"": "\"",
228
+ "\\": "\\",
229
+ b: "\b",
230
+ t: " ",
231
+ n: "\n",
232
+ f: "\f",
233
+ r: "\r"
234
+ })[escaped] ?? escaped);
235
+ }
236
+ function stripInlineTomlComment(raw) {
237
+ let quote;
238
+ for (let index = 0; index < raw.length; index += 1) {
239
+ const char = raw[index];
240
+ if ((char === "\"" || char === "'") && raw[index - 1] !== "\\") {
241
+ quote = quote === char ? void 0 : quote ?? char;
242
+ continue;
243
+ }
244
+ if (!quote && char === "#") return raw.slice(0, index);
245
+ }
246
+ return raw;
247
+ }
248
+ //#endregion
249
+ //#region src/doctor.ts
250
+ const execFileAsync$1 = promisify(execFile);
251
+ async function runDoctor(options) {
252
+ const checks = [];
253
+ const fetchFn = options.fetchFn ?? fetch;
254
+ const execFileFn = options.execFileFn ?? execFileAsync$1;
255
+ const readFileFn = options.readFileFn ?? readFile;
256
+ const agentPath = options.agentPath ?? process.env.CURSOR_AGENT_PATH ?? "agent";
257
+ const profile = options.profile ?? "cursor";
258
+ const codexConfigPath = options.codexConfigPath ?? resolveCodexConfigPath(profile);
259
+ checks.push(checkNodeVersion(options.nodeVersionRange ?? engines.node));
260
+ checks.push({
261
+ name: "bridge-version",
262
+ ok: true,
263
+ message: `cursor-agent-bridge ${version}`
264
+ });
265
+ const agentCheck = await checkAgentExecutable(agentPath, execFileFn);
266
+ checks.push(agentCheck);
267
+ if (agentCheck.ok) checks.push(await checkAgentLogin(agentPath, options.runner));
268
+ checks.push(await checkBridgeHealth(options.host, options.port, fetchFn));
269
+ if (!options.skipCodexConfig) checks.push(await checkCodexConfigFile({
270
+ codexConfigPath,
271
+ host: options.host,
272
+ port: options.port,
273
+ profile,
274
+ readFileFn
275
+ }));
276
+ return {
277
+ ok: checks.every((check) => check.ok),
278
+ checks
279
+ };
280
+ }
281
+ function formatDoctorReport(result) {
282
+ const lines = result.checks.map((check) => {
283
+ const prefix = check.ok ? "✓" : "✗";
284
+ const hint = check.hint ? `\n → ${check.hint}` : "";
285
+ return `${prefix} ${check.name}: ${check.message}${hint}`;
286
+ });
287
+ lines.push("");
288
+ lines.push(result.ok ? "All checks passed. Codex can use Cursor Agent through the bridge." : "Some checks failed. Fix the items above before starting Codex.");
289
+ return `${lines.join("\n")}\n`;
290
+ }
291
+ function checkNodeVersion(requiredRange) {
292
+ const current = process.version.slice(1);
293
+ const minimum = parseMinimumNodeVersion(requiredRange);
294
+ const ok = compareNodeVersion(current, minimum) >= 0;
295
+ return ok ? {
296
+ name: "node-version",
297
+ ok,
298
+ message: `Node ${current} satisfies ${requiredRange}`
299
+ } : {
300
+ name: "node-version",
301
+ ok,
302
+ message: `Node ${current} does not satisfy ${requiredRange}`,
303
+ hint: `Install Node ${minimum} or newer.`
304
+ };
305
+ }
306
+ async function checkAgentExecutable(agentPath, execFileFn) {
307
+ try {
308
+ await execFileFn(agentPath, ["--help"], { timeout: 5e3 });
309
+ return {
310
+ name: "agent-cli",
311
+ ok: true,
312
+ message: `Cursor Agent CLI found at ${agentPath}`
313
+ };
314
+ } catch (error) {
315
+ return {
316
+ name: "agent-cli",
317
+ ok: false,
318
+ message: error instanceof Error ? error.message : "Cursor Agent CLI not found",
319
+ hint: "Install the Cursor Agent CLI and ensure `agent` is on PATH, or set CURSOR_AGENT_PATH."
320
+ };
321
+ }
322
+ }
323
+ async function checkAgentLogin(agentPath, runner = new CursorRunner({ agentPath })) {
324
+ try {
325
+ const models = await runner.listModels({ refresh: true });
326
+ if (models.length === 0) return {
327
+ name: "agent-login",
328
+ ok: false,
329
+ message: "Cursor Agent responded, but returned no models",
330
+ hint: "Run `agent login` and confirm `agent --list-models` returns models."
331
+ };
332
+ return {
333
+ name: "agent-login",
334
+ ok: true,
335
+ message: `Cursor Agent is logged in (${models.length} models available)`
336
+ };
337
+ } catch (error) {
338
+ return {
339
+ name: "agent-login",
340
+ ok: false,
341
+ message: error instanceof Error ? error.message : "Cursor Agent login check failed",
342
+ hint: "Run `agent login` and retry `cursor-agent-bridge doctor`."
343
+ };
344
+ }
345
+ }
346
+ async function checkBridgeHealth(host, port, fetchFn) {
347
+ const url = `http://${host}:${port}/health`;
348
+ try {
349
+ const response = await fetchFn(url, { signal: AbortSignal.timeout(3e3) });
350
+ if (!response.ok) return {
351
+ name: "bridge-health",
352
+ ok: false,
353
+ message: `${url} returned HTTP ${response.status}`,
354
+ hint: "Start the bridge with `cursor-agent-bridge serve` or `cursor-agent-bridge launch-agent install`."
355
+ };
356
+ const payload = await response.json();
357
+ return {
358
+ name: "bridge-health",
359
+ ok: true,
360
+ message: payload.version ? `Bridge is listening on ${url} (version ${payload.version})` : `Bridge is listening on ${url}`
361
+ };
362
+ } catch (error) {
363
+ return {
364
+ name: "bridge-health",
365
+ ok: false,
366
+ message: error instanceof Error ? error.message : "Bridge health check failed",
367
+ hint: "Start the bridge with `cursor-agent-bridge serve` or `cursor-agent-bridge launch-agent install`."
368
+ };
369
+ }
370
+ }
371
+ async function checkCodexConfigFile(options) {
372
+ try {
373
+ const result = checkCodexConfig(await options.readFileFn(options.codexConfigPath, "utf8"), {
374
+ host: options.host,
375
+ port: options.port,
376
+ profile: options.profile
377
+ });
378
+ if (result.ok) return {
379
+ name: "codex-config",
380
+ ok: true,
381
+ message: `Codex config looks correct at ${options.codexConfigPath}`
382
+ };
383
+ return {
384
+ name: "codex-config",
385
+ ok: false,
386
+ message: result.issues.join("; "),
387
+ hint: "Run `cursor-agent-bridge config write` or `cursor-agent-bridge config print`."
388
+ };
389
+ } catch (error) {
390
+ if (error.code === "ENOENT") return {
391
+ name: "codex-config",
392
+ ok: false,
393
+ message: `Codex config not found at ${options.codexConfigPath}`,
394
+ hint: "Run `cursor-agent-bridge config write` to create it."
395
+ };
396
+ return {
397
+ name: "codex-config",
398
+ ok: false,
399
+ message: error instanceof Error ? error.message : "Codex config check failed",
400
+ hint: "Verify the Codex config path and file permissions."
401
+ };
402
+ }
403
+ }
404
+ function parseMinimumNodeVersion(range) {
405
+ const match = range.match(/(\d+)\.(\d+)(?:\.(\d+))?/);
406
+ if (!match) return "0.0.0";
407
+ return `${match[1]}.${match[2]}.${match[3] ?? 0}`;
408
+ }
409
+ function compareNodeVersion(left, right) {
410
+ const toParts = (value) => value.split(".").map((part) => Number.parseInt(part, 10) || 0);
411
+ const [leftMajor = 0, leftMinor = 0, leftPatch = 0] = toParts(left);
412
+ const [rightMajor = 0, rightMinor = 0, rightPatch = 0] = toParts(right);
413
+ if (leftMajor !== rightMajor) return leftMajor - rightMajor;
414
+ if (leftMinor !== rightMinor) return leftMinor - rightMinor;
415
+ return leftPatch - rightPatch;
416
+ }
417
+ //#endregion
418
+ //#region src/launch-agent.ts
419
+ const defaultLabel = "com.xwartz.cursor-agent-bridge";
420
+ const defaultLogDir = join(homedir(), ".codex", "logs");
421
+ function getLaunchAgentPaths(label = defaultLabel) {
422
+ return {
423
+ label,
424
+ plistPath: join(homedir(), "Library", "LaunchAgents", `${label}.plist`),
425
+ stdoutPath: join(defaultLogDir, "cursor-agent-bridge.log"),
426
+ stderrPath: join(defaultLogDir, "cursor-agent-bridge.err.log")
427
+ };
428
+ }
429
+ function createLaunchAgentPlist(options) {
430
+ const label = options.label ?? defaultLabel;
431
+ const host = options.host ?? "127.0.0.1";
432
+ const port = options.port ?? 4646;
433
+ const paths = getLaunchAgentPaths(label);
434
+ const args = [
435
+ resolve(options.cliPath),
436
+ "serve",
437
+ "--host",
438
+ host,
439
+ "--port",
440
+ String(port)
441
+ ];
442
+ const env = { PATH: [
443
+ dirname(resolve(options.cliPath)),
444
+ join(homedir(), ".local", "bin"),
445
+ "/usr/local/bin",
446
+ "/opt/homebrew/bin",
447
+ "/usr/bin",
448
+ "/bin",
449
+ "/usr/sbin",
450
+ "/sbin"
451
+ ].join(":") };
452
+ if (options.agentPath) env.CURSOR_AGENT_PATH = options.agentPath;
453
+ return `<?xml version="1.0" encoding="UTF-8"?>
454
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
455
+ <plist version="1.0">
456
+ <dict>
457
+ <key>Label</key>
458
+ <string>${escapePlist(label)}</string>
459
+
460
+ <key>ProgramArguments</key>
461
+ <array>
462
+ ${args.map((arg) => ` <string>${escapePlist(arg)}</string>`).join("\n")}
463
+ </array>
464
+
465
+ <key>EnvironmentVariables</key>
466
+ <dict>
467
+ ${Object.entries(env).map(([key, value]) => ` <key>${escapePlist(key)}</key>\n <string>${escapePlist(value)}</string>`).join("\n")}
468
+ </dict>
469
+
470
+ <key>RunAtLoad</key>
471
+ <true/>
472
+
473
+ <key>KeepAlive</key>
474
+ <true/>
475
+
476
+ <key>StandardOutPath</key>
477
+ <string>${escapePlist(paths.stdoutPath)}</string>
478
+
479
+ <key>StandardErrorPath</key>
480
+ <string>${escapePlist(paths.stderrPath)}</string>
481
+ </dict>
482
+ </plist>
483
+ `;
484
+ }
485
+ function installLaunchAgent(options) {
486
+ ensureMacOS();
487
+ const paths = getLaunchAgentPaths(options.label ?? defaultLabel);
488
+ mkdirSync(dirname(paths.plistPath), { recursive: true });
489
+ mkdirSync(defaultLogDir, { recursive: true });
490
+ if (existsSync(paths.plistPath)) bootout(paths.plistPath);
491
+ writeFileSync(paths.plistPath, createLaunchAgentPlist(options));
492
+ chmodSync(paths.plistPath, 420);
493
+ execFileSync("launchctl", [
494
+ "bootstrap",
495
+ launchctlDomain(),
496
+ paths.plistPath
497
+ ], { stdio: "pipe" });
498
+ return paths;
499
+ }
500
+ function uninstallLaunchAgent(label = defaultLabel) {
501
+ ensureMacOS();
502
+ const paths = getLaunchAgentPaths(label);
503
+ bootout(paths.plistPath);
504
+ rmSync(paths.plistPath, { force: true });
505
+ return paths;
506
+ }
507
+ function printLaunchAgentStatus(label = defaultLabel) {
508
+ ensureMacOS();
509
+ return execFileSync("launchctl", ["print", `${launchctlDomain()}/${label}`], { encoding: "utf8" });
510
+ }
511
+ function bootout(plistPath) {
512
+ try {
513
+ execFileSync("launchctl", [
514
+ "bootout",
515
+ launchctlDomain(),
516
+ plistPath
517
+ ], { stdio: "pipe" });
518
+ } catch {}
519
+ }
520
+ function launchctlDomain() {
521
+ return `gui/${process.getuid?.() ?? execFileSync("id", ["-u"], { encoding: "utf8" }).trim()}`;
522
+ }
523
+ function ensureMacOS() {
524
+ if (process.platform !== "darwin") throw new Error("LaunchAgent management is only available on macOS.");
525
+ }
526
+ function escapePlist(value) {
527
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&apos;");
528
+ }
529
+ //#endregion
530
+ //#region src/upgrade.ts
531
+ const execFileAsync = promisify(execFile);
532
+ const PACKAGE_NAME = "cursor-agent-bridge";
533
+ const DEFAULT_REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
534
+ const REGISTRY_TIMEOUT_MS = 1e4;
535
+ function compareSemver(left, right) {
536
+ const toParts = (value) => {
537
+ const [major = 0, minor = 0, patch = 0] = value.split(".").map((part) => Number.parseInt(part, 10) || 0);
538
+ return [
539
+ major,
540
+ minor,
541
+ patch
542
+ ];
543
+ };
544
+ const [leftMajor, leftMinor, leftPatch] = toParts(left);
545
+ const [rightMajor, rightMinor, rightPatch] = toParts(right);
546
+ if (leftMajor !== rightMajor) return leftMajor < rightMajor ? -1 : 1;
547
+ if (leftMinor !== rightMinor) return leftMinor < rightMinor ? -1 : 1;
548
+ if (leftPatch !== rightPatch) return leftPatch < rightPatch ? -1 : 1;
549
+ return 0;
550
+ }
551
+ async function fetchLatestVersion(options) {
552
+ const registryUrl = options?.registryUrl ?? DEFAULT_REGISTRY_URL;
553
+ const fetchFn = options?.fetchFn ?? fetch;
554
+ const timeoutMs = options?.timeoutMs ?? REGISTRY_TIMEOUT_MS;
555
+ const controller = new AbortController();
556
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
557
+ try {
558
+ const response = await fetchFn(registryUrl, { signal: controller.signal });
559
+ if (!response.ok) throw new Error(`Registry returned ${response.status}`);
560
+ const payload = await response.json();
561
+ if (!payload.version) throw new Error("Registry response missing version");
562
+ return payload.version;
563
+ } finally {
564
+ clearTimeout(timeout);
565
+ }
566
+ }
567
+ async function commandExists(command, execFileFn) {
568
+ try {
569
+ await execFileFn("which", [command]);
570
+ return true;
571
+ } catch {
572
+ return false;
573
+ }
574
+ }
575
+ async function detectPackageManager(preference, execFileFn = execFileAsync) {
576
+ if (preference === "npm") return "npm";
577
+ if (preference === "pnpm") return "pnpm";
578
+ if (!await commandExists("pnpm", execFileFn)) return "npm";
579
+ try {
580
+ await execFileFn("pnpm", [
581
+ "list",
582
+ "-g",
583
+ PACKAGE_NAME,
584
+ "--json"
585
+ ], { env: process.env });
586
+ return "pnpm";
587
+ } catch {
588
+ return "npm";
589
+ }
590
+ }
591
+ function buildInstallCommand(manager, target) {
592
+ const packageSpec = `${PACKAGE_NAME}${target === "latest" ? "@latest" : `@${target.replace(/^@/, "")}`}`;
593
+ if (manager === "pnpm") return {
594
+ command: "pnpm",
595
+ args: [
596
+ "add",
597
+ "-g",
598
+ packageSpec
599
+ ]
600
+ };
601
+ return {
602
+ command: "npm",
603
+ args: [
604
+ "install",
605
+ "-g",
606
+ packageSpec
607
+ ]
608
+ };
609
+ }
610
+ function printManualUpgradeHint(errorLog) {
611
+ errorLog("Upgrade manually:");
612
+ errorLog(` pnpm add -g ${PACKAGE_NAME}@latest`);
613
+ errorLog(` npm install -g ${PACKAGE_NAME}@latest`);
614
+ }
615
+ async function resolveTargetVersion(target, registryUrl, fetchFn) {
616
+ if (target === "latest") return fetchLatestVersion({
617
+ registryUrl,
618
+ fetchFn
619
+ });
620
+ return target.replace(/^@/, "");
621
+ }
622
+ async function runInstall(manager, target, spawnFn) {
623
+ const { command, args } = buildInstallCommand(manager, target);
624
+ return new Promise((resolve, reject) => {
625
+ const child = spawnFn(command, args, {
626
+ stdio: "inherit",
627
+ env: process.env
628
+ });
629
+ child.on("error", reject);
630
+ child.on("close", (code) => resolve(code ?? 1));
631
+ });
7
632
  }
633
+ async function runUpgrade(options) {
634
+ const log = options.log ?? console.log;
635
+ const errorLog = options.errorLog ?? console.error;
636
+ const checkOnly = options.checkOnly ?? false;
637
+ const target = options.target ?? "latest";
638
+ const managerPreference = options.manager ?? "auto";
639
+ const registryUrl = options.registryUrl ?? DEFAULT_REGISTRY_URL;
640
+ const fetchFn = options.fetchFn ?? fetch;
641
+ const spawnFn = options.spawnFn ?? ((command, args, spawnOptions) => spawn(command, args, spawnOptions));
642
+ const execFileFn = options.execFileFn ?? execFileAsync;
643
+ const existsFn = options.existsFn ?? existsSync;
644
+ let targetVersion;
645
+ try {
646
+ targetVersion = await resolveTargetVersion(target, registryUrl, fetchFn);
647
+ } catch (error) {
648
+ errorLog(`Failed to check for updates: ${error instanceof Error ? error.message : String(error)}`);
649
+ printManualUpgradeHint(errorLog);
650
+ return 1;
651
+ }
652
+ if (compareSemver(options.currentVersion, targetVersion) >= 0) {
653
+ log(`${PACKAGE_NAME} is up to date (${options.currentVersion})`);
654
+ return 0;
655
+ }
656
+ if (checkOnly) {
657
+ log(`Update available: ${options.currentVersion} -> ${targetVersion}`);
658
+ return 1;
659
+ }
660
+ let manager;
661
+ manager = await detectPackageManager(managerPreference, execFileFn);
662
+ log(`Installing ${PACKAGE_NAME}@${targetVersion} via ${manager}...`);
663
+ try {
664
+ const exitCode = await runInstall(manager, target, spawnFn);
665
+ if (exitCode !== 0) {
666
+ errorLog(`${manager} install failed with exit code ${exitCode}`);
667
+ printManualUpgradeHint(errorLog);
668
+ return exitCode;
669
+ }
670
+ } catch (error) {
671
+ errorLog(`Install failed: ${error instanceof Error ? error.message : String(error)}`);
672
+ printManualUpgradeHint(errorLog);
673
+ return 1;
674
+ }
675
+ log(`Installed ${PACKAGE_NAME}@${targetVersion}`);
676
+ log("Run `cursor-agent-bridge --version` to verify the upgrade.");
677
+ const launchAgentPlist = getLaunchAgentPaths().plistPath;
678
+ if (existsFn(launchAgentPlist)) log("LaunchAgent detected. Run `cursor-agent-bridge launch-agent install` to refresh the service.");
679
+ return 0;
680
+ }
681
+ //#endregion
682
+ //#region src/cli.ts
8
683
  const command = process.argv[2] && !process.argv[2]?.startsWith("-") ? process.argv[2] : "serve";
9
684
  if (command === "help" || process.argv.includes("--help") || process.argv.includes("-h")) {
10
685
  console.log(`cursor-agent-bridge
11
686
 
12
687
  Usage:
13
688
  cursor-agent-bridge serve [--host 127.0.0.1] [--port 4646]
689
+ cursor-agent-bridge doctor [--host 127.0.0.1] [--port 4646] [--profile cursor] [--file ~/.codex/cursor.config.toml] [--skip-codex-config]
690
+ cursor-agent-bridge config print [--host 127.0.0.1] [--port 4646] [--profile cursor]
691
+ cursor-agent-bridge config check [--file ~/.codex/cursor.config.toml] [--host 127.0.0.1] [--port 4646] [--profile cursor]
692
+ cursor-agent-bridge config write [--file ~/.codex/cursor.config.toml] [--host 127.0.0.1] [--port 4646] [--profile cursor] [--force]
693
+ cursor-agent-bridge models [--json] [--refresh]
694
+ cursor-agent-bridge launch-agent install [--host 127.0.0.1] [--port 4646] [--agent-path agent]
695
+ cursor-agent-bridge launch-agent uninstall
696
+ cursor-agent-bridge launch-agent status
697
+ cursor-agent-bridge upgrade [--check] [--target latest] [--manager auto|npm|pnpm]
14
698
 
15
699
  Environment:
16
700
  HOST Listen host, default 127.0.0.1
@@ -19,12 +703,151 @@ Environment:
19
703
  `);
20
704
  process.exit(0);
21
705
  }
706
+ if (command === "version" || process.argv.includes("--version")) {
707
+ console.log(version);
708
+ process.exit(0);
709
+ }
710
+ if (command === "upgrade") {
711
+ const checkOnly = process.argv.includes("--check");
712
+ const target = readArg("--target", "latest") ?? "latest";
713
+ const manager = readArg("--manager", "auto") ?? "auto";
714
+ if (manager !== "auto" && manager !== "npm" && manager !== "pnpm") {
715
+ console.error("Invalid --manager value. Use auto, npm, or pnpm.");
716
+ process.exit(1);
717
+ }
718
+ const exitCode = await runUpgrade({
719
+ currentVersion: version,
720
+ checkOnly,
721
+ target,
722
+ manager
723
+ });
724
+ process.exit(exitCode);
725
+ }
726
+ if (command === "doctor") try {
727
+ const { host, port } = readHostAndPort();
728
+ const profile = readArg("--profile", "cursor") ?? "cursor";
729
+ const codexConfigPath = readArg("--file", void 0);
730
+ const result = await runDoctor({
731
+ host,
732
+ port,
733
+ profile,
734
+ skipCodexConfig: process.argv.includes("--skip-codex-config"),
735
+ ...process.env.CURSOR_AGENT_PATH ? { agentPath: process.env.CURSOR_AGENT_PATH } : {},
736
+ ...codexConfigPath ? { codexConfigPath } : {}
737
+ });
738
+ process.stdout.write(formatDoctorReport(result));
739
+ process.exit(result.ok ? 0 : 1);
740
+ } catch (error) {
741
+ console.error(error instanceof Error ? error.message : String(error));
742
+ process.exit(1);
743
+ }
744
+ if (command === "config") {
745
+ const action = process.argv[3] ?? "print";
746
+ try {
747
+ const { host, port } = readHostAndPort();
748
+ const profile = readArg("--profile", "cursor") ?? "cursor";
749
+ const filePath = readArg("--file", void 0) ?? resolveCodexConfigPath(profile);
750
+ if (action === "print") {
751
+ process.stdout.write(buildCodexConfigToml({
752
+ host,
753
+ port,
754
+ profile
755
+ }));
756
+ console.error(`Start Codex with: codex --profile ${profile}`);
757
+ process.exit(0);
758
+ }
759
+ if (action === "check") {
760
+ const result = checkCodexConfig(await readFile(filePath, "utf8"), {
761
+ host,
762
+ port,
763
+ profile
764
+ });
765
+ if (result.ok) {
766
+ console.log(`Codex config looks correct: ${filePath}`);
767
+ process.exit(0);
768
+ }
769
+ for (const issue of result.issues) console.error(issue);
770
+ process.exit(1);
771
+ }
772
+ if (action === "write") {
773
+ const result = await writeCodexConfig({
774
+ filePath,
775
+ host,
776
+ port,
777
+ profile,
778
+ force: process.argv.includes("--force")
779
+ });
780
+ if (result.created) console.log(`Created Codex config at ${result.path}`);
781
+ else if (result.updated) console.log(`Updated Codex config at ${result.path}`);
782
+ else console.log(`Codex config already up to date at ${result.path}`);
783
+ console.log(`Start Codex with: codex --profile ${profile}`);
784
+ process.exit(0);
785
+ }
786
+ } catch (error) {
787
+ console.error(error instanceof Error ? error.message : String(error));
788
+ process.exit(1);
789
+ }
790
+ console.error(`Unknown config action: ${action}`);
791
+ process.exit(1);
792
+ }
793
+ if (command === "models") try {
794
+ const models = await new CursorRunner({ ...process.env.CURSOR_AGENT_PATH ? { agentPath: process.env.CURSOR_AGENT_PATH } : {} }).listModels({ refresh: process.argv.includes("--refresh") });
795
+ if (process.argv.includes("--json")) {
796
+ console.log(JSON.stringify(toOpenAIModelList(models), null, 2));
797
+ process.exit(0);
798
+ }
799
+ for (const model of models) console.log(model.id);
800
+ process.exit(0);
801
+ } catch (error) {
802
+ console.error(error instanceof Error ? error.message : String(error));
803
+ process.exit(1);
804
+ }
805
+ if (command === "launch-agent") {
806
+ const action = process.argv[3] ?? "status";
807
+ try {
808
+ if (action === "install") {
809
+ const { host, port } = readHostAndPort();
810
+ const agentPath = readArg("--agent-path", process.env.CURSOR_AGENT_PATH);
811
+ const paths = installLaunchAgent({
812
+ cliPath: process.argv[1] ?? "cursor-agent-bridge",
813
+ host,
814
+ port,
815
+ ...agentPath ? { agentPath } : {}
816
+ });
817
+ console.log(`Installed ${paths.label}`);
818
+ console.log(paths.plistPath);
819
+ process.exit(0);
820
+ }
821
+ if (action === "uninstall") {
822
+ const paths = uninstallLaunchAgent();
823
+ console.log(`Uninstalled ${paths.label}`);
824
+ process.exit(0);
825
+ }
826
+ if (action === "status") {
827
+ process.stdout.write(printLaunchAgentStatus());
828
+ process.exit(0);
829
+ }
830
+ } catch (error) {
831
+ console.error(error instanceof Error ? error.message : String(error));
832
+ process.exit(1);
833
+ }
834
+ console.error(`Unknown launch-agent action: ${action}`);
835
+ process.exit(1);
836
+ }
22
837
  if (command !== "serve") {
23
838
  console.error(`Unknown command: ${command}`);
24
839
  process.exit(1);
25
840
  }
26
- const host = readArg("--host", process.env.HOST) ?? "127.0.0.1";
27
- const port = Number(readArg("--port", process.env.PORT) ?? 4646);
841
+ let host;
842
+ let port;
843
+ try {
844
+ const options = readHostAndPort();
845
+ host = options.host;
846
+ port = options.port;
847
+ } catch (error) {
848
+ console.error(error instanceof Error ? error.message : String(error));
849
+ process.exit(1);
850
+ }
28
851
  const server = await startServer({
29
852
  host,
30
853
  port,