@evanovation/open-cursor 2.4.15

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.
Files changed (80) hide show
  1. package/LICENSE +28 -0
  2. package/README.md +270 -0
  3. package/dist/cli/discover.js +527 -0
  4. package/dist/cli/mcptool.js +10339 -0
  5. package/dist/cli/opencode-cursor.js +2989 -0
  6. package/dist/index.js +20588 -0
  7. package/dist/plugin-entry.js +19848 -0
  8. package/package.json +82 -0
  9. package/scripts/cursor-agent-runner.mjs +272 -0
  10. package/scripts/sdk-runner.mjs +412 -0
  11. package/src/acp/metrics.ts +83 -0
  12. package/src/acp/sessions.ts +107 -0
  13. package/src/acp/tools.ts +209 -0
  14. package/src/auth.ts +175 -0
  15. package/src/cli/discover.ts +53 -0
  16. package/src/cli/mcptool.ts +133 -0
  17. package/src/cli/model-discovery.ts +71 -0
  18. package/src/cli/opencode-cursor.ts +1195 -0
  19. package/src/client/cursor-agent-child.ts +459 -0
  20. package/src/client/sdk-child.ts +550 -0
  21. package/src/client/simple.ts +293 -0
  22. package/src/commands/status.ts +39 -0
  23. package/src/index.ts +39 -0
  24. package/src/mcp/client-manager.ts +166 -0
  25. package/src/mcp/config.ts +169 -0
  26. package/src/mcp/tool-bridge.ts +133 -0
  27. package/src/models/config.ts +64 -0
  28. package/src/models/discovery.ts +105 -0
  29. package/src/models/index.ts +3 -0
  30. package/src/models/pricing.ts +196 -0
  31. package/src/models/sync.ts +247 -0
  32. package/src/models/types.ts +11 -0
  33. package/src/models/variants.ts +446 -0
  34. package/src/plugin-entry.ts +28 -0
  35. package/src/plugin-toggle.ts +81 -0
  36. package/src/plugin.ts +2802 -0
  37. package/src/provider/backend.ts +71 -0
  38. package/src/provider/boundary.ts +168 -0
  39. package/src/provider/passthrough-tracker.ts +38 -0
  40. package/src/provider/runtime-interception.ts +818 -0
  41. package/src/provider/tool-loop-guard.ts +644 -0
  42. package/src/provider/tool-schema-compat.ts +800 -0
  43. package/src/provider.ts +268 -0
  44. package/src/proxy/formatter.ts +60 -0
  45. package/src/proxy/handler.ts +29 -0
  46. package/src/proxy/incremental-prompt.ts +74 -0
  47. package/src/proxy/prompt-builder.ts +204 -0
  48. package/src/proxy/server.ts +207 -0
  49. package/src/proxy/session-resume.ts +312 -0
  50. package/src/proxy/tool-loop.ts +359 -0
  51. package/src/proxy/types.ts +13 -0
  52. package/src/services/toast-service.ts +81 -0
  53. package/src/streaming/ai-sdk-parts.ts +109 -0
  54. package/src/streaming/delta-tracker.ts +89 -0
  55. package/src/streaming/line-buffer.ts +44 -0
  56. package/src/streaming/openai-sse.ts +118 -0
  57. package/src/streaming/parser.ts +22 -0
  58. package/src/streaming/types.ts +158 -0
  59. package/src/tools/core/executor.ts +25 -0
  60. package/src/tools/core/registry.ts +27 -0
  61. package/src/tools/core/types.ts +31 -0
  62. package/src/tools/defaults.ts +954 -0
  63. package/src/tools/discovery.ts +140 -0
  64. package/src/tools/executors/cli.ts +59 -0
  65. package/src/tools/executors/local.ts +25 -0
  66. package/src/tools/executors/mcp.ts +39 -0
  67. package/src/tools/executors/sdk.ts +39 -0
  68. package/src/tools/index.ts +8 -0
  69. package/src/tools/registry.ts +34 -0
  70. package/src/tools/router.ts +123 -0
  71. package/src/tools/schema.ts +58 -0
  72. package/src/tools/skills/loader.ts +61 -0
  73. package/src/tools/skills/resolver.ts +21 -0
  74. package/src/tools/types.ts +29 -0
  75. package/src/types.ts +8 -0
  76. package/src/usage.ts +112 -0
  77. package/src/utils/binary.ts +71 -0
  78. package/src/utils/errors.ts +224 -0
  79. package/src/utils/logger.ts +191 -0
  80. package/src/utils/perf.ts +76 -0
