@stablekernel/opencode-cursor 0.4.6 → 0.5.0-next.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,5 +1,7 @@
1
1
  import {
2
2
  acquireAgent,
3
+ addUsage,
4
+ classifyError,
3
5
  dropSessionRecord,
4
6
  extractSystemText,
5
7
  getSessionRecord,
@@ -7,8 +9,9 @@ import {
7
9
  resolveCursorApiKey,
8
10
  resolveSystemDelivery,
9
11
  sendAgentTurnSilently,
12
+ setPreferredTransport,
10
13
  streamAgentTurn
11
- } from "../chunk-734L3SKU.js";
14
+ } from "../chunk-LAOFD3JB.js";
12
15
 
13
16
  // src/provider/index.ts
14
17
  import { NoSuchModelError } from "@ai-sdk/provider";
@@ -420,6 +423,26 @@ var NATIVE_ADAPTERS = {
420
423
  };
421
424
  }
422
425
  },
426
+ // Cursor `semSearch` has no opencode counterpart — format-only: query title
427
+ // + results body instead of the raw `{results}` JSON.
428
+ semSearch: {
429
+ input: (args) => {
430
+ const out = { query: strField(args, "query") ?? "" };
431
+ const dirs = isRecord(args) && Array.isArray(args["targetDirectories"]) ? args["targetDirectories"] : void 0;
432
+ if (dirs && dirs.length > 0) out["targetDirectories"] = dirs;
433
+ return out;
434
+ },
435
+ result: (value, args) => {
436
+ const query = strField(args, "query") ?? "";
437
+ const results = strField(value, "results") ?? "";
438
+ const count = results ? results.split("\n").filter((l) => l.trim().length > 0).length : 0;
439
+ return {
440
+ title: query,
441
+ metadata: { matches: count, truncated: false },
442
+ output: results || "No results"
443
+ };
444
+ }
445
+ },
423
446
  // Cursor `ls` → opencode `list` (flatten the directory tree into paths).
424
447
  ls: {
425
448
  tool: "list",
@@ -589,7 +612,39 @@ function editResultFields(id, filePath, diff, result) {
589
612
  };
590
613
  }
591
614
  function newBlockToolState() {
592
- return { open: /* @__PURE__ */ new Map(), pendingEdits: /* @__PURE__ */ new Map(), dropped: /* @__PURE__ */ new Set() };
615
+ return {
616
+ open: /* @__PURE__ */ new Map(),
617
+ pendingEdits: /* @__PURE__ */ new Map(),
618
+ dropped: /* @__PURE__ */ new Set(),
619
+ partials: /* @__PURE__ */ new Map(),
620
+ planText: /* @__PURE__ */ new Map()
621
+ };
622
+ }
623
+ function resolveToolName(name, input) {
624
+ const adapter = resolveAdapter(name, input);
625
+ return {
626
+ toolName: adapter?.tool ?? blockToolName(name),
627
+ ...adapter ? { adapter } : {}
628
+ };
629
+ }
630
+ function blockToolInputPartialParts(id, name, input, state) {
631
+ if (process.env["OPENCODE_CURSOR_TOOL_INPUT_STREAM"] === "0") return [];
632
+ const serialized = safeJsonString(input);
633
+ const prev = state.partials.get(id);
634
+ const toolName = prev?.toolName ?? resolveToolName(name, input).toolName;
635
+ state.partials.set(id, { toolName, serialized });
636
+ const parts = [];
637
+ if (!prev)
638
+ parts.push({
639
+ type: "tool-input-start",
640
+ id,
641
+ toolName,
642
+ providerExecuted: true,
643
+ dynamic: true
644
+ });
645
+ const delta = prev && serialized.startsWith(prev.serialized) ? serialized.slice(prev.serialized.length) : serialized;
646
+ if (delta) parts.push({ type: "tool-input-delta", id, delta });
647
+ return parts;
593
648
  }
