cursor-opencode-provider 0.2.7 → 0.2.9
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/README.md +22 -6
- package/dist/agent-url.d.ts +8 -5
- package/dist/auth.js +59 -27
- package/dist/context/agents.d.ts +5 -1
- package/dist/context/agents.js +90 -30
- package/dist/context/build.js +63 -8
- package/dist/context/paths.d.ts +31 -6
- package/dist/context/paths.js +56 -18
- package/dist/context/plugins.js +16 -12
- package/dist/context/rules.js +26 -23
- package/dist/context/skills.js +8 -3
- package/dist/deadline.d.ts +8 -0
- package/dist/deadline.js +25 -0
- package/dist/language-model.js +206 -70
- package/dist/models.d.ts +2 -0
- package/dist/plugin.js +8 -1
- package/dist/protocol/client-version.js +7 -6
- package/dist/protocol/messages.js +24 -1
- package/dist/protocol/struct.d.ts +18 -0
- package/dist/protocol/struct.js +82 -0
- package/dist/protocol/tool-call-bridge.js +22 -2
- package/dist/protocol/tools.d.ts +67 -0
- package/dist/protocol/tools.js +342 -18
- package/dist/replay-safety.d.ts +23 -0
- package/dist/replay-safety.js +126 -0
- package/dist/session.d.ts +5 -0
- package/dist/transport/connect.d.ts +2 -0
- package/dist/transport/connect.js +106 -82
- package/dist/web-tools.d.ts +34 -0
- package/dist/web-tools.js +127 -0
- package/package.json +2 -2
package/dist/language-model.js
CHANGED
|
@@ -6,11 +6,11 @@ import { trace, traceRequestContextPaths } from "./debug.js";
|
|
|
6
6
|
import { buildRunRequest, buildHeartbeat } from "./protocol/request.js";
|
|
7
7
|
import { decodeFramePayload } from "./protocol/framing.js";
|
|
8
8
|
import { decodeMessage } from "./protocol/messages.js";
|
|
9
|
-
import { parseExecServerMessage, buildToolCallPart, buildExecClientMessages, parseExecIdFromToolCallId, detectExecVariantField, buildRequestContextResult, buildMcpStateResult, } from "./protocol/tools.js";
|
|
9
|
+
import { parseExecServerMessage, buildToolCallPart, buildExecClientMessages, buildReadRejectionMessages, classifyMissingReadTarget, resolveReadTargetPath, parseExecIdFromToolCallId, detectExecVariantField, buildRequestContextResult, buildMcpStateResult, buildCustomWebToolAliases, extractHostSubagentCatalog, resolveCustomWebToolAlias, remapNativeSubagentForCatalog, CUSTOM_WEBFETCH_TOOL, CUSTOM_WEBSEARCH_TOOL, } from "./protocol/tools.js";
|
|
10
10
|
import { describeCursorExecVariant } from "./protocol/exec-variants.js";
|
|
11
11
|
import { advertisedToolNamesFromDescriptors, extractExecDisplayCallId, extractProtobufSubmessage, listProtobufFieldNumbers, parseDisplayToolCall, resolveBridgedOpenCodeToolCall, } from "./protocol/tool-call-bridge.js";
|
|
12
12
|
import { handleKvServerMessage } from "./protocol/kv.js";
|
|
13
|
-
import { handleInteractionQuery
|
|
13
|
+
import { handleInteractionQuery } from "./protocol/interactions.js";
|
|
14
14
|
import { getCheckpoint, setCheckpoint } from "./protocol/checkpoint.js";
|
|
15
15
|
import { conversationBlobCount } from "./protocol/blob-store.js";
|
|
16
16
|
import { bindConversationId, } from "./protocol/conversation-bind.js";
|
|
@@ -23,6 +23,8 @@ import { opencodeGlobalCacheDir, setHostCacheDirOverride } from "./context/paths
|
|
|
23
23
|
import { resolveAgentUrl } from "./agent-url.js";
|
|
24
24
|
import { CURSOR_API_HOST, CURSOR_COMPACTION_OPTION } from "./shared.js";
|
|
25
25
|
import { consumeCursorShellResult, registerCursorShellCall, } from "./shell-timeout.js";
|
|
26
|
+
import { analyzeReplayFrame, AttemptReplaySafety } from "./replay-safety.js";
|
|
27
|
+
import { readAllFieldsStrict } from "./protocol/struct.js";
|
|
26
28
|
let _availableModels;
|
|
27
29
|
// mtime of the cache file the last time we loaded it. Compared on each call
|
|
28
30
|
// so discoverModels' background refresh is picked up without a process restart.
|
|
@@ -44,6 +46,28 @@ const DEFAULT_RETRY_POLICY = {
|
|
|
44
46
|
};
|
|
45
47
|
const MAX_RETRY_ATTEMPTS = 10;
|
|
46
48
|
const MAX_RETRY_DELAY_MS = 30_000;
|
|
49
|
+
const RUN_REQUEST_DECODE_FAILED = "CURSOR_RUN_REQUEST_DECODE_FAILED";
|
|
50
|
+
const RUN_REQUEST_UNSUPPORTED = "CURSOR_RUN_REQUEST_UNSUPPORTED";
|
|
51
|
+
const RUN_REPLY_FAILED = "CURSOR_RUN_REPLY_FAILED";
|
|
52
|
+
const RESPONSE_REQUIRED_CHANNEL_BY_FIELD = new Map([
|
|
53
|
+
[2, "exec"],
|
|
54
|
+
[4, "kv"],
|
|
55
|
+
[7, "interaction"],
|
|
56
|
+
]);
|
|
57
|
+
function responseRequiredChannel(payload) {
|
|
58
|
+
const fields = readAllFieldsStrict(payload);
|
|
59
|
+
if (fields) {
|
|
60
|
+
const channels = fields
|
|
61
|
+
.map((field) => RESPONSE_REQUIRED_CHANNEL_BY_FIELD.get(field.fn))
|
|
62
|
+
.filter((channel) => !!channel);
|
|
63
|
+
if (channels.length > 1)
|
|
64
|
+
return "multiple";
|
|
65
|
+
return channels[0];
|
|
66
|
+
}
|
|
67
|
+
// Request tags are single-byte because all must-reply top-level fields are <16.
|
|
68
|
+
const tag = payload[0];
|
|
69
|
+
return tag !== undefined ? RESPONSE_REQUIRED_CHANNEL_BY_FIELD.get(tag >> 3) : undefined;
|
|
70
|
+
}
|
|
47
71
|
function retryInteger(name, value, fallback) {
|
|
48
72
|
const resolved = value === undefined ? fallback : value;
|
|
49
73
|
if (typeof resolved !== "number" || !Number.isSafeInteger(resolved) || resolved <= 0) {
|
|
@@ -430,7 +454,16 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
430
454
|
isCompaction,
|
|
431
455
|
});
|
|
432
456
|
const tools = toolState.advertisedTools;
|
|
457
|
+
const webToolAliases = buildCustomWebToolAliases(tools);
|
|
458
|
+
const cursorTools = webToolAliases.advertisedTools;
|
|
459
|
+
for (const [alias, candidates] of webToolAliases.ambiguous) {
|
|
460
|
+
trace(`web tool alias skipped: alias=${alias} ambiguous=[${candidates.join(",")}]`);
|
|
461
|
+
}
|
|
462
|
+
for (const [alias, original] of webToolAliases.aliases) {
|
|
463
|
+
trace(`web tool alias: ${alias} -> ${original}`);
|
|
464
|
+
}
|
|
433
465
|
const allowTools = toolState.allowTools;
|
|
466
|
+
const discoveredSubagentCatalog = extractHostSubagentCatalog(cursorTools);
|
|
434
467
|
const resetState = resolveTurnConversationReset({ sessionKey, isCompaction });
|
|
435
468
|
const recovery = startOptions?.recovery;
|
|
436
469
|
const resuming = recovery?.kind === "resume";
|
|
@@ -451,7 +484,7 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
451
484
|
: (extractUserText([...prompt].reverse().find((m) => m.role === "user")) || ".");
|
|
452
485
|
const workspaceRoot = path.resolve(options.workspaceRoot || process.cwd());
|
|
453
486
|
const baseSystemPrompt = extractSystemPrompt(prompt);
|
|
454
|
-
const interactionGuidance = buildOpenCodeInteractionGuidance(
|
|
487
|
+
const interactionGuidance = buildOpenCodeInteractionGuidance(cursorTools, isCompaction, workspaceRoot);
|
|
455
488
|
const systemPrompt = interactionGuidance
|
|
456
489
|
? [baseSystemPrompt, interactionGuidance].filter(Boolean).join("\n\n")
|
|
457
490
|
: baseSystemPrompt;
|
|
@@ -493,7 +526,24 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
493
526
|
baseURL: agentBaseUrl,
|
|
494
527
|
headers: options.headers,
|
|
495
528
|
});
|
|
496
|
-
const requestContext = await buildRequestContext({ workspaceRoot, tools });
|
|
529
|
+
const requestContext = await buildRequestContext({ workspaceRoot, tools: cursorTools });
|
|
530
|
+
const contextSubagents = Array.isArray(requestContext.custom_subagents)
|
|
531
|
+
? requestContext.custom_subagents
|
|
532
|
+
.map((agent) => agent && typeof agent === "object" && typeof agent.name === "string"
|
|
533
|
+
? {
|
|
534
|
+
name: agent.name,
|
|
535
|
+
description: typeof agent.description === "string"
|
|
536
|
+
? agent.description
|
|
537
|
+
: undefined,
|
|
538
|
+
}
|
|
539
|
+
: undefined)
|
|
540
|
+
.filter((agent) => !!agent)
|
|
541
|
+
: [];
|
|
542
|
+
const subagentCatalog = {
|
|
543
|
+
...discoveredSubagentCatalog,
|
|
544
|
+
agents: [...new Map([...discoveredSubagentCatalog.agents, ...contextSubagents]
|
|
545
|
+
.map((agent) => [agent.name, agent])).values()],
|
|
546
|
+
};
|
|
497
547
|
// Resolve descriptors once from the merged OpenCode config so MCP identity is
|
|
498
548
|
// consistent across AgentRunRequest and both request_context reply paths.
|
|
499
549
|
const toolDescriptors = Array.isArray(requestContext.tools)
|
|
@@ -513,7 +563,7 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
513
563
|
conversationState,
|
|
514
564
|
parameterValues,
|
|
515
565
|
maxMode,
|
|
516
|
-
tools,
|
|
566
|
+
tools: cursorTools,
|
|
517
567
|
toolDescriptors,
|
|
518
568
|
requestContext,
|
|
519
569
|
action: resuming ? "resume" : "user",
|
|
@@ -570,6 +620,8 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
570
620
|
nextBridgedExecId: 900_000,
|
|
571
621
|
blobs: new Map(),
|
|
572
622
|
toolDescriptors,
|
|
623
|
+
toolAliases: webToolAliases.aliases,
|
|
624
|
+
subagentCatalog,
|
|
573
625
|
requestContext,
|
|
574
626
|
allowTools,
|
|
575
627
|
usageEstimate,
|
|
@@ -814,11 +866,17 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
814
866
|
const { textId, reasoningId } = ids;
|
|
815
867
|
const promptTokens = ids.promptTokens ?? 0;
|
|
816
868
|
const advertisedToolNames = advertisedToolNamesFromDescriptors(session.toolDescriptors);
|
|
817
|
-
const advertisedToolNameSet = new Set(advertisedToolNames);
|
|
869
|
+
const advertisedToolNameSet = new Set(advertisedToolNames.map((name) => resolveCustomWebToolAlias(name, session.toolAliases)));
|
|
818
870
|
let textStarted = false;
|
|
819
871
|
let reasoningStarted = false;
|
|
820
872
|
const requestUsage = ids.requestUsage ?? { outputChars: 0 };
|
|
821
|
-
|
|
873
|
+
const replaySafety = new AttemptReplaySafety(session.sessionId);
|
|
874
|
+
const failRunProtocol = (message, code) => {
|
|
875
|
+
replaySafety.markBarrier("unknown-or-malformed-frame");
|
|
876
|
+
const error = new CursorProtocolError(message, { code });
|
|
877
|
+
sessionManager.close(session, "remote-error", error);
|
|
878
|
+
throw error;
|
|
879
|
+
};
|
|
822
880
|
// OpenCode cancels the ReadableStream between turns (see the cancel handler
|
|
823
881
|
// in doStreamImpl). The frames iterator can still yield a final `done` after
|
|
824
882
|
// the cancel lands — controller.enqueue on a cancelled controller throws.
|
|
@@ -922,6 +980,49 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
922
980
|
return true;
|
|
923
981
|
}
|
|
924
982
|
};
|
|
983
|
+
/**
|
|
984
|
+
* A read of a path that does not exist would otherwise reach OpenCode as a
|
|
985
|
+
* real tool call and raise a permission prompt for a file the user never had.
|
|
986
|
+
* Cursor's own LocalReadExecutor instead resolves the target against the
|
|
987
|
+
* workspace, stats it, and returns a typed ReadResult (file_not_found /
|
|
988
|
+
* invalid_file) before execution. Mirror that here: reply on the held-open
|
|
989
|
+
* exec channel with the exact typed case so the model receives a structured
|
|
990
|
+
* observation and the host surfaces no prompt.
|
|
991
|
+
*
|
|
992
|
+
* Read-only by construction — write/edit targets are never checked, so
|
|
993
|
+
* legitimate file creation (path resolves, file absent) is never denied. Runs
|
|
994
|
+
* after recoverMissingEditRead, which has already converted the new-file edit
|
|
995
|
+
* handshake read into an empty-file success. EACCES/EPERM and other stat
|
|
996
|
+
* errors fall through to OpenCode so a genuine permission decision stands.
|
|
997
|
+
*/
|
|
998
|
+
const rejectMissingReadTarget = (parsed) => {
|
|
999
|
+
if (parsed.toolName !== "read")
|
|
1000
|
+
return false;
|
|
1001
|
+
const requested = typeof parsed.args.filePath === "string" ? parsed.args.filePath : "";
|
|
1002
|
+
if (!requested)
|
|
1003
|
+
return false;
|
|
1004
|
+
const workspaceRoot = workspaceRootFromRequestContext(session.requestContext);
|
|
1005
|
+
const absolutePath = resolveReadTargetPath(requested, workspaceRoot);
|
|
1006
|
+
const readResult = classifyMissingReadTarget(absolutePath);
|
|
1007
|
+
if (!readResult)
|
|
1008
|
+
return false;
|
|
1009
|
+
try {
|
|
1010
|
+
for (const frame of buildReadRejectionMessages(parsed.id, readResult)) {
|
|
1011
|
+
session.stream.write(frame);
|
|
1012
|
+
}
|
|
1013
|
+
const kind = Object.keys(readResult)[0];
|
|
1014
|
+
trace(`exec: rejected missing read target id=${parsed.id} kind=${kind} ` +
|
|
1015
|
+
`path=${JSON.stringify(absolutePath)}`);
|
|
1016
|
+
return true;
|
|
1017
|
+
}
|
|
1018
|
+
catch (e) {
|
|
1019
|
+
const error = new Error(`Failed to reject Cursor read of a missing path: ${e.message}`);
|
|
1020
|
+
trace(`exec: missing read rejection FAILED ${error.message}`);
|
|
1021
|
+
safeError(error);
|
|
1022
|
+
sessionManager.close(session);
|
|
1023
|
+
return true;
|
|
1024
|
+
}
|
|
1025
|
+
};
|
|
925
1026
|
/** AI SDK V3 requires text-end / reasoning-end before finish or tool-call. */
|
|
926
1027
|
const closeOpenSpans = () => {
|
|
927
1028
|
for (const part of spanEndParts({ textStarted, reasoningStarted, textId, reasoningId })) {
|
|
@@ -933,7 +1034,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
933
1034
|
const emitText = (text) => {
|
|
934
1035
|
if (!text)
|
|
935
1036
|
return;
|
|
936
|
-
|
|
1037
|
+
replaySafety.markBarrier("visible-text");
|
|
937
1038
|
// Close reasoning before text (hosts expect reasoning-end before text-start).
|
|
938
1039
|
if (reasoningStarted && !textStarted) {
|
|
939
1040
|
safeEnqueue({ type: "reasoning-end", id: reasoningId });
|
|
@@ -951,7 +1052,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
951
1052
|
const emitReasoning = (text) => {
|
|
952
1053
|
if (!text)
|
|
953
1054
|
return;
|
|
954
|
-
|
|
1055
|
+
replaySafety.markBarrier("visible-reasoning");
|
|
955
1056
|
if (!reasoningStarted) {
|
|
956
1057
|
safeEnqueue({ type: "reasoning-start", id: reasoningId });
|
|
957
1058
|
reasoningStarted = true;
|
|
@@ -1029,15 +1130,13 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1029
1130
|
const failure = error instanceof CursorProviderError
|
|
1030
1131
|
? error
|
|
1031
1132
|
: new CursorRunInterruptedError(`Cursor Run frame stream interrupted: ${error.message}`, { cause: error });
|
|
1032
|
-
|
|
1033
|
-
throw failure;
|
|
1133
|
+
throw replaySafety.applyTo(failure);
|
|
1034
1134
|
}
|
|
1035
1135
|
if (next.done) {
|
|
1036
1136
|
closeOpenSpans();
|
|
1037
1137
|
trace("pump: frames iterator ended before turn_ended");
|
|
1038
1138
|
const failure = new CursorRunInterruptedError();
|
|
1039
|
-
failure
|
|
1040
|
-
throw failure;
|
|
1139
|
+
throw replaySafety.applyTo(failure);
|
|
1041
1140
|
}
|
|
1042
1141
|
const frame = next.value;
|
|
1043
1142
|
if (frame.flags & 0x02) {
|
|
@@ -1049,14 +1148,15 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1049
1148
|
try {
|
|
1050
1149
|
payload = new TextDecoder().decode(decodeFramePayload(frame));
|
|
1051
1150
|
}
|
|
1052
|
-
catch {
|
|
1151
|
+
catch {
|
|
1152
|
+
replaySafety.markBarrier("unknown-or-malformed-frame");
|
|
1153
|
+
}
|
|
1053
1154
|
}
|
|
1054
1155
|
closeOpenSpans();
|
|
1055
1156
|
const failure = payload
|
|
1056
1157
|
? connectFrameError(payload)
|
|
1057
1158
|
: new CursorRunInterruptedError();
|
|
1058
|
-
|
|
1059
|
-
throw failure;
|
|
1159
|
+
throw replaySafety.applyTo(failure);
|
|
1060
1160
|
}
|
|
1061
1161
|
// decodeFramePayload can throw on a corrupt gzip payload (gunzipSync).
|
|
1062
1162
|
// Skip the frame rather than abort the whole turn.
|
|
@@ -1065,6 +1165,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1065
1165
|
payload = decodeFramePayload(frame);
|
|
1066
1166
|
}
|
|
1067
1167
|
catch (e) {
|
|
1168
|
+
replaySafety.markBarrier("unknown-or-malformed-frame");
|
|
1068
1169
|
trace(`gunzip FAILED (skipping frame): flags=0x${frame.flags.toString(16)} len=${frame.payload.length} err=${e.message}`);
|
|
1069
1170
|
continue;
|
|
1070
1171
|
}
|
|
@@ -1072,39 +1173,46 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1072
1173
|
try {
|
|
1073
1174
|
asm = decodeMessage("AgentServerMessage", payload);
|
|
1074
1175
|
}
|
|
1075
|
-
catch
|
|
1176
|
+
catch {
|
|
1076
1177
|
// A single malformed/truncated frame must not abort the whole turn
|
|
1077
1178
|
// (protobufjs throws "index out of range: …" on length overruns). Log it
|
|
1078
1179
|
// and keep pumping.
|
|
1079
|
-
|
|
1080
|
-
const
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1180
|
+
replaySafety.markBarrier("unknown-or-malformed-frame");
|
|
1181
|
+
const channel = responseRequiredChannel(payload);
|
|
1182
|
+
if (channel) {
|
|
1183
|
+
failRunProtocol(`Cursor ${channel} request could not be decoded`, RUN_REQUEST_DECODE_FAILED);
|
|
1184
|
+
}
|
|
1185
|
+
trace(`decode FAILED (skipping non-request frame): flags=0x${frame.flags.toString(16)} len=${payload.length}`);
|
|
1085
1186
|
continue;
|
|
1086
1187
|
}
|
|
1087
1188
|
const iu = asm.interaction_update;
|
|
1088
1189
|
const esm = asm.exec_server_message;
|
|
1089
1190
|
const kv = asm.kv_server_message;
|
|
1191
|
+
const execControl = asm.exec_server_control_message;
|
|
1090
1192
|
const interactionQuery = asm.interaction_query;
|
|
1091
1193
|
const checkpointRaw = asm.conversation_checkpoint_update;
|
|
1092
1194
|
const topField = payload.length > 0 ? payload[0] >> 3 : 0;
|
|
1093
|
-
const textProgress = iu?.text_delta?.text;
|
|
1094
|
-
const thinkingProgress = iu?.thinking_delta?.text;
|
|
1095
1195
|
const checkpointProgress = normalizeCheckpointBytes(checkpointRaw);
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1196
|
+
const requiredChannel = responseRequiredChannel(payload);
|
|
1197
|
+
if (requiredChannel === "multiple" ||
|
|
1198
|
+
(requiredChannel === "exec" && !esm) ||
|
|
1199
|
+
(requiredChannel === "kv" && !kv) ||
|
|
1200
|
+
(requiredChannel === "interaction" && !interactionQuery)) {
|
|
1201
|
+
failRunProtocol("Cursor response-requiring request could not be decoded", RUN_REQUEST_DECODE_FAILED);
|
|
1202
|
+
}
|
|
1203
|
+
const replayFrame = analyzeReplayFrame(payload, {
|
|
1204
|
+
interactionUpdate: iu,
|
|
1205
|
+
exec: esm,
|
|
1206
|
+
kv,
|
|
1207
|
+
execControl,
|
|
1208
|
+
interactionQuery,
|
|
1209
|
+
checkpointBytes: checkpointProgress,
|
|
1210
|
+
});
|
|
1211
|
+
if (replayFrame.semanticProgress) {
|
|
1106
1212
|
sessionManager.recordSemanticProgress(session);
|
|
1107
1213
|
}
|
|
1214
|
+
if (replayFrame.barrier)
|
|
1215
|
+
replaySafety.markBarrier(replayFrame.barrier);
|
|
1108
1216
|
{
|
|
1109
1217
|
const iuKind = iu ? Object.keys(iu).find((k) => iu[k]) : undefined;
|
|
1110
1218
|
trace(`pump frame: topField=${topField} interaction_update=${iuKind ?? "-"} ` +
|
|
@@ -1229,12 +1337,8 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1229
1337
|
session.stream.write(buildRequestContextResult(esmId, session.requestContext));
|
|
1230
1338
|
trace(`exec request_context: replied`);
|
|
1231
1339
|
}
|
|
1232
|
-
catch
|
|
1233
|
-
|
|
1234
|
-
trace(`exec request_context: write FAILED ${error.message}`);
|
|
1235
|
-
safeError(error);
|
|
1236
|
-
sessionManager.close(session);
|
|
1237
|
-
return;
|
|
1340
|
+
catch {
|
|
1341
|
+
failRunProtocol("Cursor request-context reply failed", RUN_REPLY_FAILED);
|
|
1238
1342
|
}
|
|
1239
1343
|
}
|
|
1240
1344
|
else if (esm.mcp_state_exec_args) {
|
|
@@ -1249,16 +1353,21 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1249
1353
|
session.stream.write(buildMcpStateResult(esmId, stateArgs, session.requestContext));
|
|
1250
1354
|
trace(`exec mcp_state: replied id=${esmId} requested=[${requested}]`);
|
|
1251
1355
|
}
|
|
1252
|
-
catch
|
|
1253
|
-
|
|
1254
|
-
trace(`exec mcp_state: write FAILED ${error.message}`);
|
|
1255
|
-
safeError(error);
|
|
1256
|
-
sessionManager.close(session);
|
|
1257
|
-
return;
|
|
1356
|
+
catch {
|
|
1357
|
+
failRunProtocol("Cursor MCP-state reply failed", RUN_REPLY_FAILED);
|
|
1258
1358
|
}
|
|
1259
1359
|
}
|
|
1260
1360
|
else {
|
|
1361
|
+
replaySafety.markBarrier("non-control-exec");
|
|
1261
1362
|
const parsed = parseExecServerMessage(esm);
|
|
1363
|
+
if (parsed) {
|
|
1364
|
+
const executableToolName = resolveCustomWebToolAlias(parsed.toolName, session.toolAliases);
|
|
1365
|
+
if (executableToolName !== parsed.toolName) {
|
|
1366
|
+
trace(`web tool alias resolved: ${parsed.toolName} -> ${executableToolName}`);
|
|
1367
|
+
parsed.toolName = executableToolName;
|
|
1368
|
+
}
|
|
1369
|
+
remapNativeSubagentForCatalog(parsed, advertisedToolNameSet, session.subagentCatalog);
|
|
1370
|
+
}
|
|
1262
1371
|
const displayCallId = extractExecDisplayCallId(esm);
|
|
1263
1372
|
trace(`exec: id=${parsed?.id} variant=${parsed ? Object.keys(parsed).join(",") : "none"} toolName=${parsed?.toolName} resultField=${parsed?.resultField}`);
|
|
1264
1373
|
if (parsed) {
|
|
@@ -1298,6 +1407,11 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1298
1407
|
}
|
|
1299
1408
|
if (recoverMissingEditRead(parsed, displayCallId))
|
|
1300
1409
|
continue;
|
|
1410
|
+
if (rejectMissingReadTarget(parsed)) {
|
|
1411
|
+
if (displayCallId)
|
|
1412
|
+
session.displayToolCalls.delete(displayCallId);
|
|
1413
|
+
continue;
|
|
1414
|
+
}
|
|
1301
1415
|
if (displayCallId) {
|
|
1302
1416
|
session.displayToolCalls.delete(displayCallId);
|
|
1303
1417
|
trace(`exec: claimed display callId=${displayCallId}`);
|
|
@@ -1332,10 +1446,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1332
1446
|
.map((x) => x.toString(16).padStart(2, "0"))
|
|
1333
1447
|
.join("");
|
|
1334
1448
|
trace(`exec UNMAPPED: id=${esmId} variant=${variantDescription} keys=[${Object.keys(esm).join(",")}] hex=${hex}`);
|
|
1335
|
-
|
|
1336
|
-
safeError(err);
|
|
1337
|
-
sessionManager.close(session);
|
|
1338
|
-
return;
|
|
1449
|
+
failRunProtocol(`Unsupported Cursor exec variant ${variantDescription} (id=${esmId})`, RUN_REQUEST_UNSUPPORTED);
|
|
1339
1450
|
}
|
|
1340
1451
|
}
|
|
1341
1452
|
else if (interactionQuery) {
|
|
@@ -1343,20 +1454,24 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1343
1454
|
// SDK has no Cursor-specific UI callback, so answer immediately with the
|
|
1344
1455
|
// conservative headless policy from protocol/interactions.ts (including
|
|
1345
1456
|
// F14 create_plan auto-ack / empty plan_uri for CLI headless parity).
|
|
1457
|
+
const handled = (() => {
|
|
1458
|
+
try {
|
|
1459
|
+
return handleInteractionQuery(interactionQuery, payload);
|
|
1460
|
+
}
|
|
1461
|
+
catch {
|
|
1462
|
+
return failRunProtocol("Cursor interaction request could not be handled", RUN_REQUEST_UNSUPPORTED);
|
|
1463
|
+
}
|
|
1464
|
+
})();
|
|
1346
1465
|
try {
|
|
1347
|
-
|
|
1466
|
+
if (handled.outcome === "acknowledged") {
|
|
1467
|
+
replaySafety.markBarrier("stateful-interaction");
|
|
1468
|
+
}
|
|
1348
1469
|
session.stream.write(handled.reply);
|
|
1349
1470
|
trace(`interaction_query: replied id=${handled.id} variant=${handled.variantName} ` +
|
|
1350
1471
|
`field=${handled.variantField} outcome=${handled.outcome}`);
|
|
1351
1472
|
}
|
|
1352
|
-
catch
|
|
1353
|
-
|
|
1354
|
-
const err = e instanceof Error ? e : new Error(String(e));
|
|
1355
|
-
trace(`interaction_query: FAILED id=${info.id ?? "?"} ` +
|
|
1356
|
-
`variantField=${info.variantField ?? "?"} err=${err.message}`);
|
|
1357
|
-
safeError(err);
|
|
1358
|
-
sessionManager.close(session);
|
|
1359
|
-
return;
|
|
1473
|
+
catch {
|
|
1474
|
+
failRunProtocol("Cursor interaction reply failed", RUN_REPLY_FAILED);
|
|
1360
1475
|
}
|
|
1361
1476
|
}
|
|
1362
1477
|
else if (kv) {
|
|
@@ -1375,20 +1490,24 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1375
1490
|
`found=${handled.found} echoed=${!!handled.echoed} ` +
|
|
1376
1491
|
`sessionBlobs=${session.blobs.size} convBlobs=${conversationBlobCount(session.conversationId)}`);
|
|
1377
1492
|
}
|
|
1378
|
-
catch
|
|
1379
|
-
|
|
1380
|
-
trace(`kv: write FAILED ${error.message}`);
|
|
1381
|
-
safeError(error);
|
|
1382
|
-
sessionManager.close(session);
|
|
1383
|
-
return;
|
|
1493
|
+
catch {
|
|
1494
|
+
failRunProtocol("Cursor KV reply failed", RUN_REPLY_FAILED);
|
|
1384
1495
|
}
|
|
1385
1496
|
}
|
|
1497
|
+
else {
|
|
1498
|
+
failRunProtocol("Cursor KV request could not be handled", RUN_REQUEST_UNSUPPORTED);
|
|
1499
|
+
}
|
|
1386
1500
|
}
|
|
1387
1501
|
}
|
|
1388
1502
|
catch (e) {
|
|
1503
|
+
if (e instanceof CursorProtocolError)
|
|
1504
|
+
throw e;
|
|
1505
|
+
if (requiredChannel) {
|
|
1506
|
+
failRunProtocol(`Cursor ${requiredChannel} request could not be handled`, RUN_REQUEST_UNSUPPORTED);
|
|
1507
|
+
}
|
|
1389
1508
|
// Any per-frame dispatch throw (e.g. protobufjs length overrun in
|
|
1390
1509
|
// exec/args decode) must not abort the whole turn — log and skip.
|
|
1391
|
-
trace(`frame dispatch FAILED (skipping): topField=${topField}
|
|
1510
|
+
trace(`frame dispatch FAILED (skipping): topField=${topField}`);
|
|
1392
1511
|
}
|
|
1393
1512
|
// heartbeat / step / partial_tool_call → ignore (partial args are
|
|
1394
1513
|
// display-only; the exec channel is authoritative. Checkpoints and
|
|
@@ -1499,6 +1618,7 @@ export function buildOpenCodeInteractionGuidance(tools, isCompaction, workspaceR
|
|
|
1499
1618
|
if (names.size === 0)
|
|
1500
1619
|
return undefined;
|
|
1501
1620
|
const instructions = [];
|
|
1621
|
+
const subagents = extractHostSubagentCatalog(tools);
|
|
1502
1622
|
if (names.has("question")) {
|
|
1503
1623
|
instructions.push("- When user input is required, call the OpenCode `question` tool; do not use Cursor's native AskQuestion interaction.");
|
|
1504
1624
|
}
|
|
@@ -1511,8 +1631,22 @@ export function buildOpenCodeInteractionGuidance(tools, isCompaction, workspaceR
|
|
|
1511
1631
|
if (names.has("plan_exit")) {
|
|
1512
1632
|
instructions.push("- To leave plan mode, call the OpenCode `plan_exit` tool.");
|
|
1513
1633
|
}
|
|
1514
|
-
if (names.has(
|
|
1515
|
-
instructions.push(
|
|
1634
|
+
if (names.has(CUSTOM_WEBSEARCH_TOOL)) {
|
|
1635
|
+
instructions.push(`- For web searches, call \`${CUSTOM_WEBSEARCH_TOOL}\`; do not use Cursor's native WebSearch interaction.`);
|
|
1636
|
+
}
|
|
1637
|
+
if (names.has(CUSTOM_WEBFETCH_TOOL)) {
|
|
1638
|
+
instructions.push(`- To fetch a known URL, call \`${CUSTOM_WEBFETCH_TOOL}\`; do not use Cursor's native WebFetch interaction.`);
|
|
1639
|
+
}
|
|
1640
|
+
if (names.has("task") || names.has("actor")) {
|
|
1641
|
+
const target = names.has("actor") ? "`actor`" : "`task`";
|
|
1642
|
+
const available = subagents.agents.map((agent) => `\`${agent.name}\``).join(", ");
|
|
1643
|
+
instructions.push(`- Native Cursor Task/subagent requests are executed through OpenCode ${target}. ` +
|
|
1644
|
+
"Advertised custom subagent names are used exactly; otherwise `unspecified` and `generalPurpose` select host `general`, " +
|
|
1645
|
+
"`bugbot`, `security-review`, and `explore` select host `explore` (then `general`), and other specialized Cursor types fall back to `general`." +
|
|
1646
|
+
(available ? ` Spawnable host agents this turn: ${available}.` : ""));
|
|
1647
|
+
if (subagents.agents.some((agent) => agent.name === "scout")) {
|
|
1648
|
+
instructions.push("- Host `scout` is available for external documentation and dependency-source research. Use Cursor `cursor-guide` for that use case; local repository discovery still uses `bugbot`/`explore`.");
|
|
1649
|
+
}
|
|
1516
1650
|
}
|
|
1517
1651
|
if (names.has("write")) {
|
|
1518
1652
|
instructions.push(names.has("edit")
|
|
@@ -1522,7 +1656,9 @@ export function buildOpenCodeInteractionGuidance(tools, isCompaction, workspaceR
|
|
|
1522
1656
|
return [
|
|
1523
1657
|
`OpenCode exposes exactly these executable tools for this turn: ${[...names].map((name) => `\`${name}\``).join(", ")}.`,
|
|
1524
1658
|
`Workspace root: ${JSON.stringify(workspaceRoot)}. Resolve workspace paths against exactly this root; never invent an absolute prefix, and verify uncertain paths with an available tool before using them.`,
|
|
1525
|
-
|
|
1659
|
+
subagents.executor
|
|
1660
|
+
? "Call only tools in that exact list. Cursor-native Task/subagent requests are permitted because a compatible host executor is listed; other unlisted Cursor-native tools remain unavailable."
|
|
1661
|
+
: "Call only tools in that exact list. Cursor-native tools that are not listed—including Task/subagents—are unavailable; do not invoke them. If a capability is absent, complete the work directly with the listed tools or explain the limitation.",
|
|
1526
1662
|
...(instructions.length > 0
|
|
1527
1663
|
? ["Use these OpenCode tools instead of equivalent Cursor-native UI interactions:"]
|
|
1528
1664
|
: []),
|
package/dist/models.d.ts
CHANGED
|
@@ -73,9 +73,11 @@ export declare function resolveVariantParameters(model: ModelInfo | undefined, o
|
|
|
73
73
|
export declare function fetchModels(token: string, options?: {
|
|
74
74
|
baseURL?: string;
|
|
75
75
|
headers?: Record<string, string>;
|
|
76
|
+
timeoutMs?: number;
|
|
76
77
|
}): Promise<ModelInfo[]>;
|
|
77
78
|
export declare function refreshModelCache(cacheDir: string, fetcher: () => Promise<ModelInfo[]>): Promise<ModelInfo[]>;
|
|
78
79
|
export declare function discoverModels(token: string, cacheDir: string, options?: {
|
|
79
80
|
baseURL?: string;
|
|
80
81
|
headers?: Record<string, string>;
|
|
82
|
+
timeoutMs?: number;
|
|
81
83
|
}): Promise<ModelInfo[]>;
|
package/dist/plugin.js
CHANGED
|
@@ -6,6 +6,7 @@ import { readStoredAuth } from "./context/auth-store.js";
|
|
|
6
6
|
import { resolveAgentUrl } from "./agent-url.js";
|
|
7
7
|
import { captureCursorShellResult, cursorShellEnvForCall, cursorShellOriginalCommand, prepareCursorShellArgs, releaseCursorShellEnv, sanitizeRegisteredCursorShellOutput, setCursorShellPath, } from "./shell-timeout.js";
|
|
8
8
|
import { sessionActivity } from "./activity.js";
|
|
9
|
+
import { openCodeWebSearchTool } from "./web-tools.js";
|
|
9
10
|
const MODULE_URL = new URL("./index.js", import.meta.url).href;
|
|
10
11
|
/**
|
|
11
12
|
* Strip characters and markup that break rendering in the OpenCode TUI/GUI from
|
|
@@ -194,7 +195,7 @@ function cursorGetServerConfigTelemetryEnabled() {
|
|
|
194
195
|
process.env.CURSOR_GET_SERVER_CONFIG_TELEMETRY === "true");
|
|
195
196
|
}
|
|
196
197
|
export async function CursorPlugin(input) {
|
|
197
|
-
// Prefer OCP
|
|
198
|
+
// Prefer strong OCP host identity when available; otherwise resolve from explicit host signals/install path.
|
|
198
199
|
await adoptCompatHostCacheDir();
|
|
199
200
|
const cacheDir = opencodeGlobalCacheDir();
|
|
200
201
|
const apiBaseURL = cursorApiBaseURL();
|
|
@@ -315,6 +316,12 @@ export async function CursorPlugin(input) {
|
|
|
315
316
|
return cached?.models.length ? modelsToConfig(cached.models) : {};
|
|
316
317
|
}
|
|
317
318
|
return {
|
|
319
|
+
tool: {
|
|
320
|
+
// `websearch` is a reserved OpenCode id and is filtered for third-party
|
|
321
|
+
// providers after plugin tools are merged. Use the collision-safe id
|
|
322
|
+
// Cursor already sees so this host-side fallback survives that filter.
|
|
323
|
+
custom_websearch: openCodeWebSearchTool,
|
|
324
|
+
},
|
|
318
325
|
async event({ event }) {
|
|
319
326
|
switch (event.type) {
|
|
320
327
|
case "session.created":
|
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { FALLBACK_CLIENT_VERSION, MODEL_CACHE_TTL_MS, VERSION_CACHE_FILE, } from "../shared.js";
|
|
4
4
|
import { opencodeGlobalCacheDir } from "../context/paths.js";
|
|
5
|
+
import { withAbortDeadline } from "../deadline.js";
|
|
5
6
|
const INSTALL_URL = "https://cursor.com/install";
|
|
6
7
|
const REMOTE_TIMEOUT_MS = 5_000;
|
|
7
8
|
const BUILD_RE = /^\d{4}\.\d{2}\.\d{2}-[0-9A-Za-z][0-9A-Za-z.-]*$/;
|
|
@@ -103,13 +104,13 @@ function isCacheFresh(cache, now = Date.now()) {
|
|
|
103
104
|
return age >= 0 && age < MODEL_CACHE_TTL_MS;
|
|
104
105
|
}
|
|
105
106
|
async function fetchInstallerVersion() {
|
|
106
|
-
|
|
107
|
-
signal
|
|
107
|
+
return withAbortDeadline(REMOTE_TIMEOUT_MS, () => new Error("Cursor installer version request timed out"), async (signal) => {
|
|
108
|
+
const response = await fetch(INSTALL_URL, { signal });
|
|
109
|
+
if (!response.ok)
|
|
110
|
+
return undefined;
|
|
111
|
+
const build = extractVersionFromInstaller(await response.text());
|
|
112
|
+
return build ? `cli-${build}` : undefined;
|
|
108
113
|
});
|
|
109
|
-
if (!response.ok)
|
|
110
|
-
return undefined;
|
|
111
|
-
const build = extractVersionFromInstaller(await response.text());
|
|
112
|
-
return build ? `cli-${build}` : undefined;
|
|
113
114
|
}
|
|
114
115
|
async function refreshVersionCache() {
|
|
115
116
|
const version = await fetchInstallerVersion();
|
|
@@ -366,10 +366,29 @@ export function createMessageTypes() {
|
|
|
366
366
|
{ id: 1, name: "path", type: "string" },
|
|
367
367
|
{ id: 2, name: "error", type: "string" },
|
|
368
368
|
]);
|
|
369
|
+
// Cursor's LocalReadExecutor validates the target before execution and, for a
|
|
370
|
+
// missing / non-regular path, returns one of these typed ReadResult cases
|
|
371
|
+
// instead of running the tool. Mirroring the exact field ids/shapes lets this
|
|
372
|
+
// provider reply the same way so the model sees a structured file-not-found
|
|
373
|
+
// observation and the host never surfaces a permission prompt.
|
|
374
|
+
addType(root, "ReadFileNotFound", [{ id: 1, name: "path", type: "string" }]);
|
|
375
|
+
addType(root, "ReadPermissionDenied", [{ id: 1, name: "path", type: "string" }]);
|
|
376
|
+
addType(root, "ReadInvalidFile", [
|
|
377
|
+
{ id: 1, name: "path", type: "string" },
|
|
378
|
+
{ id: 2, name: "reason", type: "string" },
|
|
379
|
+
]);
|
|
369
380
|
addType(root, "ReadResult", [
|
|
370
381
|
{ id: 1, name: "success", type: "ReadSuccess" },
|
|
371
382
|
{ id: 2, name: "error", type: "ReadError" },
|
|
372
|
-
|
|
383
|
+
{ id: 4, name: "file_not_found", type: "ReadFileNotFound" },
|
|
384
|
+
{ id: 5, name: "permission_denied", type: "ReadPermissionDenied" },
|
|
385
|
+
{ id: 6, name: "invalid_file", type: "ReadInvalidFile" },
|
|
386
|
+
], [
|
|
387
|
+
{
|
|
388
|
+
name: "result",
|
|
389
|
+
fields: ["success", "error", "file_not_found", "permission_denied", "invalid_file"],
|
|
390
|
+
},
|
|
391
|
+
]);
|
|
373
392
|
addType(root, "GrepArgs", [
|
|
374
393
|
{ id: 1, name: "pattern", type: "string" },
|
|
375
394
|
{ id: 2, name: "path", type: "string" },
|
|
@@ -724,8 +743,10 @@ export function createMessageTypes() {
|
|
|
724
743
|
{ id: 7, name: "tools", type: "McpToolDefinition", repeated: true },
|
|
725
744
|
{ id: 11, name: "git_repos", type: "GitRepoInfo", repeated: true },
|
|
726
745
|
{ id: 13, name: "project_layouts", type: "LsDirectoryTreeNode", repeated: true },
|
|
746
|
+
{ id: 17, name: "web_search_enabled", type: "bool" },
|
|
727
747
|
{ id: 22, name: "custom_subagents", type: "CustomSubagent", repeated: true },
|
|
728
748
|
{ id: 23, name: "mcp_file_system_options", type: "McpFileSystemOptions" },
|
|
749
|
+
{ id: 24, name: "web_fetch_enabled", type: "bool" },
|
|
729
750
|
{ id: 25, name: "hooks_additional_context", type: "string" },
|
|
730
751
|
{ id: 29, name: "agent_skills", type: "AgentSkill", repeated: true },
|
|
731
752
|
{ id: 33, name: "git_repo_info_complete", type: "bool" },
|
|
@@ -883,8 +904,10 @@ export function createMessageTypes() {
|
|
|
883
904
|
{ id: 7, name: "tools", type: "McpToolDefinition", repeated: true },
|
|
884
905
|
{ id: 11, name: "git_repos", type: "GitRepoInfo", repeated: true },
|
|
885
906
|
{ id: 13, name: "project_layouts", type: "LsDirectoryTreeNode", repeated: true },
|
|
907
|
+
{ id: 17, name: "web_search_enabled", type: "bool" },
|
|
886
908
|
{ id: 22, name: "custom_subagents", type: "CustomSubagent", repeated: true },
|
|
887
909
|
{ id: 23, name: "mcp_file_system_options", type: "McpFileSystemOptions" },
|
|
910
|
+
{ id: 24, name: "web_fetch_enabled", type: "bool" },
|
|
888
911
|
{ id: 25, name: "hooks_additional_context", type: "string" },
|
|
889
912
|
{ id: 29, name: "agent_skills", type: "AgentSkill", repeated: true },
|
|
890
913
|
{ id: 33, name: "git_repo_info_complete", type: "bool" },
|
|
@@ -7,6 +7,24 @@ export type RawField = {
|
|
|
7
7
|
};
|
|
8
8
|
/** Walk a protobuf message's top-level fields off the raw wire bytes. */
|
|
9
9
|
export declare function readAllFields(b: Uint8Array): RawField[];
|
|
10
|
+
export type StrictRawField = {
|
|
11
|
+
fn: number;
|
|
12
|
+
wt: number;
|
|
13
|
+
varint?: bigint;
|
|
14
|
+
bytes?: Uint8Array;
|
|
15
|
+
fixed64?: Uint8Array;
|
|
16
|
+
fixed32?: Uint8Array;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Strictly walk a protobuf message for security/replay decisions.
|
|
20
|
+
*
|
|
21
|
+
* Unlike `readAllFields`, this parser consumes the complete input, preserves
|
|
22
|
+
* uint64 varints as bigint, represents fixed-width fields, and rejects invalid
|
|
23
|
+
* tags, truncation, overflow, groups, and unsupported wire types. It deliberately
|
|
24
|
+
* returns `undefined` instead of a partial result: callers must fail closed when
|
|
25
|
+
* deciding whether replay is safe.
|
|
26
|
+
*/
|
|
27
|
+
export declare function readAllFieldsStrict(bytes: Uint8Array): StrictRawField[] | undefined;
|
|
10
28
|
/** Decode a google.protobuf.Value message (bytes) back into a JSON value. */
|
|
11
29
|
export declare function decodeValueToJson(bytes: Uint8Array): unknown;
|
|
12
30
|
/**
|