@stablekernel/opencode-cursor 0.1.0 → 0.3.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/CHANGELOG.md +70 -3
- package/README.md +81 -16
- package/dist/{chunk-D4YQ7ZEM.js → chunk-BTI2NHEE.js} +73 -8
- package/dist/chunk-BTI2NHEE.js.map +1 -0
- package/dist/plugin/index.js +100 -13
- package/dist/plugin/index.js.map +1 -1
- package/dist/provider/index.d.ts +7 -5
- package/dist/provider/index.js +504 -29
- package/dist/provider/index.js.map +1 -1
- package/package.json +6 -1
- package/dist/chunk-D4YQ7ZEM.js.map +0 -1
package/dist/provider/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
acquireAgent,
|
|
3
|
+
getSessionRecord,
|
|
3
4
|
resolveControls,
|
|
4
5
|
resolveCursorApiKey,
|
|
5
6
|
streamAgentTurn
|
|
6
|
-
} from "../chunk-
|
|
7
|
+
} from "../chunk-BTI2NHEE.js";
|
|
7
8
|
|
|
8
9
|
// src/provider/index.ts
|
|
9
10
|
import { NoSuchModelError } from "@ai-sdk/provider";
|
|
@@ -12,6 +13,19 @@ import { NoSuchModelError } from "@ai-sdk/provider";
|
|
|
12
13
|
import { LoadAPIKeyError } from "@ai-sdk/provider";
|
|
13
14
|
|
|
14
15
|
// src/provider/message-map.ts
|
|
16
|
+
var TOOL_RESULT_CAP = 2e3;
|
|
17
|
+
var TOOL_ARGS_CAP = 500;
|
|
18
|
+
function stringify(value) {
|
|
19
|
+
if (typeof value === "string") return value;
|
|
20
|
+
try {
|
|
21
|
+
return JSON.stringify(value ?? null);
|
|
22
|
+
} catch {
|
|
23
|
+
return String(value);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function truncate(text, cap) {
|
|
27
|
+
return text.length > cap ? `${text.slice(0, cap)}\u2026[+${text.length - cap} chars]` : text;
|
|
28
|
+
}
|
|
15
29
|
function promptToCursorMessage(prompt) {
|
|
16
30
|
const lines = [];
|
|
17
31
|
const images = [];
|
|
@@ -40,9 +54,16 @@ ${text.join("\n")}`);
|
|
|
40
54
|
const text = [];
|
|
41
55
|
for (const part of message.content) {
|
|
42
56
|
if (part.type === "text") text.push(part.text);
|
|
43
|
-
else if (part.type === "reasoning")
|
|
44
|
-
|
|
45
|
-
else if (part.type === "tool-
|
|
57
|
+
else if (part.type === "reasoning")
|
|
58
|
+
text.push(`(thinking) ${part.text}`);
|
|
59
|
+
else if (part.type === "tool-call")
|
|
60
|
+
text.push(
|
|
61
|
+
`[called ${part.toolName}(${truncate(stringify(part.input), TOOL_ARGS_CAP)})]`
|
|
62
|
+
);
|
|
63
|
+
else if (part.type === "tool-result")
|
|
64
|
+
text.push(
|
|
65
|
+
`[result of ${part.toolName}: ${truncate(stringify(part.output), TOOL_RESULT_CAP)}]`
|
|
66
|
+
);
|
|
46
67
|
}
|
|
47
68
|
lines.push(`# Assistant
|
|
48
69
|
${text.join("\n")}`);
|
|
@@ -51,8 +72,10 @@ ${text.join("\n")}`);
|
|
|
51
72
|
case "tool": {
|
|
52
73
|
for (const part of message.content) {
|
|
53
74
|
if (part.type === "tool-result") {
|
|
54
|
-
lines.push(
|
|
55
|
-
${
|
|
75
|
+
lines.push(
|
|
76
|
+
`# Tool result (${part.toolName})
|
|
77
|
+
${truncate(stringify(part.output), TOOL_RESULT_CAP)}`
|
|
78
|
+
);
|
|
56
79
|
}
|
|
57
80
|
}
|
|
58
81
|
break;
|
|
@@ -111,31 +134,366 @@ function safeJsonString(input) {
|
|
|
111
134
|
function blockToolName(name) {
|
|
112
135
|
return `cursor_${name.replace(/[^A-Za-z0-9_-]/g, "_")}`;
|
|
113
136
|
}
|
|
114
|
-
function
|
|
137
|
+
function nativeToolCall(id, toolName, input) {
|
|
115
138
|
return {
|
|
116
139
|
type: "tool-call",
|
|
117
140
|
toolCallId: id,
|
|
118
|
-
toolName
|
|
141
|
+
toolName,
|
|
119
142
|
input: safeJsonString(input),
|
|
120
143
|
providerExecuted: true,
|
|
121
144
|
dynamic: true
|
|
122
145
|
};
|
|
123
146
|
}
|
|
124
|
-
function
|
|
147
|
+
function nativeToolResult(id, toolName, result, isError) {
|
|
125
148
|
return {
|
|
126
149
|
type: "tool-result",
|
|
127
150
|
toolCallId: id,
|
|
128
|
-
toolName
|
|
151
|
+
toolName,
|
|
129
152
|
result: result ?? null,
|
|
130
153
|
isError,
|
|
131
154
|
providerExecuted: true,
|
|
132
155
|
dynamic: true
|
|
133
156
|
};
|
|
134
157
|
}
|
|
158
|
+
function toolCallObj(id, name, input) {
|
|
159
|
+
return nativeToolCall(id, blockToolName(name), input);
|
|
160
|
+
}
|
|
161
|
+
function toolResultObj(id, name, result, isError) {
|
|
162
|
+
return nativeToolResult(id, blockToolName(name), result, isError);
|
|
163
|
+
}
|
|
135
164
|
var EDIT_TOOL_NAME = "edit";
|
|
136
165
|
function isRecord(v) {
|
|
137
166
|
return typeof v === "object" && v !== null;
|
|
138
167
|
}
|
|
168
|
+
function strField(v, key) {
|
|
169
|
+
return isRecord(v) && typeof v[key] === "string" ? v[key] : void 0;
|
|
170
|
+
}
|
|
171
|
+
function numField(v, key) {
|
|
172
|
+
return isRecord(v) && typeof v[key] === "number" ? v[key] : void 0;
|
|
173
|
+
}
|
|
174
|
+
function successValue(result) {
|
|
175
|
+
return isRecord(result) && result["status"] === "success" ? result["value"] : void 0;
|
|
176
|
+
}
|
|
177
|
+
function flattenMcpContent(value) {
|
|
178
|
+
if (!isRecord(value) || !Array.isArray(value["content"])) return null;
|
|
179
|
+
const parts = value["content"].flatMap((item) => {
|
|
180
|
+
const text = isRecord(item) ? strField(item["text"], "text") : void 0;
|
|
181
|
+
if (text !== void 0) return [text];
|
|
182
|
+
if (isRecord(item) && isRecord(item["image"])) return ["[image]"];
|
|
183
|
+
return [];
|
|
184
|
+
});
|
|
185
|
+
return parts.join("\n");
|
|
186
|
+
}
|
|
187
|
+
function mcpFold(result) {
|
|
188
|
+
const text = flattenMcpContent(successValue(result));
|
|
189
|
+
if (text === null) return null;
|
|
190
|
+
return { title: "", metadata: {}, output: text };
|
|
191
|
+
}
|
|
192
|
+
function mcpInputArgs(args) {
|
|
193
|
+
return isRecord(args) ? args["args"] : void 0;
|
|
194
|
+
}
|
|
195
|
+
function webSearchProvider(args) {
|
|
196
|
+
const id = (strField(args, "providerIdentifier") ?? "").toLowerCase();
|
|
197
|
+
if (id.includes("exa")) return "exa";
|
|
198
|
+
if (id.includes("parallel")) return "parallel";
|
|
199
|
+
return void 0;
|
|
200
|
+
}
|
|
201
|
+
function isWebSearchName(name) {
|
|
202
|
+
return /web[_-]?search/i.test(name);
|
|
203
|
+
}
|
|
204
|
+
var WEBSEARCH_ADAPTER = {
|
|
205
|
+
tool: "websearch",
|
|
206
|
+
input: (args) => {
|
|
207
|
+
const query = strField(mcpInputArgs(args), "query");
|
|
208
|
+
return query !== void 0 ? { query } : {};
|
|
209
|
+
},
|
|
210
|
+
result: (value, args) => {
|
|
211
|
+
const provider = webSearchProvider(args);
|
|
212
|
+
return {
|
|
213
|
+
title: "",
|
|
214
|
+
metadata: provider ? { provider } : {},
|
|
215
|
+
output: flattenMcpContent(value) ?? ""
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
function resolveAdapter(name, _input) {
|
|
220
|
+
const exact = NATIVE_ADAPTERS[name];
|
|
221
|
+
if (exact) return exact;
|
|
222
|
+
if (isWebSearchName(name)) return WEBSEARCH_ADAPTER;
|
|
223
|
+
return void 0;
|
|
224
|
+
}
|
|
225
|
+
function mapTodoStatus(status) {
|
|
226
|
+
if (status === "inProgress") return "in_progress";
|
|
227
|
+
return typeof status === "string" ? status : "pending";
|
|
228
|
+
}
|
|
229
|
+
function mapTodos(args) {
|
|
230
|
+
const todos = isRecord(args) && Array.isArray(args["todos"]) ? args["todos"] : [];
|
|
231
|
+
return todos.flatMap(
|
|
232
|
+
(t) => isRecord(t) && typeof t["content"] === "string" ? [
|
|
233
|
+
{
|
|
234
|
+
content: t["content"],
|
|
235
|
+
status: mapTodoStatus(t["status"])
|
|
236
|
+
}
|
|
237
|
+
] : []
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
var NATIVE_ADAPTERS = {
|
|
241
|
+
// Cursor `shell` → opencode `bash` (console renderer).
|
|
242
|
+
shell: {
|
|
243
|
+
tool: "bash",
|
|
244
|
+
input: (args) => ({ command: strField(args, "command") ?? "" }),
|
|
245
|
+
result: (value, args) => {
|
|
246
|
+
if (!isRecord(value)) return null;
|
|
247
|
+
const command = strField(args, "command") ?? "";
|
|
248
|
+
const stdout = strField(value, "stdout") ?? "";
|
|
249
|
+
const stderr = strField(value, "stderr") ?? "";
|
|
250
|
+
const exit = numField(value, "exitCode");
|
|
251
|
+
const body = [stdout, stderr].filter((s) => s.length > 0).join("\n");
|
|
252
|
+
const output = exit !== void 0 && exit !== 0 ? `${body}${body ? "\n" : ""}(exit ${exit})` : body;
|
|
253
|
+
return {
|
|
254
|
+
title: command,
|
|
255
|
+
metadata: { command, output, exit: exit ?? 0 },
|
|
256
|
+
output
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
// Cursor `read` → opencode `read`.
|
|
261
|
+
read: {
|
|
262
|
+
tool: "read",
|
|
263
|
+
input: (args) => ({ filePath: strField(args, "path") ?? "" }),
|
|
264
|
+
result: (value, args) => {
|
|
265
|
+
const content = strField(value, "content");
|
|
266
|
+
if (content === void 0) return null;
|
|
267
|
+
const filePath = strField(args, "path") ?? "";
|
|
268
|
+
const totalLines = numField(value, "totalLines");
|
|
269
|
+
return {
|
|
270
|
+
title: filePath,
|
|
271
|
+
metadata: {
|
|
272
|
+
preview: content.split("\n").slice(0, 20).join("\n"),
|
|
273
|
+
loaded: [],
|
|
274
|
+
...totalLines !== void 0 ? { totalLines } : {}
|
|
275
|
+
},
|
|
276
|
+
output: content
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
// Cursor `write` → opencode `write` (renders input.content as the new file).
|
|
281
|
+
write: {
|
|
282
|
+
tool: "write",
|
|
283
|
+
input: (args) => ({
|
|
284
|
+
filePath: strField(args, "path") ?? "",
|
|
285
|
+
content: strField(args, "fileText") ?? ""
|
|
286
|
+
}),
|
|
287
|
+
result: (value, args) => {
|
|
288
|
+
const filePath = strField(args, "path") ?? "";
|
|
289
|
+
const lines = numField(value, "linesCreated");
|
|
290
|
+
const output = lines !== void 0 ? `Wrote ${lines} line${lines === 1 ? "" : "s"}.` : "Wrote file successfully.";
|
|
291
|
+
return {
|
|
292
|
+
title: filePath,
|
|
293
|
+
metadata: { diagnostics: {}, filepath: filePath, exists: false },
|
|
294
|
+
output
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
// Cursor `glob` → opencode `glob`.
|
|
299
|
+
glob: {
|
|
300
|
+
tool: "glob",
|
|
301
|
+
input: (args) => {
|
|
302
|
+
const pattern = strField(args, "globPattern") ?? "";
|
|
303
|
+
const dir = strField(args, "targetDirectory");
|
|
304
|
+
return dir ? { pattern, path: dir } : { pattern };
|
|
305
|
+
},
|
|
306
|
+
result: (value) => {
|
|
307
|
+
if (!isRecord(value) || !Array.isArray(value["files"])) return null;
|
|
308
|
+
const files = value["files"].filter(
|
|
309
|
+
(f) => typeof f === "string"
|
|
310
|
+
);
|
|
311
|
+
const truncated = value["clientTruncated"] === true || value["ripgrepTruncated"] === true;
|
|
312
|
+
return {
|
|
313
|
+
title: "",
|
|
314
|
+
metadata: { count: files.length, truncated },
|
|
315
|
+
output: files.length > 0 ? files.join("\n") : "No files found"
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
// Cursor `grep` → opencode `grep` (flatten matches into ripgrep-style text).
|
|
320
|
+
grep: {
|
|
321
|
+
tool: "grep",
|
|
322
|
+
input: (args) => {
|
|
323
|
+
const out = {
|
|
324
|
+
pattern: strField(args, "pattern") ?? ""
|
|
325
|
+
};
|
|
326
|
+
const p = strField(args, "path");
|
|
327
|
+
if (p) out["path"] = p;
|
|
328
|
+
const g = strField(args, "glob");
|
|
329
|
+
if (g) out["include"] = g;
|
|
330
|
+
return out;
|
|
331
|
+
},
|
|
332
|
+
result: (value) => {
|
|
333
|
+
if (!isRecord(value)) return null;
|
|
334
|
+
const unions = [];
|
|
335
|
+
const ws = value["workspaceResults"];
|
|
336
|
+
if (isRecord(ws)) unions.push(...Object.values(ws));
|
|
337
|
+
if (value["activeEditorResult"] !== void 0)
|
|
338
|
+
unions.push(value["activeEditorResult"]);
|
|
339
|
+
const lines = [];
|
|
340
|
+
let total = 0;
|
|
341
|
+
let current = "";
|
|
342
|
+
for (const u of unions) {
|
|
343
|
+
if (!isRecord(u)) continue;
|
|
344
|
+
const output = u["output"];
|
|
345
|
+
if (u["type"] === "content" && isRecord(output) && Array.isArray(output["matches"])) {
|
|
346
|
+
for (const m of output["matches"]) {
|
|
347
|
+
if (!isRecord(m)) continue;
|
|
348
|
+
const file = strField(m, "file") ?? "";
|
|
349
|
+
const line = numField(m, "lineNumber");
|
|
350
|
+
const text = strField(m, "line") ?? "";
|
|
351
|
+
if (current !== file) {
|
|
352
|
+
if (current) lines.push("");
|
|
353
|
+
current = file;
|
|
354
|
+
lines.push(`${file}:`);
|
|
355
|
+
}
|
|
356
|
+
lines.push(
|
|
357
|
+
line !== void 0 ? ` Line ${line}: ${text}` : ` ${text}`
|
|
358
|
+
);
|
|
359
|
+
total++;
|
|
360
|
+
}
|
|
361
|
+
} else if (u["type"] === "files" && isRecord(output) && Array.isArray(output["files"])) {
|
|
362
|
+
for (const f of output["files"]) {
|
|
363
|
+
if (typeof f === "string") {
|
|
364
|
+
lines.push(f);
|
|
365
|
+
total++;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return {
|
|
371
|
+
title: "",
|
|
372
|
+
metadata: { matches: total, truncated: false },
|
|
373
|
+
output: total > 0 ? [
|
|
374
|
+
`Found ${total} match${total === 1 ? "" : "es"}`,
|
|
375
|
+
"",
|
|
376
|
+
...lines
|
|
377
|
+
].join("\n") : "No matches found"
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
},
|
|
381
|
+
// Cursor `ls` → opencode `list` (flatten the directory tree into paths).
|
|
382
|
+
ls: {
|
|
383
|
+
tool: "list",
|
|
384
|
+
input: (args) => ({ path: strField(args, "path") ?? "" }),
|
|
385
|
+
result: (value) => {
|
|
386
|
+
if (!isRecord(value)) return null;
|
|
387
|
+
const root = value["directoryTreeRoot"];
|
|
388
|
+
if (!isRecord(root)) return null;
|
|
389
|
+
const out = [];
|
|
390
|
+
const walk = (node) => {
|
|
391
|
+
const base = strField(node, "absPath") ?? "";
|
|
392
|
+
const files = Array.isArray(node["childrenFiles"]) ? node["childrenFiles"] : [];
|
|
393
|
+
for (const f of files) {
|
|
394
|
+
const name = strField(f, "name");
|
|
395
|
+
if (name) out.push(`${base}/${name}`);
|
|
396
|
+
}
|
|
397
|
+
const dirs = Array.isArray(node["childrenDirs"]) ? node["childrenDirs"] : [];
|
|
398
|
+
for (const d of dirs) {
|
|
399
|
+
if (!isRecord(d)) continue;
|
|
400
|
+
out.push(`${strField(d, "absPath") ?? ""}/`);
|
|
401
|
+
walk(d);
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
walk(root);
|
|
405
|
+
return {
|
|
406
|
+
title: strField(root, "absPath") ?? "",
|
|
407
|
+
metadata: {},
|
|
408
|
+
output: out.length > 0 ? out.join("\n") : "(empty)"
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
// Cursor `updateTodos` → opencode `todowrite` (todo checklist renderer).
|
|
413
|
+
updateTodos: {
|
|
414
|
+
tool: "todowrite",
|
|
415
|
+
input: (args) => ({ todos: mapTodos(args) }),
|
|
416
|
+
result: (_value, args) => {
|
|
417
|
+
const todos = mapTodos(args);
|
|
418
|
+
const done = todos.filter((t) => t.status === "completed").length;
|
|
419
|
+
return {
|
|
420
|
+
title: `${done}/${todos.length}`,
|
|
421
|
+
metadata: { todos },
|
|
422
|
+
output: `Updated ${todos.length} todo${todos.length === 1 ? "" : "s"}.`
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
},
|
|
426
|
+
// Cursor `task` (subagent) → opencode `task` (agent card: name + description).
|
|
427
|
+
// Non-clickable here — the subagent ran inside Cursor, not as an opencode
|
|
428
|
+
// child session — but the native card reads far better than raw JSON.
|
|
429
|
+
task: {
|
|
430
|
+
tool: "task",
|
|
431
|
+
input: (args) => {
|
|
432
|
+
const description = strField(args, "description") ?? "";
|
|
433
|
+
const sub = isRecord(args) ? args["subagentType"] : void 0;
|
|
434
|
+
const subagent = strField(sub, "name") ?? strField(sub, "kind") ?? void 0;
|
|
435
|
+
return subagent ? { description, subagent_type: subagent } : { description };
|
|
436
|
+
},
|
|
437
|
+
result: (value, args) => {
|
|
438
|
+
const description = strField(args, "description") ?? "";
|
|
439
|
+
const suffix = strField(value, "resultSuffix");
|
|
440
|
+
const background = isRecord(value) && value["isBackground"] === true;
|
|
441
|
+
return {
|
|
442
|
+
title: description,
|
|
443
|
+
metadata: background ? { background: true } : {},
|
|
444
|
+
output: suffix ?? "Subagent task completed."
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
},
|
|
448
|
+
// Cursor `readLints` has no opencode counterpart — format-only: render the
|
|
449
|
+
// diagnostics as a readable list instead of dumping the nested JSON.
|
|
450
|
+
readLints: {
|
|
451
|
+
input: (args) => {
|
|
452
|
+
const paths = isRecord(args) && Array.isArray(args["paths"]) ? args["paths"] : [];
|
|
453
|
+
return { paths };
|
|
454
|
+
},
|
|
455
|
+
result: (value) => {
|
|
456
|
+
const files = isRecord(value) && Array.isArray(value["fileDiagnostics"]) ? value["fileDiagnostics"] : [];
|
|
457
|
+
const lines = [];
|
|
458
|
+
let total = 0;
|
|
459
|
+
for (const file of files) {
|
|
460
|
+
if (!isRecord(file)) continue;
|
|
461
|
+
const diags = Array.isArray(file["diagnostics"]) ? file["diagnostics"] : [];
|
|
462
|
+
if (diags.length === 0) continue;
|
|
463
|
+
lines.push(`${strField(file, "path") ?? ""}`);
|
|
464
|
+
for (const d of diags) {
|
|
465
|
+
if (!isRecord(d)) continue;
|
|
466
|
+
const severity = strField(d, "severity") ?? "info";
|
|
467
|
+
const start = isRecord(d["range"]) ? d["range"]["start"] : void 0;
|
|
468
|
+
const line = numField(start, "line");
|
|
469
|
+
const char = numField(start, "character");
|
|
470
|
+
const loc = line !== void 0 ? ` L${line + 1}${char !== void 0 ? `:${char + 1}` : ""}` : "";
|
|
471
|
+
lines.push(` ${severity}${loc}: ${strField(d, "message") ?? ""}`);
|
|
472
|
+
total++;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return {
|
|
476
|
+
title: total > 0 ? `${total} problem${total === 1 ? "" : "s"}` : "No problems",
|
|
477
|
+
metadata: { count: total },
|
|
478
|
+
output: total > 0 ? lines.join("\n") : "No problems found."
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
},
|
|
482
|
+
// Cursor `delete` has no opencode counterpart — format-only: a one-line
|
|
483
|
+
// confirmation instead of `{"fileSize":N}`.
|
|
484
|
+
delete: {
|
|
485
|
+
input: (args) => ({ path: strField(args, "path") ?? "" }),
|
|
486
|
+
result: (value, args) => {
|
|
487
|
+
const path = strField(args, "path") ?? "";
|
|
488
|
+
const size = numField(value, "fileSize");
|
|
489
|
+
return {
|
|
490
|
+
title: path,
|
|
491
|
+
metadata: {},
|
|
492
|
+
output: size !== void 0 ? `Deleted ${path} (${size} bytes).` : `Deleted ${path}.`
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
};
|
|
139
497
|
function editFilePath(input) {
|
|
140
498
|
return isRecord(input) && typeof input["path"] === "string" ? input["path"] : "";
|
|
141
499
|
}
|
|
@@ -189,15 +547,22 @@ function editResultFields(id, filePath, diff, result) {
|
|
|
189
547
|
};
|
|
190
548
|
}
|
|
191
549
|
function newBlockToolState() {
|
|
192
|
-
return {
|
|
550
|
+
return { open: /* @__PURE__ */ new Map(), pendingEdits: /* @__PURE__ */ new Map() };
|
|
193
551
|
}
|
|
194
552
|
function blockToolCallParts(id, name, input, state) {
|
|
195
553
|
if (name === EDIT_TOOL_NAME) {
|
|
196
554
|
state.pendingEdits.set(id, editFilePath(input));
|
|
197
555
|
return [];
|
|
198
556
|
}
|
|
199
|
-
|
|
200
|
-
|
|
557
|
+
const adapter = resolveAdapter(name, input);
|
|
558
|
+
if (adapter) {
|
|
559
|
+
const toolName2 = adapter.tool ?? blockToolName(name);
|
|
560
|
+
state.open.set(id, { toolName: toolName2, adapter, args: input });
|
|
561
|
+
return [nativeToolCall(id, toolName2, adapter.input(input))];
|
|
562
|
+
}
|
|
563
|
+
const toolName = blockToolName(name);
|
|
564
|
+
state.open.set(id, { toolName, args: input });
|
|
565
|
+
return [nativeToolCall(id, toolName, input)];
|
|
201
566
|
}
|
|
202
567
|
function blockToolResultParts(id, name, result, isError, state) {
|
|
203
568
|
if (state.pendingEdits.has(id)) {
|
|
@@ -215,15 +580,28 @@ function blockToolResultParts(id, name, result, isError, state) {
|
|
|
215
580
|
toolResultObj(id, EDIT_TOOL_NAME, result, isError)
|
|
216
581
|
];
|
|
217
582
|
}
|
|
218
|
-
state.
|
|
219
|
-
|
|
583
|
+
const open = state.open.get(id);
|
|
584
|
+
state.open.delete(id);
|
|
585
|
+
const toolName = open?.toolName ?? blockToolName(name);
|
|
586
|
+
if (open?.adapter && !isError) {
|
|
587
|
+
const value = successValue(result);
|
|
588
|
+
if (value !== void 0) {
|
|
589
|
+
const folded = open.adapter.result(value, open.args);
|
|
590
|
+
if (folded) return [nativeToolResult(id, toolName, folded, false)];
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
if (!isError) {
|
|
594
|
+
const folded = mcpFold(result);
|
|
595
|
+
if (folded) return [nativeToolResult(id, toolName, folded, false)];
|
|
596
|
+
}
|
|
597
|
+
return [nativeToolResult(id, toolName, result, isError)];
|
|
220
598
|
}
|
|
221
599
|
function blockDanglingParts(state) {
|
|
222
600
|
const parts = [];
|
|
223
|
-
for (const [id,
|
|
224
|
-
parts.push(
|
|
601
|
+
for (const [id, open] of state.open) {
|
|
602
|
+
parts.push(nativeToolResult(id, open.toolName, DANGLING_TOOL_RESULT, true));
|
|
225
603
|
}
|
|
226
|
-
state.
|
|
604
|
+
state.open.clear();
|
|
227
605
|
for (const [id, filePath] of state.pendingEdits) {
|
|
228
606
|
parts.push(toolCallObj(id, EDIT_TOOL_NAME, { path: filePath }));
|
|
229
607
|
parts.push(toolResultObj(id, EDIT_TOOL_NAME, DANGLING_TOOL_RESULT, true));
|
|
@@ -336,12 +714,17 @@ function cursorEventsToStream(events, toolDisplay = "blocks") {
|
|
|
336
714
|
break;
|
|
337
715
|
case "tool-call":
|
|
338
716
|
if (toolDisplay === "blocks") {
|
|
339
|
-
|
|
717
|
+
const parts = blockToolCallParts(
|
|
340
718
|
event.id,
|
|
341
719
|
event.name,
|
|
342
720
|
event.input,
|
|
343
721
|
toolState
|
|
344
|
-
)
|
|
722
|
+
);
|
|
723
|
+
if (parts.length > 0) {
|
|
724
|
+
closeText();
|
|
725
|
+
closeReasoning();
|
|
726
|
+
}
|
|
727
|
+
for (const part of parts) {
|
|
345
728
|
controller.enqueue(part);
|
|
346
729
|
}
|
|
347
730
|
} else {
|
|
@@ -352,13 +735,18 @@ ${formatToolCall(event.name, event.input)}
|
|
|
352
735
|
break;
|
|
353
736
|
case "tool-result":
|
|
354
737
|
if (toolDisplay === "blocks") {
|
|
355
|
-
|
|
738
|
+
const parts = blockToolResultParts(
|
|
356
739
|
event.id,
|
|
357
740
|
event.name,
|
|
358
741
|
event.result,
|
|
359
742
|
event.isError,
|
|
360
743
|
toolState
|
|
361
|
-
)
|
|
744
|
+
);
|
|
745
|
+
if (parts.length > 0) {
|
|
746
|
+
closeText();
|
|
747
|
+
closeReasoning();
|
|
748
|
+
}
|
|
749
|
+
for (const part of parts) {
|
|
362
750
|
controller.enqueue(part);
|
|
363
751
|
}
|
|
364
752
|
} else if (event.isError) {
|
|
@@ -473,6 +861,55 @@ ${formatToolCall(event.name, event.input)}
|
|
|
473
861
|
return { content, finishReason, usage };
|
|
474
862
|
}
|
|
475
863
|
|
|
864
|
+
// src/provider/transcript-fingerprint.ts
|
|
865
|
+
import { createHash } from "crypto";
|
|
866
|
+
function sha(input) {
|
|
867
|
+
return createHash("sha256").update(input).digest("hex");
|
|
868
|
+
}
|
|
869
|
+
function mcpServersFingerprint(servers) {
|
|
870
|
+
if (!servers) return "";
|
|
871
|
+
const keys = Object.keys(servers).sort();
|
|
872
|
+
if (keys.length === 0) return "";
|
|
873
|
+
return sha(JSON.stringify(keys.map((k) => [k, servers[k]])));
|
|
874
|
+
}
|
|
875
|
+
function userMessageKey(message) {
|
|
876
|
+
const parts = [];
|
|
877
|
+
for (const part of message.content) {
|
|
878
|
+
if (part.type === "text") parts.push(`t:${part.text}`);
|
|
879
|
+
else if (part.type === "file") parts.push(`f:${part.mediaType}`);
|
|
880
|
+
}
|
|
881
|
+
return parts.join("\n");
|
|
882
|
+
}
|
|
883
|
+
function fingerprint(prompt) {
|
|
884
|
+
const systemParts = [];
|
|
885
|
+
const userHashes = [];
|
|
886
|
+
for (const message of prompt) {
|
|
887
|
+
if (message.role === "system") systemParts.push(message.content);
|
|
888
|
+
else if (message.role === "user")
|
|
889
|
+
userHashes.push(sha(userMessageKey(message)));
|
|
890
|
+
}
|
|
891
|
+
return { systemHash: sha(systemParts.join("\n")), userHashes };
|
|
892
|
+
}
|
|
893
|
+
function isStrictPrefix(prefix, full) {
|
|
894
|
+
if (prefix.length >= full.length) return false;
|
|
895
|
+
for (let i = 0; i < prefix.length; i++) {
|
|
896
|
+
if (prefix[i] !== full[i]) return false;
|
|
897
|
+
}
|
|
898
|
+
return true;
|
|
899
|
+
}
|
|
900
|
+
function classifyTurn(prev, prompt) {
|
|
901
|
+
const fp = fingerprint(prompt);
|
|
902
|
+
if (!prev) return { kind: "new", fingerprint: fp };
|
|
903
|
+
if (prev.systemHash !== fp.systemHash)
|
|
904
|
+
return { kind: "side-call", fingerprint: fp };
|
|
905
|
+
const lastIsUser = prompt[prompt.length - 1]?.role === "user";
|
|
906
|
+
const exactlyOneNew = fp.userHashes.length === prev.userHashes.length + 1;
|
|
907
|
+
if (lastIsUser && exactlyOneNew && isStrictPrefix(prev.userHashes, fp.userHashes)) {
|
|
908
|
+
return { kind: "continuation", fingerprint: fp };
|
|
909
|
+
}
|
|
910
|
+
return { kind: "divergence", fingerprint: fp };
|
|
911
|
+
}
|
|
912
|
+
|
|
476
913
|
// src/provider/language-model.ts
|
|
477
914
|
var CursorLanguageModel = class {
|
|
478
915
|
constructor(modelId, config) {
|
|
@@ -503,8 +940,46 @@ var CursorLanguageModel = class {
|
|
|
503
940
|
providerOptions
|
|
504
941
|
);
|
|
505
942
|
const sessionID = typeof providerOptions?.["sessionID"] === "string" ? providerOptions["sessionID"] : void 0;
|
|
506
|
-
const
|
|
943
|
+
const dynamicMcp = providerOptions?.["mcpServers"];
|
|
944
|
+
const mcpServers = dynamicMcp ?? this.config.mcpServers;
|
|
945
|
+
const mcpHash = mcpServersFingerprint(mcpServers);
|
|
946
|
+
const sessionEnabled = (this.config.session ?? "auto") !== false;
|
|
507
947
|
const explicitAgentId = typeof providerOptions?.["agentId"] === "string" ? providerOptions["agentId"] : void 0;
|
|
948
|
+
const ephemeral = providerOptions?.["ephemeral"] === true;
|
|
949
|
+
const usePool = sessionEnabled && Boolean(sessionID) && !explicitAgentId;
|
|
950
|
+
let resumeAgentId = explicitAgentId;
|
|
951
|
+
let poolKey;
|
|
952
|
+
let record;
|
|
953
|
+
if (usePool) {
|
|
954
|
+
const classification = ephemeral ? {
|
|
955
|
+
kind: "side-call",
|
|
956
|
+
fingerprint: fingerprint(options.prompt)
|
|
957
|
+
} : classifyTurn(getSessionRecord(sessionID), options.prompt);
|
|
958
|
+
switch (classification.kind) {
|
|
959
|
+
case "continuation": {
|
|
960
|
+
const prev = getSessionRecord(sessionID);
|
|
961
|
+
if (prev?.mcpHash === mcpHash) {
|
|
962
|
+
resumeAgentId = prev?.agentId;
|
|
963
|
+
}
|
|
964
|
+
poolKey = sessionID;
|
|
965
|
+
record = { ...classification.fingerprint, mcpHash };
|
|
966
|
+
break;
|
|
967
|
+
}
|
|
968
|
+
case "new":
|
|
969
|
+
case "divergence":
|
|
970
|
+
poolKey = sessionID;
|
|
971
|
+
record = { ...classification.fingerprint, mcpHash };
|
|
972
|
+
break;
|
|
973
|
+
case "side-call":
|
|
974
|
+
break;
|
|
975
|
+
}
|
|
976
|
+
if (process.env["OPENCODE_CURSOR_DEBUG"] === "1") {
|
|
977
|
+
const label = classification.kind === "continuation" ? "resume" : `fresh:${classification.kind}`;
|
|
978
|
+
console.error(
|
|
979
|
+
`[cursor:debug] turn classification=${label} session=${sessionID}`
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
508
983
|
const acquired = await acquireAgent({
|
|
509
984
|
apiKey: this.requireApiKey(),
|
|
510
985
|
modelSelection,
|
|
@@ -512,12 +987,12 @@ var CursorLanguageModel = class {
|
|
|
512
987
|
cwd: this.config.cwd,
|
|
513
988
|
...this.config.settingSources ? { settingSources: this.config.settingSources } : {},
|
|
514
989
|
...this.config.sandbox !== void 0 ? { sandbox: this.config.sandbox } : {},
|
|
515
|
-
...
|
|
990
|
+
...mcpServers ? { mcpServers } : {},
|
|
516
991
|
...this.config.agents ? { agents: this.config.agents } : {},
|
|
517
|
-
...
|
|
518
|
-
...
|
|
519
|
-
|
|
520
|
-
|
|
992
|
+
...poolKey ? { name: `opencode/${sessionID.slice(-8)}` } : {},
|
|
993
|
+
...resumeAgentId ? { resumeAgentId } : {},
|
|
994
|
+
...poolKey ? { poolKey } : {},
|
|
995
|
+
...record ? { record } : {}
|
|
521
996
|
});
|
|
522
997
|
const message = acquired.resumed ? latestUserMessage(options.prompt) ?? promptToCursorMessage(options.prompt) : promptToCursorMessage(options.prompt);
|
|
523
998
|
try {
|
|
@@ -559,7 +1034,7 @@ function createCursor(options = {}) {
|
|
|
559
1034
|
...options.settingSources ? { settingSources: options.settingSources } : {},
|
|
560
1035
|
...options.sandbox !== void 0 ? { sandbox: options.sandbox } : {},
|
|
561
1036
|
...options.agents ? { agents: options.agents } : {},
|
|
562
|
-
|
|
1037
|
+
session: options.session ?? "auto",
|
|
563
1038
|
toolDisplay: options.toolDisplay ?? "blocks"
|
|
564
1039
|
};
|
|
565
1040
|
const notImplemented = (kind, modelId) => {
|