594
649
  function blockToolCallParts(id, name, input, state) {
595
650
  if (name === EDIT_TOOL_NAME) {
@@ -649,6 +704,20 @@ function blockDanglingParts(state) {
649
704
  parts.push(toolResultObj(id, EDIT_TOOL_NAME, DANGLING_TOOL_RESULT, true));
650
705
  }
651
706
  state.pendingEdits.clear();
707
+ for (const [id, partial] of state.partials) {
708
+ parts.push({ type: "tool-input-end", id });
709
+ let args = {};
710
+ try {
711
+ args = JSON.parse(partial.serialized);
712
+ } catch {
713
+ args = {};
714
+ }
715
+ parts.push(nativeToolCall(id, partial.toolName, args));
716
+ parts.push(
717
+ nativeToolResult(id, partial.toolName, DANGLING_TOOL_RESULT, true)
718
+ );
719
+ }
720
+ state.partials.clear();
652
721
  return parts;
653
722
  }
654
723
  var EMPTY_USAGE = {
@@ -699,6 +768,8 @@ function cursorEventsToStream(events, toolDisplay = "blocks") {
699
768
  let reasoningCount = 0;
700
769
  let usage;
701
770
  let streamedText = false;
771
+ let thinkingMs = 0;
772
+ let compactions = 0;
702
773
  const toolState = newBlockToolState();
703
774
  const closeDanglingToolCalls = () => {
704
775
  for (const part of blockDanglingParts(toolState)) {
@@ -754,22 +825,61 @@ function cursorEventsToStream(events, toolDisplay = "blocks") {
754
825
  case "reasoning-delta":
755
826
  reasoningLine(event.text);
756
827
  break;
828
+ case "tool-input-partial":
829
+ if (isCreatePlanTool(event.name)) {
830
+ const plan = createPlanContent(event.input) ?? "";
831
+ const prevPlan = toolState.planText.get(event.id) ?? "";
832
+ if (plan.length > prevPlan.length) {
833
+ closeReasoning();
834
+ streamedText = true;
835
+ controller.enqueue({
836
+ type: "text-delta",
837
+ id: ensureText(),
838
+ delta: plan.slice(prevPlan.length)
839
+ });
840
+ }
841
+ toolState.planText.set(event.id, plan);
842
+ toolState.dropped.add(event.id);
843
+ break;
844
+ }
845
+ if (toolDisplay === "blocks" && event.name !== EDIT_TOOL_NAME) {
846
+ const parts = blockToolInputPartialParts(
847
+ event.id,
848
+ event.name,
849
+ event.input,
850
+ toolState
851
+ );
852
+ if (parts.length > 0) {
853
+ closeText();
854
+ closeReasoning();
855
+ }
856
+ for (const part of parts) controller.enqueue(part);
857
+ }
858
+ break;
757
859
  case "tool-call":
758
860
  if (isCreatePlanTool(event.name)) {
759
- const plan = createPlanContent(event.input);
760
- if (plan) {
861
+ const plan = createPlanContent(event.input) ?? "";
862
+ const prevPlan = toolState.planText.get(event.id) ?? "";
863
+ if (plan.length > prevPlan.length) {
761
864
  closeReasoning();
762
865
  streamedText = true;
763
866
  controller.enqueue({
764
867
  type: "text-delta",
765
868
  id: ensureText(),
766
- delta: plan
869
+ delta: plan.slice(prevPlan.length)
767
870
  });
768
871
  }
872
+ toolState.planText.set(event.id, plan);
769
873
  toolState.dropped.add(event.id);
770
874
  break;
771
875
  }
772
876
  if (toolDisplay === "blocks") {
877
+ if (toolState.partials.delete(event.id)) {
878
+ controller.enqueue({
879
+ type: "tool-input-end",
880
+ id: event.id
881
+ });
882
+ }
773
883
  const parts = blockToolCallParts(
774
884
  event.id,
775
885
  event.name,
@@ -814,6 +924,14 @@ ${formatToolCall(event.name, event.input)}
814
924
  case "usage":
815
925
  usage = mapUsage(event.usage);
816
926
  break;
927
+ case "reasoning-complete":
928
+ closeReasoning();
929
+ if (typeof event.durationMs === "number")
930
+ thinkingMs += event.durationMs;
931
+ break;
932
+ case "compaction":
933
+ compactions++;
934
+ break;
817
935
  case "finish":
818
936
  if (!streamedText && event.text) {
819
937
  controller.enqueue({
@@ -828,22 +946,34 @@ ${formatToolCall(event.name, event.input)}
828
946
  closeDanglingToolCalls();
829
947
  closeReasoning();
830
948
  closeText();
831
- controller.enqueue({
832
- type: "finish",
833
- usage: usage ?? EMPTY_USAGE,
834
- finishReason: FINISH_STOP
835
- });
949
+ {
950
+ const cursorMeta = {};
951
+ if (thinkingMs > 0) cursorMeta["thinkingDurationMs"] = thinkingMs;
952
+ if (compactions > 0) cursorMeta["compactions"] = compactions;
953
+ controller.enqueue({
954
+ type: "finish",
955
+ usage: usage ?? EMPTY_USAGE,
956
+ finishReason: FINISH_STOP,
957
+ ...Object.keys(cursorMeta).length > 0 ? { providerMetadata: { cursor: cursorMeta } } : {}
958
+ });
959
+ }
836
960
  controller.close();
837
961
  } catch (err) {
838
962
  controller.enqueue({ type: "error", error: err });
839
963
  closeDanglingToolCalls();
840
964
  closeReasoning();
841
965
  closeText();
842
- controller.enqueue({
843
- type: "finish",
844
- usage: usage ?? EMPTY_USAGE,
845
- finishReason: FINISH_ERROR
846
- });
966
+ {
967
+ const cursorMeta = {};
968
+ if (thinkingMs > 0) cursorMeta["thinkingDurationMs"] = thinkingMs;
969
+ if (compactions > 0) cursorMeta["compactions"] = compactions;
970
+ controller.enqueue({
971
+ type: "finish",
972
+ usage: usage ?? EMPTY_USAGE,
973
+ finishReason: FINISH_ERROR,
974
+ ...Object.keys(cursorMeta).length > 0 ? { providerMetadata: { cursor: cursorMeta } } : {}
975
+ });
976
+ }
847
977
  controller.close();
848
978
  }
849
979
  }
@@ -866,6 +996,8 @@ async function cursorEventsToContent(events, toolDisplay = "blocks") {
866
996
  case "reasoning-delta":
867
997
  reasoning += event.text;
868
998
  break;
999
+ case "tool-input-partial":
1000
+ break;
869
1001
  case "tool-call":
870
1002
  if (isCreatePlanTool(event.name)) {
871
1003
  const plan = createPlanContent(event.input);
@@ -908,6 +1040,10 @@ ${formatToolCall(event.name, event.input)}
908
1040
  case "usage":
909
1041
  usage = mapUsage(event.usage);
910
1042
  break;
1043
+ case "reasoning-complete":
1044
+ break;
1045
+ case "compaction":
1046
+ break;
911
1047
  case "finish":
912
1048
  if (!text && event.text) text = event.text;
913
1049
  break;
@@ -978,6 +1114,9 @@ function classifyTurn(prev, prompt) {
978
1114
  }
979
1115
  return { kind: "divergence", fingerprint: fp };
980
1116
  }
1117
+ function sendIdempotencyKey(sessionID, record, messageText) {
1118
+ return createHash("sha256").update(`${sessionID ?? "ephemeral"}|${record?.userHashes.join(",") ?? ""}|${messageText}`).digest("hex").slice(0, 32);
1119
+ }
981
1120
 
982
1121
  // src/provider/language-model.ts
983
1122
  var CursorLanguageModel = class {
@@ -1014,9 +1153,18 @@ var CursorLanguageModel = class {
1014
1153
  const providerOptions = options.providerOptions?.[this.provider];
1015
1154
  const { mode, modelSelection } = resolveControls(
1016
1155
  this.modelId,
1017
- { mode: this.config.mode, params: this.config.params },
1156
+ {
1157
+ mode: this.config.mode,
1158
+ params: this.config.params,
1159
+ defaults: this.config.modelParamDefaults?.[this.modelId]
1160
+ },
1018
1161
  providerOptions
1019
1162
  );
1163
+ if (process.env["OPENCODE_CURSOR_DEBUG"] === "1") {
1164
+ console.error(
1165
+ `[cursor:debug] model=${this.modelId} selection=${JSON.stringify(modelSelection)}`
1166
+ );
1167
+ }
1020
1168
  const sessionID = typeof providerOptions?.["sessionID"] === "string" ? providerOptions["sessionID"] : void 0;
1021
1169
  const dynamicMcp = providerOptions?.["mcpServers"];
1022
1170
  const mcpServers = dynamicMcp ?? this.config.mcpServers;
@@ -1072,6 +1220,12 @@ var CursorLanguageModel = class {
1072
1220
  resumeAgentId = void 0;
1073
1221
  }
1074
1222
  }
1223
+ const latestUser = latestUserMessage(options.prompt);
1224
+ const idempotencyKey = sendIdempotencyKey(
1225
+ sessionID,
1226
+ record,
1227
+ latestUser?.text ?? JSON.stringify(options.prompt)
1228
+ );
1075
1229
  const delivery = resolveSystemDelivery({
1076
1230
  mode: this.config.systemPrompt ?? "rules",
1077
1231
  settingSources: this.config.settingSources,
@@ -1088,6 +1242,7 @@ var CursorLanguageModel = class {
1088
1242
  cwd: this.config.cwd,
1089
1243
  ...settingSources ? { settingSources } : {},
1090
1244
  ...this.config.sandbox !== void 0 ? { sandbox: this.config.sandbox } : {},
1245
+ ...this.config.autoReview !== void 0 ? { autoReview: this.config.autoReview } : {},
1091
1246
  ...mcpServers ? { mcpServers } : {},
1092
1247
  ...this.config.agents ? { agents: this.config.agents } : {},
1093
1248
  ...poolKey ? { name: `opencode/${sessionID.slice(-8)}` } : {},
@@ -1105,21 +1260,45 @@ var CursorLanguageModel = class {
1105
1260
  let delivered = false;
1106
1261
  try {
1107
1262
  let aborted = false;
1263
+ let replayUsage;
1108
1264
  for (let i = 0; i < multiTurns.length - 1; i++) {
1109
1265
  if (options.abortSignal?.aborted) {
1110
1266
  aborted = true;
1111
1267
  break;
1112
1268
  }
1113
- await sendAgentTurnSilently(acquired.agent, multiTurns[i], {
1114
- mode,
1115
- abortSignal: options.abortSignal
1116
- });
1269
+ replayUsage = addUsage(
1270
+ replayUsage,
1271
+ await sendAgentTurnSilently(acquired.agent, multiTurns[i], {
1272
+ mode,
1273
+ abortSignal: options.abortSignal,
1274
+ idempotencyKey: sendIdempotencyKey(
1275
+ sessionID,
1276
+ { userHashes: [...record?.userHashes ?? [], String(i)] },
1277
+ multiTurns[i].text
1278
+ )
1279
+ })
1280
+ );
1117
1281
  }
1118
1282
  if (!aborted && !options.abortSignal?.aborted) {
1119
1283
  for await (const event of streamAgentTurn(
1120
1284
  acquired.agent,
1121
1285
  multiTurns[multiTurns.length - 1],
1122
- { mode, abortSignal: options.abortSignal }
1286
+ {
1287
+ mode,
1288
+ abortSignal: options.abortSignal,
1289
+ // Key intentionally diverges from the single-turn key: the multi-replay and single-turn branches are mutually exclusive.
1290
+ idempotencyKey: sendIdempotencyKey(
1291
+ sessionID,
1292
+ {
1293
+ userHashes: [
1294
+ ...record?.userHashes ?? [],
1295
+ String(multiTurns.length - 1)
1296
+ ]
1297
+ },
1298
+ multiTurns[multiTurns.length - 1].text
1299
+ ),
1300
+ ...replayUsage ? { usageBase: replayUsage } : {}
1301
+ }
1123
1302
  )) {
1124
1303
  yielded = true;
1125
1304
  yield event;
@@ -1134,13 +1313,22 @@ var CursorLanguageModel = class {
1134
1313
  try {
1135
1314
  for await (const event of streamAgentTurn(acquired.agent, message, {
1136
1315
  mode,
1137
- abortSignal: options.abortSignal
1316
+ abortSignal: options.abortSignal,
1317
+ idempotencyKey
1138
1318
  })) {
1139
1319
  yielded = true;
1140
1320
  yield event;
1141
1321
  }
1142
1322
  } catch (err) {
1143
- if (acquired.resumed && !yielded && !options.abortSignal?.aborted) {
1323
+ const classified = classifyError(err);
1324
+ if (classified.kind === "auth" || classified.kind === "config") {
1325
+ throw classified.helpUrl ? new Error(
1326
+ `${classified.message} (see ${classified.helpUrl})`,
1327
+ { cause: err }
1328
+ ) : err;
1329
+ }
1330
+ const replayable = classified.kind === "agent-not-found" || classified.kind === "rate-limit" || classified.kind === "network" || classified.kind === "unknown";
1331
+ if (replayable && acquired.resumed && !yielded && !options.abortSignal?.aborted) {
1144
1332
  if (process.env["OPENCODE_CURSOR_DEBUG"] === "1") {
1145
1333
  console.error(
1146
1334
  "[cursor:debug] resumed turn failed before emitting; retrying with a fresh agent"
@@ -1166,7 +1354,8 @@ var CursorLanguageModel = class {
1166
1354
  const replay = promptToCursorMessage(options.prompt, systemMode);
1167
1355
  yield* streamAgentTurn(retry.agent, replay, {
1168
1356
  mode,
1169
- abortSignal: options.abortSignal
1357
+ abortSignal: options.abortSignal,
1358
+ idempotencyKey
1170
1359
  });
1171
1360
  } finally {
1172
1361
  retry.release();
@@ -1199,6 +1388,7 @@ var CursorLanguageModel = class {
1199
1388
 
1200
1389
  // src/provider/index.ts
1201
1390
  function createCursor(options = {}) {
1391
+ if (options.transport) setPreferredTransport(options.transport);
1202
1392
  const mcpServers = options.mcpServers && Object.keys(options.mcpServers).length > 0 ? options.mcpServers : void 0;
1203
1393
  const config = {
1204
1394
  providerName: options.name ?? "cursor",
@@ -1206,9 +1396,11 @@ function createCursor(options = {}) {
1206
1396
  cwd: options.cwd ?? process.cwd(),
1207
1397
  mode: options.mode ?? "agent",
1208
1398
  ...options.params ? { params: options.params } : {},
1399
+ ...options.modelParamDefaults ? { modelParamDefaults: options.modelParamDefaults } : {},
1209
1400
  ...mcpServers ? { mcpServers } : {},
1210
1401
  ...options.settingSources ? { settingSources: options.settingSources } : {},
1211
1402
  ...options.sandbox !== void 0 ? { sandbox: options.sandbox } : {},
1403
+ ...options.autoReview !== void 0 ? { autoReview: options.autoReview } : {},
1212
1404
  ...options.agents ? { agents: options.agents } : {},
1213
1405
  session: options.session ?? "auto",
1214
1406
  toolDisplay: options.toolDisplay ?? "blocks",