cursor-opencode-provider 0.2.6 → 0.2.8
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 +31 -15
- package/dist/agent-url.d.ts +8 -5
- package/dist/auth.js +59 -27
- package/dist/context/build.js +12 -1
- package/dist/context/env.d.ts +6 -0
- package/dist/context/env.js +34 -2
- package/dist/context/index.d.ts +1 -1
- package/dist/context/index.js +1 -1
- package/dist/context/paths.d.ts +56 -3
- package/dist/context/paths.js +135 -9
- package/dist/deadline.d.ts +8 -0
- package/dist/deadline.js +25 -0
- package/dist/debug.d.ts +7 -0
- package/dist/debug.js +42 -0
- package/dist/index.d.ts +6 -0
- package/dist/language-model.js +133 -74
- package/dist/models.d.ts +2 -0
- package/dist/plugin-v2.js +4 -0
- package/dist/plugin.js +16 -5
- package/dist/protocol/client-version.js +7 -6
- package/dist/protocol/messages.js +6 -0
- package/dist/protocol/struct.d.ts +18 -0
- package/dist/protocol/struct.js +82 -0
- package/dist/protocol/tools.d.ts +18 -0
- package/dist/protocol/tools.js +89 -21
- package/dist/replay-safety.d.ts +23 -0
- package/dist/replay-safety.js +126 -0
- package/dist/session.d.ts +3 -0
- package/dist/shell-timeout.d.ts +15 -10
- package/dist/shell-timeout.js +91 -54
- 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 +9 -3
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { FALLBACK_CLIENT_VERSION, MODEL_CACHE_TTL_MS, VERSION_CACHE_FILE, } from "../shared.js";
|
|
4
4
|
import { opencodeGlobalCacheDir } from "../context/paths.js";
|
|
5
|
+
import { withAbortDeadline } from "../deadline.js";
|
|
5
6
|
const INSTALL_URL = "https://cursor.com/install";
|
|
6
7
|
const REMOTE_TIMEOUT_MS = 5_000;
|
|
7
8
|
const BUILD_RE = /^\d{4}\.\d{2}\.\d{2}-[0-9A-Za-z][0-9A-Za-z.-]*$/;
|
|
@@ -103,13 +104,13 @@ function isCacheFresh(cache, now = Date.now()) {
|
|
|
103
104
|
return age >= 0 && age < MODEL_CACHE_TTL_MS;
|
|
104
105
|
}
|
|
105
106
|
async function fetchInstallerVersion() {
|
|
106
|
-
|
|
107
|
-
signal
|
|
107
|
+
return withAbortDeadline(REMOTE_TIMEOUT_MS, () => new Error("Cursor installer version request timed out"), async (signal) => {
|
|
108
|
+
const response = await fetch(INSTALL_URL, { signal });
|
|
109
|
+
if (!response.ok)
|
|
110
|
+
return undefined;
|
|
111
|
+
const build = extractVersionFromInstaller(await response.text());
|
|
112
|
+
return build ? `cli-${build}` : undefined;
|
|
108
113
|
});
|
|
109
|
-
if (!response.ok)
|
|
110
|
-
return undefined;
|
|
111
|
-
const build = extractVersionFromInstaller(await response.text());
|
|
112
|
-
return build ? `cli-${build}` : undefined;
|
|
113
114
|
}
|
|
114
115
|
async function refreshVersionCache() {
|
|
115
116
|
const version = await fetchInstallerVersion();
|
|
@@ -653,8 +653,10 @@ export function createMessageTypes() {
|
|
|
653
653
|
{ id: 2, name: "workspace_paths", type: "string", repeated: true },
|
|
654
654
|
{ id: 3, name: "shell", type: "string" },
|
|
655
655
|
{ id: 5, name: "sandbox_enabled", type: "bool" },
|
|
656
|
+
{ id: 7, name: "terminals_folder", type: "string" },
|
|
656
657
|
{ id: 10, name: "time_zone", type: "string" },
|
|
657
658
|
{ id: 11, name: "project_folder", type: "string" },
|
|
659
|
+
{ id: 12, name: "agent_transcripts_folder", type: "string" },
|
|
658
660
|
{ id: 14, name: "sandbox_supported", type: "bool" },
|
|
659
661
|
{ id: 20, name: "is_working_dir_home_dir", type: "bool" },
|
|
660
662
|
{ id: 21, name: "process_working_directory", type: "string" },
|
|
@@ -722,8 +724,10 @@ export function createMessageTypes() {
|
|
|
722
724
|
{ id: 7, name: "tools", type: "McpToolDefinition", repeated: true },
|
|
723
725
|
{ id: 11, name: "git_repos", type: "GitRepoInfo", repeated: true },
|
|
724
726
|
{ id: 13, name: "project_layouts", type: "LsDirectoryTreeNode", repeated: true },
|
|
727
|
+
{ id: 17, name: "web_search_enabled", type: "bool" },
|
|
725
728
|
{ id: 22, name: "custom_subagents", type: "CustomSubagent", repeated: true },
|
|
726
729
|
{ id: 23, name: "mcp_file_system_options", type: "McpFileSystemOptions" },
|
|
730
|
+
{ id: 24, name: "web_fetch_enabled", type: "bool" },
|
|
727
731
|
{ id: 25, name: "hooks_additional_context", type: "string" },
|
|
728
732
|
{ id: 29, name: "agent_skills", type: "AgentSkill", repeated: true },
|
|
729
733
|
{ id: 33, name: "git_repo_info_complete", type: "bool" },
|
|
@@ -881,8 +885,10 @@ export function createMessageTypes() {
|
|
|
881
885
|
{ id: 7, name: "tools", type: "McpToolDefinition", repeated: true },
|
|
882
886
|
{ id: 11, name: "git_repos", type: "GitRepoInfo", repeated: true },
|
|
883
887
|
{ id: 13, name: "project_layouts", type: "LsDirectoryTreeNode", repeated: true },
|
|
888
|
+
{ id: 17, name: "web_search_enabled", type: "bool" },
|
|
884
889
|
{ id: 22, name: "custom_subagents", type: "CustomSubagent", repeated: true },
|
|
885
890
|
{ id: 23, name: "mcp_file_system_options", type: "McpFileSystemOptions" },
|
|
891
|
+
{ id: 24, name: "web_fetch_enabled", type: "bool" },
|
|
886
892
|
{ id: 25, name: "hooks_additional_context", type: "string" },
|
|
887
893
|
{ id: 29, name: "agent_skills", type: "AgentSkill", repeated: true },
|
|
888
894
|
{ id: 33, name: "git_repo_info_complete", type: "bool" },
|
|
@@ -7,6 +7,24 @@ export type RawField = {
|
|
|
7
7
|
};
|
|
8
8
|
/** Walk a protobuf message's top-level fields off the raw wire bytes. */
|
|
9
9
|
export declare function readAllFields(b: Uint8Array): RawField[];
|
|
10
|
+
export type StrictRawField = {
|
|
11
|
+
fn: number;
|
|
12
|
+
wt: number;
|
|
13
|
+
varint?: bigint;
|
|
14
|
+
bytes?: Uint8Array;
|
|
15
|
+
fixed64?: Uint8Array;
|
|
16
|
+
fixed32?: Uint8Array;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Strictly walk a protobuf message for security/replay decisions.
|
|
20
|
+
*
|
|
21
|
+
* Unlike `readAllFields`, this parser consumes the complete input, preserves
|
|
22
|
+
* uint64 varints as bigint, represents fixed-width fields, and rejects invalid
|
|
23
|
+
* tags, truncation, overflow, groups, and unsupported wire types. It deliberately
|
|
24
|
+
* returns `undefined` instead of a partial result: callers must fail closed when
|
|
25
|
+
* deciding whether replay is safe.
|
|
26
|
+
*/
|
|
27
|
+
export declare function readAllFieldsStrict(bytes: Uint8Array): StrictRawField[] | undefined;
|
|
10
28
|
/** Decode a google.protobuf.Value message (bytes) back into a JSON value. */
|
|
11
29
|
export declare function decodeValueToJson(bytes: Uint8Array): unknown;
|
|
12
30
|
/**
|
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)) {
|
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
|
package/dist/protocol/tools.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import { encodeMessage } from "./messages.js";
|
|
3
3
|
import { encodeJsonAsValue, decodeStructEntriesToJson, readAllFields } from "./struct.js";
|
|
4
|
-
import {
|
|
4
|
+
import { buildEnv } from "../context/env.js";
|
|
5
|
+
import { ensureOpencodeProjectDir } from "../context/paths.js";
|
|
6
|
+
import { trace, traceRequestContextPaths } from "../debug.js";
|
|
5
7
|
import { cursorExecVariantByRequestName } from "./exec-variants.js";
|
|
6
|
-
import { BACKGROUND_SHELL_MARKER } from "../shell-timeout.js";
|
|
8
|
+
import { BACKGROUND_SHELL_MARKER, buildBackgroundShellCommand, } from "../shell-timeout.js";
|
|
7
9
|
// Exec variant field number whose reply is the server-initiated request_context
|
|
8
10
|
// probe (ExecServerMessage #10 → ExecClientMessage #10). request/result share a
|
|
9
11
|
// field number for every exec variant, so this is also the result field.
|
|
10
12
|
export const REQUEST_CONTEXT_RESULT_FIELD = 10;
|
|
13
|
+
export const CUSTOM_WEBSEARCH_TOOL = "custom_websearch";
|
|
14
|
+
export const CUSTOM_WEBFETCH_TOOL = "custom_webfetch";
|
|
11
15
|
/** Match OpenCode's McpCatalog.sanitize for config server ids. */
|
|
12
16
|
export function sanitizeMcpServerId(value) {
|
|
13
17
|
return value.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
@@ -49,13 +53,17 @@ export function resolveToolServerIdentity(opencodeName, defaultServer = "opencod
|
|
|
49
53
|
*/
|
|
50
54
|
export function toolsToDescriptors(tools, providerIdentifier = "opencode", knownMcpServers = []) {
|
|
51
55
|
return tools.map((t) => {
|
|
52
|
-
const id = resolveToolServerIdentity(t.name, providerIdentifier, knownMcpServers);
|
|
56
|
+
const id = resolveToolServerIdentity(t.sourceName ?? t.name, providerIdentifier, knownMcpServers);
|
|
57
|
+
const collisionSafeWebAlias = t.name === CUSTOM_WEBSEARCH_TOOL || t.name === CUSTOM_WEBFETCH_TOOL;
|
|
53
58
|
return {
|
|
54
|
-
name
|
|
59
|
+
// Keep the collision-safe public name exact. Prefixing it with the
|
|
60
|
+
// synthetic default server weakens the distinction from Cursor-native
|
|
61
|
+
// web tools in the model-visible catalog.
|
|
62
|
+
name: collisionSafeWebAlias ? t.name : `${id.server}-${id.toolName}`,
|
|
55
63
|
description: t.description ?? "",
|
|
56
64
|
input_schema: encodeJsonAsValue(normalizeInputSchema(t.inputSchema)),
|
|
57
65
|
provider_identifier: id.server,
|
|
58
|
-
tool_name: id.toolName,
|
|
66
|
+
tool_name: t.sourceName ? t.name : id.toolName,
|
|
59
67
|
};
|
|
60
68
|
});
|
|
61
69
|
}
|
|
@@ -65,6 +73,67 @@ function normalizeInputSchema(schema) {
|
|
|
65
73
|
}
|
|
66
74
|
return { type: "object", properties: {} };
|
|
67
75
|
}
|
|
76
|
+
const WEB_ALIAS_RULES = [
|
|
77
|
+
{
|
|
78
|
+
alias: CUSTOM_WEBSEARCH_TOOL,
|
|
79
|
+
exact: ["websearch", "web_search"],
|
|
80
|
+
suffixes: ["_web_search", "-web_search", "_websearch", "-websearch"],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
alias: CUSTOM_WEBFETCH_TOOL,
|
|
84
|
+
exact: ["webfetch", "web_fetch"],
|
|
85
|
+
suffixes: ["_web_fetch", "-web_fetch", "_webfetch", "-webfetch"],
|
|
86
|
+
},
|
|
87
|
+
];
|
|
88
|
+
/**
|
|
89
|
+
* Give collision-prone web capabilities names Cursor will not confuse with its
|
|
90
|
+
* native UI-bound WebSearch/WebFetch interactions. Exact host tools win; a
|
|
91
|
+
* flattened MCP suffix is accepted only when it identifies one unique tool.
|
|
92
|
+
*/
|
|
93
|
+
export function buildCustomWebToolAliases(tools) {
|
|
94
|
+
const aliases = new Map();
|
|
95
|
+
const ambiguous = new Map();
|
|
96
|
+
const replacements = new Map();
|
|
97
|
+
for (const rule of WEB_ALIAS_RULES) {
|
|
98
|
+
// A host/plugin may already expose the collision-safe public name. Keep it
|
|
99
|
+
// authoritative instead of hiding it behind a second mapping.
|
|
100
|
+
if (tools.some((tool) => tool.name === rule.alias))
|
|
101
|
+
continue;
|
|
102
|
+
const exact = tools.filter((tool) => rule.exact.includes(tool.name.toLowerCase()));
|
|
103
|
+
const candidates = exact.length > 0
|
|
104
|
+
? exact
|
|
105
|
+
: tools.filter((tool) => {
|
|
106
|
+
const name = tool.name.toLowerCase();
|
|
107
|
+
return rule.suffixes.some((suffix) => name.endsWith(suffix));
|
|
108
|
+
});
|
|
109
|
+
if (candidates.length !== 1) {
|
|
110
|
+
if (candidates.length > 1)
|
|
111
|
+
ambiguous.set(rule.alias, candidates.map((tool) => tool.name));
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const original = candidates[0].name;
|
|
115
|
+
aliases.set(rule.alias, original);
|
|
116
|
+
replacements.set(original, rule.alias);
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
advertisedTools: tools.map((tool) => {
|
|
120
|
+
const alias = replacements.get(tool.name);
|
|
121
|
+
return alias ? { ...tool, name: alias, sourceName: tool.name } : { ...tool };
|
|
122
|
+
}),
|
|
123
|
+
aliases,
|
|
124
|
+
ambiguous,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
export function resolveCustomWebToolAlias(toolName, aliases) {
|
|
128
|
+
const direct = aliases?.get(toolName);
|
|
129
|
+
if (direct)
|
|
130
|
+
return direct;
|
|
131
|
+
for (const [alias, original] of aliases ?? []) {
|
|
132
|
+
if (toolName.endsWith(`_${alias}`))
|
|
133
|
+
return original;
|
|
134
|
+
}
|
|
135
|
+
return toolName;
|
|
136
|
+
}
|
|
68
137
|
/**
|
|
69
138
|
* Build the nested McpFileSystemOptions / McpMetaToolOptions shape used by
|
|
70
139
|
* requestContext.#23 / #34. One `McpDescriptor` per resolved server (builtins
|
|
@@ -77,7 +146,7 @@ export function toolsToMcpDescriptors(tools, providerIdentifier = "opencode", kn
|
|
|
77
146
|
const order = [];
|
|
78
147
|
const byServer = new Map();
|
|
79
148
|
for (const t of tools) {
|
|
80
|
-
const id = resolveToolServerIdentity(t.name, providerIdentifier, knownMcpServers);
|
|
149
|
+
const id = resolveToolServerIdentity(t.sourceName ?? t.name, providerIdentifier, knownMcpServers);
|
|
81
150
|
let list = byServer.get(id.server);
|
|
82
151
|
if (!list) {
|
|
83
152
|
list = [];
|
|
@@ -85,7 +154,7 @@ export function toolsToMcpDescriptors(tools, providerIdentifier = "opencode", kn
|
|
|
85
154
|
order.push(id.server);
|
|
86
155
|
}
|
|
87
156
|
list.push({
|
|
88
|
-
tool_name: id.toolName,
|
|
157
|
+
tool_name: t.sourceName ? t.name : id.toolName,
|
|
89
158
|
description: t.description ?? "",
|
|
90
159
|
input_schema: encodeJsonAsValue(normalizeInputSchema(t.inputSchema)),
|
|
91
160
|
});
|
|
@@ -104,31 +173,28 @@ export function buildLiveRequestContext(tools, providerIdentifier = "opencode",
|
|
|
104
173
|
const flat = toolsToDescriptors(tools, providerIdentifier, knownMcpServers);
|
|
105
174
|
const nested = toolsToMcpDescriptors(tools, providerIdentifier, knownMcpServers);
|
|
106
175
|
const cwd = process.cwd();
|
|
107
|
-
|
|
108
|
-
env:
|
|
109
|
-
os_version: process.platform,
|
|
110
|
-
workspace_paths: [cwd],
|
|
111
|
-
shell: process.env.SHELL || "/bin/bash",
|
|
112
|
-
time_zone: "UTC",
|
|
113
|
-
project_folder: cwd,
|
|
114
|
-
process_working_directory: cwd,
|
|
115
|
-
},
|
|
176
|
+
const ctx = {
|
|
177
|
+
env: buildEnv(cwd),
|
|
116
178
|
tools: flat,
|
|
117
179
|
mcp_file_system_options: {
|
|
118
180
|
enabled: true,
|
|
119
|
-
workspace_project_dir: cwd,
|
|
181
|
+
workspace_project_dir: ensureOpencodeProjectDir(cwd),
|
|
120
182
|
mcp_descriptors: nested,
|
|
121
183
|
},
|
|
122
184
|
mcp_meta_tool_options: {
|
|
123
185
|
enabled: true,
|
|
124
186
|
mcp_descriptors: nested,
|
|
125
187
|
},
|
|
188
|
+
web_search_enabled: false,
|
|
189
|
+
web_fetch_enabled: false,
|
|
126
190
|
rules_info_complete: true,
|
|
127
191
|
env_info_complete: true,
|
|
128
192
|
repository_info_complete: true,
|
|
129
193
|
mcp_file_system_info_complete: true,
|
|
130
194
|
git_status_info_complete: true,
|
|
131
195
|
};
|
|
196
|
+
traceRequestContextPaths("buildLiveRequestContext", ctx);
|
|
197
|
+
return ctx;
|
|
132
198
|
}
|
|
133
199
|
// ── Cursor exec-variant → opencode tool name ──
|
|
134
200
|
// Native ExecServerMessage variants only (agent.v1). There is no edit_args or
|
|
@@ -235,16 +301,17 @@ export function parseExecServerMessage(msg) {
|
|
|
235
301
|
if (!resultField)
|
|
236
302
|
return undefined;
|
|
237
303
|
const execId = msg.exec_id ?? "";
|
|
238
|
-
// F11: Cursor native background shell → OpenCode bash.
|
|
239
|
-
//
|
|
240
|
-
//
|
|
304
|
+
// F11: Cursor native background shell → OpenCode bash. Keep the wrapper
|
|
305
|
+
// self-contained so direct provider / hosts without shell.env still detach
|
|
306
|
+
// and return a PID. The classic plugin replaces this with its display-safe
|
|
307
|
+
// env or wrapper-file path before execution.
|
|
241
308
|
if (execVariant === "background_shell_spawn_args") {
|
|
242
309
|
const raw = msg.background_shell_spawn_args ?? {};
|
|
243
310
|
const command = str(raw.command);
|
|
244
311
|
const workingDirectory = str(raw.working_directory) ?? "";
|
|
245
312
|
const args = {};
|
|
246
313
|
if (command)
|
|
247
|
-
args.command = command;
|
|
314
|
+
args.command = buildBackgroundShellCommand(command);
|
|
248
315
|
if (workingDirectory)
|
|
249
316
|
args.workdir = workingDirectory;
|
|
250
317
|
return {
|
|
@@ -1112,6 +1179,7 @@ export function detectExecVariantField(agentServerPayload) {
|
|
|
1112
1179
|
* Encode exec #10 request_context_result from a prebuilt RequestContext payload.
|
|
1113
1180
|
*/
|
|
1114
1181
|
export function buildRequestContextResult(execId, requestContext) {
|
|
1182
|
+
traceRequestContextPaths(`buildRequestContextResult id=${execId}`, requestContext);
|
|
1115
1183
|
return encodeMessage("AgentClientMessage", {
|
|
1116
1184
|
exec_client_message: {
|
|
1117
1185
|
id: execId,
|
|
@@ -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;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { trace } from "./debug.js";
|
|
2
|
+
import { readAllFieldsStrict } from "./protocol/struct.js";
|
|
3
|
+
export class AttemptReplaySafety {
|
|
4
|
+
sessionId;
|
|
5
|
+
barrierReason;
|
|
6
|
+
constructor(sessionId) {
|
|
7
|
+
this.sessionId = sessionId;
|
|
8
|
+
}
|
|
9
|
+
markBarrier(reason) {
|
|
10
|
+
if (this.barrierReason)
|
|
11
|
+
return;
|
|
12
|
+
this.barrierReason = reason;
|
|
13
|
+
trace(`replay barrier: reason=${reason} sessionId=${this.sessionId}`);
|
|
14
|
+
}
|
|
15
|
+
applyTo(failure) {
|
|
16
|
+
failure.replaySafe = this.barrierReason === undefined && failure.replaySafe;
|
|
17
|
+
if (this.barrierReason) {
|
|
18
|
+
trace(`replay suppressed: reason=${this.barrierReason} sessionId=${this.sessionId}`);
|
|
19
|
+
}
|
|
20
|
+
return failure;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const INTERACTION_UPDATE_FIELDS = new Set([1, 2, 3, 4, 7, 13, 14, 16, 17]);
|
|
24
|
+
const INTERACTION_QUERY_FIELDS = new Set([2, 3, 4, 7, 8, 9, 10, 11, 12, 13, 14]);
|
|
25
|
+
const TOP_LEVEL_FIELDS = new Set([1, 2, 3, 4, 5, 7]);
|
|
26
|
+
function nestedFields(topLevel, field) {
|
|
27
|
+
if (topLevel?.fn !== field || topLevel.wt !== 2 || !topLevel.bytes)
|
|
28
|
+
return [];
|
|
29
|
+
return readAllFieldsStrict(topLevel.bytes) ?? [];
|
|
30
|
+
}
|
|
31
|
+
function analyzeExecWire(topLevel) {
|
|
32
|
+
const variants = nestedFields(topLevel, 2)
|
|
33
|
+
.filter((field) => ![1, 15, 19].includes(field.fn));
|
|
34
|
+
const exactVariant = (field) => variants.length === 1 && variants[0].fn === field && variants[0].wt === 2;
|
|
35
|
+
return {
|
|
36
|
+
exactRequestContext: exactVariant(10),
|
|
37
|
+
exactMcpState: exactVariant(36),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function validKvWire(topLevel) {
|
|
41
|
+
const variants = nestedFields(topLevel, 4).filter((field) => field.fn !== 1);
|
|
42
|
+
const variant = variants.length === 1 ? variants[0] : undefined;
|
|
43
|
+
const args = variant?.wt === 2 && variant.bytes
|
|
44
|
+
? (readAllFieldsStrict(variant.bytes) ?? [])
|
|
45
|
+
: [];
|
|
46
|
+
const blobIds = args.filter((field) => field.fn === 1 && field.wt === 2 && (field.bytes?.length ?? 0) > 0);
|
|
47
|
+
return !!variant
|
|
48
|
+
&& [2, 3].includes(variant.fn)
|
|
49
|
+
&& variant.wt === 2
|
|
50
|
+
&& blobIds.length === 1
|
|
51
|
+
&& args.every((field) => field.wt === 2 && (field.fn === 1 || (variant.fn === 3 && field.fn === 2)));
|
|
52
|
+
}
|
|
53
|
+
function validInteractionUpdateWire(topLevel, decoded) {
|
|
54
|
+
if (!decoded)
|
|
55
|
+
return true;
|
|
56
|
+
const fields = nestedFields(topLevel, 1);
|
|
57
|
+
const update = fields.length === 1 ? fields[0] : undefined;
|
|
58
|
+
if (!update || update.wt !== 2 || !INTERACTION_UPDATE_FIELDS.has(update.fn))
|
|
59
|
+
return false;
|
|
60
|
+
if (![1, 4].includes(update.fn))
|
|
61
|
+
return true;
|
|
62
|
+
const delta = update.bytes ? (readAllFieldsStrict(update.bytes) ?? []) : [];
|
|
63
|
+
return delta.length === 1 && delta[0].fn === 1 && delta[0].wt === 2;
|
|
64
|
+
}
|
|
65
|
+
function validInteractionQueryWire(topLevel, decoded) {
|
|
66
|
+
if (!decoded)
|
|
67
|
+
return true;
|
|
68
|
+
const fields = nestedFields(topLevel, 7);
|
|
69
|
+
const ids = fields.filter((field) => field.fn === 1);
|
|
70
|
+
const variants = fields.filter((field) => field.fn !== 1);
|
|
71
|
+
return ids.length <= 1
|
|
72
|
+
&& ids.every((field) => field.wt === 0)
|
|
73
|
+
&& variants.length === 1
|
|
74
|
+
&& variants[0].wt === 2
|
|
75
|
+
&& INTERACTION_QUERY_FIELDS.has(variants[0].fn);
|
|
76
|
+
}
|
|
77
|
+
function decodedMatchesWire(topLevel, decoded) {
|
|
78
|
+
return !((topLevel?.fn === 1 && !decoded.interactionUpdate)
|
|
79
|
+
|| (topLevel?.fn === 2 && !decoded.exec)
|
|
80
|
+
|| (topLevel?.fn === 4 && !decoded.kv)
|
|
81
|
+
|| (topLevel?.fn === 5 && !decoded.execControl)
|
|
82
|
+
|| (topLevel?.fn === 7 && !decoded.interactionQuery));
|
|
83
|
+
}
|
|
84
|
+
function hasSemanticProgress(decoded) {
|
|
85
|
+
const update = decoded.interactionUpdate;
|
|
86
|
+
const text = update?.text_delta?.text;
|
|
87
|
+
const thinking = update?.thinking_delta?.text;
|
|
88
|
+
return (typeof text === "string" && text.length > 0)
|
|
89
|
+
|| (typeof thinking === "string" && thinking.length > 0)
|
|
90
|
+
|| !!update?.turn_ended
|
|
91
|
+
|| !!update?.tool_call_started
|
|
92
|
+
|| !!update?.tool_call_completed
|
|
93
|
+
|| !!decoded.exec
|
|
94
|
+
|| !!decoded.kv
|
|
95
|
+
|| !!decoded.execControl
|
|
96
|
+
|| !!decoded.interactionQuery
|
|
97
|
+
|| !!decoded.checkpointBytes?.length;
|
|
98
|
+
}
|
|
99
|
+
/** Classify one decoded server frame without performing any protocol side effects. */
|
|
100
|
+
export function analyzeReplayFrame(payload, decoded) {
|
|
101
|
+
const topLevelFields = readAllFieldsStrict(payload) ?? [];
|
|
102
|
+
const topLevel = topLevelFields.length === 1 ? topLevelFields[0] : undefined;
|
|
103
|
+
const exec = analyzeExecWire(topLevel);
|
|
104
|
+
const validKv = validKvWire(topLevel);
|
|
105
|
+
const malformed = !topLevel
|
|
106
|
+
|| topLevel.wt !== 2
|
|
107
|
+
|| !TOP_LEVEL_FIELDS.has(topLevel.fn)
|
|
108
|
+
|| !validInteractionUpdateWire(topLevel, decoded.interactionUpdate)
|
|
109
|
+
|| !validInteractionQueryWire(topLevel, decoded.interactionQuery)
|
|
110
|
+
|| !decodedMatchesWire(topLevel, decoded)
|
|
111
|
+
|| !!decoded.execControl;
|
|
112
|
+
let barrier;
|
|
113
|
+
if (decoded.interactionUpdate?.tool_call_started || decoded.interactionUpdate?.tool_call_completed) {
|
|
114
|
+
barrier = "display-tool-lifecycle";
|
|
115
|
+
}
|
|
116
|
+
else if (malformed || (topLevel.fn === 4 && !validKv)) {
|
|
117
|
+
barrier = "unknown-or-malformed-frame";
|
|
118
|
+
}
|
|
119
|
+
else if (topLevel.fn === 2 && !exec.exactRequestContext && !exec.exactMcpState) {
|
|
120
|
+
barrier = "non-control-exec";
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
semanticProgress: hasSemanticProgress(decoded),
|
|
124
|
+
barrier,
|
|
125
|
+
};
|
|
126
|
+
}
|
package/dist/session.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { BidiStream } from "./transport/connect.js";
|
|
2
2
|
import { type CursorProviderError } from "./errors.js";
|
|
3
3
|
import { type SessionActivitySource } from "./activity.js";
|
|
4
|
+
import type { ToolAliasRegistry } from "./protocol/tools.js";
|
|
4
5
|
export type Frame = {
|
|
5
6
|
flags: number;
|
|
6
7
|
payload: Uint8Array;
|
|
@@ -117,6 +118,8 @@ export type CursorSession = {
|
|
|
117
118
|
blobs: Map<string, Uint8Array>;
|
|
118
119
|
/** McpToolDefinition list advertised this turn — echoed into the request_context reply. */
|
|
119
120
|
toolDescriptors: Array<Record<string, unknown>>;
|
|
121
|
+
/** Cursor-facing web alias → exact executable host tool for this Run. */
|
|
122
|
+
toolAliases?: ToolAliasRegistry;
|
|
120
123
|
/** Full RequestContext for exec #10 replies. */
|
|
121
124
|
requestContext: Record<string, unknown>;
|
|
122
125
|
/**
|
package/dist/shell-timeout.d.ts
CHANGED
|
@@ -27,6 +27,13 @@ export type CursorShellOutcome = {
|
|
|
27
27
|
msToWait: number;
|
|
28
28
|
reason: 1;
|
|
29
29
|
};
|
|
30
|
+
/** Track OpenCode's configured shell from the classic config hook. */
|
|
31
|
+
export declare function setCursorShellPath(shell: string | undefined): void;
|
|
32
|
+
/**
|
|
33
|
+
* Mirror the relevant part of OpenCode Shell.acceptable(): fish/nu are denied,
|
|
34
|
+
* then POSIX falls back to bash when installed and `/bin/sh` otherwise.
|
|
35
|
+
*/
|
|
36
|
+
export declare function resolveCursorShellKind(shell?: string | undefined): "bash" | "zsh" | "sh" | "dash" | "other";
|
|
30
37
|
export declare function shellPolicyFromMetadata(metadata: Record<string, unknown> | undefined): CursorShellPolicy | undefined;
|
|
31
38
|
/** Register a Cursor shell request before OpenCode executes its emitted tool call. */
|
|
32
39
|
export declare function registerCursorShellCall(toolCallId: string, metadata: Record<string, unknown> | undefined): void;
|
|
@@ -58,15 +65,13 @@ export declare function buildBackgroundShellCommand(command: string): string;
|
|
|
58
65
|
/**
|
|
59
66
|
* Prepare OpenCode Bash args before execution when Cursor requested wrapping.
|
|
60
67
|
*
|
|
61
|
-
*
|
|
62
|
-
* OpenCode's
|
|
63
|
-
*
|
|
64
|
-
* `args.command` therefore leaks the private wrapper into the TUI/GUI.
|
|
68
|
+
* bash/zsh source the shell.env injector, so the original command remains in
|
|
69
|
+
* OpenCode's permission/UI state. sh/dash ignore those startup variables; for
|
|
70
|
+
* them, use a short `exec wrapper.sh` command that contains no user payload.
|
|
65
71
|
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* Permissions also keep analyzing the real user command.
|
|
72
|
+
* background_shell_spawn may already contain the inline non-plugin fallback.
|
|
73
|
+
* The classic hook replaces it with the original command (bash/zsh) or the
|
|
74
|
+
* shorter wrapper-file command (sh/dash), avoiding duplicate execution.
|
|
70
75
|
*/
|
|
71
76
|
export declare function prepareCursorShellArgs(toolCallId: string, args: Record<string, unknown>): void;
|
|
72
77
|
/** Restore the model-facing command in OpenCode's completed tool title. */
|
|
@@ -74,8 +79,8 @@ export declare function cursorShellOriginalCommand(toolCallId: string): string |
|
|
|
74
79
|
/** Drop injector temp files for a finished/abandoned Cursor shell call. */
|
|
75
80
|
export declare function releaseCursorShellEnv(toolCallId: string): void;
|
|
76
81
|
/**
|
|
77
|
-
* Env vars for OpenCode's
|
|
78
|
-
*
|
|
82
|
+
* Env vars for OpenCode's shell.env hook. bash/zsh execute the injector; the
|
|
83
|
+
* same materialized wrapper backs the direct-command sh/dash fallback.
|
|
79
84
|
*/
|
|
80
85
|
export declare function cursorShellEnvForCall(toolCallId: string | undefined): Record<string, string> | undefined;
|
|
81
86
|
/**
|