cursor-opencode-provider 0.1.5 → 0.2.2
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/DISCLAIMER.md +9 -0
- package/README.md +38 -8
- package/dist/context/build.js +3 -2
- package/dist/context/rules.d.ts +1 -0
- package/dist/context/rules.js +1 -0
- package/dist/language-model.d.ts +52 -6
- package/dist/language-model.js +435 -63
- package/dist/models.d.ts +29 -4
- package/dist/models.js +134 -17
- package/dist/plugin.d.ts +2 -0
- package/dist/plugin.js +145 -43
- package/dist/protocol/blob-store.d.ts +2 -0
- package/dist/protocol/blob-store.js +4 -0
- package/dist/protocol/conversation-bind.d.ts +18 -0
- package/dist/protocol/conversation-bind.js +66 -0
- package/dist/protocol/interactions.d.ts +23 -0
- package/dist/protocol/interactions.js +136 -0
- package/dist/protocol/messages.js +421 -13
- package/dist/protocol/request.d.ts +30 -0
- package/dist/protocol/request.js +23 -8
- package/dist/protocol/stream.js +9 -3
- package/dist/protocol/tool-call-bridge.d.ts +44 -0
- package/dist/protocol/tool-call-bridge.js +480 -0
- package/dist/protocol/tools.d.ts +35 -12
- package/dist/protocol/tools.js +179 -74
- package/dist/session.d.ts +26 -1
- package/dist/session.js +2 -2
- package/dist/shared.d.ts +4 -0
- package/dist/shared.js +4 -0
- package/dist/transport/connect.js +11 -1
- package/package.json +3 -2
package/dist/protocol/tools.js
CHANGED
|
@@ -5,24 +5,56 @@ import { trace } from "../debug.js";
|
|
|
5
5
|
// probe (ExecServerMessage #10 → ExecClientMessage #10). request/result share a
|
|
6
6
|
// field number for every exec variant, so this is also the result field.
|
|
7
7
|
export const REQUEST_CONTEXT_RESULT_FIELD = 10;
|
|
8
|
+
/** Match OpenCode's McpCatalog.sanitize for config server ids. */
|
|
9
|
+
export function sanitizeMcpServerId(value) {
|
|
10
|
+
return value.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Resolve an OpenCode tool id using known MCP server ids from merged config.
|
|
14
|
+
* Unknown names remain under the synthetic default server: a flattened tool
|
|
15
|
+
* name alone cannot distinguish plugin/custom tools from `<server>_<tool>`.
|
|
16
|
+
*/
|
|
17
|
+
export function resolveToolServerIdentity(opencodeName, defaultServer = "opencode", knownMcpServers = []) {
|
|
18
|
+
if (!opencodeName) {
|
|
19
|
+
return { server: defaultServer, toolName: "mcp", opencodeName: "mcp" };
|
|
20
|
+
}
|
|
21
|
+
// Longest first handles configured ids where one is a prefix of another
|
|
22
|
+
// (e.g. "git" and "git_hub"). OpenCode flattens with the sanitized id.
|
|
23
|
+
const servers = [...new Set([...knownMcpServers].map(sanitizeMcpServerId).filter(Boolean))]
|
|
24
|
+
.sort((a, b) => b.length - a.length);
|
|
25
|
+
for (const server of servers) {
|
|
26
|
+
const prefix = `${server}_`;
|
|
27
|
+
if (!opencodeName.startsWith(prefix) || opencodeName.length === prefix.length)
|
|
28
|
+
continue;
|
|
29
|
+
return {
|
|
30
|
+
server,
|
|
31
|
+
toolName: opencodeName.slice(prefix.length),
|
|
32
|
+
opencodeName,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
return { server: defaultServer, toolName: opencodeName, opencodeName };
|
|
36
|
+
}
|
|
8
37
|
/**
|
|
9
38
|
* Convert opencode's per-turn tool list into Cursor `McpToolDefinition`
|
|
10
39
|
* entries for `request_context.tools` (#7) and `AgentRunRequest.mcp_tools`.
|
|
11
40
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
41
|
+
* Builtins and unknown plugin/custom tools are advertised under the synthetic
|
|
42
|
+
* default server (`opencode`). Tools whose prefixes match configured MCP
|
|
43
|
+
* servers keep those server ids (`github`, …). Composite `name` is
|
|
44
|
+
* `<server>-<bareTool>`; local execution still uses the full OpenCode id
|
|
45
|
+
* reconstructed in `mcpRealToolName`.
|
|
17
46
|
*/
|
|
18
|
-
export function toolsToDescriptors(tools, providerIdentifier = "opencode") {
|
|
19
|
-
return tools.map((t) =>
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
47
|
+
export function toolsToDescriptors(tools, providerIdentifier = "opencode", knownMcpServers = []) {
|
|
48
|
+
return tools.map((t) => {
|
|
49
|
+
const id = resolveToolServerIdentity(t.name, providerIdentifier, knownMcpServers);
|
|
50
|
+
return {
|
|
51
|
+
name: `${id.server}-${id.toolName}`,
|
|
52
|
+
description: t.description ?? "",
|
|
53
|
+
input_schema: encodeJsonAsValue(normalizeInputSchema(t.inputSchema)),
|
|
54
|
+
provider_identifier: id.server,
|
|
55
|
+
tool_name: id.toolName,
|
|
56
|
+
};
|
|
57
|
+
});
|
|
26
58
|
}
|
|
27
59
|
function normalizeInputSchema(schema) {
|
|
28
60
|
if (schema && typeof schema === "object" && !Array.isArray(schema)) {
|
|
@@ -32,31 +64,42 @@ function normalizeInputSchema(schema) {
|
|
|
32
64
|
}
|
|
33
65
|
/**
|
|
34
66
|
* Build the nested McpFileSystemOptions / McpMetaToolOptions shape used by
|
|
35
|
-
* requestContext.#23 / #34.
|
|
36
|
-
*
|
|
67
|
+
* requestContext.#23 / #34. One `McpDescriptor` per resolved server (builtins
|
|
68
|
+
* and unknown tools under the synthetic default; configured MCP tools under
|
|
69
|
+
* their upstream server id).
|
|
37
70
|
*/
|
|
38
|
-
export function toolsToMcpDescriptors(tools, providerIdentifier = "opencode") {
|
|
71
|
+
export function toolsToMcpDescriptors(tools, providerIdentifier = "opencode", knownMcpServers = []) {
|
|
39
72
|
if (tools.length === 0)
|
|
40
73
|
return [];
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
|
|
74
|
+
const order = [];
|
|
75
|
+
const byServer = new Map();
|
|
76
|
+
for (const t of tools) {
|
|
77
|
+
const id = resolveToolServerIdentity(t.name, providerIdentifier, knownMcpServers);
|
|
78
|
+
let list = byServer.get(id.server);
|
|
79
|
+
if (!list) {
|
|
80
|
+
list = [];
|
|
81
|
+
byServer.set(id.server, list);
|
|
82
|
+
order.push(id.server);
|
|
83
|
+
}
|
|
84
|
+
list.push({
|
|
85
|
+
tool_name: id.toolName,
|
|
86
|
+
description: t.description ?? "",
|
|
87
|
+
input_schema: encodeJsonAsValue(normalizeInputSchema(t.inputSchema)),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return order.map((server) => ({
|
|
91
|
+
server_name: server,
|
|
92
|
+
server_identifier: server,
|
|
93
|
+
tools: byServer.get(server),
|
|
94
|
+
}));
|
|
52
95
|
}
|
|
53
96
|
/**
|
|
54
97
|
* @deprecated Prefer `buildRequestContext` from `../context/build.js`.
|
|
55
98
|
* Kept as a sync tools-only fallback for unit tests that don't need collectors.
|
|
56
99
|
*/
|
|
57
|
-
export function buildLiveRequestContext(tools, providerIdentifier = "opencode") {
|
|
58
|
-
const flat = toolsToDescriptors(tools, providerIdentifier);
|
|
59
|
-
const nested = toolsToMcpDescriptors(tools, providerIdentifier);
|
|
100
|
+
export function buildLiveRequestContext(tools, providerIdentifier = "opencode", knownMcpServers = []) {
|
|
101
|
+
const flat = toolsToDescriptors(tools, providerIdentifier, knownMcpServers);
|
|
102
|
+
const nested = toolsToMcpDescriptors(tools, providerIdentifier, knownMcpServers);
|
|
60
103
|
const cwd = process.cwd();
|
|
61
104
|
return {
|
|
62
105
|
env: {
|
|
@@ -94,7 +137,13 @@ export function buildLiveRequestContext(tools, providerIdentifier = "opencode")
|
|
|
94
137
|
const cursorToolToOpencode = {
|
|
95
138
|
read_args: "read",
|
|
96
139
|
write_args: "write",
|
|
140
|
+
pi_read_args: "read",
|
|
141
|
+
pi_bash_args: "bash",
|
|
142
|
+
pi_edit_args: "edit",
|
|
97
143
|
pi_write_args: "write",
|
|
144
|
+
pi_grep_args: "grep",
|
|
145
|
+
pi_find_args: "glob",
|
|
146
|
+
pi_ls_args: "read",
|
|
98
147
|
grep_args: "grep",
|
|
99
148
|
ls_args: "read",
|
|
100
149
|
delete_args: "bash",
|
|
@@ -137,6 +186,17 @@ const CURSOR_INTERNAL_KEYS = new Set([
|
|
|
137
186
|
"encoding_hint",
|
|
138
187
|
"ignore",
|
|
139
188
|
]);
|
|
189
|
+
/** Required content fields where an empty string is meaningful (for example, truncating a file). */
|
|
190
|
+
const PRESERVE_EMPTY_STRING_KEYS = new Set([
|
|
191
|
+
"content",
|
|
192
|
+
"file_text",
|
|
193
|
+
"fileText",
|
|
194
|
+
"stream_content",
|
|
195
|
+
"oldString",
|
|
196
|
+
"old_string",
|
|
197
|
+
"newString",
|
|
198
|
+
"new_string",
|
|
199
|
+
]);
|
|
140
200
|
export function mapExecServerToToolName(execField) {
|
|
141
201
|
return cursorToolToOpencode[execField];
|
|
142
202
|
}
|
|
@@ -150,22 +210,28 @@ export function mapToolNameToExecField(toolName) {
|
|
|
150
210
|
const execVariantToResultField = {
|
|
151
211
|
read_args: "read_result",
|
|
152
212
|
write_args: "write_result",
|
|
153
|
-
|
|
213
|
+
pi_read_args: "pi_read_result",
|
|
214
|
+
pi_bash_args: "pi_bash_result",
|
|
215
|
+
pi_edit_args: "pi_edit_result",
|
|
154
216
|
pi_write_args: "pi_write_result",
|
|
217
|
+
pi_grep_args: "pi_grep_result",
|
|
218
|
+
pi_find_args: "pi_find_result",
|
|
219
|
+
pi_ls_args: "pi_ls_result",
|
|
155
220
|
grep_args: "grep_result",
|
|
156
221
|
ls_args: "ls_result",
|
|
157
222
|
delete_args: "delete_result",
|
|
158
223
|
shell_stream_args: "shell_stream",
|
|
159
224
|
mcp_args: "mcp_result",
|
|
160
225
|
};
|
|
161
|
-
const MCP_PREFIX = "opencode-";
|
|
162
226
|
export function parseExecServerMessage(msg) {
|
|
163
227
|
const id = msg.id;
|
|
164
228
|
if (id === undefined)
|
|
165
229
|
return undefined;
|
|
166
230
|
// Find which args variant is set
|
|
167
231
|
const execVariant = findOneOfVariant(msg, [
|
|
168
|
-
"read_args", "write_args",
|
|
232
|
+
"read_args", "write_args",
|
|
233
|
+
"pi_read_args", "pi_bash_args", "pi_edit_args", "pi_write_args",
|
|
234
|
+
"pi_grep_args", "pi_find_args", "pi_ls_args",
|
|
169
235
|
"grep_args", "ls_args",
|
|
170
236
|
"delete_args", "shell_stream_args", "mcp_args",
|
|
171
237
|
]);
|
|
@@ -187,6 +253,33 @@ export function parseExecServerMessage(msg) {
|
|
|
187
253
|
resultField,
|
|
188
254
|
};
|
|
189
255
|
}
|
|
256
|
+
if (execVariant === "pi_edit_args") {
|
|
257
|
+
const raw = msg.pi_edit_args ?? {};
|
|
258
|
+
const edits = Array.isArray(raw.edits) ? raw.edits : [];
|
|
259
|
+
const replacement = edits.length === 1 && edits[0] && typeof edits[0] === "object"
|
|
260
|
+
? edits[0]
|
|
261
|
+
: undefined;
|
|
262
|
+
const path = str(raw.path);
|
|
263
|
+
const oldString = replacement ? stringValue(replacement.old_text) : undefined;
|
|
264
|
+
const newString = replacement ? stringValue(replacement.new_text) : undefined;
|
|
265
|
+
const args = {};
|
|
266
|
+
if (path)
|
|
267
|
+
args.filePath = path;
|
|
268
|
+
if (oldString !== undefined)
|
|
269
|
+
args.oldString = oldString;
|
|
270
|
+
if (newString !== undefined)
|
|
271
|
+
args.newString = newString;
|
|
272
|
+
return {
|
|
273
|
+
id,
|
|
274
|
+
execId,
|
|
275
|
+
toolName: "edit",
|
|
276
|
+
args,
|
|
277
|
+
resultField,
|
|
278
|
+
localError: path && oldString && newString !== undefined
|
|
279
|
+
? undefined
|
|
280
|
+
: "Cursor Pi edit cannot be represented safely: expected one non-empty replacement.",
|
|
281
|
+
};
|
|
282
|
+
}
|
|
190
283
|
const toolName = cursorToolToOpencode[execVariant];
|
|
191
284
|
if (!toolName)
|
|
192
285
|
return undefined;
|
|
@@ -211,8 +304,9 @@ export function mapCursorArgsToOpencode(toolName, raw, execVariant) {
|
|
|
211
304
|
continue;
|
|
212
305
|
if (CURSOR_INTERNAL_KEYS.has(k))
|
|
213
306
|
continue;
|
|
214
|
-
// Drop empty strings from optional protobuf defaults
|
|
215
|
-
|
|
307
|
+
// Drop empty strings from optional protobuf defaults, but retain required
|
|
308
|
+
// content fields where empty means a valid destructive edit/write.
|
|
309
|
+
if (typeof v === "string" && v.length === 0 && !PRESERVE_EMPTY_STRING_KEYS.has(k))
|
|
216
310
|
continue;
|
|
217
311
|
cleaned[k] = v;
|
|
218
312
|
}
|
|
@@ -250,7 +344,7 @@ export function mapCursorArgsToOpencode(toolName, raw, execVariant) {
|
|
|
250
344
|
const filePath = str(cleaned.filePath) ?? str(cleaned.path) ?? str(cleaned.file_path);
|
|
251
345
|
if (filePath)
|
|
252
346
|
args.filePath = filePath;
|
|
253
|
-
const content =
|
|
347
|
+
const content = stringValue(cleaned.content) ?? stringValue(cleaned.file_text) ?? stringValue(cleaned.fileText);
|
|
254
348
|
if (content !== undefined)
|
|
255
349
|
args.content = content;
|
|
256
350
|
return { toolName: "write", args };
|
|
@@ -260,10 +354,10 @@ export function mapCursorArgsToOpencode(toolName, raw, execVariant) {
|
|
|
260
354
|
const filePath = str(cleaned.filePath) ?? str(cleaned.path) ?? str(cleaned.file_path);
|
|
261
355
|
if (filePath)
|
|
262
356
|
args.filePath = filePath;
|
|
263
|
-
const oldString =
|
|
357
|
+
const oldString = stringValue(cleaned.oldString) ?? stringValue(cleaned.old_string);
|
|
264
358
|
if (oldString !== undefined)
|
|
265
359
|
args.oldString = oldString;
|
|
266
|
-
const newString =
|
|
360
|
+
const newString = stringValue(cleaned.newString) ?? stringValue(cleaned.new_string);
|
|
267
361
|
if (newString !== undefined)
|
|
268
362
|
args.newString = newString;
|
|
269
363
|
if (typeof cleaned.replaceAll === "boolean")
|
|
@@ -321,6 +415,9 @@ export function mapCursorArgsToOpencode(toolName, raw, execVariant) {
|
|
|
321
415
|
function str(v) {
|
|
322
416
|
return typeof v === "string" && v.length > 0 ? v : undefined;
|
|
323
417
|
}
|
|
418
|
+
function stringValue(v) {
|
|
419
|
+
return typeof v === "string" ? v : undefined;
|
|
420
|
+
}
|
|
324
421
|
function num(v) {
|
|
325
422
|
if (typeof v === "number" && Number.isFinite(v))
|
|
326
423
|
return v;
|
|
@@ -331,12 +428,35 @@ function num(v) {
|
|
|
331
428
|
function shellQuote(s) {
|
|
332
429
|
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
333
430
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
431
|
+
/**
|
|
432
|
+
* Map Cursor McpArgs back to the OpenCode tool id.
|
|
433
|
+
* Prefers provider_identifier + bare tool_name (github + create_pull_request
|
|
434
|
+
* → github_create_pull_request). Builtins under the default server stay bare.
|
|
435
|
+
*/
|
|
436
|
+
export function mcpRealToolName(mcpArgs, defaultServer = "opencode") {
|
|
437
|
+
const toolName = typeof mcpArgs.tool_name === "string" ? mcpArgs.tool_name : undefined;
|
|
438
|
+
const provider = typeof mcpArgs.provider_identifier === "string" ? mcpArgs.provider_identifier : undefined;
|
|
439
|
+
if (toolName) {
|
|
440
|
+
if (provider && provider !== defaultServer) {
|
|
441
|
+
// Already a full OpenCode id (legacy ads or model echo).
|
|
442
|
+
if (toolName.startsWith(`${provider}_`))
|
|
443
|
+
return toolName;
|
|
444
|
+
return `${provider}_${toolName}`;
|
|
445
|
+
}
|
|
337
446
|
return toolName;
|
|
338
|
-
|
|
339
|
-
|
|
447
|
+
}
|
|
448
|
+
const name = typeof mcpArgs.name === "string" ? mcpArgs.name : "";
|
|
449
|
+
const dash = name.indexOf("-");
|
|
450
|
+
if (dash > 0) {
|
|
451
|
+
const server = name.slice(0, dash);
|
|
452
|
+
const bare = name.slice(dash + 1);
|
|
453
|
+
if (server && bare) {
|
|
454
|
+
if (server === defaultServer)
|
|
455
|
+
return bare;
|
|
456
|
+
return `${server}_${bare}`;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return name || "mcp";
|
|
340
460
|
}
|
|
341
461
|
function decodeMcpArgs(raw) {
|
|
342
462
|
if (!Array.isArray(raw))
|
|
@@ -537,6 +657,18 @@ export function buildTypedExecResult(resultField, output, error, toolName) {
|
|
|
537
657
|
if (error)
|
|
538
658
|
return { error: { error } };
|
|
539
659
|
return { success: { output: output || "Wrote file successfully." } };
|
|
660
|
+
case "pi_read_result":
|
|
661
|
+
if (error)
|
|
662
|
+
return { error: { error } };
|
|
663
|
+
return { success: { output: unwrapReadOutput(output) } };
|
|
664
|
+
case "pi_bash_result":
|
|
665
|
+
case "pi_edit_result":
|
|
666
|
+
case "pi_grep_result":
|
|
667
|
+
case "pi_find_result":
|
|
668
|
+
case "pi_ls_result":
|
|
669
|
+
if (error)
|
|
670
|
+
return { error: { error } };
|
|
671
|
+
return { success: { output } };
|
|
540
672
|
case "delete_result":
|
|
541
673
|
if (error)
|
|
542
674
|
return { error: { path: "", error } };
|
|
@@ -639,15 +771,11 @@ export function parseExecIdFromToolCallId(toolCallId) {
|
|
|
639
771
|
return undefined;
|
|
640
772
|
return { sessionId: match[1], execId };
|
|
641
773
|
}
|
|
642
|
-
// ──
|
|
774
|
+
// ── Unknown exec diagnostics ──
|
|
643
775
|
//
|
|
644
|
-
//
|
|
645
|
-
//
|
|
646
|
-
//
|
|
647
|
-
// to — but we MUST still reply on the Run stream or the server blocks forever
|
|
648
|
-
// (endless heartbeats, no response). For any unmapped variant we emit an empty
|
|
649
|
-
// result at the SAME field number (request/result field numbers are identical
|
|
650
|
-
// for every exec variant: read #7→#7, mcp #11→#11, request_context #10→#10…).
|
|
776
|
+
// Request/result field numbers are not universally identical (the Pi range is
|
|
777
|
+
// offset by one), so unknown variants must never receive a guessed empty reply.
|
|
778
|
+
// The pump uses this raw detector to report schema drift and fail the Run.
|
|
651
779
|
/**
|
|
652
780
|
* Find the exec variant field number from the raw (gunzipped) AgentServerMessage
|
|
653
781
|
* payload: peel field #2 (exec_server_message), then return the first
|
|
@@ -667,29 +795,6 @@ export function detectExecVariantField(agentServerPayload) {
|
|
|
667
795
|
}
|
|
668
796
|
return undefined;
|
|
669
797
|
}
|
|
670
|
-
/** Build ExecClientMessage{1:id, <resultField>: empty} as raw bytes. */
|
|
671
|
-
export function buildRawEmptyExecReply(execId, resultField) {
|
|
672
|
-
const inner = [];
|
|
673
|
-
writeVarintRaw(inner, (1 << 3) | 0); // field 1 (id), wire 0 (varint)
|
|
674
|
-
writeVarintRaw(inner, execId >>> 0);
|
|
675
|
-
writeVarintRaw(inner, (resultField << 3) | 2); // field N, wire 2 (length-delimited)
|
|
676
|
-
writeVarintRaw(inner, 0); // empty submessage
|
|
677
|
-
// Wrap as AgentClientMessage.exec_client_message (field #2).
|
|
678
|
-
const acm = [];
|
|
679
|
-
writeVarintRaw(acm, (2 << 3) | 2);
|
|
680
|
-
writeVarintRaw(acm, inner.length);
|
|
681
|
-
for (const b of inner)
|
|
682
|
-
acm.push(b);
|
|
683
|
-
return new Uint8Array(acm);
|
|
684
|
-
}
|
|
685
|
-
function writeVarintRaw(out, n) {
|
|
686
|
-
let v = n >>> 0;
|
|
687
|
-
while (v > 0x7f) {
|
|
688
|
-
out.push((v & 0x7f) | 0x80);
|
|
689
|
-
v >>>= 7;
|
|
690
|
-
}
|
|
691
|
-
out.push(v);
|
|
692
|
-
}
|
|
693
798
|
/**
|
|
694
799
|
* Encode exec #10 request_context_result from a prebuilt RequestContext payload.
|
|
695
800
|
*/
|
package/dist/session.d.ts
CHANGED
|
@@ -19,6 +19,12 @@ export type PendingExec = {
|
|
|
19
19
|
* mcp_result can unwrap read envelopes even if the prompt omits toolName.
|
|
20
20
|
*/
|
|
21
21
|
toolName?: string;
|
|
22
|
+
/**
|
|
23
|
+
* True when this pending entry was synthesized from a Cursor display-only
|
|
24
|
+
* tool_call_* frame (no ExecServerMessage). Continuation must not write an
|
|
25
|
+
* exec result back to Cursor — just clear pending and keep pumping.
|
|
26
|
+
*/
|
|
27
|
+
bridged?: boolean;
|
|
22
28
|
};
|
|
23
29
|
export type CursorSession = {
|
|
24
30
|
/**
|
|
@@ -35,6 +41,14 @@ export type CursorSession = {
|
|
|
35
41
|
stream: BidiStream;
|
|
36
42
|
frames: AsyncIterator<Frame>;
|
|
37
43
|
pending: Map<number, PendingExec>;
|
|
44
|
+
/**
|
|
45
|
+
* Cursor display tool calls (tool_call_started) awaiting either an exec or a
|
|
46
|
+
* tool_call_completed. Keyed by call_id. Cleared when exec handles the call
|
|
47
|
+
* or when we bridge the completed display call into an OpenCode tool-call.
|
|
48
|
+
*/
|
|
49
|
+
displayToolCalls: Map<string, Record<string, unknown>>;
|
|
50
|
+
/** Monotonic synthetic exec ids for bridged (display-only) OpenCode tool calls. */
|
|
51
|
+
nextBridgedExecId: number;
|
|
38
52
|
/** KV blob store: blob_id (hex) → data, for Cursor's out-of-band payload channel. */
|
|
39
53
|
blobs: Map<string, Uint8Array>;
|
|
40
54
|
/** McpToolDefinition list advertised this turn — echoed into the request_context reply. */
|
|
@@ -47,6 +61,17 @@ export type CursorSession = {
|
|
|
47
61
|
* stream instead of emitting tool-call parts OpenCode will reject.
|
|
48
62
|
*/
|
|
49
63
|
allowTools: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Best-effort token usage for this held-open Run. Updated from text/tool
|
|
66
|
+
* activity and replaced by TurnEnded when the turn completes. Emitted on
|
|
67
|
+
* tool-calls finishes so OpenCode does not store all-zero usage mid-loop.
|
|
68
|
+
*/
|
|
69
|
+
usageEstimate: {
|
|
70
|
+
inputTokens: number;
|
|
71
|
+
outputTokens: number;
|
|
72
|
+
cacheRead: number;
|
|
73
|
+
cacheWrite: number;
|
|
74
|
+
};
|
|
50
75
|
/**
|
|
51
76
|
* True while a doStream pull() is actively reading this session's frames.
|
|
52
77
|
* Prevents a late cancel/abort from a prior ReadableStream from destroying
|
|
@@ -63,7 +88,7 @@ export declare class SessionManager {
|
|
|
63
88
|
constructor(idleTimeoutMs?: number);
|
|
64
89
|
touch(session: CursorSession): void;
|
|
65
90
|
/** Register that `session` is awaiting a result for `execId`. */
|
|
66
|
-
registerPending(execId: number, session: CursorSession, resultField: string, toolName?: string): void;
|
|
91
|
+
registerPending(execId: number, session: CursorSession, resultField: string, toolName?: string, bridged?: boolean): void;
|
|
67
92
|
/** The pending exec info for an id on a specific session, if still awaiting it. */
|
|
68
93
|
pendingFor(sessionId: string, execId: number): PendingExec | undefined;
|
|
69
94
|
/** Find the live session awaiting one of the given exec ids. */
|
package/dist/session.js
CHANGED
|
@@ -12,8 +12,8 @@ export class SessionManager {
|
|
|
12
12
|
session.expiresAt = Date.now() + this.idleTimeoutMs;
|
|
13
13
|
}
|
|
14
14
|
/** Register that `session` is awaiting a result for `execId`. */
|
|
15
|
-
registerPending(execId, session, resultField, toolName) {
|
|
16
|
-
session.pending.set(execId, { resultField, toolName });
|
|
15
|
+
registerPending(execId, session, resultField, toolName, bridged = false) {
|
|
16
|
+
session.pending.set(execId, { resultField, toolName, bridged });
|
|
17
17
|
this.byExecId.set(this.key(session.sessionId, execId), session);
|
|
18
18
|
this.touch(session);
|
|
19
19
|
}
|
package/dist/shared.d.ts
CHANGED
|
@@ -2,11 +2,15 @@ export declare const CURSOR_API_HOST = "api2.cursor.sh";
|
|
|
2
2
|
export declare const CURSOR_WEBSITE_HOST = "cursor.com";
|
|
3
3
|
export declare const FALLBACK_CLIENT_VERSION = "cli-2026.07.09-a3815c0";
|
|
4
4
|
export declare const CURSOR_PROVIDER_ID = "cursor";
|
|
5
|
+
/** Private provider option injected by the OpenCode plugin for summary turns. */
|
|
6
|
+
export declare const CURSOR_COMPACTION_OPTION = "opencodeCompaction";
|
|
5
7
|
export declare const TOKEN_EXPIRY_THRESHOLD_S = 300;
|
|
6
8
|
export declare const RUN_PATH = "/agent.v1.AgentService/Run";
|
|
7
9
|
export declare const AVAILABLE_MODELS_PATH = "/aiserver.v1.AiService/AvailableModels";
|
|
8
10
|
export declare const SERVER_CONFIG_PATH = "/aiserver.v1.ServerConfigService/GetServerConfig";
|
|
9
11
|
export declare const MODEL_CACHE_FILE = "cursor-models.json";
|
|
12
|
+
/** Bumped when the on-disk model cache shape/semantics change (forces refetch). */
|
|
13
|
+
export declare const MODEL_CACHE_SCHEMA_VERSION = 2;
|
|
10
14
|
export declare const MODEL_CACHE_TTL_MS = 86400000;
|
|
11
15
|
export declare const VERSION_CACHE_FILE = "cursor-client-version.json";
|
|
12
16
|
export declare const CONTENT_TYPE_CONNECT_PROTO = "application/connect+proto";
|
package/dist/shared.js
CHANGED
|
@@ -2,11 +2,15 @@ export const CURSOR_API_HOST = "api2.cursor.sh";
|
|
|
2
2
|
export const CURSOR_WEBSITE_HOST = "cursor.com";
|
|
3
3
|
export const FALLBACK_CLIENT_VERSION = "cli-2026.07.09-a3815c0";
|
|
4
4
|
export const CURSOR_PROVIDER_ID = "cursor";
|
|
5
|
+
/** Private provider option injected by the OpenCode plugin for summary turns. */
|
|
6
|
+
export const CURSOR_COMPACTION_OPTION = "opencodeCompaction";
|
|
5
7
|
export const TOKEN_EXPIRY_THRESHOLD_S = 300;
|
|
6
8
|
export const RUN_PATH = "/agent.v1.AgentService/Run";
|
|
7
9
|
export const AVAILABLE_MODELS_PATH = "/aiserver.v1.AiService/AvailableModels";
|
|
8
10
|
export const SERVER_CONFIG_PATH = "/aiserver.v1.ServerConfigService/GetServerConfig";
|
|
9
11
|
export const MODEL_CACHE_FILE = "cursor-models.json";
|
|
12
|
+
/** Bumped when the on-disk model cache shape/semantics change (forces refetch). */
|
|
13
|
+
export const MODEL_CACHE_SCHEMA_VERSION = 2;
|
|
10
14
|
export const MODEL_CACHE_TTL_MS = 86_400_000;
|
|
11
15
|
export const VERSION_CACHE_FILE = "cursor-client-version.json";
|
|
12
16
|
export const CONTENT_TYPE_CONNECT_PROTO = "application/connect+proto";
|
|
@@ -36,7 +36,17 @@ export async function unaryAvailableModels(token, options = {}) {
|
|
|
36
36
|
"content-type": "application/json",
|
|
37
37
|
accept: "application/json",
|
|
38
38
|
},
|
|
39
|
-
|
|
39
|
+
// AvailableModelsRequest flags (proto aiserver.v1). The IDE sets these
|
|
40
|
+
// (modelConfigService.js useModelParameters, entry.js includeLongContextModels).
|
|
41
|
+
// useModelParameters + useCloudAgentEffortModes return parameterized
|
|
42
|
+
// variants (effort/context/fast). includeLongContextModels may populate
|
|
43
|
+
// context_token_limit fields; when those stay empty we still derive limits
|
|
44
|
+
// from each variant's `context` param in mapAvailableModelsResponse.
|
|
45
|
+
body: JSON.stringify({
|
|
46
|
+
includeLongContextModels: true,
|
|
47
|
+
useModelParameters: true,
|
|
48
|
+
useCloudAgentEffortModes: true,
|
|
49
|
+
}),
|
|
40
50
|
});
|
|
41
51
|
if (!res.ok) {
|
|
42
52
|
const text = await res.text().catch(() => "");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursor-opencode-provider",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Use Cursor subscription models from OpenCode via Cursor's Connect-RPC agent protocol",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"files": [
|
|
41
41
|
"dist",
|
|
42
42
|
"LICENSE",
|
|
43
|
+
"DISCLAIMER.md",
|
|
43
44
|
"README.md"
|
|
44
45
|
],
|
|
45
46
|
"scripts": {
|
|
@@ -62,4 +63,4 @@
|
|
|
62
63
|
"@types/node": "^22.15.3",
|
|
63
64
|
"typescript": "^5.8.3"
|
|
64
65
|
}
|
|
65
|
-
}
|
|
66
|
+
}
|