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.
- package/CHANGELOG.md +79 -0
- package/README.md +36 -15
- package/SECURITY.md +43 -7
- package/dist/agent-url.js +2 -2
- package/dist/auth-renewal.d.ts +85 -0
- package/dist/auth-renewal.js +284 -0
- package/dist/auth.d.ts +38 -22
- package/dist/auth.js +79 -96
- package/dist/classic-tools.d.ts +15 -0
- package/dist/classic-tools.js +45 -0
- package/dist/context/build.d.ts +62 -7
- package/dist/context/build.js +127 -95
- package/dist/context/dynamic-catalog.d.ts +2 -58
- package/dist/context/dynamic-catalog.js +16 -161
- package/dist/context/epoch.d.ts +6 -4
- package/dist/context/epoch.js +22 -10
- package/dist/context/frozen.js +42 -15
- package/dist/context/index.d.ts +3 -1
- package/dist/context/index.js +3 -1
- package/dist/context/overlay.d.ts +2 -23
- package/dist/context/overlay.js +6 -30
- package/dist/context/paths.d.ts +51 -21
- package/dist/context/paths.js +78 -27
- package/dist/context/rules.d.ts +3 -15
- package/dist/context/rules.js +10 -186
- package/dist/context/skills-bridge.d.ts +22 -0
- package/dist/context/skills-bridge.js +20 -0
- package/dist/context/skills.d.ts +31 -8
- package/dist/context/skills.js +187 -86
- package/dist/debug.d.ts +2 -0
- package/dist/debug.js +4 -0
- package/dist/errors.d.ts +13 -0
- package/dist/errors.js +27 -3
- package/dist/host-agent-mode.d.ts +53 -4
- package/dist/host-agent-mode.js +119 -15
- package/dist/host-plan-file.d.ts +28 -0
- package/dist/host-plan-file.js +76 -0
- package/dist/image-input.d.ts +18 -0
- package/dist/image-input.js +119 -28
- package/dist/image-save.d.ts +10 -2
- package/dist/image-save.js +35 -23
- package/dist/index.d.ts +10 -0
- package/dist/language-model.d.ts +86 -25
- package/dist/language-model.js +1295 -369
- package/dist/model-config.d.ts +9 -0
- package/dist/model-config.js +28 -1
- package/dist/opencode2/catalog.d.ts +1 -0
- package/dist/opencode2/catalog.js +2 -0
- package/dist/opencode2/image-save-tool.d.ts +8 -0
- package/dist/opencode2/image-save-tool.js +84 -0
- package/dist/opencode2/integration.d.ts +30 -7
- package/dist/opencode2/integration.js +75 -46
- package/dist/opencode2/types.d.ts +15 -0
- package/dist/parallel-step.d.ts +65 -0
- package/dist/parallel-step.js +102 -0
- package/dist/plugin-opencode2.js +58 -56
- package/dist/plugin.d.ts +0 -10
- package/dist/plugin.js +207 -127
- package/dist/pricing-data.d.ts +14 -1
- package/dist/pricing-data.js +14 -1
- package/dist/protocol/ask-question.d.ts +12 -1
- package/dist/protocol/ask-question.js +77 -3
- package/dist/protocol/blob-store.js +3 -2
- package/dist/protocol/conversation-bind.js +1 -1
- package/dist/protocol/conversation-persistence.d.ts +2 -0
- package/dist/protocol/conversation-persistence.js +11 -1
- package/dist/protocol/conversation-state.d.ts +2 -0
- package/dist/protocol/conversation-state.js +19 -0
- package/dist/protocol/create-plan.d.ts +47 -25
- package/dist/protocol/create-plan.js +35 -24
- package/dist/protocol/interactions.d.ts +26 -17
- package/dist/protocol/interactions.js +80 -31
- package/dist/protocol/messages.js +96 -18
- package/dist/protocol/request.d.ts +16 -9
- package/dist/protocol/request.js +17 -25
- package/dist/protocol/switch-mode.d.ts +66 -27
- package/dist/protocol/switch-mode.js +147 -73
- package/dist/protocol/token-details.d.ts +2 -0
- package/dist/protocol/token-details.js +11 -1
- package/dist/protocol/tool-call-bridge.d.ts +4 -0
- package/dist/protocol/tool-call-bridge.js +14 -0
- package/dist/protocol/tools.d.ts +33 -11
- package/dist/protocol/tools.js +165 -103
- package/dist/protocol/turn-provenance.d.ts +52 -0
- package/dist/protocol/turn-provenance.js +174 -0
- package/dist/replay-safety.d.ts +6 -0
- package/dist/replay-safety.js +56 -6
- package/dist/session.d.ts +44 -2
- package/dist/session.js +45 -1
- package/dist/shared.d.ts +7 -0
- package/dist/shared.js +7 -0
- package/dist/todo-store.d.ts +2 -0
- package/dist/todo-store.js +19 -0
- package/dist/transport/connect.d.ts +2 -0
- package/dist/transport/connect.js +23 -6
- package/dist/usage.js +23 -6
- package/package.json +3 -2
- package/dist/context/agents.d.ts +0 -12
- package/dist/context/agents.js +0 -136
- package/dist/plan-execution-kickoff.d.ts +0 -57
- package/dist/plan-execution-kickoff.js +0 -186
package/dist/protocol/tools.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
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
|
|
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
|
|
344
|
-
*
|
|
345
|
-
*
|
|
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>,
|
|
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
|
package/dist/protocol/tools.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
1166
|
-
|
|
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 {
|
|
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
|
|
3200
|
-
*
|
|
3201
|
-
*
|
|
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,
|
|
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
|
-
|
|
3208
|
-
const
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
.
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
.
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
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;
|