cursor-opencode-provider 0.2.7 → 0.2.9
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 +22 -6
- package/dist/agent-url.d.ts +8 -5
- package/dist/auth.js +59 -27
- package/dist/context/agents.d.ts +5 -1
- package/dist/context/agents.js +90 -30
- package/dist/context/build.js +63 -8
- package/dist/context/paths.d.ts +31 -6
- package/dist/context/paths.js +56 -18
- package/dist/context/plugins.js +16 -12
- package/dist/context/rules.js +26 -23
- package/dist/context/skills.js +8 -3
- package/dist/deadline.d.ts +8 -0
- package/dist/deadline.js +25 -0
- package/dist/language-model.js +206 -70
- package/dist/models.d.ts +2 -0
- package/dist/plugin.js +8 -1
- package/dist/protocol/client-version.js +7 -6
- package/dist/protocol/messages.js +24 -1
- package/dist/protocol/struct.d.ts +18 -0
- package/dist/protocol/struct.js +82 -0
- package/dist/protocol/tool-call-bridge.js +22 -2
- package/dist/protocol/tools.d.ts +67 -0
- package/dist/protocol/tools.js +342 -18
- package/dist/replay-safety.d.ts +23 -0
- package/dist/replay-safety.js +126 -0
- package/dist/session.d.ts +5 -0
- package/dist/transport/connect.d.ts +2 -0
- package/dist/transport/connect.js +106 -82
- package/dist/web-tools.d.ts +34 -0
- package/dist/web-tools.js +127 -0
- package/package.json +2 -2
package/dist/protocol/struct.js
CHANGED
|
@@ -82,6 +82,88 @@ export function readAllFields(b) {
|
|
|
82
82
|
}
|
|
83
83
|
return out;
|
|
84
84
|
}
|
|
85
|
+
const MAX_UINT32 = 0xffffffffn;
|
|
86
|
+
const MAX_UINT64 = 0xffffffffffffffffn;
|
|
87
|
+
const MAX_PROTOBUF_FIELD_NUMBER = 0x1fff_ffff;
|
|
88
|
+
/**
|
|
89
|
+
* Decode one unsigned protobuf varint without JavaScript's 32-bit bitwise
|
|
90
|
+
* truncation. `maximum` also enforces the protocol width: tags and lengths are
|
|
91
|
+
* uint32, while wire-type 0 scalar values may use the full uint64 range.
|
|
92
|
+
*/
|
|
93
|
+
function readBoundedVarint(bytes, offset, maximum) {
|
|
94
|
+
let value = 0n;
|
|
95
|
+
let shift = 0n;
|
|
96
|
+
for (let index = offset; index < bytes.length && index < offset + 10; index++) {
|
|
97
|
+
const byte = bytes[index];
|
|
98
|
+
value |= BigInt(byte & 0x7f) << shift;
|
|
99
|
+
if (value > maximum)
|
|
100
|
+
return undefined;
|
|
101
|
+
if ((byte & 0x80) === 0)
|
|
102
|
+
return { value, nextOffset: index + 1 };
|
|
103
|
+
shift += 7n;
|
|
104
|
+
}
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Strictly walk a protobuf message for security/replay decisions.
|
|
109
|
+
*
|
|
110
|
+
* Unlike `readAllFields`, this parser consumes the complete input, preserves
|
|
111
|
+
* uint64 varints as bigint, represents fixed-width fields, and rejects invalid
|
|
112
|
+
* tags, truncation, overflow, groups, and unsupported wire types. It deliberately
|
|
113
|
+
* returns `undefined` instead of a partial result: callers must fail closed when
|
|
114
|
+
* deciding whether replay is safe.
|
|
115
|
+
*/
|
|
116
|
+
export function readAllFieldsStrict(bytes) {
|
|
117
|
+
let offset = 0;
|
|
118
|
+
const fields = [];
|
|
119
|
+
while (offset < bytes.length) {
|
|
120
|
+
const tag = readBoundedVarint(bytes, offset, MAX_UINT32);
|
|
121
|
+
if (!tag)
|
|
122
|
+
return undefined;
|
|
123
|
+
offset = tag.nextOffset;
|
|
124
|
+
const fieldNumber = Number(tag.value >> 3n);
|
|
125
|
+
const wireType = Number(tag.value & 7n);
|
|
126
|
+
if (fieldNumber === 0 || fieldNumber > MAX_PROTOBUF_FIELD_NUMBER)
|
|
127
|
+
return undefined;
|
|
128
|
+
if (wireType === 0) {
|
|
129
|
+
const scalar = readBoundedVarint(bytes, offset, MAX_UINT64);
|
|
130
|
+
if (!scalar)
|
|
131
|
+
return undefined;
|
|
132
|
+
fields.push({ fn: fieldNumber, wt: wireType, varint: scalar.value });
|
|
133
|
+
offset = scalar.nextOffset;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (wireType === 1) {
|
|
137
|
+
if (offset + 8 > bytes.length)
|
|
138
|
+
return undefined;
|
|
139
|
+
fields.push({ fn: fieldNumber, wt: wireType, fixed64: bytes.subarray(offset, offset + 8) });
|
|
140
|
+
offset += 8;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (wireType === 2) {
|
|
144
|
+
const encodedLength = readBoundedVarint(bytes, offset, MAX_UINT32);
|
|
145
|
+
if (!encodedLength)
|
|
146
|
+
return undefined;
|
|
147
|
+
offset = encodedLength.nextOffset;
|
|
148
|
+
const length = Number(encodedLength.value);
|
|
149
|
+
if (offset + length > bytes.length)
|
|
150
|
+
return undefined;
|
|
151
|
+
fields.push({ fn: fieldNumber, wt: wireType, bytes: bytes.subarray(offset, offset + length) });
|
|
152
|
+
offset += length;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (wireType === 5) {
|
|
156
|
+
if (offset + 4 > bytes.length)
|
|
157
|
+
return undefined;
|
|
158
|
+
fields.push({ fn: fieldNumber, wt: wireType, fixed32: bytes.subarray(offset, offset + 4) });
|
|
159
|
+
offset += 4;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
// Groups (3/4) are deprecated and recursive; no replay-safe frame uses them.
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
return fields;
|
|
166
|
+
}
|
|
85
167
|
function decodeStructBytes(b) {
|
|
86
168
|
const obj = {};
|
|
87
169
|
for (const f of readAllFields(b)) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mapCursorArgsToOpencode, mcpRealToolName } from "./tools.js";
|
|
1
|
+
import { mapCursorArgsToOpencode, mapCursorSubagentTypeToOpenCode, mcpRealToolName } from "./tools.js";
|
|
2
2
|
import { decodeStructEntriesToJson } from "./struct.js";
|
|
3
3
|
const TODO_STATUS = {
|
|
4
4
|
0: "pending",
|
|
@@ -334,7 +334,27 @@ function openCodeSubagentType(raw) {
|
|
|
334
334
|
if (typeof custom?.name === "string" && custom.name.length > 0)
|
|
335
335
|
return custom.name;
|
|
336
336
|
if (subtype.explore != null)
|
|
337
|
-
return "explore";
|
|
337
|
+
return mapCursorSubagentTypeToOpenCode("explore");
|
|
338
|
+
if (subtype.cursor_guide != null)
|
|
339
|
+
return mapCursorSubagentTypeToOpenCode("cursor_guide");
|
|
340
|
+
if (subtype.bash != null)
|
|
341
|
+
return mapCursorSubagentTypeToOpenCode("bash");
|
|
342
|
+
if (subtype.shell != null)
|
|
343
|
+
return mapCursorSubagentTypeToOpenCode("shell");
|
|
344
|
+
if (subtype.debug != null)
|
|
345
|
+
return mapCursorSubagentTypeToOpenCode("debug");
|
|
346
|
+
if (subtype.computer_use != null)
|
|
347
|
+
return mapCursorSubagentTypeToOpenCode("computer_use");
|
|
348
|
+
if (subtype.browser_use != null)
|
|
349
|
+
return mapCursorSubagentTypeToOpenCode("browser_use");
|
|
350
|
+
if (subtype.media_review != null)
|
|
351
|
+
return mapCursorSubagentTypeToOpenCode("media_review");
|
|
352
|
+
if (subtype.watch_video != null)
|
|
353
|
+
return mapCursorSubagentTypeToOpenCode("watch_video");
|
|
354
|
+
if (subtype.vm_setup_helper != null)
|
|
355
|
+
return mapCursorSubagentTypeToOpenCode("vm_setup_helper");
|
|
356
|
+
if (subtype.unspecified != null)
|
|
357
|
+
return mapCursorSubagentTypeToOpenCode("unspecified");
|
|
338
358
|
return undefined;
|
|
339
359
|
}
|
|
340
360
|
/** Advertised OpenCode tool ids from Cursor McpToolDefinition descriptors. */
|
package/dist/protocol/tools.d.ts
CHANGED
|
@@ -4,6 +4,17 @@ export type OpencodeToolDef = {
|
|
|
4
4
|
name: string;
|
|
5
5
|
description?: string;
|
|
6
6
|
inputSchema?: unknown;
|
|
7
|
+
/** Original flattened identity when `name` is a Cursor-facing alias. */
|
|
8
|
+
sourceName?: string;
|
|
9
|
+
};
|
|
10
|
+
export declare const CUSTOM_WEBSEARCH_TOOL = "custom_websearch";
|
|
11
|
+
export declare const CUSTOM_WEBFETCH_TOOL = "custom_webfetch";
|
|
12
|
+
/** Cursor-facing alias → exact host tool name accepted by the AI SDK call. */
|
|
13
|
+
export type ToolAliasRegistry = ReadonlyMap<string, string>;
|
|
14
|
+
export type AliasedToolCatalog = {
|
|
15
|
+
advertisedTools: OpencodeToolDef[];
|
|
16
|
+
aliases: ToolAliasRegistry;
|
|
17
|
+
ambiguous: ReadonlyMap<string, string[]>;
|
|
7
18
|
};
|
|
8
19
|
export type ToolServerIdentity = {
|
|
9
20
|
/** Cursor MCP server id / provider_identifier (e.g. opencode, github). */
|
|
@@ -32,6 +43,13 @@ export declare function resolveToolServerIdentity(opencodeName: string, defaultS
|
|
|
32
43
|
* reconstructed in `mcpRealToolName`.
|
|
33
44
|
*/
|
|
34
45
|
export declare function toolsToDescriptors(tools: OpencodeToolDef[], providerIdentifier?: string, knownMcpServers?: Iterable<string>): Array<Record<string, unknown>>;
|
|
46
|
+
/**
|
|
47
|
+
* Give collision-prone web capabilities names Cursor will not confuse with its
|
|
48
|
+
* native UI-bound WebSearch/WebFetch interactions. Exact host tools win; a
|
|
49
|
+
* flattened MCP suffix is accepted only when it identifies one unique tool.
|
|
50
|
+
*/
|
|
51
|
+
export declare function buildCustomWebToolAliases(tools: OpencodeToolDef[]): AliasedToolCatalog;
|
|
52
|
+
export declare function resolveCustomWebToolAlias(toolName: string, aliases: ToolAliasRegistry | undefined): string;
|
|
35
53
|
/**
|
|
36
54
|
* Build the nested McpFileSystemOptions / McpMetaToolOptions shape used by
|
|
37
55
|
* requestContext.#23 / #34. One `McpDescriptor` per resolved server (builtins
|
|
@@ -46,6 +64,29 @@ export declare function toolsToMcpDescriptors(tools: OpencodeToolDef[], provider
|
|
|
46
64
|
export declare function buildLiveRequestContext(tools: OpencodeToolDef[], providerIdentifier?: string, knownMcpServers?: Iterable<string>): Record<string, unknown>;
|
|
47
65
|
export declare function mapExecServerToToolName(execField: string): string | undefined;
|
|
48
66
|
export declare function mapToolNameToExecField(toolName: string): string | undefined;
|
|
67
|
+
export type HostSubagentDefinition = {
|
|
68
|
+
name: string;
|
|
69
|
+
description?: string;
|
|
70
|
+
};
|
|
71
|
+
export type HostSubagentCatalog = {
|
|
72
|
+
executor?: "task" | "actor";
|
|
73
|
+
agents: HostSubagentDefinition[];
|
|
74
|
+
/** True when the host tool supplied its complete, permission-filtered catalog. */
|
|
75
|
+
complete: boolean;
|
|
76
|
+
};
|
|
77
|
+
/** Extract the current host's permission-filtered Task/Actor recipient catalog. */
|
|
78
|
+
export declare function extractHostSubagentCatalog(tools: OpencodeToolDef[]): HostSubagentCatalog;
|
|
79
|
+
/** Resolve a Cursor subtype against the agents the host can spawn this turn. */
|
|
80
|
+
export declare function resolveCursorSubagentType(subagentType: string, catalog?: HostSubagentCatalog): string | undefined;
|
|
81
|
+
/**
|
|
82
|
+
* Cursor's native Task/subagent protocol uses Cursor-owned subtype names while
|
|
83
|
+
* OpenCode-family hosts execute named agents. Map known Cursor built-ins to
|
|
84
|
+
* the closest host agent and preserve unknown values so user-defined OpenCode
|
|
85
|
+
* agents can still be called by exact name.
|
|
86
|
+
*/
|
|
87
|
+
export declare function mapCursorSubagentTypeToOpenCode(subagentType: string): string;
|
|
88
|
+
/** Resolve a native Cursor subagent request to this turn's executor and catalog. */
|
|
89
|
+
export declare function remapNativeSubagentForCatalog(parsed: ParsedExecRequest, advertisedToolNames: Iterable<string>, catalog?: HostSubagentCatalog): void;
|
|
49
90
|
export type ParsedExecRequest = {
|
|
50
91
|
id: number;
|
|
51
92
|
execId: string;
|
|
@@ -74,6 +115,32 @@ export declare function mapCursorArgsToOpencode(toolName: string, raw: Record<st
|
|
|
74
115
|
* → github_create_pull_request). Builtins under the default server stay bare.
|
|
75
116
|
*/
|
|
76
117
|
export declare function mcpRealToolName(mcpArgs: Record<string, unknown>, defaultServer?: string): string;
|
|
118
|
+
/**
|
|
119
|
+
* Resolve a read target the way Cursor's LocalReadExecutor does before it
|
|
120
|
+
* stats the file: untildify, then resolve a relative path against the workspace
|
|
121
|
+
* root (`env.workspace_paths[0]`), else resolve as-is. Kept in lockstep with
|
|
122
|
+
* agent utils `resolvePath(path, workspaceRoot)`.
|
|
123
|
+
*/
|
|
124
|
+
export declare function resolveReadTargetPath(requested: string, workspaceRoot: string): string;
|
|
125
|
+
/**
|
|
126
|
+
* A typed ReadResult oneof case for a read target that fails Cursor's
|
|
127
|
+
* pre-execution validation, or undefined when the path is a readable file the
|
|
128
|
+
* tool should actually read. Mirrors LocalReadExecutor exactly:
|
|
129
|
+
* - missing path (ENOENT/ENOTDIR) → file_not_found
|
|
130
|
+
* - directory → invalid_file "Path is a directory, not a file"
|
|
131
|
+
* - socket/fifo/etc (not a file) → invalid_file "Path is neither a file nor a directory"
|
|
132
|
+
* EACCES/EPERM (exists but unreadable) and any other stat error return undefined
|
|
133
|
+
* so the call proceeds to OpenCode and a genuine permission decision is never
|
|
134
|
+
* masked. Never rejects a non-existent *write/edit* target — this is read-only.
|
|
135
|
+
*/
|
|
136
|
+
export declare function classifyMissingReadTarget(absolutePath: string): Record<string, unknown> | undefined;
|
|
137
|
+
/**
|
|
138
|
+
* Encode a pre-execution read rejection as the exact frames Cursor's client
|
|
139
|
+
* emits: the ExecClientMessage carrying the typed ReadResult oneof case, then
|
|
140
|
+
* the ACM #5 stream_close for that id. `readResult` is the case object from
|
|
141
|
+
* classifyMissingReadTarget (e.g. `{ file_not_found: { path } }`).
|
|
142
|
+
*/
|
|
143
|
+
export declare function buildReadRejectionMessages(execId: number, readResult: Record<string, unknown>): Uint8Array[];
|
|
77
144
|
export type ToolResultInput = {
|
|
78
145
|
execId: number;
|
|
79
146
|
/** ExecClientMessage result field (from ParsedExecRequest.resultField). */
|
package/dist/protocol/tools.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
2
4
|
import { encodeMessage } from "./messages.js";
|
|
3
5
|
import { encodeJsonAsValue, decodeStructEntriesToJson, readAllFields } from "./struct.js";
|
|
4
6
|
import { buildEnv } from "../context/env.js";
|
|
@@ -10,6 +12,8 @@ import { BACKGROUND_SHELL_MARKER, buildBackgroundShellCommand, } from "../shell-
|
|
|
10
12
|
// probe (ExecServerMessage #10 → ExecClientMessage #10). request/result share a
|
|
11
13
|
// field number for every exec variant, so this is also the result field.
|
|
12
14
|
export const REQUEST_CONTEXT_RESULT_FIELD = 10;
|
|
15
|
+
export const CUSTOM_WEBSEARCH_TOOL = "custom_websearch";
|
|
16
|
+
export const CUSTOM_WEBFETCH_TOOL = "custom_webfetch";
|
|
13
17
|
/** Match OpenCode's McpCatalog.sanitize for config server ids. */
|
|
14
18
|
export function sanitizeMcpServerId(value) {
|
|
15
19
|
return value.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
@@ -51,13 +55,17 @@ export function resolveToolServerIdentity(opencodeName, defaultServer = "opencod
|
|
|
51
55
|
*/
|
|
52
56
|
export function toolsToDescriptors(tools, providerIdentifier = "opencode", knownMcpServers = []) {
|
|
53
57
|
return tools.map((t) => {
|
|
54
|
-
const id = resolveToolServerIdentity(t.name, providerIdentifier, knownMcpServers);
|
|
58
|
+
const id = resolveToolServerIdentity(t.sourceName ?? t.name, providerIdentifier, knownMcpServers);
|
|
59
|
+
const collisionSafeWebAlias = t.name === CUSTOM_WEBSEARCH_TOOL || t.name === CUSTOM_WEBFETCH_TOOL;
|
|
55
60
|
return {
|
|
56
|
-
name
|
|
61
|
+
// Keep the collision-safe public name exact. Prefixing it with the
|
|
62
|
+
// synthetic default server weakens the distinction from Cursor-native
|
|
63
|
+
// web tools in the model-visible catalog.
|
|
64
|
+
name: collisionSafeWebAlias ? t.name : `${id.server}-${id.toolName}`,
|
|
57
65
|
description: t.description ?? "",
|
|
58
66
|
input_schema: encodeJsonAsValue(normalizeInputSchema(t.inputSchema)),
|
|
59
67
|
provider_identifier: id.server,
|
|
60
|
-
tool_name: id.toolName,
|
|
68
|
+
tool_name: t.sourceName ? t.name : id.toolName,
|
|
61
69
|
};
|
|
62
70
|
});
|
|
63
71
|
}
|
|
@@ -67,6 +75,67 @@ function normalizeInputSchema(schema) {
|
|
|
67
75
|
}
|
|
68
76
|
return { type: "object", properties: {} };
|
|
69
77
|
}
|
|
78
|
+
const WEB_ALIAS_RULES = [
|
|
79
|
+
{
|
|
80
|
+
alias: CUSTOM_WEBSEARCH_TOOL,
|
|
81
|
+
exact: ["websearch", "web_search"],
|
|
82
|
+
suffixes: ["_web_search", "-web_search", "_websearch", "-websearch"],
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
alias: CUSTOM_WEBFETCH_TOOL,
|
|
86
|
+
exact: ["webfetch", "web_fetch"],
|
|
87
|
+
suffixes: ["_web_fetch", "-web_fetch", "_webfetch", "-webfetch"],
|
|
88
|
+
},
|
|
89
|
+
];
|
|
90
|
+
/**
|
|
91
|
+
* Give collision-prone web capabilities names Cursor will not confuse with its
|
|
92
|
+
* native UI-bound WebSearch/WebFetch interactions. Exact host tools win; a
|
|
93
|
+
* flattened MCP suffix is accepted only when it identifies one unique tool.
|
|
94
|
+
*/
|
|
95
|
+
export function buildCustomWebToolAliases(tools) {
|
|
96
|
+
const aliases = new Map();
|
|
97
|
+
const ambiguous = new Map();
|
|
98
|
+
const replacements = new Map();
|
|
99
|
+
for (const rule of WEB_ALIAS_RULES) {
|
|
100
|
+
// A host/plugin may already expose the collision-safe public name. Keep it
|
|
101
|
+
// authoritative instead of hiding it behind a second mapping.
|
|
102
|
+
if (tools.some((tool) => tool.name === rule.alias))
|
|
103
|
+
continue;
|
|
104
|
+
const exact = tools.filter((tool) => rule.exact.includes(tool.name.toLowerCase()));
|
|
105
|
+
const candidates = exact.length > 0
|
|
106
|
+
? exact
|
|
107
|
+
: tools.filter((tool) => {
|
|
108
|
+
const name = tool.name.toLowerCase();
|
|
109
|
+
return rule.suffixes.some((suffix) => name.endsWith(suffix));
|
|
110
|
+
});
|
|
111
|
+
if (candidates.length !== 1) {
|
|
112
|
+
if (candidates.length > 1)
|
|
113
|
+
ambiguous.set(rule.alias, candidates.map((tool) => tool.name));
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const original = candidates[0].name;
|
|
117
|
+
aliases.set(rule.alias, original);
|
|
118
|
+
replacements.set(original, rule.alias);
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
advertisedTools: tools.map((tool) => {
|
|
122
|
+
const alias = replacements.get(tool.name);
|
|
123
|
+
return alias ? { ...tool, name: alias, sourceName: tool.name } : { ...tool };
|
|
124
|
+
}),
|
|
125
|
+
aliases,
|
|
126
|
+
ambiguous,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
export function resolveCustomWebToolAlias(toolName, aliases) {
|
|
130
|
+
const direct = aliases?.get(toolName);
|
|
131
|
+
if (direct)
|
|
132
|
+
return direct;
|
|
133
|
+
for (const [alias, original] of aliases ?? []) {
|
|
134
|
+
if (toolName.endsWith(`_${alias}`))
|
|
135
|
+
return original;
|
|
136
|
+
}
|
|
137
|
+
return toolName;
|
|
138
|
+
}
|
|
70
139
|
/**
|
|
71
140
|
* Build the nested McpFileSystemOptions / McpMetaToolOptions shape used by
|
|
72
141
|
* requestContext.#23 / #34. One `McpDescriptor` per resolved server (builtins
|
|
@@ -79,7 +148,7 @@ export function toolsToMcpDescriptors(tools, providerIdentifier = "opencode", kn
|
|
|
79
148
|
const order = [];
|
|
80
149
|
const byServer = new Map();
|
|
81
150
|
for (const t of tools) {
|
|
82
|
-
const id = resolveToolServerIdentity(t.name, providerIdentifier, knownMcpServers);
|
|
151
|
+
const id = resolveToolServerIdentity(t.sourceName ?? t.name, providerIdentifier, knownMcpServers);
|
|
83
152
|
let list = byServer.get(id.server);
|
|
84
153
|
if (!list) {
|
|
85
154
|
list = [];
|
|
@@ -87,7 +156,7 @@ export function toolsToMcpDescriptors(tools, providerIdentifier = "opencode", kn
|
|
|
87
156
|
order.push(id.server);
|
|
88
157
|
}
|
|
89
158
|
list.push({
|
|
90
|
-
tool_name: id.toolName,
|
|
159
|
+
tool_name: t.sourceName ? t.name : id.toolName,
|
|
91
160
|
description: t.description ?? "",
|
|
92
161
|
input_schema: encodeJsonAsValue(normalizeInputSchema(t.inputSchema)),
|
|
93
162
|
});
|
|
@@ -118,6 +187,8 @@ export function buildLiveRequestContext(tools, providerIdentifier = "opencode",
|
|
|
118
187
|
enabled: true,
|
|
119
188
|
mcp_descriptors: nested,
|
|
120
189
|
},
|
|
190
|
+
web_search_enabled: false,
|
|
191
|
+
web_fetch_enabled: false,
|
|
121
192
|
rules_info_complete: true,
|
|
122
193
|
env_info_complete: true,
|
|
123
194
|
repository_info_complete: true,
|
|
@@ -211,6 +282,200 @@ export function mapExecServerToToolName(execField) {
|
|
|
211
282
|
export function mapToolNameToExecField(toolName) {
|
|
212
283
|
return opencodeToolToCursor[toolName];
|
|
213
284
|
}
|
|
285
|
+
const CURSOR_SUBAGENT_TYPE_TO_OPENCODE = {
|
|
286
|
+
generalPurpose: "general",
|
|
287
|
+
"general-purpose": "general",
|
|
288
|
+
general_purpose: "general",
|
|
289
|
+
general: "general",
|
|
290
|
+
unspecified: "general",
|
|
291
|
+
"cursor-guide": "explore",
|
|
292
|
+
cursor_guide: "explore",
|
|
293
|
+
"best-of-n-runner": "general",
|
|
294
|
+
best_of_n_runner: "general",
|
|
295
|
+
bash: "general",
|
|
296
|
+
shell: "general",
|
|
297
|
+
debug: "general",
|
|
298
|
+
computer_use: "general",
|
|
299
|
+
computerUse: "general",
|
|
300
|
+
browser_use: "general",
|
|
301
|
+
browserUse: "general",
|
|
302
|
+
media_review: "general",
|
|
303
|
+
mediaReview: "general",
|
|
304
|
+
watch_video: "general",
|
|
305
|
+
watchVideo: "general",
|
|
306
|
+
vm_setup_helper: "general",
|
|
307
|
+
vmSetupHelper: "general",
|
|
308
|
+
explore: "explore",
|
|
309
|
+
bugbot: "explore",
|
|
310
|
+
"security-review": "explore",
|
|
311
|
+
security_review: "explore",
|
|
312
|
+
};
|
|
313
|
+
const SUBAGENT_CATALOG_MARKER = "Available agent types and the tools they have access to:";
|
|
314
|
+
function parseSubagentDescriptionCatalog(description) {
|
|
315
|
+
if (!description)
|
|
316
|
+
return { found: false, agents: [] };
|
|
317
|
+
const marker = description.indexOf(SUBAGENT_CATALOG_MARKER);
|
|
318
|
+
if (marker < 0)
|
|
319
|
+
return { found: false, agents: [] };
|
|
320
|
+
const agents = [];
|
|
321
|
+
const lines = description.slice(marker + SUBAGENT_CATALOG_MARKER.length).split(/\r?\n/);
|
|
322
|
+
let started = false;
|
|
323
|
+
for (const line of lines) {
|
|
324
|
+
const match = line.match(/^\s*-\s+([^:]+):\s*(.*)$/);
|
|
325
|
+
if (!match) {
|
|
326
|
+
if (started && line.trim())
|
|
327
|
+
break;
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
started = true;
|
|
331
|
+
const name = match[1].trim().replace(/^`|`$/g, "");
|
|
332
|
+
if (!name)
|
|
333
|
+
continue;
|
|
334
|
+
const agentDescription = match[2].trim();
|
|
335
|
+
agents.push({
|
|
336
|
+
name,
|
|
337
|
+
...(agentDescription ? { description: agentDescription } : {}),
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
return { found: true, agents };
|
|
341
|
+
}
|
|
342
|
+
function subagentTypeEnumValues(schema) {
|
|
343
|
+
const out = new Set();
|
|
344
|
+
const seen = new Set();
|
|
345
|
+
const visit = (value, propertyName) => {
|
|
346
|
+
if (!value || typeof value !== "object")
|
|
347
|
+
return;
|
|
348
|
+
if (seen.has(value))
|
|
349
|
+
return;
|
|
350
|
+
seen.add(value);
|
|
351
|
+
if (Array.isArray(value)) {
|
|
352
|
+
for (const item of value)
|
|
353
|
+
visit(item, propertyName);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
const record = value;
|
|
357
|
+
if (propertyName === "subagent_type" && Array.isArray(record.enum)) {
|
|
358
|
+
for (const item of record.enum) {
|
|
359
|
+
if (typeof item === "string" && item)
|
|
360
|
+
out.add(item);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
for (const [key, child] of Object.entries(record))
|
|
364
|
+
visit(child, key);
|
|
365
|
+
};
|
|
366
|
+
visit(schema);
|
|
367
|
+
return [...out];
|
|
368
|
+
}
|
|
369
|
+
/** Extract the current host's permission-filtered Task/Actor recipient catalog. */
|
|
370
|
+
export function extractHostSubagentCatalog(tools) {
|
|
371
|
+
const executorTool = tools.find((tool) => tool.name === "actor") ??
|
|
372
|
+
tools.find((tool) => tool.name === "task");
|
|
373
|
+
if (!executorTool)
|
|
374
|
+
return { agents: [], complete: true };
|
|
375
|
+
const described = parseSubagentDescriptionCatalog(executorTool.description);
|
|
376
|
+
const enumNames = subagentTypeEnumValues(executorTool.inputSchema);
|
|
377
|
+
const descriptions = new Map(described.agents.map((agent) => [agent.name, agent.description]));
|
|
378
|
+
// MiMo's Actor enum is stricter than its prose catalog (`mode: subagent`
|
|
379
|
+
// rather than every non-primary agent), so structured names are authoritative.
|
|
380
|
+
const names = new Set(enumNames.length > 0 ? enumNames : described.agents.map((agent) => agent.name));
|
|
381
|
+
return {
|
|
382
|
+
executor: executorTool.name,
|
|
383
|
+
agents: [...names].map((name) => ({
|
|
384
|
+
name,
|
|
385
|
+
...(descriptions.get(name) ? { description: descriptions.get(name) } : {}),
|
|
386
|
+
})),
|
|
387
|
+
complete: enumNames.length > 0 || described.found,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
const GENERIC_CURSOR_SUBAGENT_TYPES = new Set([
|
|
391
|
+
"generalPurpose",
|
|
392
|
+
"general-purpose",
|
|
393
|
+
"general_purpose",
|
|
394
|
+
"general",
|
|
395
|
+
"unspecified",
|
|
396
|
+
]);
|
|
397
|
+
function cursorSubagentCandidates(subagentType) {
|
|
398
|
+
if (GENERIC_CURSOR_SUBAGENT_TYPES.has(subagentType))
|
|
399
|
+
return ["general"];
|
|
400
|
+
if (subagentType === "cursor-guide" || subagentType === "cursor_guide") {
|
|
401
|
+
return ["scout", "explore", "general"];
|
|
402
|
+
}
|
|
403
|
+
if (subagentType === "explore" ||
|
|
404
|
+
subagentType === "bugbot" ||
|
|
405
|
+
subagentType === "security-review" ||
|
|
406
|
+
subagentType === "security_review") {
|
|
407
|
+
return ["explore", "general"];
|
|
408
|
+
}
|
|
409
|
+
return ["general"];
|
|
410
|
+
}
|
|
411
|
+
/** Resolve a Cursor subtype against the agents the host can spawn this turn. */
|
|
412
|
+
export function resolveCursorSubagentType(subagentType, catalog) {
|
|
413
|
+
const available = new Set(catalog?.agents.map((agent) => agent.name) ?? []);
|
|
414
|
+
// Explicit host/custom names win. `unspecified` and general aliases are
|
|
415
|
+
// intentionally excluded so they always select the host's generic agent.
|
|
416
|
+
if (!GENERIC_CURSOR_SUBAGENT_TYPES.has(subagentType) && available.has(subagentType)) {
|
|
417
|
+
return subagentType;
|
|
418
|
+
}
|
|
419
|
+
const candidates = cursorSubagentCandidates(subagentType);
|
|
420
|
+
for (const candidate of candidates) {
|
|
421
|
+
if (available.has(candidate))
|
|
422
|
+
return candidate;
|
|
423
|
+
}
|
|
424
|
+
if (catalog?.complete)
|
|
425
|
+
return undefined;
|
|
426
|
+
return candidates[0];
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Cursor's native Task/subagent protocol uses Cursor-owned subtype names while
|
|
430
|
+
* OpenCode-family hosts execute named agents. Map known Cursor built-ins to
|
|
431
|
+
* the closest host agent and preserve unknown values so user-defined OpenCode
|
|
432
|
+
* agents can still be called by exact name.
|
|
433
|
+
*/
|
|
434
|
+
export function mapCursorSubagentTypeToOpenCode(subagentType) {
|
|
435
|
+
return CURSOR_SUBAGENT_TYPE_TO_OPENCODE[subagentType] ?? subagentType;
|
|
436
|
+
}
|
|
437
|
+
/** Resolve a native Cursor subagent request to this turn's executor and catalog. */
|
|
438
|
+
export function remapNativeSubagentForCatalog(parsed, advertisedToolNames, catalog) {
|
|
439
|
+
if (parsed.resultField !== "subagent_result" || parsed.toolName !== "task")
|
|
440
|
+
return;
|
|
441
|
+
const advertised = new Set(advertisedToolNames);
|
|
442
|
+
const executor = advertised.has("actor") ? "actor" : advertised.has("task") ? "task" : undefined;
|
|
443
|
+
if (!executor)
|
|
444
|
+
return;
|
|
445
|
+
const description = str(parsed.args.description) ?? "";
|
|
446
|
+
const prompt = str(parsed.args.prompt) ?? "";
|
|
447
|
+
const cursorSubagentType = str(parsed.resultMetadata?.cursor_subagent_type) ??
|
|
448
|
+
str(parsed.args.subagent_type) ?? "";
|
|
449
|
+
const subagentType = resolveCursorSubagentType(cursorSubagentType, catalog);
|
|
450
|
+
if (!subagentType) {
|
|
451
|
+
const available = catalog?.agents.map((agent) => agent.name).join(", ") || "none";
|
|
452
|
+
parsed.localError =
|
|
453
|
+
`Cursor subagent '${cursorSubagentType}' has no compatible host agent. ` +
|
|
454
|
+
`Available subagents: ${available}.`;
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
const resumeAgentId = str(parsed.args.task_id);
|
|
458
|
+
parsed.toolName = executor;
|
|
459
|
+
if (executor === "task") {
|
|
460
|
+
parsed.args = {
|
|
461
|
+
description,
|
|
462
|
+
prompt,
|
|
463
|
+
subagent_type: subagentType,
|
|
464
|
+
...(resumeAgentId ? { task_id: resumeAgentId } : {}),
|
|
465
|
+
...(parsed.args.background === true ? { background: true } : {}),
|
|
466
|
+
};
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
parsed.args = {
|
|
470
|
+
operation: {
|
|
471
|
+
action: parsed.args.background === true ? "spawn" : "run",
|
|
472
|
+
description,
|
|
473
|
+
prompt,
|
|
474
|
+
subagent_type: subagentType,
|
|
475
|
+
...(resumeAgentId ? { actor_id: resumeAgentId } : {}),
|
|
476
|
+
},
|
|
477
|
+
};
|
|
478
|
+
}
|
|
214
479
|
export function parseExecServerMessage(msg) {
|
|
215
480
|
const id = msg.id;
|
|
216
481
|
if (id === undefined)
|
|
@@ -289,6 +554,9 @@ export function parseExecServerMessage(msg) {
|
|
|
289
554
|
toolName: "task",
|
|
290
555
|
args,
|
|
291
556
|
resultField,
|
|
557
|
+
resultMetadata: cursorSubagentType
|
|
558
|
+
? { cursor_subagent_type: cursorSubagentType }
|
|
559
|
+
: undefined,
|
|
292
560
|
localError: prompt && cursorSubagentType
|
|
293
561
|
? undefined
|
|
294
562
|
: "Cursor subagent request is missing a required prompt or subagent type.",
|
|
@@ -526,19 +794,6 @@ function describeSubagentTask(prompt, subagentType) {
|
|
|
526
794
|
return words.join(" ");
|
|
527
795
|
return `${subagentType || "Delegated"} task`;
|
|
528
796
|
}
|
|
529
|
-
function mapCursorSubagentTypeToOpenCode(subagentType) {
|
|
530
|
-
// Cursor's built-in general agent uses a camelCase protocol identifier, and
|
|
531
|
-
// its native Bugbot reviewer has no OpenCode-specific agent definition.
|
|
532
|
-
// OpenCode `general` is the general-purpose equivalent; `explore` preserves
|
|
533
|
-
// Bugbot's review-only/read-oriented semantics while retaining grep/read/bash.
|
|
534
|
-
// Preserve every other value so `explore` and configured custom agents keep
|
|
535
|
-
// their exact advertised names.
|
|
536
|
-
if (subagentType === "generalPurpose")
|
|
537
|
-
return "general";
|
|
538
|
-
if (subagentType === "bugbot")
|
|
539
|
-
return "explore";
|
|
540
|
-
return subagentType;
|
|
541
|
-
}
|
|
542
797
|
function shellQuote(s) {
|
|
543
798
|
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
544
799
|
}
|
|
@@ -598,6 +853,75 @@ function findOneOfVariant(msg, candidates) {
|
|
|
598
853
|
}
|
|
599
854
|
return undefined;
|
|
600
855
|
}
|
|
856
|
+
// ── Pre-execution read validation (Cursor LocalReadExecutor parity) ──
|
|
857
|
+
/** Expand a leading `~` to the home directory, matching Cursor's untildify. */
|
|
858
|
+
function untildify(input) {
|
|
859
|
+
return input.replace(/^~(?=$|\/|\\)/, os.homedir());
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Resolve a read target the way Cursor's LocalReadExecutor does before it
|
|
863
|
+
* stats the file: untildify, then resolve a relative path against the workspace
|
|
864
|
+
* root (`env.workspace_paths[0]`), else resolve as-is. Kept in lockstep with
|
|
865
|
+
* agent utils `resolvePath(path, workspaceRoot)`.
|
|
866
|
+
*/
|
|
867
|
+
export function resolveReadTargetPath(requested, workspaceRoot) {
|
|
868
|
+
const expanded = untildify(requested);
|
|
869
|
+
if (workspaceRoot && !path.isAbsolute(expanded)) {
|
|
870
|
+
return path.resolve(workspaceRoot, expanded);
|
|
871
|
+
}
|
|
872
|
+
return path.resolve(expanded);
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* A typed ReadResult oneof case for a read target that fails Cursor's
|
|
876
|
+
* pre-execution validation, or undefined when the path is a readable file the
|
|
877
|
+
* tool should actually read. Mirrors LocalReadExecutor exactly:
|
|
878
|
+
* - missing path (ENOENT/ENOTDIR) → file_not_found
|
|
879
|
+
* - directory → invalid_file "Path is a directory, not a file"
|
|
880
|
+
* - socket/fifo/etc (not a file) → invalid_file "Path is neither a file nor a directory"
|
|
881
|
+
* EACCES/EPERM (exists but unreadable) and any other stat error return undefined
|
|
882
|
+
* so the call proceeds to OpenCode and a genuine permission decision is never
|
|
883
|
+
* masked. Never rejects a non-existent *write/edit* target — this is read-only.
|
|
884
|
+
*/
|
|
885
|
+
export function classifyMissingReadTarget(absolutePath) {
|
|
886
|
+
let stat;
|
|
887
|
+
try {
|
|
888
|
+
stat = fs.statSync(absolutePath);
|
|
889
|
+
}
|
|
890
|
+
catch (e) {
|
|
891
|
+
const code = e.code;
|
|
892
|
+
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
893
|
+
return { file_not_found: { path: absolutePath } };
|
|
894
|
+
}
|
|
895
|
+
return undefined;
|
|
896
|
+
}
|
|
897
|
+
if (stat.isDirectory()) {
|
|
898
|
+
return { invalid_file: { path: absolutePath, reason: "Path is a directory, not a file" } };
|
|
899
|
+
}
|
|
900
|
+
if (!stat.isFile()) {
|
|
901
|
+
return {
|
|
902
|
+
invalid_file: { path: absolutePath, reason: "Path is neither a file nor a directory" },
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
return undefined;
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Encode a pre-execution read rejection as the exact frames Cursor's client
|
|
909
|
+
* emits: the ExecClientMessage carrying the typed ReadResult oneof case, then
|
|
910
|
+
* the ACM #5 stream_close for that id. `readResult` is the case object from
|
|
911
|
+
* classifyMissingReadTarget (e.g. `{ file_not_found: { path } }`).
|
|
912
|
+
*/
|
|
913
|
+
export function buildReadRejectionMessages(execId, readResult) {
|
|
914
|
+
return [
|
|
915
|
+
encodeMessage("AgentClientMessage", {
|
|
916
|
+
exec_client_message: {
|
|
917
|
+
id: execId,
|
|
918
|
+
local_execution_time_ms: 0,
|
|
919
|
+
read_result: readResult,
|
|
920
|
+
},
|
|
921
|
+
}),
|
|
922
|
+
buildExecStreamClose(execId),
|
|
923
|
+
];
|
|
924
|
+
}
|
|
601
925
|
/**
|
|
602
926
|
* Build one or more ExecClientMessage frames for a tool result.
|
|
603
927
|
* Shell replies are a sequence of ShellStream oneofs under the same id —
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { CursorProviderError } from "./errors.js";
|
|
2
|
+
export type ReplayBarrierReason = "visible-text" | "visible-reasoning" | "display-tool-lifecycle" | "non-control-exec" | "stateful-interaction" | "unknown-or-malformed-frame";
|
|
3
|
+
export declare class AttemptReplaySafety {
|
|
4
|
+
private readonly sessionId;
|
|
5
|
+
private barrierReason;
|
|
6
|
+
constructor(sessionId: string);
|
|
7
|
+
markBarrier(reason: ReplayBarrierReason): void;
|
|
8
|
+
applyTo(failure: CursorProviderError): CursorProviderError;
|
|
9
|
+
}
|
|
10
|
+
export type DecodedReplayFrame = {
|
|
11
|
+
interactionUpdate?: Record<string, unknown>;
|
|
12
|
+
exec?: Record<string, unknown>;
|
|
13
|
+
kv?: Record<string, unknown>;
|
|
14
|
+
execControl?: Record<string, unknown>;
|
|
15
|
+
interactionQuery?: Record<string, unknown>;
|
|
16
|
+
checkpointBytes?: Uint8Array;
|
|
17
|
+
};
|
|
18
|
+
export type ReplayFrameAnalysis = {
|
|
19
|
+
semanticProgress: boolean;
|
|
20
|
+
barrier?: ReplayBarrierReason;
|
|
21
|
+
};
|
|
22
|
+
/** Classify one decoded server frame without performing any protocol side effects. */
|
|
23
|
+
export declare function analyzeReplayFrame(payload: Uint8Array, decoded: DecodedReplayFrame): ReplayFrameAnalysis;
|