@stablekernel/opencode-cursor 0.1.0-rc.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/CHANGELOG.md +88 -0
- package/LICENSE +21 -0
- package/README.md +356 -0
- package/dist/chunk-YYO6O43T.js +502 -0
- package/dist/chunk-YYO6O43T.js.map +1 -0
- package/dist/plugin/index.d.ts +17 -0
- package/dist/plugin/index.js +572 -0
- package/dist/plugin/index.js.map +1 -0
- package/dist/provider/index.d.ts +77 -0
- package/dist/provider/index.js +427 -0
- package/dist/provider/index.js.map +1 -0
- package/dist/sidecar/agent-host.d.ts +128 -0
- package/dist/sidecar/agent-host.js +93 -0
- package/dist/sidecar/agent-host.js.map +1 -0
- package/package.json +68 -0
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
import {
|
|
2
|
+
acquireAgent,
|
|
3
|
+
buildModelSelection,
|
|
4
|
+
fingerprintApiKey,
|
|
5
|
+
loadCursorSdk,
|
|
6
|
+
resolveControls,
|
|
7
|
+
resolveCursorApiKey,
|
|
8
|
+
streamAgentTurn
|
|
9
|
+
} from "../chunk-YYO6O43T.js";
|
|
10
|
+
|
|
11
|
+
// src/model-cache.ts
|
|
12
|
+
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
13
|
+
import { homedir, tmpdir } from "os";
|
|
14
|
+
import { join } from "path";
|
|
15
|
+
var DEFAULT_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
16
|
+
function ttlMs() {
|
|
17
|
+
const raw = process.env.OPENCODE_CURSOR_MODEL_CACHE_TTL_MS;
|
|
18
|
+
const parsed = raw ? Number.parseInt(raw, 10) : NaN;
|
|
19
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_TTL_MS;
|
|
20
|
+
}
|
|
21
|
+
function cacheDir() {
|
|
22
|
+
const base = process.env.XDG_CACHE_HOME?.trim() || (homedir() ? join(homedir(), ".cache") : tmpdir());
|
|
23
|
+
return join(base, "opencode-cursor");
|
|
24
|
+
}
|
|
25
|
+
function cacheFile(fingerprint) {
|
|
26
|
+
return join(cacheDir(), `models-${fingerprint}.json`);
|
|
27
|
+
}
|
|
28
|
+
function latestCacheFile() {
|
|
29
|
+
return join(cacheDir(), "models-latest.json");
|
|
30
|
+
}
|
|
31
|
+
var LATEST_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
32
|
+
function readCacheFile(file, maxAgeMs) {
|
|
33
|
+
try {
|
|
34
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
35
|
+
if (!parsed?.savedAt || !Array.isArray(parsed.models)) return void 0;
|
|
36
|
+
if (Date.now() - parsed.savedAt > maxAgeMs) return void 0;
|
|
37
|
+
return parsed.models;
|
|
38
|
+
} catch {
|
|
39
|
+
return void 0;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function writeCacheFile(file, models) {
|
|
43
|
+
try {
|
|
44
|
+
mkdirSync(cacheDir(), { recursive: true });
|
|
45
|
+
const envelope = { savedAt: Date.now(), models };
|
|
46
|
+
writeFileSync(file, JSON.stringify(envelope), "utf8");
|
|
47
|
+
} catch {
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function readModelCache(fingerprint) {
|
|
51
|
+
return readCacheFile(cacheFile(fingerprint), ttlMs());
|
|
52
|
+
}
|
|
53
|
+
function writeModelCache(fingerprint, models) {
|
|
54
|
+
writeCacheFile(cacheFile(fingerprint), models);
|
|
55
|
+
writeCacheFile(latestCacheFile(), models);
|
|
56
|
+
}
|
|
57
|
+
function readLatestModelCache() {
|
|
58
|
+
return readCacheFile(latestCacheFile(), LATEST_TTL_MS);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/fallback-models.ts
|
|
62
|
+
var FALLBACK_MODELS = [
|
|
63
|
+
{
|
|
64
|
+
id: "composer-2.5",
|
|
65
|
+
displayName: "Composer 2.5",
|
|
66
|
+
description: "Cursor's default agent model (fallback entry).",
|
|
67
|
+
parameters: [
|
|
68
|
+
{ id: "thinking", displayName: "Thinking", values: [{ value: "off" }, { value: "on" }] }
|
|
69
|
+
]
|
|
70
|
+
},
|
|
71
|
+
{ id: "claude-opus-4-8", displayName: "Claude Opus 4.8 (via Cursor)" },
|
|
72
|
+
{ id: "claude-sonnet-4-6", displayName: "Claude Sonnet 4.6 (via Cursor)" },
|
|
73
|
+
{ id: "gpt-5.5", displayName: "GPT-5.5 (via Cursor)" }
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
// src/model-discovery.ts
|
|
77
|
+
async function discoverModels(options = {}) {
|
|
78
|
+
const apiKey = resolveCursorApiKey(options.apiKey);
|
|
79
|
+
if (!apiKey) {
|
|
80
|
+
const latest = readLatestModelCache();
|
|
81
|
+
if (latest && latest.length > 0) return { models: latest, source: "cache" };
|
|
82
|
+
return {
|
|
83
|
+
models: FALLBACK_MODELS,
|
|
84
|
+
source: "fallback",
|
|
85
|
+
warning: "No Cursor API key found. Run `opencode auth login` and choose Cursor, or set CURSOR_API_KEY. Showing fallback models."
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const fingerprint = fingerprintApiKey(apiKey);
|
|
89
|
+
if (!options.forceRefresh) {
|
|
90
|
+
const cached = readModelCache(fingerprint);
|
|
91
|
+
if (cached && cached.length > 0) {
|
|
92
|
+
return { models: cached, source: "cache" };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
const { Cursor } = await loadCursorSdk();
|
|
97
|
+
const models = await Cursor.models.list({ apiKey });
|
|
98
|
+
if (models.length > 0) {
|
|
99
|
+
writeModelCache(fingerprint, models);
|
|
100
|
+
return { models, source: "live" };
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
models: FALLBACK_MODELS,
|
|
104
|
+
source: "fallback",
|
|
105
|
+
warning: "Cursor.models.list() returned no models; showing fallback models."
|
|
106
|
+
};
|
|
107
|
+
} catch (err) {
|
|
108
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
109
|
+
const stale = readModelCache(fingerprint);
|
|
110
|
+
if (stale && stale.length > 0) {
|
|
111
|
+
return { models: stale, source: "cache", warning: `Live discovery failed (${detail}); using cached models.` };
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
models: FALLBACK_MODELS,
|
|
115
|
+
source: "fallback",
|
|
116
|
+
warning: `Live discovery failed (${detail}); showing fallback models.`
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function modelSupportsReasoning(item) {
|
|
121
|
+
return (item.parameters ?? []).some((p) => /think|reason/i.test(p.id));
|
|
122
|
+
}
|
|
123
|
+
function toOpencodeModels(items) {
|
|
124
|
+
const out = {};
|
|
125
|
+
for (const item of items) {
|
|
126
|
+
out[item.id] = {
|
|
127
|
+
id: item.id,
|
|
128
|
+
name: item.displayName || item.id,
|
|
129
|
+
attachment: true,
|
|
130
|
+
reasoning: modelSupportsReasoning(item),
|
|
131
|
+
temperature: false,
|
|
132
|
+
tool_call: true
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/model-variants.ts
|
|
139
|
+
var REASONING_PARAM = /think|reason|effort/i;
|
|
140
|
+
function buildModelVariants(item) {
|
|
141
|
+
const out = {};
|
|
142
|
+
for (const param of item.parameters ?? []) {
|
|
143
|
+
if (!REASONING_PARAM.test(param.id)) continue;
|
|
144
|
+
for (const { value } of param.values ?? []) {
|
|
145
|
+
const key = param.id.toLowerCase() === "thinking" ? value : `${param.id}-${value}`;
|
|
146
|
+
out[key] = { params: { [param.id]: value } };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
out["plan"] = { mode: "plan" };
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// src/plugin/model-v2.ts
|
|
154
|
+
var PROVIDER_ID = "cursor";
|
|
155
|
+
var NPM_PACKAGE = "@stablekernel/opencode-cursor";
|
|
156
|
+
function providerNpm() {
|
|
157
|
+
return process.env.OPENCODE_CURSOR_PROVIDER_NPM?.trim() || NPM_PACKAGE;
|
|
158
|
+
}
|
|
159
|
+
function buildModelV2Map(items) {
|
|
160
|
+
const out = {};
|
|
161
|
+
for (const item of items) {
|
|
162
|
+
out[item.id] = {
|
|
163
|
+
id: item.id,
|
|
164
|
+
providerID: PROVIDER_ID,
|
|
165
|
+
api: { id: item.id, url: "", npm: providerNpm() },
|
|
166
|
+
name: item.displayName || item.id,
|
|
167
|
+
capabilities: {
|
|
168
|
+
temperature: false,
|
|
169
|
+
reasoning: modelSupportsReasoning(item),
|
|
170
|
+
attachment: true,
|
|
171
|
+
toolcall: true,
|
|
172
|
+
input: { text: true, audio: false, image: true, video: false, pdf: false },
|
|
173
|
+
output: { text: true, audio: false, image: false, video: false, pdf: false },
|
|
174
|
+
interleaved: false
|
|
175
|
+
},
|
|
176
|
+
cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
|
|
177
|
+
limit: { context: 2e5, output: 32e3 },
|
|
178
|
+
status: "active",
|
|
179
|
+
options: {},
|
|
180
|
+
headers: {},
|
|
181
|
+
release_date: "",
|
|
182
|
+
variants: buildModelVariants(item)
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
return out;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// src/plugin/mcp-config.ts
|
|
189
|
+
function translateMcpServers(mcp) {
|
|
190
|
+
const out = {};
|
|
191
|
+
if (!mcp) return out;
|
|
192
|
+
for (const [name, entry] of Object.entries(mcp)) {
|
|
193
|
+
if (!entry || entry.enabled === false) continue;
|
|
194
|
+
if (entry.type === "local") {
|
|
195
|
+
const [command, ...args] = entry.command ?? [];
|
|
196
|
+
if (!command) continue;
|
|
197
|
+
out[name] = {
|
|
198
|
+
type: "stdio",
|
|
199
|
+
command,
|
|
200
|
+
...args.length > 0 ? { args } : {},
|
|
201
|
+
...entry.environment && Object.keys(entry.environment).length > 0 ? { env: entry.environment } : {}
|
|
202
|
+
};
|
|
203
|
+
} else if (entry.type === "remote") {
|
|
204
|
+
if (!entry.url) continue;
|
|
205
|
+
out[name] = {
|
|
206
|
+
type: "http",
|
|
207
|
+
url: entry.url,
|
|
208
|
+
...entry.headers && Object.keys(entry.headers).length > 0 ? { headers: entry.headers } : {}
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return out;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// src/plugin/cursor-tools.ts
|
|
216
|
+
import { tool } from "@opencode-ai/plugin";
|
|
217
|
+
|
|
218
|
+
// src/provider/cloud-agent.ts
|
|
219
|
+
async function runCloudAgent(params) {
|
|
220
|
+
const { Agent } = await loadCursorSdk();
|
|
221
|
+
const modelSelection = params.model ? buildModelSelection(params.model, params.thinking ? { thinking: params.thinking } : void 0) : void 0;
|
|
222
|
+
const mode = params.mode ?? "agent";
|
|
223
|
+
const createOptions = {
|
|
224
|
+
apiKey: params.apiKey,
|
|
225
|
+
...modelSelection ? { model: modelSelection } : {},
|
|
226
|
+
mode,
|
|
227
|
+
cloud: {
|
|
228
|
+
repos: [
|
|
229
|
+
{
|
|
230
|
+
url: params.repoUrl,
|
|
231
|
+
...params.startingRef ? { startingRef: params.startingRef } : {}
|
|
232
|
+
}
|
|
233
|
+
],
|
|
234
|
+
...params.autoCreatePR !== void 0 ? { autoCreatePR: params.autoCreatePR } : {},
|
|
235
|
+
...params.workOnCurrentBranch !== void 0 ? { workOnCurrentBranch: params.workOnCurrentBranch } : {}
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
const progress = [];
|
|
239
|
+
const agent = await Agent.create(createOptions);
|
|
240
|
+
const onDelta = ({ update }) => {
|
|
241
|
+
if (update.type === "summary") progress.push(`summary: ${update.summary}`);
|
|
242
|
+
};
|
|
243
|
+
const onStep = ({ step }) => {
|
|
244
|
+
progress.push(`step: ${describeStep(step)}`);
|
|
245
|
+
};
|
|
246
|
+
try {
|
|
247
|
+
const run = await agent.send(params.prompt, { mode, onDelta, onStep });
|
|
248
|
+
const off = run.onDidChangeStatus?.((status) => {
|
|
249
|
+
progress.push(`status: ${status}`);
|
|
250
|
+
});
|
|
251
|
+
const onAbort = () => {
|
|
252
|
+
run.cancel().catch(() => {
|
|
253
|
+
});
|
|
254
|
+
};
|
|
255
|
+
params.abortSignal?.addEventListener("abort", onAbort);
|
|
256
|
+
try {
|
|
257
|
+
const result = await run.wait();
|
|
258
|
+
const branches = (result.git?.branches ?? []).map((b) => ({
|
|
259
|
+
repoUrl: b.repoUrl,
|
|
260
|
+
...b.branch ? { branch: b.branch } : {},
|
|
261
|
+
...b.prUrl ? { prUrl: b.prUrl } : {}
|
|
262
|
+
}));
|
|
263
|
+
const prUrl = branches.find((b) => b.prUrl)?.prUrl;
|
|
264
|
+
return {
|
|
265
|
+
agentId: agent.agentId,
|
|
266
|
+
status: result.status,
|
|
267
|
+
...result.result !== void 0 ? { result: result.result } : {},
|
|
268
|
+
...prUrl ? { prUrl } : {},
|
|
269
|
+
branches,
|
|
270
|
+
...result.durationMs !== void 0 ? { durationMs: result.durationMs } : {},
|
|
271
|
+
progress
|
|
272
|
+
};
|
|
273
|
+
} finally {
|
|
274
|
+
off?.();
|
|
275
|
+
params.abortSignal?.removeEventListener("abort", onAbort);
|
|
276
|
+
}
|
|
277
|
+
} finally {
|
|
278
|
+
try {
|
|
279
|
+
agent.close();
|
|
280
|
+
} catch {
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function describeStep(step) {
|
|
285
|
+
if (step.type === "toolCall") return `toolCall:${step.message.type}`;
|
|
286
|
+
return step.type;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// src/provider/delegate.ts
|
|
290
|
+
async function runDelegate(params) {
|
|
291
|
+
const { mode, modelSelection } = resolveControls(
|
|
292
|
+
params.model,
|
|
293
|
+
{
|
|
294
|
+
mode: params.mode ?? "agent",
|
|
295
|
+
...params.thinking ? { params: { thinking: params.thinking } } : {}
|
|
296
|
+
},
|
|
297
|
+
void 0
|
|
298
|
+
);
|
|
299
|
+
const acquired = await acquireAgent({
|
|
300
|
+
apiKey: params.apiKey,
|
|
301
|
+
modelSelection,
|
|
302
|
+
mode,
|
|
303
|
+
cwd: params.cwd,
|
|
304
|
+
...params.sandbox !== void 0 ? { sandbox: params.sandbox } : {},
|
|
305
|
+
...params.agentId ? { agentId: params.agentId } : {},
|
|
306
|
+
session: false
|
|
307
|
+
});
|
|
308
|
+
const text = [];
|
|
309
|
+
const reasoning = [];
|
|
310
|
+
const toolActivity = [];
|
|
311
|
+
let usage;
|
|
312
|
+
try {
|
|
313
|
+
for await (const event of streamAgentTurn(
|
|
314
|
+
acquired.agent,
|
|
315
|
+
{ text: params.prompt },
|
|
316
|
+
{ mode, ...params.abortSignal ? { abortSignal: params.abortSignal } : {} }
|
|
317
|
+
)) {
|
|
318
|
+
switch (event.type) {
|
|
319
|
+
case "text-delta":
|
|
320
|
+
text.push(event.text);
|
|
321
|
+
break;
|
|
322
|
+
case "reasoning-delta":
|
|
323
|
+
reasoning.push(event.text);
|
|
324
|
+
break;
|
|
325
|
+
case "tool-call":
|
|
326
|
+
toolActivity.push({ name: event.name, isError: false });
|
|
327
|
+
break;
|
|
328
|
+
case "tool-result":
|
|
329
|
+
if (event.isError) toolActivity.push({ name: event.name, isError: true });
|
|
330
|
+
break;
|
|
331
|
+
case "usage":
|
|
332
|
+
usage = event.usage;
|
|
333
|
+
break;
|
|
334
|
+
case "finish":
|
|
335
|
+
if (event.text && text.length === 0) text.push(event.text);
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
} finally {
|
|
340
|
+
acquired.release();
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
agentId: acquired.agent.agentId,
|
|
344
|
+
text: text.join(""),
|
|
345
|
+
reasoning: reasoning.join(""),
|
|
346
|
+
toolActivity,
|
|
347
|
+
...usage ? { usage } : {}
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// src/plugin/cursor-tools.ts
|
|
352
|
+
var s = tool.schema;
|
|
353
|
+
var NEEDS_AUTH = "No Cursor API key available. Run `opencode auth login` and choose Cursor, or set CURSOR_API_KEY.";
|
|
354
|
+
async function requestApproval(context, permission, patterns, metadata) {
|
|
355
|
+
try {
|
|
356
|
+
await context.ask({ permission, patterns, always: patterns, metadata });
|
|
357
|
+
return { ok: true };
|
|
358
|
+
} catch (err) {
|
|
359
|
+
return { ok: false, reason: err instanceof Error ? err.message : String(err) };
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
function errorMessage(err) {
|
|
363
|
+
return err instanceof Error ? err.message : String(err);
|
|
364
|
+
}
|
|
365
|
+
function buildCursorTools(deps) {
|
|
366
|
+
return {
|
|
367
|
+
cursor_cloud_agent: tool({
|
|
368
|
+
description: "Launch a Cursor background ('cloud') agent on a remote repository. Runs autonomously (may take minutes) and can open a pull request. Returns the cloud agent id, final status, result, and PR url when available.",
|
|
369
|
+
args: {
|
|
370
|
+
prompt: s.string().describe("The task/instruction for the background agent."),
|
|
371
|
+
repoUrl: s.string().describe("Target repository URL, e.g. https://github.com/owner/repo."),
|
|
372
|
+
startingRef: s.string().optional().describe("Branch or ref to start from (defaults to the repo default branch)."),
|
|
373
|
+
model: s.string().optional().describe("Cursor model id (optional for cloud)."),
|
|
374
|
+
mode: s.enum(["agent", "plan"]).optional().describe("Conversation mode."),
|
|
375
|
+
thinking: s.string().optional().describe("Thinking level, e.g. 'high'."),
|
|
376
|
+
autoCreatePR: s.boolean().optional().describe("Open a pull request automatically when finished."),
|
|
377
|
+
workOnCurrentBranch: s.boolean().optional().describe("Operate on the current branch instead of creating a new one.")
|
|
378
|
+
},
|
|
379
|
+
execute: async (args, context) => {
|
|
380
|
+
const apiKey = deps.resolveApiKey();
|
|
381
|
+
if (!apiKey) return NEEDS_AUTH;
|
|
382
|
+
const approval = await requestApproval(
|
|
383
|
+
context,
|
|
384
|
+
"cursor_cloud_agent",
|
|
385
|
+
[args.repoUrl],
|
|
386
|
+
{ repoUrl: args.repoUrl, autoCreatePR: args.autoCreatePR ?? false }
|
|
387
|
+
);
|
|
388
|
+
if (!approval.ok) {
|
|
389
|
+
return `Cloud agent not approved for ${args.repoUrl}${approval.reason ? `: ${approval.reason}` : "."}`;
|
|
390
|
+
}
|
|
391
|
+
let result;
|
|
392
|
+
try {
|
|
393
|
+
result = await runCloudAgent({
|
|
394
|
+
apiKey,
|
|
395
|
+
prompt: args.prompt,
|
|
396
|
+
repoUrl: args.repoUrl,
|
|
397
|
+
...args.startingRef ? { startingRef: args.startingRef } : {},
|
|
398
|
+
...args.model ? { model: args.model } : {},
|
|
399
|
+
...args.mode ? { mode: args.mode } : {},
|
|
400
|
+
...args.thinking ? { thinking: args.thinking } : {},
|
|
401
|
+
...args.autoCreatePR !== void 0 ? { autoCreatePR: args.autoCreatePR } : {},
|
|
402
|
+
...args.workOnCurrentBranch !== void 0 ? { workOnCurrentBranch: args.workOnCurrentBranch } : {},
|
|
403
|
+
abortSignal: context.abort
|
|
404
|
+
});
|
|
405
|
+
} catch (err) {
|
|
406
|
+
return `Cloud agent failed: ${errorMessage(err)}`;
|
|
407
|
+
}
|
|
408
|
+
const lines = [
|
|
409
|
+
`Cloud agent ${result.agentId} \u2014 ${result.status}`,
|
|
410
|
+
...result.prUrl ? [`PR: ${result.prUrl}`] : [],
|
|
411
|
+
...result.branches.length > 0 ? [`Branches: ${result.branches.map((b) => b.branch ?? b.repoUrl).join(", ")}`] : [],
|
|
412
|
+
...result.result ? ["", result.result] : [],
|
|
413
|
+
...result.progress.length > 0 ? ["", "Progress:", ...result.progress] : []
|
|
414
|
+
];
|
|
415
|
+
return {
|
|
416
|
+
title: `Cursor cloud agent (${result.status})`,
|
|
417
|
+
output: lines.join("\n"),
|
|
418
|
+
metadata: {
|
|
419
|
+
agentId: result.agentId,
|
|
420
|
+
status: result.status,
|
|
421
|
+
prUrl: result.prUrl ?? null,
|
|
422
|
+
durationMs: result.durationMs ?? null
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
}),
|
|
427
|
+
cursor_delegate: tool({
|
|
428
|
+
description: "Delegate a single subtask to a local Cursor agent and return its result. Use to hand off discrete work to Cursor while keeping your primary model in control. Permission-gated.",
|
|
429
|
+
args: {
|
|
430
|
+
prompt: s.string().describe("The subtask to delegate to Cursor."),
|
|
431
|
+
model: s.string().describe("Cursor model id to run the delegation on."),
|
|
432
|
+
mode: s.enum(["agent", "plan"]).optional().describe("Conversation mode."),
|
|
433
|
+
thinking: s.string().optional().describe("Thinking level, e.g. 'high'."),
|
|
434
|
+
cwd: s.string().optional().describe("Working directory (defaults to the session directory)."),
|
|
435
|
+
sandbox: s.boolean().optional().describe("Run the agent's tools in Cursor's sandbox."),
|
|
436
|
+
agentId: s.string().optional().describe("Resume a specific Cursor agent id instead of starting fresh.")
|
|
437
|
+
},
|
|
438
|
+
execute: async (args, context) => {
|
|
439
|
+
const apiKey = deps.resolveApiKey();
|
|
440
|
+
if (!apiKey) return NEEDS_AUTH;
|
|
441
|
+
const approval = await requestApproval(context, "cursor_delegate", [args.model], {
|
|
442
|
+
model: args.model,
|
|
443
|
+
prompt: args.prompt
|
|
444
|
+
});
|
|
445
|
+
if (!approval.ok) {
|
|
446
|
+
return `Delegation to ${args.model} not approved${approval.reason ? `: ${approval.reason}` : "."}`;
|
|
447
|
+
}
|
|
448
|
+
let result;
|
|
449
|
+
try {
|
|
450
|
+
result = await runDelegate({
|
|
451
|
+
apiKey,
|
|
452
|
+
prompt: args.prompt,
|
|
453
|
+
model: args.model,
|
|
454
|
+
cwd: args.cwd ?? context.directory ?? deps.defaultCwd(),
|
|
455
|
+
...args.mode ? { mode: args.mode } : {},
|
|
456
|
+
...args.thinking ? { thinking: args.thinking } : {},
|
|
457
|
+
...args.sandbox !== void 0 ? { sandbox: args.sandbox } : {},
|
|
458
|
+
...args.agentId ? { agentId: args.agentId } : {},
|
|
459
|
+
abortSignal: context.abort
|
|
460
|
+
});
|
|
461
|
+
} catch (err) {
|
|
462
|
+
return `Delegation failed: ${errorMessage(err)}`;
|
|
463
|
+
}
|
|
464
|
+
const toolNote = result.toolActivity.length > 0 ? `
|
|
465
|
+
|
|
466
|
+
(${result.toolActivity.length} tool call(s)${result.toolActivity.some((t) => t.isError) ? ", some failed" : ""})` : "";
|
|
467
|
+
return {
|
|
468
|
+
title: `Cursor delegate (${args.model})`,
|
|
469
|
+
output: (result.text || "(no text output)") + toolNote,
|
|
470
|
+
metadata: {
|
|
471
|
+
agentId: result.agentId,
|
|
472
|
+
model: args.model,
|
|
473
|
+
toolCalls: result.toolActivity.length,
|
|
474
|
+
usage: result.usage ?? null
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
})
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// src/plugin/index.ts
|
|
483
|
+
function apiKeyFromAuth(auth) {
|
|
484
|
+
return auth?.type === "api" ? auth.key : void 0;
|
|
485
|
+
}
|
|
486
|
+
var CursorPlugin = async (input) => {
|
|
487
|
+
let capturedApiKey;
|
|
488
|
+
return {
|
|
489
|
+
auth: {
|
|
490
|
+
provider: PROVIDER_ID,
|
|
491
|
+
loader: async (getAuth) => {
|
|
492
|
+
const apiKey = resolveCursorApiKey(apiKeyFromAuth(await getAuth().catch(() => void 0)));
|
|
493
|
+
if (apiKey) {
|
|
494
|
+
capturedApiKey = apiKey;
|
|
495
|
+
void discoverModels({ apiKey });
|
|
496
|
+
}
|
|
497
|
+
return apiKey ? { apiKey } : {};
|
|
498
|
+
},
|
|
499
|
+
// A single API-key method. opencode always shows its built-in "Enter your
|
|
500
|
+
// API key" prompt for `type: "api"`, so we intentionally do NOT declare
|
|
501
|
+
// custom `prompts` (that asks for the key a second time) or an `authorize`
|
|
502
|
+
// callback. opencode only passes `authorize` the *custom-prompt* inputs —
|
|
503
|
+
// never the built-in key — so validating the key in `authorize` would
|
|
504
|
+
// force that redundant extra prompt. Instead the key is validated on first
|
|
505
|
+
// use (model discovery / the first call both surface a bad key clearly).
|
|
506
|
+
methods: [{ type: "api", label: "Cursor API Key" }]
|
|
507
|
+
},
|
|
508
|
+
config: async (config) => {
|
|
509
|
+
const { models } = await discoverModels({});
|
|
510
|
+
config.provider ??= {};
|
|
511
|
+
const existing = config.provider[PROVIDER_ID] ?? {};
|
|
512
|
+
const existingOptions = existing.options ?? {};
|
|
513
|
+
const forwardMcp = existingOptions["forwardMcp"] !== false;
|
|
514
|
+
const userMcp = existingOptions["mcpServers"] ?? {};
|
|
515
|
+
const mcpServers = forwardMcp ? { ...userMcp, ...translateMcpServers(config.mcp) } : userMcp;
|
|
516
|
+
config.provider[PROVIDER_ID] = {
|
|
517
|
+
name: "Cursor",
|
|
518
|
+
npm: providerNpm(),
|
|
519
|
+
...existing,
|
|
520
|
+
options: {
|
|
521
|
+
...existingOptions,
|
|
522
|
+
...Object.keys(mcpServers).length > 0 ? { mcpServers } : {}
|
|
523
|
+
},
|
|
524
|
+
models: { ...toOpencodeModels(models), ...existing.models ?? {} }
|
|
525
|
+
};
|
|
526
|
+
},
|
|
527
|
+
provider: {
|
|
528
|
+
id: PROVIDER_ID,
|
|
529
|
+
models: async (_provider, ctx) => {
|
|
530
|
+
const apiKey = apiKeyFromAuth(ctx.auth);
|
|
531
|
+
const { models } = await discoverModels({ apiKey });
|
|
532
|
+
return buildModelV2Map(models);
|
|
533
|
+
}
|
|
534
|
+
},
|
|
535
|
+
// Bridge opencode's session id to the provider: it lands in
|
|
536
|
+
// providerOptions.cursor.sessionID, which the provider reads to pool/resume a
|
|
537
|
+
// Cursor agent per session (when the `session` option is enabled).
|
|
538
|
+
"chat.params": async (input2, output) => {
|
|
539
|
+
if (input2.model?.providerID !== PROVIDER_ID) return;
|
|
540
|
+
output.options = { ...output.options ?? {}, sessionID: input2.sessionID };
|
|
541
|
+
},
|
|
542
|
+
tool: {
|
|
543
|
+
cursor_refresh_models: {
|
|
544
|
+
description: "Refresh the live Cursor model catalog (bypasses the 24h cache) and report the available models.",
|
|
545
|
+
args: {},
|
|
546
|
+
execute: async () => {
|
|
547
|
+
const result = await discoverModels({ forceRefresh: true });
|
|
548
|
+
const lines = result.models.map((m) => `- ${m.id} \u2014 ${m.displayName}`);
|
|
549
|
+
const header = result.source === "live" ? `Refreshed ${result.models.length} Cursor models (live):` : `Could not fetch live models (${result.source}). ${result.warning ?? ""}`.trim();
|
|
550
|
+
return {
|
|
551
|
+
title: `Cursor models (${result.source})`,
|
|
552
|
+
output: [header, ...lines].join("\n"),
|
|
553
|
+
metadata: { source: result.source, count: result.models.length }
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
},
|
|
557
|
+
// Delegation tools that complement the provider: a cloud/background agent
|
|
558
|
+
// and a permission-gated local delegate. They resolve the Cursor key from
|
|
559
|
+
// the auth loader (captured above) or CURSOR_API_KEY.
|
|
560
|
+
...buildCursorTools({
|
|
561
|
+
resolveApiKey: () => resolveCursorApiKey(capturedApiKey),
|
|
562
|
+
defaultCwd: () => input?.directory ?? process.cwd()
|
|
563
|
+
})
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
};
|
|
567
|
+
var plugin_default = CursorPlugin;
|
|
568
|
+
export {
|
|
569
|
+
CursorPlugin,
|
|
570
|
+
plugin_default as default
|
|
571
|
+
};
|
|
572
|
+
//# sourceMappingURL=index.js.map
|