@rama_nigg/open-cursor 2.3.20 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -48
- package/dist/cli/discover.js +177 -8
- package/dist/cli/mcptool.js +6 -1
- package/dist/cli/opencode-cursor.js +930 -50
- package/dist/index.js +587 -227
- package/dist/plugin-entry.js +565 -207
- package/package.json +4 -2
- package/src/auth.ts +3 -1
- package/src/cli/model-discovery.ts +3 -2
- package/src/cli/opencode-cursor.ts +402 -23
- package/src/client/simple.ts +6 -3
- package/src/mcp/tool-bridge.ts +1 -1
- package/src/models/discovery.ts +3 -2
- package/src/models/pricing.ts +196 -0
- package/src/models/variants.ts +446 -0
- package/src/plugin-toggle.ts +7 -1
- package/src/plugin.ts +150 -32
- package/src/provider/boundary.ts +10 -0
- package/src/proxy/formatter.ts +30 -12
- package/src/streaming/types.ts +5 -0
- package/src/tools/defaults.ts +181 -1
- package/src/tools/executors/cli.ts +1 -0
- package/src/usage.ts +112 -0
- package/src/utils/binary.ts +57 -0
|
@@ -137,6 +137,173 @@ function formatErrorForUser(error) {
|
|
|
137
137
|
return output;
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
// src/utils/logger.ts
|
|
141
|
+
import * as fs from "node:fs";
|
|
142
|
+
import * as path from "node:path";
|
|
143
|
+
import * as os from "node:os";
|
|
144
|
+
function getConfiguredLevel() {
|
|
145
|
+
const env = process.env.CURSOR_ACP_LOG_LEVEL?.toLowerCase();
|
|
146
|
+
if (env && env in LEVEL_PRIORITY) {
|
|
147
|
+
return env;
|
|
148
|
+
}
|
|
149
|
+
return "info";
|
|
150
|
+
}
|
|
151
|
+
function isSilent() {
|
|
152
|
+
return process.env.CURSOR_ACP_LOG_SILENT === "1" || process.env.CURSOR_ACP_LOG_SILENT === "true";
|
|
153
|
+
}
|
|
154
|
+
function shouldLog(level) {
|
|
155
|
+
if (isSilent())
|
|
156
|
+
return false;
|
|
157
|
+
const configured = getConfiguredLevel();
|
|
158
|
+
return LEVEL_PRIORITY[level] >= LEVEL_PRIORITY[configured];
|
|
159
|
+
}
|
|
160
|
+
function formatMessage(level, component, message, data) {
|
|
161
|
+
const timestamp = new Date().toISOString();
|
|
162
|
+
const prefix = `[cursor-acp:${component}]`;
|
|
163
|
+
const levelTag = level.toUpperCase().padEnd(5);
|
|
164
|
+
let formatted = `${prefix} ${levelTag} ${message}`;
|
|
165
|
+
if (data !== undefined) {
|
|
166
|
+
if (typeof data === "object") {
|
|
167
|
+
formatted += ` ${JSON.stringify(data)}`;
|
|
168
|
+
} else {
|
|
169
|
+
formatted += ` ${data}`;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return formatted;
|
|
173
|
+
}
|
|
174
|
+
function isConsoleEnabled() {
|
|
175
|
+
const consoleEnv = process.env.CURSOR_ACP_LOG_CONSOLE;
|
|
176
|
+
return consoleEnv === "1" || consoleEnv === "true";
|
|
177
|
+
}
|
|
178
|
+
function ensureLogDir() {
|
|
179
|
+
if (logDirEnsured)
|
|
180
|
+
return;
|
|
181
|
+
try {
|
|
182
|
+
if (!fs.existsSync(LOG_DIR)) {
|
|
183
|
+
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
184
|
+
}
|
|
185
|
+
logDirEnsured = true;
|
|
186
|
+
} catch {
|
|
187
|
+
logFileError = true;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function rotateIfNeeded() {
|
|
191
|
+
try {
|
|
192
|
+
const stats = fs.statSync(LOG_FILE);
|
|
193
|
+
if (stats.size >= MAX_LOG_SIZE) {
|
|
194
|
+
const backupFile = LOG_FILE + ".1";
|
|
195
|
+
fs.renameSync(LOG_FILE, backupFile);
|
|
196
|
+
}
|
|
197
|
+
} catch {}
|
|
198
|
+
}
|
|
199
|
+
function writeToFile(message) {
|
|
200
|
+
if (logFileError)
|
|
201
|
+
return;
|
|
202
|
+
ensureLogDir();
|
|
203
|
+
if (logFileError)
|
|
204
|
+
return;
|
|
205
|
+
try {
|
|
206
|
+
rotateIfNeeded();
|
|
207
|
+
const timestamp = new Date().toISOString();
|
|
208
|
+
fs.appendFileSync(LOG_FILE, `${timestamp} ${message}
|
|
209
|
+
`);
|
|
210
|
+
} catch {
|
|
211
|
+
if (!logFileError) {
|
|
212
|
+
logFileError = true;
|
|
213
|
+
console.error(`[cursor-acp] Failed to write logs. Using: ${LOG_FILE}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function createLogger(component) {
|
|
218
|
+
return {
|
|
219
|
+
debug: (message, data) => {
|
|
220
|
+
if (!shouldLog("debug"))
|
|
221
|
+
return;
|
|
222
|
+
const formatted = formatMessage("debug", component, message, data);
|
|
223
|
+
writeToFile(formatted);
|
|
224
|
+
if (isConsoleEnabled())
|
|
225
|
+
console.error(formatted);
|
|
226
|
+
},
|
|
227
|
+
info: (message, data) => {
|
|
228
|
+
if (!shouldLog("info"))
|
|
229
|
+
return;
|
|
230
|
+
const formatted = formatMessage("info", component, message, data);
|
|
231
|
+
writeToFile(formatted);
|
|
232
|
+
if (isConsoleEnabled())
|
|
233
|
+
console.error(formatted);
|
|
234
|
+
},
|
|
235
|
+
warn: (message, data) => {
|
|
236
|
+
if (!shouldLog("warn"))
|
|
237
|
+
return;
|
|
238
|
+
const formatted = formatMessage("warn", component, message, data);
|
|
239
|
+
writeToFile(formatted);
|
|
240
|
+
if (isConsoleEnabled())
|
|
241
|
+
console.error(formatted);
|
|
242
|
+
},
|
|
243
|
+
error: (message, data) => {
|
|
244
|
+
if (!shouldLog("error"))
|
|
245
|
+
return;
|
|
246
|
+
const formatted = formatMessage("error", component, message, data);
|
|
247
|
+
writeToFile(formatted);
|
|
248
|
+
if (isConsoleEnabled())
|
|
249
|
+
console.error(formatted);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
var LOG_DIR, LOG_FILE, MAX_LOG_SIZE, LEVEL_PRIORITY, logDirEnsured = false, logFileError = false;
|
|
254
|
+
var init_logger = __esm(() => {
|
|
255
|
+
LOG_DIR = path.join(os.homedir(), ".opencode-cursor");
|
|
256
|
+
LOG_FILE = path.join(LOG_DIR, "plugin.log");
|
|
257
|
+
MAX_LOG_SIZE = 5 * 1024 * 1024;
|
|
258
|
+
LEVEL_PRIORITY = {
|
|
259
|
+
debug: 0,
|
|
260
|
+
info: 1,
|
|
261
|
+
warn: 2,
|
|
262
|
+
error: 3
|
|
263
|
+
};
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// src/utils/binary.ts
|
|
267
|
+
import { existsSync as fsExistsSync } from "fs";
|
|
268
|
+
import * as pathModule from "path";
|
|
269
|
+
import { homedir as osHomedir } from "os";
|
|
270
|
+
function resolveCursorAgentBinary(deps = {}) {
|
|
271
|
+
const platform = deps.platform ?? process.platform;
|
|
272
|
+
const env = deps.env ?? process.env;
|
|
273
|
+
const checkExists = deps.existsSync ?? fsExistsSync;
|
|
274
|
+
const home = (deps.homedir ?? osHomedir)();
|
|
275
|
+
const envOverride = env.CURSOR_AGENT_EXECUTABLE;
|
|
276
|
+
if (envOverride && envOverride.length > 0) {
|
|
277
|
+
return envOverride;
|
|
278
|
+
}
|
|
279
|
+
if (platform === "win32") {
|
|
280
|
+
const pathJoin = pathModule.win32.join;
|
|
281
|
+
const localAppData = env.LOCALAPPDATA ?? pathJoin(home, "AppData", "Local");
|
|
282
|
+
const knownPath = pathJoin(localAppData, "cursor-agent", "cursor-agent.cmd");
|
|
283
|
+
if (checkExists(knownPath)) {
|
|
284
|
+
return knownPath;
|
|
285
|
+
}
|
|
286
|
+
log.warn("cursor-agent not found at known Windows path, falling back to PATH", { checkedPath: knownPath });
|
|
287
|
+
return "cursor-agent.cmd";
|
|
288
|
+
}
|
|
289
|
+
const knownPaths = [
|
|
290
|
+
pathModule.join(home, ".cursor-agent", "cursor-agent"),
|
|
291
|
+
"/usr/local/bin/cursor-agent"
|
|
292
|
+
];
|
|
293
|
+
for (const p of knownPaths) {
|
|
294
|
+
if (checkExists(p)) {
|
|
295
|
+
return p;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
log.warn("cursor-agent not found at known paths, falling back to PATH", { checkedPaths: knownPaths });
|
|
299
|
+
return "cursor-agent";
|
|
300
|
+
}
|
|
301
|
+
var log;
|
|
302
|
+
var init_binary = __esm(() => {
|
|
303
|
+
init_logger();
|
|
304
|
+
log = createLogger("binary");
|
|
305
|
+
});
|
|
306
|
+
|
|
140
307
|
// src/cli/model-discovery.ts
|
|
141
308
|
import { execFileSync } from "child_process";
|
|
142
309
|
function parseCursorModelsOutput(output) {
|
|
@@ -160,9 +327,9 @@ function parseCursorModelsOutput(output) {
|
|
|
160
327
|
return models;
|
|
161
328
|
}
|
|
162
329
|
function discoverModelsFromCursorAgent() {
|
|
163
|
-
const raw = execFileSync(
|
|
330
|
+
const raw = execFileSync(resolveCursorAgentBinary(), ["models"], {
|
|
164
331
|
encoding: "utf8",
|
|
165
|
-
killSignal: "SIGTERM",
|
|
332
|
+
...process.platform !== "win32" && { killSignal: "SIGTERM" },
|
|
166
333
|
stdio: ["ignore", "pipe", "pipe"],
|
|
167
334
|
timeout: MODEL_DISCOVERY_TIMEOUT_MS
|
|
168
335
|
});
|
|
@@ -197,24 +364,453 @@ function fallbackModels() {
|
|
|
197
364
|
];
|
|
198
365
|
}
|
|
199
366
|
var MODEL_DISCOVERY_TIMEOUT_MS = 5000;
|
|
200
|
-
var init_model_discovery = () => {
|
|
367
|
+
var init_model_discovery = __esm(() => {
|
|
368
|
+
init_binary();
|
|
369
|
+
});
|
|
201
370
|
|
|
202
371
|
// src/cli/opencode-cursor.ts
|
|
203
372
|
init_model_discovery();
|
|
373
|
+
init_binary();
|
|
204
374
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
205
375
|
import {
|
|
206
376
|
copyFileSync,
|
|
207
|
-
existsSync,
|
|
377
|
+
existsSync as existsSync2,
|
|
208
378
|
lstatSync,
|
|
209
|
-
mkdirSync,
|
|
379
|
+
mkdirSync as mkdirSync2,
|
|
210
380
|
readFileSync,
|
|
211
381
|
rmSync,
|
|
212
382
|
symlinkSync,
|
|
213
383
|
writeFileSync
|
|
214
384
|
} from "fs";
|
|
215
|
-
import { homedir } from "os";
|
|
216
|
-
import { basename, dirname, join, resolve } from "path";
|
|
385
|
+
import { homedir as homedir2 } from "os";
|
|
386
|
+
import { basename, dirname, join as join3, resolve } from "path";
|
|
217
387
|
import { fileURLToPath } from "url";
|
|
388
|
+
|
|
389
|
+
// src/models/pricing.ts
|
|
390
|
+
var AUTO_COST = cost(1.25, 6, 0.25, 1.25);
|
|
391
|
+
var COMPOSER_2_COST = cost(0.5, 2.5, 0.2, 0.5);
|
|
392
|
+
var COMPOSER_2_FAST_COST = cost(1.5, 7.5, 0.35, 1.5);
|
|
393
|
+
var COMPOSER_1_5_COST = cost(3.5, 17.5, 0.35, 3.5);
|
|
394
|
+
var CLAUDE_SONNET_COST = cost(3, 15, 0.3, 3.75);
|
|
395
|
+
var CLAUDE_SONNET_LONG_CONTEXT_COST = cost(6, 22.5, 0.6, 7.5);
|
|
396
|
+
var CLAUDE_SONNET_WITH_LONG_CONTEXT_COST = withLongContext(CLAUDE_SONNET_COST, CLAUDE_SONNET_LONG_CONTEXT_COST);
|
|
397
|
+
var CLAUDE_OPUS_COST = cost(5, 25, 0.5, 6.25);
|
|
398
|
+
var CLAUDE_OPUS_FAST_COST = cost(30, 150, 3, 37.5);
|
|
399
|
+
var GEMINI_3_PRO_COST = withLongContext(cost(2, 12, 0.2, 2), cost(4, 18, 0.4, 4));
|
|
400
|
+
var GEMINI_3_FLASH_COST = cost(0.5, 3, 0.05, 0.5);
|
|
401
|
+
var GPT_5_1_COST = cost(1.25, 10, 0.125, 1.25);
|
|
402
|
+
var GPT_5_2_COST = cost(1.75, 14, 0.175, 1.75);
|
|
403
|
+
var GPT_5_3_CODEX_COST = cost(1.75, 14, 0.175, 1.75);
|
|
404
|
+
var GPT_5_4_COST = withLongContext(cost(2.5, 15, 0.25, 2.5), cost(5, 22.5, 0.5, 5));
|
|
405
|
+
var GPT_5_4_FAST_COST = cost(5, 30, 0.5, 5);
|
|
406
|
+
var GPT_5_4_MINI_COST = cost(0.75, 4.5, 0.075, 0.75);
|
|
407
|
+
var GPT_5_4_NANO_COST = cost(0.2, 1.25, 0.02, 0.2);
|
|
408
|
+
var GPT_5_5_COST = withLongContext(cost(5, 30, 0.5, 5), cost(10, 45, 1, 10));
|
|
409
|
+
var GPT_5_MINI_COST = cost(0.25, 2, 0.025, 0.25);
|
|
410
|
+
var GROK_4_20_COST = withLongContext(cost(2, 6, 0.2, 2), cost(4, 12, 0.4, 4));
|
|
411
|
+
var KIMI_K2_5_COST = cost(0.6, 3, 0.1, 0.6);
|
|
412
|
+
function getCursorModelCost(modelId) {
|
|
413
|
+
if (modelId === "auto")
|
|
414
|
+
return AUTO_COST;
|
|
415
|
+
if (modelId === "composer-2-fast")
|
|
416
|
+
return COMPOSER_2_FAST_COST;
|
|
417
|
+
if (modelId === "composer-2")
|
|
418
|
+
return COMPOSER_2_COST;
|
|
419
|
+
if (modelId === "composer-1.5")
|
|
420
|
+
return COMPOSER_1_5_COST;
|
|
421
|
+
if (modelId.startsWith("claude-opus-4-7"))
|
|
422
|
+
return CLAUDE_OPUS_COST;
|
|
423
|
+
if (modelId.startsWith("claude-4.6-opus")) {
|
|
424
|
+
return modelId.endsWith("-fast") ? CLAUDE_OPUS_FAST_COST : CLAUDE_OPUS_COST;
|
|
425
|
+
}
|
|
426
|
+
if (modelId.startsWith("claude-4.5-opus"))
|
|
427
|
+
return CLAUDE_OPUS_COST;
|
|
428
|
+
if (modelId.startsWith("claude-4.6-sonnet"))
|
|
429
|
+
return CLAUDE_SONNET_WITH_LONG_CONTEXT_COST;
|
|
430
|
+
if (modelId.startsWith("claude-4.5-sonnet"))
|
|
431
|
+
return CLAUDE_SONNET_WITH_LONG_CONTEXT_COST;
|
|
432
|
+
if (modelId.startsWith("claude-4-sonnet"))
|
|
433
|
+
return CLAUDE_SONNET_COST;
|
|
434
|
+
if (modelId === "gemini-3.1-pro")
|
|
435
|
+
return GEMINI_3_PRO_COST;
|
|
436
|
+
if (modelId === "gemini-3-flash")
|
|
437
|
+
return GEMINI_3_FLASH_COST;
|
|
438
|
+
if (modelId.startsWith("gpt-5.5"))
|
|
439
|
+
return GPT_5_5_COST;
|
|
440
|
+
if (modelId.startsWith("gpt-5.4-mini"))
|
|
441
|
+
return GPT_5_4_MINI_COST;
|
|
442
|
+
if (modelId.startsWith("gpt-5.4-nano"))
|
|
443
|
+
return GPT_5_4_NANO_COST;
|
|
444
|
+
if (modelId.startsWith("gpt-5.4")) {
|
|
445
|
+
return modelId.endsWith("-fast") ? GPT_5_4_FAST_COST : GPT_5_4_COST;
|
|
446
|
+
}
|
|
447
|
+
if (modelId.startsWith("gpt-5.3-codex"))
|
|
448
|
+
return GPT_5_3_CODEX_COST;
|
|
449
|
+
if (modelId.startsWith("gpt-5.2-codex"))
|
|
450
|
+
return GPT_5_2_COST;
|
|
451
|
+
if (modelId.startsWith("gpt-5.2"))
|
|
452
|
+
return GPT_5_2_COST;
|
|
453
|
+
if (modelId.startsWith("gpt-5.1-codex-mini"))
|
|
454
|
+
return GPT_5_MINI_COST;
|
|
455
|
+
if (modelId.startsWith("gpt-5.1-codex-max"))
|
|
456
|
+
return GPT_5_1_COST;
|
|
457
|
+
if (modelId.startsWith("gpt-5.1"))
|
|
458
|
+
return GPT_5_1_COST;
|
|
459
|
+
if (modelId === "gpt-5-mini")
|
|
460
|
+
return GPT_5_MINI_COST;
|
|
461
|
+
if (modelId.startsWith("grok-4-20"))
|
|
462
|
+
return GROK_4_20_COST;
|
|
463
|
+
if (modelId === "kimi-k2.5")
|
|
464
|
+
return KIMI_K2_5_COST;
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
function cost(input, output, cacheRead, cacheWrite) {
|
|
468
|
+
return {
|
|
469
|
+
input,
|
|
470
|
+
output,
|
|
471
|
+
cache_read: cacheRead,
|
|
472
|
+
cache_write: cacheWrite
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
function withLongContext(base, longContext) {
|
|
476
|
+
return {
|
|
477
|
+
...base,
|
|
478
|
+
context_over_200k: longContext
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// src/models/variants.ts
|
|
483
|
+
var DEFAULT_VARIANT_ORDER = [
|
|
484
|
+
null,
|
|
485
|
+
"medium",
|
|
486
|
+
"high",
|
|
487
|
+
"low",
|
|
488
|
+
"none",
|
|
489
|
+
"xhigh",
|
|
490
|
+
"max"
|
|
491
|
+
];
|
|
492
|
+
var VARIANT_DISPLAY_ORDER = [
|
|
493
|
+
"none",
|
|
494
|
+
"low",
|
|
495
|
+
"low-fast",
|
|
496
|
+
"fast",
|
|
497
|
+
"medium",
|
|
498
|
+
"medium-fast",
|
|
499
|
+
"medium-thinking",
|
|
500
|
+
"high",
|
|
501
|
+
"high-fast",
|
|
502
|
+
"high-thinking",
|
|
503
|
+
"high-thinking-fast",
|
|
504
|
+
"xhigh",
|
|
505
|
+
"xhigh-fast",
|
|
506
|
+
"max",
|
|
507
|
+
"max-thinking",
|
|
508
|
+
"max-thinking-fast",
|
|
509
|
+
"thinking",
|
|
510
|
+
"thinking-low",
|
|
511
|
+
"thinking-medium",
|
|
512
|
+
"thinking-high",
|
|
513
|
+
"thinking-high-fast",
|
|
514
|
+
"thinking-xhigh",
|
|
515
|
+
"thinking-max",
|
|
516
|
+
"extra-high",
|
|
517
|
+
"spark-preview",
|
|
518
|
+
"spark-preview-low",
|
|
519
|
+
"spark-preview-medium",
|
|
520
|
+
"spark-preview-high",
|
|
521
|
+
"spark-preview-xhigh"
|
|
522
|
+
];
|
|
523
|
+
function isSafeBaseId(baseId) {
|
|
524
|
+
const parts = baseId.split("-").filter(Boolean);
|
|
525
|
+
if (parts.length < 2)
|
|
526
|
+
return false;
|
|
527
|
+
if (baseId === "gpt-5")
|
|
528
|
+
return false;
|
|
529
|
+
return true;
|
|
530
|
+
}
|
|
531
|
+
function generateBaseCandidates(modelId) {
|
|
532
|
+
const tokens = modelId.split("-");
|
|
533
|
+
const candidates = [];
|
|
534
|
+
for (let i = tokens.length - 1;i >= 1; i--) {
|
|
535
|
+
const prefix = tokens.slice(0, i).join("-");
|
|
536
|
+
if (isSafeBaseId(prefix))
|
|
537
|
+
candidates.push(prefix);
|
|
538
|
+
}
|
|
539
|
+
return candidates;
|
|
540
|
+
}
|
|
541
|
+
function computeStats(candidate, modelIds) {
|
|
542
|
+
const prefix = `${candidate}-`;
|
|
543
|
+
const firstTokens = new Set;
|
|
544
|
+
let count = 0;
|
|
545
|
+
for (const otherId of modelIds) {
|
|
546
|
+
if (!otherId.startsWith(prefix))
|
|
547
|
+
continue;
|
|
548
|
+
count++;
|
|
549
|
+
const firstToken = otherId.slice(prefix.length).split("-", 1)[0];
|
|
550
|
+
if (firstToken)
|
|
551
|
+
firstTokens.add(firstToken);
|
|
552
|
+
}
|
|
553
|
+
return { count, diversity: firstTokens.size };
|
|
554
|
+
}
|
|
555
|
+
function chooseBase(modelId, knownModelIds, modelIds) {
|
|
556
|
+
const candidates = generateBaseCandidates(modelId);
|
|
557
|
+
if (candidates.length === 0)
|
|
558
|
+
return null;
|
|
559
|
+
const stats = new Map;
|
|
560
|
+
for (const candidate of candidates) {
|
|
561
|
+
stats.set(candidate, computeStats(candidate, modelIds));
|
|
562
|
+
}
|
|
563
|
+
let stepA = null;
|
|
564
|
+
for (const candidate of candidates) {
|
|
565
|
+
if (!knownModelIds.has(candidate))
|
|
566
|
+
continue;
|
|
567
|
+
const stat = stats.get(candidate);
|
|
568
|
+
if (!stat || stat.count < 2 || stat.diversity < 2)
|
|
569
|
+
continue;
|
|
570
|
+
if (stepA === null || candidate.length < stepA.length)
|
|
571
|
+
stepA = candidate;
|
|
572
|
+
}
|
|
573
|
+
if (stepA !== null)
|
|
574
|
+
return stepA;
|
|
575
|
+
let stepB = null;
|
|
576
|
+
for (const candidate of candidates) {
|
|
577
|
+
const stat = stats.get(candidate);
|
|
578
|
+
if (!stat || stat.count < 2)
|
|
579
|
+
continue;
|
|
580
|
+
if (stepB === null || stat.diversity > stepB.diversity || stat.diversity === stepB.diversity && candidate.length > stepB.base.length) {
|
|
581
|
+
stepB = { base: candidate, diversity: stat.diversity };
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
if (stepB !== null)
|
|
585
|
+
return stepB.base;
|
|
586
|
+
let stepC = null;
|
|
587
|
+
for (const candidate of candidates) {
|
|
588
|
+
if (!knownModelIds.has(candidate))
|
|
589
|
+
continue;
|
|
590
|
+
if (stepC === null || candidate.length < stepC.length)
|
|
591
|
+
stepC = candidate;
|
|
592
|
+
}
|
|
593
|
+
return stepC;
|
|
594
|
+
}
|
|
595
|
+
function getDefaultMember(members) {
|
|
596
|
+
for (const variant of DEFAULT_VARIANT_ORDER) {
|
|
597
|
+
const member = members.find((candidate) => candidate.variant === variant);
|
|
598
|
+
if (member)
|
|
599
|
+
return member;
|
|
600
|
+
}
|
|
601
|
+
return members[0];
|
|
602
|
+
}
|
|
603
|
+
function formatModelName(modelId) {
|
|
604
|
+
return modelId.split("-").map((part) => {
|
|
605
|
+
if (part === "gpt")
|
|
606
|
+
return "GPT";
|
|
607
|
+
if (part === "xhigh")
|
|
608
|
+
return "XHigh";
|
|
609
|
+
return part.charAt(0).toUpperCase() + part.slice(1);
|
|
610
|
+
}).join(" ");
|
|
611
|
+
}
|
|
612
|
+
function compareVariants(a, b) {
|
|
613
|
+
if (a.variant === null)
|
|
614
|
+
return -1;
|
|
615
|
+
if (b.variant === null)
|
|
616
|
+
return 1;
|
|
617
|
+
const aIndex = VARIANT_DISPLAY_ORDER.indexOf(a.variant);
|
|
618
|
+
const bIndex = VARIANT_DISPLAY_ORDER.indexOf(b.variant);
|
|
619
|
+
if (aIndex !== -1 && bIndex !== -1)
|
|
620
|
+
return aIndex - bIndex;
|
|
621
|
+
if (aIndex !== -1)
|
|
622
|
+
return -1;
|
|
623
|
+
if (bIndex !== -1)
|
|
624
|
+
return 1;
|
|
625
|
+
return a.variant.localeCompare(b.variant);
|
|
626
|
+
}
|
|
627
|
+
function createGroup(baseId, members) {
|
|
628
|
+
const defaultMember = getDefaultMember(members);
|
|
629
|
+
const variants = {};
|
|
630
|
+
for (const member of [...members].sort(compareVariants)) {
|
|
631
|
+
if (member.variant) {
|
|
632
|
+
variants[member.variant] = member.cursorModelId;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
return {
|
|
636
|
+
baseId,
|
|
637
|
+
name: defaultMember.variant === null ? defaultMember.name : formatModelName(baseId),
|
|
638
|
+
defaultCursorModelId: defaultMember.cursorModelId,
|
|
639
|
+
variants,
|
|
640
|
+
members
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
function groupCursorModels(models) {
|
|
644
|
+
const knownModelIds = new Set(models.map((model) => model.id));
|
|
645
|
+
const modelIds = models.map((model) => model.id);
|
|
646
|
+
const preferredBase = new Map;
|
|
647
|
+
for (const model of models) {
|
|
648
|
+
const base = chooseBase(model.id, knownModelIds, modelIds);
|
|
649
|
+
if (base)
|
|
650
|
+
preferredBase.set(model.id, base);
|
|
651
|
+
}
|
|
652
|
+
const baseSet = new Set(preferredBase.values());
|
|
653
|
+
const groupMembers = new Map;
|
|
654
|
+
const groupOrder = [];
|
|
655
|
+
const recordMember = (baseId, member) => {
|
|
656
|
+
const existing = groupMembers.get(baseId);
|
|
657
|
+
if (existing) {
|
|
658
|
+
existing.push(member);
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
groupMembers.set(baseId, [member]);
|
|
662
|
+
groupOrder.push(baseId);
|
|
663
|
+
};
|
|
664
|
+
for (const model of models) {
|
|
665
|
+
if (baseSet.has(model.id) && knownModelIds.has(model.id)) {
|
|
666
|
+
recordMember(model.id, {
|
|
667
|
+
baseId: model.id,
|
|
668
|
+
variant: null,
|
|
669
|
+
cursorModelId: model.id,
|
|
670
|
+
name: model.name
|
|
671
|
+
});
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
const base = preferredBase.get(model.id);
|
|
675
|
+
if (!base)
|
|
676
|
+
continue;
|
|
677
|
+
recordMember(base, {
|
|
678
|
+
baseId: base,
|
|
679
|
+
variant: model.id.slice(base.length + 1),
|
|
680
|
+
cursorModelId: model.id,
|
|
681
|
+
name: model.name
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
const groupedIds = new Set;
|
|
685
|
+
const groups = [];
|
|
686
|
+
for (const baseId of groupOrder) {
|
|
687
|
+
const members = groupMembers.get(baseId);
|
|
688
|
+
if (!members || members.length < 2)
|
|
689
|
+
continue;
|
|
690
|
+
groups.push(createGroup(baseId, members));
|
|
691
|
+
for (const member of members)
|
|
692
|
+
groupedIds.add(member.cursorModelId);
|
|
693
|
+
}
|
|
694
|
+
const direct = [];
|
|
695
|
+
for (const model of models) {
|
|
696
|
+
if (groupedIds.has(model.id))
|
|
697
|
+
continue;
|
|
698
|
+
direct.push(model);
|
|
699
|
+
}
|
|
700
|
+
return { groups, direct };
|
|
701
|
+
}
|
|
702
|
+
function createVariantModelEntries(models) {
|
|
703
|
+
const { groups, direct } = groupCursorModels(models);
|
|
704
|
+
const entries = {};
|
|
705
|
+
const groupedModelIds = new Set;
|
|
706
|
+
for (const group of groups) {
|
|
707
|
+
const variants = {};
|
|
708
|
+
for (const [variant, cursorModel] of Object.entries(group.variants)) {
|
|
709
|
+
const variantEntry = { cursorModel };
|
|
710
|
+
const variantCost = getCursorModelCost(cursorModel);
|
|
711
|
+
if (variantCost)
|
|
712
|
+
variantEntry.cost = variantCost;
|
|
713
|
+
variants[variant] = variantEntry;
|
|
714
|
+
}
|
|
715
|
+
const groupEntry = {
|
|
716
|
+
name: group.name,
|
|
717
|
+
options: {
|
|
718
|
+
cursorModel: group.defaultCursorModelId
|
|
719
|
+
},
|
|
720
|
+
variants
|
|
721
|
+
};
|
|
722
|
+
const defaultCost = getCursorModelCost(group.defaultCursorModelId);
|
|
723
|
+
if (defaultCost)
|
|
724
|
+
groupEntry.cost = defaultCost;
|
|
725
|
+
entries[group.baseId] = groupEntry;
|
|
726
|
+
for (const member of group.members) {
|
|
727
|
+
groupedModelIds.add(member.cursorModelId);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
for (const model of direct) {
|
|
731
|
+
const entry = { name: model.name };
|
|
732
|
+
const directCost = getCursorModelCost(model.id);
|
|
733
|
+
if (directCost)
|
|
734
|
+
entry.cost = directCost;
|
|
735
|
+
entries[model.id] = entry;
|
|
736
|
+
}
|
|
737
|
+
return { entries, groupedModelIds };
|
|
738
|
+
}
|
|
739
|
+
function mergeCursorModelEntries(existingModels, discoveredModels, options) {
|
|
740
|
+
if (!options.variants) {
|
|
741
|
+
return mergeDirectModelEntries(existingModels, discoveredModels);
|
|
742
|
+
}
|
|
743
|
+
const { entries, groupedModelIds } = createVariantModelEntries(discoveredModels);
|
|
744
|
+
const models = { ...existingModels };
|
|
745
|
+
let removedCount = 0;
|
|
746
|
+
if (options.compact) {
|
|
747
|
+
for (const modelId of groupedModelIds) {
|
|
748
|
+
if (!Object.prototype.hasOwnProperty.call(models, modelId))
|
|
749
|
+
continue;
|
|
750
|
+
if (Object.prototype.hasOwnProperty.call(entries, modelId))
|
|
751
|
+
continue;
|
|
752
|
+
delete models[modelId];
|
|
753
|
+
removedCount++;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
for (const [modelId, entry] of Object.entries(entries)) {
|
|
757
|
+
models[modelId] = mergeEntryPreservingUserFields(models[modelId], entry);
|
|
758
|
+
}
|
|
759
|
+
return {
|
|
760
|
+
models,
|
|
761
|
+
syncedCount: Object.keys(entries).length,
|
|
762
|
+
groupedCount: groupedModelIds.size,
|
|
763
|
+
removedCount
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
function mergeDirectModelEntries(existingModels, discoveredModels) {
|
|
767
|
+
const models = { ...existingModels };
|
|
768
|
+
for (const model of discoveredModels) {
|
|
769
|
+
const generated = { name: model.name };
|
|
770
|
+
const directCost = getCursorModelCost(model.id);
|
|
771
|
+
if (directCost)
|
|
772
|
+
generated.cost = directCost;
|
|
773
|
+
models[model.id] = mergeEntryPreservingUserFields(models[model.id], generated);
|
|
774
|
+
}
|
|
775
|
+
return {
|
|
776
|
+
models,
|
|
777
|
+
syncedCount: discoveredModels.length,
|
|
778
|
+
groupedCount: 0,
|
|
779
|
+
removedCount: 0
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
function isPlainObject(value) {
|
|
783
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
784
|
+
}
|
|
785
|
+
function mergeEntryPreservingUserFields(existing, generated) {
|
|
786
|
+
if (!isPlainObject(existing))
|
|
787
|
+
return generated;
|
|
788
|
+
const merged = { ...existing, ...generated };
|
|
789
|
+
if (existing.cost !== undefined) {
|
|
790
|
+
merged.cost = existing.cost;
|
|
791
|
+
}
|
|
792
|
+
if (isPlainObject(existing.variants) && isPlainObject(generated.variants)) {
|
|
793
|
+
const mergedVariants = { ...generated.variants };
|
|
794
|
+
for (const [variantKey, existingVariant] of Object.entries(existing.variants)) {
|
|
795
|
+
const generatedVariant = generated.variants[variantKey];
|
|
796
|
+
if (!isPlainObject(existingVariant))
|
|
797
|
+
continue;
|
|
798
|
+
if (!isPlainObject(generatedVariant)) {
|
|
799
|
+
mergedVariants[variantKey] = existingVariant;
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
const variantMerged = { ...generatedVariant };
|
|
803
|
+
if (existingVariant.cost !== undefined) {
|
|
804
|
+
variantMerged.cost = existingVariant.cost;
|
|
805
|
+
}
|
|
806
|
+
mergedVariants[variantKey] = variantMerged;
|
|
807
|
+
}
|
|
808
|
+
merged.variants = mergedVariants;
|
|
809
|
+
}
|
|
810
|
+
return merged;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// src/cli/opencode-cursor.ts
|
|
218
814
|
var BRANDING_HEADER = `
|
|
219
815
|
▄▄▄ ▄▄▄▄ ▄▄▄▄▄ ▄▄ ▄▄ ▄▄▄ ▄▄ ▄▄ ▄▄▄▄ ▄▄▄▄ ▄▄▄ ▄▄▄▄
|
|
220
816
|
██ ██ ██ ██ ██▄▄ ███▄██ ▄▄▄ ██ ▀▀ ██ ██ ██ ██ ██▄▄▄ ██ ██ ██ ██
|
|
@@ -237,7 +833,7 @@ function checkBun() {
|
|
|
237
833
|
}
|
|
238
834
|
function checkCursorAgent() {
|
|
239
835
|
try {
|
|
240
|
-
const output = execFileSync2(
|
|
836
|
+
const output = execFileSync2(resolveCursorAgentBinary(), ["--version"], { encoding: "utf8" }).trim();
|
|
241
837
|
const version = output.split(`
|
|
242
838
|
`)[0] || "installed";
|
|
243
839
|
return { name: "cursor-agent", passed: true, message: version };
|
|
@@ -251,7 +847,11 @@ function checkCursorAgent() {
|
|
|
251
847
|
}
|
|
252
848
|
function checkCursorAgentLogin() {
|
|
253
849
|
try {
|
|
254
|
-
execFileSync2(
|
|
850
|
+
execFileSync2(resolveCursorAgentBinary(), ["models"], {
|
|
851
|
+
encoding: "utf8",
|
|
852
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
853
|
+
timeout: 3000
|
|
854
|
+
});
|
|
255
855
|
return { name: "cursor-agent login", passed: true, message: "logged in" };
|
|
256
856
|
} catch {
|
|
257
857
|
return {
|
|
@@ -284,7 +884,7 @@ function isNpmDirectInstalled(config) {
|
|
|
284
884
|
}
|
|
285
885
|
function checkPluginFile(pluginPath, config) {
|
|
286
886
|
try {
|
|
287
|
-
if (!
|
|
887
|
+
if (!existsSync2(pluginPath)) {
|
|
288
888
|
if (isNpmDirectInstalled(config)) {
|
|
289
889
|
return {
|
|
290
890
|
name: "Plugin file",
|
|
@@ -314,7 +914,7 @@ function checkPluginFile(pluginPath, config) {
|
|
|
314
914
|
}
|
|
315
915
|
function checkProviderConfig(configPath) {
|
|
316
916
|
try {
|
|
317
|
-
if (!
|
|
917
|
+
if (!existsSync2(configPath)) {
|
|
318
918
|
return {
|
|
319
919
|
name: "Provider config",
|
|
320
920
|
passed: false,
|
|
@@ -342,8 +942,8 @@ function checkProviderConfig(configPath) {
|
|
|
342
942
|
}
|
|
343
943
|
function checkAiSdk(opencodeDir) {
|
|
344
944
|
try {
|
|
345
|
-
const sdkPath =
|
|
346
|
-
if (
|
|
945
|
+
const sdkPath = join3(opencodeDir, "node_modules", "@ai-sdk", "openai-compatible");
|
|
946
|
+
if (existsSync2(sdkPath)) {
|
|
347
947
|
return { name: "AI SDK", passed: true, message: "@ai-sdk/openai-compatible installed" };
|
|
348
948
|
}
|
|
349
949
|
return {
|
|
@@ -388,6 +988,7 @@ function printHelp() {
|
|
|
388
988
|
Commands:
|
|
389
989
|
install Configure OpenCode for Cursor (idempotent, safe to re-run)
|
|
390
990
|
sync-models Refresh model list from cursor-agent
|
|
991
|
+
models Explain discovered Cursor model groups and variants
|
|
391
992
|
status Show current configuration state
|
|
392
993
|
doctor Diagnose common issues
|
|
393
994
|
uninstall Remove cursor-acp from OpenCode config
|
|
@@ -399,8 +1000,13 @@ Options:
|
|
|
399
1000
|
--base-url <url> Proxy base URL (default: http://127.0.0.1:32124/v1)
|
|
400
1001
|
--copy Copy plugin instead of symlink
|
|
401
1002
|
--skip-models Skip model sync during install
|
|
1003
|
+
--variants Generate compact OpenCode model variants from Cursor models
|
|
1004
|
+
--compact With --variants, remove raw grouped Cursor model entries
|
|
1005
|
+
--dry-run Preview sync/install config changes without writing files
|
|
1006
|
+
--deep Run extra doctor checks for models and variant config
|
|
1007
|
+
--explain Show model grouping explanation (models command)
|
|
402
1008
|
--no-backup Don't create config backup
|
|
403
|
-
--json Output in JSON format
|
|
1009
|
+
--json Output in JSON format where supported
|
|
404
1010
|
`);
|
|
405
1011
|
}
|
|
406
1012
|
function parseArgs(argv) {
|
|
@@ -413,6 +1019,16 @@ function parseArgs(argv) {
|
|
|
413
1019
|
options.copy = true;
|
|
414
1020
|
} else if (arg === "--skip-models") {
|
|
415
1021
|
options.skipModels = true;
|
|
1022
|
+
} else if (arg === "--variants") {
|
|
1023
|
+
options.variants = true;
|
|
1024
|
+
} else if (arg === "--compact") {
|
|
1025
|
+
options.compact = true;
|
|
1026
|
+
} else if (arg === "--dry-run") {
|
|
1027
|
+
options.dryRun = true;
|
|
1028
|
+
} else if (arg === "--deep") {
|
|
1029
|
+
options.deep = true;
|
|
1030
|
+
} else if (arg === "--explain") {
|
|
1031
|
+
options.explain = true;
|
|
416
1032
|
} else if (arg === "--no-backup") {
|
|
417
1033
|
options.noBackup = true;
|
|
418
1034
|
} else if (arg === "--config" && rest[i + 1]) {
|
|
@@ -436,6 +1052,7 @@ function normalizeCommand(value) {
|
|
|
436
1052
|
switch ((value || "help").toLowerCase()) {
|
|
437
1053
|
case "install":
|
|
438
1054
|
case "sync-models":
|
|
1055
|
+
case "models":
|
|
439
1056
|
case "uninstall":
|
|
440
1057
|
case "status":
|
|
441
1058
|
case "doctor":
|
|
@@ -449,24 +1066,24 @@ function getConfigHome() {
|
|
|
449
1066
|
const xdg = process.env.XDG_CONFIG_HOME;
|
|
450
1067
|
if (xdg && xdg.length > 0)
|
|
451
1068
|
return xdg;
|
|
452
|
-
return
|
|
1069
|
+
return join3(homedir2(), ".config");
|
|
453
1070
|
}
|
|
454
1071
|
function resolvePaths(options) {
|
|
455
|
-
const opencodeDir =
|
|
456
|
-
const configPath = resolve(options.config ||
|
|
457
|
-
const pluginDir = resolve(options.pluginDir ||
|
|
458
|
-
const pluginPath =
|
|
1072
|
+
const opencodeDir = join3(getConfigHome(), "opencode");
|
|
1073
|
+
const configPath = resolve(options.config || join3(opencodeDir, "opencode.json"));
|
|
1074
|
+
const pluginDir = resolve(options.pluginDir || join3(opencodeDir, "plugin"));
|
|
1075
|
+
const pluginPath = join3(pluginDir, `${PROVIDER_ID}.js`);
|
|
459
1076
|
return { opencodeDir, configPath, pluginDir, pluginPath };
|
|
460
1077
|
}
|
|
461
1078
|
function resolvePluginSource() {
|
|
462
1079
|
const currentFile = fileURLToPath(import.meta.url);
|
|
463
1080
|
const currentDir = dirname(currentFile);
|
|
464
1081
|
const candidates = [
|
|
465
|
-
|
|
466
|
-
|
|
1082
|
+
join3(currentDir, "plugin-entry.js"),
|
|
1083
|
+
join3(currentDir, "..", "plugin-entry.js")
|
|
467
1084
|
];
|
|
468
1085
|
for (const candidate of candidates) {
|
|
469
|
-
if (
|
|
1086
|
+
if (existsSync2(candidate)) {
|
|
470
1087
|
return candidate;
|
|
471
1088
|
}
|
|
472
1089
|
}
|
|
@@ -476,7 +1093,7 @@ function isErrnoException(error) {
|
|
|
476
1093
|
return typeof error === "object" && error !== null && "code" in error;
|
|
477
1094
|
}
|
|
478
1095
|
function readConfig(configPath) {
|
|
479
|
-
if (!
|
|
1096
|
+
if (!existsSync2(configPath)) {
|
|
480
1097
|
return { plugin: [], provider: {} };
|
|
481
1098
|
}
|
|
482
1099
|
let raw;
|
|
@@ -494,12 +1111,14 @@ function readConfig(configPath) {
|
|
|
494
1111
|
throw new Error(`Invalid JSON in config: ${configPath} (${String(error)})`);
|
|
495
1112
|
}
|
|
496
1113
|
}
|
|
497
|
-
function writeConfig(configPath, config, noBackup) {
|
|
498
|
-
|
|
499
|
-
if (!noBackup &&
|
|
1114
|
+
function writeConfig(configPath, config, noBackup, silent = false) {
|
|
1115
|
+
mkdirSync2(dirname(configPath), { recursive: true });
|
|
1116
|
+
if (!noBackup && existsSync2(configPath)) {
|
|
500
1117
|
const backupPath = `${configPath}.bak.${new Date().toISOString().replace(/[:]/g, "-")}`;
|
|
501
1118
|
copyFileSync(configPath, backupPath);
|
|
502
|
-
|
|
1119
|
+
if (!silent) {
|
|
1120
|
+
console.log(`Backup written: ${backupPath}`);
|
|
1121
|
+
}
|
|
503
1122
|
}
|
|
504
1123
|
writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
|
|
505
1124
|
`, "utf8");
|
|
@@ -525,7 +1144,7 @@ function ensureProvider(config, baseUrl) {
|
|
|
525
1144
|
};
|
|
526
1145
|
}
|
|
527
1146
|
function ensurePluginLink(pluginSource, pluginPath, copyMode) {
|
|
528
|
-
|
|
1147
|
+
mkdirSync2(dirname(pluginPath), { recursive: true });
|
|
529
1148
|
rmSync(pluginPath, { force: true });
|
|
530
1149
|
if (copyMode) {
|
|
531
1150
|
copyFileSync(pluginSource, pluginPath);
|
|
@@ -542,6 +1161,104 @@ function discoverModelsSafe() {
|
|
|
542
1161
|
return fallbackModels();
|
|
543
1162
|
}
|
|
544
1163
|
}
|
|
1164
|
+
function syncModelsIntoProvider(config, options) {
|
|
1165
|
+
if (options.compact && !options.variants) {
|
|
1166
|
+
throw new Error("--compact requires --variants");
|
|
1167
|
+
}
|
|
1168
|
+
const discoveredModels = discoverModelsSafe();
|
|
1169
|
+
const provider = config.provider[PROVIDER_ID];
|
|
1170
|
+
const existingModels = provider.models && typeof provider.models === "object" ? provider.models : {};
|
|
1171
|
+
const beforeModels = snapshotModels(existingModels);
|
|
1172
|
+
const result = mergeCursorModelEntries(existingModels, discoveredModels, {
|
|
1173
|
+
variants: options.variants === true,
|
|
1174
|
+
compact: options.compact === true
|
|
1175
|
+
});
|
|
1176
|
+
provider.models = result.models;
|
|
1177
|
+
return {
|
|
1178
|
+
syncedCount: result.syncedCount,
|
|
1179
|
+
groupedCount: result.groupedCount,
|
|
1180
|
+
removedCount: result.removedCount,
|
|
1181
|
+
summary: summarizeModelSync(beforeModels, result.models)
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
function explainCursorModels(models) {
|
|
1185
|
+
const grouped = groupCursorModels(models);
|
|
1186
|
+
const groupedCount = grouped.groups.reduce((total, group) => total + group.members.length, 0);
|
|
1187
|
+
return {
|
|
1188
|
+
modelCount: models.length,
|
|
1189
|
+
groupedCount,
|
|
1190
|
+
directCount: grouped.direct.length,
|
|
1191
|
+
groups: grouped.groups.map((group) => ({
|
|
1192
|
+
id: group.baseId,
|
|
1193
|
+
name: group.name,
|
|
1194
|
+
defaultCursorModel: group.defaultCursorModelId,
|
|
1195
|
+
memberCount: group.members.length,
|
|
1196
|
+
variants: group.variants
|
|
1197
|
+
})),
|
|
1198
|
+
direct: grouped.direct.map((model) => model.id)
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
function createSyncJsonResult(result, options, configPath) {
|
|
1202
|
+
return {
|
|
1203
|
+
...result,
|
|
1204
|
+
configPath,
|
|
1205
|
+
dryRun: options.dryRun === true,
|
|
1206
|
+
variants: options.variants === true,
|
|
1207
|
+
compact: options.compact === true
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
function snapshotModels(models) {
|
|
1211
|
+
return JSON.parse(JSON.stringify(models));
|
|
1212
|
+
}
|
|
1213
|
+
function summarizeModelSync(beforeModels, afterModels) {
|
|
1214
|
+
let added = 0;
|
|
1215
|
+
let updated = 0;
|
|
1216
|
+
let removed = 0;
|
|
1217
|
+
let skipped = 0;
|
|
1218
|
+
for (const [modelId, afterEntry] of Object.entries(afterModels)) {
|
|
1219
|
+
if (!Object.prototype.hasOwnProperty.call(beforeModels, modelId)) {
|
|
1220
|
+
added++;
|
|
1221
|
+
continue;
|
|
1222
|
+
}
|
|
1223
|
+
if (JSON.stringify(beforeModels[modelId]) === JSON.stringify(afterEntry)) {
|
|
1224
|
+
skipped++;
|
|
1225
|
+
} else {
|
|
1226
|
+
updated++;
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
for (const modelId of Object.keys(beforeModels)) {
|
|
1230
|
+
if (!Object.prototype.hasOwnProperty.call(afterModels, modelId)) {
|
|
1231
|
+
removed++;
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
return {
|
|
1235
|
+
added,
|
|
1236
|
+
updated,
|
|
1237
|
+
removed,
|
|
1238
|
+
priced: countPricedModelEntries(afterModels),
|
|
1239
|
+
skipped
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
function countPricedModelEntries(models) {
|
|
1243
|
+
let priced = 0;
|
|
1244
|
+
for (const entry of Object.values(models)) {
|
|
1245
|
+
if (!isRecord(entry))
|
|
1246
|
+
continue;
|
|
1247
|
+
if (isRecord(entry.cost))
|
|
1248
|
+
priced++;
|
|
1249
|
+
if (!isRecord(entry.variants))
|
|
1250
|
+
continue;
|
|
1251
|
+
for (const variantEntry of Object.values(entry.variants)) {
|
|
1252
|
+
if (isRecord(variantEntry) && isRecord(variantEntry.cost)) {
|
|
1253
|
+
priced++;
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
return priced;
|
|
1258
|
+
}
|
|
1259
|
+
function isRecord(value) {
|
|
1260
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1261
|
+
}
|
|
545
1262
|
function installAiSdk(opencodeDir) {
|
|
546
1263
|
try {
|
|
547
1264
|
execFileSync2("bun", ["install", "@ai-sdk/openai-compatible"], {
|
|
@@ -558,20 +1275,23 @@ function commandInstall(options) {
|
|
|
558
1275
|
const baseUrl = options.baseUrl || DEFAULT_BASE_URL;
|
|
559
1276
|
const copyMode = options.copy === true;
|
|
560
1277
|
const pluginSource = resolvePluginSource();
|
|
561
|
-
|
|
562
|
-
|
|
1278
|
+
if (!options.dryRun) {
|
|
1279
|
+
mkdirSync2(opencodeDir, { recursive: true });
|
|
1280
|
+
ensurePluginLink(pluginSource, pluginPath, copyMode);
|
|
1281
|
+
}
|
|
563
1282
|
const config = readConfig(configPath);
|
|
564
1283
|
ensureProvider(config, baseUrl);
|
|
565
1284
|
if (!options.skipModels) {
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
console.log(
|
|
1285
|
+
const result = syncModelsIntoProvider(config, options);
|
|
1286
|
+
printSyncResult(result, options);
|
|
1287
|
+
}
|
|
1288
|
+
if (options.dryRun) {
|
|
1289
|
+
console.log("Dry run: no files changed.");
|
|
1290
|
+
} else {
|
|
1291
|
+
writeConfig(configPath, config, options.noBackup === true);
|
|
1292
|
+
installAiSdk(opencodeDir);
|
|
571
1293
|
}
|
|
572
|
-
|
|
573
|
-
installAiSdk(opencodeDir);
|
|
574
|
-
console.log(`Installed ${PROVIDER_ID}`);
|
|
1294
|
+
console.log(`${options.dryRun ? "Would install" : "Installed"} ${PROVIDER_ID}`);
|
|
575
1295
|
console.log(`Plugin path: ${pluginPath}${copyMode ? " (copy)" : " (symlink)"}`);
|
|
576
1296
|
console.log(`Config path: ${configPath}`);
|
|
577
1297
|
}
|
|
@@ -579,19 +1299,74 @@ function commandSyncModels(options) {
|
|
|
579
1299
|
const { configPath } = resolvePaths(options);
|
|
580
1300
|
const config = readConfig(configPath);
|
|
581
1301
|
ensureProvider(config, options.baseUrl || DEFAULT_BASE_URL);
|
|
582
|
-
const
|
|
583
|
-
|
|
584
|
-
config.
|
|
1302
|
+
const result = syncModelsIntoProvider(config, options);
|
|
1303
|
+
if (!options.dryRun) {
|
|
1304
|
+
writeConfig(configPath, config, options.noBackup === true, options.json === true);
|
|
1305
|
+
}
|
|
1306
|
+
if (options.json) {
|
|
1307
|
+
console.log(JSON.stringify(createSyncJsonResult(result, options, configPath), null, 2));
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
printSyncResult(result, options);
|
|
1311
|
+
if (options.dryRun) {
|
|
1312
|
+
console.log("Dry run: no changes written.");
|
|
585
1313
|
}
|
|
586
|
-
writeConfig(configPath, config, options.noBackup === true);
|
|
587
|
-
console.log(`Models synced: ${models.length}`);
|
|
588
1314
|
console.log(`Config path: ${configPath}`);
|
|
589
1315
|
}
|
|
1316
|
+
function commandModels(options) {
|
|
1317
|
+
const models = discoverModelsSafe();
|
|
1318
|
+
const explanation = explainCursorModels(models);
|
|
1319
|
+
if (options.json) {
|
|
1320
|
+
console.log(JSON.stringify(explanation, null, 2));
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
console.log(`Cursor models discovered: ${explanation.modelCount}`);
|
|
1324
|
+
console.log(`Grouped Cursor models: ${explanation.groupedCount}`);
|
|
1325
|
+
console.log(`Direct models: ${explanation.directCount}`);
|
|
1326
|
+
if (!options.explain) {
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
console.log("");
|
|
1330
|
+
console.log("Model groups:");
|
|
1331
|
+
for (const group of explanation.groups) {
|
|
1332
|
+
console.log(` ${group.id}`);
|
|
1333
|
+
console.log(` Default: ${group.defaultCursorModel}`);
|
|
1334
|
+
const variants = Object.entries(group.variants);
|
|
1335
|
+
if (variants.length === 0) {
|
|
1336
|
+
console.log(" Variants: none");
|
|
1337
|
+
continue;
|
|
1338
|
+
}
|
|
1339
|
+
console.log(" Variants:");
|
|
1340
|
+
for (const [variant, cursorModel] of variants) {
|
|
1341
|
+
console.log(` ${variant}: ${cursorModel}`);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
console.log("");
|
|
1345
|
+
console.log("Direct models:");
|
|
1346
|
+
for (const modelId of explanation.direct) {
|
|
1347
|
+
console.log(` ${modelId}`);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
function printSyncResult(result, options) {
|
|
1351
|
+
console.log(`Models synced: ${result.syncedCount}`);
|
|
1352
|
+
if (options.variants) {
|
|
1353
|
+
console.log(`Grouped Cursor models: ${result.groupedCount}`);
|
|
1354
|
+
}
|
|
1355
|
+
if (result.removedCount > 0) {
|
|
1356
|
+
console.log(`Raw grouped models removed: ${result.removedCount}`);
|
|
1357
|
+
}
|
|
1358
|
+
console.log("Sync summary:");
|
|
1359
|
+
console.log(` Added: ${result.summary.added}`);
|
|
1360
|
+
console.log(` Updated: ${result.summary.updated}`);
|
|
1361
|
+
console.log(` Removed: ${result.summary.removed}`);
|
|
1362
|
+
console.log(` Priced: ${result.summary.priced}`);
|
|
1363
|
+
console.log(` Skipped: ${result.summary.skipped}`);
|
|
1364
|
+
}
|
|
590
1365
|
var NPM_PACKAGE = "@rama_nigg/open-cursor";
|
|
591
1366
|
function commandUninstall(options) {
|
|
592
1367
|
const { configPath, pluginPath } = resolvePaths(options);
|
|
593
1368
|
rmSync(pluginPath, { force: true });
|
|
594
|
-
if (
|
|
1369
|
+
if (existsSync2(configPath)) {
|
|
595
1370
|
const config = readConfig(configPath);
|
|
596
1371
|
if (Array.isArray(config.plugin)) {
|
|
597
1372
|
config.plugin = config.plugin.filter((name) => {
|
|
@@ -613,7 +1388,7 @@ function commandUninstall(options) {
|
|
|
613
1388
|
function getStatusResult(configPath, pluginPath) {
|
|
614
1389
|
let pluginType = "missing";
|
|
615
1390
|
let pluginTarget;
|
|
616
|
-
if (
|
|
1391
|
+
if (existsSync2(pluginPath)) {
|
|
617
1392
|
try {
|
|
618
1393
|
const stat = lstatSync(pluginPath);
|
|
619
1394
|
pluginType = stat.isSymbolicLink() ? "symlink" : "file";
|
|
@@ -636,7 +1411,7 @@ function getStatusResult(configPath, pluginPath) {
|
|
|
636
1411
|
let providerEnabled = false;
|
|
637
1412
|
let baseUrl = "http://127.0.0.1:32124/v1";
|
|
638
1413
|
let modelCount = 0;
|
|
639
|
-
if (
|
|
1414
|
+
if (existsSync2(configPath)) {
|
|
640
1415
|
config = readConfig(configPath);
|
|
641
1416
|
const provider = config.provider?.["cursor-acp"];
|
|
642
1417
|
providerEnabled = !!provider;
|
|
@@ -648,8 +1423,8 @@ function getStatusResult(configPath, pluginPath) {
|
|
|
648
1423
|
config = undefined;
|
|
649
1424
|
}
|
|
650
1425
|
const opencodeDir = dirname(configPath);
|
|
651
|
-
const sdkPath =
|
|
652
|
-
const aiSdkInstalled =
|
|
1426
|
+
const sdkPath = join3(opencodeDir, "node_modules", "@ai-sdk", "openai-compatible");
|
|
1427
|
+
const aiSdkInstalled = existsSync2(sdkPath);
|
|
653
1428
|
let installMethod = "none";
|
|
654
1429
|
if (pluginType !== "missing") {
|
|
655
1430
|
installMethod = "symlink";
|
|
@@ -675,6 +1450,95 @@ function getStatusResult(configPath, pluginPath) {
|
|
|
675
1450
|
}
|
|
676
1451
|
};
|
|
677
1452
|
}
|
|
1453
|
+
function runDeepDoctorChecks(configPath) {
|
|
1454
|
+
const checks = [];
|
|
1455
|
+
let config;
|
|
1456
|
+
try {
|
|
1457
|
+
config = readConfig(configPath);
|
|
1458
|
+
} catch (error) {
|
|
1459
|
+
return [{
|
|
1460
|
+
name: "Deep config read",
|
|
1461
|
+
passed: false,
|
|
1462
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1463
|
+
}];
|
|
1464
|
+
}
|
|
1465
|
+
const provider = config.provider?.[PROVIDER_ID];
|
|
1466
|
+
const models = isRecord(provider?.models) ? provider.models : {};
|
|
1467
|
+
const baseUrl = typeof provider?.options?.baseURL === "string" ? provider.options.baseURL : "";
|
|
1468
|
+
checks.push({
|
|
1469
|
+
name: "Provider base URL",
|
|
1470
|
+
passed: baseUrl.startsWith("http://") || baseUrl.startsWith("https://"),
|
|
1471
|
+
message: baseUrl || "missing - run: open-cursor install"
|
|
1472
|
+
});
|
|
1473
|
+
checks.push({
|
|
1474
|
+
name: "Provider models",
|
|
1475
|
+
passed: Object.keys(models).length > 0,
|
|
1476
|
+
message: `${Object.keys(models).length} configured model(s)`
|
|
1477
|
+
});
|
|
1478
|
+
const variantEntryCount = countVariantModelEntries(models);
|
|
1479
|
+
checks.push({
|
|
1480
|
+
name: "Compact variants",
|
|
1481
|
+
passed: variantEntryCount > 0,
|
|
1482
|
+
warning: variantEntryCount === 0,
|
|
1483
|
+
message: variantEntryCount > 0 ? `${variantEntryCount} model entr${variantEntryCount === 1 ? "y" : "ies"} with variants` : "no compact variants found - run: open-cursor sync-models --variants --compact"
|
|
1484
|
+
});
|
|
1485
|
+
let discoveredModels;
|
|
1486
|
+
try {
|
|
1487
|
+
discoveredModels = discoverModelsFromCursorAgent();
|
|
1488
|
+
checks.push({
|
|
1489
|
+
name: "Cursor model discovery",
|
|
1490
|
+
passed: true,
|
|
1491
|
+
message: `${discoveredModels.length} model(s) from cursor-agent`
|
|
1492
|
+
});
|
|
1493
|
+
} catch (error) {
|
|
1494
|
+
checks.push({
|
|
1495
|
+
name: "Cursor model discovery",
|
|
1496
|
+
passed: false,
|
|
1497
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1498
|
+
warning: true
|
|
1499
|
+
});
|
|
1500
|
+
return checks;
|
|
1501
|
+
}
|
|
1502
|
+
const knownModelIds = new Set(discoveredModels.map((model) => model.id));
|
|
1503
|
+
const unknownTargets = collectConfiguredCursorModels(models).filter((modelId) => !knownModelIds.has(modelId));
|
|
1504
|
+
checks.push({
|
|
1505
|
+
name: "Configured Cursor model targets",
|
|
1506
|
+
passed: unknownTargets.length === 0,
|
|
1507
|
+
warning: unknownTargets.length > 0,
|
|
1508
|
+
message: unknownTargets.length === 0 ? "all configured targets exist in cursor-agent models" : `${unknownTargets.length} target(s) not found: ${unknownTargets.slice(0, 5).join(", ")}`
|
|
1509
|
+
});
|
|
1510
|
+
return checks;
|
|
1511
|
+
}
|
|
1512
|
+
function countVariantModelEntries(models) {
|
|
1513
|
+
return Object.values(models).filter((entry) => {
|
|
1514
|
+
return isRecord(entry) && isRecord(entry.variants) && Object.keys(entry.variants).length > 0;
|
|
1515
|
+
}).length;
|
|
1516
|
+
}
|
|
1517
|
+
function collectConfiguredCursorModels(models) {
|
|
1518
|
+
const targets = [];
|
|
1519
|
+
for (const [modelId, entry] of Object.entries(models)) {
|
|
1520
|
+
if (!isRecord(entry)) {
|
|
1521
|
+
targets.push(modelId);
|
|
1522
|
+
continue;
|
|
1523
|
+
}
|
|
1524
|
+
const optionTarget = readCursorModel(entry.options);
|
|
1525
|
+
targets.push(optionTarget || modelId);
|
|
1526
|
+
if (!isRecord(entry.variants))
|
|
1527
|
+
continue;
|
|
1528
|
+
for (const variantEntry of Object.values(entry.variants)) {
|
|
1529
|
+
const variantTarget = readCursorModel(variantEntry);
|
|
1530
|
+
if (variantTarget)
|
|
1531
|
+
targets.push(variantTarget);
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
return [...new Set(targets)];
|
|
1535
|
+
}
|
|
1536
|
+
function readCursorModel(value) {
|
|
1537
|
+
if (!isRecord(value))
|
|
1538
|
+
return;
|
|
1539
|
+
const cursorModel = value.cursorModel;
|
|
1540
|
+
return typeof cursorModel === "string" && cursorModel.trim().length > 0 ? cursorModel.trim() : undefined;
|
|
1541
|
+
}
|
|
678
1542
|
function commandStatus(options) {
|
|
679
1543
|
const { configPath, pluginPath } = resolvePaths(options);
|
|
680
1544
|
const result = getStatusResult(configPath, pluginPath);
|
|
@@ -706,7 +1570,15 @@ function commandStatus(options) {
|
|
|
706
1570
|
}
|
|
707
1571
|
function commandDoctor(options) {
|
|
708
1572
|
const { configPath, pluginPath } = resolvePaths(options);
|
|
709
|
-
const checks =
|
|
1573
|
+
const checks = [
|
|
1574
|
+
...runDoctorChecks(configPath, pluginPath),
|
|
1575
|
+
...options.deep ? runDeepDoctorChecks(configPath) : []
|
|
1576
|
+
];
|
|
1577
|
+
if (options.json) {
|
|
1578
|
+
const failed2 = checks.filter((c) => !c.passed && !c.warning);
|
|
1579
|
+
console.log(JSON.stringify({ deep: options.deep === true, checks, failed: failed2.length }, null, 2));
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
710
1582
|
console.log("");
|
|
711
1583
|
for (const check of checks) {
|
|
712
1584
|
const symbol = check.passed ? "✓" : check.warning ? "⚠" : "✗";
|
|
@@ -740,6 +1612,9 @@ function main() {
|
|
|
740
1612
|
case "sync-models":
|
|
741
1613
|
commandSyncModels(parsed.options);
|
|
742
1614
|
return;
|
|
1615
|
+
case "models":
|
|
1616
|
+
commandModels(parsed.options);
|
|
1617
|
+
return;
|
|
743
1618
|
case "uninstall":
|
|
744
1619
|
commandUninstall(parsed.options);
|
|
745
1620
|
return;
|
|
@@ -759,11 +1634,16 @@ function main() {
|
|
|
759
1634
|
process.exit(1);
|
|
760
1635
|
}
|
|
761
1636
|
}
|
|
762
|
-
|
|
1637
|
+
if (fileURLToPath(import.meta.url) === resolve(process.argv[1] || "")) {
|
|
1638
|
+
main();
|
|
1639
|
+
}
|
|
763
1640
|
export {
|
|
1641
|
+
summarizeModelSync,
|
|
764
1642
|
runDoctorChecks,
|
|
1643
|
+
runDeepDoctorChecks,
|
|
765
1644
|
getStatusResult,
|
|
766
1645
|
getBrandingHeader,
|
|
1646
|
+
explainCursorModels,
|
|
767
1647
|
checkCursorAgentLogin,
|
|
768
1648
|
checkCursorAgent,
|
|
769
1649
|
checkBun
|