@evanovation/open-cursor 2.4.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/LICENSE +28 -0
  2. package/README.md +270 -0
  3. package/dist/cli/discover.js +527 -0
  4. package/dist/cli/mcptool.js +10339 -0
  5. package/dist/cli/opencode-cursor.js +2989 -0
  6. package/dist/index.js +20588 -0
  7. package/dist/plugin-entry.js +19848 -0
  8. package/package.json +82 -0
  9. package/scripts/cursor-agent-runner.mjs +272 -0
  10. package/scripts/sdk-runner.mjs +412 -0
  11. package/src/acp/metrics.ts +83 -0
  12. package/src/acp/sessions.ts +107 -0
  13. package/src/acp/tools.ts +209 -0
  14. package/src/auth.ts +175 -0
  15. package/src/cli/discover.ts +53 -0
  16. package/src/cli/mcptool.ts +133 -0
  17. package/src/cli/model-discovery.ts +71 -0
  18. package/src/cli/opencode-cursor.ts +1195 -0
  19. package/src/client/cursor-agent-child.ts +459 -0
  20. package/src/client/sdk-child.ts +550 -0
  21. package/src/client/simple.ts +293 -0
  22. package/src/commands/status.ts +39 -0
  23. package/src/index.ts +39 -0
  24. package/src/mcp/client-manager.ts +166 -0
  25. package/src/mcp/config.ts +169 -0
  26. package/src/mcp/tool-bridge.ts +133 -0
  27. package/src/models/config.ts +64 -0
  28. package/src/models/discovery.ts +105 -0
  29. package/src/models/index.ts +3 -0
  30. package/src/models/pricing.ts +196 -0
  31. package/src/models/sync.ts +247 -0
  32. package/src/models/types.ts +11 -0
  33. package/src/models/variants.ts +446 -0
  34. package/src/plugin-entry.ts +28 -0
  35. package/src/plugin-toggle.ts +81 -0
  36. package/src/plugin.ts +2802 -0
  37. package/src/provider/backend.ts +71 -0
  38. package/src/provider/boundary.ts +168 -0
  39. package/src/provider/passthrough-tracker.ts +38 -0
  40. package/src/provider/runtime-interception.ts +818 -0
  41. package/src/provider/tool-loop-guard.ts +644 -0
  42. package/src/provider/tool-schema-compat.ts +800 -0
  43. package/src/provider.ts +268 -0
  44. package/src/proxy/formatter.ts +60 -0
  45. package/src/proxy/handler.ts +29 -0
  46. package/src/proxy/incremental-prompt.ts +74 -0
  47. package/src/proxy/prompt-builder.ts +204 -0
  48. package/src/proxy/server.ts +207 -0
  49. package/src/proxy/session-resume.ts +312 -0
  50. package/src/proxy/tool-loop.ts +359 -0
  51. package/src/proxy/types.ts +13 -0
  52. package/src/services/toast-service.ts +81 -0
  53. package/src/streaming/ai-sdk-parts.ts +109 -0
  54. package/src/streaming/delta-tracker.ts +89 -0
  55. package/src/streaming/line-buffer.ts +44 -0
  56. package/src/streaming/openai-sse.ts +118 -0
  57. package/src/streaming/parser.ts +22 -0
  58. package/src/streaming/types.ts +158 -0
  59. package/src/tools/core/executor.ts +25 -0
  60. package/src/tools/core/registry.ts +27 -0
  61. package/src/tools/core/types.ts +31 -0
  62. package/src/tools/defaults.ts +954 -0
  63. package/src/tools/discovery.ts +140 -0
  64. package/src/tools/executors/cli.ts +59 -0
  65. package/src/tools/executors/local.ts +25 -0
  66. package/src/tools/executors/mcp.ts +39 -0
  67. package/src/tools/executors/sdk.ts +39 -0
  68. package/src/tools/index.ts +8 -0
  69. package/src/tools/registry.ts +34 -0
  70. package/src/tools/router.ts +123 -0
  71. package/src/tools/schema.ts +58 -0
  72. package/src/tools/skills/loader.ts +61 -0
  73. package/src/tools/skills/resolver.ts +21 -0
  74. package/src/tools/types.ts +29 -0
  75. package/src/types.ts +8 -0
  76. package/src/usage.ts +112 -0
  77. package/src/utils/binary.ts +71 -0
  78. package/src/utils/errors.ts +224 -0
  79. package/src/utils/logger.ts +191 -0
  80. package/src/utils/perf.ts +76 -0
