cursor-opencode-provider 0.7.6 → 0.8.1

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.
Files changed (101) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/README.md +36 -15
  3. package/SECURITY.md +43 -7
  4. package/dist/agent-url.js +2 -2
  5. package/dist/auth-renewal.d.ts +85 -0
  6. package/dist/auth-renewal.js +284 -0
  7. package/dist/auth.d.ts +38 -22
  8. package/dist/auth.js +79 -96
  9. package/dist/classic-tools.d.ts +15 -0
  10. package/dist/classic-tools.js +45 -0
  11. package/dist/context/build.d.ts +62 -7
  12. package/dist/context/build.js +127 -95
  13. package/dist/context/dynamic-catalog.d.ts +2 -58
  14. package/dist/context/dynamic-catalog.js +16 -161
  15. package/dist/context/epoch.d.ts +6 -4
  16. package/dist/context/epoch.js +22 -10
  17. package/dist/context/frozen.js +42 -15
  18. package/dist/context/index.d.ts +3 -1
  19. package/dist/context/index.js +3 -1
  20. package/dist/context/overlay.d.ts +2 -23
  21. package/dist/context/overlay.js +6 -30
  22. package/dist/context/paths.d.ts +51 -21
  23. package/dist/context/paths.js +78 -27
  24. package/dist/context/rules.d.ts +3 -15
  25. package/dist/context/rules.js +10 -186
  26. package/dist/context/skills-bridge.d.ts +22 -0
  27. package/dist/context/skills-bridge.js +20 -0
  28. package/dist/context/skills.d.ts +31 -8
  29. package/dist/context/skills.js +187 -86
  30. package/dist/debug.d.ts +2 -0
  31. package/dist/debug.js +4 -0
  32. package/dist/errors.d.ts +13 -0
  33. package/dist/errors.js +27 -3
  34. package/dist/host-agent-mode.d.ts +53 -4
  35. package/dist/host-agent-mode.js +119 -15
  36. package/dist/host-plan-file.d.ts +28 -0
  37. package/dist/host-plan-file.js +76 -0
  38. package/dist/image-input.d.ts +18 -0
  39. package/dist/image-input.js +119 -28
  40. package/dist/image-save.d.ts +10 -2
  41. package/dist/image-save.js +35 -23
  42. package/dist/index.d.ts +10 -0
  43. package/dist/language-model.d.ts +86 -25
  44. package/dist/language-model.js +1295 -369
  45. package/dist/model-config.d.ts +9 -0
  46. package/dist/model-config.js +28 -1
  47. package/dist/opencode2/catalog.d.ts +1 -0
  48. package/dist/opencode2/catalog.js +2 -0
  49. package/dist/opencode2/image-save-tool.d.ts +8 -0
  50. package/dist/opencode2/image-save-tool.js +84 -0
  51. package/dist/opencode2/integration.d.ts +30 -7
  52. package/dist/opencode2/integration.js +75 -46
  53. package/dist/opencode2/types.d.ts +15 -0
  54. package/dist/parallel-step.d.ts +65 -0
  55. package/dist/parallel-step.js +102 -0
  56. package/dist/plugin-opencode2.js +58 -56
  57. package/dist/plugin.d.ts +0 -10
  58. package/dist/plugin.js +207 -127
  59. package/dist/pricing-data.d.ts +14 -1
  60. package/dist/pricing-data.js +14 -1
  61. package/dist/protocol/ask-question.d.ts +12 -1
  62. package/dist/protocol/ask-question.js +77 -3
  63. package/dist/protocol/blob-store.js +3 -2
  64. package/dist/protocol/conversation-bind.js +1 -1
  65. package/dist/protocol/conversation-persistence.d.ts +2 -0
  66. package/dist/protocol/conversation-persistence.js +11 -1
  67. package/dist/protocol/conversation-state.d.ts +2 -0
  68. package/dist/protocol/conversation-state.js +19 -0
  69. package/dist/protocol/create-plan.d.ts +47 -25
  70. package/dist/protocol/create-plan.js +35 -24
  71. package/dist/protocol/interactions.d.ts +26 -17
  72. package/dist/protocol/interactions.js +80 -31
  73. package/dist/protocol/messages.js +96 -18
  74. package/dist/protocol/request.d.ts +16 -9
  75. package/dist/protocol/request.js +17 -25
  76. package/dist/protocol/switch-mode.d.ts +66 -27
  77. package/dist/protocol/switch-mode.js +147 -73
  78. package/dist/protocol/token-details.d.ts +2 -0
  79. package/dist/protocol/token-details.js +11 -1
  80. package/dist/protocol/tool-call-bridge.d.ts +4 -0
  81. package/dist/protocol/tool-call-bridge.js +14 -0
  82. package/dist/protocol/tools.d.ts +33 -11
  83. package/dist/protocol/tools.js +165 -103
  84. package/dist/protocol/turn-provenance.d.ts +52 -0
  85. package/dist/protocol/turn-provenance.js +174 -0
  86. package/dist/replay-safety.d.ts +6 -0
  87. package/dist/replay-safety.js +56 -6
  88. package/dist/session.d.ts +44 -2
  89. package/dist/session.js +45 -1
  90. package/dist/shared.d.ts +7 -0
  91. package/dist/shared.js +7 -0
  92. package/dist/todo-store.d.ts +2 -0
  93. package/dist/todo-store.js +19 -0
  94. package/dist/transport/connect.d.ts +2 -0
  95. package/dist/transport/connect.js +23 -6
  96. package/dist/usage.js +23 -6
  97. package/package.json +3 -2
  98. package/dist/context/agents.d.ts +0 -12
  99. package/dist/context/agents.js +0 -136
  100. package/dist/plan-execution-kickoff.d.ts +0 -57
  101. package/dist/plan-execution-kickoff.js +0 -186
