@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rama_nigg/open-cursor",
3
- "version": "2.3.20",
3
+ "version": "2.4.1",
4
4
  "description": "No prompt limits. No broken streams. Full thinking + tool support. Your Cursor subscription, properly integrated.",
5
5
  "type": "module",
6
6
  "main": "dist/plugin-entry.js",
@@ -11,8 +11,10 @@
11
11
  "test": "bun test",
12
12
  "test:unit": "bun test tests/unit",
13
13
  "test:integration": "bun test tests/integration",
14
- "test:ci:unit": "bun test tests/tools/defaults.test.ts tests/tools/executor-chain.test.ts tests/tools/sdk-executor.test.ts tests/tools/mcp-executor.test.ts tests/tools/skills.test.ts tests/tools/registry.test.ts tests/unit/cli/model-discovery.test.ts tests/unit/proxy/prompt-builder.test.ts tests/unit/proxy/tool-loop.test.ts tests/unit/provider-boundary.test.ts tests/unit/provider-runtime-interception.test.ts tests/unit/provider-tool-schema-compat.test.ts tests/unit/provider-tool-loop-guard.test.ts tests/unit/plugin.test.ts tests/unit/plugin-tools-hook.test.ts tests/unit/plugin-tool-resolution.test.ts tests/unit/plugin-config.test.ts tests/unit/auth.test.ts tests/unit/streaming/line-buffer.test.ts tests/unit/streaming/parser.test.ts tests/unit/streaming/types.test.ts tests/unit/streaming/delta-tracker.test.ts tests/competitive/edge.test.ts",
14
+ "test:ci:unit": "bun test tests/tools/defaults.test.ts tests/tools/executor-chain.test.ts tests/tools/sdk-executor.test.ts tests/tools/mcp-executor.test.ts tests/tools/skills.test.ts tests/tools/registry.test.ts tests/unit/cli/model-discovery.test.ts tests/unit/proxy/prompt-builder.test.ts tests/unit/proxy/tool-loop.test.ts tests/unit/provider-boundary.test.ts tests/unit/provider-runtime-interception.test.ts tests/unit/provider-tool-schema-compat.test.ts tests/unit/provider-tool-loop-guard.test.ts tests/unit/plugin.test.ts tests/unit/plugin-tools-hook.test.ts tests/unit/plugin-tool-resolution.test.ts tests/unit/plugin-config.test.ts tests/unit/plugin-stream-extraction.test.ts tests/unit/auth.test.ts tests/unit/streaming/line-buffer.test.ts tests/unit/streaming/parser.test.ts tests/unit/streaming/types.test.ts tests/unit/streaming/delta-tracker.test.ts tests/unit/streaming/openai-sse.test.ts tests/unit/streaming/ai-sdk-parts.test.ts tests/competitive/edge.test.ts",
15
15
  "test:ci:integration": "bun test tests/integration/comprehensive.test.ts tests/integration/tools-router.integration.test.ts tests/integration/stream-router.integration.test.ts tests/integration/opencode-loop.integration.test.ts",
16
+ "check:pricing": "bun run scripts/check-cursor-pricing-coverage.ts",
17
+ "check:pricing:fixture": "bun run scripts/check-cursor-pricing-coverage.ts --models-file tests/fixtures/cursor-pricing-models.txt --skip-doc-fetch",
16
18
  "discover": "bun run src/cli/discover.ts",
17
19
  "prepublishOnly": "bun run build"
18
20
  },
package/src/auth.ts CHANGED
@@ -6,6 +6,7 @@ import { homedir, platform } from "os";
6
6
  import { join } from "path";
7
7
  import { createLogger } from "./utils/logger";
8
8
  import { stripAnsi } from "./utils/errors";
9
+ import { resolveCursorAgentBinary } from "./utils/binary.js";
9
10
 
10
11
  const log = createLogger("auth");
11
12
 
