cursor-opencode-provider 0.6.8 → 0.7.0
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 +50 -180
- package/dist/compaction-marker.d.ts +3 -2
- package/dist/compaction-marker.js +3 -2
- package/dist/host-agent-mode.d.ts +32 -0
- package/dist/host-agent-mode.js +92 -0
- package/dist/language-model.d.ts +6 -1
- package/dist/language-model.js +170 -34
- package/dist/models.d.ts +12 -1
- package/dist/models.js +16 -6
- package/dist/opencode2/catalog.d.ts +46 -11
- package/dist/opencode2/catalog.js +60 -45
- package/dist/opencode2/todo-tools.d.ts +64 -0
- package/dist/opencode2/todo-tools.js +165 -0
- package/dist/opencode2/types.d.ts +138 -83
- package/dist/opencode2/types.js +8 -10
- package/dist/plan-execution-kickoff.d.ts +2 -0
- package/dist/plan-execution-kickoff.js +25 -1
- package/dist/plugin-opencode2.d.ts +4 -1
- package/dist/plugin-opencode2.js +335 -154
- package/dist/plugin.js +5 -1
- package/dist/protocol/conversation-persistence.d.ts +2 -0
- package/dist/protocol/conversation-persistence.js +19 -1
- package/dist/protocol/conversation-state.d.ts +4 -0
- package/dist/protocol/conversation-state.js +4 -0
- package/dist/protocol/switch-mode.d.ts +5 -4
- package/dist/protocol/switch-mode.js +12 -4
- package/dist/protocol/tool-call-bridge.js +1 -1
- package/dist/protocol/tools.d.ts +30 -4
- package/dist/protocol/tools.js +185 -58
- package/dist/session.d.ts +7 -1
- package/dist/shared.d.ts +3 -1
- package/dist/shared.js +3 -1
- package/dist/shell-timeout.d.ts +13 -0
- package/dist/shell-timeout.js +38 -0
- package/dist/todo-store.d.ts +29 -0
- package/dist/todo-store.js +78 -0
- package/dist/web-tools.d.ts +13 -0
- package/dist/web-tools.js +83 -5
- package/package.json +6 -3
package/dist/protocol/tools.js
CHANGED
|
@@ -13,6 +13,67 @@ import { BACKGROUND_SHELL_MARKER, buildBackgroundShellCommand, } from "../shell-
|
|
|
13
13
|
// probe (ExecServerMessage #10 → ExecClientMessage #10). request/result share a
|
|
14
14
|
// field number for every exec variant, so this is also the result field.
|
|
15
15
|
export const REQUEST_CONTEXT_RESULT_FIELD = 10;
|
|
16
|
+
export const OPENCODE_1_TOOL_DIALECT = {
|
|
17
|
+
filePathKey: "filePath",
|
|
18
|
+
shellTool: "bash",
|
|
19
|
+
};
|
|
20
|
+
function jsonSchemaProperties(schema) {
|
|
21
|
+
if (!schema || typeof schema !== "object" || Array.isArray(schema))
|
|
22
|
+
return undefined;
|
|
23
|
+
const obj = schema;
|
|
24
|
+
if (obj.properties && typeof obj.properties === "object" && !Array.isArray(obj.properties)) {
|
|
25
|
+
return obj.properties;
|
|
26
|
+
}
|
|
27
|
+
const nested = obj.jsonSchema;
|
|
28
|
+
if (nested && typeof nested === "object" && !Array.isArray(nested)) {
|
|
29
|
+
const props = nested.properties;
|
|
30
|
+
if (props && typeof props === "object" && !Array.isArray(props)) {
|
|
31
|
+
return props;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
/** Prefer `filePath`, then `path` / `file_path` — Cursor and both OpenCode majors. */
|
|
37
|
+
export function opencodePathArg(args) {
|
|
38
|
+
if (!args)
|
|
39
|
+
return undefined;
|
|
40
|
+
return str(args.filePath) ?? str(args.path) ?? str(args.file_path);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Infer the host tool dialect from advertised AI SDK schemas.
|
|
44
|
+
* OpenCode 2.0 read/edit/write require `path` and rename bash → `shell`.
|
|
45
|
+
*/
|
|
46
|
+
export function hostToolDialectFromTools(tools) {
|
|
47
|
+
let filePathKey;
|
|
48
|
+
for (const name of ["read", "write", "edit"]) {
|
|
49
|
+
const tool = tools.find((candidate) => candidate.name === name);
|
|
50
|
+
const props = jsonSchemaProperties(tool?.inputSchema);
|
|
51
|
+
if (!props)
|
|
52
|
+
continue;
|
|
53
|
+
if ("path" in props && !("filePath" in props)) {
|
|
54
|
+
filePathKey = "path";
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
if ("filePath" in props) {
|
|
58
|
+
filePathKey = "filePath";
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const names = new Set(tools.map((tool) => tool.name).filter((name) => typeof name === "string"));
|
|
63
|
+
const shellTool = names.has("shell") && !names.has("bash") ? "shell" : "bash";
|
|
64
|
+
if (!filePathKey) {
|
|
65
|
+
// OpenCode 2 hosts advertise `shell` and `path` together; use that when schemas are opaque.
|
|
66
|
+
filePathKey = shellTool === "shell" ? "path" : "filePath";
|
|
67
|
+
}
|
|
68
|
+
return { filePathKey, shellTool };
|
|
69
|
+
}
|
|
70
|
+
function assignHostFilePath(args, filePath, dialect) {
|
|
71
|
+
delete args.filePath;
|
|
72
|
+
delete args.path;
|
|
73
|
+
delete args.file_path;
|
|
74
|
+
if (filePath)
|
|
75
|
+
args[dialect.filePathKey] = filePath;
|
|
76
|
+
}
|
|
16
77
|
export const CUSTOM_WEBSEARCH_TOOL = "custom_websearch";
|
|
17
78
|
export const CUSTOM_WEBFETCH_TOOL = "custom_webfetch";
|
|
18
79
|
export const CUSTOM_LIST_MCP_RESOURCES_TOOL = "custom_list_mcp_resources";
|
|
@@ -171,15 +232,17 @@ export function resolveCustomWebToolAlias(toolName, aliases) {
|
|
|
171
232
|
export function toolsToMcpDescriptors(tools, providerIdentifier = "opencode", knownMcpServers = []) {
|
|
172
233
|
if (tools.length === 0)
|
|
173
234
|
return [];
|
|
174
|
-
const order = [];
|
|
175
235
|
const byServer = new Map();
|
|
176
|
-
|
|
236
|
+
// OpenCode's tool catalog is a set even though the hook exposes it as an
|
|
237
|
+
// array. Keep both the server and per-server tool order canonical so a host
|
|
238
|
+
// that enumerates the same catalog differently does not invalidate Cursor's
|
|
239
|
+
// RequestContext prefix cache.
|
|
240
|
+
for (const t of [...tools].sort((left, right) => left.name.localeCompare(right.name))) {
|
|
177
241
|
const id = resolveToolServerIdentity(t.sourceName ?? t.name, providerIdentifier, knownMcpServers);
|
|
178
242
|
let list = byServer.get(id.server);
|
|
179
243
|
if (!list) {
|
|
180
244
|
list = [];
|
|
181
245
|
byServer.set(id.server, list);
|
|
182
|
-
order.push(id.server);
|
|
183
246
|
}
|
|
184
247
|
list.push({
|
|
185
248
|
tool_name: t.sourceName ? t.name : id.toolName,
|
|
@@ -187,7 +250,14 @@ export function toolsToMcpDescriptors(tools, providerIdentifier = "opencode", kn
|
|
|
187
250
|
input_schema: encodeJsonAsValue(normalizeInputSchema(t.inputSchema)),
|
|
188
251
|
});
|
|
189
252
|
}
|
|
190
|
-
|
|
253
|
+
const orderedServers = [...byServer.keys()].sort((left, right) => {
|
|
254
|
+
if (left === providerIdentifier)
|
|
255
|
+
return right === providerIdentifier ? 0 : -1;
|
|
256
|
+
if (right === providerIdentifier)
|
|
257
|
+
return 1;
|
|
258
|
+
return left.localeCompare(right);
|
|
259
|
+
});
|
|
260
|
+
return orderedServers.map((server) => ({
|
|
191
261
|
server_name: server,
|
|
192
262
|
server_identifier: server,
|
|
193
263
|
tools: byServer.get(server),
|
|
@@ -262,6 +332,7 @@ const opencodeToolToCursor = {
|
|
|
262
332
|
write: "write_args",
|
|
263
333
|
grep: "grep_args",
|
|
264
334
|
bash: "shell_stream_args",
|
|
335
|
+
shell: "shell_stream_args",
|
|
265
336
|
task: "subagent_args",
|
|
266
337
|
mcp: "mcp_args",
|
|
267
338
|
};
|
|
@@ -348,33 +419,54 @@ const CURSOR_SUBAGENT_TYPE_TO_OPENCODE = {
|
|
|
348
419
|
security_review: "explore",
|
|
349
420
|
};
|
|
350
421
|
const SUBAGENT_CATALOG_MARKER = "Available agent types and the tools they have access to:";
|
|
422
|
+
const SUBAGENT_INLINE_MARKER = "Available subagents:";
|
|
351
423
|
function parseSubagentDescriptionCatalog(description) {
|
|
352
424
|
if (!description)
|
|
353
425
|
return { found: false, agents: [] };
|
|
354
426
|
const marker = description.indexOf(SUBAGENT_CATALOG_MARKER);
|
|
355
|
-
if (marker
|
|
427
|
+
if (marker >= 0) {
|
|
428
|
+
const agents = [];
|
|
429
|
+
const lines = description.slice(marker + SUBAGENT_CATALOG_MARKER.length).split(/\r?\n/);
|
|
430
|
+
let started = false;
|
|
431
|
+
for (const line of lines) {
|
|
432
|
+
const match = line.match(/^\s*-\s+([^:]+):\s*(.*)$/);
|
|
433
|
+
if (!match) {
|
|
434
|
+
if (started && line.trim())
|
|
435
|
+
break;
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
started = true;
|
|
439
|
+
const name = match[1].trim().replace(/^`|`$/g, "");
|
|
440
|
+
if (!name)
|
|
441
|
+
continue;
|
|
442
|
+
const agentDescription = match[2].trim();
|
|
443
|
+
agents.push({
|
|
444
|
+
name,
|
|
445
|
+
...(agentDescription ? { description: agentDescription } : {}),
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
return { found: true, agents };
|
|
449
|
+
}
|
|
450
|
+
// OpenCode 2 `subagent` inlines the list after "Available subagents:".
|
|
451
|
+
const inlineAt = description.indexOf(SUBAGENT_INLINE_MARKER);
|
|
452
|
+
if (inlineAt < 0)
|
|
356
453
|
return { found: false, agents: [] };
|
|
454
|
+
const rest = description.slice(inlineAt + SUBAGENT_INLINE_MARKER.length);
|
|
357
455
|
const agents = [];
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
for (
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
}
|
|
367
|
-
started = true;
|
|
368
|
-
const name = match[1].trim().replace(/^`|`$/g, "");
|
|
369
|
-
if (!name)
|
|
370
|
-
continue;
|
|
371
|
-
const agentDescription = match[2].trim();
|
|
456
|
+
const matches = rest.matchAll(/-\s+([A-Za-z0-9_-]+):\s*/g);
|
|
457
|
+
const hits = [...matches];
|
|
458
|
+
for (let i = 0; i < hits.length; i++) {
|
|
459
|
+
const hit = hits[i];
|
|
460
|
+
const name = hit[1];
|
|
461
|
+
const start = (hit.index ?? 0) + hit[0].length;
|
|
462
|
+
const end = i + 1 < hits.length ? (hits[i + 1].index ?? rest.length) : rest.length;
|
|
463
|
+
const agentDescription = rest.slice(start, end).trim();
|
|
372
464
|
agents.push({
|
|
373
465
|
name,
|
|
374
466
|
...(agentDescription ? { description: agentDescription } : {}),
|
|
375
467
|
});
|
|
376
468
|
}
|
|
377
|
-
return { found:
|
|
469
|
+
return { found: agents.length > 0, agents };
|
|
378
470
|
}
|
|
379
471
|
function subagentTypeEnumValues(schema) {
|
|
380
472
|
const out = new Set();
|
|
@@ -391,7 +483,8 @@ function subagentTypeEnumValues(schema) {
|
|
|
391
483
|
return;
|
|
392
484
|
}
|
|
393
485
|
const record = value;
|
|
394
|
-
if (propertyName === "subagent_type"
|
|
486
|
+
if ((propertyName === "subagent_type" || propertyName === "agent")
|
|
487
|
+
&& Array.isArray(record.enum)) {
|
|
395
488
|
for (const item of record.enum) {
|
|
396
489
|
if (typeof item === "string" && item)
|
|
397
490
|
out.add(item);
|
|
@@ -405,16 +498,17 @@ function subagentTypeEnumValues(schema) {
|
|
|
405
498
|
}
|
|
406
499
|
/** Extract the current host's permission-filtered Task/Actor recipient catalog. */
|
|
407
500
|
export function extractHostSubagentCatalog(tools) {
|
|
408
|
-
const executorTool = tools.find((tool) => tool.name === "task")
|
|
501
|
+
const executorTool = tools.find((tool) => tool.name === "task")
|
|
502
|
+
?? tools.find((tool) => tool.name === "subagent");
|
|
409
503
|
if (!executorTool)
|
|
410
504
|
return { agents: [], complete: true };
|
|
411
505
|
const described = parseSubagentDescriptionCatalog(executorTool.description);
|
|
412
506
|
const enumNames = subagentTypeEnumValues(executorTool.inputSchema);
|
|
413
507
|
const descriptions = new Map(described.agents.map((agent) => [agent.name, agent.description]));
|
|
414
|
-
// A structured subagent_type enum is stricter than prose, so it is authoritative.
|
|
508
|
+
// A structured subagent_type/agent enum is stricter than prose, so it is authoritative.
|
|
415
509
|
const names = new Set(enumNames.length > 0 ? enumNames : described.agents.map((agent) => agent.name));
|
|
416
510
|
return {
|
|
417
|
-
executor: "task",
|
|
511
|
+
executor: executorTool.name === "subagent" ? "subagent" : "task",
|
|
418
512
|
agents: [...names].map((name) => ({
|
|
419
513
|
name,
|
|
420
514
|
...(descriptions.get(name) ? { description: descriptions.get(name) } : {}),
|
|
@@ -474,13 +568,19 @@ export function remapNativeSubagentForCatalog(parsed, advertisedToolNames, catal
|
|
|
474
568
|
if (parsed.resultField !== "subagent_result" || parsed.toolName !== "task")
|
|
475
569
|
return;
|
|
476
570
|
const advertised = new Set(advertisedToolNames);
|
|
477
|
-
|
|
571
|
+
const executor = advertised.has("task")
|
|
572
|
+
? "task"
|
|
573
|
+
: advertised.has("subagent")
|
|
574
|
+
? "subagent"
|
|
575
|
+
: undefined;
|
|
576
|
+
if (!executor)
|
|
478
577
|
return;
|
|
479
|
-
const executor = "task";
|
|
480
578
|
const description = str(parsed.args.description) ?? "";
|
|
481
579
|
const prompt = str(parsed.args.prompt) ?? "";
|
|
482
580
|
const cursorSubagentType = str(parsed.resultMetadata?.cursor_subagent_type) ??
|
|
483
|
-
str(parsed.args.subagent_type) ??
|
|
581
|
+
str(parsed.args.subagent_type) ??
|
|
582
|
+
str(parsed.args.agent) ??
|
|
583
|
+
"";
|
|
484
584
|
const subagentType = resolveCursorSubagentType(cursorSubagentType, catalog);
|
|
485
585
|
if (!subagentType) {
|
|
486
586
|
const available = catalog?.agents.map((agent) => agent.name).join(", ") || "none";
|
|
@@ -489,15 +589,23 @@ export function remapNativeSubagentForCatalog(parsed, advertisedToolNames, catal
|
|
|
489
589
|
`Available subagents: ${available}.`;
|
|
490
590
|
return;
|
|
491
591
|
}
|
|
492
|
-
const resumeAgentId = str(parsed.args.task_id);
|
|
592
|
+
const resumeAgentId = str(parsed.args.task_id) ?? str(parsed.args.sessionID);
|
|
493
593
|
parsed.toolName = executor;
|
|
494
|
-
parsed.args =
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
594
|
+
parsed.args = executor === "subagent"
|
|
595
|
+
? {
|
|
596
|
+
agent: subagentType,
|
|
597
|
+
description,
|
|
598
|
+
prompt,
|
|
599
|
+
...(resumeAgentId ? { sessionID: resumeAgentId } : {}),
|
|
600
|
+
...(parsed.args.background === true ? { background: true } : {}),
|
|
601
|
+
}
|
|
602
|
+
: {
|
|
603
|
+
description,
|
|
604
|
+
prompt,
|
|
605
|
+
subagent_type: subagentType,
|
|
606
|
+
...(resumeAgentId ? { task_id: resumeAgentId } : {}),
|
|
607
|
+
...(parsed.args.background === true ? { background: true } : {}),
|
|
608
|
+
};
|
|
501
609
|
}
|
|
502
610
|
/** Matches Cursor's own pre-write read threshold (`local-exec` 52428800). */
|
|
503
611
|
const MAX_EDIT_SOURCE_BYTES = 50 * 1024 * 1024;
|
|
@@ -625,7 +733,7 @@ export function remapCorrelatedEditWriteForCatalog(parsed, advertisedToolNames,
|
|
|
625
733
|
const advertised = new Set(advertisedToolNames);
|
|
626
734
|
if (!advertised.has("edit") && !advertised.has(APPLY_PATCH_TOOL))
|
|
627
735
|
return false;
|
|
628
|
-
const filePath =
|
|
736
|
+
const filePath = opencodePathArg(parsed.args);
|
|
629
737
|
const content = stringValue(parsed.args.content);
|
|
630
738
|
if (!filePath || content === undefined || !editPath)
|
|
631
739
|
return false;
|
|
@@ -651,8 +759,11 @@ export function remapCorrelatedEditWriteForCatalog(parsed, advertisedToolNames,
|
|
|
651
759
|
if (!replacement)
|
|
652
760
|
return false;
|
|
653
761
|
parsed.toolName = "edit";
|
|
762
|
+
const filePathKey = typeof parsed.args.path === "string" && typeof parsed.args.filePath !== "string"
|
|
763
|
+
? "path"
|
|
764
|
+
: "filePath";
|
|
654
765
|
parsed.args = {
|
|
655
|
-
filePath,
|
|
766
|
+
[filePathKey]: filePath,
|
|
656
767
|
oldString: replacement.oldString,
|
|
657
768
|
newString: replacement.newString,
|
|
658
769
|
};
|
|
@@ -687,7 +798,7 @@ export function remapEditToolsForCatalog(parsed, advertisedToolNames, workspaceR
|
|
|
687
798
|
if (advertised.has(parsed.toolName) || !advertised.has(APPLY_PATCH_TOOL))
|
|
688
799
|
return;
|
|
689
800
|
const requested = parsed.toolName;
|
|
690
|
-
const filePath =
|
|
801
|
+
const filePath = opencodePathArg(parsed.args);
|
|
691
802
|
const refuse = (reason) => {
|
|
692
803
|
parsed.toolName = APPLY_PATCH_TOOL;
|
|
693
804
|
parsed.args = {};
|
|
@@ -775,9 +886,7 @@ export function rejectPartialReadMutation(parsed) {
|
|
|
775
886
|
|| !content.includes("[Partial read:")
|
|
776
887
|
|| !content.includes("It is NOT the complete file."))
|
|
777
888
|
return;
|
|
778
|
-
const filePath =
|
|
779
|
-
? parsed.args.filePath
|
|
780
|
-
: "the target file";
|
|
889
|
+
const filePath = opencodePathArg(parsed.args) ?? "the target file";
|
|
781
890
|
const nextOffset = /Continue with offset=(\d+)/.exec(content)?.[1];
|
|
782
891
|
parsed.localError =
|
|
783
892
|
`NO FILE CHANGE WAS MADE. Refusing a whole-file mutation of ${JSON.stringify(filePath)} ` +
|
|
@@ -786,7 +895,7 @@ export function rejectPartialReadMutation(parsed) {
|
|
|
786
895
|
? `Read the file from offset=${nextOffset}, then use a targeted edit or Update File patch.`
|
|
787
896
|
: "Read the remaining file ranges, then use a targeted edit or Update File patch.");
|
|
788
897
|
}
|
|
789
|
-
export function parseExecServerMessage(msg) {
|
|
898
|
+
export function parseExecServerMessage(msg, dialect = OPENCODE_1_TOOL_DIALECT) {
|
|
790
899
|
const id = msg.id;
|
|
791
900
|
if (id === undefined)
|
|
792
901
|
return undefined;
|
|
@@ -823,7 +932,7 @@ export function parseExecServerMessage(msg) {
|
|
|
823
932
|
return {
|
|
824
933
|
id,
|
|
825
934
|
execId,
|
|
826
|
-
toolName:
|
|
935
|
+
toolName: dialect.shellTool,
|
|
827
936
|
args,
|
|
828
937
|
resultField,
|
|
829
938
|
resultMetadata: {
|
|
@@ -877,7 +986,7 @@ export function parseExecServerMessage(msg) {
|
|
|
877
986
|
// tool name (Cursor's model may have shortened "opencode-read" → "read") and
|
|
878
987
|
// decode the argument map back into JSON for opencode to execute.
|
|
879
988
|
const m = msg.mcp_args ?? {};
|
|
880
|
-
const mapped = mapCursorArgsToOpencode(mcpRealToolName(m), decodeMcpArgs(m.args), "mcp_args");
|
|
989
|
+
const mapped = mapCursorArgsToOpencode(mcpRealToolName(m), decodeMcpArgs(m.args), "mcp_args", dialect);
|
|
881
990
|
return {
|
|
882
991
|
id,
|
|
883
992
|
execId,
|
|
@@ -896,8 +1005,7 @@ export function parseExecServerMessage(msg) {
|
|
|
896
1005
|
const oldString = replacement ? stringValue(replacement.old_text) : undefined;
|
|
897
1006
|
const newString = replacement ? stringValue(replacement.new_text) : undefined;
|
|
898
1007
|
const args = {};
|
|
899
|
-
|
|
900
|
-
args.filePath = path;
|
|
1008
|
+
assignHostFilePath(args, path, dialect);
|
|
901
1009
|
if (oldString !== undefined)
|
|
902
1010
|
args.oldString = oldString;
|
|
903
1011
|
if (newString !== undefined)
|
|
@@ -916,7 +1024,7 @@ export function parseExecServerMessage(msg) {
|
|
|
916
1024
|
const toolName = cursorToolToOpencode[execVariant];
|
|
917
1025
|
if (!toolName)
|
|
918
1026
|
return undefined;
|
|
919
|
-
const mapped = mapCursorArgsToOpencode(toolName, msg[execVariant] ?? {}, execVariant);
|
|
1027
|
+
const mapped = mapCursorArgsToOpencode(toolName, msg[execVariant] ?? {}, execVariant, dialect);
|
|
920
1028
|
const rawArgs = msg[execVariant] ?? {};
|
|
921
1029
|
const resultMetadata = execVariant === "shell_stream_args" || execVariant === "shell_args"
|
|
922
1030
|
? shellStreamResultMetadata(rawArgs)
|
|
@@ -980,7 +1088,7 @@ function shellStreamResultMetadata(raw) {
|
|
|
980
1088
|
* Without this, OpenCode rejects calls with InvalidArgumentsError
|
|
981
1089
|
* (e.g. `path` instead of `filePath`, `file_text` instead of `content`).
|
|
982
1090
|
*/
|
|
983
|
-
export function mapCursorArgsToOpencode(toolName, raw, execVariant) {
|
|
1091
|
+
export function mapCursorArgsToOpencode(toolName, raw, execVariant, dialect = OPENCODE_1_TOOL_DIALECT) {
|
|
984
1092
|
const cleaned = {};
|
|
985
1093
|
for (const [k, v] of Object.entries(raw)) {
|
|
986
1094
|
if (v === undefined || v === null)
|
|
@@ -996,7 +1104,7 @@ export function mapCursorArgsToOpencode(toolName, raw, execVariant) {
|
|
|
996
1104
|
// Native ls_args → OpenCode read (directory listing via read).
|
|
997
1105
|
if (execVariant === "ls_args") {
|
|
998
1106
|
const filePath = str(cleaned.path) ?? str(cleaned.filePath);
|
|
999
|
-
return { toolName: "read", args: filePath ? { filePath } : {} };
|
|
1107
|
+
return { toolName: "read", args: filePath ? { [dialect.filePathKey]: filePath } : {} };
|
|
1000
1108
|
}
|
|
1001
1109
|
// F11: Cursor native delete_args → OpenCode bash. No delete builtin exists, so
|
|
1002
1110
|
// we emulate with `rm -f -- <path>` (shell-quoted). Same permission boundary
|
|
@@ -1004,7 +1112,7 @@ export function mapCursorArgsToOpencode(toolName, raw, execVariant) {
|
|
|
1004
1112
|
if (execVariant === "delete_args") {
|
|
1005
1113
|
const target = str(cleaned.path) ?? str(cleaned.filePath);
|
|
1006
1114
|
return {
|
|
1007
|
-
toolName:
|
|
1115
|
+
toolName: dialect.shellTool,
|
|
1008
1116
|
args: target ? { command: `rm -f -- ${shellQuote(target)}` } : { command: "true" },
|
|
1009
1117
|
};
|
|
1010
1118
|
}
|
|
@@ -1012,8 +1120,7 @@ export function mapCursorArgsToOpencode(toolName, raw, execVariant) {
|
|
|
1012
1120
|
case "read": {
|
|
1013
1121
|
const args = {};
|
|
1014
1122
|
const filePath = str(cleaned.filePath) ?? str(cleaned.path) ?? str(cleaned.file_path);
|
|
1015
|
-
|
|
1016
|
-
args.filePath = filePath;
|
|
1123
|
+
assignHostFilePath(args, filePath, dialect);
|
|
1017
1124
|
// Cursor often sends offset=0/limit=0 as protobuf defaults. OpenCode's
|
|
1018
1125
|
// read treats limit=0 as "read zero lines" (empty content) — omit zeros.
|
|
1019
1126
|
const offset = num(cleaned.offset);
|
|
@@ -1027,8 +1134,7 @@ export function mapCursorArgsToOpencode(toolName, raw, execVariant) {
|
|
|
1027
1134
|
case "write": {
|
|
1028
1135
|
const args = {};
|
|
1029
1136
|
const filePath = str(cleaned.filePath) ?? str(cleaned.path) ?? str(cleaned.file_path);
|
|
1030
|
-
|
|
1031
|
-
args.filePath = filePath;
|
|
1137
|
+
assignHostFilePath(args, filePath, dialect);
|
|
1032
1138
|
// Cursor's LocalWriteExecutor prefers `file_bytes` whenever it is
|
|
1033
1139
|
// non-empty and only falls back to `file_text`; mirror that order so a
|
|
1034
1140
|
// byte-encoded write is not silently seen as empty content.
|
|
@@ -1060,8 +1166,7 @@ export function mapCursorArgsToOpencode(toolName, raw, execVariant) {
|
|
|
1060
1166
|
}
|
|
1061
1167
|
const args = {};
|
|
1062
1168
|
const filePath = str(cleaned.filePath) ?? str(cleaned.path) ?? str(cleaned.file_path);
|
|
1063
|
-
|
|
1064
|
-
args.filePath = filePath;
|
|
1169
|
+
assignHostFilePath(args, filePath, dialect);
|
|
1065
1170
|
const oldString = stringValue(cleaned.oldString) ?? stringValue(cleaned.old_string);
|
|
1066
1171
|
if (oldString !== undefined)
|
|
1067
1172
|
args.oldString = oldString;
|
|
@@ -1072,7 +1177,8 @@ export function mapCursorArgsToOpencode(toolName, raw, execVariant) {
|
|
|
1072
1177
|
args.replaceAll = cleaned.replaceAll;
|
|
1073
1178
|
return { toolName: "edit", args };
|
|
1074
1179
|
}
|
|
1075
|
-
case "bash":
|
|
1180
|
+
case "bash":
|
|
1181
|
+
case "shell": {
|
|
1076
1182
|
const args = {};
|
|
1077
1183
|
const command = str(cleaned.command);
|
|
1078
1184
|
if (command)
|
|
@@ -1083,7 +1189,7 @@ export function mapCursorArgsToOpencode(toolName, raw, execVariant) {
|
|
|
1083
1189
|
const timeout = num(cleaned.timeout);
|
|
1084
1190
|
if (timeout !== undefined)
|
|
1085
1191
|
args.timeout = timeout;
|
|
1086
|
-
return { toolName:
|
|
1192
|
+
return { toolName: dialect.shellTool, args };
|
|
1087
1193
|
}
|
|
1088
1194
|
case "grep": {
|
|
1089
1195
|
const pattern = str(cleaned.pattern);
|
|
@@ -1193,12 +1299,33 @@ function num(v) {
|
|
|
1193
1299
|
return Number(v);
|
|
1194
1300
|
return undefined;
|
|
1195
1301
|
}
|
|
1302
|
+
/**
|
|
1303
|
+
* Fallback OpenCode task title when Cursor's SubagentArgs has no description.
|
|
1304
|
+
* Prefer {@link preferCorrelatedTaskDescription} when a TaskToolCall is available —
|
|
1305
|
+
* that carries Cursor's real 3–5 word title. This only slices the prompt.
|
|
1306
|
+
*/
|
|
1196
1307
|
function describeSubagentTask(prompt, subagentType) {
|
|
1197
1308
|
const words = prompt?.replace(/\s+/g, " ").trim().split(" ").filter(Boolean).slice(0, 5);
|
|
1198
1309
|
if (words?.length)
|
|
1199
1310
|
return words.join(" ");
|
|
1200
1311
|
return `${subagentType || "Delegated"} task`;
|
|
1201
1312
|
}
|
|
1313
|
+
/**
|
|
1314
|
+
* Replace the prompt-derived task title with Cursor's TaskToolCall description
|
|
1315
|
+
* when the display call is correlated via tool_call_id. SubagentArgs itself has
|
|
1316
|
+
* no description field, so without this OpenCode shows a cut-off first-five-words
|
|
1317
|
+
* slice of the prompt.
|
|
1318
|
+
*/
|
|
1319
|
+
export function preferCorrelatedTaskDescription(parsed, description) {
|
|
1320
|
+
if (parsed.toolName !== "task" && parsed.toolName !== "subagent")
|
|
1321
|
+
return;
|
|
1322
|
+
if (parsed.resultField !== "subagent_result")
|
|
1323
|
+
return;
|
|
1324
|
+
const trimmed = description?.trim();
|
|
1325
|
+
if (!trimmed)
|
|
1326
|
+
return;
|
|
1327
|
+
parsed.args.description = trimmed;
|
|
1328
|
+
}
|
|
1202
1329
|
function shellQuote(s) {
|
|
1203
1330
|
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
1204
1331
|
}
|
package/dist/session.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { BidiStream } from "./transport/connect.js";
|
|
2
2
|
import { type CursorProviderError } from "./errors.js";
|
|
3
3
|
import { type SessionActivitySource } from "./activity.js";
|
|
4
|
-
import type { HostSubagentCatalog, OpencodeToolDef, ToolAliasRegistry } from "./protocol/tools.js";
|
|
4
|
+
import type { HostSubagentCatalog, HostToolDialect, OpencodeToolDef, ToolAliasRegistry } from "./protocol/tools.js";
|
|
5
5
|
import type { CursorConversationTokenDetails } from "./protocol/token-details.js";
|
|
6
6
|
export type Frame = {
|
|
7
7
|
flags: number;
|
|
@@ -134,6 +134,10 @@ export type CursorSession = {
|
|
|
134
134
|
};
|
|
135
135
|
/** OpenCode session whose own or descendant activity renews tool leases. */
|
|
136
136
|
openCodeSessionId?: string;
|
|
137
|
+
/** Host primary agent whose prompt/permissions this Run was seeded with. */
|
|
138
|
+
hostAgent?: string;
|
|
139
|
+
/** Stable host-system + provider-guidance identity for restart validation. */
|
|
140
|
+
stableSystemPromptHash?: string;
|
|
137
141
|
/** Completed compaction must rebase once before resuming a normal agent. */
|
|
138
142
|
postCompactionRebase?: boolean;
|
|
139
143
|
/** Last real host catalog, retained only as a lifecycle-turn fallback. */
|
|
@@ -170,6 +174,8 @@ export type CursorSession = {
|
|
|
170
174
|
toolDescriptors: Array<Record<string, unknown>>;
|
|
171
175
|
/** Cursor-facing web alias → exact executable host tool for this Run. */
|
|
172
176
|
toolAliases?: ToolAliasRegistry;
|
|
177
|
+
/** OpenCode 1.x `filePath`/`bash` vs 2.x `path`/`shell`, from advertised schemas. */
|
|
178
|
+
hostToolDialect?: HostToolDialect;
|
|
173
179
|
/** Spawnable host agents extracted from this turn's Task/Actor definition. */
|
|
174
180
|
subagentCatalog?: HostSubagentCatalog;
|
|
175
181
|
/** Full RequestContext for exec #10 replies. */
|
package/dist/shared.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export declare const FALLBACK_CLIENT_VERSION = "cli-2026.07.09-a3815c0";
|
|
|
4
4
|
export declare const CURSOR_PROVIDER_ID = "cursor";
|
|
5
5
|
/** Private provider option injected by the OpenCode plugin for summary turns. */
|
|
6
6
|
export declare const CURSOR_COMPACTION_OPTION = "opencodeCompaction";
|
|
7
|
+
/** Current OpenCode primary agent; used to invalidate incompatible checkpoints. */
|
|
8
|
+
export declare const CURSOR_HOST_AGENT_OPTION = "opencodeHostAgent";
|
|
7
9
|
export declare const TOKEN_EXPIRY_THRESHOLD_S = 300;
|
|
8
10
|
export declare const RUN_PATH = "/agent.v1.AgentService/Run";
|
|
9
11
|
export declare const AVAILABLE_MODELS_PATH = "/aiserver.v1.AiService/AvailableModels";
|
|
@@ -13,7 +15,7 @@ export declare const MODEL_CACHE_FILE = "cursor-models.json";
|
|
|
13
15
|
export declare const MODEL_CACHE_SCHEMA_VERSION = 3;
|
|
14
16
|
export declare const MODEL_CACHE_TTL_MS = 86400000;
|
|
15
17
|
export declare const CONVERSATION_CACHE_DIR = "cursor-conversations";
|
|
16
|
-
export declare const CONVERSATION_CACHE_SCHEMA_VERSION =
|
|
18
|
+
export declare const CONVERSATION_CACHE_SCHEMA_VERSION = 4;
|
|
17
19
|
export declare const CONVERSATION_CACHE_TTL_MS = 86400000;
|
|
18
20
|
export declare const VERSION_CACHE_FILE = "cursor-client-version.json";
|
|
19
21
|
export declare const CONTENT_TYPE_CONNECT_PROTO = "application/connect+proto";
|
package/dist/shared.js
CHANGED
|
@@ -4,6 +4,8 @@ export const FALLBACK_CLIENT_VERSION = "cli-2026.07.09-a3815c0";
|
|
|
4
4
|
export const CURSOR_PROVIDER_ID = "cursor";
|
|
5
5
|
/** Private provider option injected by the OpenCode plugin for summary turns. */
|
|
6
6
|
export const CURSOR_COMPACTION_OPTION = "opencodeCompaction";
|
|
7
|
+
/** Current OpenCode primary agent; used to invalidate incompatible checkpoints. */
|
|
8
|
+
export const CURSOR_HOST_AGENT_OPTION = "opencodeHostAgent";
|
|
7
9
|
export const TOKEN_EXPIRY_THRESHOLD_S = 300;
|
|
8
10
|
export const RUN_PATH = "/agent.v1.AgentService/Run";
|
|
9
11
|
export const AVAILABLE_MODELS_PATH = "/aiserver.v1.AiService/AvailableModels";
|
|
@@ -13,7 +15,7 @@ export const MODEL_CACHE_FILE = "cursor-models.json";
|
|
|
13
15
|
export const MODEL_CACHE_SCHEMA_VERSION = 3;
|
|
14
16
|
export const MODEL_CACHE_TTL_MS = 86_400_000;
|
|
15
17
|
export const CONVERSATION_CACHE_DIR = "cursor-conversations";
|
|
16
|
-
export const CONVERSATION_CACHE_SCHEMA_VERSION =
|
|
18
|
+
export const CONVERSATION_CACHE_SCHEMA_VERSION = 4;
|
|
17
19
|
export const CONVERSATION_CACHE_TTL_MS = 86_400_000;
|
|
18
20
|
export const VERSION_CACHE_FILE = "cursor-client-version.json";
|
|
19
21
|
export const CONTENT_TYPE_CONNECT_PROTO = "application/connect+proto";
|
package/dist/shell-timeout.d.ts
CHANGED
|
@@ -85,9 +85,19 @@ export declare function releaseCursorShellEnv(toolCallId: string): void;
|
|
|
85
85
|
* same materialized wrapper backs the direct-command sh/dash fallback.
|
|
86
86
|
*/
|
|
87
87
|
export declare function cursorShellEnvForCall(toolCallId: string | undefined): Record<string, string> | undefined;
|
|
88
|
+
/**
|
|
89
|
+
* OpenCode 2.0 `shell.create.before` has no tool-call id. Correlate the pending
|
|
90
|
+
* wrap by command + working directory when possible, then fall back to the
|
|
91
|
+
* original command for hosts that omit the directory.
|
|
92
|
+
*/
|
|
93
|
+
export declare function cursorShellEnvForCommand(command: string | undefined, workingDirectory?: string): Record<string, string> | undefined;
|
|
88
94
|
/**
|
|
89
95
|
* Strip private wrapper sentinels / OpenCode timeout envelopes for display.
|
|
90
96
|
* Does not record outcomes — use {@link captureCursorShellResult} for that.
|
|
97
|
+
*
|
|
98
|
+
* OpenCode 2.0 `Tool.Result.output` is structured (an object for shell), not
|
|
99
|
+
* a string. Non-string input is returned unchanged so the 2.0 after-hook can
|
|
100
|
+
* pass structured output through safely.
|
|
91
101
|
*/
|
|
92
102
|
export declare function sanitizeCursorShellDisplayOutput(output: string, policy?: CursorShellPolicy): string;
|
|
93
103
|
/** Sanitize a secondary display string (e.g. Bash `metadata.output`) for a registered call. */
|
|
@@ -95,6 +105,9 @@ export declare function sanitizeRegisteredCursorShellOutput(toolCallId: string,
|
|
|
95
105
|
/**
|
|
96
106
|
* Capture Bash completion in the classic plugin's after hook. Returns the
|
|
97
107
|
* sanitized output that OpenCode should store and render.
|
|
108
|
+
*
|
|
109
|
+
* Guards non-string output (OpenCode 2.0 structured `Tool.Result.output`)
|
|
110
|
+
* by returning it unchanged.
|
|
98
111
|
*/
|
|
99
112
|
export declare function captureCursorShellResult(toolCallId: string, output: string, metadata?: Record<string, unknown>): string;
|
|
100
113
|
/** Consume the structured result, with an inline fallback when no plugin hook ran. */
|
package/dist/shell-timeout.js
CHANGED
|
@@ -269,6 +269,31 @@ export function cursorShellEnvForCall(toolCallId) {
|
|
|
269
269
|
pendingEnvWraps.delete(toolCallId);
|
|
270
270
|
return wrap.env;
|
|
271
271
|
}
|
|
272
|
+
/**
|
|
273
|
+
* OpenCode 2.0 `shell.create.before` has no tool-call id. Correlate the pending
|
|
274
|
+
* wrap by command + working directory when possible, then fall back to the
|
|
275
|
+
* original command for hosts that omit the directory.
|
|
276
|
+
*/
|
|
277
|
+
export function cursorShellEnvForCommand(command, workingDirectory) {
|
|
278
|
+
if (typeof command !== "string" || !command)
|
|
279
|
+
return undefined;
|
|
280
|
+
if (workingDirectory) {
|
|
281
|
+
for (const [id, policy] of policies) {
|
|
282
|
+
if (!pendingEnvWraps.has(id))
|
|
283
|
+
continue;
|
|
284
|
+
if (policy.command === command && policy.workingDirectory === workingDirectory) {
|
|
285
|
+
return cursorShellEnvForCall(id);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
for (const [id, policy] of policies) {
|
|
290
|
+
if (!pendingEnvWraps.has(id))
|
|
291
|
+
continue;
|
|
292
|
+
if (policy.command === command)
|
|
293
|
+
return cursorShellEnvForCall(id);
|
|
294
|
+
}
|
|
295
|
+
return undefined;
|
|
296
|
+
}
|
|
272
297
|
function withoutMarker(output, index) {
|
|
273
298
|
let clean = output.slice(0, index).replace(/[\t ]+$/gm, "").replace(/\n{2,}$/, "\n");
|
|
274
299
|
if (clean.trim() === "" || clean.trim() === "(no output)")
|
|
@@ -390,8 +415,14 @@ function parseBackgroundSpawnOutcome(output, policy) {
|
|
|
390
415
|
/**
|
|
391
416
|
* Strip private wrapper sentinels / OpenCode timeout envelopes for display.
|
|
392
417
|
* Does not record outcomes — use {@link captureCursorShellResult} for that.
|
|
418
|
+
*
|
|
419
|
+
* OpenCode 2.0 `Tool.Result.output` is structured (an object for shell), not
|
|
420
|
+
* a string. Non-string input is returned unchanged so the 2.0 after-hook can
|
|
421
|
+
* pass structured output through safely.
|
|
393
422
|
*/
|
|
394
423
|
export function sanitizeCursorShellDisplayOutput(output, policy) {
|
|
424
|
+
if (typeof output !== "string")
|
|
425
|
+
return output;
|
|
395
426
|
if (policy?.backgroundSpawn) {
|
|
396
427
|
const spawn = parseBackgroundSpawnOutcome(output, policy);
|
|
397
428
|
if (spawn)
|
|
@@ -413,6 +444,8 @@ export function sanitizeCursorShellDisplayOutput(output, policy) {
|
|
|
413
444
|
}
|
|
414
445
|
/** Sanitize a secondary display string (e.g. Bash `metadata.output`) for a registered call. */
|
|
415
446
|
export function sanitizeRegisteredCursorShellOutput(toolCallId, output) {
|
|
447
|
+
if (typeof output !== "string")
|
|
448
|
+
return output;
|
|
416
449
|
if (typeof toolCallId !== "string" || !toolCallId)
|
|
417
450
|
return output;
|
|
418
451
|
return sanitizeCursorShellDisplayOutput(output, policies.get(toolCallId));
|
|
@@ -420,8 +453,13 @@ export function sanitizeRegisteredCursorShellOutput(toolCallId, output) {
|
|
|
420
453
|
/**
|
|
421
454
|
* Capture Bash completion in the classic plugin's after hook. Returns the
|
|
422
455
|
* sanitized output that OpenCode should store and render.
|
|
456
|
+
*
|
|
457
|
+
* Guards non-string output (OpenCode 2.0 structured `Tool.Result.output`)
|
|
458
|
+
* by returning it unchanged.
|
|
423
459
|
*/
|
|
424
460
|
export function captureCursorShellResult(toolCallId, output, metadata) {
|
|
461
|
+
if (typeof output !== "string")
|
|
462
|
+
return output;
|
|
425
463
|
if (typeof toolCallId !== "string" || !toolCallId.startsWith("cursor_"))
|
|
426
464
|
return output;
|
|
427
465
|
const policy = policies.get(toolCallId);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-session todo list used when the host does not ship `todowrite` /
|
|
3
|
+
* `todoread` (OpenCode 2 dropped those builtins; OpenCode 1.x still has
|
|
4
|
+
* `todowrite`).
|
|
5
|
+
*
|
|
6
|
+
* In-memory only — same lifetime as the provider process. A restart clears
|
|
7
|
+
* the list until the next write/read, matching the mirrored Cursor-todo
|
|
8
|
+
* snapshot in `language-model.ts`.
|
|
9
|
+
*/
|
|
10
|
+
export declare const TODOWRITE_TOOL = "todowrite";
|
|
11
|
+
export declare const TODOREAD_TOOL = "todoread";
|
|
12
|
+
export declare const TODO_STATUSES: readonly ["pending", "in_progress", "completed", "cancelled"];
|
|
13
|
+
export type TodoStatus = (typeof TODO_STATUSES)[number];
|
|
14
|
+
export declare const TODO_PRIORITIES: readonly ["high", "medium", "low"];
|
|
15
|
+
export type TodoPriority = (typeof TODO_PRIORITIES)[number];
|
|
16
|
+
export type SessionTodo = {
|
|
17
|
+
id: string;
|
|
18
|
+
content: string;
|
|
19
|
+
status: TodoStatus;
|
|
20
|
+
priority: TodoPriority;
|
|
21
|
+
};
|
|
22
|
+
/** Normalize one host/Cursor todo item. Drops entries with empty content. */
|
|
23
|
+
export declare function normalizeSessionTodo(value: unknown, index: number): SessionTodo | undefined;
|
|
24
|
+
export declare function normalizeSessionTodos(value: unknown): SessionTodo[];
|
|
25
|
+
export declare function getSessionTodos(sessionID: string): SessionTodo[];
|
|
26
|
+
export declare function setSessionTodos(sessionID: string, todos: unknown): SessionTodo[];
|
|
27
|
+
export declare function clearSessionTodos(sessionID: string): void;
|
|
28
|
+
/** Test-only: drop every session list. */
|
|
29
|
+
export declare function clearAllSessionTodos(): void;
|