@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.
- package/LICENSE +28 -0
- package/README.md +270 -0
- package/dist/cli/discover.js +527 -0
- package/dist/cli/mcptool.js +10339 -0
- package/dist/cli/opencode-cursor.js +2989 -0
- package/dist/index.js +20588 -0
- package/dist/plugin-entry.js +19848 -0
- package/package.json +82 -0
- package/scripts/cursor-agent-runner.mjs +272 -0
- package/scripts/sdk-runner.mjs +412 -0
- package/src/acp/metrics.ts +83 -0
- package/src/acp/sessions.ts +107 -0
- package/src/acp/tools.ts +209 -0
- package/src/auth.ts +175 -0
- package/src/cli/discover.ts +53 -0
- package/src/cli/mcptool.ts +133 -0
- package/src/cli/model-discovery.ts +71 -0
- package/src/cli/opencode-cursor.ts +1195 -0
- package/src/client/cursor-agent-child.ts +459 -0
- package/src/client/sdk-child.ts +550 -0
- package/src/client/simple.ts +293 -0
- package/src/commands/status.ts +39 -0
- package/src/index.ts +39 -0
- package/src/mcp/client-manager.ts +166 -0
- package/src/mcp/config.ts +169 -0
- package/src/mcp/tool-bridge.ts +133 -0
- package/src/models/config.ts +64 -0
- package/src/models/discovery.ts +105 -0
- package/src/models/index.ts +3 -0
- package/src/models/pricing.ts +196 -0
- package/src/models/sync.ts +247 -0
- package/src/models/types.ts +11 -0
- package/src/models/variants.ts +446 -0
- package/src/plugin-entry.ts +28 -0
- package/src/plugin-toggle.ts +81 -0
- package/src/plugin.ts +2802 -0
- package/src/provider/backend.ts +71 -0
- package/src/provider/boundary.ts +168 -0
- package/src/provider/passthrough-tracker.ts +38 -0
- package/src/provider/runtime-interception.ts +818 -0
- package/src/provider/tool-loop-guard.ts +644 -0
- package/src/provider/tool-schema-compat.ts +800 -0
- package/src/provider.ts +268 -0
- package/src/proxy/formatter.ts +60 -0
- package/src/proxy/handler.ts +29 -0
- package/src/proxy/incremental-prompt.ts +74 -0
- package/src/proxy/prompt-builder.ts +204 -0
- package/src/proxy/server.ts +207 -0
- package/src/proxy/session-resume.ts +312 -0
- package/src/proxy/tool-loop.ts +359 -0
- package/src/proxy/types.ts +13 -0
- package/src/services/toast-service.ts +81 -0
- package/src/streaming/ai-sdk-parts.ts +109 -0
- package/src/streaming/delta-tracker.ts +89 -0
- package/src/streaming/line-buffer.ts +44 -0
- package/src/streaming/openai-sse.ts +118 -0
- package/src/streaming/parser.ts +22 -0
- package/src/streaming/types.ts +158 -0
- package/src/tools/core/executor.ts +25 -0
- package/src/tools/core/registry.ts +27 -0
- package/src/tools/core/types.ts +31 -0
- package/src/tools/defaults.ts +954 -0
- package/src/tools/discovery.ts +140 -0
- package/src/tools/executors/cli.ts +59 -0
- package/src/tools/executors/local.ts +25 -0
- package/src/tools/executors/mcp.ts +39 -0
- package/src/tools/executors/sdk.ts +39 -0
- package/src/tools/index.ts +8 -0
- package/src/tools/registry.ts +34 -0
- package/src/tools/router.ts +123 -0
- package/src/tools/schema.ts +58 -0
- package/src/tools/skills/loader.ts +61 -0
- package/src/tools/skills/resolver.ts +21 -0
- package/src/tools/types.ts +29 -0
- package/src/types.ts +8 -0
- package/src/usage.ts +112 -0
- package/src/utils/binary.ts +71 -0
- package/src/utils/errors.ts +224 -0
- package/src/utils/logger.ts +191 -0
- package/src/utils/perf.ts +76 -0
package/src/acp/tools.ts
ADDED
|
@@ -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
|
+
}
|