cursor-opencode-provider 0.2.6 → 0.2.8
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 +31 -15
- package/dist/agent-url.d.ts +8 -5
- package/dist/auth.js +59 -27
- package/dist/context/build.js +12 -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 +56 -3
- package/dist/context/paths.js +135 -9
- package/dist/deadline.d.ts +8 -0
- package/dist/deadline.js +25 -0
- package/dist/debug.d.ts +7 -0
- package/dist/debug.js +42 -0
- package/dist/index.d.ts +6 -0
- package/dist/language-model.js +133 -74
- package/dist/models.d.ts +2 -0
- package/dist/plugin-v2.js +4 -0
- package/dist/plugin.js +16 -5
- package/dist/protocol/client-version.js +7 -6
- package/dist/protocol/messages.js +6 -0
- package/dist/protocol/struct.d.ts +18 -0
- package/dist/protocol/struct.js +82 -0
- package/dist/protocol/tools.d.ts +18 -0
- package/dist/protocol/tools.js +89 -21
- package/dist/replay-safety.d.ts +23 -0
- package/dist/replay-safety.js +126 -0
- package/dist/session.d.ts +3 -0
- package/dist/shell-timeout.d.ts +15 -10
- package/dist/shell-timeout.js +91 -54
- 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 +9 -3
package/dist/deadline.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bound a complete async operation, not only fetch(). Response body readers can
|
|
3
|
+
* ignore abort signals, so Promise.race remains the authoritative deadline.
|
|
4
|
+
*
|
|
5
|
+
* Rejects with `timeoutError()` before aborting so callers observe the domain
|
|
6
|
+
* timeout error rather than a generic AbortError.
|
|
7
|
+
*/
|
|
8
|
+
export async function withAbortDeadline(timeoutMs, timeoutError, run) {
|
|
9
|
+
const controller = new AbortController();
|
|
10
|
+
let timer;
|
|
11
|
+
const deadline = new Promise((_, reject) => {
|
|
12
|
+
timer = setTimeout(() => {
|
|
13
|
+
reject(timeoutError());
|
|
14
|
+
controller.abort();
|
|
15
|
+
}, timeoutMs);
|
|
16
|
+
timer.unref?.();
|
|
17
|
+
});
|
|
18
|
+
try {
|
|
19
|
+
return await Promise.race([run(controller.signal), deadline]);
|
|
20
|
+
}
|
|
21
|
+
finally {
|
|
22
|
+
if (timer)
|
|
23
|
+
clearTimeout(timer);
|
|
24
|
+
}
|
|
25
|
+
}
|
package/dist/debug.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** Whether `CURSOR_PROVIDER_DEBUG` is enabled for this process. */
|
|
2
|
+
export declare function isDebugEnabled(): boolean;
|
|
1
3
|
/** Resolve the debug log path (env override or per-uid tmpdir default). */
|
|
2
4
|
export declare function resolveDebugLogPath(): string;
|
|
3
5
|
/**
|
|
@@ -8,3 +10,8 @@ export declare function ensureSecureDebugLog(filePath: string, options?: {
|
|
|
8
10
|
secureParent?: boolean;
|
|
9
11
|
}): void;
|
|
10
12
|
export declare function trace(msg: string): void;
|
|
13
|
+
/**
|
|
14
|
+
* Compact path-advertising summary for RequestContext troubleshooting.
|
|
15
|
+
* Safe: no tokens / file contents — only workspace vs metadata roots.
|
|
16
|
+
*/
|
|
17
|
+
export declare function traceRequestContextPaths(label: string, requestContext: Record<string, unknown> | undefined): void;
|
package/dist/debug.js
CHANGED
|
@@ -10,6 +10,11 @@ const DEBUG_ENABLED = process.env.CURSOR_PROVIDER_DEBUG === "1" ||
|
|
|
10
10
|
let _traceInitialized = false;
|
|
11
11
|
let _debugFile;
|
|
12
12
|
let _debugFileUsesManagedDirectory = false;
|
|
13
|
+
let _announcedLogPath = false;
|
|
14
|
+
/** Whether `CURSOR_PROVIDER_DEBUG` is enabled for this process. */
|
|
15
|
+
export function isDebugEnabled() {
|
|
16
|
+
return DEBUG_ENABLED;
|
|
17
|
+
}
|
|
13
18
|
/** Resolve the debug log path (env override or per-uid tmpdir default). */
|
|
14
19
|
export function resolveDebugLogPath() {
|
|
15
20
|
if (process.env.CURSOR_PROVIDER_DEBUG_FILE) {
|
|
@@ -41,6 +46,18 @@ export function ensureSecureDebugLog(filePath, options = {}) {
|
|
|
41
46
|
fs.writeFileSync(filePath, "", { mode: 0o600 });
|
|
42
47
|
fs.chmodSync(filePath, 0o600);
|
|
43
48
|
}
|
|
49
|
+
function announceLogPath(filePath) {
|
|
50
|
+
if (_announcedLogPath)
|
|
51
|
+
return;
|
|
52
|
+
_announcedLogPath = true;
|
|
53
|
+
try {
|
|
54
|
+
// Visible in the OpenCode / terminal session so operators know where to look.
|
|
55
|
+
console.error(`[cursor-provider] CURSOR_PROVIDER_DEBUG logging to ${filePath}`);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
/* ignore */
|
|
59
|
+
}
|
|
60
|
+
}
|
|
44
61
|
export function trace(msg) {
|
|
45
62
|
if (!DEBUG_ENABLED)
|
|
46
63
|
return;
|
|
@@ -53,6 +70,10 @@ export function trace(msg) {
|
|
|
53
70
|
ensureSecureDebugLog(_debugFile, { secureParent: _debugFileUsesManagedDirectory });
|
|
54
71
|
fs.writeFileSync(_debugFile, `--- cursor-provider debug (pid ${process.pid}) ${new Date().toISOString()} ---\n`, { mode: 0o600 });
|
|
55
72
|
_traceInitialized = true;
|
|
73
|
+
announceLogPath(_debugFile);
|
|
74
|
+
fs.appendFileSync(_debugFile, `[${new Date().toISOString()}] debug: enabled file=${_debugFile} ` +
|
|
75
|
+
`xdg_cache_home=${process.env.XDG_CACHE_HOME ?? "(unset)"} ` +
|
|
76
|
+
`cwd=${process.cwd()}\n`);
|
|
56
77
|
}
|
|
57
78
|
fs.appendFileSync(_debugFile, `[${new Date().toISOString()}] ${msg}\n`);
|
|
58
79
|
}
|
|
@@ -60,3 +81,24 @@ export function trace(msg) {
|
|
|
60
81
|
/* ignore */
|
|
61
82
|
}
|
|
62
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Compact path-advertising summary for RequestContext troubleshooting.
|
|
86
|
+
* Safe: no tokens / file contents — only workspace vs metadata roots.
|
|
87
|
+
*/
|
|
88
|
+
export function traceRequestContextPaths(label, requestContext) {
|
|
89
|
+
if (!DEBUG_ENABLED)
|
|
90
|
+
return;
|
|
91
|
+
const env = requestContext?.env && typeof requestContext.env === "object"
|
|
92
|
+
? requestContext.env
|
|
93
|
+
: undefined;
|
|
94
|
+
const mcp = requestContext?.mcp_file_system_options &&
|
|
95
|
+
typeof requestContext.mcp_file_system_options === "object"
|
|
96
|
+
? requestContext.mcp_file_system_options
|
|
97
|
+
: undefined;
|
|
98
|
+
trace(`${label}: workspace_paths=${JSON.stringify(env?.workspace_paths ?? null)} ` +
|
|
99
|
+
`process_working_directory=${JSON.stringify(env?.process_working_directory ?? null)} ` +
|
|
100
|
+
`project_folder=${JSON.stringify(env?.project_folder ?? null)} ` +
|
|
101
|
+
`terminals_folder=${JSON.stringify(env?.terminals_folder ?? null)} ` +
|
|
102
|
+
`agent_transcripts_folder=${JSON.stringify(env?.agent_transcripts_folder ?? null)} ` +
|
|
103
|
+
`workspace_project_dir=${JSON.stringify(mcp?.workspace_project_dir ?? null)}`);
|
|
104
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -23,6 +23,12 @@ export type CreateCursorOptions = {
|
|
|
23
23
|
telemetryEnabled?: boolean;
|
|
24
24
|
/** OpenCode project / worktree directory for request_context collectors. */
|
|
25
25
|
workspaceRoot?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Host cache root for Cursor project metadata + model/version caches.
|
|
28
|
+
* Prefer the host's Path.cache (Effect v2) when available; otherwise the
|
|
29
|
+
* provider resolves OpenCode / MiMo / Kilo XDG cache dirs automatically.
|
|
30
|
+
*/
|
|
31
|
+
cacheDir?: string;
|
|
26
32
|
/** Held-stream policy. Defaults: heartbeat 5s, semantic idle 120s, tool inactivity 10m. */
|
|
27
33
|
continuation?: CursorContinuationOptions;
|
|
28
34
|
/** Fresh-turn retry policy. Defaults: 3 attempts, 500ms base, 8000ms cap. */
|
package/dist/language-model.js
CHANGED
|
@@ -2,15 +2,15 @@ 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";
|
|
9
|
-
import { parseExecServerMessage, buildToolCallPart, buildExecClientMessages, parseExecIdFromToolCallId, detectExecVariantField, buildRequestContextResult, buildMcpStateResult, } from "./protocol/tools.js";
|
|
9
|
+
import { parseExecServerMessage, buildToolCallPart, buildExecClientMessages, parseExecIdFromToolCallId, detectExecVariantField, buildRequestContextResult, buildMcpStateResult, buildCustomWebToolAliases, resolveCustomWebToolAlias, 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";
|
|
@@ -18,10 +18,13 @@ 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";
|
|
26
|
+
import { analyzeReplayFrame, AttemptReplaySafety } from "./replay-safety.js";
|
|
27
|
+
import { readAllFieldsStrict } from "./protocol/struct.js";
|
|
25
28
|
let _availableModels;
|
|
26
29
|
// mtime of the cache file the last time we loaded it. Compared on each call
|
|
27
30
|
// so discoverModels' background refresh is picked up without a process restart.
|
|
@@ -43,6 +46,28 @@ const DEFAULT_RETRY_POLICY = {
|
|
|
43
46
|
};
|
|
44
47
|
const MAX_RETRY_ATTEMPTS = 10;
|
|
45
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
|
+
}
|
|
46
71
|
function retryInteger(name, value, fallback) {
|
|
47
72
|
const resolved = value === undefined ? fallback : value;
|
|
48
73
|
if (typeof resolved !== "number" || !Number.isSafeInteger(resolved) || resolved <= 0) {
|
|
@@ -224,6 +249,9 @@ function rememberPostCompactionRebase(sessionKey) {
|
|
|
224
249
|
}
|
|
225
250
|
}
|
|
226
251
|
export function createCursorLanguageModel(modelId, providerId, options) {
|
|
252
|
+
// Host Path.cache / explicit override wins over XDG heuristics for this process.
|
|
253
|
+
if (options.cacheDir)
|
|
254
|
+
setHostCacheDirOverride(options.cacheDir);
|
|
227
255
|
return {
|
|
228
256
|
specificationVersion: "v3",
|
|
229
257
|
provider: providerId,
|
|
@@ -426,6 +454,14 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
426
454
|
isCompaction,
|
|
427
455
|
});
|
|
428
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
|
+
}
|
|
429
465
|
const allowTools = toolState.allowTools;
|
|
430
466
|
const resetState = resolveTurnConversationReset({ sessionKey, isCompaction });
|
|
431
467
|
const recovery = startOptions?.recovery;
|
|
@@ -447,7 +483,7 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
447
483
|
: (extractUserText([...prompt].reverse().find((m) => m.role === "user")) || ".");
|
|
448
484
|
const workspaceRoot = path.resolve(options.workspaceRoot || process.cwd());
|
|
449
485
|
const baseSystemPrompt = extractSystemPrompt(prompt);
|
|
450
|
-
const interactionGuidance = buildOpenCodeInteractionGuidance(
|
|
486
|
+
const interactionGuidance = buildOpenCodeInteractionGuidance(cursorTools, isCompaction, workspaceRoot);
|
|
451
487
|
const systemPrompt = interactionGuidance
|
|
452
488
|
? [baseSystemPrompt, interactionGuidance].filter(Boolean).join("\n\n")
|
|
453
489
|
: baseSystemPrompt;
|
|
@@ -489,7 +525,7 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
489
525
|
baseURL: agentBaseUrl,
|
|
490
526
|
headers: options.headers,
|
|
491
527
|
});
|
|
492
|
-
const requestContext = await buildRequestContext({ workspaceRoot, tools });
|
|
528
|
+
const requestContext = await buildRequestContext({ workspaceRoot, tools: cursorTools });
|
|
493
529
|
// Resolve descriptors once from the merged OpenCode config so MCP identity is
|
|
494
530
|
// consistent across AgentRunRequest and both request_context reply paths.
|
|
495
531
|
const toolDescriptors = Array.isArray(requestContext.tools)
|
|
@@ -509,7 +545,7 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
509
545
|
conversationState,
|
|
510
546
|
parameterValues,
|
|
511
547
|
maxMode,
|
|
512
|
-
tools,
|
|
548
|
+
tools: cursorTools,
|
|
513
549
|
toolDescriptors,
|
|
514
550
|
requestContext,
|
|
515
551
|
action: resuming ? "resume" : "user",
|
|
@@ -566,6 +602,7 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
566
602
|
nextBridgedExecId: 900_000,
|
|
567
603
|
blobs: new Map(),
|
|
568
604
|
toolDescriptors,
|
|
605
|
+
toolAliases: webToolAliases.aliases,
|
|
569
606
|
requestContext,
|
|
570
607
|
allowTools,
|
|
571
608
|
usageEstimate,
|
|
@@ -810,11 +847,17 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
810
847
|
const { textId, reasoningId } = ids;
|
|
811
848
|
const promptTokens = ids.promptTokens ?? 0;
|
|
812
849
|
const advertisedToolNames = advertisedToolNamesFromDescriptors(session.toolDescriptors);
|
|
813
|
-
const advertisedToolNameSet = new Set(advertisedToolNames);
|
|
850
|
+
const advertisedToolNameSet = new Set(advertisedToolNames.map((name) => resolveCustomWebToolAlias(name, session.toolAliases)));
|
|
814
851
|
let textStarted = false;
|
|
815
852
|
let reasoningStarted = false;
|
|
816
853
|
const requestUsage = ids.requestUsage ?? { outputChars: 0 };
|
|
817
|
-
|
|
854
|
+
const replaySafety = new AttemptReplaySafety(session.sessionId);
|
|
855
|
+
const failRunProtocol = (message, code) => {
|
|
856
|
+
replaySafety.markBarrier("unknown-or-malformed-frame");
|
|
857
|
+
const error = new CursorProtocolError(message, { code });
|
|
858
|
+
sessionManager.close(session, "remote-error", error);
|
|
859
|
+
throw error;
|
|
860
|
+
};
|
|
818
861
|
// OpenCode cancels the ReadableStream between turns (see the cancel handler
|
|
819
862
|
// in doStreamImpl). The frames iterator can still yield a final `done` after
|
|
820
863
|
// the cancel lands — controller.enqueue on a cancelled controller throws.
|
|
@@ -889,9 +932,9 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
889
932
|
const editPath = typeof display.args.path === "string" ? display.args.path : "";
|
|
890
933
|
if (!requestedPath || !editPath)
|
|
891
934
|
return false;
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
935
|
+
// Prefer env.workspace_paths — project_folder / workspace_project_dir are
|
|
936
|
+
// Cursor metadata roots under <host-cache>/projects/, not the git tree.
|
|
937
|
+
const workspaceRoot = workspaceRootFromRequestContext(session.requestContext);
|
|
895
938
|
const resolvePath = (value) => path.resolve(workspaceRoot, value);
|
|
896
939
|
const absolutePath = resolvePath(requestedPath);
|
|
897
940
|
if (absolutePath !== resolvePath(editPath) || fs.existsSync(absolutePath))
|
|
@@ -929,7 +972,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
929
972
|
const emitText = (text) => {
|
|
930
973
|
if (!text)
|
|
931
974
|
return;
|
|
932
|
-
|
|
975
|
+
replaySafety.markBarrier("visible-text");
|
|
933
976
|
// Close reasoning before text (hosts expect reasoning-end before text-start).
|
|
934
977
|
if (reasoningStarted && !textStarted) {
|
|
935
978
|
safeEnqueue({ type: "reasoning-end", id: reasoningId });
|
|
@@ -947,7 +990,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
947
990
|
const emitReasoning = (text) => {
|
|
948
991
|
if (!text)
|
|
949
992
|
return;
|
|
950
|
-
|
|
993
|
+
replaySafety.markBarrier("visible-reasoning");
|
|
951
994
|
if (!reasoningStarted) {
|
|
952
995
|
safeEnqueue({ type: "reasoning-start", id: reasoningId });
|
|
953
996
|
reasoningStarted = true;
|
|
@@ -1025,15 +1068,13 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1025
1068
|
const failure = error instanceof CursorProviderError
|
|
1026
1069
|
? error
|
|
1027
1070
|
: new CursorRunInterruptedError(`Cursor Run frame stream interrupted: ${error.message}`, { cause: error });
|
|
1028
|
-
|
|
1029
|
-
throw failure;
|
|
1071
|
+
throw replaySafety.applyTo(failure);
|
|
1030
1072
|
}
|
|
1031
1073
|
if (next.done) {
|
|
1032
1074
|
closeOpenSpans();
|
|
1033
1075
|
trace("pump: frames iterator ended before turn_ended");
|
|
1034
1076
|
const failure = new CursorRunInterruptedError();
|
|
1035
|
-
failure
|
|
1036
|
-
throw failure;
|
|
1077
|
+
throw replaySafety.applyTo(failure);
|
|
1037
1078
|
}
|
|
1038
1079
|
const frame = next.value;
|
|
1039
1080
|
if (frame.flags & 0x02) {
|
|
@@ -1045,14 +1086,15 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1045
1086
|
try {
|
|
1046
1087
|
payload = new TextDecoder().decode(decodeFramePayload(frame));
|
|
1047
1088
|
}
|
|
1048
|
-
catch {
|
|
1089
|
+
catch {
|
|
1090
|
+
replaySafety.markBarrier("unknown-or-malformed-frame");
|
|
1091
|
+
}
|
|
1049
1092
|
}
|
|
1050
1093
|
closeOpenSpans();
|
|
1051
1094
|
const failure = payload
|
|
1052
1095
|
? connectFrameError(payload)
|
|
1053
1096
|
: new CursorRunInterruptedError();
|
|
1054
|
-
|
|
1055
|
-
throw failure;
|
|
1097
|
+
throw replaySafety.applyTo(failure);
|
|
1056
1098
|
}
|
|
1057
1099
|
// decodeFramePayload can throw on a corrupt gzip payload (gunzipSync).
|
|
1058
1100
|
// Skip the frame rather than abort the whole turn.
|
|
@@ -1061,6 +1103,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1061
1103
|
payload = decodeFramePayload(frame);
|
|
1062
1104
|
}
|
|
1063
1105
|
catch (e) {
|
|
1106
|
+
replaySafety.markBarrier("unknown-or-malformed-frame");
|
|
1064
1107
|
trace(`gunzip FAILED (skipping frame): flags=0x${frame.flags.toString(16)} len=${frame.payload.length} err=${e.message}`);
|
|
1065
1108
|
continue;
|
|
1066
1109
|
}
|
|
@@ -1068,39 +1111,46 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1068
1111
|
try {
|
|
1069
1112
|
asm = decodeMessage("AgentServerMessage", payload);
|
|
1070
1113
|
}
|
|
1071
|
-
catch
|
|
1114
|
+
catch {
|
|
1072
1115
|
// A single malformed/truncated frame must not abort the whole turn
|
|
1073
1116
|
// (protobufjs throws "index out of range: …" on length overruns). Log it
|
|
1074
1117
|
// and keep pumping.
|
|
1075
|
-
|
|
1076
|
-
const
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1118
|
+
replaySafety.markBarrier("unknown-or-malformed-frame");
|
|
1119
|
+
const channel = responseRequiredChannel(payload);
|
|
1120
|
+
if (channel) {
|
|
1121
|
+
failRunProtocol(`Cursor ${channel} request could not be decoded`, RUN_REQUEST_DECODE_FAILED);
|
|
1122
|
+
}
|
|
1123
|
+
trace(`decode FAILED (skipping non-request frame): flags=0x${frame.flags.toString(16)} len=${payload.length}`);
|
|
1081
1124
|
continue;
|
|
1082
1125
|
}
|
|
1083
1126
|
const iu = asm.interaction_update;
|
|
1084
1127
|
const esm = asm.exec_server_message;
|
|
1085
1128
|
const kv = asm.kv_server_message;
|
|
1129
|
+
const execControl = asm.exec_server_control_message;
|
|
1086
1130
|
const interactionQuery = asm.interaction_query;
|
|
1087
1131
|
const checkpointRaw = asm.conversation_checkpoint_update;
|
|
1088
1132
|
const topField = payload.length > 0 ? payload[0] >> 3 : 0;
|
|
1089
|
-
const textProgress = iu?.text_delta?.text;
|
|
1090
|
-
const thinkingProgress = iu?.thinking_delta?.text;
|
|
1091
1133
|
const checkpointProgress = normalizeCheckpointBytes(checkpointRaw);
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1134
|
+
const requiredChannel = responseRequiredChannel(payload);
|
|
1135
|
+
if (requiredChannel === "multiple" ||
|
|
1136
|
+
(requiredChannel === "exec" && !esm) ||
|
|
1137
|
+
(requiredChannel === "kv" && !kv) ||
|
|
1138
|
+
(requiredChannel === "interaction" && !interactionQuery)) {
|
|
1139
|
+
failRunProtocol("Cursor response-requiring request could not be decoded", RUN_REQUEST_DECODE_FAILED);
|
|
1140
|
+
}
|
|
1141
|
+
const replayFrame = analyzeReplayFrame(payload, {
|
|
1142
|
+
interactionUpdate: iu,
|
|
1143
|
+
exec: esm,
|
|
1144
|
+
kv,
|
|
1145
|
+
execControl,
|
|
1146
|
+
interactionQuery,
|
|
1147
|
+
checkpointBytes: checkpointProgress,
|
|
1148
|
+
});
|
|
1149
|
+
if (replayFrame.semanticProgress) {
|
|
1102
1150
|
sessionManager.recordSemanticProgress(session);
|
|
1103
1151
|
}
|
|
1152
|
+
if (replayFrame.barrier)
|
|
1153
|
+
replaySafety.markBarrier(replayFrame.barrier);
|
|
1104
1154
|
{
|
|
1105
1155
|
const iuKind = iu ? Object.keys(iu).find((k) => iu[k]) : undefined;
|
|
1106
1156
|
trace(`pump frame: topField=${topField} interaction_update=${iuKind ?? "-"} ` +
|
|
@@ -1221,15 +1271,12 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1221
1271
|
trace(`exec request_context hooks_additional_context: ${hooks}`);
|
|
1222
1272
|
}
|
|
1223
1273
|
try {
|
|
1274
|
+
traceRequestContextPaths(`exec request_context reply id=${esmId}`, session.requestContext);
|
|
1224
1275
|
session.stream.write(buildRequestContextResult(esmId, session.requestContext));
|
|
1225
1276
|
trace(`exec request_context: replied`);
|
|
1226
1277
|
}
|
|
1227
|
-
catch
|
|
1228
|
-
|
|
1229
|
-
trace(`exec request_context: write FAILED ${error.message}`);
|
|
1230
|
-
safeError(error);
|
|
1231
|
-
sessionManager.close(session);
|
|
1232
|
-
return;
|
|
1278
|
+
catch {
|
|
1279
|
+
failRunProtocol("Cursor request-context reply failed", RUN_REPLY_FAILED);
|
|
1233
1280
|
}
|
|
1234
1281
|
}
|
|
1235
1282
|
else if (esm.mcp_state_exec_args) {
|
|
@@ -1244,16 +1291,20 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1244
1291
|
session.stream.write(buildMcpStateResult(esmId, stateArgs, session.requestContext));
|
|
1245
1292
|
trace(`exec mcp_state: replied id=${esmId} requested=[${requested}]`);
|
|
1246
1293
|
}
|
|
1247
|
-
catch
|
|
1248
|
-
|
|
1249
|
-
trace(`exec mcp_state: write FAILED ${error.message}`);
|
|
1250
|
-
safeError(error);
|
|
1251
|
-
sessionManager.close(session);
|
|
1252
|
-
return;
|
|
1294
|
+
catch {
|
|
1295
|
+
failRunProtocol("Cursor MCP-state reply failed", RUN_REPLY_FAILED);
|
|
1253
1296
|
}
|
|
1254
1297
|
}
|
|
1255
1298
|
else {
|
|
1299
|
+
replaySafety.markBarrier("non-control-exec");
|
|
1256
1300
|
const parsed = parseExecServerMessage(esm);
|
|
1301
|
+
if (parsed) {
|
|
1302
|
+
const executableToolName = resolveCustomWebToolAlias(parsed.toolName, session.toolAliases);
|
|
1303
|
+
if (executableToolName !== parsed.toolName) {
|
|
1304
|
+
trace(`web tool alias resolved: ${parsed.toolName} -> ${executableToolName}`);
|
|
1305
|
+
parsed.toolName = executableToolName;
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1257
1308
|
const displayCallId = extractExecDisplayCallId(esm);
|
|
1258
1309
|
trace(`exec: id=${parsed?.id} variant=${parsed ? Object.keys(parsed).join(",") : "none"} toolName=${parsed?.toolName} resultField=${parsed?.resultField}`);
|
|
1259
1310
|
if (parsed) {
|
|
@@ -1327,10 +1378,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1327
1378
|
.map((x) => x.toString(16).padStart(2, "0"))
|
|
1328
1379
|
.join("");
|
|
1329
1380
|
trace(`exec UNMAPPED: id=${esmId} variant=${variantDescription} keys=[${Object.keys(esm).join(",")}] hex=${hex}`);
|
|
1330
|
-
|
|
1331
|
-
safeError(err);
|
|
1332
|
-
sessionManager.close(session);
|
|
1333
|
-
return;
|
|
1381
|
+
failRunProtocol(`Unsupported Cursor exec variant ${variantDescription} (id=${esmId})`, RUN_REQUEST_UNSUPPORTED);
|
|
1334
1382
|
}
|
|
1335
1383
|
}
|
|
1336
1384
|
else if (interactionQuery) {
|
|
@@ -1338,20 +1386,24 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1338
1386
|
// SDK has no Cursor-specific UI callback, so answer immediately with the
|
|
1339
1387
|
// conservative headless policy from protocol/interactions.ts (including
|
|
1340
1388
|
// F14 create_plan auto-ack / empty plan_uri for CLI headless parity).
|
|
1389
|
+
const handled = (() => {
|
|
1390
|
+
try {
|
|
1391
|
+
return handleInteractionQuery(interactionQuery, payload);
|
|
1392
|
+
}
|
|
1393
|
+
catch {
|
|
1394
|
+
return failRunProtocol("Cursor interaction request could not be handled", RUN_REQUEST_UNSUPPORTED);
|
|
1395
|
+
}
|
|
1396
|
+
})();
|
|
1341
1397
|
try {
|
|
1342
|
-
|
|
1398
|
+
if (handled.outcome === "acknowledged") {
|
|
1399
|
+
replaySafety.markBarrier("stateful-interaction");
|
|
1400
|
+
}
|
|
1343
1401
|
session.stream.write(handled.reply);
|
|
1344
1402
|
trace(`interaction_query: replied id=${handled.id} variant=${handled.variantName} ` +
|
|
1345
1403
|
`field=${handled.variantField} outcome=${handled.outcome}`);
|
|
1346
1404
|
}
|
|
1347
|
-
catch
|
|
1348
|
-
|
|
1349
|
-
const err = e instanceof Error ? e : new Error(String(e));
|
|
1350
|
-
trace(`interaction_query: FAILED id=${info.id ?? "?"} ` +
|
|
1351
|
-
`variantField=${info.variantField ?? "?"} err=${err.message}`);
|
|
1352
|
-
safeError(err);
|
|
1353
|
-
sessionManager.close(session);
|
|
1354
|
-
return;
|
|
1405
|
+
catch {
|
|
1406
|
+
failRunProtocol("Cursor interaction reply failed", RUN_REPLY_FAILED);
|
|
1355
1407
|
}
|
|
1356
1408
|
}
|
|
1357
1409
|
else if (kv) {
|
|
@@ -1370,20 +1422,24 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1370
1422
|
`found=${handled.found} echoed=${!!handled.echoed} ` +
|
|
1371
1423
|
`sessionBlobs=${session.blobs.size} convBlobs=${conversationBlobCount(session.conversationId)}`);
|
|
1372
1424
|
}
|
|
1373
|
-
catch
|
|
1374
|
-
|
|
1375
|
-
trace(`kv: write FAILED ${error.message}`);
|
|
1376
|
-
safeError(error);
|
|
1377
|
-
sessionManager.close(session);
|
|
1378
|
-
return;
|
|
1425
|
+
catch {
|
|
1426
|
+
failRunProtocol("Cursor KV reply failed", RUN_REPLY_FAILED);
|
|
1379
1427
|
}
|
|
1380
1428
|
}
|
|
1429
|
+
else {
|
|
1430
|
+
failRunProtocol("Cursor KV request could not be handled", RUN_REQUEST_UNSUPPORTED);
|
|
1431
|
+
}
|
|
1381
1432
|
}
|
|
1382
1433
|
}
|
|
1383
1434
|
catch (e) {
|
|
1435
|
+
if (e instanceof CursorProtocolError)
|
|
1436
|
+
throw e;
|
|
1437
|
+
if (requiredChannel) {
|
|
1438
|
+
failRunProtocol(`Cursor ${requiredChannel} request could not be handled`, RUN_REQUEST_UNSUPPORTED);
|
|
1439
|
+
}
|
|
1384
1440
|
// Any per-frame dispatch throw (e.g. protobufjs length overrun in
|
|
1385
1441
|
// exec/args decode) must not abort the whole turn — log and skip.
|
|
1386
|
-
trace(`frame dispatch FAILED (skipping): topField=${topField}
|
|
1442
|
+
trace(`frame dispatch FAILED (skipping): topField=${topField}`);
|
|
1387
1443
|
}
|
|
1388
1444
|
// heartbeat / step / partial_tool_call → ignore (partial args are
|
|
1389
1445
|
// display-only; the exec channel is authoritative. Checkpoints and
|
|
@@ -1506,8 +1562,11 @@ export function buildOpenCodeInteractionGuidance(tools, isCompaction, workspaceR
|
|
|
1506
1562
|
if (names.has("plan_exit")) {
|
|
1507
1563
|
instructions.push("- To leave plan mode, call the OpenCode `plan_exit` tool.");
|
|
1508
1564
|
}
|
|
1509
|
-
if (names.has(
|
|
1510
|
-
instructions.push(
|
|
1565
|
+
if (names.has(CUSTOM_WEBSEARCH_TOOL)) {
|
|
1566
|
+
instructions.push(`- For web searches, call \`${CUSTOM_WEBSEARCH_TOOL}\`; do not use Cursor's native WebSearch interaction.`);
|
|
1567
|
+
}
|
|
1568
|
+
if (names.has(CUSTOM_WEBFETCH_TOOL)) {
|
|
1569
|
+
instructions.push(`- To fetch a known URL, call \`${CUSTOM_WEBFETCH_TOOL}\`; do not use Cursor's native WebFetch interaction.`);
|
|
1511
1570
|
}
|
|
1512
1571
|
if (names.has("write")) {
|
|
1513
1572
|
instructions.push(names.has("edit")
|
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-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,11 +1,12 @@
|
|
|
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, cursorShellEnvForCall, cursorShellOriginalCommand, prepareCursorShellArgs, releaseCursorShellEnv, sanitizeRegisteredCursorShellOutput, } 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
|
+
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,6 +195,8 @@ function cursorGetServerConfigTelemetryEnabled() {
|
|
|
194
195
|
process.env.CURSOR_GET_SERVER_CONFIG_TELEMETRY === "true");
|
|
195
196
|
}
|
|
196
197
|
export async function CursorPlugin(input) {
|
|
198
|
+
// Prefer strong OCP host identity when available; otherwise resolve from explicit host signals/install path.
|
|
199
|
+
await adoptCompatHostCacheDir();
|
|
197
200
|
const cacheDir = opencodeGlobalCacheDir();
|
|
198
201
|
const apiBaseURL = cursorApiBaseURL();
|
|
199
202
|
// Last access token successfully resolved in this plugin instance. Config's
|
|
@@ -313,6 +316,12 @@ export async function CursorPlugin(input) {
|
|
|
313
316
|
return cached?.models.length ? modelsToConfig(cached.models) : {};
|
|
314
317
|
}
|
|
315
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
|
+
},
|
|
316
325
|
async event({ event }) {
|
|
317
326
|
switch (event.type) {
|
|
318
327
|
case "session.created":
|
|
@@ -336,9 +345,9 @@ export async function CursorPlugin(input) {
|
|
|
336
345
|
async "tool.execute.before"(hookInput, output) {
|
|
337
346
|
if (hookInput.tool !== "bash")
|
|
338
347
|
return;
|
|
339
|
-
//
|
|
340
|
-
//
|
|
341
|
-
//
|
|
348
|
+
// bash/zsh retain the original display/permission command and wrap via
|
|
349
|
+
// shell.env. sh/dash need a short wrapper-file command because their
|
|
350
|
+
// non-interactive `-c` path ignores BASH_ENV / ZDOTDIR.
|
|
342
351
|
prepareCursorShellArgs(hookInput.callID, output.args);
|
|
343
352
|
},
|
|
344
353
|
async "shell.env"(hookInput, output) {
|
|
@@ -377,6 +386,7 @@ export async function CursorPlugin(input) {
|
|
|
377
386
|
}
|
|
378
387
|
},
|
|
379
388
|
async config(cfg) {
|
|
389
|
+
setCursorShellPath(cfg.shell);
|
|
380
390
|
cfg.provider ??= {};
|
|
381
391
|
const models = await loadModels();
|
|
382
392
|
const existing = cfg.provider[CURSOR_PROVIDER_ID];
|
|
@@ -486,6 +496,7 @@ export async function CursorPlugin(input) {
|
|
|
486
496
|
return {
|
|
487
497
|
...(accessToken ? { accessToken } : {}),
|
|
488
498
|
workspaceRoot: input.directory,
|
|
499
|
+
cacheDir,
|
|
489
500
|
};
|
|
490
501
|
},
|
|
491
502
|
},
|