cursor-opencode-provider 0.2.5 → 0.2.7
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 +27 -15
- package/dist/context/build.js +6 -1
- package/dist/context/env.d.ts +6 -0
- package/dist/context/env.js +34 -2
- package/dist/context/index.d.ts +1 -1
- package/dist/context/index.js +1 -1
- package/dist/context/paths.d.ts +41 -3
- package/dist/context/paths.js +121 -9
- package/dist/debug.d.ts +7 -0
- package/dist/debug.js +42 -0
- package/dist/index.d.ts +6 -0
- package/dist/language-model.d.ts +11 -1
- package/dist/language-model.js +47 -22
- package/dist/plugin-v2.js +4 -0
- package/dist/plugin.js +30 -4
- package/dist/protocol/messages.js +5 -1
- package/dist/protocol/request.d.ts +2 -0
- package/dist/protocol/request.js +5 -5
- package/dist/protocol/tools.d.ts +2 -2
- package/dist/protocol/tools.js +35 -38
- package/dist/session.d.ts +2 -0
- package/dist/shell-timeout.d.ts +48 -1
- package/dist/shell-timeout.js +308 -24
- package/package.json +9 -3
package/dist/language-model.js
CHANGED
|
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { bidiRunStream, CursorRunInterruptedError, normalizeAgentRunOrigin, } from "./transport/connect.js";
|
|
5
|
-
import { trace } from "./debug.js";
|
|
5
|
+
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";
|
|
@@ -18,7 +18,8 @@ import { resolveContinuationPolicy, sessionManager, } from "./session.js";
|
|
|
18
18
|
import { CursorAuthError, CursorLocalCancellationError, CursorProtocolError, CursorProviderError, CursorRetryExhaustedError, CursorServerError, CursorTransportError, isTransientGrpcStatus, retrySuppressedError, toCursorProviderError, } from "./errors.js";
|
|
19
19
|
import { readCache, cacheFilePath, resolveVariantParameters, paramsImplyMaxMode, extractCursorVariantParameters, resolveCursorWireModelId } from "./models.js";
|
|
20
20
|
import { buildRequestContext } from "./context/build.js";
|
|
21
|
-
import {
|
|
21
|
+
import { workspaceRootFromRequestContext } from "./context/env.js";
|
|
22
|
+
import { opencodeGlobalCacheDir, setHostCacheDirOverride } from "./context/paths.js";
|
|
22
23
|
import { resolveAgentUrl } from "./agent-url.js";
|
|
23
24
|
import { CURSOR_API_HOST, CURSOR_COMPACTION_OPTION } from "./shared.js";
|
|
24
25
|
import { consumeCursorShellResult, registerCursorShellCall, } from "./shell-timeout.js";
|
|
@@ -224,6 +225,9 @@ function rememberPostCompactionRebase(sessionKey) {
|
|
|
224
225
|
}
|
|
225
226
|
}
|
|
226
227
|
export function createCursorLanguageModel(modelId, providerId, options) {
|
|
228
|
+
// Host Path.cache / explicit override wins over XDG heuristics for this process.
|
|
229
|
+
if (options.cacheDir)
|
|
230
|
+
setHostCacheDirOverride(options.cacheDir);
|
|
227
231
|
return {
|
|
228
232
|
specificationVersion: "v3",
|
|
229
233
|
provider: providerId,
|
|
@@ -288,7 +292,7 @@ async function doStreamImpl(modelId, options, callOptions) {
|
|
|
288
292
|
// Cursor can continue instead of deadlocking.
|
|
289
293
|
const ids = trailingToolResults.map((r) => `${r.sessionId}:${r.execId}`).join(",");
|
|
290
294
|
trace(`continuation: ${trailingToolResults.length} interrupted trailing tool result(s) [${ids}] — rebasing fresh Run`);
|
|
291
|
-
session = await openSession({ recovery:
|
|
295
|
+
session = await openSession({ recovery: { kind: "rebase" } });
|
|
292
296
|
}
|
|
293
297
|
else {
|
|
294
298
|
// Fresh turn (prompt ends with user/assistant text). Historical tool
|
|
@@ -323,7 +327,7 @@ async function doStreamImpl(modelId, options, callOptions) {
|
|
|
323
327
|
abortSignal: callOptions.abortSignal,
|
|
324
328
|
promptTokens,
|
|
325
329
|
retryPolicy,
|
|
326
|
-
recover: () => openSession({ recovery
|
|
330
|
+
recover: (recovery) => openSession({ recovery }),
|
|
327
331
|
onSession: (next) => { activeSession = next; },
|
|
328
332
|
});
|
|
329
333
|
try {
|
|
@@ -361,6 +365,7 @@ export async function pumpWithRecovery(input) {
|
|
|
361
365
|
maxAttempts: (input.maxRecoveries ?? 1) + 1,
|
|
362
366
|
};
|
|
363
367
|
const maxRecoveries = retryPolicy.maxAttempts - 1;
|
|
368
|
+
const requestUsage = { outputChars: 0 };
|
|
364
369
|
input.onSession?.(session);
|
|
365
370
|
for (let attempt = 0;; attempt++) {
|
|
366
371
|
const pumpedSession = session;
|
|
@@ -371,6 +376,7 @@ export async function pumpWithRecovery(input) {
|
|
|
371
376
|
textId: crypto.randomUUID(),
|
|
372
377
|
reasoningId: crypto.randomUUID(),
|
|
373
378
|
promptTokens: input.promptTokens ?? 0,
|
|
379
|
+
requestUsage,
|
|
374
380
|
}, input.abortSignal);
|
|
375
381
|
return session;
|
|
376
382
|
}
|
|
@@ -381,19 +387,26 @@ export async function pumpWithRecovery(input) {
|
|
|
381
387
|
});
|
|
382
388
|
if (!failure.transient)
|
|
383
389
|
throw failure;
|
|
384
|
-
|
|
390
|
+
const checkpoint = pumpedSession.resumeCheckpoint;
|
|
391
|
+
if (!failure.replaySafe && !checkpoint) {
|
|
385
392
|
throw retrySuppressedError(failure, "after visible output or stateful server activity", attempt + 1, maxRecoveries + 1);
|
|
386
393
|
}
|
|
387
394
|
if (attempt >= maxRecoveries) {
|
|
388
395
|
throw new CursorRetryExhaustedError(attempt + 1, failure);
|
|
389
396
|
}
|
|
390
397
|
trace(`Run interrupted: sessionId=${pumpedSession.sessionId} attempt=${attempt + 1}/${maxRecoveries} ` +
|
|
391
|
-
`err=${failure.message} — rebasing fresh Run`);
|
|
398
|
+
`err=${failure.message} — ${checkpoint ? `resuming ${checkpoint.length}B checkpoint` : "rebasing fresh Run"}`);
|
|
392
399
|
sessionManager.close(pumpedSession, "remote-error", failure);
|
|
393
400
|
const delayMs = retryDelayMs(failure, attempt + 1, retryPolicy);
|
|
394
401
|
trace(`Run retry backoff: attempt=${attempt + 1}/${maxRecoveries} delayMs=${delayMs}`);
|
|
395
402
|
await sleepForRetry(delayMs, input.abortSignal);
|
|
396
|
-
session = await input.recover(
|
|
403
|
+
session = await input.recover(checkpoint
|
|
404
|
+
? {
|
|
405
|
+
kind: "resume",
|
|
406
|
+
conversationId: pumpedSession.conversationId,
|
|
407
|
+
checkpoint: Uint8Array.from(checkpoint),
|
|
408
|
+
}
|
|
409
|
+
: { kind: "rebase" });
|
|
397
410
|
input.onSession?.(session);
|
|
398
411
|
}
|
|
399
412
|
finally {
|
|
@@ -419,18 +432,21 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
419
432
|
const tools = toolState.advertisedTools;
|
|
420
433
|
const allowTools = toolState.allowTools;
|
|
421
434
|
const resetState = resolveTurnConversationReset({ sessionKey, isCompaction });
|
|
422
|
-
const recovery = startOptions?.recovery
|
|
435
|
+
const recovery = startOptions?.recovery;
|
|
436
|
+
const resuming = recovery?.kind === "resume";
|
|
423
437
|
// Compaction must not reuse the prior conversation; its first normal turn
|
|
424
438
|
// must also rebase so the summary-agent checkpoint cannot replace the normal
|
|
425
439
|
// system prompt and OpenCode's newly compacted history.
|
|
426
|
-
const bound =
|
|
440
|
+
const bound = resuming
|
|
441
|
+
? { conversationId: recovery.conversationId, reset: false, previousId: undefined }
|
|
442
|
+
: bindConversationId(sessionKey, { reset: resetState.reset || recovery?.kind === "rebase" });
|
|
427
443
|
const conversationId = bound.conversationId;
|
|
428
444
|
if (bound.reset) {
|
|
429
|
-
trace(`conversation reset: reason=${recovery ? "interrupted-run" : (resetState.reason ?? "unknown")} ` +
|
|
445
|
+
trace(`conversation reset: reason=${recovery?.kind === "rebase" ? "interrupted-run" : (resetState.reason ?? "unknown")} ` +
|
|
430
446
|
`sessionKey=${sessionKey ?? "(none)"} ` +
|
|
431
447
|
`previousId=${bound.previousId ?? "-"} → conversationId=${conversationId}`);
|
|
432
448
|
}
|
|
433
|
-
const userText = recovery
|
|
449
|
+
const userText = recovery?.kind === "rebase"
|
|
434
450
|
? "Continue the interrupted turn from the conversation history above. Do not repeat completed work."
|
|
435
451
|
: (extractUserText([...prompt].reverse().find((m) => m.role === "user")) || ".");
|
|
436
452
|
const workspaceRoot = path.resolve(options.workspaceRoot || process.cwd());
|
|
@@ -440,8 +456,8 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
440
456
|
? [baseSystemPrompt, interactionGuidance].filter(Boolean).join("\n\n")
|
|
441
457
|
: baseSystemPrompt;
|
|
442
458
|
const history = extractPromptHistory(prompt, {
|
|
443
|
-
preserveTrailingUser: recovery,
|
|
444
|
-
toolResults: isCompaction ? "all" : (recovery ? "trailing" : "omit"),
|
|
459
|
+
preserveTrailingUser: recovery?.kind === "rebase",
|
|
460
|
+
toolResults: isCompaction ? "all" : (recovery?.kind === "rebase" ? "trailing" : "omit"),
|
|
445
461
|
});
|
|
446
462
|
await loadAvailableModels();
|
|
447
463
|
// Resolve the region-specific Run stream origin once per process (memoized
|
|
@@ -485,7 +501,9 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
485
501
|
: [];
|
|
486
502
|
// CLI parity: echo the last conversation_checkpoint_update as conversation_state.
|
|
487
503
|
// After compaction reset there is no checkpoint — seed from OpenCode history.
|
|
488
|
-
const conversationState =
|
|
504
|
+
const conversationState = resuming
|
|
505
|
+
? recovery.checkpoint
|
|
506
|
+
: (bound.reset ? undefined : getCheckpoint(conversationId));
|
|
489
507
|
const reqBytes = buildRunRequest({
|
|
490
508
|
text: userText,
|
|
491
509
|
modelId: cursorModelId,
|
|
@@ -498,6 +516,7 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
498
516
|
tools,
|
|
499
517
|
toolDescriptors,
|
|
500
518
|
requestContext,
|
|
519
|
+
action: resuming ? "resume" : "user",
|
|
501
520
|
});
|
|
502
521
|
// Content hashes — Cursor content-addresses large payloads; logging these lets
|
|
503
522
|
// us match a server get_blob_args.blob_id to what it wants served.
|
|
@@ -523,6 +542,7 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
523
542
|
`availableModels=${_availableModels?.length ?? 0} userTextLen=${userText.length} ` +
|
|
524
543
|
`historyMsgs=${history.length} historyChars=${historyChars} ` +
|
|
525
544
|
`checkpointLen=${conversationState?.length ?? 0} reset=${bound.reset} ` +
|
|
545
|
+
`resume=${resuming} ` +
|
|
526
546
|
`usageEstimateIn=${usageEstimate.inputTokens} runRequestBytes=${reqBytes.length}`);
|
|
527
547
|
if (hooksCtx)
|
|
528
548
|
trace(`outbound Run hooks_additional_context: ${hooksCtx}`);
|
|
@@ -541,6 +561,7 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
541
561
|
const session = {
|
|
542
562
|
sessionId: crypto.randomUUID(),
|
|
543
563
|
conversationId,
|
|
564
|
+
resumeCheckpoint: undefined,
|
|
544
565
|
openCodeSessionId: sessionKey,
|
|
545
566
|
stream,
|
|
546
567
|
frames: stream.frames()[Symbol.asyncIterator](),
|
|
@@ -646,6 +667,7 @@ export function deliverContinuationResults(session, trailingToolResults) {
|
|
|
646
667
|
if (!pending.bridged) {
|
|
647
668
|
try {
|
|
648
669
|
const shellResult = pending.resultField === "shell_stream"
|
|
670
|
+
|| pending.resultField === "background_shell_spawn_result"
|
|
649
671
|
? consumeCursorShellResult(r.toolCallId, r.output)
|
|
650
672
|
: undefined;
|
|
651
673
|
frames = buildExecClientMessages({
|
|
@@ -795,7 +817,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
795
817
|
const advertisedToolNameSet = new Set(advertisedToolNames);
|
|
796
818
|
let textStarted = false;
|
|
797
819
|
let reasoningStarted = false;
|
|
798
|
-
|
|
820
|
+
const requestUsage = ids.requestUsage ?? { outputChars: 0 };
|
|
799
821
|
let replaySafe = true;
|
|
800
822
|
// OpenCode cancels the ReadableStream between turns (see the cancel handler
|
|
801
823
|
// in doStreamImpl). The frames iterator can still yield a final `done` after
|
|
@@ -871,9 +893,9 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
871
893
|
const editPath = typeof display.args.path === "string" ? display.args.path : "";
|
|
872
894
|
if (!requestedPath || !editPath)
|
|
873
895
|
return false;
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
896
|
+
// Prefer env.workspace_paths — project_folder / workspace_project_dir are
|
|
897
|
+
// Cursor metadata roots under <host-cache>/projects/, not the git tree.
|
|
898
|
+
const workspaceRoot = workspaceRootFromRequestContext(session.requestContext);
|
|
877
899
|
const resolvePath = (value) => path.resolve(workspaceRoot, value);
|
|
878
900
|
const absolutePath = resolvePath(requestedPath);
|
|
879
901
|
if (absolutePath !== resolvePath(editPath) || fs.existsSync(absolutePath))
|
|
@@ -923,7 +945,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
923
945
|
}
|
|
924
946
|
session.usageEstimate.outputTokens += estimateTokens(text.length);
|
|
925
947
|
if (safeEnqueue({ type: "text-delta", id: textId, delta: text })) {
|
|
926
|
-
|
|
948
|
+
requestUsage.outputChars += text.length;
|
|
927
949
|
}
|
|
928
950
|
};
|
|
929
951
|
const emitReasoning = (text) => {
|
|
@@ -936,7 +958,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
936
958
|
}
|
|
937
959
|
session.usageEstimate.outputTokens += estimateTokens(text.length);
|
|
938
960
|
if (safeEnqueue({ type: "reasoning-delta", id: reasoningId, delta: text })) {
|
|
939
|
-
|
|
961
|
+
requestUsage.outputChars += text.length;
|
|
940
962
|
}
|
|
941
963
|
};
|
|
942
964
|
const emitFinish = (te, reason) => {
|
|
@@ -959,7 +981,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
959
981
|
cacheWrite: 0,
|
|
960
982
|
},
|
|
961
983
|
outputTokens: {
|
|
962
|
-
total: estimateTokens(
|
|
984
|
+
total: estimateTokens(requestUsage.outputChars),
|
|
963
985
|
text: undefined,
|
|
964
986
|
reasoning: undefined,
|
|
965
987
|
},
|
|
@@ -1097,6 +1119,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1097
1119
|
const bytes = normalizeCheckpointBytes(checkpointRaw);
|
|
1098
1120
|
if (bytes && bytes.length > 0) {
|
|
1099
1121
|
setCheckpoint(session.conversationId, bytes);
|
|
1122
|
+
session.resumeCheckpoint = Uint8Array.from(bytes);
|
|
1100
1123
|
trace(`checkpoint: stored ${bytes.length}B for conversationId=${session.conversationId}`);
|
|
1101
1124
|
}
|
|
1102
1125
|
}
|
|
@@ -1202,6 +1225,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1202
1225
|
trace(`exec request_context hooks_additional_context: ${hooks}`);
|
|
1203
1226
|
}
|
|
1204
1227
|
try {
|
|
1228
|
+
traceRequestContextPaths(`exec request_context reply id=${esmId}`, session.requestContext);
|
|
1205
1229
|
session.stream.write(buildRequestContextResult(esmId, session.requestContext));
|
|
1206
1230
|
trace(`exec request_context: replied`);
|
|
1207
1231
|
}
|
|
@@ -1279,7 +1303,8 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1279
1303
|
trace(`exec: claimed display callId=${displayCallId}`);
|
|
1280
1304
|
}
|
|
1281
1305
|
const tc = buildToolCallPart(parsed, session.sessionId);
|
|
1282
|
-
if (parsed.resultField === "shell_stream"
|
|
1306
|
+
if (parsed.resultField === "shell_stream"
|
|
1307
|
+
|| parsed.resultField === "background_shell_spawn_result") {
|
|
1283
1308
|
registerCursorShellCall(tc.toolCallId, parsed.resultMetadata);
|
|
1284
1309
|
}
|
|
1285
1310
|
// Keep the stream open; the result arrives on the next doStream call.
|
package/dist/plugin-v2.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { define } from "@opencode-ai/plugin/v2/promise";
|
|
2
2
|
import { CURSOR_PROVIDER_ID } from "./shared.js";
|
|
3
3
|
import { createCursorLanguageModel } from "./language-model.js";
|
|
4
|
+
import { adoptCompatHostCacheDir } from "./context/paths.js";
|
|
4
5
|
/**
|
|
5
6
|
* OpenCode Effect / Promise v2 plugin.
|
|
6
7
|
*
|
|
@@ -25,6 +26,9 @@ function createSdk(options) {
|
|
|
25
26
|
export default define({
|
|
26
27
|
id: "cursor.provider",
|
|
27
28
|
setup: async (ctx) => {
|
|
29
|
+
// Prefer OCP HostProfile.cacheDir when present; else XDG host heuristic.
|
|
30
|
+
// Explicit createCursor({ cacheDir }) / event.options.cacheDir still wins via LM.
|
|
31
|
+
await adoptCompatHostCacheDir();
|
|
28
32
|
await ctx.aisdk.sdk((event) => {
|
|
29
33
|
if (event.sdk)
|
|
30
34
|
return;
|
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
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
|
-
import { opencodeGlobalCacheDir } from "./context/paths.js";
|
|
4
|
+
import { adoptCompatHostCacheDir, 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, setCursorShellPath, } 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
|
/**
|
|
@@ -194,6 +194,8 @@ function cursorGetServerConfigTelemetryEnabled() {
|
|
|
194
194
|
process.env.CURSOR_GET_SERVER_CONFIG_TELEMETRY === "true");
|
|
195
195
|
}
|
|
196
196
|
export async function CursorPlugin(input) {
|
|
197
|
+
// Prefer OCP HostProfile.cacheDir when the compat package is present; else XDG heuristic.
|
|
198
|
+
await adoptCompatHostCacheDir();
|
|
197
199
|
const cacheDir = opencodeGlobalCacheDir();
|
|
198
200
|
const apiBaseURL = cursorApiBaseURL();
|
|
199
201
|
// Last access token successfully resolved in this plugin instance. Config's
|
|
@@ -336,13 +338,35 @@ export async function CursorPlugin(input) {
|
|
|
336
338
|
async "tool.execute.before"(hookInput, output) {
|
|
337
339
|
if (hookInput.tool !== "bash")
|
|
338
340
|
return;
|
|
341
|
+
// bash/zsh retain the original display/permission command and wrap via
|
|
342
|
+
// shell.env. sh/dash need a short wrapper-file command because their
|
|
343
|
+
// non-interactive `-c` path ignores BASH_ENV / ZDOTDIR.
|
|
339
344
|
prepareCursorShellArgs(hookInput.callID, output.args);
|
|
340
345
|
},
|
|
346
|
+
async "shell.env"(hookInput, output) {
|
|
347
|
+
const env = cursorShellEnvForCall(hookInput.callID);
|
|
348
|
+
if (!env)
|
|
349
|
+
return;
|
|
350
|
+
Object.assign(output.env, env);
|
|
351
|
+
},
|
|
341
352
|
async "tool.execute.after"(hookInput, output) {
|
|
342
353
|
if (hookInput.tool !== "bash")
|
|
343
354
|
return;
|
|
344
|
-
|
|
345
|
-
|
|
355
|
+
try {
|
|
356
|
+
output.title = cursorShellOriginalCommand(hookInput.callID) ?? output.title;
|
|
357
|
+
output.output = captureCursorShellResult(hookInput.callID, output.output, output.metadata);
|
|
358
|
+
// OpenCode's bash GUI falls back to metadata.output when output is empty
|
|
359
|
+
// (`props.output || props.metadata.output`), so strip private markers there too.
|
|
360
|
+
if (output.metadata && typeof output.metadata === "object") {
|
|
361
|
+
const metadata = output.metadata;
|
|
362
|
+
if (typeof metadata.output === "string") {
|
|
363
|
+
metadata.output = sanitizeRegisteredCursorShellOutput(hookInput.callID, metadata.output);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
finally {
|
|
368
|
+
releaseCursorShellEnv(hookInput.callID);
|
|
369
|
+
}
|
|
346
370
|
},
|
|
347
371
|
async "chat.params"(hookInput, output) {
|
|
348
372
|
if (hookInput.model.providerID !== CURSOR_PROVIDER_ID)
|
|
@@ -355,6 +379,7 @@ export async function CursorPlugin(input) {
|
|
|
355
379
|
}
|
|
356
380
|
},
|
|
357
381
|
async config(cfg) {
|
|
382
|
+
setCursorShellPath(cfg.shell);
|
|
358
383
|
cfg.provider ??= {};
|
|
359
384
|
const models = await loadModels();
|
|
360
385
|
const existing = cfg.provider[CURSOR_PROVIDER_ID];
|
|
@@ -464,6 +489,7 @@ export async function CursorPlugin(input) {
|
|
|
464
489
|
return {
|
|
465
490
|
...(accessToken ? { accessToken } : {}),
|
|
466
491
|
workspaceRoot: input.directory,
|
|
492
|
+
cacheDir,
|
|
467
493
|
};
|
|
468
494
|
},
|
|
469
495
|
},
|
|
@@ -653,8 +653,10 @@ export function createMessageTypes() {
|
|
|
653
653
|
{ id: 2, name: "workspace_paths", type: "string", repeated: true },
|
|
654
654
|
{ id: 3, name: "shell", type: "string" },
|
|
655
655
|
{ id: 5, name: "sandbox_enabled", type: "bool" },
|
|
656
|
+
{ id: 7, name: "terminals_folder", type: "string" },
|
|
656
657
|
{ id: 10, name: "time_zone", type: "string" },
|
|
657
658
|
{ id: 11, name: "project_folder", type: "string" },
|
|
659
|
+
{ id: 12, name: "agent_transcripts_folder", type: "string" },
|
|
658
660
|
{ id: 14, name: "sandbox_supported", type: "bool" },
|
|
659
661
|
{ id: 20, name: "is_working_dir_home_dir", type: "bool" },
|
|
660
662
|
{ id: 21, name: "process_working_directory", type: "string" },
|
|
@@ -903,7 +905,9 @@ export function createMessageTypes() {
|
|
|
903
905
|
{ id: 2, name: "request_context", type: "RequestContext" },
|
|
904
906
|
]);
|
|
905
907
|
addType(root, "ResumeAction", [
|
|
906
|
-
|
|
908
|
+
// Cursor CLI sends an empty ResumeAction. Field #2 is available for a
|
|
909
|
+
// refreshed RequestContext; the conversation id belongs to AgentRunRequest.
|
|
910
|
+
{ id: 2, name: "request_context", type: "RequestContext" },
|
|
907
911
|
]);
|
|
908
912
|
addType(root, "CancelAction", [
|
|
909
913
|
{ id: 1, name: "conversation_id", type: "string" },
|
|
@@ -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
|
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;
|
package/dist/protocol/tools.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import { encodeMessage } from "./messages.js";
|
|
3
3
|
import { encodeJsonAsValue, decodeStructEntriesToJson, readAllFields } from "./struct.js";
|
|
4
|
-
import {
|
|
4
|
+
import { buildEnv } from "../context/env.js";
|
|
5
|
+
import { ensureOpencodeProjectDir } from "../context/paths.js";
|
|
6
|
+
import { trace, traceRequestContextPaths } from "../debug.js";
|
|
5
7
|
import { cursorExecVariantByRequestName } from "./exec-variants.js";
|
|
8
|
+
import { BACKGROUND_SHELL_MARKER, buildBackgroundShellCommand, } from "../shell-timeout.js";
|
|
6
9
|
// Exec variant field number whose reply is the server-initiated request_context
|
|
7
10
|
// probe (ExecServerMessage #10 → ExecClientMessage #10). request/result share a
|
|
8
11
|
// field number for every exec variant, so this is also the result field.
|
|
@@ -103,19 +106,12 @@ export function buildLiveRequestContext(tools, providerIdentifier = "opencode",
|
|
|
103
106
|
const flat = toolsToDescriptors(tools, providerIdentifier, knownMcpServers);
|
|
104
107
|
const nested = toolsToMcpDescriptors(tools, providerIdentifier, knownMcpServers);
|
|
105
108
|
const cwd = process.cwd();
|
|
106
|
-
|
|
107
|
-
env:
|
|
108
|
-
os_version: process.platform,
|
|
109
|
-
workspace_paths: [cwd],
|
|
110
|
-
shell: process.env.SHELL || "/bin/bash",
|
|
111
|
-
time_zone: "UTC",
|
|
112
|
-
project_folder: cwd,
|
|
113
|
-
process_working_directory: cwd,
|
|
114
|
-
},
|
|
109
|
+
const ctx = {
|
|
110
|
+
env: buildEnv(cwd),
|
|
115
111
|
tools: flat,
|
|
116
112
|
mcp_file_system_options: {
|
|
117
113
|
enabled: true,
|
|
118
|
-
workspace_project_dir: cwd,
|
|
114
|
+
workspace_project_dir: ensureOpencodeProjectDir(cwd),
|
|
119
115
|
mcp_descriptors: nested,
|
|
120
116
|
},
|
|
121
117
|
mcp_meta_tool_options: {
|
|
@@ -128,6 +124,8 @@ export function buildLiveRequestContext(tools, providerIdentifier = "opencode",
|
|
|
128
124
|
mcp_file_system_info_complete: true,
|
|
129
125
|
git_status_info_complete: true,
|
|
130
126
|
};
|
|
127
|
+
traceRequestContextPaths("buildLiveRequestContext", ctx);
|
|
128
|
+
return ctx;
|
|
131
129
|
}
|
|
132
130
|
// ── Cursor exec-variant → opencode tool name ──
|
|
133
131
|
// Native ExecServerMessage variants only (agent.v1). There is no edit_args or
|
|
@@ -137,7 +135,7 @@ export function buildLiveRequestContext(tools, providerIdentifier = "opencode",
|
|
|
137
135
|
// Design note (F11): Cursor has native delete / background-shell tools; OpenCode
|
|
138
136
|
// does not. This provider intentionally remaps:
|
|
139
137
|
// - delete_args → bash `rm -f -- <quoted-path>`
|
|
140
|
-
// - background_shell_spawn_args → bash
|
|
138
|
+
// - background_shell_spawn_args → bash (original command; plugin wraps nohup)
|
|
141
139
|
// Permissions still flow through OpenCode's advertised `bash` tool. Soft-
|
|
142
140
|
// background / detached children can outlive the OpenCode tool call; leftover
|
|
143
141
|
// process cleanup is left to the user / OS. Arg key remapping happens below.
|
|
@@ -213,7 +211,6 @@ export function mapExecServerToToolName(execField) {
|
|
|
213
211
|
export function mapToolNameToExecField(toolName) {
|
|
214
212
|
return opencodeToolToCursor[toolName];
|
|
215
213
|
}
|
|
216
|
-
const BACKGROUND_SHELL_MARKER = "__CURSOR_BACKGROUND_SHELL__";
|
|
217
214
|
export function parseExecServerMessage(msg) {
|
|
218
215
|
const id = msg.id;
|
|
219
216
|
if (id === undefined)
|
|
@@ -235,9 +232,10 @@ export function parseExecServerMessage(msg) {
|
|
|
235
232
|
if (!resultField)
|
|
236
233
|
return undefined;
|
|
237
234
|
const execId = msg.exec_id ?? "";
|
|
238
|
-
// F11: Cursor native background shell → OpenCode bash.
|
|
239
|
-
//
|
|
240
|
-
//
|
|
235
|
+
// F11: Cursor native background shell → OpenCode bash. Keep the wrapper
|
|
236
|
+
// self-contained so direct provider / hosts without shell.env still detach
|
|
237
|
+
// and return a PID. The classic plugin replaces this with its display-safe
|
|
238
|
+
// env or wrapper-file path before execution.
|
|
241
239
|
if (execVariant === "background_shell_spawn_args") {
|
|
242
240
|
const raw = msg.background_shell_spawn_args ?? {};
|
|
243
241
|
const command = str(raw.command);
|
|
@@ -253,7 +251,11 @@ export function parseExecServerMessage(msg) {
|
|
|
253
251
|
toolName: "bash",
|
|
254
252
|
args,
|
|
255
253
|
resultField,
|
|
256
|
-
resultMetadata: {
|
|
254
|
+
resultMetadata: {
|
|
255
|
+
background_shell_spawn: true,
|
|
256
|
+
command: command ?? "",
|
|
257
|
+
working_directory: workingDirectory,
|
|
258
|
+
},
|
|
257
259
|
localError: raw.enable_write_shell_stdin_tool === true
|
|
258
260
|
? "Interactive background shells are not available through OpenCode's bash tool."
|
|
259
261
|
: command
|
|
@@ -540,25 +542,6 @@ function mapCursorSubagentTypeToOpenCode(subagentType) {
|
|
|
540
542
|
function shellQuote(s) {
|
|
541
543
|
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
542
544
|
}
|
|
543
|
-
/**
|
|
544
|
-
* F11 / background_shell_spawn_args helper.
|
|
545
|
-
*
|
|
546
|
-
* OpenCode's bash tool is foreground-only. Detach the requested command inside
|
|
547
|
-
* that one foreground call (`nohup … &`) and print a private marker containing
|
|
548
|
-
* the spawned PID and log path. With stdin and all output redirected, the host
|
|
549
|
-
* shell can return immediately instead of retaining OpenCode's tool pipe.
|
|
550
|
-
*
|
|
551
|
-
* Residual: the detached child is not reaped by this provider after OpenCode
|
|
552
|
-
* completes the tool call; cleanup is left to the user / OS.
|
|
553
|
-
*/
|
|
554
|
-
function buildBackgroundShellCommand(command) {
|
|
555
|
-
return [
|
|
556
|
-
'bg_log="$(mktemp "${TMPDIR:-/tmp}/cursor-opencode-bg.XXXXXX")" || exit 1',
|
|
557
|
-
`nohup sh -c ${shellQuote(command)} >"$bg_log" 2>&1 </dev/null &`,
|
|
558
|
-
"bg_pid=$!",
|
|
559
|
-
`printf '${BACKGROUND_SHELL_MARKER}%s:%s\\n' "$bg_pid" "$bg_log"`,
|
|
560
|
-
].join("\n");
|
|
561
|
-
}
|
|
562
545
|
/**
|
|
563
546
|
* Map Cursor McpArgs back to the OpenCode tool id.
|
|
564
547
|
* Prefers provider_identifier + bare tool_name (github + create_pull_request
|
|
@@ -669,7 +652,7 @@ export function buildExecClientMessages(input) {
|
|
|
669
652
|
id: input.execId,
|
|
670
653
|
local_execution_time_ms: input.executionTimeMs ?? 0,
|
|
671
654
|
};
|
|
672
|
-
clientMsg[resultField] = buildTypedExecResult(resultField, input.output, input.error, input.toolName, input.resultMetadata);
|
|
655
|
+
clientMsg[resultField] = buildTypedExecResult(resultField, input.output, input.error, input.toolName, input.resultMetadata, input.shellOutcome);
|
|
673
656
|
frames.push(encodeMessage("AgentClientMessage", {
|
|
674
657
|
exec_client_message: clientMsg,
|
|
675
658
|
}));
|
|
@@ -756,7 +739,7 @@ export function unwrapReadOutput(output) {
|
|
|
756
739
|
* OpenCode returns free-form text; we wrap it in the minimal success shape the
|
|
757
740
|
* server accepts (verified against agent.v1 wire captures).
|
|
758
741
|
*/
|
|
759
|
-
export function buildTypedExecResult(resultField, output, error, toolName, resultMetadata) {
|
|
742
|
+
export function buildTypedExecResult(resultField, output, error, toolName, resultMetadata, shellOutcome) {
|
|
760
743
|
switch (resultField) {
|
|
761
744
|
case "read_result": {
|
|
762
745
|
const readPath = str(resultMetadata?.path) ?? extractPathTag(output) ?? "";
|
|
@@ -841,6 +824,19 @@ export function buildTypedExecResult(resultField, output, error, toolName, resul
|
|
|
841
824
|
const workingDirectory = str(resultMetadata?.working_directory) ?? "";
|
|
842
825
|
if (error)
|
|
843
826
|
return { error: { command, working_directory: workingDirectory, error } };
|
|
827
|
+
// Prefer the structured outcome captured by the plugin after-hook; markers
|
|
828
|
+
// are already stripped from the stored/rendered OpenCode output by then.
|
|
829
|
+
if (shellOutcome?.kind === "backgrounded") {
|
|
830
|
+
return {
|
|
831
|
+
success: {
|
|
832
|
+
shell_id: shellOutcome.shellId,
|
|
833
|
+
command: shellOutcome.command || command,
|
|
834
|
+
working_directory: shellOutcome.workingDirectory || workingDirectory,
|
|
835
|
+
pid: shellOutcome.pid,
|
|
836
|
+
},
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
// Fallback when no plugin hook ran: parse the private spawn marker inline.
|
|
844
840
|
const match = new RegExp(`${BACKGROUND_SHELL_MARKER}(\\d+):([^\\r\\n]+)`).exec(output);
|
|
845
841
|
const pid = match ? Number(match[1]) : 0;
|
|
846
842
|
if (!Number.isSafeInteger(pid) || pid <= 0 || pid > 0xffff_ffff) {
|
|
@@ -1114,6 +1110,7 @@ export function detectExecVariantField(agentServerPayload) {
|
|
|
1114
1110
|
* Encode exec #10 request_context_result from a prebuilt RequestContext payload.
|
|
1115
1111
|
*/
|
|
1116
1112
|
export function buildRequestContextResult(execId, requestContext) {
|
|
1113
|
+
traceRequestContextPaths(`buildRequestContextResult id=${execId}`, requestContext);
|
|
1117
1114
|
return encodeMessage("AgentClientMessage", {
|
|
1118
1115
|
exec_client_message: {
|
|
1119
1116
|
id: execId,
|
package/dist/session.d.ts
CHANGED
|
@@ -98,6 +98,8 @@ export type CursorSession = {
|
|
|
98
98
|
* conversation_checkpoint_update (CLI parity).
|
|
99
99
|
*/
|
|
100
100
|
conversationId: string;
|
|
101
|
+
/** Latest eligible checkpoint emitted by this Run attempt. */
|
|
102
|
+
resumeCheckpoint?: Uint8Array;
|
|
101
103
|
/** OpenCode session whose own or descendant activity renews tool leases. */
|
|
102
104
|
openCodeSessionId?: string;
|
|
103
105
|
stream: BidiStream;
|