cursor-opencode-provider 0.2.4 → 0.2.6
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 +15 -11
- package/SECURITY.md +42 -0
- package/dist/auth.d.ts +2 -1
- package/dist/auth.js +19 -13
- package/dist/context/rules.d.ts +4 -0
- package/dist/context/rules.js +53 -17
- package/dist/debug.d.ts +9 -0
- package/dist/debug.js +44 -4
- package/dist/language-model.d.ts +18 -4
- package/dist/language-model.js +149 -30
- package/dist/plugin.js +28 -12
- package/dist/protocol/interactions.d.ts +8 -1
- package/dist/protocol/interactions.js +15 -2
- package/dist/protocol/messages.js +7 -2
- package/dist/protocol/request.d.ts +4 -2
- package/dist/protocol/request.js +5 -5
- package/dist/protocol/tool-call-bridge.js +11 -1
- package/dist/protocol/tools.d.ts +2 -2
- package/dist/protocol/tools.js +145 -28
- package/dist/session.d.ts +2 -0
- package/dist/shell-timeout.d.ts +52 -3
- package/dist/shell-timeout.js +277 -23
- package/package.json +3 -2
package/dist/language-model.js
CHANGED
|
@@ -288,7 +288,7 @@ async function doStreamImpl(modelId, options, callOptions) {
|
|
|
288
288
|
// Cursor can continue instead of deadlocking.
|
|
289
289
|
const ids = trailingToolResults.map((r) => `${r.sessionId}:${r.execId}`).join(",");
|
|
290
290
|
trace(`continuation: ${trailingToolResults.length} interrupted trailing tool result(s) [${ids}] — rebasing fresh Run`);
|
|
291
|
-
session = await openSession({ recovery:
|
|
291
|
+
session = await openSession({ recovery: { kind: "rebase" } });
|
|
292
292
|
}
|
|
293
293
|
else {
|
|
294
294
|
// Fresh turn (prompt ends with user/assistant text). Historical tool
|
|
@@ -323,7 +323,7 @@ async function doStreamImpl(modelId, options, callOptions) {
|
|
|
323
323
|
abortSignal: callOptions.abortSignal,
|
|
324
324
|
promptTokens,
|
|
325
325
|
retryPolicy,
|
|
326
|
-
recover: () => openSession({ recovery
|
|
326
|
+
recover: (recovery) => openSession({ recovery }),
|
|
327
327
|
onSession: (next) => { activeSession = next; },
|
|
328
328
|
});
|
|
329
329
|
try {
|
|
@@ -361,6 +361,7 @@ export async function pumpWithRecovery(input) {
|
|
|
361
361
|
maxAttempts: (input.maxRecoveries ?? 1) + 1,
|
|
362
362
|
};
|
|
363
363
|
const maxRecoveries = retryPolicy.maxAttempts - 1;
|
|
364
|
+
const requestUsage = { outputChars: 0 };
|
|
364
365
|
input.onSession?.(session);
|
|
365
366
|
for (let attempt = 0;; attempt++) {
|
|
366
367
|
const pumpedSession = session;
|
|
@@ -371,6 +372,7 @@ export async function pumpWithRecovery(input) {
|
|
|
371
372
|
textId: crypto.randomUUID(),
|
|
372
373
|
reasoningId: crypto.randomUUID(),
|
|
373
374
|
promptTokens: input.promptTokens ?? 0,
|
|
375
|
+
requestUsage,
|
|
374
376
|
}, input.abortSignal);
|
|
375
377
|
return session;
|
|
376
378
|
}
|
|
@@ -381,19 +383,26 @@ export async function pumpWithRecovery(input) {
|
|
|
381
383
|
});
|
|
382
384
|
if (!failure.transient)
|
|
383
385
|
throw failure;
|
|
384
|
-
|
|
386
|
+
const checkpoint = pumpedSession.resumeCheckpoint;
|
|
387
|
+
if (!failure.replaySafe && !checkpoint) {
|
|
385
388
|
throw retrySuppressedError(failure, "after visible output or stateful server activity", attempt + 1, maxRecoveries + 1);
|
|
386
389
|
}
|
|
387
390
|
if (attempt >= maxRecoveries) {
|
|
388
391
|
throw new CursorRetryExhaustedError(attempt + 1, failure);
|
|
389
392
|
}
|
|
390
393
|
trace(`Run interrupted: sessionId=${pumpedSession.sessionId} attempt=${attempt + 1}/${maxRecoveries} ` +
|
|
391
|
-
`err=${failure.message} — rebasing fresh Run`);
|
|
394
|
+
`err=${failure.message} — ${checkpoint ? `resuming ${checkpoint.length}B checkpoint` : "rebasing fresh Run"}`);
|
|
392
395
|
sessionManager.close(pumpedSession, "remote-error", failure);
|
|
393
396
|
const delayMs = retryDelayMs(failure, attempt + 1, retryPolicy);
|
|
394
397
|
trace(`Run retry backoff: attempt=${attempt + 1}/${maxRecoveries} delayMs=${delayMs}`);
|
|
395
398
|
await sleepForRetry(delayMs, input.abortSignal);
|
|
396
|
-
session = await input.recover(
|
|
399
|
+
session = await input.recover(checkpoint
|
|
400
|
+
? {
|
|
401
|
+
kind: "resume",
|
|
402
|
+
conversationId: pumpedSession.conversationId,
|
|
403
|
+
checkpoint: Uint8Array.from(checkpoint),
|
|
404
|
+
}
|
|
405
|
+
: { kind: "rebase" });
|
|
397
406
|
input.onSession?.(session);
|
|
398
407
|
}
|
|
399
408
|
finally {
|
|
@@ -419,18 +428,21 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
419
428
|
const tools = toolState.advertisedTools;
|
|
420
429
|
const allowTools = toolState.allowTools;
|
|
421
430
|
const resetState = resolveTurnConversationReset({ sessionKey, isCompaction });
|
|
422
|
-
const recovery = startOptions?.recovery
|
|
431
|
+
const recovery = startOptions?.recovery;
|
|
432
|
+
const resuming = recovery?.kind === "resume";
|
|
423
433
|
// Compaction must not reuse the prior conversation; its first normal turn
|
|
424
434
|
// must also rebase so the summary-agent checkpoint cannot replace the normal
|
|
425
435
|
// system prompt and OpenCode's newly compacted history.
|
|
426
|
-
const bound =
|
|
436
|
+
const bound = resuming
|
|
437
|
+
? { conversationId: recovery.conversationId, reset: false, previousId: undefined }
|
|
438
|
+
: bindConversationId(sessionKey, { reset: resetState.reset || recovery?.kind === "rebase" });
|
|
427
439
|
const conversationId = bound.conversationId;
|
|
428
440
|
if (bound.reset) {
|
|
429
|
-
trace(`conversation reset: reason=${recovery ? "interrupted-run" : (resetState.reason ?? "unknown")} ` +
|
|
441
|
+
trace(`conversation reset: reason=${recovery?.kind === "rebase" ? "interrupted-run" : (resetState.reason ?? "unknown")} ` +
|
|
430
442
|
`sessionKey=${sessionKey ?? "(none)"} ` +
|
|
431
443
|
`previousId=${bound.previousId ?? "-"} → conversationId=${conversationId}`);
|
|
432
444
|
}
|
|
433
|
-
const userText = recovery
|
|
445
|
+
const userText = recovery?.kind === "rebase"
|
|
434
446
|
? "Continue the interrupted turn from the conversation history above. Do not repeat completed work."
|
|
435
447
|
: (extractUserText([...prompt].reverse().find((m) => m.role === "user")) || ".");
|
|
436
448
|
const workspaceRoot = path.resolve(options.workspaceRoot || process.cwd());
|
|
@@ -439,7 +451,10 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
439
451
|
const systemPrompt = interactionGuidance
|
|
440
452
|
? [baseSystemPrompt, interactionGuidance].filter(Boolean).join("\n\n")
|
|
441
453
|
: baseSystemPrompt;
|
|
442
|
-
const history = extractPromptHistory(prompt, {
|
|
454
|
+
const history = extractPromptHistory(prompt, {
|
|
455
|
+
preserveTrailingUser: recovery?.kind === "rebase",
|
|
456
|
+
toolResults: isCompaction ? "all" : (recovery?.kind === "rebase" ? "trailing" : "omit"),
|
|
457
|
+
});
|
|
443
458
|
await loadAvailableModels();
|
|
444
459
|
// Resolve the region-specific Run stream origin once per process (memoized
|
|
445
460
|
// in agent-url.ts). Explicit agent host overrides skip GetServerConfig but
|
|
@@ -482,7 +497,9 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
482
497
|
: [];
|
|
483
498
|
// CLI parity: echo the last conversation_checkpoint_update as conversation_state.
|
|
484
499
|
// After compaction reset there is no checkpoint — seed from OpenCode history.
|
|
485
|
-
const conversationState =
|
|
500
|
+
const conversationState = resuming
|
|
501
|
+
? recovery.checkpoint
|
|
502
|
+
: (bound.reset ? undefined : getCheckpoint(conversationId));
|
|
486
503
|
const reqBytes = buildRunRequest({
|
|
487
504
|
text: userText,
|
|
488
505
|
modelId: cursorModelId,
|
|
@@ -495,6 +512,7 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
495
512
|
tools,
|
|
496
513
|
toolDescriptors,
|
|
497
514
|
requestContext,
|
|
515
|
+
action: resuming ? "resume" : "user",
|
|
498
516
|
});
|
|
499
517
|
// Content hashes — Cursor content-addresses large payloads; logging these lets
|
|
500
518
|
// us match a server get_blob_args.blob_id to what it wants served.
|
|
@@ -520,6 +538,7 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
520
538
|
`availableModels=${_availableModels?.length ?? 0} userTextLen=${userText.length} ` +
|
|
521
539
|
`historyMsgs=${history.length} historyChars=${historyChars} ` +
|
|
522
540
|
`checkpointLen=${conversationState?.length ?? 0} reset=${bound.reset} ` +
|
|
541
|
+
`resume=${resuming} ` +
|
|
523
542
|
`usageEstimateIn=${usageEstimate.inputTokens} runRequestBytes=${reqBytes.length}`);
|
|
524
543
|
if (hooksCtx)
|
|
525
544
|
trace(`outbound Run hooks_additional_context: ${hooksCtx}`);
|
|
@@ -538,6 +557,7 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
538
557
|
const session = {
|
|
539
558
|
sessionId: crypto.randomUUID(),
|
|
540
559
|
conversationId,
|
|
560
|
+
resumeCheckpoint: undefined,
|
|
541
561
|
openCodeSessionId: sessionKey,
|
|
542
562
|
stream,
|
|
543
563
|
frames: stream.frames()[Symbol.asyncIterator](),
|
|
@@ -643,6 +663,7 @@ export function deliverContinuationResults(session, trailingToolResults) {
|
|
|
643
663
|
if (!pending.bridged) {
|
|
644
664
|
try {
|
|
645
665
|
const shellResult = pending.resultField === "shell_stream"
|
|
666
|
+
|| pending.resultField === "background_shell_spawn_result"
|
|
646
667
|
? consumeCursorShellResult(r.toolCallId, r.output)
|
|
647
668
|
: undefined;
|
|
648
669
|
frames = buildExecClientMessages({
|
|
@@ -792,7 +813,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
792
813
|
const advertisedToolNameSet = new Set(advertisedToolNames);
|
|
793
814
|
let textStarted = false;
|
|
794
815
|
let reasoningStarted = false;
|
|
795
|
-
|
|
816
|
+
const requestUsage = ids.requestUsage ?? { outputChars: 0 };
|
|
796
817
|
let replaySafe = true;
|
|
797
818
|
// OpenCode cancels the ReadableStream between turns (see the cancel handler
|
|
798
819
|
// in doStreamImpl). The frames iterator can still yield a final `done` after
|
|
@@ -847,6 +868,56 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
847
868
|
return false;
|
|
848
869
|
}
|
|
849
870
|
};
|
|
871
|
+
/**
|
|
872
|
+
* Cursor's streamed edit handshake reads the target before it sends the
|
|
873
|
+
* replacement through write_args. For a new file that read naturally fails,
|
|
874
|
+
* which makes the model abandon the edit and fall back to a shell heredoc.
|
|
875
|
+
* Treat only a missing target correlated to an edit_tool_call as an empty
|
|
876
|
+
* file, allowing Cursor to continue to the ordinary OpenCode write call.
|
|
877
|
+
*/
|
|
878
|
+
const recoverMissingEditRead = (parsed, displayCallId) => {
|
|
879
|
+
if (!displayCallId ||
|
|
880
|
+
parsed.resultField !== "read_result" ||
|
|
881
|
+
parsed.toolName !== "read" ||
|
|
882
|
+
!advertisedToolNameSet.has("write"))
|
|
883
|
+
return false;
|
|
884
|
+
const stored = session.displayToolCalls.get(displayCallId);
|
|
885
|
+
const display = parseDisplayToolCall(displayCallId, stored);
|
|
886
|
+
if (display?.variant !== "edit_tool_call" || display.bridgeable === false)
|
|
887
|
+
return false;
|
|
888
|
+
const requestedPath = typeof parsed.args.filePath === "string" ? parsed.args.filePath : "";
|
|
889
|
+
const editPath = typeof display.args.path === "string" ? display.args.path : "";
|
|
890
|
+
if (!requestedPath || !editPath)
|
|
891
|
+
return false;
|
|
892
|
+
const workspaceRoot = typeof session.requestContext.workspace_project_dir === "string"
|
|
893
|
+
? session.requestContext.workspace_project_dir
|
|
894
|
+
: process.cwd();
|
|
895
|
+
const resolvePath = (value) => path.resolve(workspaceRoot, value);
|
|
896
|
+
const absolutePath = resolvePath(requestedPath);
|
|
897
|
+
if (absolutePath !== resolvePath(editPath) || fs.existsSync(absolutePath))
|
|
898
|
+
return false;
|
|
899
|
+
try {
|
|
900
|
+
for (const frame of buildExecClientMessages({
|
|
901
|
+
execId: parsed.id,
|
|
902
|
+
resultField: parsed.resultField,
|
|
903
|
+
output: "",
|
|
904
|
+
toolName: parsed.toolName,
|
|
905
|
+
resultMetadata: { path: requestedPath },
|
|
906
|
+
})) {
|
|
907
|
+
session.stream.write(frame);
|
|
908
|
+
}
|
|
909
|
+
trace(`exec: missing edit target treated as empty file id=${parsed.id} ` +
|
|
910
|
+
`path=${JSON.stringify(requestedPath)}; awaiting write_args`);
|
|
911
|
+
return true;
|
|
912
|
+
}
|
|
913
|
+
catch (e) {
|
|
914
|
+
const error = new Error(`Failed to recover Cursor edit read for a new file: ${e.message}`);
|
|
915
|
+
trace(`exec: edit read recovery FAILED ${error.message}`);
|
|
916
|
+
safeError(error);
|
|
917
|
+
sessionManager.close(session);
|
|
918
|
+
return true;
|
|
919
|
+
}
|
|
920
|
+
};
|
|
850
921
|
/** AI SDK V3 requires text-end / reasoning-end before finish or tool-call. */
|
|
851
922
|
const closeOpenSpans = () => {
|
|
852
923
|
for (const part of spanEndParts({ textStarted, reasoningStarted, textId, reasoningId })) {
|
|
@@ -870,7 +941,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
870
941
|
}
|
|
871
942
|
session.usageEstimate.outputTokens += estimateTokens(text.length);
|
|
872
943
|
if (safeEnqueue({ type: "text-delta", id: textId, delta: text })) {
|
|
873
|
-
|
|
944
|
+
requestUsage.outputChars += text.length;
|
|
874
945
|
}
|
|
875
946
|
};
|
|
876
947
|
const emitReasoning = (text) => {
|
|
@@ -883,7 +954,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
883
954
|
}
|
|
884
955
|
session.usageEstimate.outputTokens += estimateTokens(text.length);
|
|
885
956
|
if (safeEnqueue({ type: "reasoning-delta", id: reasoningId, delta: text })) {
|
|
886
|
-
|
|
957
|
+
requestUsage.outputChars += text.length;
|
|
887
958
|
}
|
|
888
959
|
};
|
|
889
960
|
const emitFinish = (te, reason) => {
|
|
@@ -906,7 +977,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
906
977
|
cacheWrite: 0,
|
|
907
978
|
},
|
|
908
979
|
outputTokens: {
|
|
909
|
-
total: estimateTokens(
|
|
980
|
+
total: estimateTokens(requestUsage.outputChars),
|
|
910
981
|
text: undefined,
|
|
911
982
|
reasoning: undefined,
|
|
912
983
|
},
|
|
@@ -1044,6 +1115,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1044
1115
|
const bytes = normalizeCheckpointBytes(checkpointRaw);
|
|
1045
1116
|
if (bytes && bytes.length > 0) {
|
|
1046
1117
|
setCheckpoint(session.conversationId, bytes);
|
|
1118
|
+
session.resumeCheckpoint = Uint8Array.from(bytes);
|
|
1047
1119
|
trace(`checkpoint: stored ${bytes.length}B for conversationId=${session.conversationId}`);
|
|
1048
1120
|
}
|
|
1049
1121
|
}
|
|
@@ -1153,7 +1225,11 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1153
1225
|
trace(`exec request_context: replied`);
|
|
1154
1226
|
}
|
|
1155
1227
|
catch (e) {
|
|
1156
|
-
|
|
1228
|
+
const error = new Error(`Failed to answer Cursor request_context probe: ${e.message}`);
|
|
1229
|
+
trace(`exec request_context: write FAILED ${error.message}`);
|
|
1230
|
+
safeError(error);
|
|
1231
|
+
sessionManager.close(session);
|
|
1232
|
+
return;
|
|
1157
1233
|
}
|
|
1158
1234
|
}
|
|
1159
1235
|
else if (esm.mcp_state_exec_args) {
|
|
@@ -1179,10 +1255,6 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1179
1255
|
else {
|
|
1180
1256
|
const parsed = parseExecServerMessage(esm);
|
|
1181
1257
|
const displayCallId = extractExecDisplayCallId(esm);
|
|
1182
|
-
if (displayCallId) {
|
|
1183
|
-
session.displayToolCalls.delete(displayCallId);
|
|
1184
|
-
trace(`exec: claimed display callId=${displayCallId}`);
|
|
1185
|
-
}
|
|
1186
1258
|
trace(`exec: id=${parsed?.id} variant=${parsed ? Object.keys(parsed).join(",") : "none"} toolName=${parsed?.toolName} resultField=${parsed?.resultField}`);
|
|
1187
1259
|
if (parsed) {
|
|
1188
1260
|
if (parsed.localError) {
|
|
@@ -1219,8 +1291,15 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1219
1291
|
return;
|
|
1220
1292
|
continue;
|
|
1221
1293
|
}
|
|
1294
|
+
if (recoverMissingEditRead(parsed, displayCallId))
|
|
1295
|
+
continue;
|
|
1296
|
+
if (displayCallId) {
|
|
1297
|
+
session.displayToolCalls.delete(displayCallId);
|
|
1298
|
+
trace(`exec: claimed display callId=${displayCallId}`);
|
|
1299
|
+
}
|
|
1222
1300
|
const tc = buildToolCallPart(parsed, session.sessionId);
|
|
1223
|
-
if (parsed.resultField === "shell_stream"
|
|
1301
|
+
if (parsed.resultField === "shell_stream"
|
|
1302
|
+
|| parsed.resultField === "background_shell_spawn_result") {
|
|
1224
1303
|
registerCursorShellCall(tc.toolCallId, parsed.resultMetadata);
|
|
1225
1304
|
}
|
|
1226
1305
|
// Keep the stream open; the result arrives on the next doStream call.
|
|
@@ -1257,7 +1336,8 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1257
1336
|
else if (interactionQuery) {
|
|
1258
1337
|
// InteractionQuery is a must-reply channel, just like exec and KV. AI
|
|
1259
1338
|
// SDK has no Cursor-specific UI callback, so answer immediately with the
|
|
1260
|
-
// conservative headless policy from protocol/interactions.ts
|
|
1339
|
+
// conservative headless policy from protocol/interactions.ts (including
|
|
1340
|
+
// F14 create_plan auto-ack / empty plan_uri for CLI headless parity).
|
|
1261
1341
|
try {
|
|
1262
1342
|
const handled = handleInteractionQuery(interactionQuery, payload);
|
|
1263
1343
|
session.stream.write(handled.reply);
|
|
@@ -1290,7 +1370,13 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1290
1370
|
`found=${handled.found} echoed=${!!handled.echoed} ` +
|
|
1291
1371
|
`sessionBlobs=${session.blobs.size} convBlobs=${conversationBlobCount(session.conversationId)}`);
|
|
1292
1372
|
}
|
|
1293
|
-
catch {
|
|
1373
|
+
catch (e) {
|
|
1374
|
+
const error = new Error(`Failed to answer Cursor KV blob request: ${e.message}`);
|
|
1375
|
+
trace(`kv: write FAILED ${error.message}`);
|
|
1376
|
+
safeError(error);
|
|
1377
|
+
sessionManager.close(session);
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1294
1380
|
}
|
|
1295
1381
|
}
|
|
1296
1382
|
}
|
|
@@ -1423,6 +1509,11 @@ export function buildOpenCodeInteractionGuidance(tools, isCompaction, workspaceR
|
|
|
1423
1509
|
if (names.has("webfetch")) {
|
|
1424
1510
|
instructions.push("- To fetch a known URL, call the OpenCode `webfetch` tool; do not use Cursor's native WebFetch interaction.");
|
|
1425
1511
|
}
|
|
1512
|
+
if (names.has("write")) {
|
|
1513
|
+
instructions.push(names.has("edit")
|
|
1514
|
+
? "- For file changes, use OpenCode `edit` for targeted changes to existing files and `write` to create files or intentionally replace complete contents; do not use shell, Python, or heredocs to change file content while these tools are available."
|
|
1515
|
+
: "- Use OpenCode `write` for file-content changes; do not use shell, Python, or heredocs to change file content while it is available.");
|
|
1516
|
+
}
|
|
1426
1517
|
return [
|
|
1427
1518
|
`OpenCode exposes exactly these executable tools for this turn: ${[...names].map((name) => `\`${name}\``).join(", ")}.`,
|
|
1428
1519
|
`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.`,
|
|
@@ -1469,13 +1560,24 @@ export function cursorTurnEndedProviderMetadata(te) {
|
|
|
1469
1560
|
};
|
|
1470
1561
|
}
|
|
1471
1562
|
/**
|
|
1472
|
-
* Prior prompt turns for a seed ConversationStateStructure
|
|
1473
|
-
*
|
|
1474
|
-
* result
|
|
1563
|
+
* Prior prompt turns for a seed ConversationStateStructure. Tool results must
|
|
1564
|
+
* never be replayed as assistant-authored prose: that teaches the model to
|
|
1565
|
+
* counterfeit `Tool result (...)` text instead of emitting a real tool call.
|
|
1566
|
+
* Normal rebases omit old results; compaction can retain all results and
|
|
1567
|
+
* interrupted continuations retain only the trailing live result suffix as
|
|
1568
|
+
* explicit OpenCode-host observations.
|
|
1475
1569
|
*/
|
|
1476
1570
|
export function extractPromptHistory(prompt, options) {
|
|
1477
1571
|
const out = [];
|
|
1478
|
-
|
|
1572
|
+
const toolResults = options?.toolResults ?? "omit";
|
|
1573
|
+
let trailingToolStart = prompt.length;
|
|
1574
|
+
if (toolResults === "trailing") {
|
|
1575
|
+
while (trailingToolStart > 0 && prompt[trailingToolStart - 1]?.role === "tool") {
|
|
1576
|
+
trailingToolStart--;
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
for (let messageIndex = 0; messageIndex < prompt.length; messageIndex++) {
|
|
1580
|
+
const m = prompt[messageIndex];
|
|
1479
1581
|
if (m.role === "system") {
|
|
1480
1582
|
if (typeof m.content === "string" && m.content.length > 0) {
|
|
1481
1583
|
out.push({ role: "system", content: m.content });
|
|
@@ -1495,18 +1597,26 @@ export function extractPromptHistory(prompt, options) {
|
|
|
1495
1597
|
continue;
|
|
1496
1598
|
}
|
|
1497
1599
|
if (m.role === "tool" && Array.isArray(m.content)) {
|
|
1600
|
+
if (toolResults === "omit" ||
|
|
1601
|
+
(toolResults === "trailing" && messageIndex < trailingToolStart))
|
|
1602
|
+
continue;
|
|
1498
1603
|
const results = [];
|
|
1499
1604
|
for (const part of m.content) {
|
|
1500
1605
|
const p = part;
|
|
1501
1606
|
if (p.type !== "tool-result")
|
|
1502
1607
|
continue;
|
|
1503
1608
|
const toolName = typeof p.toolName === "string" && p.toolName ? p.toolName : "tool";
|
|
1609
|
+
const toolCallId = typeof p.toolCallId === "string" ? p.toolCallId : "";
|
|
1504
1610
|
const result = toolResultOutputToText(p.output);
|
|
1505
|
-
|
|
1506
|
-
|
|
1611
|
+
results.push(formatSeedToolObservation({
|
|
1612
|
+
toolName,
|
|
1613
|
+
toolCallId,
|
|
1614
|
+
output: result.text,
|
|
1615
|
+
isError: result.isError,
|
|
1616
|
+
}));
|
|
1507
1617
|
}
|
|
1508
1618
|
if (results.length > 0)
|
|
1509
|
-
appendSeedHistory(out, "
|
|
1619
|
+
appendSeedHistory(out, "user", results.join("\n\n"));
|
|
1510
1620
|
}
|
|
1511
1621
|
}
|
|
1512
1622
|
// Live user message is the Run action, not seed history.
|
|
@@ -1515,6 +1625,15 @@ export function extractPromptHistory(prompt, options) {
|
|
|
1515
1625
|
}
|
|
1516
1626
|
return out;
|
|
1517
1627
|
}
|
|
1628
|
+
function formatSeedToolObservation(input) {
|
|
1629
|
+
const metadata = JSON.stringify({
|
|
1630
|
+
source: "opencode-tool",
|
|
1631
|
+
tool: input.toolName,
|
|
1632
|
+
callId: input.toolCallId,
|
|
1633
|
+
status: input.isError ? "error" : "completed",
|
|
1634
|
+
});
|
|
1635
|
+
return `OpenCode host observation ${metadata}:\n${input.output}`;
|
|
1636
|
+
}
|
|
1518
1637
|
function extractAssistantHistoryText(msg) {
|
|
1519
1638
|
const content = msg.content;
|
|
1520
1639
|
if (typeof content === "string")
|
package/dist/plugin.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { CURSOR_COMPACTION_OPTION, CURSOR_PROVIDER_ID, CURSOR_WEBSITE_HOST, CURSOR_API_HOST } from "./shared.js";
|
|
2
|
-
import { pollForTokens, exchangeApiKey, refreshAccessToken, isExpiringSoon, generatePkceParams, generatePkceChallenge, buildLoginUrl,
|
|
2
|
+
import { pollForTokens, exchangeApiKey, refreshAccessToken, isExpiringSoon, generatePkceParams, generatePkceChallenge, buildLoginUrl, decodeJwtExpiryMs } from "./auth.js";
|
|
3
3
|
import { CURSOR_VARIANT_PARAMETERS_KEY, CURSOR_WIRE_MODEL_ID_KEY, readCache, discoverModels, isCacheFresh, parseCursorContextLimit } from "./models.js";
|
|
4
4
|
import { opencodeGlobalCacheDir } from "./context/paths.js";
|
|
5
5
|
import { readStoredAuth } from "./context/auth-store.js";
|
|
6
6
|
import { resolveAgentUrl } from "./agent-url.js";
|
|
7
|
-
import { captureCursorShellResult, cursorShellOriginalCommand, prepareCursorShellArgs, } from "./shell-timeout.js";
|
|
7
|
+
import { captureCursorShellResult, cursorShellEnvForCall, cursorShellOriginalCommand, prepareCursorShellArgs, releaseCursorShellEnv, sanitizeRegisteredCursorShellOutput, } from "./shell-timeout.js";
|
|
8
8
|
import { sessionActivity } from "./activity.js";
|
|
9
9
|
const MODULE_URL = new URL("./index.js", import.meta.url).href;
|
|
10
10
|
/**
|
|
@@ -275,7 +275,7 @@ export async function CursorPlugin(input) {
|
|
|
275
275
|
type: "oauth",
|
|
276
276
|
access: newTokens.accessToken,
|
|
277
277
|
refresh: newTokens.refreshToken,
|
|
278
|
-
expires:
|
|
278
|
+
expires: decodeJwtExpiryMs(newTokens.accessToken) ?? Date.now(),
|
|
279
279
|
...(extras.accountId !== undefined ? { accountId: extras.accountId } : {}),
|
|
280
280
|
...(extras.enterpriseUrl !== undefined ? { enterpriseUrl: extras.enterpriseUrl } : {}),
|
|
281
281
|
});
|
|
@@ -336,13 +336,35 @@ export async function CursorPlugin(input) {
|
|
|
336
336
|
async "tool.execute.before"(hookInput, output) {
|
|
337
337
|
if (hookInput.tool !== "bash")
|
|
338
338
|
return;
|
|
339
|
+
// Keep args.command as the original display/permission command. Wrapping
|
|
340
|
+
// happens via shell.env (BASH_ENV / ZDOTDIR) so OpenCode's bash UI never
|
|
341
|
+
// stores or renders the private wrapper script.
|
|
339
342
|
prepareCursorShellArgs(hookInput.callID, output.args);
|
|
340
343
|
},
|
|
344
|
+
async "shell.env"(hookInput, output) {
|
|
345
|
+
const env = cursorShellEnvForCall(hookInput.callID);
|
|
346
|
+
if (!env)
|
|
347
|
+
return;
|
|
348
|
+
Object.assign(output.env, env);
|
|
349
|
+
},
|
|
341
350
|
async "tool.execute.after"(hookInput, output) {
|
|
342
351
|
if (hookInput.tool !== "bash")
|
|
343
352
|
return;
|
|
344
|
-
|
|
345
|
-
|
|
353
|
+
try {
|
|
354
|
+
output.title = cursorShellOriginalCommand(hookInput.callID) ?? output.title;
|
|
355
|
+
output.output = captureCursorShellResult(hookInput.callID, output.output, output.metadata);
|
|
356
|
+
// OpenCode's bash GUI falls back to metadata.output when output is empty
|
|
357
|
+
// (`props.output || props.metadata.output`), so strip private markers there too.
|
|
358
|
+
if (output.metadata && typeof output.metadata === "object") {
|
|
359
|
+
const metadata = output.metadata;
|
|
360
|
+
if (typeof metadata.output === "string") {
|
|
361
|
+
metadata.output = sanitizeRegisteredCursorShellOutput(hookInput.callID, metadata.output);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
finally {
|
|
366
|
+
releaseCursorShellEnv(hookInput.callID);
|
|
367
|
+
}
|
|
346
368
|
},
|
|
347
369
|
async "chat.params"(hookInput, output) {
|
|
348
370
|
if (hookInput.model.providerID !== CURSOR_PROVIDER_ID)
|
|
@@ -397,7 +419,7 @@ export async function CursorPlugin(input) {
|
|
|
397
419
|
provider: CURSOR_PROVIDER_ID,
|
|
398
420
|
access: result.accessToken,
|
|
399
421
|
refresh: result.refreshToken,
|
|
400
|
-
expires:
|
|
422
|
+
expires: decodeJwtExpiryMs(result.accessToken) ?? Date.now(),
|
|
401
423
|
};
|
|
402
424
|
},
|
|
403
425
|
};
|
|
@@ -469,9 +491,3 @@ export async function CursorPlugin(input) {
|
|
|
469
491
|
},
|
|
470
492
|
};
|
|
471
493
|
}
|
|
472
|
-
function decodeExpFromJwt(jwt) {
|
|
473
|
-
const payload = decodeJwtPayload(jwt);
|
|
474
|
-
if (payload && typeof payload.exp === "number")
|
|
475
|
-
return payload.exp * 1000;
|
|
476
|
-
return Date.now() + 3600_000;
|
|
477
|
-
}
|
|
@@ -19,5 +19,12 @@ export declare class UnsupportedInteractionQueryError extends Error {
|
|
|
19
19
|
* instead of accidentally restoring the heartbeat-only deadlock.
|
|
20
20
|
*/
|
|
21
21
|
export declare function inspectInteractionQueryWire(agentServerPayload: Uint8Array): InteractionQueryWireInfo;
|
|
22
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* Build the immediate typed response required by Cursor's Run RPC.
|
|
24
|
+
*
|
|
25
|
+
* OpenCode has no Cursor UI callbacks, so every InteractionQuery is answered
|
|
26
|
+
* here with a conservative headless policy (reject UI-bound prompts; ack a few
|
|
27
|
+
* no-UI cases). See case 7 / F14 for create_plan auto-ack parity with Cursor
|
|
28
|
+
* CLI headless mode.
|
|
29
|
+
*/
|
|
23
30
|
export declare function handleInteractionQuery(query: Record<string, unknown>, agentServerPayload: Uint8Array): HandledInteraction;
|
|
@@ -51,7 +51,14 @@ export function inspectInteractionQueryWire(agentServerPayload) {
|
|
|
51
51
|
variantName: variantField === undefined ? undefined : variantNames[variantField],
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
|
-
/**
|
|
54
|
+
/**
|
|
55
|
+
* Build the immediate typed response required by Cursor's Run RPC.
|
|
56
|
+
*
|
|
57
|
+
* OpenCode has no Cursor UI callbacks, so every InteractionQuery is answered
|
|
58
|
+
* here with a conservative headless policy (reject UI-bound prompts; ack a few
|
|
59
|
+
* no-UI cases). See case 7 / F14 for create_plan auto-ack parity with Cursor
|
|
60
|
+
* CLI headless mode.
|
|
61
|
+
*/
|
|
55
62
|
export function handleInteractionQuery(query, agentServerPayload) {
|
|
56
63
|
const info = inspectInteractionQueryWire(agentServerPayload);
|
|
57
64
|
if (info.id === undefined || info.variantField === undefined || !info.variantName) {
|
|
@@ -80,8 +87,14 @@ export function handleInteractionQuery(query, agentServerPayload) {
|
|
|
80
87
|
outcome = "rejected";
|
|
81
88
|
break;
|
|
82
89
|
case 7:
|
|
90
|
+
// F14: create_plan_request_query auto-ack (Cursor CLI headless parity).
|
|
83
91
|
// Cursor CLI's headless fallback acknowledges plan creation without a
|
|
84
|
-
// client-side URI; the plan remains in
|
|
92
|
+
// client-side URI (`success` + empty `plan_uri`); the plan remains in
|
|
93
|
+
// conversation state / checkpoints. This provider mirrors that reply so
|
|
94
|
+
// the Run RPC does not deadlock waiting for UI approval. Impact: Cursor
|
|
95
|
+
// may treat the plan as accepted without an OpenCode UI confirm; local
|
|
96
|
+
// tool execution is still gated by OpenCode permissions. Do not change
|
|
97
|
+
// this to reject/prompt without CLI parity evidence.
|
|
85
98
|
response = { create_plan_request_response: { result: { success: {}, plan_uri: "" } } };
|
|
86
99
|
outcome = "acknowledged";
|
|
87
100
|
break;
|
|
@@ -357,8 +357,11 @@ export function createMessageTypes() {
|
|
|
357
357
|
{ id: 2, name: "content", type: "string" },
|
|
358
358
|
{ id: 3, name: "total_lines", type: "int32" },
|
|
359
359
|
{ id: 4, name: "file_size", type: "int64" },
|
|
360
|
+
{ id: 5, name: "data", type: "bytes" },
|
|
360
361
|
{ id: 6, name: "truncated", type: "bool" },
|
|
361
|
-
|
|
362
|
+
{ id: 7, name: "output_blob_id", type: "bytes" },
|
|
363
|
+
{ id: 8, name: "range_applied", type: "bool" },
|
|
364
|
+
], [{ name: "output", fields: ["content", "data"] }]);
|
|
362
365
|
addType(root, "ReadError", [
|
|
363
366
|
{ id: 1, name: "path", type: "string" },
|
|
364
367
|
{ id: 2, name: "error", type: "string" },
|
|
@@ -900,7 +903,9 @@ export function createMessageTypes() {
|
|
|
900
903
|
{ id: 2, name: "request_context", type: "RequestContext" },
|
|
901
904
|
]);
|
|
902
905
|
addType(root, "ResumeAction", [
|
|
903
|
-
|
|
906
|
+
// Cursor CLI sends an empty ResumeAction. Field #2 is available for a
|
|
907
|
+
// refreshed RequestContext; the conversation id belongs to AgentRunRequest.
|
|
908
|
+
{ id: 2, name: "request_context", type: "RequestContext" },
|
|
904
909
|
]);
|
|
905
910
|
addType(root, "CancelAction", [
|
|
906
911
|
{ id: 1, name: "conversation_id", type: "string" },
|
|
@@ -10,8 +10,8 @@ export type RunRequestInput = {
|
|
|
10
10
|
systemPrompt?: string;
|
|
11
11
|
/**
|
|
12
12
|
* Prior chat turns for a seed ConversationStateStructure (no checkpoint).
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* Tool outputs, when required for compaction/recovery, are represented as
|
|
14
|
+
* user-role OpenCode host observations rather than assistant-authored prose.
|
|
15
15
|
*/
|
|
16
16
|
history?: SeedHistoryMessage[];
|
|
17
17
|
/**
|
|
@@ -32,6 +32,8 @@ export type RunRequestInput = {
|
|
|
32
32
|
toolDescriptors?: Array<Record<string, unknown>>;
|
|
33
33
|
/** Prebuilt RequestContext (OpenCode-sourced). */
|
|
34
34
|
requestContext?: Record<string, unknown>;
|
|
35
|
+
/** Resume the supplied checkpoint instead of submitting another user turn. */
|
|
36
|
+
action?: "user" | "resume";
|
|
35
37
|
};
|
|
36
38
|
/**
|
|
37
39
|
* Seed ConversationStateStructure for the first turn (no checkpoint yet).
|
package/dist/protocol/request.js
CHANGED
|
@@ -54,9 +54,11 @@ export function buildRunRequest(input) {
|
|
|
54
54
|
message_id: msgId,
|
|
55
55
|
},
|
|
56
56
|
};
|
|
57
|
-
if (requestContext)
|
|
57
|
+
if (requestContext)
|
|
58
58
|
userMessageAction.request_context = requestContext;
|
|
59
|
-
|
|
59
|
+
const action = input.action === "resume"
|
|
60
|
+
? { resume_action: {} }
|
|
61
|
+
: { user_message_action: userMessageAction };
|
|
60
62
|
const conversationState = input.conversationState && input.conversationState.length > 0
|
|
61
63
|
? input.conversationState
|
|
62
64
|
: buildSeedConversationState({
|
|
@@ -65,9 +67,7 @@ export function buildRunRequest(input) {
|
|
|
65
67
|
});
|
|
66
68
|
const runRequest = {
|
|
67
69
|
conversation_id: input.conversationId,
|
|
68
|
-
action
|
|
69
|
-
user_message_action: userMessageAction,
|
|
70
|
-
},
|
|
70
|
+
action,
|
|
71
71
|
requested_model: {
|
|
72
72
|
// The provider always selects a concrete model. Cursor's "default"
|
|
73
73
|
// pseudo-model (Auto) is never used here — we send the real id plus the
|
|
@@ -7,7 +7,12 @@ const TODO_STATUS = {
|
|
|
7
7
|
3: "completed",
|
|
8
8
|
4: "cancelled",
|
|
9
9
|
};
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* Cursor ToolCall oneof field → default OpenCode tool id.
|
|
12
|
+
*
|
|
13
|
+
* F11: `delete_tool_call` has no OpenCode builtin; it remaps to bash (see the
|
|
14
|
+
* delete_tool_call branch below for `rm -f -- <path>`).
|
|
15
|
+
*/
|
|
11
16
|
const VARIANT_TO_OPENCODE = {
|
|
12
17
|
shell_tool_call: "bash",
|
|
13
18
|
delete_tool_call: "bash",
|
|
@@ -121,6 +126,10 @@ export function parseDisplayToolCall(callId, toolCall) {
|
|
|
121
126
|
args: mcpArgs,
|
|
122
127
|
};
|
|
123
128
|
}
|
|
129
|
+
// create_plan_tool_call here is display-state mirroring into todowrite.
|
|
130
|
+
// Separately, InteractionQuery create_plan_request_query is auto-acked in
|
|
131
|
+
// protocol/interactions.ts (F14 / CLI headless parity) — that ack is not an
|
|
132
|
+
// execution of this display payload.
|
|
124
133
|
if (variant === "update_todos_tool_call" || variant === "create_plan_tool_call") {
|
|
125
134
|
const result = asRecord(payload.result);
|
|
126
135
|
const success = asRecord(result?.success);
|
|
@@ -238,6 +247,7 @@ export function parseDisplayToolCall(callId, toolCall) {
|
|
|
238
247
|
args: {},
|
|
239
248
|
};
|
|
240
249
|
}
|
|
250
|
+
// F11: display-path delete → bash `rm -f` (OpenCode has no delete tool).
|
|
241
251
|
if (variant === "delete_tool_call") {
|
|
242
252
|
const path = typeof args.path === "string" ? args.path : "";
|
|
243
253
|
return {
|
package/dist/protocol/tools.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type CursorShellOutcome } from "../shell-timeout.js";
|
|
2
2
|
export declare const REQUEST_CONTEXT_RESULT_FIELD = 10;
|
|
3
3
|
export type OpencodeToolDef = {
|
|
4
4
|
name: string;
|
|
@@ -128,7 +128,7 @@ export declare function unwrapReadOutput(output: string): string;
|
|
|
128
128
|
* OpenCode returns free-form text; we wrap it in the minimal success shape the
|
|
129
129
|
* server accepts (verified against agent.v1 wire captures).
|
|
130
130
|
*/
|
|
131
|
-
export declare function buildTypedExecResult(resultField: string, output: string, error?: string, toolName?: string, resultMetadata?: Record<string, unknown
|
|
131
|
+
export declare function buildTypedExecResult(resultField: string, output: string, error?: string, toolName?: string, resultMetadata?: Record<string, unknown>, shellOutcome?: CursorShellOutcome): Record<string, unknown>;
|
|
132
132
|
export declare function buildToolCallPart(execMsg: ParsedExecRequest, sessionId: string): {
|
|
133
133
|
toolCallId: string;
|
|
134
134
|
toolName: string;
|