@@ -0,0 +1,209 @@
1
+ export interface ToolUpdate {
2
+ sessionId: string;
3
+ toolCallId: string;
4
+ title?: string;
5
+ kind?: 'read' | 'write' | 'edit' | 'search' | 'execute' | 'other';
6
+ status?: 'pending' | 'in_progress' | 'completed' | 'failed';
7
+ locations?: Array<{ path: string; line?: number }>;
8
+ content?: Array<{ type: string; [key: string]: unknown }>;
9
+ rawOutput?: string;
10
+ startTime?: number;
11
+ endTime?: number;
12
+ }
13
+
14
+ interface CursorEvent {
15
+ type: string;
16
+ call_id?: string;
17
+ tool_call_id?: string;
18
+ subtype?: string;
19
+ tool_call?: {
20
+ [key: string]: {
21
+ args?: Record<string, unknown>;
22
+ result?: Record<string, unknown>;
23
+ };
24
+ };
25
+ }
26
+
27
+ export class ToolMapper {
28
+ async mapCursorEventToAcp(event: CursorEvent, sessionId: string): Promise<ToolUpdate[]> {
29
+ if (event.type !== 'tool_call') {
30
+ return [];
31
+ }
32
+
33
+ const updates: ToolUpdate[] = [];
34
+ const toolCallId = event.call_id || event.tool_call_id || 'unknown';
35
+ const subtype = event.subtype || 'started';
36
+
37
+ // Completed/failed events return 1 update with results
38
+ if (subtype === 'completed' || subtype === 'failed') {
39
+ const result = this.extractResult(event.tool_call || {});
40
+ const locations = result.locations?.length ? result.locations : this.extractLocations(event.tool_call || {});
41
+
42
+ updates.push({
43
+ sessionId,
44
+ toolCallId,
45
+ title: this.buildToolTitle(event.tool_call || {}),
46
+ kind: this.inferToolType(event.tool_call || {}),
47
+ status: result.error ? 'failed' : 'completed',
48
+ content: result.content,
49
+ locations,
50
+ rawOutput: result.rawOutput,
51
+ endTime: Date.now()
52
+ });
53
+ } else {
54
+ // Started events return 2 updates: pending and in_progress
55
+ updates.push({
56
+ sessionId,
57
+ toolCallId,
58
+ title: this.buildToolTitle(event.tool_call || {}),
59
+ kind: this.inferToolType(event.tool_call || {}),
60
+ status: 'pending',
61
+ locations: this.extractLocations(event.tool_call || {}),
62
+ startTime: Date.now()
63
+ });
64
+
65
+ updates.push({
66
+ sessionId,
67
+ toolCallId,
68
+ status: 'in_progress'
69
+ });
70
+ }
71
+
72
+ return updates;
73
+ }
74
+
75
+ private inferToolType(toolCall: Record<string, unknown>): ToolUpdate['kind'] {
76
+ const keys = Object.keys(toolCall);
77
+ for (const key of keys) {
78
+ if (key.includes('read')) return 'read';
79
+ if (key.includes('write')) return 'edit';
80
+ if (key.includes('grep') || key.includes('glob')) return 'search';
81
+ if (key.includes('bash') || key.includes('shell')) return 'execute';
82
+ }
83
+ return 'other';
84
+ }
85
+
86
+ private buildToolTitle(toolCall: Record<string, unknown>): string {
87
+ const keys = Object.keys(toolCall);
88
+ for (const key of keys) {
89
+ const tool = toolCall[key] as { args?: Record<string, unknown> } | undefined;
90
+ const args = tool?.args || {};
91
+
92
+ if (key.includes('read') && args.path) return `Read ${args.path}`;
93
+ if (key.includes('write') && args.path) return `Write ${args.path}`;
94
+ if (key.includes('grep')) {
95
+ const pattern = args.pattern || 'pattern';
96
+ const path = args.path;
97
+ return path ? `Search ${path} for ${pattern}` : `Search for ${pattern}`;
98
+ }
99
+ if (key.includes('glob') && args.pattern) return `Glob ${args.pattern}`;
100
+ if ((key.includes('bash') || key.includes('shell')) && (args.command || args.cmd)) {
101
+ return `\`${args.command || args.cmd}\``;
102
+ }
103
+ if ((key.includes('bash') || key.includes('shell')) && args.commands && Array.isArray(args.commands)) {
104
+ return `\`${args.commands.join(' && ')}\``;
105
+ }
106
+ }
107
+ return 'other';
108
+ }
109
+
110
+ private extractLocations(toolCall: Record<string, unknown>): ToolUpdate['locations'] {
111
+ const keys = Object.keys(toolCall);
112
+ for (const key of keys) {
113
+ const tool = toolCall[key] as { args?: Record<string, unknown> } | undefined;
114
+ const args = tool?.args || {};
115
+
116
+ if (args.path) {
117
+ if (typeof args.path === 'string') {
118
+ return [{ path: args.path, line: args.line as number | undefined }];
119
+ }
120
+ if (Array.isArray(args.path)) {
121
+ return args.path.map((p: string | { path: string; line?: number }) =>
122
+ typeof p === 'string' ? { path: p } : { path: p.path, line: p.line }
123
+ );
124
+ }
125
+ }
126
+
127
+ if (args.paths && Array.isArray(args.paths)) {
128
+ return args.paths.map((p: string | { path: string; line?: number }) =>
129
+ typeof p === 'string' ? { path: p } : { path: p.path, line: p.line }
130
+ );
131
+ }
132
+ }
133
+ return undefined;
134
+ }
135
+
136
+ private extractResult(toolCall: Record<string, unknown>): {
137
+ error?: string;
138
+ content?: ToolUpdate['content'];
139
+ locations?: ToolUpdate['locations'];
140
+ rawOutput?: string;
141
+ } {
142
+ const keys = Object.keys(toolCall);
143
+ for (const key of keys) {
144
+ const tool = toolCall[key] as {
145
+ result?: Record<string, unknown>;
146
+ args?: Record<string, unknown>;
147
+ } | undefined;
148
+ const result = tool?.result || {};
149
+
150
+ if (result.error) {
151
+ return { error: result.error as string };
152
+ }
153
+
154
+ const locations: ToolUpdate['locations'] = [];
155
+ if (result.matches && Array.isArray(result.matches)) {
156
+ locations.push(...result.matches.map((m: { path: string; line?: number }) => ({
157
+ path: m.path,
158
+ line: m.line
159
+ })));
160
+ }
161
+ if (result.files && Array.isArray(result.files)) {
162
+ locations.push(...result.files.map((f: string) => ({ path: f })));
163
+ }
164
+ if (result.path) {
165
+ locations.push({ path: result.path as string, line: result.line as number | undefined });
166
+ }
167
+
168
+ const content: ToolUpdate['content'] = [];
169
+
170
+ // Handle write operations with diff generation
171
+ if (key.includes('write')) {
172
+ const oldText = result.oldText ?? null;
173
+ const newText = result.newText as string | undefined;
174
+ const path = (tool?.args?.path as string) || (result.path as string);
175
+ if (newText !== undefined || oldText !== undefined) {
176
+ content.push({
177
+ type: 'diff',
178
+ path,
179
+ oldText,
180
+ newText
181
+ });
182
+ }
183
+ }
184
+
185
+ if (result.content) {
186
+ content.push({
187
+ type: 'content',
188
+ content: { text: result.content as string }
189
+ });
190
+ }
191
+
192
+ if (result.output !== undefined || result.exitCode !== undefined) {
193
+ content.push({
194
+ type: 'content',
195
+ content: {
196
+ text: `Exit code: ${result.exitCode ?? 0}\n${result.output || '(no output)'}`
197
+ }
198
+ });
199
+ }
200
+
201
+ return {
202
+ content: content.length > 0 ? content : undefined,
203
+ locations: locations.length > 0 ? locations : undefined,
204
+ rawOutput: JSON.stringify(result)
205
+ };
206
+ }
207
+ return {};
208
+ }
209
+ }
package/src/auth.ts ADDED
@@ -0,0 +1,175 @@
1
+ // src/auth.ts
2
+
3
+ import { existsSync } from "fs";
4
+ import { homedir, platform } from "os";
5
+ import { join } from "path";
6
+ import { createLogger } from "./utils/logger";
7
+
8
+ const log = createLogger("auth");
9
+
10
+ // Polling configuration for auth file detection
11
+ const AUTH_POLL_INTERVAL = 2000; // Check every 2 seconds
12
+ const AUTH_POLL_TIMEOUT = 5 * 60 * 1000; // 5 minutes total timeout
13
+ const URL_EXTRACTION_TIMEOUT = 10000; // Wait up to 10 seconds for URL
14
+
15
+ export interface AuthResult {
16
+ type: "success" | "failed";
17
+ provider?: string;
18
+ key?: string;
19
+ error?: string;
20
+ }
21
+
22
+ export interface ResolveSdkApiKeyInput {
23
+ env?: Pick<NodeJS.ProcessEnv, "CURSOR_API_KEY">;
24
+ storedApiKey?: string;
25
+ authorizationHeader?: string | null;
26
+ }
27
+
28
+ const PLACEHOLDER_API_KEYS = new Set(["cursor-agent"]);
29
+
30
+ function getHomeDir(): string {
31
+ const override = process.env.CURSOR_ACP_HOME_DIR;
32
+ if (override && override.length > 0) {
33
+ return override;
34
+ }
35
+ return homedir();
36
+ }
37
+
38
+ export async function pollForAuthFile(
39
+ timeoutMs: number = AUTH_POLL_TIMEOUT,
40
+ intervalMs: number = AUTH_POLL_INTERVAL
41
+ ): Promise<boolean> {
42
+ const startTime = Date.now();
43
+ const possiblePaths = getPossibleAuthPaths();
44
+
45
+ return new Promise((resolve) => {
46
+ const check = () => {
47
+ const elapsed = Date.now() - startTime;
48
+
49
+ for (const authPath of possiblePaths) {
50
+ if (existsSync(authPath)) {
51
+ log.debug("Auth file detected", { path: authPath });
52
+ resolve(true);
53
+ return;
54
+ }
55
+ }
56
+
57
+ log.debug("Polling for auth file", {
58
+ checkedPaths: possiblePaths,
59
+ elapsed: `${elapsed}ms`,
60
+ timeout: `${timeoutMs}ms`,
61
+ });
62
+
63
+ if (elapsed >= timeoutMs) {
64
+ log.debug("Auth file polling timed out");
65
+ resolve(false);
66
+ return;
67
+ }
68
+
69
+ setTimeout(check, intervalMs);
70
+ };
71
+
72
+ check();
73
+ });
74
+ }
75
+
76
+ export function verifyCursorAuth(): boolean {
77
+ // API key takes priority over auth file
78
+ const apiKey = process.env.CURSOR_API_KEY;
79
+ if (apiKey && apiKey.trim().length > 0) {
80
+ log.debug("CURSOR_API_KEY found, auth verified");
81
+ return true;
82
+ }
83
+
84
+ const possiblePaths = getPossibleAuthPaths();
85
+ for (const authPath of possiblePaths) {
86
+ if (existsSync(authPath)) {
87
+ log.debug("Auth file found", { path: authPath });
88
+ return true;
89
+ }
90
+ }
91
+
92
+ log.debug("No auth found (no CURSOR_API_KEY, no auth file)", { checkedPaths: possiblePaths });
93
+ return false;
94
+ }
95
+
96
+ export function isUsableSdkApiKey(value: string | undefined | null): value is string {
97
+ const trimmed = value?.trim();
98
+ if (!trimmed) {
99
+ return false;
100
+ }
101
+
102
+ return !PLACEHOLDER_API_KEYS.has(trimmed.toLowerCase());
103
+ }
104
+
105
+ export function normalizeAuthorizationHeader(value: string | null | undefined): string | undefined {
106
+ const trimmed = value?.trim();
107
+ if (!trimmed) {
108
+ return undefined;
109
+ }
110
+
111
+ const bearerMatch = /^bearer\s+(.+)$/i.exec(trimmed);
112
+ return bearerMatch?.[1]?.trim() ?? trimmed;
113
+ }
114
+
115
+ export function resolveSdkApiKey(input: ResolveSdkApiKeyInput): string | undefined {
116
+ const candidates = [
117
+ input.env?.CURSOR_API_KEY,
118
+ input.storedApiKey,
119
+ normalizeAuthorizationHeader(input.authorizationHeader),
120
+ ];
121
+
122
+ return candidates.find(isUsableSdkApiKey)?.trim();
123
+ }
124
+
125
+ /**
126
+ * Returns all possible auth file paths in priority order.
127
+ * Checks both auth.json (legacy) and cli-config.json (current cursor-agent format).
128
+ * - macOS: ~/.cursor/ (primary), ~/.config/cursor/ (fallback)
129
+ * - Linux: ~/.config/cursor/ (XDG), XDG_CONFIG_HOME/cursor/, ~/.cursor/
130
+ */
131
+ export function getPossibleAuthPaths(): string[] {
132
+ const home = getHomeDir();
133
+ const paths: string[] = [];
134
+ const isDarwin = platform() === "darwin";
135
+
136
+ const authFiles = ["cli-config.json", "auth.json"];
137
+
138
+ if (isDarwin) {
139
+ for (const file of authFiles) {
140
+ paths.push(join(home, ".cursor", file));
141
+ }
142
+ for (const file of authFiles) {
143
+ paths.push(join(home, ".config", "cursor", file));
144
+ }
145
+ } else {
146
+ for (const file of authFiles) {
147
+ paths.push(join(home, ".config", "cursor", file));
148
+ }
149
+
150
+ const xdgConfig = process.env.XDG_CONFIG_HOME;
151
+ if (xdgConfig && xdgConfig !== join(home, ".config")) {
152
+ for (const file of authFiles) {
153
+ paths.push(join(xdgConfig, "cursor", file));
154
+ }
155
+ }
156
+
157
+ for (const file of authFiles) {
158
+ paths.push(join(home, ".cursor", file));
159
+ }
160
+ }
161
+
162
+ return paths;
163
+ }
164
+
165
+ export function getAuthFilePath(): string {
166
+ const possiblePaths = getPossibleAuthPaths();
167
+
168
+ for (const authPath of possiblePaths) {
169
+ if (existsSync(authPath)) {
170
+ return authPath;
171
+ }
172
+ }
173
+
174
+ return possiblePaths[0];
175
+ }
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, writeFileSync, existsSync } from "fs";
3
+ import { join } from "path";
4
+ import { homedir } from "os";
5
+ import {
6
+ discoverModelsFromCursorAgent,
7
+ fallbackModels,
8
+ } from "./model-discovery.js";
9
+
10
+ async function main() {
11
+ console.log("Discovering Cursor models...");
12
+ let models = fallbackModels();
13
+ try {
14
+ models = discoverModelsFromCursorAgent();
15
+ } catch (error) {
16
+ const message = error instanceof Error ? error.message : String(error);
17
+ console.warn(`Warning: cursor-agent model discovery failed, using fallback list (${message})`);
18
+ }
19
+
20
+ console.log(`Found ${models.length} models:`);
21
+ for (const model of models) {
22
+ console.log(` - ${model.id}: ${model.name}`);
23
+ }
24
+
25
+ // Update config
26
+ const configPath = join(homedir(), ".config/opencode/opencode.json");
27
+
28
+ if (!existsSync(configPath)) {
29
+ console.error(`Config not found: ${configPath}`);
30
+ process.exit(1);
31
+ }
32
+
33
+ const existingConfig = JSON.parse(readFileSync(configPath, "utf-8"));
34
+
35
+ // Update cursor-acp provider models
36
+ if (existingConfig.provider?.["cursor-acp"]) {
37
+ const formatted = Object.fromEntries(models.map((model) => [model.id, { name: model.name }]));
38
+ existingConfig.provider["cursor-acp"].models = {
39
+ ...existingConfig.provider["cursor-acp"].models,
40
+ ...formatted
41
+ };
42
+
43
+ writeFileSync(configPath, JSON.stringify(existingConfig, null, 2));
44
+ console.log(`Updated ${configPath}`);
45
+ } else {
46
+ console.error("cursor-acp provider not found in config");
47
+ process.exit(1);
48
+ }
49
+
50
+ console.log("Done!");
51
+ }
52
+
53
+ main().catch(console.error);
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * mcptool — CLI for calling MCP server tools from the shell.
5
+ *
6
+ * Usage:
7
+ * mcptool servers List configured MCP servers
8
+ * mcptool tools [server] List tools (optionally filter by server)
9
+ * mcptool call <server> <tool> [json-args] Call a tool
10
+ *
11
+ * Reads MCP server configuration from opencode.json (same config the plugin uses).
12
+ */
13
+
14
+ import { readMcpConfigs } from "../mcp/config.js";
15
+ import { McpClientManager } from "../mcp/client-manager.js";
16
+
17
+ const USAGE = `mcptool — call MCP server tools from the shell
18
+
19
+ Usage:
20
+ mcptool servers List configured servers
21
+ mcptool tools [server] List available tools
22
+ mcptool call <server> <tool> [json-args] Call a tool
23
+
24
+ Examples:
25
+ mcptool servers
26
+ mcptool tools
27
+ mcptool tools hybrid-memory
28
+ mcptool call hybrid-memory memory_stats
29
+ mcptool call hybrid-memory memory_search '{"query":"auth"}'
30
+ mcptool call test-filesystem list_directory '{"path":"/tmp"}'`;
31
+
32
+ async function main(): Promise<void> {
33
+ const args = process.argv.slice(2);
34
+
35
+ if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
36
+ console.log(USAGE);
37
+ process.exit(0);
38
+ }
39
+
40
+ const command = args[0];
41
+ const configs = readMcpConfigs();
42
+
43
+ if (configs.length === 0) {
44
+ console.error("No MCP servers configured in opencode.json");
45
+ process.exit(1);
46
+ }
47
+
48
+ const manager = new McpClientManager();
49
+
50
+ if (command === "servers") {
51
+ for (const c of configs) {
52
+ const detail =
53
+ c.type === "local" ? c.command.join(" ") : (c as any).url ?? "";
54
+ console.log(`${c.name} (${c.type}) ${detail}`);
55
+ }
56
+ process.exit(0);
57
+ }
58
+
59
+ if (command === "tools") {
60
+ const filter = args[1];
61
+ const toConnect = filter
62
+ ? configs.filter((c) => c.name === filter)
63
+ : configs;
64
+
65
+ if (filter && toConnect.length === 0) {
66
+ console.error(`Unknown server: ${filter}`);
67
+ console.error(`Available: ${configs.map((c) => c.name).join(", ")}`);
68
+ process.exit(1);
69
+ }
70
+
71
+ await Promise.allSettled(toConnect.map((c) => manager.connectServer(c)));
72
+ const tools = manager.listTools();
73
+
74
+ if (tools.length === 0) {
75
+ console.log("No tools discovered.");
76
+ } else {
77
+ for (const t of tools) {
78
+ const params = t.inputSchema
79
+ ? Object.keys((t.inputSchema as any).properties ?? {}).join(", ")
80
+ : "";
81
+ console.log(`${t.serverName}/${t.name} ${t.description ?? ""}`);
82
+ if (params) console.log(` params: ${params}`);
83
+ }
84
+ }
85
+
86
+ await manager.disconnectAll();
87
+ process.exit(0);
88
+ }
89
+
90
+ if (command === "call") {
91
+ const serverName = args[1];
92
+ const toolName = args[2];
93
+ const rawArgs = args[3];
94
+
95
+ if (!serverName || !toolName) {
96
+ console.error("Usage: mcptool call <server> <tool> [json-args]");
97
+ process.exit(1);
98
+ }
99
+
100
+ const config = configs.find((c) => c.name === serverName);
101
+ if (!config) {
102
+ console.error(`Unknown server: ${serverName}`);
103
+ console.error(`Available: ${configs.map((c) => c.name).join(", ")}`);
104
+ process.exit(1);
105
+ }
106
+
107
+ let toolArgs: Record<string, unknown> = {};
108
+ if (rawArgs) {
109
+ try {
110
+ toolArgs = JSON.parse(rawArgs);
111
+ } catch {
112
+ console.error(`Invalid JSON args: ${rawArgs}`);
113
+ process.exit(1);
114
+ }
115
+ }
116
+
117
+ await manager.connectServer(config);
118
+ const result = await manager.callTool(serverName, toolName, toolArgs);
119
+ console.log(result);
120
+
121
+ await manager.disconnectAll();
122
+ process.exit(0);
123
+ }
124
+
125
+ console.error(`Unknown command: ${command}`);
126
+ console.log(USAGE);
127
+ process.exit(1);
128
+ }
129
+
130
+ main().catch((err) => {
131
+ console.error(`mcptool error: ${err.message || err}`);
132
+ process.exit(1);
133
+ });
@@ -0,0 +1,71 @@
1
+ import { execFileSync } from "child_process";
2
+ import { stripAnsi } from "../utils/errors.js";
3
+ import { resolveCursorAgentBinary } from "../utils/binary.js";
4
+
5
+ const MODEL_DISCOVERY_TIMEOUT_MS = 5000;
6
+
7
+ export type DiscoveredModel = {
8
+ id: string;
9
+ name: string;
10
+ };
11
+
12
+ export function parseCursorModelsOutput(output: string): DiscoveredModel[] {
13
+ const clean = stripAnsi(output);
14
+ const models: DiscoveredModel[] = [];
15
+ const seen = new Set<string>();
16
+
17
+ for (const line of clean.split("\n")) {
18
+ const trimmed = line.trim();
19
+ if (!trimmed) continue;
20
+ const match = trimmed.match(
21
+ /^([a-zA-Z0-9._-]+)\s+-\s+(.+?)(?:\s+\((?:current|default)\))*\s*$/,
22
+ );
23
+ if (!match) continue;
24
+
25
+ const id = match[1];
26
+ if (seen.has(id)) continue;
27
+ seen.add(id);
28
+ models.push({ id, name: match[2].trim() });
29
+ }
30
+
31
+ return models;
32
+ }
33
+
34
+ export function discoverModelsFromCursorAgent(): DiscoveredModel[] {
35
+ const raw = execFileSync(resolveCursorAgentBinary(), ["models"], {
36
+ encoding: "utf8",
37
+ ...(process.platform !== "win32" && { killSignal: "SIGTERM" as const }),
38
+ stdio: ["ignore", "pipe", "pipe"],
39
+ timeout: MODEL_DISCOVERY_TIMEOUT_MS,
40
+ });
41
+ const models = parseCursorModelsOutput(raw);
42
+ if (models.length === 0) {
43
+ throw new Error("No models parsed from cursor-agent output");
44
+ }
45
+ return models;
46
+ }
47
+
48
+ export function fallbackModels(): DiscoveredModel[] {
49
+ return [
50
+ { id: "auto", name: "Auto" },
51
+ { id: "composer-1.5", name: "Composer 1.5" },
52
+ { id: "composer-1", name: "Composer 1" },
53
+ { id: "opus-4.6-thinking", name: "Claude 4.6 Opus (Thinking)" },
54
+ { id: "opus-4.6", name: "Claude 4.6 Opus" },
55
+ { id: "sonnet-4.6", name: "Claude 4.6 Sonnet" },
56
+ { id: "sonnet-4.6-thinking", name: "Claude 4.6 Sonnet (Thinking)" },
57
+ { id: "opus-4.5", name: "Claude 4.5 Opus" },
58
+ { id: "opus-4.5-thinking", name: "Claude 4.5 Opus (Thinking)" },
59
+ { id: "sonnet-4.5", name: "Claude 4.5 Sonnet" },
60
+ { id: "sonnet-4.5-thinking", name: "Claude 4.5 Sonnet (Thinking)" },
61
+ { id: "gpt-5.4-high", name: "GPT-5.4 High" },
62
+ { id: "gpt-5.4-medium", name: "GPT-5.4" },
63
+ { id: "gpt-5.3-codex", name: "GPT-5.3 Codex" },
64
+ { id: "gpt-5.2", name: "GPT-5.2" },
65
+ { id: "gemini-3.1-pro", name: "Gemini 3.1 Pro" },
66
+ { id: "gemini-3-pro", name: "Gemini 3 Pro" },
67
+ { id: "gemini-3-flash", name: "Gemini 3 Flash" },
68
+ { id: "grok", name: "Grok" },
69
+ { id: "kimi-k2.5", name: "Kimi K2.5" },
70
+ ];
71
+ }