@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.
@@ -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("cursor-agent", ["models"], {
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("cursor-agent", ["--version"], { encoding: "utf8" }).trim();
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("cursor-agent", ["models"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
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 (!existsSync(pluginPath)) {
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 (!existsSync(configPath)) {
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 = join(opencodeDir, "node_modules", "@ai-sdk", "openai-compatible");
346
- if (existsSync(sdkPath)) {
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 (status command only)
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 join(homedir(), ".config");
1069
+ return join3(homedir2(), ".config");
453
1070
  }
454
1071
  function resolvePaths(options) {
455
- const opencodeDir = join(getConfigHome(), "opencode");
456
- const configPath = resolve(options.config || join(opencodeDir, "opencode.json"));
457
- const pluginDir = resolve(options.pluginDir || join(opencodeDir, "plugin"));
458
- const pluginPath = join(pluginDir, `${PROVIDER_ID}.js`);
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
- join(currentDir, "plugin-entry.js"),
466
- join(currentDir, "..", "plugin-entry.js")
1082
+ join3(currentDir, "plugin-entry.js"),
1083
+ join3(currentDir, "..", "plugin-entry.js")
467
1084
  ];
468
1085
  for (const candidate of candidates) {
469
- if (existsSync(candidate)) {
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 (!existsSync(configPath)) {
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
- mkdirSync(dirname(configPath), { recursive: true });
499
- if (!noBackup && existsSync(configPath)) {
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
- console.log(`Backup written: ${backupPath}`);
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
- mkdirSync(dirname(pluginPath), { recursive: true });
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
- mkdirSync(opencodeDir, { recursive: true });
562
- ensurePluginLink(pluginSource, pluginPath, copyMode);
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 models = discoverModelsSafe();
567
- for (const model of models) {
568
- config.provider[PROVIDER_ID].models[model.id] = { name: model.name };
569
- }
570
- console.log(`Models synced: ${models.length}`);
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
- writeConfig(configPath, config, options.noBackup === true);
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 models = discoverModelsSafe();
583
- for (const model of models) {
584
- config.provider[PROVIDER_ID].models[model.id] = { name: model.name };
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 (existsSync(configPath)) {
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 (existsSync(pluginPath)) {
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 (existsSync(configPath)) {
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 = join(opencodeDir, "node_modules", "@ai-sdk", "openai-compatible");
652
- const aiSdkInstalled = existsSync(sdkPath);
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 = runDoctorChecks(configPath, pluginPath);
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
- main();
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