@@ -75,8 +76,9 @@ export async function startCursorOAuth(): Promise<{
75
76
  return new Promise((resolve, reject) => {
76
77
  log.info("Starting cursor-cli login process");
77
78
 
78
- const proc = spawn("cursor-agent", ["login"], {
79
+ const proc = spawn(resolveCursorAgentBinary(), ["login"], {
79
80
  stdio: ["pipe", "pipe", "pipe"],
81
+ shell: process.platform === "win32",
80
82
  });
81
83
 
82
84
  let stdout = "";
@@ -1,5 +1,6 @@
1
1
  import { execFileSync } from "child_process";
2
2
  import { stripAnsi } from "../utils/errors.js";
3
+ import { resolveCursorAgentBinary } from "../utils/binary.js";
3
4
 
4
5
  const MODEL_DISCOVERY_TIMEOUT_MS = 5000;
5
6
 
@@ -31,9 +32,9 @@ export function parseCursorModelsOutput(output: string): DiscoveredModel[] {
31
32
  }
32
33
 
33
34
  export function discoverModelsFromCursorAgent(): DiscoveredModel[] {
34
- const raw = execFileSync("cursor-agent", ["models"], {
35
+ const raw = execFileSync(resolveCursorAgentBinary(), ["models"], {
35
36
  encoding: "utf8",
36
- killSignal: "SIGTERM",
37
+ ...(process.platform !== "win32" && { killSignal: "SIGTERM" as const }),
37
38
  stdio: ["ignore", "pipe", "pipe"],
38
39
  timeout: MODEL_DISCOVERY_TIMEOUT_MS,
39
40
  });
@@ -18,6 +18,9 @@ import {
18
18
  discoverModelsFromCursorAgent,
19
19
  fallbackModels,
20
20
  } from "./model-discovery.js";
21
+ import { resolveCursorAgentBinary } from "../utils/binary.js";
22
+ import { groupCursorModels, mergeCursorModelEntries } from "../models/variants.js";
23
+ import type { DiscoveredModel } from "./model-discovery.js";
21
24
 
22
25
  const BRANDING_HEADER = `
23
26
  ▄▄▄ ▄▄▄▄ ▄▄▄▄▄ ▄▄ ▄▄ ▄▄▄ ▄▄ ▄▄ ▄▄▄▄ ▄▄▄▄ ▄▄▄ ▄▄▄▄
@@ -55,6 +58,20 @@ type StatusResult = {
55
58
  };
56
59
  };
57
60
 
61
+ type ModelExplanation = {
62
+ modelCount: number;
63
+ groupedCount: number;
64
+ directCount: number;
65
+ groups: Array<{
66
+ id: string;
67
+ name: string;
68
+ defaultCursorModel: string;
69
+ memberCount: number;
70
+ variants: Record<string, string>;
71
+ }>;
72
+ direct: string[];
73
+ };
74
+
58
75
  export function checkBun(): CheckResult {
59
76
  try {
60
77
  const version = execFileSync("bun", ["--version"], { encoding: "utf8" }).trim();
@@ -70,7 +87,7 @@ export function checkBun(): CheckResult {
70
87
 
71
88
  export function checkCursorAgent(): CheckResult {
72
89
  try {
73
- const output = execFileSync("cursor-agent", ["--version"], { encoding: "utf8" }).trim();
90
+ const output = execFileSync(resolveCursorAgentBinary(), ["--version"], { encoding: "utf8" }).trim();
74
91
  const version = output.split("\n")[0] || "installed";
75
92
  return { name: "cursor-agent", passed: true, message: version };
76
93
  } catch {
@@ -86,7 +103,11 @@ export function checkCursorAgentLogin(): CheckResult {
86
103
  try {
87
104
  // cursor-agent stores credentials in ~/.cursor-agent or similar
88
105
  // Try running a command that requires auth
89
- execFileSync("cursor-agent", ["models"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
106
+ execFileSync(resolveCursorAgentBinary(), ["models"], {
107
+ encoding: "utf8",
108
+ stdio: ["ignore", "pipe", "pipe"],
109
+ timeout: 3000,
110
+ });
90
111
  return { name: "cursor-agent login", passed: true, message: "logged in" };
91
112
  } catch {
92
113
  return {
@@ -217,7 +238,7 @@ export function runDoctorChecks(configPath: string, pluginPath: string): CheckRe
217
238
  ];
218
239
  }
219
240
 
220
- type Command = "install" | "sync-models" | "uninstall" | "status" | "doctor" | "help";
241
+ type Command = "install" | "sync-models" | "models" | "uninstall" | "status" | "doctor" | "help";
221
242
 
222
243
  type Options = {
223
244
  config?: string;
@@ -226,9 +247,36 @@ type Options = {
226
247
  copy?: boolean;
227
248
  skipModels?: boolean;
228
249
  noBackup?: boolean;
250
+ variants?: boolean;
251
+ compact?: boolean;
252
+ dryRun?: boolean;
253
+ deep?: boolean;
254
+ explain?: boolean;
229
255
  json?: boolean;
230
256
  };
231
257
 
258
+ type SyncSummary = {
259
+ added: number;
260
+ updated: number;
261
+ removed: number;
262
+ priced: number;
263
+ skipped: number;
264
+ };
265
+
266
+ type SyncModelsResult = {
267
+ syncedCount: number;
268
+ groupedCount: number;
269
+ removedCount: number;
270
+ summary: SyncSummary;
271
+ };
272
+
273
+ type SyncModelsJsonResult = SyncModelsResult & {
274
+ configPath: string;
275
+ dryRun: boolean;
276
+ variants: boolean;
277
+ compact: boolean;
278
+ };
279
+
232
280
  const PROVIDER_ID = "cursor-acp";
233
281
  const NPM_PACKAGE_PREFIX = "@rama_nigg/open-cursor";
234
282
  const DEFAULT_BASE_URL = "http://127.0.0.1:32124/v1";
@@ -241,6 +289,7 @@ function printHelp() {
241
289
  Commands:
242
290
  install Configure OpenCode for Cursor (idempotent, safe to re-run)
243
291
  sync-models Refresh model list from cursor-agent
292
+ models Explain discovered Cursor model groups and variants
244
293
  status Show current configuration state
245
294
  doctor Diagnose common issues
246
295
  uninstall Remove cursor-acp from OpenCode config
@@ -252,8 +301,13 @@ Options:
252
301
  --base-url <url> Proxy base URL (default: http://127.0.0.1:32124/v1)
253
302
  --copy Copy plugin instead of symlink
254
303
  --skip-models Skip model sync during install
304
+ --variants Generate compact OpenCode model variants from Cursor models
305
+ --compact With --variants, remove raw grouped Cursor model entries
306
+ --dry-run Preview sync/install config changes without writing files
307
+ --deep Run extra doctor checks for models and variant config
308
+ --explain Show model grouping explanation (models command)
255
309
  --no-backup Don't create config backup
256
- --json Output in JSON format (status command only)
310
+ --json Output in JSON format where supported
257
311
  `);
258
312
  }
259
313
 
@@ -268,6 +322,16 @@ function parseArgs(argv: string[]): { command: Command; options: Options } {
268
322
  options.copy = true;
269
323
  } else if (arg === "--skip-models") {
270
324
  options.skipModels = true;
325
+ } else if (arg === "--variants") {
326
+ options.variants = true;
327
+ } else if (arg === "--compact") {
328
+ options.compact = true;
329
+ } else if (arg === "--dry-run") {
330
+ options.dryRun = true;
331
+ } else if (arg === "--deep") {
332
+ options.deep = true;
333
+ } else if (arg === "--explain") {
334
+ options.explain = true;
271
335
  } else if (arg === "--no-backup") {
272
336
  options.noBackup = true;
273
337
  } else if (arg === "--config" && rest[i + 1]) {
@@ -293,6 +357,7 @@ function normalizeCommand(value: string | undefined): Command {
293
357
  switch ((value || "help").toLowerCase()) {
294
358
  case "install":
295
359
  case "sync-models":
360
+ case "models":
296
361
  case "uninstall":
297
362
  case "status":
298
363
  case "doctor":
@@ -356,12 +421,14 @@ function readConfig(configPath: string): any {
356
421
  }
357
422
  }
358
423
 
359
- function writeConfig(configPath: string, config: any, noBackup: boolean) {
424
+ function writeConfig(configPath: string, config: any, noBackup: boolean, silent = false) {
360
425
  mkdirSync(dirname(configPath), { recursive: true });
361
426
  if (!noBackup && existsSync(configPath)) {
362
427
  const backupPath = `${configPath}.bak.${new Date().toISOString().replace(/[:]/g, "-")}`;
363
428
  copyFileSync(configPath, backupPath);
364
- console.log(`Backup written: ${backupPath}`);
429
+ if (!silent) {
430
+ console.log(`Backup written: ${backupPath}`);
431
+ }
365
432
  }
366
433
  writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
367
434
  }
@@ -411,6 +478,127 @@ function discoverModelsSafe() {
411
478
  }
412
479
  }
413
480
 
481
+ function syncModelsIntoProvider(config: any, options: Options): SyncModelsResult {
482
+ if (options.compact && !options.variants) {
483
+ throw new Error("--compact requires --variants");
484
+ }
485
+
486
+ const discoveredModels = discoverModelsSafe();
487
+ const provider = config.provider[PROVIDER_ID];
488
+ const existingModels = provider.models && typeof provider.models === "object"
489
+ ? provider.models
490
+ : {};
491
+ const beforeModels = snapshotModels(existingModels);
492
+ const result = mergeCursorModelEntries(existingModels, discoveredModels, {
493
+ variants: options.variants === true,
494
+ compact: options.compact === true,
495
+ });
496
+
497
+ provider.models = result.models;
498
+ return {
499
+ syncedCount: result.syncedCount,
500
+ groupedCount: result.groupedCount,
501
+ removedCount: result.removedCount,
502
+ summary: summarizeModelSync(beforeModels, result.models),
503
+ };
504
+ }
505
+
506
+ export function explainCursorModels(models: DiscoveredModel[]): ModelExplanation {
507
+ const grouped = groupCursorModels(models);
508
+ const groupedCount = grouped.groups.reduce((total, group) => total + group.members.length, 0);
509
+
510
+ return {
511
+ modelCount: models.length,
512
+ groupedCount,
513
+ directCount: grouped.direct.length,
514
+ groups: grouped.groups.map(group => ({
515
+ id: group.baseId,
516
+ name: group.name,
517
+ defaultCursorModel: group.defaultCursorModelId,
518
+ memberCount: group.members.length,
519
+ variants: group.variants,
520
+ })),
521
+ direct: grouped.direct.map(model => model.id),
522
+ };
523
+ }
524
+
525
+ function createSyncJsonResult(
526
+ result: SyncModelsResult,
527
+ options: Options,
528
+ configPath: string,
529
+ ): SyncModelsJsonResult {
530
+ return {
531
+ ...result,
532
+ configPath,
533
+ dryRun: options.dryRun === true,
534
+ variants: options.variants === true,
535
+ compact: options.compact === true,
536
+ };
537
+ }
538
+
539
+ function snapshotModels(models: Record<string, unknown>): Record<string, unknown> {
540
+ return JSON.parse(JSON.stringify(models));
541
+ }
542
+
543
+ export function summarizeModelSync(
544
+ beforeModels: Record<string, unknown>,
545
+ afterModels: Record<string, unknown>,
546
+ ): SyncSummary {
547
+ let added = 0;
548
+ let updated = 0;
549
+ let removed = 0;
550
+ let skipped = 0;
551
+
552
+ for (const [modelId, afterEntry] of Object.entries(afterModels)) {
553
+ if (!Object.prototype.hasOwnProperty.call(beforeModels, modelId)) {
554
+ added++;
555
+ continue;
556
+ }
557
+
558
+ if (JSON.stringify(beforeModels[modelId]) === JSON.stringify(afterEntry)) {
559
+ skipped++;
560
+ } else {
561
+ updated++;
562
+ }
563
+ }
564
+
565
+ for (const modelId of Object.keys(beforeModels)) {
566
+ if (!Object.prototype.hasOwnProperty.call(afterModels, modelId)) {
567
+ removed++;
568
+ }
569
+ }
570
+
571
+ return {
572
+ added,
573
+ updated,
574
+ removed,
575
+ priced: countPricedModelEntries(afterModels),
576
+ skipped,
577
+ };
578
+ }
579
+
580
+ function countPricedModelEntries(models: Record<string, unknown>): number {
581
+ let priced = 0;
582
+
583
+ for (const entry of Object.values(models)) {
584
+ if (!isRecord(entry)) continue;
585
+ if (isRecord(entry.cost)) priced++;
586
+
587
+ if (!isRecord(entry.variants)) continue;
588
+ for (const variantEntry of Object.values(entry.variants)) {
589
+ if (isRecord(variantEntry) && isRecord(variantEntry.cost)) {
590
+ priced++;
591
+ }
592
+ }
593
+ }
594
+
595
+ return priced;
596
+ }
597
+
598
+ function isRecord(value: unknown): value is Record<string, unknown> {
599
+ return typeof value === "object" && value !== null && !Array.isArray(value);
600
+ }
601
+
414
602
  function installAiSdk(opencodeDir: string) {
415
603
  try {
416
604
  execFileSync("bun", ["install", "@ai-sdk/openai-compatible"], {
@@ -429,23 +617,26 @@ function commandInstall(options: Options) {
429
617
  const copyMode = options.copy === true;
430
618
  const pluginSource = resolvePluginSource();
431
619
 
432
- mkdirSync(opencodeDir, { recursive: true });
433
- ensurePluginLink(pluginSource, pluginPath, copyMode);
620
+ if (!options.dryRun) {
621
+ mkdirSync(opencodeDir, { recursive: true });
622
+ ensurePluginLink(pluginSource, pluginPath, copyMode);
623
+ }
434
624
  const config = readConfig(configPath);
435
625
  ensureProvider(config, baseUrl);
436
626
 
437
627
  if (!options.skipModels) {
438
- const models = discoverModelsSafe();
439
- for (const model of models) {
440
- config.provider[PROVIDER_ID].models[model.id] = { name: model.name };
441
- }
442
- console.log(`Models synced: ${models.length}`);
628
+ const result = syncModelsIntoProvider(config, options);
629
+ printSyncResult(result, options);
443
630
  }
444
631
 
445
- writeConfig(configPath, config, options.noBackup === true);
446
- installAiSdk(opencodeDir);
632
+ if (options.dryRun) {
633
+ console.log("Dry run: no files changed.");
634
+ } else {
635
+ writeConfig(configPath, config, options.noBackup === true);
636
+ installAiSdk(opencodeDir);
637
+ }
447
638
 
448
- console.log(`Installed ${PROVIDER_ID}`);
639
+ console.log(`${options.dryRun ? "Would install" : "Installed"} ${PROVIDER_ID}`);
449
640
  console.log(`Plugin path: ${pluginPath}${copyMode ? " (copy)" : " (symlink)"}`);
450
641
  console.log(`Config path: ${configPath}`);
451
642
  }
@@ -455,16 +646,81 @@ function commandSyncModels(options: Options) {
455
646
  const config = readConfig(configPath);
456
647
  ensureProvider(config, options.baseUrl || DEFAULT_BASE_URL);
457
648
 
458
- const models = discoverModelsSafe();
459
- for (const model of models) {
460
- config.provider[PROVIDER_ID].models[model.id] = { name: model.name };
649
+ const result = syncModelsIntoProvider(config, options);
650
+
651
+ if (!options.dryRun) {
652
+ writeConfig(configPath, config, options.noBackup === true, options.json === true);
653
+ }
654
+
655
+ if (options.json) {
656
+ console.log(JSON.stringify(createSyncJsonResult(result, options, configPath), null, 2));
657
+ return;
461
658
  }
462
659
 
463
- writeConfig(configPath, config, options.noBackup === true);
464
- console.log(`Models synced: ${models.length}`);
660
+ printSyncResult(result, options);
661
+ if (options.dryRun) {
662
+ console.log("Dry run: no changes written.");
663
+ }
465
664
  console.log(`Config path: ${configPath}`);
466
665
  }
467
666
 
667
+ function commandModels(options: Options) {
668
+ const models = discoverModelsSafe();
669
+ const explanation = explainCursorModels(models);
670
+
671
+ if (options.json) {
672
+ console.log(JSON.stringify(explanation, null, 2));
673
+ return;
674
+ }
675
+
676
+ console.log(`Cursor models discovered: ${explanation.modelCount}`);
677
+ console.log(`Grouped Cursor models: ${explanation.groupedCount}`);
678
+ console.log(`Direct models: ${explanation.directCount}`);
679
+
680
+ if (!options.explain) {
681
+ return;
682
+ }
683
+
684
+ console.log("");
685
+ console.log("Model groups:");
686
+ for (const group of explanation.groups) {
687
+ console.log(` ${group.id}`);
688
+ console.log(` Default: ${group.defaultCursorModel}`);
689
+ const variants = Object.entries(group.variants);
690
+ if (variants.length === 0) {
691
+ console.log(" Variants: none");
692
+ continue;
693
+ }
694
+ console.log(" Variants:");
695
+ for (const [variant, cursorModel] of variants) {
696
+ console.log(` ${variant}: ${cursorModel}`);
697
+ }
698
+ }
699
+
700
+ console.log("");
701
+ console.log("Direct models:");
702
+ for (const modelId of explanation.direct) {
703
+ console.log(` ${modelId}`);
704
+ }
705
+ }
706
+
707
+ function printSyncResult(result: SyncModelsResult, options: Options) {
708
+ console.log(`Models synced: ${result.syncedCount}`);
709
+ if (options.variants) {
710
+ console.log(`Grouped Cursor models: ${result.groupedCount}`);
711
+ }
712
+ if (result.removedCount > 0) {
713
+ console.log(`Raw grouped models removed: ${result.removedCount}`);
714
+ }
715
+
716
+ console.log("Sync summary:");
717
+ console.log(` Added: ${result.summary.added}`);
718
+ console.log(` Updated: ${result.summary.updated}`);
719
+ console.log(` Removed: ${result.summary.removed}`);
720
+ console.log(` Priced: ${result.summary.priced}`);
721
+ console.log(` Skipped: ${result.summary.skipped}`);
722
+ }
723
+
468
724
  const NPM_PACKAGE = "@rama_nigg/open-cursor";
469
725
 
470
726
  function commandUninstall(options: Options) {
@@ -564,6 +820,115 @@ export function getStatusResult(configPath: string, pluginPath: string): StatusR
564
820
  };
565
821
  }
566
822
 
823
+ export function runDeepDoctorChecks(configPath: string): CheckResult[] {
824
+ const checks: CheckResult[] = [];
825
+ let config: any;
826
+
827
+ try {
828
+ config = readConfig(configPath);
829
+ } catch (error) {
830
+ return [{
831
+ name: "Deep config read",
832
+ passed: false,
833
+ message: error instanceof Error ? error.message : String(error),
834
+ }];
835
+ }
836
+
837
+ const provider = config.provider?.[PROVIDER_ID];
838
+ const models = isRecord(provider?.models) ? provider.models : {};
839
+ const baseUrl = typeof provider?.options?.baseURL === "string" ? provider.options.baseURL : "";
840
+
841
+ checks.push({
842
+ name: "Provider base URL",
843
+ passed: baseUrl.startsWith("http://") || baseUrl.startsWith("https://"),
844
+ message: baseUrl || "missing - run: open-cursor install",
845
+ });
846
+
847
+ checks.push({
848
+ name: "Provider models",
849
+ passed: Object.keys(models).length > 0,
850
+ message: `${Object.keys(models).length} configured model(s)`,
851
+ });
852
+
853
+ const variantEntryCount = countVariantModelEntries(models);
854
+ checks.push({
855
+ name: "Compact variants",
856
+ passed: variantEntryCount > 0,
857
+ warning: variantEntryCount === 0,
858
+ message: variantEntryCount > 0
859
+ ? `${variantEntryCount} model entr${variantEntryCount === 1 ? "y" : "ies"} with variants`
860
+ : "no compact variants found - run: open-cursor sync-models --variants --compact",
861
+ });
862
+
863
+ let discoveredModels: DiscoveredModel[];
864
+ try {
865
+ discoveredModels = discoverModelsFromCursorAgent();
866
+ checks.push({
867
+ name: "Cursor model discovery",
868
+ passed: true,
869
+ message: `${discoveredModels.length} model(s) from cursor-agent`,
870
+ });
871
+ } catch (error) {
872
+ checks.push({
873
+ name: "Cursor model discovery",
874
+ passed: false,
875
+ message: error instanceof Error ? error.message : String(error),
876
+ warning: true,
877
+ });
878
+ return checks;
879
+ }
880
+
881
+ const knownModelIds = new Set(discoveredModels.map(model => model.id));
882
+ const unknownTargets = collectConfiguredCursorModels(models)
883
+ .filter(modelId => !knownModelIds.has(modelId));
884
+ checks.push({
885
+ name: "Configured Cursor model targets",
886
+ passed: unknownTargets.length === 0,
887
+ warning: unknownTargets.length > 0,
888
+ message: unknownTargets.length === 0
889
+ ? "all configured targets exist in cursor-agent models"
890
+ : `${unknownTargets.length} target(s) not found: ${unknownTargets.slice(0, 5).join(", ")}`,
891
+ });
892
+
893
+ return checks;
894
+ }
895
+
896
+ function countVariantModelEntries(models: Record<string, unknown>): number {
897
+ return Object.values(models).filter(entry => {
898
+ return isRecord(entry) && isRecord(entry.variants) && Object.keys(entry.variants).length > 0;
899
+ }).length;
900
+ }
901
+
902
+ function collectConfiguredCursorModels(models: Record<string, unknown>): string[] {
903
+ const targets: string[] = [];
904
+
905
+ for (const [modelId, entry] of Object.entries(models)) {
906
+ if (!isRecord(entry)) {
907
+ targets.push(modelId);
908
+ continue;
909
+ }
910
+
911
+ const optionTarget = readCursorModel(entry.options);
912
+ targets.push(optionTarget || modelId);
913
+
914
+ if (!isRecord(entry.variants)) continue;
915
+ for (const variantEntry of Object.values(entry.variants)) {
916
+ const variantTarget = readCursorModel(variantEntry);
917
+ if (variantTarget) targets.push(variantTarget);
918
+ }
919
+ }
920
+
921
+ return [...new Set(targets)];
922
+ }
923
+
924
+ function readCursorModel(value: unknown): string | undefined {
925
+ if (!isRecord(value)) return undefined;
926
+ const cursorModel = value.cursorModel;
927
+ return typeof cursorModel === "string" && cursorModel.trim().length > 0
928
+ ? cursorModel.trim()
929
+ : undefined;
930
+ }
931
+
567
932
  function commandStatus(options: Options) {
568
933
  const { configPath, pluginPath } = resolvePaths(options);
569
934
  const result = getStatusResult(configPath, pluginPath);
@@ -600,7 +965,16 @@ function commandStatus(options: Options) {
600
965
 
601
966
  function commandDoctor(options: Options) {
602
967
  const { configPath, pluginPath } = resolvePaths(options);
603
- const checks = runDoctorChecks(configPath, pluginPath);
968
+ const checks = [
969
+ ...runDoctorChecks(configPath, pluginPath),
970
+ ...(options.deep ? runDeepDoctorChecks(configPath) : []),
971
+ ];
972
+
973
+ if (options.json) {
974
+ const failed = checks.filter(c => !c.passed && !c.warning);
975
+ console.log(JSON.stringify({ deep: options.deep === true, checks, failed: failed.length }, null, 2));
976
+ return;
977
+ }
604
978
 
605
979
  console.log("");
606
980
  for (const check of checks) {
@@ -638,6 +1012,9 @@ function main() {
638
1012
  case "sync-models":
639
1013
  commandSyncModels(parsed.options);
640
1014
  return;
1015
+ case "models":
1016
+ commandModels(parsed.options);
1017
+ return;
641
1018
  case "uninstall":
642
1019
  commandUninstall(parsed.options);
643
1020
  return;
@@ -658,4 +1035,6 @@ function main() {
658
1035
  }
659
1036
  }
660
1037
 
661
- main();
1038
+ if (process.env.NODE_ENV !== "test" && fileURLToPath(import.meta.url) === resolve(process.argv[1] || "")) {
1039
+ main();
1040
+ }
@@ -7,6 +7,7 @@ import {
7
7
  type StreamJsonEvent,
8
8
  } from '../streaming/types.js';
9
9
  import { createLogger } from '../utils/logger.js';
10
+ import { resolveCursorAgentBinary } from '../utils/binary.js';
10
11
 
11
12
  export interface CursorClientConfig {
12
13
  timeout?: number;
@@ -30,7 +31,7 @@ export class SimpleCursorClient {
30
31
  timeout: 30000,
31
32
  maxRetries: 3,
32
33
  streamOutput: true,
33
- cursorAgentPath: process.env.CURSOR_AGENT_EXECUTABLE || 'cursor-agent',
34
+ cursorAgentPath: resolveCursorAgentBinary(),
34
35
  ...config
35
36
  };
36
37
 
@@ -78,7 +79,8 @@ export class SimpleCursorClient {
78
79
 
79
80
  const child = spawn(this.config.cursorAgentPath, args, {
80
81
  cwd,
81
- stdio: ['pipe', 'pipe', 'pipe']
82
+ stdio: ['pipe', 'pipe', 'pipe'],
83
+ shell: process.platform === 'win32',
82
84
  });
83
85
 
84
86
  if (prompt) {
@@ -189,7 +191,8 @@ export class SimpleCursorClient {
189
191
  return new Promise((resolve, reject) => {
190
192
  const child = spawn(this.config.cursorAgentPath, args, {
191
193
  cwd,
192
- stdio: ['pipe', 'pipe', 'pipe']
194
+ stdio: ['pipe', 'pipe', 'pipe'],
195
+ shell: process.platform === 'win32',
193
196
  });
194
197
 
195
198
  let stdoutBuffer = '';
@@ -109,7 +109,7 @@ function mcpSchemaToZod(inputSchema: Record<string, unknown> | undefined, z: any
109
109
  zodType = z.array(z.any());
110
110
  break;
111
111
  case "object":
112
- zodType = z.record(z.any());
112
+ zodType = z.record(z.string(), z.any());
113
113
  break;
114
114
  default:
115
115
  zodType = z.any();
@@ -1,4 +1,5 @@
1
1
  import type { ModelInfo, DiscoveryConfig } from "./types.js";
2
+ import { resolveCursorAgentBinary } from "../utils/binary.js";
2
3
 
3
4
  interface CacheEntry {
4
5
  models: ModelInfo[];
@@ -51,7 +52,7 @@ export class ModelDiscoveryService {
51
52
  private async queryViaCLI(): Promise<ModelInfo[]> {
52
53
  try {
53
54
  const bunAny = (globalThis as any).Bun;
54
- const proc = bunAny.spawn(["cursor-agent", "models", "--json"], {
55
+ const proc = bunAny.spawn([resolveCursorAgentBinary(), "models", "--json"], {
55
56
  timeout: 5000,
56
57
  stdout: "pipe",
57
58
  stderr: "pipe"
@@ -78,7 +79,7 @@ export class ModelDiscoveryService {
78
79
  private async queryViaHelp(): Promise<ModelInfo[]> {
79
80
  try {
80
81
  const bunAny = (globalThis as any).Bun;
81
- const proc = bunAny.spawn(["cursor-agent", "--help"], {
82
+ const proc = bunAny.spawn([resolveCursorAgentBinary(), "--help"], {
82
83
  timeout: 5000,
83
84
  stdout: "pipe",
84
85
  stderr: "pipe"