@@ -1,3 +1,4 @@
1
+ import type { CursorImageInput } from "../image-input.js";
1
2
  import { type CursorExecVariant } from "./exec-variants.js";
2
3
  import { type CursorShellOutcome } from "../shell-timeout.js";
3
4
  export declare const REQUEST_CONTEXT_RESULT_FIELD = 10;
@@ -8,6 +9,10 @@ export type OpencodeToolDef = {
8
9
  /** Original flattened identity when `name` is a Cursor-facing alias. */
9
10
  sourceName?: string;
10
11
  };
12
+ /** Check explicit required keys after normalization, before host execution. */
13
+ export declare function missingRequiredToolArguments(tool: OpencodeToolDef | undefined, args: Record<string, unknown>): string[];
14
+ /** Canonical file/search tools must not silently ignore a misplaced shell command. */
15
+ export declare function misplacedShellCommand(tool: OpencodeToolDef | undefined, args: Record<string, unknown>): boolean;
11
16
  /** Host file-tool argument key. OpenCode 1.x uses `filePath`; 2.0 uses `path`. */
12
17
  export type HostFilePathKey = "path" | "filePath";
13
18
  /** Host shell tool id. OpenCode 1.x uses `bash`; 2.0 uses `shell`. */
@@ -18,6 +23,7 @@ export type HostToolDialect = {
18
23
  filePathKey: HostFilePathKey;
19
24
  shellTool: HostShellTool;
20
25
  skillArgKey: HostSkillArgKey;
26
+ shellDescription?: "optional" | "required";
21
27
  };
22
28
  export declare const OPENCODE_1_TOOL_DIALECT: HostToolDialect;
23
29
  export declare const OPENCODE_2_TOOL_DIALECT: HostToolDialect;
@@ -61,7 +67,9 @@ export declare function sanitizeMcpServerId(value: string): string;
61
67
  export declare function resolveToolServerIdentity(opencodeName: string, defaultServer?: string, knownMcpServers?: Iterable<string>): ToolServerIdentity;
62
68
  /**
63
69
  * Convert opencode's per-turn tool list into Cursor `McpToolDefinition`
64
- * entries for `request_context.tools` (#7) and `AgentRunRequest.mcp_tools`.
70
+ * entries for session exec remap / bridges. These are not sent on
71
+ * RequestContext.tools (#7) or AgentRunRequest.mcp_tools (both omitted/empty
72
+ * on the wire). Exec #36 still uses the same identity fields.
65
73
  *
66
74
  * Builtins and unknown plugin/custom tools are advertised under the synthetic
67
75
  * default server (`opencode`). Tools whose prefixes match configured MCP
@@ -79,12 +87,13 @@ export declare function toolsToDescriptors(tools: OpencodeToolDef[], providerIde
79
87
  export declare function buildCustomWebToolAliases(tools: OpencodeToolDef[]): AliasedToolCatalog;
80
88
  export declare function resolveCustomWebToolAlias(toolName: string, aliases: ToolAliasRegistry | undefined): string;
81
89
  /**
82
- * Build the nested McpFileSystemOptions / McpMetaToolOptions shape used by
83
- * requestContext.#23 / #34. One `McpDescriptor` per resolved server (builtins
84
- * and unknown tools under the synthetic default; configured MCP tools under
85
- * their upstream server id).
90
+ * Nested MCP descriptors. Full name/description/schema is for exec #36.
91
+ * RequestContext `mcp_meta_tool_options` uses `{ namesOnly: true }` (`tool_name`
92
+ * only). File-system `mcp_descriptors` are omitted on the wire.
86
93
  */
87
- export declare function toolsToMcpDescriptors(tools: OpencodeToolDef[], providerIdentifier?: string, knownMcpServers?: Iterable<string>): Array<Record<string, unknown>>;
94
+ export declare function toolsToMcpDescriptors(tools: OpencodeToolDef[], providerIdentifier?: string, knownMcpServers?: Iterable<string>, options?: {
95
+ namesOnly?: boolean;
96
+ }): Array<Record<string, unknown>>;
88
97
  /**
89
98
  * @deprecated Prefer `buildRequestContext` from `../context/build.js`.
90
99
  * Kept as a sync tools-only fallback for unit tests that don't need collectors.
@@ -277,7 +286,16 @@ export type ToolResultInput = {
277
286
  * often $HOME and would re-advertise the wrong folder to the model.
278
287
  */
