@rama_nigg/open-cursor 2.3.20 → 2.4.0
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 +578 -226
- package/dist/plugin-entry.js +556 -206
- 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 +166 -0
- package/src/tools/executors/cli.ts +1 -0
- package/src/usage.ts +112 -0
- package/src/utils/binary.ts +57 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rama_nigg/open-cursor",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
433
|
-
|
|
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
|
|
439
|
-
|
|
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
|
-
|
|
446
|
-
|
|
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(
|
|
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
|
|
459
|
-
|
|
460
|
-
|
|
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
|
-
|
|
464
|
-
|
|
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 =
|
|
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
|
-
|
|
1038
|
+
if (process.env.NODE_ENV !== "test" && fileURLToPath(import.meta.url) === resolve(process.argv[1] || "")) {
|
|
1039
|
+
main();
|
|
1040
|
+
}
|
package/src/client/simple.ts
CHANGED
|
@@ -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:
|
|
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 = '';
|
package/src/mcp/tool-bridge.ts
CHANGED
|
@@ -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();
|
package/src/models/discovery.ts
CHANGED
|
@@ -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([
|
|
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([
|
|
82
|
+
const proc = bunAny.spawn([resolveCursorAgentBinary(), "--help"], {
|
|
82
83
|
timeout: 5000,
|
|
83
84
|
stdout: "pipe",
|
|
84
85
|
stderr: "pipe"
|