@stablekernel/opencode-cursor 0.4.7-next.0 → 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-SVIXYMHP.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 {
@@ -1081,6 +1220,12 @@ var CursorLanguageModel = class {
1081
1220
  resumeAgentId = void 0;
1082
1221
  }
1083
1222
  }
1223
+ const latestUser = latestUserMessage(options.prompt);
1224
+ const idempotencyKey = sendIdempotencyKey(
1225
+ sessionID,
1226
+ record,
1227
+ latestUser?.text ?? JSON.stringify(options.prompt)
1228
+ );
1084
1229
  const delivery = resolveSystemDelivery({
1085
1230
  mode: this.config.systemPrompt ?? "rules",
1086
1231
  settingSources: this.config.settingSources,
@@ -1097,6 +1242,7 @@ var CursorLanguageModel = class {
1097
1242
  cwd: this.config.cwd,
1098
1243
  ...settingSources ? { settingSources } : {},
1099
1244
  ...this.config.sandbox !== void 0 ? { sandbox: this.config.sandbox } : {},
1245
+ ...this.config.autoReview !== void 0 ? { autoReview: this.config.autoReview } : {},
1100
1246
  ...mcpServers ? { mcpServers } : {},
1101
1247
  ...this.config.agents ? { agents: this.config.agents } : {},
1102
1248
  ...poolKey ? { name: `opencode/${sessionID.slice(-8)}` } : {},
@@ -1114,21 +1260,45 @@ var CursorLanguageModel = class {
1114
1260
  let delivered = false;
1115
1261
  try {
1116
1262
  let aborted = false;
1263
+ let replayUsage;
1117
1264
  for (let i = 0; i < multiTurns.length - 1; i++) {
1118
1265
  if (options.abortSignal?.aborted) {
1119
1266
  aborted = true;
1120
1267
  break;
1121
1268
  }
1122
- await sendAgentTurnSilently(acquired.agent, multiTurns[i], {
1123
- mode,
1124
- abortSignal: options.abortSignal
1125
- });
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
+ );
1126
1281
  }
1127
1282
  if (!aborted && !options.abortSignal?.aborted) {
1128
1283
  for await (const event of streamAgentTurn(
1129
1284
  acquired.agent,
1130
1285
  multiTurns[multiTurns.length - 1],
1131
- { 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
+ }
1132
1302
  )) {
1133
1303
  yielded = true;
1134
1304
  yield event;
@@ -1143,13 +1313,22 @@ var CursorLanguageModel = class {
1143
1313
  try {
1144
1314
  for await (const event of streamAgentTurn(acquired.agent, message, {
1145
1315
  mode,
1146
- abortSignal: options.abortSignal
1316
+ abortSignal: options.abortSignal,
1317
+ idempotencyKey
1147
1318
  })) {
1148
1319
  yielded = true;
1149
1320
  yield event;
1150
1321
  }
1151
1322
  } catch (err) {
1152
- 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) {
1153
1332
  if (process.env["OPENCODE_CURSOR_DEBUG"] === "1") {
1154
1333
  console.error(
1155
1334
  "[cursor:debug] resumed turn failed before emitting; retrying with a fresh agent"
@@ -1175,7 +1354,8 @@ var CursorLanguageModel = class {
1175
1354
  const replay = promptToCursorMessage(options.prompt, systemMode);
1176
1355
  yield* streamAgentTurn(retry.agent, replay, {
1177
1356
  mode,
1178
- abortSignal: options.abortSignal
1357
+ abortSignal: options.abortSignal,
1358
+ idempotencyKey
1179
1359
  });
1180
1360
  } finally {
1181
1361
  retry.release();
@@ -1208,6 +1388,7 @@ var CursorLanguageModel = class {
1208
1388
 
1209
1389
  // src/provider/index.ts
1210
1390
  function createCursor(options = {}) {
1391
+ if (options.transport) setPreferredTransport(options.transport);
1211
1392
  const mcpServers = options.mcpServers && Object.keys(options.mcpServers).length > 0 ? options.mcpServers : void 0;
1212
1393
  const config = {
1213
1394
  providerName: options.name ?? "cursor",
@@ -1219,6 +1400,7 @@ function createCursor(options = {}) {
1219
1400
  ...mcpServers ? { mcpServers } : {},
1220
1401
  ...options.settingSources ? { settingSources: options.settingSources } : {},
1221
1402
  ...options.sandbox !== void 0 ? { sandbox: options.sandbox } : {},
1403
+ ...options.autoReview !== void 0 ? { autoReview: options.autoReview } : {},
1222
1404
  ...options.agents ? { agents: options.agents } : {},
1223
1405
  session: options.session ?? "auto",
1224
1406
  toolDisplay: options.toolDisplay ?? "blocks",