@stablekernel/opencode-cursor 0.4.4 → 0.4.5-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,10 +1,14 @@
1
1
  import {
2
2
  acquireAgent,
3
+ dropSessionRecord,
4
+ extractSystemText,
3
5
  getSessionRecord,
4
6
  resolveControls,
5
7
  resolveCursorApiKey,
8
+ resolveSystemDelivery,
9
+ sendAgentTurnSilently,
6
10
  streamAgentTurn
7
- } from "../chunk-KGDYXGZL.js";
11
+ } from "../chunk-734L3SKU.js";
8
12
 
9
13
  // src/provider/index.ts
10
14
  import { NoSuchModelError } from "@ai-sdk/provider";
@@ -13,6 +17,7 @@ import { NoSuchModelError } from "@ai-sdk/provider";
13
17
  import { LoadAPIKeyError } from "@ai-sdk/provider";
14
18
 
15
19
  // src/provider/message-map.ts
20
+ import { fileURLToPath } from "url";
16
21
  var TOOL_RESULT_CAP = 2e3;
17
22
  var TOOL_ARGS_CAP = 500;
18
23
  function stringify(value) {
@@ -26,25 +31,21 @@ function stringify(value) {
26
31
  function truncate(text, cap) {
27
32
  return text.length > cap ? `${text.slice(0, cap)}\u2026[+${text.length - cap} chars]` : text;
28
33
  }
29
- function promptToCursorMessage(prompt) {
34
+ function promptToCursorMessage(prompt, systemPrompt = "rules") {
30
35
  const lines = [];
31
- const images = [];
32
- prompt.forEach((message, index) => {
33
- const isLast = index === prompt.length - 1;
36
+ prompt.forEach((message) => {
34
37
  switch (message.role) {
35
38
  case "system":
36
- lines.push(`# System
39
+ if (systemPrompt === "message") {
40
+ lines.push(`# System
37
41
  ${message.content}`);
42
+ }
38
43
  break;
39
44
  case "user": {
40
45
  const text = [];
41
46
  for (const part of message.content) {
42
47
  if (part.type === "text") text.push(part.text);
43
- else if (part.type === "file" && part.mediaType.startsWith("image/")) {
44
- const image = fileToImage(part.data, part.mediaType);
45
- if (isLast && image) images.push(image);
46
- text.push("[image attached]");
47
- }
48
+ else if (part.type === "file") text.push(fileNote(part));
48
49
  }
49
50
  lines.push(`# User
50
51
  ${text.join("\n")}`);
@@ -82,37 +83,49 @@ ${truncate(stringify(part.output), TOOL_RESULT_CAP)}`
82
83
  }
83
84
  }
84
85
  });
85
- const out = { text: lines.join("\n\n") };
86
- if (images.length > 0) out.images = images;
87
- return out;
86
+ return { text: lines.join("\n\n") };
87
+ }
88
+ function fileNote(part) {
89
+ const name = part.filename ?? describeSource(part.data) ?? "file";
90
+ return `[attached file: ${name} (${part.mediaType}) \u2014 not forwarded to Cursor]`;
88
91
  }
89
- function fileToImage(data, mediaType) {
90
- if (data instanceof URL) return { url: data.toString() };
91
- if (typeof data === "string") {
92
- if (/^https?:\/\//i.test(data)) return { url: data };
93
- return { data, mimeType: mediaType };
92
+ var URL_SCHEME = /^[a-z][a-z0-9+.-]*:\/\//i;
93
+ function describeSource(data) {
94
+ if (data instanceof URL)
95
+ return data.protocol === "file:" ? fileUrlToPath(data) : data.href;
96
+ if (typeof data === "string" && URL_SCHEME.test(data))
97
+ return data.startsWith("file://") ? fileUrlToPath(data) : data;
98
+ return void 0;
99
+ }
100
+ function fileUrlToPath(url) {
101
+ try {
102
+ return fileURLToPath(url);
103
+ } catch {
104
+ return typeof url === "string" ? url : url.href;
94
105
  }
95
- if (data instanceof Uint8Array) {
96
- return { data: Buffer.from(data).toString("base64"), mimeType: mediaType };
106
+ }
107
+ function userTurnToCursorMessage(message) {
108
+ const text = [];
109
+ for (const part of message.content) {
110
+ if (part.type === "text") text.push(part.text);
111
+ else if (part.type === "file") text.push(fileNote(part));
97
112
  }
98
- return void 0;
113
+ return { text: text.join("\n") };
99
114
  }
100
115
  function latestUserMessage(prompt) {
101
116
  const last = prompt[prompt.length - 1];
102
117
  if (!last || last.role !== "user") return void 0;
103
- const text = [];
104
- const images = [];
105
- for (const part of last.content) {
106
- if (part.type === "text") text.push(part.text);
107
- else if (part.type === "file" && part.mediaType.startsWith("image/")) {
108
- const image = fileToImage(part.data, part.mediaType);
109
- if (image) images.push(image);
110
- text.push("[image attached]");
111
- }
118
+ return userTurnToCursorMessage(last);
119
+ }
120
+ function trailingUserMessages(prompt, count) {
121
+ if (count <= 0) return [];
122
+ const collected = [];
123
+ for (let i = prompt.length - 1; i >= 0 && collected.length < count; i--) {
124
+ const message = prompt[i];
125
+ if (!message || message.role !== "user") break;
126
+ collected.push(userTurnToCursorMessage(message));
112
127
  }
113
- const out = { text: text.join("\n") };
114
- if (images.length > 0) out.images = images;
115
- return out;
128
+ return collected.reverse();
116
129
  }
117
130
 
118
131
  // src/provider/stream-map.ts
@@ -954,9 +967,14 @@ function classifyTurn(prev, prompt) {
954
967
  if (prev.systemHash !== fp.systemHash)
955
968
  return { kind: "side-call", fingerprint: fp };
956
969
  const lastIsUser = prompt[prompt.length - 1]?.role === "user";
957
- const exactlyOneNew = fp.userHashes.length === prev.userHashes.length + 1;
958
- if (lastIsUser && exactlyOneNew && isStrictPrefix(prev.userHashes, fp.userHashes)) {
959
- return { kind: "continuation", fingerprint: fp };
970
+ const newUserCount = fp.userHashes.length - prev.userHashes.length;
971
+ const isPrefix = isStrictPrefix(prev.userHashes, fp.userHashes);
972
+ if (lastIsUser && isPrefix && newUserCount === 1) {
973
+ return { kind: "continuation", fingerprint: fp, newUserCount: 1 };
974
+ }
975
+ const tailContiguous = newUserCount >= 2 && prompt.length >= newUserCount && prompt.slice(prompt.length - newUserCount).every((m) => m.role === "user");
976
+ if (lastIsUser && isPrefix && tailContiguous) {
977
+ return { kind: "continuation-multi", fingerprint: fp, newUserCount };
960
978
  }
961
979
  return { kind: "divergence", fingerprint: fp };
962
980
  }
@@ -972,8 +990,17 @@ var CursorLanguageModel = class {
972
990
  specificationVersion = "v3";
973
991
  modelId;
974
992
  provider;
975
- // Images are passed inline as base64 data, so no URLs are fetched natively.
993
+ // The local Cursor agent has no attachment channel, so file parts (images,
994
+ // directories, other media) are noted as text in message-map rather than
995
+ // fetched or attached; no URLs are resolved natively.
976
996
  supportedUrls = {};
997
+ /** Messages already emitted, so degradation warnings fire once, not per turn. */
998
+ warned = /* @__PURE__ */ new Set();
999
+ warnOnce(message) {
1000
+ if (this.warned.has(message)) return;
1001
+ this.warned.add(message);
1002
+ console.warn(`[${this.provider}] ${message}`);
1003
+ }
977
1004
  requireApiKey() {
978
1005
  const apiKey = resolveCursorApiKey(this.config.apiKey);
979
1006
  if (!apiKey) {
@@ -1001,19 +1028,24 @@ var CursorLanguageModel = class {
1001
1028
  let resumeAgentId = explicitAgentId;
1002
1029
  let poolKey;
1003
1030
  let record;
1031
+ let multiNewUserCount = 0;
1004
1032
  if (usePool) {
1005
1033
  const classification = ephemeral ? {
1006
1034
  kind: "side-call",
1007
1035
  fingerprint: fingerprint(options.prompt)
1008
1036
  } : classifyTurn(getSessionRecord(sessionID), options.prompt);
1009
1037
  switch (classification.kind) {
1010
- case "continuation": {
1038
+ case "continuation":
1039
+ case "continuation-multi": {
1011
1040
  const prev = getSessionRecord(sessionID);
1012
1041
  if (prev?.mcpHash === mcpHash) {
1013
1042
  resumeAgentId = prev?.agentId;
1014
1043
  }
1015
1044
  poolKey = sessionID;
1016
1045
  record = { ...classification.fingerprint, mcpHash };
1046
+ if (classification.kind === "continuation-multi") {
1047
+ multiNewUserCount = classification.newUserCount ?? 0;
1048
+ }
1017
1049
  break;
1018
1050
  }
1019
1051
  case "new":
@@ -1025,34 +1057,127 @@ var CursorLanguageModel = class {
1025
1057
  break;
1026
1058
  }
1027
1059
  if (process.env["OPENCODE_CURSOR_DEBUG"] === "1") {
1028
- const label = classification.kind === "continuation" ? "resume" : `fresh:${classification.kind}`;
1060
+ const label = classification.kind === "continuation" ? "resume" : classification.kind === "continuation-multi" ? `resume-multi:${multiNewUserCount}` : `fresh:${classification.kind}`;
1029
1061
  console.error(
1030
1062
  `[cursor:debug] turn classification=${label} session=${sessionID}`
1031
1063
  );
1032
1064
  }
1033
1065
  }
1034
- const acquired = await acquireAgent({
1066
+ let multiTurns;
1067
+ if (multiNewUserCount >= 2) {
1068
+ const turns = trailingUserMessages(options.prompt, multiNewUserCount);
1069
+ if (turns.length === multiNewUserCount) {
1070
+ multiTurns = turns;
1071
+ } else {
1072
+ resumeAgentId = void 0;
1073
+ }
1074
+ }
1075
+ const delivery = resolveSystemDelivery({
1076
+ mode: this.config.systemPrompt ?? "rules",
1077
+ settingSources: this.config.settingSources,
1078
+ cwd: this.config.cwd,
1079
+ systemText: extractSystemText(options.prompt),
1080
+ warn: (message) => this.warnOnce(message)
1081
+ });
1082
+ const systemMode = delivery.mode;
1083
+ const settingSources = delivery.settingSources;
1084
+ const baseAcquire = {
1035
1085
  apiKey: this.requireApiKey(),
1036
1086
  modelSelection,
1037
1087
  mode,
1038
1088
  cwd: this.config.cwd,
1039
- ...this.config.settingSources ? { settingSources: this.config.settingSources } : {},
1089
+ ...settingSources ? { settingSources } : {},
1040
1090
  ...this.config.sandbox !== void 0 ? { sandbox: this.config.sandbox } : {},
1041
1091
  ...mcpServers ? { mcpServers } : {},
1042
1092
  ...this.config.agents ? { agents: this.config.agents } : {},
1043
1093
  ...poolKey ? { name: `opencode/${sessionID.slice(-8)}` } : {},
1044
- ...resumeAgentId ? { resumeAgentId } : {},
1045
1094
  ...poolKey ? { poolKey } : {},
1046
1095
  ...record ? { record } : {}
1096
+ };
1097
+ const acquired = await acquireAgent({
1098
+ ...baseAcquire,
1099
+ ...resumeAgentId ? { resumeAgentId } : {}
1047
1100
  });
1048
- const message = acquired.resumed ? latestUserMessage(options.prompt) ?? promptToCursorMessage(options.prompt) : promptToCursorMessage(options.prompt);
1101
+ let yielded = false;
1102
+ let releasedOriginal = false;
1049
1103
  try {
1050
- yield* streamAgentTurn(acquired.agent, message, {
1051
- mode,
1052
- abortSignal: options.abortSignal
1053
- });
1104
+ if (acquired.resumed && multiTurns) {
1105
+ let delivered = false;
1106
+ try {
1107
+ let aborted = false;
1108
+ for (let i = 0; i < multiTurns.length - 1; i++) {
1109
+ if (options.abortSignal?.aborted) {
1110
+ aborted = true;
1111
+ break;
1112
+ }
1113
+ await sendAgentTurnSilently(acquired.agent, multiTurns[i], {
1114
+ mode,
1115
+ abortSignal: options.abortSignal
1116
+ });
1117
+ }
1118
+ if (!aborted && !options.abortSignal?.aborted) {
1119
+ for await (const event of streamAgentTurn(
1120
+ acquired.agent,
1121
+ multiTurns[multiTurns.length - 1],
1122
+ { mode, abortSignal: options.abortSignal }
1123
+ )) {
1124
+ yielded = true;
1125
+ yield event;
1126
+ }
1127
+ delivered = true;
1128
+ }
1129
+ } finally {
1130
+ if (!delivered && sessionID) dropSessionRecord(sessionID);
1131
+ }
1132
+ } else {
1133
+ const message = acquired.resumed ? latestUserMessage(options.prompt) ?? promptToCursorMessage(options.prompt, systemMode) : promptToCursorMessage(options.prompt, systemMode);
1134
+ try {
1135
+ for await (const event of streamAgentTurn(acquired.agent, message, {
1136
+ mode,
1137
+ abortSignal: options.abortSignal
1138
+ })) {
1139
+ yielded = true;
1140
+ yield event;
1141
+ }
1142
+ } catch (err) {
1143
+ if (acquired.resumed && !yielded && !options.abortSignal?.aborted) {
1144
+ if (process.env["OPENCODE_CURSOR_DEBUG"] === "1") {
1145
+ console.error(
1146
+ "[cursor:debug] resumed turn failed before emitting; retrying with a fresh agent"
1147
+ );
1148
+ }
1149
+ acquired.release();
1150
+ releasedOriginal = true;
1151
+ let retry;
1152
+ try {
1153
+ retry = await acquireAgent({ ...baseAcquire });
1154
+ } catch (retryErr) {
1155
+ if (retryErr instanceof Error && retryErr.cause === void 0) {
1156
+ retryErr.cause = err;
1157
+ } else if (process.env["OPENCODE_CURSOR_DEBUG"] === "1") {
1158
+ console.error(
1159
+ "[cursor:debug] original resume failure (not attachable as cause):",
1160
+ err
1161
+ );
1162
+ }
1163
+ throw retryErr;
1164
+ }
1165
+ try {
1166
+ const replay = promptToCursorMessage(options.prompt, systemMode);
1167
+ yield* streamAgentTurn(retry.agent, replay, {
1168
+ mode,
1169
+ abortSignal: options.abortSignal
1170
+ });
1171
+ } finally {
1172
+ retry.release();
1173
+ }
1174
+ } else {
1175
+ throw err;
1176
+ }
1177
+ }
1178
+ }
1054
1179
  } finally {
1055
- acquired.release();
1180
+ if (!releasedOriginal) acquired.release();
1056
1181
  }
1057
1182
  }
1058
1183
  async doStream(options) {
@@ -1086,7 +1211,8 @@ function createCursor(options = {}) {
1086
1211
  ...options.sandbox !== void 0 ? { sandbox: options.sandbox } : {},
1087
1212
  ...options.agents ? { agents: options.agents } : {},
1088
1213
  session: options.session ?? "auto",
1089
- toolDisplay: options.toolDisplay ?? "blocks"
1214
+ toolDisplay: options.toolDisplay ?? "blocks",
1215
+ systemPrompt: options.systemPrompt ?? "rules"
1090
1216
  };
1091
1217
  const notImplemented = (kind, modelId) => {
1092
1218
  throw new NoSuchModelError({