279
288
  workspaceRoot?: string;
289
+ /** Images the host tool returned; see `execResultImages` for which results carry them. */
290
+ images?: readonly CursorImageInput[];
280
291
  };
292
+ /**
293
+ * The leading tool-result images an exec result can carry on a held Run.
294
+ * Cursor's own read executor answers an image file with its bytes in
295
+ * `ReadSuccess.data` (#5) and no text; its MCP executor appends each MCP image
296
+ * as an `McpImageContent` item (#2). Other result shapes have no image field.
297
+ */
298
+ export declare function execResultImages(resultField: string, images: readonly CursorImageInput[] | undefined): CursorImageInput[];
281
299
  /**
282
300
  * Build one or more ExecClientMessage frames for a tool result.
283
301
  * Shell replies are a sequence of ShellStream oneofs under the same id —
@@ -318,7 +336,7 @@ export declare function unwrapReadOutput(output: string): string;
318
336
  * OpenCode returns free-form text; we wrap it in the minimal success shape the
319
337
  * server accepts (verified against agent.v1 wire captures).
320
338
  */
321
- export declare function buildTypedExecResult(resultField: string, output: string, error?: string, toolName?: string, resultMetadata?: Record<string, unknown>, shellOutcome?: CursorShellOutcome, workspaceRoot?: string): Record<string, unknown>;
339
+ export declare function buildTypedExecResult(resultField: string, output: string, error?: string, toolName?: string, resultMetadata?: Record<string, unknown>, shellOutcome?: CursorShellOutcome, workspaceRoot?: string, images?: readonly CursorImageInput[]): Record<string, unknown>;
322
340
  export declare function buildToolCallPart(execMsg: ParsedExecRequest, sessionId: string): {
323
341
  toolCallId: string;
324
342
  toolName: string;
@@ -340,11 +358,15 @@ export declare function detectExecVariantField(agentServerPayload: Uint8Array):
340
358
  */
341
359
  export declare function buildRequestContextResult(execId: number, requestContext: Record<string, unknown>): Uint8Array;
342
360
  /**
343
- * Answer Cursor's exec #36 MCP-state probe from the same descriptors advertised
344
- * in RequestContext. OpenCode remains the executor; this only confirms that the
345
- * provider's virtual MCP servers and their tools are available.
361
+ * Answer Cursor's exec #36 MCP-state probe from the session's live tool
362
+ * descriptors: `toolsToDescriptors` output for the advertised catalog (after
363
+ * web-tool aliasing), refreshed on every `doStream`. These are the same
364
+ * identities the names-only RequestContext advertises and exec remap uses, so
365
+ * Cursor's native get_mcp_tools can correlate the later
366
+ * provider_identifier/tool_name request. OpenCode remains the executor; this
367
+ * only confirms those tools are available, with full name/description/schema.
346
368
  */
347
- export declare function buildMcpStateResult(execId: number, args: Record<string, unknown>, requestContext: Record<string, unknown>): Uint8Array;
369
+ export declare function buildMcpStateResult(execId: number, args: Record<string, unknown>, toolDescriptors: ReadonlyArray<Record<string, unknown>>): Uint8Array;
348
370
  /**
349
371
  * Total fallback for Cursor's native MCP-resource exec channel (agent.v1
350
372
  * fields #17/#18, tasks/plans/fix-cursor-mcp-resource-exec.md). Under Option B
@@ -9,10 +9,36 @@ import { trace, traceRequestContextPaths } from "../debug.js";
9
9
  import { cursorExecVariantByRequestName, FORCE_BACKGROUND_STATUS_ERROR, } from "./exec-variants.js";
10
10
  import { APPLY_PATCH_TOOL, buildAddFilePatch, buildUpdateFilePatch, planSubstringEdit, } from "./apply-patch.js";
11
11
  import { BACKGROUND_SHELL_MARKER, buildBackgroundShellCommand, } from "../shell-timeout.js";
12
+ import { normalizeOpencodeQuestionArgs } from "./ask-question.js";
13
+ import { normalizeOpencodeTodoArgs } from "../todo-store.js";
12
14
  // Exec variant field number whose reply is the server-initiated request_context
13
15
  // probe (ExecServerMessage #10 → ExecClientMessage #10). request/result share a
14
16
  // field number for every exec variant, so this is also the result field.
15
17
  export const REQUEST_CONTEXT_RESULT_FIELD = 10;
18
+ /** Check explicit required keys after normalization, before host execution. */
19
+ export function missingRequiredToolArguments(tool, args) {
20
+ const schema = tool?.inputSchema;
21
+ if (!schema || typeof schema !== "object" || Array.isArray(schema))
22
+ return [];
23
+ const required = schema.required;
24
+ if (!Array.isArray(required))
25
+ return [];
26
+ return required.filter((key) => typeof key === "string" && (!Object.hasOwn(args, key) || args[key] === undefined));
27
+ }
28
+ /** Canonical file/search tools must not silently ignore a misplaced shell command. */
29
+ export function misplacedShellCommand(tool, args) {
30
+ if (!tool || args.command === undefined)
31
+ return false;
32
+ if (!["read", "write", "edit", "grep", "glob", "ls"].includes(tool.name))
33
+ return false;
34
+ const schema = tool.inputSchema;
35
+ if (!schema || typeof schema !== "object" || Array.isArray(schema))
36
+ return false;
37
+ const properties = schema.properties;
38
+ if (!properties || typeof properties !== "object" || Array.isArray(properties))
39
+ return false;
40
+ return !Object.hasOwn(properties, "command");
41
+ }
16
42
  export const OPENCODE_1_TOOL_DIALECT = {
17
43
  filePathKey: "filePath",
18
44
  shellTool: "bash",
@@ -88,7 +114,20 @@ export function hostToolDialectFromTools(tools, defaultDialect = OPENCODE_1_TOOL
88
114
  ? "id"
89
115
  : defaultDialect.skillArgKey;
90
116
  }
91
- return { filePathKey, shellTool, skillArgKey };
117
+ const shellSchema = tools.find((tool) => tool.name === shellTool)?.inputSchema;
118
+ const shellProps = jsonSchemaProperties(shellSchema);
119
+ const description = shellProps?.description;
120
+ const required = shellSchema && typeof shellSchema === "object" && !Array.isArray(shellSchema)
121
+ ? shellSchema.required
122
+ : undefined;
123
+ return {
124
+ filePathKey, shellTool, skillArgKey,
125
+ ...(description && typeof description === "object"
126
+ && description.type === "string"
127
+ ? { shellDescription: Array.isArray(required) && required.includes("description")
128
+ ? "required" : "optional" }
129
+ : {}),
130
+ };
92
131
  }
93
132
  function assignHostFilePath(args, filePath, dialect) {
94
133
  delete args.filePath;
@@ -132,7 +171,9 @@ export function resolveToolServerIdentity(opencodeName, defaultServer = "opencod
132
171
  }
133
172
  /**
134
173
  * Convert opencode's per-turn tool list into Cursor `McpToolDefinition`
135
- * entries for `request_context.tools` (#7) and `AgentRunRequest.mcp_tools`.
174
+ * entries for session exec remap / bridges. These are not sent on
175
+ * RequestContext.tools (#7) or AgentRunRequest.mcp_tools (both omitted/empty
176
+ * on the wire). Exec #36 still uses the same identity fields.
136
177
  *
137
178
  * Builtins and unknown plugin/custom tools are advertised under the synthetic
138
179
  * default server (`opencode`). Tools whose prefixes match configured MCP
@@ -249,15 +290,15 @@ export function resolveCustomWebToolAlias(toolName, aliases) {
249
290
  return toolName;
250
291
  }
251
292
  /**
252
- * Build the nested McpFileSystemOptions / McpMetaToolOptions shape used by
253
- * requestContext.#23 / #34. One `McpDescriptor` per resolved server (builtins
254
- * and unknown tools under the synthetic default; configured MCP tools under
255
- * their upstream server id).
293
+ * Nested MCP descriptors. Full name/description/schema is for exec #36.
294
+ * RequestContext `mcp_meta_tool_options` uses `{ namesOnly: true }` (`tool_name`
295
+ * only). File-system `mcp_descriptors` are omitted on the wire.
256
296
  */
257
- export function toolsToMcpDescriptors(tools, providerIdentifier = "opencode", knownMcpServers = []) {
297
+ export function toolsToMcpDescriptors(tools, providerIdentifier = "opencode", knownMcpServers = [], options) {
258
298
  if (tools.length === 0)
259
299
  return [];
260
300
  const byServer = new Map();
301
+ const namesOnly = options?.namesOnly === true;
261
302
  // Walk advertised order: first-seen server, then append tools onto that
262
303
  // server. Same-set host reorder is resolved by `resolveTurnToolState`
263
304
  // before encode so this walk stays epoch-stable.
@@ -268,11 +309,14 @@ export function toolsToMcpDescriptors(tools, providerIdentifier = "opencode", kn
268
309
  list = [];
269
310
  byServer.set(id.server, list);
270
311
  }
271
- list.push({
272
- tool_name: t.sourceName ? t.name : id.toolName,
273
- description: t.description ?? "",
274
- input_schema: encodeJsonAsValue(normalizeInputSchema(t.inputSchema)),
275
- });
312
+ const toolName = t.sourceName ? t.name : id.toolName;
313
+ list.push(namesOnly
314
+ ? { tool_name: toolName }
315
+ : {
316
+ tool_name: toolName,
317
+ description: t.description ?? "",
318
+ input_schema: encodeJsonAsValue(normalizeInputSchema(t.inputSchema)),
319
+ });
276
320
  }
277
321
  return [...byServer.keys()].map((server) => ({
278
322
  server_name: server,
@@ -285,20 +329,17 @@ export function toolsToMcpDescriptors(tools, providerIdentifier = "opencode", kn
285
329
  * Kept as a sync tools-only fallback for unit tests that don't need collectors.
286
330
  */
287
331
  export function buildLiveRequestContext(tools, providerIdentifier = "opencode", knownMcpServers = []) {
288
- const flat = toolsToDescriptors(tools, providerIdentifier, knownMcpServers);
289
- const nested = toolsToMcpDescriptors(tools, providerIdentifier, knownMcpServers);
332
+ const slim = toolsToMcpDescriptors(tools, providerIdentifier, knownMcpServers, { namesOnly: true });
290
333
  const cwd = process.cwd();
291
334
  const ctx = {
292
335
  env: buildEnv(cwd),
293
- tools: flat,
294
336
  mcp_file_system_options: {
295
337
  enabled: true,
296
338
  workspace_project_dir: ensureOpencodeProjectDir(cwd),
297
- mcp_descriptors: nested,
298
339
  },
299
340
  mcp_meta_tool_options: {
300
341
  enabled: true,
301
- mcp_descriptors: nested,
342
+ ...(slim.length > 0 ? { mcp_descriptors: slim } : {}),
302
343
  },
303
344
  web_search_enabled: false,
304
345
  web_fetch_enabled: false,
@@ -1161,10 +1202,9 @@ export function mapCursorArgsToOpencode(toolName, raw, execVariant, dialect = OP
1161
1202
  // as any other bash tool call.
1162
1203
  if (execVariant === "delete_args") {
1163
1204
  const target = str(cleaned.path) ?? str(cleaned.filePath);
1164
- return {
1165
- toolName: dialect.shellTool,
1166
- args: target ? { command: `rm -f -- ${shellQuote(target)}` } : { command: "true" },
1167
- };
1205
+ return mapCursorArgsToOpencode(dialect.shellTool, {
1206
+ command: target ? `rm -f -- ${shellQuote(target)}` : "true",
1207
+ }, undefined, dialect);
1168
1208
  }
1169
1209
  switch (toolName) {
1170
1210
  case "read": {
@@ -1235,6 +1275,14 @@ export function mapCursorArgsToOpencode(toolName, raw, execVariant, dialect = OP
1235
1275
  const command = str(cleaned.command);
1236
1276
  if (command)
1237
1277
  args.command = command;
1278
+ if (dialect.shellDescription) {
1279
+ const description = str(cleaned.description)
1280
+ ?? (dialect.shellDescription === "required" && command
1281
+ ? `Run: ${command.length > 60 ? `${command.slice(0, 57)}...` : command}`
1282
+ : undefined);
1283
+ if (description)
1284
+ args.description = description;
1285
+ }
1238
1286
  const workdir = str(cleaned.workdir) ?? str(cleaned.working_directory);
1239
1287
  if (workdir)
1240
1288
  args.workdir = workdir;
@@ -1286,6 +1334,22 @@ export function mapCursorArgsToOpencode(toolName, raw, execVariant, dialect = OP
1286
1334
  args[dialect.skillArgKey] = value;
1287
1335
  return { toolName: "skill", args };
1288
1336
  }
1337
+ case "todowrite": {
1338
+ // OpenCode requires `priority` on every todo item. Cursor TodoWrite /
1339
+ // model MCP calls often omit it; fill host defaults before validation.
1340
+ return {
1341
+ toolName: "todowrite",
1342
+ args: normalizeOpencodeTodoArgs(cleaned),
1343
+ };
1344
+ }
1345
+ case "question": {
1346
+ // OpenCode requires `header` on every question. Model MCP calls often
1347
+ // omit it (AskQuestion bridge already fills it); default before validation.
1348
+ return {
1349
+ toolName: "question",
1350
+ args: normalizeOpencodeQuestionArgs(cleaned),
1351
+ };
1352
+ }
1289
1353
  default:
1290
1354
  return { toolName, args: cleaned };
1291
1355
  }
@@ -1537,6 +1601,24 @@ export function buildReadRejectionMessages(execId, readResult) {
1537
1601
  buildExecStreamClose(execId),
1538
1602
  ];
1539
1603
  }
1604
+ /**
1605
+ * The leading tool-result images an exec result can carry on a held Run.
1606
+ * Cursor's own read executor answers an image file with its bytes in
1607
+ * `ReadSuccess.data` (#5) and no text; its MCP executor appends each MCP image
1608
+ * as an `McpImageContent` item (#2). Other result shapes have no image field.
1609
+ */
1610
+ export function execResultImages(resultField, images) {
1611
+ if (!images?.length)
1612
+ return [];
1613
+ if (resultField === "read_result")
1614
+ return [images[0]];
1615
+ if (resultField === "mcp_result")
1616
+ return [...images];
1617
+ return [];
1618
+ }
1619
+ function mcpImageItems(images) {
1620
+ return images.map((image) => ({ image: { data: image.data, mime_type: image.mimeType } }));
1621
+ }
1540
1622
  /**
1541
1623
  * Build one or more ExecClientMessage frames for a tool result.
1542
1624
  * Shell replies are a sequence of ShellStream oneofs under the same id —
@@ -1592,7 +1674,7 @@ export function buildExecClientMessages(input) {
1592
1674
  id: input.execId,
1593
1675
  local_execution_time_ms: input.executionTimeMs ?? 0,
1594
1676
  };
1595
- clientMsg[resultField] = buildTypedExecResult(resultField, input.output, input.error, input.toolName, input.resultMetadata, input.shellOutcome, input.workspaceRoot);
1677
+ clientMsg[resultField] = buildTypedExecResult(resultField, input.output, input.error, input.toolName, input.resultMetadata, input.shellOutcome, input.workspaceRoot, execResultImages(resultField, input.images));
1596
1678
  frames.push(encodeMessage("AgentClientMessage", {
1597
1679
  exec_client_message: clientMsg,
1598
1680
  }));
@@ -2461,7 +2543,7 @@ export function unwrapReadOutput(output) {
2461
2543
  * OpenCode returns free-form text; we wrap it in the minimal success shape the
2462
2544
  * server accepts (verified against agent.v1 wire captures).
2463
2545
  */
2464
- export function buildTypedExecResult(resultField, output, error, toolName, resultMetadata, shellOutcome, workspaceRoot) {
2546
+ export function buildTypedExecResult(resultField, output, error, toolName, resultMetadata, shellOutcome, workspaceRoot, images = []) {
2465
2547
  // Prefer the session workspace; never advertise the host process cwd (daemon
2466
2548
  // often starts in $HOME) as the path Cursor shows the model for glob/ls.
2467
2549
  const trimmedRoot = typeof workspaceRoot === "string" ? workspaceRoot.trim() : "";
@@ -2481,6 +2563,18 @@ export function buildTypedExecResult(resultField, output, error, toolName, resul
2481
2563
  const readPath = resolveToolPath(rawPath, resultRoot);
2482
2564
  if (error)
2483
2565
  return { error: { path: readPath, error } };
2566
+ const image = images[0];
2567
+ if (image) {
2568
+ return {
2569
+ success: {
2570
+ path: readPath,
2571
+ data: image.data,
2572
+ total_lines: 0,
2573
+ file_size: readFileSize(readPath) || image.data.length,
2574
+ truncated: false,
2575
+ },
2576
+ };
2577
+ }
2484
2578
  // The host may have resolved an alternate spelling after the request
2485
2579
  // (for example a Unicode-space filename). Use the result's own path for
2486
2580
  // local stat/newline recovery while preserving the requested path in the
@@ -2748,18 +2842,20 @@ export function buildTypedExecResult(resultField, output, error, toolName, resul
2748
2842
  if (toolName === "grep" || toolName === "glob") {
2749
2843
  return {
2750
2844
  success: {
2751
- content: [{ text: { text: groundSearchOutput(output, resultRoot) } }],
2845
+ content: [{ text: { text: groundSearchOutput(output, resultRoot) } }, ...mcpImageItems(images)],
2752
2846
  is_error: false,
2753
2847
  },
2754
2848
  };
2755
2849
  }
2756
2850
  if (toolName !== "read") {
2757
- return { success: { content: [{ text: { text: output } }], is_error: false } };
2851
+ return {
2852
+ success: { content: [{ text: { text: output } }, ...mcpImageItems(images)], is_error: false },
2853
+ };
2758
2854
  }
2759
2855
  const listing = parseOpenCode2DirectoryListing(output, resultRoot);
2760
2856
  if (listing) {
2761
2857
  return {
2762
- success: { content: [{ text: { text: listing.text } }], is_error: false },
2858
+ success: { content: [{ text: { text: listing.text } }, ...mcpImageItems(images)], is_error: false },
2763
2859
  };
2764
2860
  }
2765
2861
  // Carry the truncation notice as its own content item: the file content
@@ -2773,6 +2869,7 @@ export function buildTypedExecResult(resultField, output, error, toolName, resul
2773
2869
  content: [
2774
2870
  { text: { text: unwrapReadOutput(output) } },
2775
2871
  ...notices.map((notice) => ({ text: { text: notice } })),
2872
+ ...mcpImageItems(images),
2776
2873
  ],
2777
2874
  is_error: false,
2778
2875
  },
@@ -3196,41 +3293,52 @@ export function buildRequestContextResult(execId, requestContext) {
3196
3293
  });
3197
3294
  }
3198
3295
  /**
3199
- * Answer Cursor's exec #36 MCP-state probe from the same descriptors advertised
3200
- * in RequestContext. OpenCode remains the executor; this only confirms that the
3201
- * provider's virtual MCP servers and their tools are available.
3296
+ * Answer Cursor's exec #36 MCP-state probe from the session's live tool
3297
+ * descriptors: `toolsToDescriptors` output for the advertised catalog (after
3298
+ * web-tool aliasing), refreshed on every `doStream`. These are the same
3299
+ * identities the names-only RequestContext advertises and exec remap uses, so
3300
+ * Cursor's native get_mcp_tools can correlate the later
3301
+ * provider_identifier/tool_name request. OpenCode remains the executor; this
3302
+ * only confirms those tools are available, with full name/description/schema.
3202
3303
  */
3203
- export function buildMcpStateResult(execId, args, requestContext) {
3304
+ export function buildMcpStateResult(execId, args, toolDescriptors) {
3204
3305
  const requested = new Set(Array.isArray(args.server_identifiers)
3205
3306
  ? args.server_identifiers.filter((id) => typeof id === "string" && id.length > 0)
3206
3307
  : []);
3207
- const fsOptions = recordValue(requestContext.mcp_file_system_options);
3208
- const nested = Array.isArray(fsOptions?.mcp_descriptors)
3209
- ? fsOptions.mcp_descriptors.map(recordValue).filter((d) => !!d)
3210
- : [];
3211
- const descriptors = nested.length > 0 ? nested : descriptorsFromFlatTools(requestContext.tools);
3212
- const flatTools = Array.isArray(requestContext.tools)
3213
- ? requestContext.tools.map(recordValue).filter((tool) => !!tool)
3214
- : [];
3215
- const servers = descriptors
3216
- .filter((descriptor) => {
3217
- const id = stringValue(descriptor.server_identifier);
3218
- return requested.size === 0 || (id !== undefined && requested.has(id));
3219
- })
3220
- .map((descriptor) => {
3221
- const serverIdentifier = stringValue(descriptor.server_identifier) ?? stringValue(descriptor.server_name) ?? "";
3222
- const tools = Array.isArray(descriptor.tools)
3223
- ? descriptor.tools
3224
- .map(recordValue)
3225
- .filter((tool) => !!tool)
3226
- .map((tool) => mcpStateToolDefinition(serverIdentifier, tool, flatTools))
3227
- : [];
3228
- return {
3229
- server_name: stringValue(descriptor.server_name) ?? serverIdentifier,
3230
- server_identifier: serverIdentifier,
3231
- tools,
3232
- };
3233
- });
3308
+ // Group in advertised order: first-seen server, then its tools.
3309
+ const byServer = new Map();
3310
+ for (const tool of toolDescriptors) {
3311
+ const server = stringValue(tool.provider_identifier);
3312
+ const toolName = stringValue(tool.tool_name);
3313
+ if (!server || !toolName)
3314
+ continue;
3315
+ if (requested.size > 0 && !requested.has(server))
3316
+ continue;
3317
+ let list = byServer.get(server);
3318
+ if (!list) {
3319
+ list = [];
3320
+ byServer.set(server, list);
3321
+ }
3322
+ list.push({
3323
+ name: stringValue(tool.name) ?? `${server}-${toolName}`,
3324
+ // GetDynamicTools exposes the inner schema without the outer call identity.
3325
+ // Keep the exact identity in the definition, including truncated searches.
3326
+ description: `CallDynamicTool identity: ${JSON.stringify({ namespace: server, toolName })}. `
3327
+ + `Complete outer envelope: ${JSON.stringify({ namespace: server, toolName, arguments: {} })}. `
3328
+ + "Replace arguments with this inputSchema's object; keep both identity fields outside it. "
3329
+ + "Write namespace and toolName FIRST, then arguments containing this inputSchema's object. "
3330
+ + "All three outer fields are required on every invocation.\n\n"
3331
+ + (stringValue(tool.description) ?? ""),
3332
+ input_schema: tool.input_schema,
3333
+ provider_identifier: server,
3334
+ tool_name: toolName,
3335
+ });
3336
+ }
3337
+ const servers = [...byServer].map(([server, tools]) => ({
3338
+ server_name: server,
3339
+ server_identifier: server,
3340
+ tools,
3341
+ }));
3234
3342
  return encodeMessage("AgentClientMessage", {
3235
3343
  exec_client_message: {
3236
3344
  id: execId,
@@ -3238,24 +3346,6 @@ export function buildMcpStateResult(execId, args, requestContext) {
3238
3346
  },
3239
3347
  });
3240
3348
  }
3241
- /**
3242
- * Exec #36 uses McpToolDefinition, not the narrower McpToolDescriptor used by
3243
- * RequestContext's filesystem/meta-tool catalogs. Rehydrate the full identity
3244
- * from RequestContext.tools so Cursor's native get_mcp_tools can correlate the
3245
- * discovered definition with the later provider_identifier/tool_name request.
3246
- */
3247
- function mcpStateToolDefinition(serverIdentifier, descriptor, flatTools) {
3248
- const toolName = stringValue(descriptor.tool_name) ?? "";
3249
- const advertised = flatTools.find((tool) => stringValue(tool.provider_identifier) === serverIdentifier
3250
- && stringValue(tool.tool_name) === toolName);
3251
- return {
3252
- name: stringValue(advertised?.name) ?? `${serverIdentifier}-${toolName}`,
3253
- description: stringValue(advertised?.description) ?? stringValue(descriptor.description) ?? "",
3254
- input_schema: advertised?.input_schema ?? descriptor.input_schema,
3255
- provider_identifier: serverIdentifier,
3256
- tool_name: toolName,
3257
- };
3258
- }
3259
3349
  /**
3260
3350
  * Total fallback for Cursor's native MCP-resource exec channel (agent.v1
3261
3351
  * fields #17/#18, tasks/plans/fix-cursor-mcp-resource-exec.md). Under Option B
@@ -3285,31 +3375,3 @@ export function buildReadMcpResourceFallback(execId, server, uri) {
3285
3375
  },
3286
3376
  });
3287
3377
  }
3288
- function recordValue(value) {
3289
- return value && typeof value === "object" && !Array.isArray(value)
3290
- ? value
3291
- : undefined;
3292
- }
3293
- function descriptorsFromFlatTools(value) {
3294
- if (!Array.isArray(value))
3295
- return [];
3296
- const byServer = new Map();
3297
- for (const raw of value) {
3298
- const tool = recordValue(raw);
3299
- if (!tool)
3300
- continue;
3301
- const server = stringValue(tool.provider_identifier) ?? "opencode";
3302
- const tools = byServer.get(server) ?? [];
3303
- tools.push({
3304
- tool_name: stringValue(tool.tool_name) ?? stringValue(tool.name) ?? "",
3305
- description: stringValue(tool.description) ?? "",
3306
- input_schema: tool.input_schema,
3307
- });
3308
- byServer.set(server, tools);
3309
- }
3310
- return [...byServer].map(([server, tools]) => ({
3311
- server_name: server,
3312
- server_identifier: server,
3313
- tools,
3314
- }));
3315
- }
@@ -0,0 +1,52 @@
1
+ import type { LanguageModelV3CallOptions } from "@ai-sdk/provider";
2
+ /** Mirrors MAX_TURN_STATE_SESSIONS; evicted sessions re-hydrate from disk. */
3
+ export declare const MAX_PROVENANCE_SESSIONS = 256;
4
+ export type TurnProvenance = {
5
+ conversationId: string;
6
+ /** Tool call ids emitted in the latest non-empty step. */
7
+ toolCallIds: string[];
8
+ /** Whitespace-free text emitted in the latest non-empty step (first 4 KiB). */
9
+ text: string;
10
+ /**
11
+ * Whitespace-free reasoning and text of the same step in emission order
12
+ * (first 4 KiB). OpenCode 1.x replays a previous model's reasoning as text
13
+ * parts after a model switch (`session/message-v2.ts` `differentModel`), and
14
+ * OpenCode 2 does the same for an assistant message that ended in error.
15
+ */
16
+ textWithReasoning: string;
17
+ };
18
+ export type ForeignHistoryReason = "foreign-assistant";
19
+ /**
20
+ * Mark the start of one host step (one doStream). The previous step stays
21
+ * recorded until this one emits content, because hosts drop empty assistant
22
+ * turns from history.
23
+ */
24
+ export declare function beginEmittedStep(sessionKey: string, conversationId: string): void;
25
+ /** Record one stream part this provider handed to the host. */
26
+ export declare function recordEmittedPart(sessionKey: string, conversationId: string, part: {
27
+ type: string;
28
+ delta?: unknown;
29
+ toolCallId?: unknown;
30
+ }): void;
31
+ /**
32
+ * Bind provenance to the conversation a Run was opened on. A reminted
33
+ * conversation starts with an empty record, so it never inherits the previous
34
+ * conversation's steps.
35
+ */
36
+ export declare function trackTurnProvenance(sessionKey: string, conversationId: string): void;
37
+ export declare function getTurnProvenance(sessionKey: string): TurnProvenance | undefined;
38
+ export declare function restoreTurnProvenance(sessionKey: string, value: TurnProvenance): void;
39
+ export declare function resetTurnProvenanceForTests(): void;
40
+ export declare function serializeTurnProvenance(value: TurnProvenance): string;
41
+ export declare function parseTurnProvenance(raw: string): TurnProvenance | undefined;
42
+ /**
43
+ * Decide whether the host history moved past this Cursor conversation.
44
+ * Returns undefined when there is no evidence either way (no record for this
45
+ * conversation, or no assistant turn yet), so unknown state never forces a
46
+ * rebase.
47
+ */
48
+ export declare function detectForeignHistory(input: {
49
+ sessionKey: string | undefined;
50
+ conversationId: string;
51
+ prompt: LanguageModelV3CallOptions["prompt"];
52
+ }): ForeignHistoryReason | undefined;