@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.
@@ -1,9 +1,10 @@
1
1
  import {
2
2
  acquireAgent,
3
+ getSessionRecord,
3
4
  resolveControls,
4
5
  resolveCursorApiKey,
5
6
  streamAgentTurn
6
- } from "../chunk-D4YQ7ZEM.js";
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") text.push(`(thinking) ${part.text}`);
44
- else if (part.type === "tool-call") text.push(`[called ${part.toolName}(${part.input})]`);
45
- else if (part.type === "tool-result") text.push(`[result of ${part.toolName}]`);
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(`# Tool result (${part.toolName})
55
- ${JSON.stringify(part.output)}`);
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 toolCallObj(id, name, input) {
137
+ function nativeToolCall(id, toolName, input) {
115
138
  return {
116
139
  type: "tool-call",
117
140
  toolCallId: id,
118
- toolName: blockToolName(name),
141
+ toolName,
119
142
  input: safeJsonString(input),
120
143
  providerExecuted: true,
121
144
  dynamic: true
122
145
  };
123
146
  }
124
- function toolResultObj(id, name, result, isError) {
147
+ function nativeToolResult(id, toolName, result, isError) {
125
148
  return {
126
149
  type: "tool-result",
127
150
  toolCallId: id,
128
- toolName: blockToolName(name),
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 { openToolCalls: /* @__PURE__ */ new Map(), pendingEdits: /* @__PURE__ */ new Map() };
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
- state.openToolCalls.set(id, name);
200
- return [toolCallObj(id, name, input)];
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.openToolCalls.delete(id);
219
- return [toolResultObj(id, name, result, isError)];
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, name] of state.openToolCalls) {
224
- parts.push(toolResultObj(id, name, DANGLING_TOOL_RESULT, true));
601
+ for (const [id, open] of state.open) {
602
+ parts.push(nativeToolResult(id, open.toolName, DANGLING_TOOL_RESULT, true));
225
603
  }
226
- state.openToolCalls.clear();
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
- for (const part of blockToolCallParts(
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
- for (const part of blockToolResultParts(
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 useSession = this.config.session === true && Boolean(sessionID);
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
- ...this.config.mcpServers ? { mcpServers: this.config.mcpServers } : {},
990
+ ...mcpServers ? { mcpServers } : {},
516
991
  ...this.config.agents ? { agents: this.config.agents } : {},
517
- ...useSession ? { name: `opencode/${sessionID.slice(-8)}` } : {},
518
- ...explicitAgentId ? { agentId: explicitAgentId } : {},
519
- sessionID,
520
- session: useSession
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
- ...options.session !== void 0 ? { session: options.session } : {},
1037
+ session: options.session ?? "auto",
563
1038
  toolDisplay: options.toolDisplay ?? "blocks"
564
1039
  };
565
1040
  const notImplemented = (kind, modelId) => {