@@ -0,0 +1,2989 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ var __create = Object.create;
4
+ var __getProtoOf = Object.getPrototypeOf;
5
+ var __defProp = Object.defineProperty;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ function __accessProp(key) {
9
+ return this[key];
10
+ }
11
+ var __toESMCache_node;
12
+ var __toESMCache_esm;
13
+ var __toESM = (mod, isNodeMode, target) => {
14
+ var canCache = mod != null && typeof mod === "object";
15
+ if (canCache) {
16
+ var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
17
+ var cached = cache.get(mod);
18
+ if (cached)
19
+ return cached;
20
+ }
21
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
22
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
23
+ for (let key of __getOwnPropNames(mod))
24
+ if (!__hasOwnProp.call(to, key))
25
+ __defProp(to, key, {
26
+ get: __accessProp.bind(mod, key),
27
+ enumerable: true
28
+ });
29
+ if (canCache)
30
+ cache.set(mod, to);
31
+ return to;
32
+ };
33
+ var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
34
+ var __returnValue = (v) => v;
35
+ function __exportSetter(name, newValue) {
36
+ this[name] = __returnValue.bind(null, newValue);
37
+ }
38
+ var __export = (target, all) => {
39
+ for (var name in all)
40
+ __defProp(target, name, {
41
+ get: all[name],
42
+ enumerable: true,
43
+ configurable: true,
44
+ set: __exportSetter.bind(all, name)
45
+ });
46
+ };
47
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
48
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
49
+
50
+ // src/utils/errors.ts
51
+ function stripAnsi(str) {
52
+ if (typeof str !== "string")
53
+ return String(str ?? "");
54
+ return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
55
+ }
56
+ function parseAgentError(stderr) {
57
+ const input = typeof stderr === "string" ? stderr : String(stderr ?? "");
58
+ const clean = stripAnsi(input).trim();
59
+ if (clean.includes("usage limit") || clean.includes("hit your usage limit")) {
60
+ const savingsMatch = clean.match(/saved \$(\d+(?:\.\d+)?)/i);
61
+ const resetMatch = clean.match(/reset[^0-9]*(\d{1,2}\/\d{1,2}\/\d{4})/i);
62
+ const modelMatch = clean.match(/continue with (\w+)/i);
63
+ const details = {};
64
+ if (savingsMatch)
65
+ details.savings = `$${savingsMatch[1]}`;
66
+ if (resetMatch)
67
+ details.resetDate = resetMatch[1];
68
+ if (modelMatch)
69
+ details.affectedModel = modelMatch[1];
70
+ return {
71
+ type: "quota",
72
+ recoverable: false,
73
+ message: clean,
74
+ userMessage: "You've hit your Cursor usage limit",
75
+ details,
76
+ suggestion: "Switch to a different model or set a Spend Limit in Cursor settings"
77
+ };
78
+ }
79
+ if (clean.includes("not logged in") || clean.includes("auth") || clean.includes("unauthorized")) {
80
+ return {
81
+ type: "auth",
82
+ recoverable: false,
83
+ message: clean,
84
+ userMessage: "Not authenticated with Cursor",
85
+ details: {},
86
+ suggestion: "Run: opencode auth login → Other → cursor-acp, or: cursor-agent login"
87
+ };
88
+ }
89
+ if (clean.includes("ECONNREFUSED") || clean.includes("network") || clean.includes("fetch failed")) {
90
+ return {
91
+ type: "network",
92
+ recoverable: true,
93
+ message: clean,
94
+ userMessage: "Connection to Cursor failed",
95
+ details: {},
96
+ suggestion: "Check your internet connection and try again"
97
+ };
98
+ }
99
+ if (clean.includes("model not found") || clean.includes("invalid model") || clean.includes("Cannot use this model")) {
100
+ const modelMatch = clean.match(/Cannot use this model: ([^.]+)/);
101
+ const availableMatch = clean.match(/Available models: (.+)/);
102
+ const details = {};
103
+ if (modelMatch)
104
+ details.requested = modelMatch[1];
105
+ if (availableMatch)
106
+ details.available = availableMatch[1].split(", ").slice(0, 5).join(", ") + "...";
107
+ return {
108
+ type: "model",
109
+ recoverable: false,
110
+ message: clean,
111
+ userMessage: modelMatch ? `Model '${modelMatch[1]}' not available` : "Requested model not available",
112
+ details,
113
+ suggestion: "Use cursor-acp/auto or check available models with: cursor-agent models"
114
+ };
115
+ }
116
+ const recoverable = clean.includes("timeout") || clean.includes("ETIMEDOUT");
117
+ return {
118
+ type: "unknown",
119
+ recoverable,
120
+ message: clean,
121
+ userMessage: clean.substring(0, 200) || "An error occurred",
122
+ details: {}
123
+ };
124
+ }
125
+ function isTransientContinuation(tail) {
126
+ const trimmed = tail.trim();
127
+ if (!trimmed)
128
+ return false;
129
+ const stripped = trimmed.replace(/^[\s:;,.]+/, "");
130
+ const causalMatch = stripped.match(/^(?:because of|due to)\s+(.+)/i);
131
+ if (causalMatch) {
132
+ const cause = causalMatch[1];
133
+ if (SESSION_SPECIFIC_CAUSE_WORDS.test(cause)) {
134
+ return false;
135
+ }
136
+ if (TRANSIENT_CAUSE_WORDS.test(cause)) {
137
+ return true;
138
+ }
139
+ }
140
+ const firstSegment = stripped.split(/[,;]/)[0]?.trim() ?? "";
141
+ if (firstSegment) {
142
+ if (SESSION_SPECIFIC_CAUSE_WORDS.test(firstSegment)) {
143
+ return false;
144
+ }
145
+ if (TRANSIENT_CAUSE_WORDS.test(firstSegment)) {
146
+ return true;
147
+ }
148
+ }
149
+ if (SESSION_SPECIFIC_CAUSE_WORDS.test(stripped)) {
150
+ return false;
151
+ }
152
+ if (TRANSIENT_CAUSE_WORDS.test(stripped)) {
153
+ return true;
154
+ }
155
+ return TRANSIENT_CONTINUATION_PATTERN.test(tail);
156
+ }
157
+ function isResumeSpecificFailure(stderr) {
158
+ const text = typeof stderr === "string" ? stderr : String(stderr ?? "");
159
+ const clean = stripAnsi(text);
160
+ for (const pattern of RESUME_FAILURE_PATTERNS) {
161
+ const match = clean.match(pattern);
162
+ if (!match)
163
+ continue;
164
+ const tail = clean.slice(match.index + match[0].length);
165
+ if (!isTransientContinuation(tail)) {
166
+ return true;
167
+ }
168
+ }
169
+ return false;
170
+ }
171
+ function formatErrorForUser(error) {
172
+ let output = `cursor-acp error: ${error.userMessage || error.message || "Unknown error"}`;
173
+ const details = error.details || {};
174
+ if (Object.keys(details).length > 0) {
175
+ const detailParts = Object.entries(details).map(([k, v]) => `${k}: ${v}`).join(" | ");
176
+ output += `
177
+ ${detailParts}`;
178
+ }
179
+ if (error.suggestion) {
180
+ output += `
181
+ Suggestion: ${error.suggestion}`;
182
+ }
183
+ return output;
184
+ }
185
+ var RESUME_FAILURE_PATTERNS, TRANSIENT_CONTINUATION_PATTERN, TRANSIENT_CAUSE_WORDS, SESSION_SPECIFIC_CAUSE_WORDS;
186
+ var init_errors = __esm(() => {
187
+ RESUME_FAILURE_PATTERNS = [
188
+ /\bsession\s+(?:has\s+(?:been\s+)?|is\s+|was\s+)?(?:not\s+found|expired|deleted|missing|no\s+longer\s+exists)/i,
189
+ /\bchat\s+(?:has\s+(?:been\s+)?|is\s+|was\s+)?(?:not\s+found|expired|deleted|missing|no\s+longer\s+exists)/i,
190
+ /\bconversation\s+(?:has\s+(?:been\s+)?|is\s+|was\s+)?(?:not\s+found|expired|deleted|missing|no\s+longer\s+exists)/i,
191
+ /\bthread\s+(?:has\s+(?:been\s+)?|is\s+|was\s+)?(?:not\s+found|expired|deleted|missing|no\s+longer\s+exists)/i,
192
+ /\bresume\s+(?:failed|error|invalid|aborted)(?:\s+(?:session|chat|conversation|thread))?/i,
193
+ /\bfailed\s+to\s+resume(?:\s+(?:session|chat|conversation|thread))?/i,
194
+ /\bcould\s+not\s+resume(?:\s+(?:session|chat|conversation|thread))?/i,
195
+ /\bno\s+active\s+session/i,
196
+ /\bno\s+such\s+session/i,
197
+ /\bno\s+such\s+chat/i,
198
+ /\binvalid\s+(?:session|chat|conversation|thread)(?:\s+id)?/i,
199
+ /\b(?:session|chat|conversation|thread)\s+invalid(?:\s+id)?/i,
200
+ /\b(?:session|chat|conversation|thread)\s+id\s+(?:is\s+)?(?:invalid|not\s+found|expired|missing)/i,
201
+ /\b(?:session|chat|conversation|thread)\s+(?:isn['’]t|wasn['’]t)\s+found/i,
202
+ /\b(?:session|chat|conversation|thread)\s+(?:can(?:not|\s+not)|could\s+not)\s+(?:be\s+)?resumed/i,
203
+ /\bunable\s+to\s+resume\b/i,
204
+ /\bcan(?:not|\s+not)\s+resume\b/i
205
+ ];
206
+ TRANSIENT_CONTINUATION_PATTERN = /^\s*[:;]?\s*(?:token|credential|credentials|auth|secret|password|format|network|quota|usage|limit|api|key|request(?:_|-|\s+)?id?|due\s+to\s+(?:network|auth|quota)|because\s+of\s+(?:network|auth|quota)|caused\s+by\s+(?:network|auth|quota))/i;
207
+ TRANSIENT_CAUSE_WORDS = /\b(?:auth(?:enticat(?:e|ion|ed))?|re-auth(?:enticate)?|token(?:\s+rotation)?|credential|password|secret|network|connection|internet|offline|quota|usage(?:\s+limit)?|api[\s-]?key|fetch\s+failed|econnrefused|timeout|timed\s+out)\b/i;
208
+ SESSION_SPECIFIC_CAUSE_WORDS = /\b(?:inactiv(?:ity|e)|idle|policy|retention|archiv(?:e|ed)|purged|deleted|removed|expired)\b/i;
209
+ });
210
+
211
+ // src/utils/logger.ts
212
+ import * as fs from "node:fs";
213
+ import * as path from "node:path";
214
+ import * as os from "node:os";
215
+ function getConfiguredLevel() {
216
+ const env = process.env.CURSOR_ACP_LOG_LEVEL?.toLowerCase();
217
+ if (env && env in LEVEL_PRIORITY) {
218
+ return env;
219
+ }
220
+ return "info";
221
+ }
222
+ function isSilent() {
223
+ return process.env.CURSOR_ACP_LOG_SILENT === "1" || process.env.CURSOR_ACP_LOG_SILENT === "true";
224
+ }
225
+ function shouldLog(level) {
226
+ if (isSilent())
227
+ return false;
228
+ const configured = getConfiguredLevel();
229
+ return LEVEL_PRIORITY[level] >= LEVEL_PRIORITY[configured];
230
+ }
231
+ function formatMessage(level, component, message, data) {
232
+ const prefix = `[cursor-acp:${component}]`;
233
+ const levelTag = level.toUpperCase().padEnd(5);
234
+ let formatted = `${prefix} ${levelTag} ${message}`;
235
+ if (data !== undefined) {
236
+ if (typeof data === "object") {
237
+ formatted += ` ${JSON.stringify(data)}`;
238
+ } else {
239
+ formatted += ` ${data}`;
240
+ }
241
+ }
242
+ return formatted;
243
+ }
244
+ function isConsoleEnabled() {
245
+ const consoleEnv = process.env.CURSOR_ACP_LOG_CONSOLE;
246
+ return consoleEnv === "1" || consoleEnv === "true";
247
+ }
248
+ function getLogDir() {
249
+ const override = process.env.CURSOR_ACP_LOG_DIR?.trim();
250
+ return override || path.join(os.homedir(), ".opencode-cursor");
251
+ }
252
+ function getLogFile() {
253
+ return path.join(getLogDir(), "plugin.log");
254
+ }
255
+ function ensureLogDir() {
256
+ if (logDirEnsured)
257
+ return;
258
+ try {
259
+ const logDir = getLogDir();
260
+ if (!fs.existsSync(logDir)) {
261
+ fs.mkdirSync(logDir, { recursive: true });
262
+ }
263
+ logDirEnsured = true;
264
+ } catch {
265
+ logFileError = true;
266
+ }
267
+ }
268
+ function openLogStream() {
269
+ if (logStream || logFileError)
270
+ return;
271
+ ensureLogDir();
272
+ if (logFileError)
273
+ return;
274
+ try {
275
+ try {
276
+ logBytesWritten = fs.statSync(getLogFile()).size;
277
+ } catch {
278
+ logBytesWritten = 0;
279
+ }
280
+ logStream = fs.createWriteStream(getLogFile(), { flags: "a" });
281
+ logStream.on("error", () => {
282
+ if (!logFileError) {
283
+ logFileError = true;
284
+ console.error(`[cursor-acp] Failed to write logs. Using: ${getLogFile()}`);
285
+ }
286
+ logStream = null;
287
+ });
288
+ } catch {
289
+ logFileError = true;
290
+ }
291
+ }
292
+ function rotateIfNeeded() {
293
+ if (logBytesWritten < MAX_LOG_SIZE)
294
+ return;
295
+ try {
296
+ if (logStream) {
297
+ logStream.end();
298
+ logStream = null;
299
+ }
300
+ const logFile = getLogFile();
301
+ fs.renameSync(logFile, logFile + ".1");
302
+ logBytesWritten = 0;
303
+ openLogStream();
304
+ } catch {
305
+ if (!logFileError && !logStream) {
306
+ openLogStream();
307
+ }
308
+ }
309
+ }
310
+ function writeToFile(message) {
311
+ if (logFileError)
312
+ return;
313
+ if (!logStream)
314
+ openLogStream();
315
+ if (logFileError || !logStream)
316
+ return;
317
+ rotateIfNeeded();
318
+ if (logFileError || !logStream)
319
+ return;
320
+ const timestamp = new Date().toISOString();
321
+ const line = `${timestamp} ${message}
322
+ `;
323
+ logStream.write(line);
324
+ logBytesWritten += Buffer.byteLength(line);
325
+ }
326
+ function createLogger(component) {
327
+ return {
328
+ isDebugEnabled: () => shouldLog("debug"),
329
+ debug: (message, data) => {
330
+ if (!shouldLog("debug"))
331
+ return;
332
+ const formatted = formatMessage("debug", component, message, data);
333
+ writeToFile(formatted);
334
+ if (isConsoleEnabled())
335
+ console.error(formatted);
336
+ },
337
+ info: (message, data) => {
338
+ if (!shouldLog("info"))
339
+ return;
340
+ const formatted = formatMessage("info", component, message, data);
341
+ writeToFile(formatted);
342
+ if (isConsoleEnabled())
343
+ console.error(formatted);
344
+ },
345
+ warn: (message, data) => {
346
+ if (!shouldLog("warn"))
347
+ return;
348
+ const formatted = formatMessage("warn", component, message, data);
349
+ writeToFile(formatted);
350
+ if (isConsoleEnabled())
351
+ console.error(formatted);
352
+ },
353
+ error: (message, data) => {
354
+ if (!shouldLog("error"))
355
+ return;
356
+ const formatted = formatMessage("error", component, message, data);
357
+ writeToFile(formatted);
358
+ if (isConsoleEnabled())
359
+ console.error(formatted);
360
+ }
361
+ };
362
+ }
363
+ var MAX_LOG_SIZE, LEVEL_PRIORITY, logDirEnsured = false, logFileError = false, logStream = null, logBytesWritten = 0;
364
+ var init_logger = __esm(() => {
365
+ MAX_LOG_SIZE = 5 * 1024 * 1024;
366
+ LEVEL_PRIORITY = {
367
+ debug: 0,
368
+ info: 1,
369
+ warn: 2,
370
+ error: 3
371
+ };
372
+ });
373
+
374
+ // src/utils/binary.ts
375
+ import { existsSync as fsExistsSync } from "fs";
376
+ import * as pathModule from "path";
377
+ import { homedir as osHomedir } from "os";
378
+ function resolveCursorAgentBinary(deps = {}) {
379
+ const platform = deps.platform ?? process.platform;
380
+ const env = deps.env ?? process.env;
381
+ const checkExists = deps.existsSync ?? fsExistsSync;
382
+ const home = (deps.homedir ?? osHomedir)();
383
+ const envOverride = env.CURSOR_AGENT_EXECUTABLE;
384
+ if (envOverride && envOverride.length > 0) {
385
+ return envOverride;
386
+ }
387
+ if (platform === "win32") {
388
+ const pathJoin = pathModule.win32.join;
389
+ const localAppData = env.LOCALAPPDATA ?? pathJoin(home, "AppData", "Local");
390
+ const knownPath = pathJoin(localAppData, "cursor-agent", "cursor-agent.cmd");
391
+ if (checkExists(knownPath)) {
392
+ return knownPath;
393
+ }
394
+ log.warn("cursor-agent not found at known Windows path, falling back to PATH", { checkedPath: knownPath });
395
+ return "cursor-agent.cmd";
396
+ }
397
+ const knownPaths = [
398
+ pathModule.join(home, ".cursor-agent", "cursor-agent"),
399
+ "/usr/local/bin/cursor-agent"
400
+ ];
401
+ for (const p of knownPaths) {
402
+ if (checkExists(p)) {
403
+ return p;
404
+ }
405
+ }
406
+ log.warn("cursor-agent not found at known paths, falling back to PATH", { checkedPaths: knownPaths });
407
+ return "cursor-agent";
408
+ }
409
+ function formatShellCommandForPlatform(command, platform = process.platform) {
410
+ if (platform !== "win32") {
411
+ return command;
412
+ }
413
+ if (command.startsWith('"') && command.endsWith('"')) {
414
+ return command;
415
+ }
416
+ return `"${command}"`;
417
+ }
418
+ var log;
419
+ var init_binary = __esm(() => {
420
+ init_logger();
421
+ log = createLogger("binary");
422
+ });
423
+
424
+ // src/cli/model-discovery.ts
425
+ import { execFileSync } from "child_process";
426
+ function parseCursorModelsOutput(output) {
427
+ const clean = stripAnsi(output);
428
+ const models = [];
429
+ const seen = new Set;
430
+ for (const line of clean.split(`
431
+ `)) {
432
+ const trimmed = line.trim();
433
+ if (!trimmed)
434
+ continue;
435
+ const match = trimmed.match(/^([a-zA-Z0-9._-]+)\s+-\s+(.+?)(?:\s+\((?:current|default)\))*\s*$/);
436
+ if (!match)
437
+ continue;
438
+ const id = match[1];
439
+ if (seen.has(id))
440
+ continue;
441
+ seen.add(id);
442
+ models.push({ id, name: match[2].trim() });
443
+ }
444
+ return models;
445
+ }
446
+ function discoverModelsFromCursorAgent() {
447
+ const raw = execFileSync(resolveCursorAgentBinary(), ["models"], {
448
+ encoding: "utf8",
449
+ ...process.platform !== "win32" && { killSignal: "SIGTERM" },
450
+ stdio: ["ignore", "pipe", "pipe"],
451
+ timeout: MODEL_DISCOVERY_TIMEOUT_MS
452
+ });
453
+ const models = parseCursorModelsOutput(raw);
454
+ if (models.length === 0) {
455
+ throw new Error("No models parsed from cursor-agent output");
456
+ }
457
+ return models;
458
+ }
459
+ function fallbackModels() {
460
+ return [
461
+ { id: "auto", name: "Auto" },
462
+ { id: "composer-1.5", name: "Composer 1.5" },
463
+ { id: "composer-1", name: "Composer 1" },
464
+ { id: "opus-4.6-thinking", name: "Claude 4.6 Opus (Thinking)" },
465
+ { id: "opus-4.6", name: "Claude 4.6 Opus" },
466
+ { id: "sonnet-4.6", name: "Claude 4.6 Sonnet" },
467
+ { id: "sonnet-4.6-thinking", name: "Claude 4.6 Sonnet (Thinking)" },
468
+ { id: "opus-4.5", name: "Claude 4.5 Opus" },
469
+ { id: "opus-4.5-thinking", name: "Claude 4.5 Opus (Thinking)" },
470
+ { id: "sonnet-4.5", name: "Claude 4.5 Sonnet" },
471
+ { id: "sonnet-4.5-thinking", name: "Claude 4.5 Sonnet (Thinking)" },
472
+ { id: "gpt-5.4-high", name: "GPT-5.4 High" },
473
+ { id: "gpt-5.4-medium", name: "GPT-5.4" },
474
+ { id: "gpt-5.3-codex", name: "GPT-5.3 Codex" },
475
+ { id: "gpt-5.2", name: "GPT-5.2" },
476
+ { id: "gemini-3.1-pro", name: "Gemini 3.1 Pro" },
477
+ { id: "gemini-3-pro", name: "Gemini 3 Pro" },
478
+ { id: "gemini-3-flash", name: "Gemini 3 Flash" },
479
+ { id: "grok", name: "Grok" },
480
+ { id: "kimi-k2.5", name: "Kimi K2.5" }
481
+ ];
482
+ }
483
+ var MODEL_DISCOVERY_TIMEOUT_MS = 5000;
484
+ var init_model_discovery = __esm(() => {
485
+ init_errors();
486
+ init_binary();
487
+ });
488
+
489
+ // src/auth.ts
490
+ import { existsSync as existsSync2 } from "fs";
491
+ import { homedir as homedir2, platform } from "os";
492
+ import { join as join3 } from "path";
493
+ function getHomeDir() {
494
+ const override = process.env.CURSOR_ACP_HOME_DIR;
495
+ if (override && override.length > 0) {
496
+ return override;
497
+ }
498
+ return homedir2();
499
+ }
500
+ function verifyCursorAuth() {
501
+ const apiKey = process.env.CURSOR_API_KEY;
502
+ if (apiKey && apiKey.trim().length > 0) {
503
+ log2.debug("CURSOR_API_KEY found, auth verified");
504
+ return true;
505
+ }
506
+ const possiblePaths = getPossibleAuthPaths();
507
+ for (const authPath of possiblePaths) {
508
+ if (existsSync2(authPath)) {
509
+ log2.debug("Auth file found", { path: authPath });
510
+ return true;
511
+ }
512
+ }
513
+ log2.debug("No auth found (no CURSOR_API_KEY, no auth file)", { checkedPaths: possiblePaths });
514
+ return false;
515
+ }
516
+ function isUsableSdkApiKey(value) {
517
+ const trimmed = value?.trim();
518
+ if (!trimmed) {
519
+ return false;
520
+ }
521
+ return !PLACEHOLDER_API_KEYS.has(trimmed.toLowerCase());
522
+ }
523
+ function normalizeAuthorizationHeader(value) {
524
+ const trimmed = value?.trim();
525
+ if (!trimmed) {
526
+ return;
527
+ }
528
+ const bearerMatch = /^bearer\s+(.+)$/i.exec(trimmed);
529
+ return bearerMatch?.[1]?.trim() ?? trimmed;
530
+ }
531
+ function resolveSdkApiKey(input) {
532
+ const candidates = [
533
+ input.env?.CURSOR_API_KEY,
534
+ input.storedApiKey,
535
+ normalizeAuthorizationHeader(input.authorizationHeader)
536
+ ];
537
+ return candidates.find(isUsableSdkApiKey)?.trim();
538
+ }
539
+ function getPossibleAuthPaths() {
540
+ const home = getHomeDir();
541
+ const paths = [];
542
+ const isDarwin = platform() === "darwin";
543
+ const authFiles = ["cli-config.json", "auth.json"];
544
+ if (isDarwin) {
545
+ for (const file of authFiles) {
546
+ paths.push(join3(home, ".cursor", file));
547
+ }
548
+ for (const file of authFiles) {
549
+ paths.push(join3(home, ".config", "cursor", file));
550
+ }
551
+ } else {
552
+ for (const file of authFiles) {
553
+ paths.push(join3(home, ".config", "cursor", file));
554
+ }
555
+ const xdgConfig = process.env.XDG_CONFIG_HOME;
556
+ if (xdgConfig && xdgConfig !== join3(home, ".config")) {
557
+ for (const file of authFiles) {
558
+ paths.push(join3(xdgConfig, "cursor", file));
559
+ }
560
+ }
561
+ for (const file of authFiles) {
562
+ paths.push(join3(home, ".cursor", file));
563
+ }
564
+ }
565
+ return paths;
566
+ }
567
+ function getAuthFilePath() {
568
+ const possiblePaths = getPossibleAuthPaths();
569
+ for (const authPath of possiblePaths) {
570
+ if (existsSync2(authPath)) {
571
+ return authPath;
572
+ }
573
+ }
574
+ return possiblePaths[0];
575
+ }
576
+ var log2, AUTH_POLL_TIMEOUT, PLACEHOLDER_API_KEYS;
577
+ var init_auth = __esm(() => {
578
+ init_logger();
579
+ log2 = createLogger("auth");
580
+ AUTH_POLL_TIMEOUT = 5 * 60 * 1000;
581
+ PLACEHOLDER_API_KEYS = new Set(["cursor-agent"]);
582
+ });
583
+
584
+ // src/provider/backend.ts
585
+ function parseCursorBackendPreference(value) {
586
+ if (value === undefined || value.trim() === "") {
587
+ return { preference: "auto", valid: true };
588
+ }
589
+ const normalized = value.trim().toLowerCase();
590
+ if (normalized === "auto" || normalized === "cursor-agent" || normalized === "sdk") {
591
+ return { preference: normalized, valid: true };
592
+ }
593
+ return { preference: "auto", valid: false };
594
+ }
595
+ function selectBackendForRequest(input) {
596
+ if (input.preference === "sdk") {
597
+ return "sdk";
598
+ }
599
+ if (input.preference === "auto" && !input.cursorAgentAvailable && isUsableSdkApiKey(input.sdkApiKey)) {
600
+ return "sdk";
601
+ }
602
+ return "cursor-agent";
603
+ }
604
+ var init_backend = __esm(() => {
605
+ init_auth();
606
+ init_auth();
607
+ });
608
+
609
+ // src/client/sdk-child.ts
610
+ import { spawn } from "node:child_process";
611
+ import { fileURLToPath } from "node:url";
612
+ import { dirname, resolve } from "node:path";
613
+ import { existsSync as existsSync3 } from "node:fs";
614
+ import { EventEmitter } from "node:events";
615
+ import { PassThrough } from "node:stream";
616
+ import { randomBytes } from "node:crypto";
617
+ function extractEventJson(line) {
618
+ const idx = line.indexOf(EVENT_KEY);
619
+ if (idx < 0)
620
+ return line;
621
+ const start = idx + EVENT_KEY.length;
622
+ const end = line.lastIndexOf("}");
623
+ if (end <= start)
624
+ return line;
625
+ return line.substring(start, end);
626
+ }
627
+ function resolveNodeBinary() {
628
+ return process.env.CURSOR_ACP_NODE_BIN || "node";
629
+ }
630
+ function resolveRunnerPath(currentFile = fileURLToPath(import.meta.url), checkExists = existsSync3, env = process.env) {
631
+ const override = env.CURSOR_ACP_SDK_RUNNER_PATH?.trim();
632
+ if (override) {
633
+ if (checkExists(override)) {
634
+ return override;
635
+ }
636
+ throw new Error(`CURSOR_ACP_SDK_RUNNER_PATH does not exist: ${override}`);
637
+ }
638
+ const currentDir = dirname(currentFile);
639
+ const candidates = [
640
+ resolve(currentDir, "../../scripts/sdk-runner.mjs"),
641
+ resolve(currentDir, "../scripts/sdk-runner.mjs")
642
+ ];
643
+ for (const candidate of candidates) {
644
+ if (checkExists(candidate)) {
645
+ return candidate;
646
+ }
647
+ }
648
+ log3.error("Could not resolve sdk-runner.mjs", {
649
+ currentFile,
650
+ candidates
651
+ });
652
+ throw new Error(`sdk-runner.mjs not found. Tried: ${candidates.join(", ")}`);
653
+ }
654
+ function generateRequestId() {
655
+ return randomBytes(8).toString("hex");
656
+ }
657
+
658
+ class SdkRunnerSingleton {
659
+ runnerProcess = null;
660
+ lastApiKey = null;
661
+ pendingRequests = new Map;
662
+ lineBuffer = "";
663
+ starting = null;
664
+ async ensureRunning(apiKey) {
665
+ if (this.lastApiKey && this.lastApiKey !== apiKey) {
666
+ log3.info("API key changed, restarting runner");
667
+ this.kill();
668
+ this.starting = null;
669
+ }
670
+ if (this.runnerProcess) {
671
+ return;
672
+ }
673
+ if (this.starting) {
674
+ return this.starting;
675
+ }
676
+ this.starting = this.doSpawn(apiKey);
677
+ try {
678
+ await this.starting;
679
+ } finally {
680
+ this.starting = null;
681
+ }
682
+ }
683
+ async doSpawn(apiKey) {
684
+ const nodeBin = resolveNodeBinary();
685
+ const runnerPath = resolveRunnerPath();
686
+ log3.info("spawning persistent sdk runner", {
687
+ runnerPath,
688
+ nodeBin
689
+ });
690
+ this.lastApiKey = apiKey;
691
+ this.runnerProcess = spawn(nodeBin, [runnerPath], {
692
+ env: { ...process.env, CURSOR_API_KEY: apiKey },
693
+ stdio: ["pipe", "pipe", "pipe"]
694
+ });
695
+ this.runnerProcess.stdout?.on("data", (chunk) => {
696
+ this.handleStdoutChunk(chunk);
697
+ });
698
+ this.runnerProcess.stderr?.on("data", (chunk) => {
699
+ const text = chunk.toString("utf8").trimEnd();
700
+ for (const line of text.split(`
701
+ `)) {
702
+ if (line) {
703
+ log3.debug(`[runner stderr] ${line}`);
704
+ }
705
+ }
706
+ });
707
+ this.runnerProcess.on("close", (code) => {
708
+ log3.error(`sdk runner exited with code ${code}`);
709
+ this.runnerProcess = null;
710
+ for (const [id, pending] of this.pendingRequests.entries()) {
711
+ pending.promiseRejector(new Error(`Runner exited with code ${code}`));
712
+ pending.controller.error(new Error(`Runner exited with code ${code}`));
713
+ }
714
+ this.pendingRequests.clear();
715
+ });
716
+ this.runnerProcess.on("error", (err) => {
717
+ log3.error("sdk runner spawn error", { error: err.message });
718
+ this.runnerProcess = null;
719
+ for (const [id, pending] of this.pendingRequests.entries()) {
720
+ pending.promiseRejector(err);
721
+ pending.controller.error(err);
722
+ }
723
+ this.pendingRequests.clear();
724
+ });
725
+ }
726
+ sendRequest(requestId, model, cwd, prompt) {
727
+ if (!this.runnerProcess || !this.runnerProcess.stdin) {
728
+ throw new Error("Runner process not ready");
729
+ }
730
+ const request = { id: requestId, model, cwd, prompt };
731
+ this.runnerProcess.stdin.write(JSON.stringify(request) + `
732
+ `);
733
+ }
734
+ sendRawRequest(request) {
735
+ if (!this.runnerProcess || !this.runnerProcess.stdin) {
736
+ throw new Error("Runner process not ready");
737
+ }
738
+ this.runnerProcess.stdin.write(JSON.stringify(request) + `
739
+ `);
740
+ }
741
+ handleStdoutChunk(chunk) {
742
+ this.lineBuffer += chunk.toString("utf8");
743
+ const lines = this.lineBuffer.split(`
744
+ `);
745
+ this.lineBuffer = lines.pop() ?? "";
746
+ for (const line of lines) {
747
+ if (!line.trim())
748
+ continue;
749
+ try {
750
+ const wrapped = JSON.parse(line);
751
+ const requestId = wrapped.id;
752
+ if (!requestId) {
753
+ log3.warn("Wrapped response missing id", { wrapped });
754
+ continue;
755
+ }
756
+ const pending = this.pendingRequests.get(requestId);
757
+ if (!pending) {
758
+ log3.warn(`Received response for unknown request ${requestId}`, { wrapped });
759
+ continue;
760
+ }
761
+ if (wrapped.done) {
762
+ log3.info(`Request ${requestId} complete with exitCode ${wrapped.exitCode}`);
763
+ pending.controller.close();
764
+ pending.promiseResolver(wrapped.exitCode ?? 0);
765
+ this.pendingRequests.delete(requestId);
766
+ } else if (wrapped.event) {
767
+ const eventJson = extractEventJson(line);
768
+ pending.controller.enqueue(textEncoder.encode(eventJson + `
769
+ `));
770
+ }
771
+ } catch (err) {
772
+ log3.error("Failed to parse wrapped response line", {
773
+ line,
774
+ error: err instanceof Error ? err.message : String(err)
775
+ });
776
+ }
777
+ }
778
+ }
779
+ registerPending(controller, promiseResolver, promiseRejector) {
780
+ const id = generateRequestId();
781
+ this.pendingRequests.set(id, { controller, promiseResolver, promiseRejector });
782
+ return id;
783
+ }
784
+ kill() {
785
+ if (this.runnerProcess) {
786
+ try {
787
+ this.runnerProcess.kill("SIGKILL");
788
+ } catch {}
789
+ this.runnerProcess = null;
790
+ }
791
+ }
792
+ }
793
+ function createSdkBunChild(options) {
794
+ log3.info("creating sdk bun child", {
795
+ model: options.model,
796
+ cwd: options.cwd
797
+ });
798
+ let requestId;
799
+ let resolveExited;
800
+ let rejectExited;
801
+ const exited = new Promise((resolve2, reject) => {
802
+ resolveExited = resolve2;
803
+ rejectExited = reject;
804
+ });
805
+ const stdout = new ReadableStream({
806
+ start: async (controller) => {
807
+ try {
808
+ await singleton.ensureRunning(options.apiKey);
809
+ requestId = singleton.registerPending(controller, resolveExited, rejectExited);
810
+ log3.info(`request ${requestId} registered (bun)`);
811
+ singleton.sendRequest(requestId, options.model, options.cwd, options.prompt);
812
+ } catch (err) {
813
+ const error = err instanceof Error ? err : new Error(String(err));
814
+ log3.error("Failed to start request (bun)", { error: error.message });
815
+ controller.error(error);
816
+ rejectExited(error);
817
+ }
818
+ },
819
+ cancel() {
820
+ log3.debug(`request ${requestId} cancelled (bun)`);
821
+ }
822
+ });
823
+ const stderr = new ReadableStream({
824
+ start(controller) {
825
+ controller.close();
826
+ }
827
+ });
828
+ return {
829
+ stdout,
830
+ stderr,
831
+ exited,
832
+ kill() {
833
+ log3.debug(`kill() called on bun child ${requestId}`);
834
+ }
835
+ };
836
+ }
837
+ function createSdkNodeChild(options) {
838
+ const child = new SdkNodeChild;
839
+ child.spawn(options).catch((err) => {
840
+ log3.error("Spawn error", { error: err instanceof Error ? err.message : String(err) });
841
+ });
842
+ return child;
843
+ }
844
+ async function listModelsViaRunner(apiKey) {
845
+ try {
846
+ await singleton.ensureRunning(apiKey);
847
+ return new Promise(async (resolve2, reject) => {
848
+ const timeout = setTimeout(() => reject(new Error("Timeout")), 15000);
849
+ const events = [];
850
+ let gotModels = false;
851
+ const decoder = new TextDecoder;
852
+ const controller = {
853
+ enqueue: (data) => {
854
+ try {
855
+ const event = JSON.parse(decoder.decode(data).trim());
856
+ events.push(event);
857
+ if (event.type === "models")
858
+ gotModels = true;
859
+ } catch (err) {
860
+ log3.warn("listModels: failed to parse event", { error: String(err) });
861
+ }
862
+ },
863
+ close: () => {},
864
+ error: (e) => reject(e)
865
+ };
866
+ const id = singleton.registerPending(controller, (code) => {
867
+ clearTimeout(timeout);
868
+ if (!gotModels)
869
+ return reject(new Error("No models"));
870
+ if (code !== 0)
871
+ return reject(new Error(`Code ${code}`));
872
+ const m = events.find((e) => e.type === "models");
873
+ resolve2(m?.models ?? []);
874
+ }, (e) => {
875
+ clearTimeout(timeout);
876
+ reject(e);
877
+ });
878
+ singleton.sendRawRequest({ id, op: "listModels" });
879
+ });
880
+ } catch (err) {
881
+ throw new Error(`listModelsViaRunner failed: ${String(err)}`);
882
+ }
883
+ }
884
+ var log3, textEncoder, EVENT_KEY = '"event":', singleton, SdkNodeChild;
885
+ var init_sdk_child = __esm(() => {
886
+ init_logger();
887
+ log3 = createLogger("sdk-child");
888
+ textEncoder = new TextEncoder;
889
+ singleton = new SdkRunnerSingleton;
890
+ SdkNodeChild = class SdkNodeChild extends EventEmitter {
891
+ stdout = new PassThrough;
892
+ stderr = new PassThrough;
893
+ requestId = null;
894
+ async spawn(options) {
895
+ try {
896
+ log3.info("spawning (via singleton) sdk node child", {
897
+ model: options.model,
898
+ cwd: options.cwd
899
+ });
900
+ await singleton.ensureRunning(options.apiKey);
901
+ let requestId;
902
+ let resolveExited;
903
+ let rejectExited;
904
+ const exited = new Promise((resolve2, reject) => {
905
+ resolveExited = resolve2;
906
+ rejectExited = reject;
907
+ });
908
+ const dummyController = {
909
+ enqueue: (data) => {
910
+ this.stdout.write(data);
911
+ },
912
+ close: () => {
913
+ this.stdout.end();
914
+ },
915
+ error: (err) => {
916
+ this.stdout.destroy(err);
917
+ }
918
+ };
919
+ requestId = singleton.registerPending(dummyController, (code) => {
920
+ this.stderr.end();
921
+ this.emit("close", code);
922
+ resolveExited(code);
923
+ }, (err) => {
924
+ this.stderr.end();
925
+ this.emit("error", err);
926
+ rejectExited(err);
927
+ });
928
+ this.requestId = requestId;
929
+ log3.info(`request ${requestId} registered (node)`);
930
+ singleton.sendRequest(requestId, options.model, options.cwd, options.prompt);
931
+ } catch (err) {
932
+ const error = err instanceof Error ? err : new Error(String(err));
933
+ log3.error("Failed to spawn sdk node child", { error: error.message });
934
+ this.emit("error", error);
935
+ }
936
+ }
937
+ kill() {
938
+ if (this.requestId) {
939
+ log3.debug(`kill() called on node child ${this.requestId}`);
940
+ }
941
+ }
942
+ };
943
+ });
944
+
945
+ // src/client/cursor-agent-child.ts
946
+ import { spawn as spawn2 } from "node:child_process";
947
+ import { randomBytes as randomBytes2 } from "node:crypto";
948
+ import { existsSync as existsSync4 } from "node:fs";
949
+ import { EventEmitter as EventEmitter2 } from "node:events";
950
+ import { PassThrough as PassThrough2 } from "node:stream";
951
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
952
+ import { dirname as dirname2, resolve as resolve2 } from "node:path";
953
+ function isAgentPoolEnabled() {
954
+ const value = process.env.CURSOR_ACP_AGENT_POOL?.toLowerCase();
955
+ return value === "1" || value === "true" || value === "on" || value === "yes";
956
+ }
957
+ function parseAgentPoolIdleMs() {
958
+ const value = process.env.CURSOR_ACP_AGENT_POOL_IDLE_MS?.trim();
959
+ if (value == null || value === "")
960
+ return DEFAULT_IDLE_MS;
961
+ const parsed = Number(value);
962
+ if (!Number.isFinite(parsed) || parsed < 0)
963
+ return DEFAULT_IDLE_MS;
964
+ return Math.floor(parsed);
965
+ }
966
+ function buildAgentPoolKey(workspace, model) {
967
+ return `${workspace}\x00${model}`;
968
+ }
969
+ function resolveCursorAgentRunnerPath(currentFile = fileURLToPath2(import.meta.url), checkExists = existsSync4, env = process.env) {
970
+ const override = env.CURSOR_ACP_CURSOR_AGENT_RUNNER_PATH?.trim();
971
+ if (override) {
972
+ if (checkExists(override)) {
973
+ return override;
974
+ }
975
+ throw new Error(`CURSOR_ACP_CURSOR_AGENT_RUNNER_PATH does not exist: ${override}`);
976
+ }
977
+ const currentDir = dirname2(currentFile);
978
+ const candidates = [
979
+ resolve2(currentDir, "../../scripts/cursor-agent-runner.mjs"),
980
+ resolve2(currentDir, "../scripts/cursor-agent-runner.mjs")
981
+ ];
982
+ for (const candidate of candidates) {
983
+ if (checkExists(candidate)) {
984
+ return candidate;
985
+ }
986
+ }
987
+ throw new Error(`cursor-agent-runner.mjs not found. Tried: ${candidates.join(", ")}`);
988
+ }
989
+ function resolveNodeBinary2() {
990
+ return process.env.CURSOR_ACP_NODE_BIN || "node";
991
+ }
992
+ function generateRequestId2() {
993
+ return randomBytes2(8).toString("hex");
994
+ }
995
+
996
+ class CursorAgentPoolRunner {
997
+ runnerProcess = null;
998
+ pendingRequests = new Map;
999
+ lineBuffer = "";
1000
+ starting = null;
1001
+ poolKey;
1002
+ onIdle;
1003
+ constructor(poolKey, onIdle) {
1004
+ this.poolKey = poolKey;
1005
+ this.onIdle = onIdle;
1006
+ }
1007
+ async ensureRunning() {
1008
+ if (this.runnerProcess)
1009
+ return;
1010
+ if (this.starting)
1011
+ return this.starting;
1012
+ this.starting = this.doSpawn();
1013
+ try {
1014
+ await this.starting;
1015
+ } finally {
1016
+ this.starting = null;
1017
+ }
1018
+ }
1019
+ async doSpawn() {
1020
+ const nodeBin = resolveNodeBinary2();
1021
+ const runnerPath = resolveCursorAgentRunnerPath();
1022
+ log4.info("spawning persistent cursor-agent runner", {
1023
+ poolKeyHash: this.poolKey.slice(0, 8) + "…",
1024
+ runnerPath,
1025
+ nodeBin
1026
+ });
1027
+ this.runnerProcess = spawn2(nodeBin, [runnerPath], {
1028
+ stdio: ["pipe", "pipe", "pipe"]
1029
+ });
1030
+ this.runnerProcess.stdout?.on("data", (chunk) => {
1031
+ this.handleStdoutChunk(chunk);
1032
+ });
1033
+ this.runnerProcess.stderr?.on("data", (chunk) => {
1034
+ const text = chunk.toString("utf8").trimEnd();
1035
+ for (const line of text.split(`
1036
+ `)) {
1037
+ if (line)
1038
+ log4.debug(`[runner stderr] ${line}`);
1039
+ }
1040
+ });
1041
+ this.runnerProcess.on("close", (code) => {
1042
+ log4.error(`cursor-agent runner exited with code ${code}`, { poolKeyHash: this.poolKey.slice(0, 8) + "…" });
1043
+ this.runnerProcess = null;
1044
+ for (const [, pending] of this.pendingRequests.entries()) {
1045
+ pending.promiseRejector(new Error(`Runner exited with code ${code}`));
1046
+ pending.controller.error(new Error(`Runner exited with code ${code}`));
1047
+ }
1048
+ this.pendingRequests.clear();
1049
+ });
1050
+ this.runnerProcess.on("error", (err) => {
1051
+ log4.error("cursor-agent runner spawn error", { error: err.message });
1052
+ this.runnerProcess = null;
1053
+ for (const [, pending] of this.pendingRequests.entries()) {
1054
+ pending.promiseRejector(err);
1055
+ pending.controller.error(err);
1056
+ }
1057
+ this.pendingRequests.clear();
1058
+ });
1059
+ }
1060
+ sendRequest(requestId, request) {
1061
+ if (!this.runnerProcess?.stdin) {
1062
+ throw new Error("Runner process not ready");
1063
+ }
1064
+ const payload = {
1065
+ id: requestId,
1066
+ model: request.model,
1067
+ cwd: request.cwd,
1068
+ prompt: request.prompt,
1069
+ resumeChatId: request.resumeChatId,
1070
+ force: request.force ?? false,
1071
+ cursorAgent: resolveCursorAgentBinary()
1072
+ };
1073
+ this.runnerProcess.stdin.write(JSON.stringify(payload) + `
1074
+ `);
1075
+ }
1076
+ cancel(requestId) {
1077
+ if (!this.runnerProcess?.stdin) {
1078
+ log4.warn("cancel() called but runner stdin not ready", { requestId });
1079
+ return;
1080
+ }
1081
+ this.runnerProcess.stdin.write(JSON.stringify({ cancel: requestId }) + `
1082
+ `);
1083
+ }
1084
+ handleStdoutChunk(chunk) {
1085
+ this.lineBuffer += chunk.toString("utf8");
1086
+ const lines = this.lineBuffer.split(`
1087
+ `);
1088
+ this.lineBuffer = lines.pop() ?? "";
1089
+ for (const line of lines) {
1090
+ if (!line.trim())
1091
+ continue;
1092
+ try {
1093
+ const wrapped = JSON.parse(line);
1094
+ const requestId = wrapped.id;
1095
+ if (!requestId) {
1096
+ log4.warn("Wrapped response missing id", { wrapped });
1097
+ continue;
1098
+ }
1099
+ const pending = this.pendingRequests.get(requestId);
1100
+ if (!pending) {
1101
+ log4.warn(`Received response for unknown request ${requestId}`);
1102
+ continue;
1103
+ }
1104
+ if (wrapped.done) {
1105
+ pending.controller.close();
1106
+ pending.controller.closeStderr();
1107
+ pending.promiseResolver(wrapped.exitCode ?? 0);
1108
+ this.pendingRequests.delete(requestId);
1109
+ this.notifyIdleIfEmpty();
1110
+ } else if (wrapped.stderr != null) {
1111
+ const text = typeof wrapped.stderr === "string" ? wrapped.stderr : String(wrapped.stderr);
1112
+ pending.controller.enqueueStderr(new TextEncoder().encode(text));
1113
+ } else if (wrapped.event) {
1114
+ const eventJson = extractEventJson(line);
1115
+ pending.controller.enqueue(new TextEncoder().encode(eventJson + `
1116
+ `));
1117
+ }
1118
+ } catch (err) {
1119
+ log4.error("Failed to parse wrapped response line", {
1120
+ line,
1121
+ error: err instanceof Error ? err.message : String(err)
1122
+ });
1123
+ }
1124
+ }
1125
+ }
1126
+ isIdle() {
1127
+ return this.pendingRequests.size === 0;
1128
+ }
1129
+ notifyIdleIfEmpty() {
1130
+ if (this.pendingRequests.size === 0) {
1131
+ this.onIdle(this.poolKey);
1132
+ }
1133
+ }
1134
+ registerPending(controller, promiseResolver, promiseRejector) {
1135
+ const id = generateRequestId2();
1136
+ this.pendingRequests.set(id, { controller, promiseResolver, promiseRejector });
1137
+ return id;
1138
+ }
1139
+ failAllPending(err) {
1140
+ for (const [, pending] of this.pendingRequests.entries()) {
1141
+ pending.promiseRejector(err);
1142
+ pending.controller.error(err);
1143
+ }
1144
+ this.pendingRequests.clear();
1145
+ }
1146
+ kill() {
1147
+ if (this.runnerProcess) {
1148
+ try {
1149
+ this.runnerProcess.kill("SIGKILL");
1150
+ } catch {}
1151
+ this.runnerProcess = null;
1152
+ }
1153
+ this.failAllPending(new Error("Runner killed"));
1154
+ }
1155
+ }
1156
+
1157
+ class CursorAgentPoolManager {
1158
+ runners = new Map;
1159
+ idleTimers = new Map;
1160
+ getRunner(poolKey) {
1161
+ this.clearIdleTimer(poolKey);
1162
+ let runner = this.runners.get(poolKey);
1163
+ if (!runner) {
1164
+ while (this.runners.size >= DEFAULT_MAX_POOL_ENTRIES) {
1165
+ const oldest = this.runners.keys().next().value;
1166
+ if (oldest === undefined)
1167
+ break;
1168
+ this.clearIdleTimer(oldest);
1169
+ this.runners.get(oldest)?.kill();
1170
+ this.runners.delete(oldest);
1171
+ }
1172
+ runner = new CursorAgentPoolRunner(poolKey, (idlePoolKey) => {
1173
+ this.scheduleIdleEviction(idlePoolKey);
1174
+ });
1175
+ this.runners.set(poolKey, runner);
1176
+ }
1177
+ return runner;
1178
+ }
1179
+ size() {
1180
+ return this.runners.size;
1181
+ }
1182
+ clearIdleTimer(poolKey) {
1183
+ const timer = this.idleTimers.get(poolKey);
1184
+ if (!timer)
1185
+ return;
1186
+ clearTimeout(timer);
1187
+ this.idleTimers.delete(poolKey);
1188
+ }
1189
+ scheduleIdleEviction(poolKey) {
1190
+ this.clearIdleTimer(poolKey);
1191
+ const idleMs = parseAgentPoolIdleMs();
1192
+ if (idleMs <= 0)
1193
+ return;
1194
+ const timer = setTimeout(() => {
1195
+ this.idleTimers.delete(poolKey);
1196
+ const runner = this.runners.get(poolKey);
1197
+ if (!runner || !runner.isIdle())
1198
+ return;
1199
+ runner.kill();
1200
+ this.runners.delete(poolKey);
1201
+ log4.debug("evicted idle cursor-agent runner", {
1202
+ poolKeyHash: poolKey.slice(0, 8) + "…",
1203
+ idleMs
1204
+ });
1205
+ }, idleMs);
1206
+ timer.unref?.();
1207
+ this.idleTimers.set(poolKey, timer);
1208
+ }
1209
+ stopAll() {
1210
+ for (const timer of this.idleTimers.values()) {
1211
+ clearTimeout(timer);
1212
+ }
1213
+ this.idleTimers.clear();
1214
+ for (const runner of this.runners.values()) {
1215
+ runner.kill();
1216
+ }
1217
+ this.runners.clear();
1218
+ }
1219
+ }
1220
+ function createCursorAgentPoolNodeChild(options) {
1221
+ const poolKey = buildAgentPoolKey(options.cwd, options.model);
1222
+ const child = new CursorAgentPoolNodeChild;
1223
+ child.spawn({ ...options, poolKey });
1224
+ return child;
1225
+ }
1226
+ var log4, DEFAULT_MAX_POOL_ENTRIES = 16, DEFAULT_IDLE_MS, poolManager, CursorAgentPoolNodeChild;
1227
+ var init_cursor_agent_child = __esm(() => {
1228
+ init_logger();
1229
+ init_binary();
1230
+ init_sdk_child();
1231
+ log4 = createLogger("cursor-agent-child");
1232
+ DEFAULT_IDLE_MS = 15 * 60 * 1000;
1233
+ poolManager = new CursorAgentPoolManager;
1234
+ CursorAgentPoolNodeChild = class CursorAgentPoolNodeChild extends EventEmitter2 {
1235
+ stdout = new PassThrough2;
1236
+ stderr = new PassThrough2;
1237
+ requestId = null;
1238
+ runner = null;
1239
+ spawn(options) {
1240
+ this.spawnInternal(options);
1241
+ }
1242
+ async spawnInternal(options) {
1243
+ try {
1244
+ const runner = poolManager.getRunner(options.poolKey);
1245
+ this.runner = runner;
1246
+ await runner.ensureRunning();
1247
+ const controller = {
1248
+ enqueue: (data) => {
1249
+ this.stdout.write(data);
1250
+ },
1251
+ enqueueStderr: (data) => {
1252
+ this.stderr.write(data);
1253
+ },
1254
+ close: () => {
1255
+ this.stdout.end();
1256
+ },
1257
+ closeStderr: () => {
1258
+ this.stderr.end();
1259
+ },
1260
+ error: (err) => {
1261
+ this.stdout.destroy(err);
1262
+ }
1263
+ };
1264
+ const requestId = runner.registerPending(controller, (code) => {
1265
+ this.stderr.end();
1266
+ this.emit("close", code);
1267
+ }, (err) => {
1268
+ this.stderr.end();
1269
+ this.emit("error", err);
1270
+ });
1271
+ this.requestId = requestId;
1272
+ runner.sendRequest(requestId, options);
1273
+ log4.debug("cursor-agent pool request dispatched", {
1274
+ requestId,
1275
+ poolKeyHash: options.poolKey.slice(0, 8) + "…",
1276
+ resume: !!options.resumeChatId
1277
+ });
1278
+ } catch (err) {
1279
+ const error = err instanceof Error ? err : new Error(String(err));
1280
+ log4.error("Failed to spawn cursor-agent pool child", { error: error.message });
1281
+ this.emit("error", error);
1282
+ this.stderr.end();
1283
+ this.stdout.end();
1284
+ this.emit("close", 1);
1285
+ }
1286
+ }
1287
+ kill() {
1288
+ if (this.runner && this.requestId) {
1289
+ log4.debug(`kill() cancelling pool request ${this.requestId}`);
1290
+ this.runner.cancel(this.requestId);
1291
+ } else if (this.requestId) {
1292
+ log4.debug(`kill() called before runner ready for ${this.requestId}`);
1293
+ }
1294
+ }
1295
+ };
1296
+ });
1297
+
1298
+ // src/models/pricing.ts
1299
+ function getCursorModelCost(modelId) {
1300
+ if (modelId === "auto")
1301
+ return AUTO_COST;
1302
+ if (modelId === "composer-2-fast")
1303
+ return COMPOSER_2_FAST_COST;
1304
+ if (modelId === "composer-2")
1305
+ return COMPOSER_2_COST;
1306
+ if (modelId === "composer-1.5")
1307
+ return COMPOSER_1_5_COST;
1308
+ if (modelId.startsWith("claude-opus-4-7"))
1309
+ return CLAUDE_OPUS_COST;
1310
+ if (modelId.startsWith("claude-4.6-opus")) {
1311
+ return modelId.endsWith("-fast") ? CLAUDE_OPUS_FAST_COST : CLAUDE_OPUS_COST;
1312
+ }
1313
+ if (modelId.startsWith("claude-4.5-opus"))
1314
+ return CLAUDE_OPUS_COST;
1315
+ if (modelId.startsWith("claude-4.6-sonnet"))
1316
+ return CLAUDE_SONNET_WITH_LONG_CONTEXT_COST;
1317
+ if (modelId.startsWith("claude-4.5-sonnet"))
1318
+ return CLAUDE_SONNET_WITH_LONG_CONTEXT_COST;
1319
+ if (modelId.startsWith("claude-4-sonnet"))
1320
+ return CLAUDE_SONNET_COST;
1321
+ if (modelId === "gemini-3.1-pro")
1322
+ return GEMINI_3_PRO_COST;
1323
+ if (modelId === "gemini-3-flash")
1324
+ return GEMINI_3_FLASH_COST;
1325
+ if (modelId.startsWith("gpt-5.5"))
1326
+ return GPT_5_5_COST;
1327
+ if (modelId.startsWith("gpt-5.4-mini"))
1328
+ return GPT_5_4_MINI_COST;
1329
+ if (modelId.startsWith("gpt-5.4-nano"))
1330
+ return GPT_5_4_NANO_COST;
1331
+ if (modelId.startsWith("gpt-5.4")) {
1332
+ return modelId.endsWith("-fast") ? GPT_5_4_FAST_COST : GPT_5_4_COST;
1333
+ }
1334
+ if (modelId.startsWith("gpt-5.3-codex"))
1335
+ return GPT_5_3_CODEX_COST;
1336
+ if (modelId.startsWith("gpt-5.2-codex"))
1337
+ return GPT_5_2_COST;
1338
+ if (modelId.startsWith("gpt-5.2"))
1339
+ return GPT_5_2_COST;
1340
+ if (modelId.startsWith("gpt-5.1-codex-mini"))
1341
+ return GPT_5_MINI_COST;
1342
+ if (modelId.startsWith("gpt-5.1-codex-max"))
1343
+ return GPT_5_1_COST;
1344
+ if (modelId.startsWith("gpt-5.1"))
1345
+ return GPT_5_1_COST;
1346
+ if (modelId === "gpt-5-mini")
1347
+ return GPT_5_MINI_COST;
1348
+ if (modelId.startsWith("grok-4-20"))
1349
+ return GROK_4_20_COST;
1350
+ if (modelId === "kimi-k2.5")
1351
+ return KIMI_K2_5_COST;
1352
+ return;
1353
+ }
1354
+ function cost(input, output, cacheRead, cacheWrite) {
1355
+ return {
1356
+ input,
1357
+ output,
1358
+ cache_read: cacheRead,
1359
+ cache_write: cacheWrite
1360
+ };
1361
+ }
1362
+ function withLongContext(base, longContext) {
1363
+ return {
1364
+ ...base,
1365
+ context_over_200k: longContext
1366
+ };
1367
+ }
1368
+ var AUTO_COST, COMPOSER_2_COST, COMPOSER_2_FAST_COST, COMPOSER_1_5_COST, CLAUDE_SONNET_COST, CLAUDE_SONNET_LONG_CONTEXT_COST, CLAUDE_SONNET_WITH_LONG_CONTEXT_COST, CLAUDE_OPUS_COST, CLAUDE_OPUS_FAST_COST, GEMINI_3_PRO_COST, GEMINI_3_FLASH_COST, GPT_5_1_COST, GPT_5_2_COST, GPT_5_3_CODEX_COST, GPT_5_4_COST, GPT_5_4_FAST_COST, GPT_5_4_MINI_COST, GPT_5_4_NANO_COST, GPT_5_5_COST, GPT_5_MINI_COST, GROK_4_20_COST, KIMI_K2_5_COST;
1369
+ var init_pricing = __esm(() => {
1370
+ AUTO_COST = cost(1.25, 6, 0.25, 1.25);
1371
+ COMPOSER_2_COST = cost(0.5, 2.5, 0.2, 0.5);
1372
+ COMPOSER_2_FAST_COST = cost(1.5, 7.5, 0.35, 1.5);
1373
+ COMPOSER_1_5_COST = cost(3.5, 17.5, 0.35, 3.5);
1374
+ CLAUDE_SONNET_COST = cost(3, 15, 0.3, 3.75);
1375
+ CLAUDE_SONNET_LONG_CONTEXT_COST = cost(6, 22.5, 0.6, 7.5);
1376
+ CLAUDE_SONNET_WITH_LONG_CONTEXT_COST = withLongContext(CLAUDE_SONNET_COST, CLAUDE_SONNET_LONG_CONTEXT_COST);
1377
+ CLAUDE_OPUS_COST = cost(5, 25, 0.5, 6.25);
1378
+ CLAUDE_OPUS_FAST_COST = cost(30, 150, 3, 37.5);
1379
+ GEMINI_3_PRO_COST = withLongContext(cost(2, 12, 0.2, 2), cost(4, 18, 0.4, 4));
1380
+ GEMINI_3_FLASH_COST = cost(0.5, 3, 0.05, 0.5);
1381
+ GPT_5_1_COST = cost(1.25, 10, 0.125, 1.25);
1382
+ GPT_5_2_COST = cost(1.75, 14, 0.175, 1.75);
1383
+ GPT_5_3_CODEX_COST = cost(1.75, 14, 0.175, 1.75);
1384
+ GPT_5_4_COST = withLongContext(cost(2.5, 15, 0.25, 2.5), cost(5, 22.5, 0.5, 5));
1385
+ GPT_5_4_FAST_COST = cost(5, 30, 0.5, 5);
1386
+ GPT_5_4_MINI_COST = cost(0.75, 4.5, 0.075, 0.75);
1387
+ GPT_5_4_NANO_COST = cost(0.2, 1.25, 0.02, 0.2);
1388
+ GPT_5_5_COST = withLongContext(cost(5, 30, 0.5, 5), cost(10, 45, 1, 10));
1389
+ GPT_5_MINI_COST = cost(0.25, 2, 0.025, 0.25);
1390
+ GROK_4_20_COST = withLongContext(cost(2, 6, 0.2, 2), cost(4, 12, 0.4, 4));
1391
+ KIMI_K2_5_COST = cost(0.6, 3, 0.1, 0.6);
1392
+ });
1393
+
1394
+ // src/models/variants.ts
1395
+ function isSafeBaseId(baseId) {
1396
+ const parts = baseId.split("-").filter(Boolean);
1397
+ if (parts.length < 2)
1398
+ return false;
1399
+ if (baseId === "gpt-5")
1400
+ return false;
1401
+ return true;
1402
+ }
1403
+ function generateBaseCandidates(modelId) {
1404
+ const tokens = modelId.split("-");
1405
+ const candidates = [];
1406
+ for (let i = tokens.length - 1;i >= 1; i--) {
1407
+ const prefix = tokens.slice(0, i).join("-");
1408
+ if (isSafeBaseId(prefix))
1409
+ candidates.push(prefix);
1410
+ }
1411
+ return candidates;
1412
+ }
1413
+ function computeStats(candidate, modelIds) {
1414
+ const prefix = `${candidate}-`;
1415
+ const firstTokens = new Set;
1416
+ let count = 0;
1417
+ for (const otherId of modelIds) {
1418
+ if (!otherId.startsWith(prefix))
1419
+ continue;
1420
+ count++;
1421
+ const firstToken = otherId.slice(prefix.length).split("-", 1)[0];
1422
+ if (firstToken)
1423
+ firstTokens.add(firstToken);
1424
+ }
1425
+ return { count, diversity: firstTokens.size };
1426
+ }
1427
+ function chooseBase(modelId, knownModelIds, modelIds) {
1428
+ const candidates = generateBaseCandidates(modelId);
1429
+ if (candidates.length === 0)
1430
+ return null;
1431
+ const stats = new Map;
1432
+ for (const candidate of candidates) {
1433
+ stats.set(candidate, computeStats(candidate, modelIds));
1434
+ }
1435
+ let stepA = null;
1436
+ for (const candidate of candidates) {
1437
+ if (!knownModelIds.has(candidate))
1438
+ continue;
1439
+ const stat = stats.get(candidate);
1440
+ if (!stat || stat.count < 2 || stat.diversity < 2)
1441
+ continue;
1442
+ if (stepA === null || candidate.length < stepA.length)
1443
+ stepA = candidate;
1444
+ }
1445
+ if (stepA !== null)
1446
+ return stepA;
1447
+ let stepB = null;
1448
+ for (const candidate of candidates) {
1449
+ const stat = stats.get(candidate);
1450
+ if (!stat || stat.count < 2)
1451
+ continue;
1452
+ if (stepB === null || stat.diversity > stepB.diversity || stat.diversity === stepB.diversity && candidate.length > stepB.base.length) {
1453
+ stepB = { base: candidate, diversity: stat.diversity };
1454
+ }
1455
+ }
1456
+ if (stepB !== null)
1457
+ return stepB.base;
1458
+ let stepC = null;
1459
+ for (const candidate of candidates) {
1460
+ if (!knownModelIds.has(candidate))
1461
+ continue;
1462
+ if (stepC === null || candidate.length < stepC.length)
1463
+ stepC = candidate;
1464
+ }
1465
+ return stepC;
1466
+ }
1467
+ function getDefaultMember(members) {
1468
+ for (const variant of DEFAULT_VARIANT_ORDER) {
1469
+ const member = members.find((candidate) => candidate.variant === variant);
1470
+ if (member)
1471
+ return member;
1472
+ }
1473
+ return members[0];
1474
+ }
1475
+ function formatModelName(modelId) {
1476
+ return modelId.split("-").map((part) => {
1477
+ if (part === "gpt")
1478
+ return "GPT";
1479
+ if (part === "xhigh")
1480
+ return "XHigh";
1481
+ return part.charAt(0).toUpperCase() + part.slice(1);
1482
+ }).join(" ");
1483
+ }
1484
+ function compareVariants(a, b) {
1485
+ if (a.variant === null)
1486
+ return -1;
1487
+ if (b.variant === null)
1488
+ return 1;
1489
+ const aIndex = VARIANT_DISPLAY_ORDER.indexOf(a.variant);
1490
+ const bIndex = VARIANT_DISPLAY_ORDER.indexOf(b.variant);
1491
+ if (aIndex !== -1 && bIndex !== -1)
1492
+ return aIndex - bIndex;
1493
+ if (aIndex !== -1)
1494
+ return -1;
1495
+ if (bIndex !== -1)
1496
+ return 1;
1497
+ return a.variant.localeCompare(b.variant);
1498
+ }
1499
+ function createGroup(baseId, members) {
1500
+ const defaultMember = getDefaultMember(members);
1501
+ const variants = {};
1502
+ for (const member of [...members].sort(compareVariants)) {
1503
+ if (member.variant) {
1504
+ variants[member.variant] = member.cursorModelId;
1505
+ }
1506
+ }
1507
+ return {
1508
+ baseId,
1509
+ name: defaultMember.variant === null ? defaultMember.name : formatModelName(baseId),
1510
+ defaultCursorModelId: defaultMember.cursorModelId,
1511
+ variants,
1512
+ members
1513
+ };
1514
+ }
1515
+ function groupCursorModels(models) {
1516
+ const knownModelIds = new Set(models.map((model) => model.id));
1517
+ const modelIds = models.map((model) => model.id);
1518
+ const preferredBase = new Map;
1519
+ for (const model of models) {
1520
+ const base = chooseBase(model.id, knownModelIds, modelIds);
1521
+ if (base)
1522
+ preferredBase.set(model.id, base);
1523
+ }
1524
+ const baseSet = new Set(preferredBase.values());
1525
+ const groupMembers = new Map;
1526
+ const groupOrder = [];
1527
+ const recordMember = (baseId, member) => {
1528
+ const existing = groupMembers.get(baseId);
1529
+ if (existing) {
1530
+ existing.push(member);
1531
+ return;
1532
+ }
1533
+ groupMembers.set(baseId, [member]);
1534
+ groupOrder.push(baseId);
1535
+ };
1536
+ for (const model of models) {
1537
+ if (baseSet.has(model.id) && knownModelIds.has(model.id)) {
1538
+ recordMember(model.id, {
1539
+ baseId: model.id,
1540
+ variant: null,
1541
+ cursorModelId: model.id,
1542
+ name: model.name
1543
+ });
1544
+ continue;
1545
+ }
1546
+ const base = preferredBase.get(model.id);
1547
+ if (!base)
1548
+ continue;
1549
+ recordMember(base, {
1550
+ baseId: base,
1551
+ variant: model.id.slice(base.length + 1),
1552
+ cursorModelId: model.id,
1553
+ name: model.name
1554
+ });
1555
+ }
1556
+ const groupedIds = new Set;
1557
+ const groups = [];
1558
+ for (const baseId of groupOrder) {
1559
+ const members = groupMembers.get(baseId);
1560
+ if (!members || members.length < 2)
1561
+ continue;
1562
+ groups.push(createGroup(baseId, members));
1563
+ for (const member of members)
1564
+ groupedIds.add(member.cursorModelId);
1565
+ }
1566
+ const direct = [];
1567
+ for (const model of models) {
1568
+ if (groupedIds.has(model.id))
1569
+ continue;
1570
+ direct.push(model);
1571
+ }
1572
+ return { groups, direct };
1573
+ }
1574
+ function createVariantModelEntries(models) {
1575
+ const { groups, direct } = groupCursorModels(models);
1576
+ const entries = {};
1577
+ const groupedModelIds = new Set;
1578
+ for (const group of groups) {
1579
+ const variants = {};
1580
+ for (const [variant, cursorModel] of Object.entries(group.variants)) {
1581
+ const variantEntry = { cursorModel };
1582
+ const variantCost = getCursorModelCost(cursorModel);
1583
+ if (variantCost)
1584
+ variantEntry.cost = variantCost;
1585
+ variants[variant] = variantEntry;
1586
+ }
1587
+ const groupEntry = {
1588
+ name: group.name,
1589
+ options: {
1590
+ cursorModel: group.defaultCursorModelId
1591
+ },
1592
+ variants
1593
+ };
1594
+ const defaultCost = getCursorModelCost(group.defaultCursorModelId);
1595
+ if (defaultCost)
1596
+ groupEntry.cost = defaultCost;
1597
+ entries[group.baseId] = groupEntry;
1598
+ for (const member of group.members) {
1599
+ groupedModelIds.add(member.cursorModelId);
1600
+ }
1601
+ }
1602
+ for (const model of direct) {
1603
+ const entry = { name: model.name };
1604
+ const directCost = getCursorModelCost(model.id);
1605
+ if (directCost)
1606
+ entry.cost = directCost;
1607
+ entries[model.id] = entry;
1608
+ }
1609
+ return { entries, groupedModelIds };
1610
+ }
1611
+ function mergeCursorModelEntries(existingModels, discoveredModels, options) {
1612
+ if (!options.variants) {
1613
+ return mergeDirectModelEntries(existingModels, discoveredModels);
1614
+ }
1615
+ const { entries, groupedModelIds } = createVariantModelEntries(discoveredModels);
1616
+ const models = { ...existingModels };
1617
+ let removedCount = 0;
1618
+ if (options.compact) {
1619
+ for (const modelId of groupedModelIds) {
1620
+ if (!Object.prototype.hasOwnProperty.call(models, modelId))
1621
+ continue;
1622
+ if (Object.prototype.hasOwnProperty.call(entries, modelId))
1623
+ continue;
1624
+ delete models[modelId];
1625
+ removedCount++;
1626
+ }
1627
+ }
1628
+ for (const [modelId, entry] of Object.entries(entries)) {
1629
+ models[modelId] = mergeEntryPreservingUserFields(models[modelId], entry);
1630
+ }
1631
+ return {
1632
+ models,
1633
+ syncedCount: Object.keys(entries).length,
1634
+ groupedCount: groupedModelIds.size,
1635
+ removedCount
1636
+ };
1637
+ }
1638
+ function mergeDirectModelEntries(existingModels, discoveredModels) {
1639
+ const models = { ...existingModels };
1640
+ for (const model of discoveredModels) {
1641
+ const generated = { name: model.name };
1642
+ const directCost = getCursorModelCost(model.id);
1643
+ if (directCost)
1644
+ generated.cost = directCost;
1645
+ models[model.id] = mergeEntryPreservingUserFields(models[model.id], generated);
1646
+ }
1647
+ return {
1648
+ models,
1649
+ syncedCount: discoveredModels.length,
1650
+ groupedCount: 0,
1651
+ removedCount: 0
1652
+ };
1653
+ }
1654
+ function isPlainObject(value) {
1655
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1656
+ }
1657
+ function mergeEntryPreservingUserFields(existing, generated) {
1658
+ if (!isPlainObject(existing))
1659
+ return generated;
1660
+ const merged = { ...existing, ...generated };
1661
+ if (existing.cost !== undefined) {
1662
+ merged.cost = existing.cost;
1663
+ }
1664
+ if (isPlainObject(existing.variants) && isPlainObject(generated.variants)) {
1665
+ const mergedVariants = { ...generated.variants };
1666
+ for (const [variantKey, existingVariant] of Object.entries(existing.variants)) {
1667
+ const generatedVariant = generated.variants[variantKey];
1668
+ if (!isPlainObject(existingVariant))
1669
+ continue;
1670
+ if (!isPlainObject(generatedVariant)) {
1671
+ mergedVariants[variantKey] = existingVariant;
1672
+ continue;
1673
+ }
1674
+ const variantMerged = { ...generatedVariant };
1675
+ if (existingVariant.cost !== undefined) {
1676
+ variantMerged.cost = existingVariant.cost;
1677
+ }
1678
+ mergedVariants[variantKey] = variantMerged;
1679
+ }
1680
+ merged.variants = mergedVariants;
1681
+ }
1682
+ return merged;
1683
+ }
1684
+ var DEFAULT_VARIANT_ORDER, VARIANT_DISPLAY_ORDER;
1685
+ var init_variants = __esm(() => {
1686
+ init_pricing();
1687
+ DEFAULT_VARIANT_ORDER = [
1688
+ null,
1689
+ "medium",
1690
+ "high",
1691
+ "low",
1692
+ "none",
1693
+ "xhigh",
1694
+ "max"
1695
+ ];
1696
+ VARIANT_DISPLAY_ORDER = [
1697
+ "none",
1698
+ "low",
1699
+ "low-fast",
1700
+ "fast",
1701
+ "medium",
1702
+ "medium-fast",
1703
+ "medium-thinking",
1704
+ "high",
1705
+ "high-fast",
1706
+ "high-thinking",
1707
+ "high-thinking-fast",
1708
+ "xhigh",
1709
+ "xhigh-fast",
1710
+ "max",
1711
+ "max-thinking",
1712
+ "max-thinking-fast",
1713
+ "thinking",
1714
+ "thinking-low",
1715
+ "thinking-medium",
1716
+ "thinking-high",
1717
+ "thinking-high-fast",
1718
+ "thinking-xhigh",
1719
+ "thinking-max",
1720
+ "extra-high",
1721
+ "spark-preview",
1722
+ "spark-preview-low",
1723
+ "spark-preview-medium",
1724
+ "spark-preview-high",
1725
+ "spark-preview-xhigh"
1726
+ ];
1727
+ });
1728
+
1729
+ // src/plugin-toggle.ts
1730
+ import { existsSync as existsSync5, readFileSync } from "fs";
1731
+ import { homedir as homedir3 } from "os";
1732
+ import { join as join4, resolve as resolve3 } from "path";
1733
+ function matchesPlugin(entry) {
1734
+ if (entry === CURSOR_PROVIDER_ID)
1735
+ return true;
1736
+ if (entry === NPM_PACKAGE_NAME)
1737
+ return true;
1738
+ if (entry.startsWith(`${NPM_PACKAGE_NAME}@`))
1739
+ return true;
1740
+ return false;
1741
+ }
1742
+ function resolveOpenCodeConfigPath(env = process.env) {
1743
+ if (env.OPENCODE_CONFIG && env.OPENCODE_CONFIG.length > 0) {
1744
+ return resolve3(env.OPENCODE_CONFIG);
1745
+ }
1746
+ const configHome = env.XDG_CONFIG_HOME && env.XDG_CONFIG_HOME.length > 0 ? env.XDG_CONFIG_HOME : join4(homedir3(), ".config");
1747
+ return join4(configHome, "opencode", "opencode.json");
1748
+ }
1749
+ function isCursorPluginEnabledInConfig(config) {
1750
+ if (!config || typeof config !== "object") {
1751
+ return true;
1752
+ }
1753
+ const configObject = config;
1754
+ if (configObject.provider && typeof configObject.provider === "object") {
1755
+ if (CURSOR_PROVIDER_ID in configObject.provider) {
1756
+ return true;
1757
+ }
1758
+ }
1759
+ if (Array.isArray(configObject.plugin)) {
1760
+ return configObject.plugin.some((entry) => matchesPlugin(entry));
1761
+ }
1762
+ return true;
1763
+ }
1764
+ function shouldEnableCursorPlugin(env = process.env) {
1765
+ const configPath = resolveOpenCodeConfigPath(env);
1766
+ if (!existsSync5(configPath)) {
1767
+ return {
1768
+ enabled: true,
1769
+ configPath,
1770
+ reason: "config_missing"
1771
+ };
1772
+ }
1773
+ try {
1774
+ const raw = readFileSync(configPath, "utf8");
1775
+ const parsed = JSON.parse(raw);
1776
+ const enabled = isCursorPluginEnabledInConfig(parsed);
1777
+ return {
1778
+ enabled,
1779
+ configPath,
1780
+ reason: enabled ? "enabled" : "disabled_in_plugin_array"
1781
+ };
1782
+ } catch {
1783
+ return {
1784
+ enabled: true,
1785
+ configPath,
1786
+ reason: "config_unreadable_or_invalid"
1787
+ };
1788
+ }
1789
+ }
1790
+ var CURSOR_PROVIDER_ID = "cursor-acp", NPM_PACKAGE_NAME = "@evanovation/open-cursor";
1791
+ var init_plugin_toggle = () => {};
1792
+
1793
+ // src/proxy/incremental-prompt.ts
1794
+ function extractTextContent(content) {
1795
+ if (typeof content === "string")
1796
+ return content;
1797
+ if (Array.isArray(content)) {
1798
+ return content.map((part) => part?.type === "text" && typeof part.text === "string" ? part.text : "").filter(Boolean).join(`
1799
+ `);
1800
+ }
1801
+ return "";
1802
+ }
1803
+ function buildIncrementalPrompt(messages) {
1804
+ if (messages.length === 0)
1805
+ return null;
1806
+ const last = messages[messages.length - 1];
1807
+ if (last?.role === "tool") {
1808
+ const lines = [];
1809
+ for (let i = messages.length - 1;i >= 0; i--) {
1810
+ const m = messages[i];
1811
+ if (m?.role !== "tool")
1812
+ break;
1813
+ const callId = m.tool_call_id || "unknown";
1814
+ const body = typeof m.content === "string" ? m.content : JSON.stringify(m.content ?? "");
1815
+ lines.unshift(`TOOL_RESULT (call_id: ${callId}): ${body}`);
1816
+ }
1817
+ if (lines.length === 0)
1818
+ return null;
1819
+ lines.push("The above tool calls have been executed. Continue your response based on these results.");
1820
+ return lines.join(`
1821
+
1822
+ `);
1823
+ }
1824
+ if (last?.role === "user") {
1825
+ const text = extractTextContent(last.content);
1826
+ if (!text.trim())
1827
+ return null;
1828
+ if (Array.isArray(last.content) && last.content.some((part) => part?.type && part.type !== "text")) {
1829
+ return null;
1830
+ }
1831
+ return text.trim();
1832
+ }
1833
+ return null;
1834
+ }
1835
+
1836
+ // src/proxy/session-resume.ts
1837
+ import { createHash } from "node:crypto";
1838
+ function simpleHash(input) {
1839
+ return createHash("sha256").update(input).digest("hex").slice(0, 32);
1840
+ }
1841
+ function isMetaUserMessage(content) {
1842
+ const lower = content.toLowerCase();
1843
+ return lower.includes("title generator") || lower.includes("thread title") || lower.includes("generate a brief title");
1844
+ }
1845
+ function deriveConversationAnchor(messages) {
1846
+ for (const message of messages) {
1847
+ if (message?.role !== "user")
1848
+ continue;
1849
+ const text = extractTextContent(message.content).trim();
1850
+ if (!text || isMetaUserMessage(text))
1851
+ continue;
1852
+ const canonical = canonicalizeContentForAnchor(message.content);
1853
+ return { anchor: simpleHash(canonical), contentPrefix: text.slice(0, 500) };
1854
+ }
1855
+ return;
1856
+ }
1857
+ function deriveConversationResumePrefixes(messages) {
1858
+ const users = [];
1859
+ for (let index = 0;index < messages.length; index++) {
1860
+ const message = messages[index];
1861
+ if (message?.role !== "user")
1862
+ continue;
1863
+ const text = extractTextContent(message.content).trim();
1864
+ if (!text || isMetaUserMessage(text))
1865
+ continue;
1866
+ users.push({
1867
+ canonical: canonicalizeContentForAnchor(message.content),
1868
+ prefix: text.slice(0, 500),
1869
+ index
1870
+ });
1871
+ }
1872
+ if (users.length === 0)
1873
+ return;
1874
+ const lastUserIsLatestMessage = users[users.length - 1]?.index === messages.length - 1;
1875
+ const lookupUsers = lastUserIsLatestMessage && users.length > 1 ? users.slice(0, -1) : users;
1876
+ return {
1877
+ lookupContentPrefix: buildUserSequencePrefix(lookupUsers),
1878
+ recordContentPrefix: buildUserSequencePrefix(users)
1879
+ };
1880
+ }
1881
+ function buildUserSequencePrefix(users) {
1882
+ if (users.length === 1)
1883
+ return users[0].prefix;
1884
+ return `users:${users.length}:${simpleHash(users.map((user) => user.canonical).join(`
1885
+ \x00
1886
+ `))}`;
1887
+ }
1888
+ function canonicalizeContentForAnchor(content) {
1889
+ if (typeof content === "string")
1890
+ return content;
1891
+ if (!Array.isArray(content))
1892
+ return "";
1893
+ const hasNonText = content.some((part) => part?.type !== "text" || typeof part.text !== "string");
1894
+ if (!hasNonText) {
1895
+ return content.map((part) => part.text).join(`
1896
+ `);
1897
+ }
1898
+ return content.map((part) => {
1899
+ if (part?.type === "text" && typeof part.text === "string") {
1900
+ return `text:${part.text}`;
1901
+ }
1902
+ if (part?.type === "image_url") {
1903
+ return `image_url:${typeof part.image_url?.url === "string" ? part.image_url.url : ""}`;
1904
+ }
1905
+ return `part:${part?.type ?? ""}`;
1906
+ }).join(`
1907
+ `);
1908
+ }
1909
+ function buildSessionKey(workspace, model, anchor) {
1910
+ return `${workspace}\x00${model}\x00${anchor}`;
1911
+ }
1912
+ function isSessionResumeEnabled() {
1913
+ const value = process.env.CURSOR_ACP_SESSION_RESUME?.toLowerCase();
1914
+ return value === "1" || value === "true" || value === "on" || value === "yes";
1915
+ }
1916
+ function getResumeChatId(sessionKey, expectedPrefix, toolFingerprint, subagentFingerprint) {
1917
+ const entry = cache.get(sessionKey);
1918
+ if (!entry)
1919
+ return;
1920
+ if (Date.now() - entry.updatedAt > DEFAULT_TTL_MS) {
1921
+ evictEntry(sessionKey, "ttlExpired", {
1922
+ ageMs: Date.now() - entry.updatedAt,
1923
+ ttlMs: DEFAULT_TTL_MS
1924
+ });
1925
+ return;
1926
+ }
1927
+ if (expectedPrefix != null && entry.contentPrefix !== expectedPrefix) {
1928
+ log5.warn("Skipping session resume entry due to content prefix mismatch", {
1929
+ sessionKeyHash: sanitizeSessionKey(sessionKey),
1930
+ storedPrefixLength: entry.contentPrefix.length,
1931
+ expectedPrefixLength: expectedPrefix.length
1932
+ });
1933
+ return;
1934
+ }
1935
+ if ((toolFingerprint || entry.toolFingerprint) && entry.toolFingerprint !== toolFingerprint) {
1936
+ evictEntry(sessionKey, "toolFingerprintMismatch", {}, "warn");
1937
+ return;
1938
+ }
1939
+ if ((subagentFingerprint || entry.subagentFingerprint) && entry.subagentFingerprint !== subagentFingerprint) {
1940
+ evictEntry(sessionKey, "subagentFingerprintMismatch", {}, "warn");
1941
+ return;
1942
+ }
1943
+ cache.delete(sessionKey);
1944
+ cache.set(sessionKey, entry);
1945
+ return entry.chatId;
1946
+ }
1947
+ function hasResumeChatId(sessionKey, expectedPrefix, toolFingerprint, subagentFingerprint) {
1948
+ const entry = cache.get(sessionKey);
1949
+ if (!entry)
1950
+ return false;
1951
+ if (Date.now() - entry.updatedAt > DEFAULT_TTL_MS)
1952
+ return false;
1953
+ if (expectedPrefix != null && entry.contentPrefix !== expectedPrefix)
1954
+ return false;
1955
+ if ((toolFingerprint || entry.toolFingerprint) && entry.toolFingerprint !== toolFingerprint) {
1956
+ return false;
1957
+ }
1958
+ if ((subagentFingerprint || entry.subagentFingerprint) && entry.subagentFingerprint !== subagentFingerprint) {
1959
+ return false;
1960
+ }
1961
+ return !!entry.chatId;
1962
+ }
1963
+ function recordResumeChatId(sessionKey, chatId, contentPrefix, toolFingerprint, subagentFingerprint) {
1964
+ if (!chatId)
1965
+ return;
1966
+ const trimmed = chatId.trim();
1967
+ if (!RESUME_CHAT_ID_SAFE_RE.test(trimmed)) {
1968
+ log5.warn("Refusing to cache unsafe resume chat ID", {
1969
+ sessionKeyHash: sanitizeSessionKey(sessionKey),
1970
+ chatIdHash: hashForLog(trimmed)
1971
+ });
1972
+ return;
1973
+ }
1974
+ cache.delete(sessionKey);
1975
+ cache.set(sessionKey, {
1976
+ chatId: trimmed,
1977
+ contentPrefix,
1978
+ toolFingerprint,
1979
+ subagentFingerprint,
1980
+ updatedAt: Date.now()
1981
+ });
1982
+ while (cache.size > DEFAULT_MAX_ENTRIES) {
1983
+ const oldest = cache.keys().next().value;
1984
+ if (oldest === undefined)
1985
+ break;
1986
+ evictEntry(oldest, "maxEntries", { maxEntries: DEFAULT_MAX_ENTRIES });
1987
+ }
1988
+ }
1989
+ function sanitizeSessionKey(sessionKey) {
1990
+ return createHash("sha256").update(sessionKey).digest("hex").slice(0, 32);
1991
+ }
1992
+ function hashForLog(input) {
1993
+ return sanitizeSessionKey(typeof input === "string" ? input : String(input ?? ""));
1994
+ }
1995
+ function evictEntry(sessionKey, reason, extra = {}, logLevel = "info") {
1996
+ const payload = {
1997
+ sessionKeyHash: sanitizeSessionKey(sessionKey),
1998
+ reason,
1999
+ ...extra
2000
+ };
2001
+ if (logLevel === "warn") {
2002
+ log5.warn("Evicting session resume entry", payload);
2003
+ } else {
2004
+ log5.info("Evicting session resume entry", payload);
2005
+ }
2006
+ cache.delete(sessionKey);
2007
+ }
2008
+ function clearResumeChatId(sessionKey) {
2009
+ cache.delete(sessionKey);
2010
+ }
2011
+ var log5, RESUME_CHAT_ID_SAFE_RE, DEFAULT_TTL_MS, DEFAULT_MAX_ENTRIES = 64, cache;
2012
+ var init_session_resume = __esm(() => {
2013
+ init_logger();
2014
+ log5 = createLogger("session-resume");
2015
+ RESUME_CHAT_ID_SAFE_RE = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
2016
+ DEFAULT_TTL_MS = 60 * 60 * 1000;
2017
+ cache = new Map;
2018
+ });
2019
+
2020
+ // src/cli/opencode-cursor.ts
2021
+ init_model_discovery();
2022
+ init_binary();
2023
+ init_auth();
2024
+ init_backend();
2025
+ init_cursor_agent_child();
2026
+ init_variants();
2027
+ init_plugin_toggle();
2028
+ init_session_resume();
2029
+ import { execFileSync as execFileSync2 } from "child_process";
2030
+ import {
2031
+ copyFileSync,
2032
+ existsSync as existsSync6,
2033
+ lstatSync,
2034
+ mkdirSync as mkdirSync2,
2035
+ realpathSync,
2036
+ readFileSync as readFileSync2,
2037
+ rmSync,
2038
+ symlinkSync,
2039
+ writeFileSync
2040
+ } from "fs";
2041
+ import { homedir as homedir4 } from "os";
2042
+ import { basename, dirname as dirname3, join as join5, resolve as resolve4 } from "path";
2043
+ import { fileURLToPath as fileURLToPath3 } from "url";
2044
+ var BRANDING_HEADER = `
2045
+ ▄▄▄ ▄▄▄▄ ▄▄▄▄▄ ▄▄ ▄▄ ▄▄▄ ▄▄ ▄▄ ▄▄▄▄ ▄▄▄▄ ▄▄▄ ▄▄▄▄
2046
+ ██ ██ ██ ██ ██▄▄ ███▄██ ▄▄▄ ██ ▀▀ ██ ██ ██ ██ ██▄▄▄ ██ ██ ██ ██
2047
+ ▀█▄█▀ ██▀▀ ██▄▄▄ ██ ▀██ ▀█▄█▀ ▀█▄█▀ ██▀█▄ ▄▄▄█▀ ▀█▄█▀ ██▀█▄
2048
+ `;
2049
+ function getBrandingHeader() {
2050
+ return BRANDING_HEADER.trim();
2051
+ }
2052
+ function checkBun() {
2053
+ try {
2054
+ const version = execFileSync2("bun", ["--version"], { encoding: "utf8" }).trim();
2055
+ return { name: "bun", passed: true, message: `v${version}` };
2056
+ } catch {
2057
+ return {
2058
+ name: "bun",
2059
+ passed: false,
2060
+ message: "not found - install with: curl -fsSL https://bun.sh/install | bash"
2061
+ };
2062
+ }
2063
+ }
2064
+ function checkCursorAgent() {
2065
+ try {
2066
+ const output = execFileSync2(resolveCursorAgentBinary(), ["--version"], { encoding: "utf8" }).trim();
2067
+ const version = output.split(`
2068
+ `)[0] || "installed";
2069
+ return { name: "cursor-agent", passed: true, message: version };
2070
+ } catch {
2071
+ return {
2072
+ name: "cursor-agent",
2073
+ passed: false,
2074
+ message: "not found - install with: curl -fsS https://cursor.com/install | bash"
2075
+ };
2076
+ }
2077
+ }
2078
+ function checkCursorAgentLogin() {
2079
+ try {
2080
+ execFileSync2(resolveCursorAgentBinary(), ["models"], {
2081
+ encoding: "utf8",
2082
+ stdio: ["ignore", "pipe", "pipe"],
2083
+ timeout: 3000
2084
+ });
2085
+ return { name: "cursor-agent login", passed: true, message: "logged in" };
2086
+ } catch {
2087
+ return {
2088
+ name: "cursor-agent login",
2089
+ passed: false,
2090
+ message: "not logged in - run: cursor-agent login",
2091
+ warning: true
2092
+ };
2093
+ }
2094
+ }
2095
+ function getProviderApiKey(config) {
2096
+ const provider = config?.provider?.[PROVIDER_ID];
2097
+ const apiKey = provider?.options?.apiKey;
2098
+ return typeof apiKey === "string" ? apiKey : undefined;
2099
+ }
2100
+ function resolveCliSdkAuthSource(config) {
2101
+ if (isUsableSdkApiKey(process.env.CURSOR_API_KEY)) {
2102
+ return "CURSOR_API_KEY";
2103
+ }
2104
+ if (isUsableSdkApiKey(getProviderApiKey(config))) {
2105
+ return "provider.options.apiKey";
2106
+ }
2107
+ return;
2108
+ }
2109
+ function getRuntimeStatus() {
2110
+ return {
2111
+ backend: {
2112
+ preference: parseCursorBackendPreference(process.env.CURSOR_ACP_BACKEND).preference
2113
+ },
2114
+ agentPool: {
2115
+ enabled: isAgentPoolEnabled(),
2116
+ idleMs: parseAgentPoolIdleMs()
2117
+ },
2118
+ sessionResume: {
2119
+ enabled: isSessionResumeEnabled()
2120
+ },
2121
+ logging: {
2122
+ level: process.env.CURSOR_ACP_LOG_LEVEL || "info",
2123
+ console: process.env.CURSOR_ACP_LOG_CONSOLE === "1",
2124
+ dir: process.env.CURSOR_ACP_LOG_DIR || join5(homedir4(), ".opencode-cursor")
2125
+ }
2126
+ };
2127
+ }
2128
+ function checkSdkApiKey(config) {
2129
+ const source = resolveCliSdkAuthSource(config);
2130
+ if (source) {
2131
+ return {
2132
+ name: "Cursor SDK API key",
2133
+ passed: true,
2134
+ message: `available via ${source}`
2135
+ };
2136
+ }
2137
+ const backend = parseCursorBackendPreference(process.env.CURSOR_ACP_BACKEND).preference;
2138
+ return {
2139
+ name: "Cursor SDK API key",
2140
+ passed: false,
2141
+ warning: backend !== "sdk",
2142
+ message: backend === "sdk" ? "not configured - required for CURSOR_ACP_BACKEND=sdk" : "not configured - required only for CURSOR_ACP_BACKEND=sdk or when cursor-agent is unavailable"
2143
+ };
2144
+ }
2145
+ function adjustCursorAgentCheckForBackend(check, config) {
2146
+ if (check.passed) {
2147
+ return check;
2148
+ }
2149
+ const backend = parseCursorBackendPreference(process.env.CURSOR_ACP_BACKEND).preference;
2150
+ const sdkSource = resolveCliSdkAuthSource(config);
2151
+ const sdkCanHandleRequest = backend === "sdk" || backend === "auto" && sdkSource;
2152
+ if (!sdkCanHandleRequest) {
2153
+ return check;
2154
+ }
2155
+ return {
2156
+ ...check,
2157
+ warning: true,
2158
+ message: sdkSource ? `${check.message}; SDK backend can be used via ${sdkSource}` : `${check.message}; SDK backend selected but no SDK API key is configured`
2159
+ };
2160
+ }
2161
+ function checkOpenCode() {
2162
+ try {
2163
+ const version = execFileSync2("opencode", ["--version"], { encoding: "utf8" }).trim();
2164
+ return { name: "OpenCode", passed: true, message: version };
2165
+ } catch {
2166
+ return {
2167
+ name: "OpenCode",
2168
+ passed: false,
2169
+ message: "not found - install with: curl -fsSL https://opencode.ai/install | bash"
2170
+ };
2171
+ }
2172
+ }
2173
+ function isNpmDirectInstalled(config) {
2174
+ if (!config || typeof config !== "object")
2175
+ return false;
2176
+ const plugins = config.plugin;
2177
+ if (!Array.isArray(plugins))
2178
+ return false;
2179
+ return plugins.some((p) => typeof p === "string" && p.startsWith(NPM_PACKAGE_PREFIX));
2180
+ }
2181
+ function checkPluginFile(pluginPath, config) {
2182
+ try {
2183
+ if (!existsSync6(pluginPath)) {
2184
+ if (isNpmDirectInstalled(config)) {
2185
+ return {
2186
+ name: "Plugin file",
2187
+ passed: true,
2188
+ message: "Installed via npm package (no symlink needed)"
2189
+ };
2190
+ }
2191
+ return {
2192
+ name: "Plugin file",
2193
+ passed: false,
2194
+ message: "not found - run: open-cursor install"
2195
+ };
2196
+ }
2197
+ const stat = lstatSync(pluginPath);
2198
+ if (stat.isSymbolicLink()) {
2199
+ const target = readFileSync2(pluginPath, "utf8");
2200
+ return { name: "Plugin file", passed: true, message: `symlink → ${target}` };
2201
+ }
2202
+ return { name: "Plugin file", passed: true, message: "file (copy)" };
2203
+ } catch {
2204
+ return {
2205
+ name: "Plugin file",
2206
+ passed: false,
2207
+ message: "error reading plugin file"
2208
+ };
2209
+ }
2210
+ }
2211
+ function checkProviderConfig(configPath) {
2212
+ try {
2213
+ if (!existsSync6(configPath)) {
2214
+ return {
2215
+ name: "Provider config",
2216
+ passed: false,
2217
+ message: "config not found - run: open-cursor install"
2218
+ };
2219
+ }
2220
+ const config = readConfig(configPath);
2221
+ const provider = config.provider?.["cursor-acp"];
2222
+ if (!provider) {
2223
+ return {
2224
+ name: "Provider config",
2225
+ passed: false,
2226
+ message: "cursor-acp provider missing - run: open-cursor install"
2227
+ };
2228
+ }
2229
+ const modelCount = Object.keys(provider.models || {}).length;
2230
+ return { name: "Provider config", passed: true, message: `${modelCount} models` };
2231
+ } catch {
2232
+ return {
2233
+ name: "Provider config",
2234
+ passed: false,
2235
+ message: "error reading config"
2236
+ };
2237
+ }
2238
+ }
2239
+ function checkAiSdk(opencodeDir) {
2240
+ try {
2241
+ const sdkPath = join5(opencodeDir, "node_modules", "@ai-sdk", "openai-compatible");
2242
+ if (existsSync6(sdkPath)) {
2243
+ return { name: "AI SDK", passed: true, message: "@ai-sdk/openai-compatible installed" };
2244
+ }
2245
+ return {
2246
+ name: "AI SDK",
2247
+ passed: false,
2248
+ message: "not installed - run: open-cursor install"
2249
+ };
2250
+ } catch {
2251
+ return {
2252
+ name: "AI SDK",
2253
+ passed: false,
2254
+ message: "error checking AI SDK"
2255
+ };
2256
+ }
2257
+ }
2258
+ function runDoctorChecks(configPath, pluginPath) {
2259
+ const opencodeDir = dirname3(configPath);
2260
+ let config;
2261
+ try {
2262
+ config = readConfig(configPath);
2263
+ } catch {
2264
+ config = undefined;
2265
+ }
2266
+ return [
2267
+ checkBun(),
2268
+ adjustCursorAgentCheckForBackend(checkCursorAgent(), config),
2269
+ checkCursorAgentLogin(),
2270
+ checkSdkApiKey(config),
2271
+ checkOpenCode(),
2272
+ checkPluginFile(pluginPath, config),
2273
+ checkProviderConfig(configPath),
2274
+ checkAiSdk(opencodeDir)
2275
+ ];
2276
+ }
2277
+ var PROVIDER_ID = "cursor-acp";
2278
+ var NPM_PACKAGE_PREFIX = "@evanovation/open-cursor";
2279
+ var DEFAULT_BASE_URL = "http://127.0.0.1:32124/v1";
2280
+ function printHelp() {
2281
+ const binName = basename(process.argv[1] || "open-cursor");
2282
+ console.log(getBrandingHeader());
2283
+ console.log(`${binName}
2284
+
2285
+ Commands:
2286
+ install Configure OpenCode for Cursor (idempotent, safe to re-run)
2287
+ sync-models Refresh model list from cursor-agent
2288
+ models Explain discovered Cursor model groups and variants
2289
+ status Show current configuration state
2290
+ doctor Diagnose common issues
2291
+ uninstall Remove cursor-acp from OpenCode config
2292
+ help Show this help message
2293
+
2294
+ Options:
2295
+ --config <path> Path to opencode.json (default: OPENCODE_CONFIG or ~/.config/opencode/opencode.json)
2296
+ --plugin-dir <path> Path to plugin directory (default: ~/.config/opencode/plugin)
2297
+ --base-url <url> Proxy base URL (default: http://127.0.0.1:32124/v1)
2298
+ --copy Copy plugin instead of symlink
2299
+ --skip-models Skip model sync during install
2300
+ --variants Generate compact OpenCode model variants from Cursor models
2301
+ --compact With --variants, remove raw grouped Cursor model entries
2302
+ --dry-run Preview sync/install config changes without writing files
2303
+ --deep Run extra doctor checks for models and variant config
2304
+ --explain Show model grouping explanation (models command)
2305
+ --no-backup Don't create config backup
2306
+ --json Output in JSON format where supported
2307
+ `);
2308
+ }
2309
+ function parseArgs(argv) {
2310
+ const [commandRaw, ...rest] = argv;
2311
+ const command = normalizeCommand(commandRaw);
2312
+ const options = {};
2313
+ for (let i = 0;i < rest.length; i += 1) {
2314
+ const arg = rest[i];
2315
+ if (arg === "--copy") {
2316
+ options.copy = true;
2317
+ } else if (arg === "--skip-models") {
2318
+ options.skipModels = true;
2319
+ } else if (arg === "--variants") {
2320
+ options.variants = true;
2321
+ } else if (arg === "--compact") {
2322
+ options.compact = true;
2323
+ } else if (arg === "--dry-run") {
2324
+ options.dryRun = true;
2325
+ } else if (arg === "--deep") {
2326
+ options.deep = true;
2327
+ } else if (arg === "--explain") {
2328
+ options.explain = true;
2329
+ } else if (arg === "--no-backup") {
2330
+ options.noBackup = true;
2331
+ } else if (arg === "--config" && rest[i + 1]) {
2332
+ options.config = rest[i + 1];
2333
+ i += 1;
2334
+ } else if (arg === "--plugin-dir" && rest[i + 1]) {
2335
+ options.pluginDir = rest[i + 1];
2336
+ i += 1;
2337
+ } else if (arg === "--base-url" && rest[i + 1]) {
2338
+ options.baseUrl = rest[i + 1];
2339
+ i += 1;
2340
+ } else if (arg === "--json") {
2341
+ options.json = true;
2342
+ } else {
2343
+ throw new Error(`Unknown argument: ${arg}`);
2344
+ }
2345
+ }
2346
+ return { command, options };
2347
+ }
2348
+ function normalizeCommand(value) {
2349
+ switch ((value || "help").toLowerCase()) {
2350
+ case "install":
2351
+ case "sync-models":
2352
+ case "models":
2353
+ case "uninstall":
2354
+ case "status":
2355
+ case "doctor":
2356
+ case "help":
2357
+ return value ? value.toLowerCase() : "help";
2358
+ default:
2359
+ throw new Error(`Unknown command: ${value}`);
2360
+ }
2361
+ }
2362
+ function getConfigHome() {
2363
+ const xdg = process.env.XDG_CONFIG_HOME;
2364
+ if (xdg && xdg.length > 0)
2365
+ return xdg;
2366
+ return join5(homedir4(), ".config");
2367
+ }
2368
+ function resolvePaths(options) {
2369
+ const opencodeDir = join5(getConfigHome(), "opencode");
2370
+ const configPath = options.config ? resolve4(options.config) : resolveOpenCodeConfigPath();
2371
+ const pluginDir = resolve4(options.pluginDir || join5(opencodeDir, "plugin"));
2372
+ const pluginPath = join5(pluginDir, `${PROVIDER_ID}.js`);
2373
+ return { opencodeDir, configPath, pluginDir, pluginPath };
2374
+ }
2375
+ function resolvePluginSource() {
2376
+ const currentFile = fileURLToPath3(import.meta.url);
2377
+ const currentDir = dirname3(currentFile);
2378
+ const candidates = [
2379
+ join5(currentDir, "plugin-entry.js"),
2380
+ join5(currentDir, "..", "plugin-entry.js")
2381
+ ];
2382
+ for (const candidate of candidates) {
2383
+ if (existsSync6(candidate)) {
2384
+ return candidate;
2385
+ }
2386
+ }
2387
+ throw new Error("Unable to locate plugin-entry.js next to CLI distribution files");
2388
+ }
2389
+ function isErrnoException(error) {
2390
+ return typeof error === "object" && error !== null && "code" in error;
2391
+ }
2392
+ function readConfig(configPath) {
2393
+ if (!existsSync6(configPath)) {
2394
+ return { plugin: [], provider: {} };
2395
+ }
2396
+ let raw;
2397
+ try {
2398
+ raw = readFileSync2(configPath, "utf8");
2399
+ } catch (error) {
2400
+ if (isErrnoException(error) && error.code === "ENOENT") {
2401
+ return { plugin: [], provider: {} };
2402
+ }
2403
+ throw error;
2404
+ }
2405
+ try {
2406
+ return JSON.parse(raw);
2407
+ } catch (error) {
2408
+ throw new Error(`Invalid JSON in config: ${configPath} (${String(error)})`);
2409
+ }
2410
+ }
2411
+ function writeConfig(configPath, config, noBackup, silent = false) {
2412
+ mkdirSync2(dirname3(configPath), { recursive: true });
2413
+ if (!noBackup && existsSync6(configPath)) {
2414
+ const backupPath = `${configPath}.bak.${new Date().toISOString().replace(/[:]/g, "-")}`;
2415
+ copyFileSync(configPath, backupPath);
2416
+ if (!silent) {
2417
+ console.log(`Backup written: ${backupPath}`);
2418
+ }
2419
+ }
2420
+ writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
2421
+ `, "utf8");
2422
+ }
2423
+ function ensureProvider(config, baseUrl) {
2424
+ config.plugin = Array.isArray(config.plugin) ? config.plugin : [];
2425
+ if (!config.plugin.includes(PROVIDER_ID)) {
2426
+ config.plugin.push(PROVIDER_ID);
2427
+ }
2428
+ config.provider = config.provider && typeof config.provider === "object" ? config.provider : {};
2429
+ const current = config.provider[PROVIDER_ID] && typeof config.provider[PROVIDER_ID] === "object" ? config.provider[PROVIDER_ID] : {};
2430
+ const options = current.options && typeof current.options === "object" ? current.options : {};
2431
+ const models = current.models && typeof current.models === "object" ? current.models : {};
2432
+ config.provider[PROVIDER_ID] = {
2433
+ ...current,
2434
+ name: "Cursor",
2435
+ npm: "@ai-sdk/openai-compatible",
2436
+ options: {
2437
+ ...options,
2438
+ baseURL: baseUrl
2439
+ },
2440
+ models
2441
+ };
2442
+ }
2443
+ function ensurePluginLink(pluginSource, pluginPath, copyMode) {
2444
+ mkdirSync2(dirname3(pluginPath), { recursive: true });
2445
+ rmSync(pluginPath, { force: true });
2446
+ if (copyMode) {
2447
+ copyFileSync(pluginSource, pluginPath);
2448
+ return;
2449
+ }
2450
+ symlinkSync(pluginSource, pluginPath);
2451
+ }
2452
+ function discoverModelsSafe() {
2453
+ try {
2454
+ return discoverModelsFromCursorAgent();
2455
+ } catch (error) {
2456
+ const message = error instanceof Error ? error.message : String(error);
2457
+ console.warn(`Warning: cursor-agent models failed; using fallback models (${message})`);
2458
+ return fallbackModels();
2459
+ }
2460
+ }
2461
+ function syncModelsIntoProvider(config, options) {
2462
+ if (options.compact && !options.variants) {
2463
+ throw new Error("--compact requires --variants");
2464
+ }
2465
+ const discoveredModels = discoverModelsSafe();
2466
+ const provider = config.provider[PROVIDER_ID];
2467
+ const existingModels = provider.models && typeof provider.models === "object" ? provider.models : {};
2468
+ const beforeModels = snapshotModels(existingModels);
2469
+ const result = mergeCursorModelEntries(existingModels, discoveredModels, {
2470
+ variants: options.variants === true,
2471
+ compact: options.compact === true
2472
+ });
2473
+ provider.models = result.models;
2474
+ return {
2475
+ syncedCount: result.syncedCount,
2476
+ groupedCount: result.groupedCount,
2477
+ removedCount: result.removedCount,
2478
+ summary: summarizeModelSync(beforeModels, result.models)
2479
+ };
2480
+ }
2481
+ function explainCursorModels(models) {
2482
+ const grouped = groupCursorModels(models);
2483
+ const groupedCount = grouped.groups.reduce((total, group) => total + group.members.length, 0);
2484
+ return {
2485
+ modelCount: models.length,
2486
+ groupedCount,
2487
+ directCount: grouped.direct.length,
2488
+ groups: grouped.groups.map((group) => ({
2489
+ id: group.baseId,
2490
+ name: group.name,
2491
+ defaultCursorModel: group.defaultCursorModelId,
2492
+ memberCount: group.members.length,
2493
+ variants: group.variants
2494
+ })),
2495
+ direct: grouped.direct.map((model) => model.id)
2496
+ };
2497
+ }
2498
+ function createSyncJsonResult(result, options, configPath) {
2499
+ return {
2500
+ ...result,
2501
+ configPath,
2502
+ dryRun: options.dryRun === true,
2503
+ variants: options.variants === true,
2504
+ compact: options.compact === true
2505
+ };
2506
+ }
2507
+ function snapshotModels(models) {
2508
+ return JSON.parse(JSON.stringify(models));
2509
+ }
2510
+ function summarizeModelSync(beforeModels, afterModels) {
2511
+ let added = 0;
2512
+ let updated = 0;
2513
+ let removed = 0;
2514
+ let skipped = 0;
2515
+ for (const [modelId, afterEntry] of Object.entries(afterModels)) {
2516
+ if (!Object.prototype.hasOwnProperty.call(beforeModels, modelId)) {
2517
+ added++;
2518
+ continue;
2519
+ }
2520
+ if (JSON.stringify(beforeModels[modelId]) === JSON.stringify(afterEntry)) {
2521
+ skipped++;
2522
+ } else {
2523
+ updated++;
2524
+ }
2525
+ }
2526
+ for (const modelId of Object.keys(beforeModels)) {
2527
+ if (!Object.prototype.hasOwnProperty.call(afterModels, modelId)) {
2528
+ removed++;
2529
+ }
2530
+ }
2531
+ return {
2532
+ added,
2533
+ updated,
2534
+ removed,
2535
+ priced: countPricedModelEntries(afterModels),
2536
+ skipped
2537
+ };
2538
+ }
2539
+ function countPricedModelEntries(models) {
2540
+ let priced = 0;
2541
+ for (const entry of Object.values(models)) {
2542
+ if (!isRecord(entry))
2543
+ continue;
2544
+ if (isRecord(entry.cost))
2545
+ priced++;
2546
+ if (!isRecord(entry.variants))
2547
+ continue;
2548
+ for (const variantEntry of Object.values(entry.variants)) {
2549
+ if (isRecord(variantEntry) && isRecord(variantEntry.cost)) {
2550
+ priced++;
2551
+ }
2552
+ }
2553
+ }
2554
+ return priced;
2555
+ }
2556
+ function isRecord(value) {
2557
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2558
+ }
2559
+ function installAiSdk(opencodeDir) {
2560
+ try {
2561
+ execFileSync2("bun", ["install", "@ai-sdk/openai-compatible"], {
2562
+ cwd: opencodeDir,
2563
+ stdio: "inherit"
2564
+ });
2565
+ } catch (error) {
2566
+ const message = error instanceof Error ? error.message : String(error);
2567
+ console.warn(`Warning: failed to install @ai-sdk/openai-compatible via bun (${message})`);
2568
+ }
2569
+ }
2570
+ function commandInstall(options) {
2571
+ const { opencodeDir, configPath, pluginPath } = resolvePaths(options);
2572
+ const baseUrl = options.baseUrl || DEFAULT_BASE_URL;
2573
+ const copyMode = options.copy === true;
2574
+ const pluginSource = resolvePluginSource();
2575
+ if (!options.dryRun) {
2576
+ mkdirSync2(opencodeDir, { recursive: true });
2577
+ ensurePluginLink(pluginSource, pluginPath, copyMode);
2578
+ }
2579
+ const config = readConfig(configPath);
2580
+ ensureProvider(config, baseUrl);
2581
+ if (!options.skipModels) {
2582
+ const result = syncModelsIntoProvider(config, options);
2583
+ printSyncResult(result, options);
2584
+ }
2585
+ if (options.dryRun) {
2586
+ console.log("Dry run: no files changed.");
2587
+ } else {
2588
+ writeConfig(configPath, config, options.noBackup === true);
2589
+ installAiSdk(opencodeDir);
2590
+ }
2591
+ console.log(`${options.dryRun ? "Would install" : "Installed"} ${PROVIDER_ID}`);
2592
+ console.log(`Plugin path: ${pluginPath}${copyMode ? " (copy)" : " (symlink)"}`);
2593
+ console.log(`Config path: ${configPath}`);
2594
+ }
2595
+ function commandSyncModels(options) {
2596
+ const { configPath } = resolvePaths(options);
2597
+ const config = readConfig(configPath);
2598
+ ensureProvider(config, options.baseUrl || DEFAULT_BASE_URL);
2599
+ const result = syncModelsIntoProvider(config, options);
2600
+ if (!options.dryRun) {
2601
+ writeConfig(configPath, config, options.noBackup === true, options.json === true);
2602
+ }
2603
+ if (options.json) {
2604
+ console.log(JSON.stringify(createSyncJsonResult(result, options, configPath), null, 2));
2605
+ return;
2606
+ }
2607
+ printSyncResult(result, options);
2608
+ if (options.dryRun) {
2609
+ console.log("Dry run: no changes written.");
2610
+ }
2611
+ console.log(`Config path: ${configPath}`);
2612
+ }
2613
+ function commandModels(options) {
2614
+ const models = discoverModelsSafe();
2615
+ const explanation = explainCursorModels(models);
2616
+ if (options.json) {
2617
+ console.log(JSON.stringify(explanation, null, 2));
2618
+ return;
2619
+ }
2620
+ console.log(`Cursor models discovered: ${explanation.modelCount}`);
2621
+ console.log(`Grouped Cursor models: ${explanation.groupedCount}`);
2622
+ console.log(`Direct models: ${explanation.directCount}`);
2623
+ if (!options.explain) {
2624
+ return;
2625
+ }
2626
+ console.log("");
2627
+ console.log("Model groups:");
2628
+ for (const group of explanation.groups) {
2629
+ console.log(` ${group.id}`);
2630
+ console.log(` Default: ${group.defaultCursorModel}`);
2631
+ const variants = Object.entries(group.variants);
2632
+ if (variants.length === 0) {
2633
+ console.log(" Variants: none");
2634
+ continue;
2635
+ }
2636
+ console.log(" Variants:");
2637
+ for (const [variant, cursorModel] of variants) {
2638
+ console.log(` ${variant}: ${cursorModel}`);
2639
+ }
2640
+ }
2641
+ console.log("");
2642
+ console.log("Direct models:");
2643
+ for (const modelId of explanation.direct) {
2644
+ console.log(` ${modelId}`);
2645
+ }
2646
+ }
2647
+ function printSyncResult(result, options) {
2648
+ console.log(`Models synced: ${result.syncedCount}`);
2649
+ if (options.variants) {
2650
+ console.log(`Grouped Cursor models: ${result.groupedCount}`);
2651
+ }
2652
+ if (result.removedCount > 0) {
2653
+ console.log(`Raw grouped models removed: ${result.removedCount}`);
2654
+ }
2655
+ console.log("Sync summary:");
2656
+ console.log(` Added: ${result.summary.added}`);
2657
+ console.log(` Updated: ${result.summary.updated}`);
2658
+ console.log(` Removed: ${result.summary.removed}`);
2659
+ console.log(` Priced: ${result.summary.priced}`);
2660
+ console.log(` Skipped: ${result.summary.skipped}`);
2661
+ }
2662
+ var NPM_PACKAGE = "@evanovation/open-cursor";
2663
+ function commandUninstall(options) {
2664
+ const { configPath, pluginPath } = resolvePaths(options);
2665
+ rmSync(pluginPath, { force: true });
2666
+ if (existsSync6(configPath)) {
2667
+ const config = readConfig(configPath);
2668
+ if (Array.isArray(config.plugin)) {
2669
+ config.plugin = config.plugin.filter((name) => {
2670
+ if (name === PROVIDER_ID)
2671
+ return false;
2672
+ if (typeof name === "string" && name.startsWith(NPM_PACKAGE))
2673
+ return false;
2674
+ return true;
2675
+ });
2676
+ }
2677
+ if (config.provider && typeof config.provider === "object") {
2678
+ delete config.provider[PROVIDER_ID];
2679
+ }
2680
+ writeConfig(configPath, config, options.noBackup === true);
2681
+ }
2682
+ console.log(`Removed plugin link: ${pluginPath}`);
2683
+ console.log(`Removed provider "${PROVIDER_ID}" from ${configPath}`);
2684
+ }
2685
+ function getStatusResult(configPath, pluginPath) {
2686
+ let pluginType = "missing";
2687
+ let pluginTarget;
2688
+ if (existsSync6(pluginPath)) {
2689
+ try {
2690
+ const stat = lstatSync(pluginPath);
2691
+ pluginType = stat.isSymbolicLink() ? "symlink" : "file";
2692
+ if (pluginType === "symlink") {
2693
+ try {
2694
+ pluginTarget = readFileSync2(pluginPath, "utf8");
2695
+ } catch {
2696
+ pluginTarget = undefined;
2697
+ }
2698
+ }
2699
+ } catch (error) {
2700
+ if (!isErrnoException(error) || error.code !== "ENOENT") {
2701
+ throw error;
2702
+ }
2703
+ pluginType = "missing";
2704
+ pluginTarget = undefined;
2705
+ }
2706
+ }
2707
+ let config;
2708
+ let providerEnabled = false;
2709
+ let baseUrl = "http://127.0.0.1:32124/v1";
2710
+ let modelCount = 0;
2711
+ if (existsSync6(configPath)) {
2712
+ config = readConfig(configPath);
2713
+ const provider = config.provider?.["cursor-acp"];
2714
+ providerEnabled = !!provider;
2715
+ if (provider?.options?.baseURL) {
2716
+ baseUrl = provider.options.baseURL;
2717
+ }
2718
+ modelCount = Object.keys(provider?.models || {}).length;
2719
+ } else {
2720
+ config = undefined;
2721
+ }
2722
+ const opencodeDir = dirname3(configPath);
2723
+ const sdkPath = join5(opencodeDir, "node_modules", "@ai-sdk", "openai-compatible");
2724
+ const aiSdkInstalled = existsSync6(sdkPath);
2725
+ const sdkApiKeySource = resolveCliSdkAuthSource(config);
2726
+ const legacyCursorAuthFile = getPossibleAuthPaths().some((authPath) => existsSync6(authPath));
2727
+ let installMethod = "none";
2728
+ if (pluginType !== "missing") {
2729
+ installMethod = "symlink";
2730
+ } else if (isNpmDirectInstalled(config)) {
2731
+ installMethod = "npm-direct";
2732
+ }
2733
+ return {
2734
+ installMethod,
2735
+ plugin: {
2736
+ path: pluginPath,
2737
+ type: pluginType,
2738
+ target: pluginTarget
2739
+ },
2740
+ provider: {
2741
+ configPath,
2742
+ name: "cursor-acp",
2743
+ enabled: providerEnabled,
2744
+ baseUrl,
2745
+ modelCount
2746
+ },
2747
+ aiSdk: {
2748
+ installed: aiSdkInstalled
2749
+ },
2750
+ auth: {
2751
+ legacyCursorAuthFile,
2752
+ sdkApiKey: sdkApiKeySource !== undefined,
2753
+ sdkApiKeySource
2754
+ },
2755
+ runtime: getRuntimeStatus()
2756
+ };
2757
+ }
2758
+ function runDeepDoctorChecks(configPath) {
2759
+ const checks = [];
2760
+ let config;
2761
+ try {
2762
+ config = readConfig(configPath);
2763
+ } catch (error) {
2764
+ return [{
2765
+ name: "Deep config read",
2766
+ passed: false,
2767
+ message: error instanceof Error ? error.message : String(error)
2768
+ }];
2769
+ }
2770
+ const provider = config.provider?.[PROVIDER_ID];
2771
+ const models = isRecord(provider?.models) ? provider.models : {};
2772
+ const baseUrl = typeof provider?.options?.baseURL === "string" ? provider.options.baseURL : "";
2773
+ checks.push({
2774
+ name: "Provider base URL",
2775
+ passed: baseUrl.startsWith("http://") || baseUrl.startsWith("https://"),
2776
+ message: baseUrl || "missing - run: open-cursor install"
2777
+ });
2778
+ checks.push({
2779
+ name: "Provider models",
2780
+ passed: Object.keys(models).length > 0,
2781
+ message: `${Object.keys(models).length} configured model(s)`
2782
+ });
2783
+ const variantEntryCount = countVariantModelEntries(models);
2784
+ checks.push({
2785
+ name: "Compact variants",
2786
+ passed: variantEntryCount > 0,
2787
+ warning: variantEntryCount === 0,
2788
+ message: variantEntryCount > 0 ? `${variantEntryCount} model entr${variantEntryCount === 1 ? "y" : "ies"} with variants` : "no compact variants found - run: open-cursor sync-models --variants --compact"
2789
+ });
2790
+ let discoveredModels;
2791
+ try {
2792
+ discoveredModels = discoverModelsFromCursorAgent();
2793
+ checks.push({
2794
+ name: "Cursor model discovery",
2795
+ passed: true,
2796
+ message: `${discoveredModels.length} model(s) from cursor-agent`
2797
+ });
2798
+ } catch (error) {
2799
+ checks.push({
2800
+ name: "Cursor model discovery",
2801
+ passed: false,
2802
+ message: error instanceof Error ? error.message : String(error),
2803
+ warning: true
2804
+ });
2805
+ return checks;
2806
+ }
2807
+ const knownModelIds = new Set(discoveredModels.map((model) => model.id));
2808
+ const unknownTargets = collectConfiguredCursorModels(models).filter((modelId) => !knownModelIds.has(modelId));
2809
+ checks.push({
2810
+ name: "Configured Cursor model targets",
2811
+ passed: unknownTargets.length === 0,
2812
+ warning: unknownTargets.length > 0,
2813
+ message: unknownTargets.length === 0 ? "all configured targets exist in cursor-agent models" : `${unknownTargets.length} target(s) not found: ${unknownTargets.slice(0, 5).join(", ")}`
2814
+ });
2815
+ return checks;
2816
+ }
2817
+ function countVariantModelEntries(models) {
2818
+ return Object.values(models).filter((entry) => {
2819
+ return isRecord(entry) && isRecord(entry.variants) && Object.keys(entry.variants).length > 0;
2820
+ }).length;
2821
+ }
2822
+ function collectConfiguredCursorModels(models) {
2823
+ const targets = [];
2824
+ for (const [modelId, entry] of Object.entries(models)) {
2825
+ if (!isRecord(entry)) {
2826
+ targets.push(modelId);
2827
+ continue;
2828
+ }
2829
+ const optionTarget = readCursorModel(entry.options);
2830
+ targets.push(optionTarget || modelId);
2831
+ if (!isRecord(entry.variants))
2832
+ continue;
2833
+ for (const variantEntry of Object.values(entry.variants)) {
2834
+ const variantTarget = readCursorModel(variantEntry);
2835
+ if (variantTarget)
2836
+ targets.push(variantTarget);
2837
+ }
2838
+ }
2839
+ return [...new Set(targets)];
2840
+ }
2841
+ function readCursorModel(value) {
2842
+ if (!isRecord(value))
2843
+ return;
2844
+ const cursorModel = value.cursorModel;
2845
+ return typeof cursorModel === "string" && cursorModel.trim().length > 0 ? cursorModel.trim() : undefined;
2846
+ }
2847
+ function commandStatus(options) {
2848
+ const { configPath, pluginPath } = resolvePaths(options);
2849
+ const result = getStatusResult(configPath, pluginPath);
2850
+ if (options.json) {
2851
+ console.log(JSON.stringify(result, null, 2));
2852
+ return;
2853
+ }
2854
+ console.log("");
2855
+ console.log("Plugin");
2856
+ console.log(` Path: ${result.plugin.path}`);
2857
+ if (result.plugin.type === "symlink" && result.plugin.target) {
2858
+ console.log(` Type: symlink → ${result.plugin.target}`);
2859
+ } else if (result.plugin.type === "file") {
2860
+ console.log(` Type: file (copy)`);
2861
+ } else {
2862
+ console.log(` Type: missing`);
2863
+ }
2864
+ console.log(` Install method: ${result.installMethod}`);
2865
+ console.log("");
2866
+ console.log("Provider");
2867
+ console.log(` Config: ${result.provider.configPath}`);
2868
+ console.log(` Name: ${result.provider.name}`);
2869
+ console.log(` Enabled: ${result.provider.enabled ? "yes" : "no"}`);
2870
+ console.log(` Base URL: ${result.provider.baseUrl}`);
2871
+ console.log(` Models: ${result.provider.modelCount}`);
2872
+ console.log("");
2873
+ console.log("AI SDK");
2874
+ console.log(` @ai-sdk/openai-compatible: ${result.aiSdk.installed ? "installed" : "not installed"}`);
2875
+ console.log("");
2876
+ console.log("Authentication");
2877
+ console.log(` Legacy cursor-agent auth file: ${result.auth.legacyCursorAuthFile ? "found" : "not found"}`);
2878
+ console.log(` Cursor SDK API key: ${result.auth.sdkApiKey ? `found via ${result.auth.sdkApiKeySource}` : "not configured"}`);
2879
+ console.log("");
2880
+ console.log("Runtime");
2881
+ console.log(` Backend preference: ${result.runtime.backend.preference}`);
2882
+ console.log(` Agent pool: ${result.runtime.agentPool.enabled ? "enabled" : "disabled"}`);
2883
+ console.log(` Agent pool idle: ${result.runtime.agentPool.idleMs}ms`);
2884
+ console.log(` Session resume: ${result.runtime.sessionResume.enabled ? "enabled" : "disabled"}`);
2885
+ console.log(` Log level: ${result.runtime.logging.level}`);
2886
+ console.log(` Console logging: ${result.runtime.logging.console ? "enabled" : "disabled"}`);
2887
+ console.log(` Log dir: ${result.runtime.logging.dir}`);
2888
+ }
2889
+ function commandDoctor(options) {
2890
+ const { configPath, pluginPath } = resolvePaths(options);
2891
+ const checks = [
2892
+ ...runDoctorChecks(configPath, pluginPath),
2893
+ ...options.deep ? runDeepDoctorChecks(configPath) : []
2894
+ ];
2895
+ if (options.json) {
2896
+ const failed2 = checks.filter((c) => !c.passed && !c.warning);
2897
+ console.log(JSON.stringify({ deep: options.deep === true, checks, failed: failed2.length }, null, 2));
2898
+ return;
2899
+ }
2900
+ console.log("");
2901
+ for (const check of checks) {
2902
+ const symbol = check.passed ? "✓" : check.warning ? "⚠" : "✗";
2903
+ const color = check.passed ? "\x1B[32m" : check.warning ? "\x1B[33m" : "\x1B[31m";
2904
+ console.log(` ${color}${symbol}\x1B[0m ${check.name}: ${check.message}`);
2905
+ }
2906
+ const failed = checks.filter((c) => !c.passed && !c.warning);
2907
+ console.log("");
2908
+ if (failed.length === 0) {
2909
+ console.log("All checks passed!");
2910
+ } else {
2911
+ console.log(`${failed.length} check(s) failed. See messages above.`);
2912
+ }
2913
+ }
2914
+ function main() {
2915
+ let parsed;
2916
+ try {
2917
+ parsed = parseArgs(process.argv.slice(2));
2918
+ } catch (error) {
2919
+ const message = error instanceof Error ? error.message : String(error);
2920
+ console.error(message);
2921
+ printHelp();
2922
+ process.exit(1);
2923
+ return;
2924
+ }
2925
+ try {
2926
+ switch (parsed.command) {
2927
+ case "install":
2928
+ commandInstall(parsed.options);
2929
+ return;
2930
+ case "sync-models":
2931
+ commandSyncModels(parsed.options);
2932
+ return;
2933
+ case "models":
2934
+ commandModels(parsed.options);
2935
+ return;
2936
+ case "uninstall":
2937
+ commandUninstall(parsed.options);
2938
+ return;
2939
+ case "status":
2940
+ commandStatus(parsed.options);
2941
+ return;
2942
+ case "doctor":
2943
+ commandDoctor(parsed.options);
2944
+ return;
2945
+ case "help":
2946
+ printHelp();
2947
+ return;
2948
+ }
2949
+ } catch (error) {
2950
+ const message = error instanceof Error ? error.message : String(error);
2951
+ console.error(`Error: ${message}`);
2952
+ process.exit(1);
2953
+ }
2954
+ }
2955
+ function resolveEntrypointArg(argvPath) {
2956
+ if (!argvPath)
2957
+ return "";
2958
+ return resolve4(argvPath);
2959
+ }
2960
+ function toRealPath(path2) {
2961
+ try {
2962
+ return realpathSync(path2);
2963
+ } catch {
2964
+ return path2;
2965
+ }
2966
+ }
2967
+ function isCliEntrypoint(metaUrl, argvPath) {
2968
+ const currentPath = fileURLToPath3(metaUrl);
2969
+ const argvResolved = resolveEntrypointArg(argvPath);
2970
+ if (!argvResolved)
2971
+ return false;
2972
+ return currentPath === argvResolved || toRealPath(currentPath) === toRealPath(argvResolved);
2973
+ }
2974
+ if (isCliEntrypoint(import.meta.url, process.argv[1])) {
2975
+ main();
2976
+ }
2977
+ export {
2978
+ summarizeModelSync,
2979
+ runDoctorChecks,
2980
+ runDeepDoctorChecks,
2981
+ resolvePaths,
2982
+ isCliEntrypoint,
2983
+ getStatusResult,
2984
+ getBrandingHeader,
2985
+ explainCursorModels,
2986
+ checkCursorAgentLogin,
2987
+ checkCursorAgent,
2988
+ checkBun
2989
+ };