cursor-opencode-provider 0.2.1 → 0.2.3
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/DISCLAIMER.md +9 -0
- package/README.md +9 -4
- package/dist/context/build.js +3 -2
- package/dist/context/rules.d.ts +1 -0
- package/dist/context/rules.js +1 -0
- package/dist/language-model.d.ts +52 -6
- package/dist/language-model.js +434 -56
- package/dist/plugin.js +17 -2
- package/dist/protocol/blob-store.d.ts +2 -0
- package/dist/protocol/blob-store.js +4 -0
- package/dist/protocol/conversation-bind.d.ts +18 -0
- package/dist/protocol/conversation-bind.js +66 -0
- package/dist/protocol/interactions.d.ts +23 -0
- package/dist/protocol/interactions.js +136 -0
- package/dist/protocol/messages.js +454 -13
- package/dist/protocol/request.d.ts +30 -0
- package/dist/protocol/request.js +23 -8
- package/dist/protocol/stream.js +9 -3
- package/dist/protocol/tool-call-bridge.d.ts +44 -0
- package/dist/protocol/tool-call-bridge.js +480 -0
- package/dist/protocol/tools.d.ts +41 -12
- package/dist/protocol/tools.js +238 -74
- package/dist/session.d.ts +26 -1
- package/dist/session.js +2 -2
- package/dist/shared.d.ts +2 -0
- package/dist/shared.js +2 -0
- package/package.json +2 -1
package/dist/protocol/request.js
CHANGED
|
@@ -7,19 +7,31 @@ import { toolsToDescriptors } from "./tools.js";
|
|
|
7
7
|
* as a JSON chat message. After the first checkpoint arrives we stop inventing
|
|
8
8
|
* state and echo the server's opaque structure instead (CLI behavior).
|
|
9
9
|
*
|
|
10
|
+
* Compaction resets also use this seed, with `history` carrying OpenCode's
|
|
11
|
+
* compacted prompt turns so Cursor can summarize without the old checkpoint.
|
|
12
|
+
*
|
|
10
13
|
* We deliberately do NOT use `AgentRunRequest.custom_system_prompt` (#8): that
|
|
11
14
|
* field is the internal `--system-prompt` CLI override and the server rejects
|
|
12
15
|
* it for normal accounts.
|
|
13
16
|
*/
|
|
14
|
-
function buildSeedConversationState(
|
|
17
|
+
export function buildSeedConversationState(input) {
|
|
15
18
|
const root = getMessageTypes();
|
|
16
19
|
const type = root.lookupType("ConversationStateStructure");
|
|
17
|
-
const
|
|
18
|
-
if (systemPrompt && systemPrompt.length > 0) {
|
|
19
|
-
|
|
20
|
-
JSON.stringify({ role: "system", content: systemPrompt }),
|
|
21
|
-
];
|
|
20
|
+
const messages = [];
|
|
21
|
+
if (input?.systemPrompt && input.systemPrompt.length > 0) {
|
|
22
|
+
messages.push(JSON.stringify({ role: "system", content: input.systemPrompt }));
|
|
22
23
|
}
|
|
24
|
+
for (const entry of input?.history ?? []) {
|
|
25
|
+
if (!entry.content)
|
|
26
|
+
continue;
|
|
27
|
+
// Avoid duplicating the system prompt when history also carries one.
|
|
28
|
+
if (entry.role === "system" && input?.systemPrompt)
|
|
29
|
+
continue;
|
|
30
|
+
messages.push(JSON.stringify({ role: entry.role, content: entry.content }));
|
|
31
|
+
}
|
|
32
|
+
const obj = {};
|
|
33
|
+
if (messages.length > 0)
|
|
34
|
+
obj.root_prompt_messages_json = messages;
|
|
23
35
|
return type.encode(type.fromObject(obj)).finish();
|
|
24
36
|
}
|
|
25
37
|
function buildAvailableModels(models) {
|
|
@@ -40,7 +52,7 @@ export function buildRunRequest(input) {
|
|
|
40
52
|
// (#2). AgentRunRequest.mcp_tools (#4) is prewarm-only / empty on real turns —
|
|
41
53
|
// putting tools only there is why the model fell back to native Grep/Read.
|
|
42
54
|
const tools = input.tools ?? [];
|
|
43
|
-
const mcpTools = tools.length > 0 ? toolsToDescriptors(tools) : [];
|
|
55
|
+
const mcpTools = input.toolDescriptors ?? (tools.length > 0 ? toolsToDescriptors(tools) : []);
|
|
44
56
|
const requestContext = input.requestContext;
|
|
45
57
|
const userMessageAction = {
|
|
46
58
|
user_message: {
|
|
@@ -53,7 +65,10 @@ export function buildRunRequest(input) {
|
|
|
53
65
|
}
|
|
54
66
|
const conversationState = input.conversationState && input.conversationState.length > 0
|
|
55
67
|
? input.conversationState
|
|
56
|
-
: buildSeedConversationState(
|
|
68
|
+
: buildSeedConversationState({
|
|
69
|
+
systemPrompt: input.systemPrompt,
|
|
70
|
+
history: input.history,
|
|
71
|
+
});
|
|
57
72
|
const runRequest = {
|
|
58
73
|
conversation_id: input.conversationId,
|
|
59
74
|
action: {
|
package/dist/protocol/stream.js
CHANGED
|
@@ -34,19 +34,25 @@ export function parseInteractionUpdate(payload) {
|
|
|
34
34
|
if (iu.tool_call_started) {
|
|
35
35
|
const tc = iu.tool_call_started;
|
|
36
36
|
const toolCall = tc.tool_call;
|
|
37
|
+
const variant = toolCall && typeof toolCall === "object"
|
|
38
|
+
? Object.keys(toolCall).find((k) => k.endsWith("_tool_call"))
|
|
39
|
+
: undefined;
|
|
40
|
+
const variantPayload = variant && toolCall ? toolCall[variant] : undefined;
|
|
41
|
+
const args = variantPayload?.args ?? variantPayload;
|
|
37
42
|
return {
|
|
38
43
|
type: "tool-call-started",
|
|
39
44
|
callId: tc.call_id ?? "",
|
|
40
|
-
toolName:
|
|
41
|
-
args:
|
|
45
|
+
toolName: variant ?? "",
|
|
46
|
+
args: JSON.stringify(args ?? {}),
|
|
42
47
|
};
|
|
43
48
|
}
|
|
44
49
|
if (iu.tool_call_completed) {
|
|
45
50
|
const tc = iu.tool_call_completed;
|
|
51
|
+
const toolCall = tc.tool_call;
|
|
46
52
|
return {
|
|
47
53
|
type: "tool-call-completed",
|
|
48
54
|
callId: tc.call_id ?? "",
|
|
49
|
-
result:
|
|
55
|
+
result: JSON.stringify(toolCall ?? {}),
|
|
50
56
|
};
|
|
51
57
|
}
|
|
52
58
|
if (iu.partial_tool_call) {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor's interaction_update.tool_call_* carries a typed ToolCall oneof
|
|
3
|
+
* (shell_tool_call, update_todos_tool_call, mcp_tool_call, …). Some of those
|
|
4
|
+
* never become ExecServerMessage requests — Cursor completes them display-only
|
|
5
|
+
* — so OpenCode would never see a tool-call unless we bridge them here.
|
|
6
|
+
*/
|
|
7
|
+
export type DisplayToolCall = {
|
|
8
|
+
callId: string;
|
|
9
|
+
variant: string;
|
|
10
|
+
/** Preferred OpenCode tool id before advertisement checks. */
|
|
11
|
+
preferredToolName: string;
|
|
12
|
+
args: Record<string, unknown>;
|
|
13
|
+
/** False when the Cursor call cannot be represented safely in OpenCode. */
|
|
14
|
+
bridgeable?: boolean;
|
|
15
|
+
};
|
|
16
|
+
export type BridgedOpenCodeToolCall = {
|
|
17
|
+
toolName: string;
|
|
18
|
+
args: Record<string, unknown>;
|
|
19
|
+
callId: string;
|
|
20
|
+
variant: string;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Decode a Cursor ToolCall oneof into a display tool call we can bridge.
|
|
24
|
+
*/
|
|
25
|
+
export declare function parseDisplayToolCall(callId: string, toolCall: Record<string, unknown> | undefined): DisplayToolCall | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Pick an advertised OpenCode tool for a display call, remapping args as needed.
|
|
28
|
+
* Returns undefined when nothing compatible is advertised this turn.
|
|
29
|
+
*/
|
|
30
|
+
export declare function resolveBridgedOpenCodeToolCall(display: DisplayToolCall, advertised: Iterable<string>): BridgedOpenCodeToolCall | undefined;
|
|
31
|
+
/** Advertised OpenCode tool ids from Cursor McpToolDefinition descriptors. */
|
|
32
|
+
export declare function advertisedToolNamesFromDescriptors(descriptors: Array<Record<string, unknown>>): string[];
|
|
33
|
+
/** Walk a protobuf message and return top-level field numbers present. */
|
|
34
|
+
export declare function listProtobufFieldNumbers(buf: Uint8Array): number[];
|
|
35
|
+
/**
|
|
36
|
+
* Extract nested bytes for path of field numbers (each must be wire type 2).
|
|
37
|
+
* Returns undefined if any segment is missing.
|
|
38
|
+
*/
|
|
39
|
+
export declare function extractProtobufSubmessage(buf: Uint8Array, path: number[]): Uint8Array | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* Pull Cursor's tool_call_id out of a raw ExecServerMessage args variant so we
|
|
42
|
+
* can correlate display tool_call_started with the authoritative exec path.
|
|
43
|
+
*/
|
|
44
|
+
export declare function extractExecDisplayCallId(execMsg: Record<string, unknown>): string | undefined;
|
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
import { mapCursorArgsToOpencode, mcpRealToolName } from "./tools.js";
|
|
2
|
+
import { decodeStructEntriesToJson } from "./struct.js";
|
|
3
|
+
const TODO_STATUS = {
|
|
4
|
+
0: "pending",
|
|
5
|
+
1: "pending",
|
|
6
|
+
2: "in_progress",
|
|
7
|
+
3: "completed",
|
|
8
|
+
4: "cancelled",
|
|
9
|
+
};
|
|
10
|
+
/** Cursor ToolCall oneof field → default OpenCode tool id. */
|
|
11
|
+
const VARIANT_TO_OPENCODE = {
|
|
12
|
+
shell_tool_call: "bash",
|
|
13
|
+
delete_tool_call: "bash",
|
|
14
|
+
glob_tool_call: "glob",
|
|
15
|
+
grep_tool_call: "grep",
|
|
16
|
+
read_tool_call: "read",
|
|
17
|
+
update_todos_tool_call: "todowrite",
|
|
18
|
+
read_todos_tool_call: "todoread",
|
|
19
|
+
edit_tool_call: "write",
|
|
20
|
+
ls_tool_call: "read",
|
|
21
|
+
mcp_tool_call: "mcp",
|
|
22
|
+
create_plan_tool_call: "todowrite",
|
|
23
|
+
web_search_tool_call: "websearch",
|
|
24
|
+
task_tool_call: "task",
|
|
25
|
+
ask_question_tool_call: "question",
|
|
26
|
+
fetch_tool_call: "webfetch",
|
|
27
|
+
web_fetch_tool_call: "webfetch",
|
|
28
|
+
switch_mode_tool_call: "plan_enter",
|
|
29
|
+
generate_image_tool_call: "generateimage",
|
|
30
|
+
await_tool_call: "await",
|
|
31
|
+
get_mcp_tools_tool_call: "get_mcp_tools",
|
|
32
|
+
pi_read_tool_call: "read",
|
|
33
|
+
pi_bash_tool_call: "bash",
|
|
34
|
+
pi_edit_tool_call: "edit",
|
|
35
|
+
pi_write_tool_call: "write",
|
|
36
|
+
pi_grep_tool_call: "grep",
|
|
37
|
+
pi_find_tool_call: "glob",
|
|
38
|
+
pi_ls_tool_call: "read",
|
|
39
|
+
};
|
|
40
|
+
const TOOL_CALL_VARIANTS = Object.keys(VARIANT_TO_OPENCODE);
|
|
41
|
+
// ToolCallCompleted is a notification, not an execution request. Only mirror
|
|
42
|
+
// client-visible state whose authoritative final value is already present in
|
|
43
|
+
// the completed payload. Data-returning, interactive, and side-effecting calls
|
|
44
|
+
// must use an exec/interaction request channel where Cursor can receive their
|
|
45
|
+
// actual result.
|
|
46
|
+
const DISPLAY_STATE_MIRROR_VARIANTS = new Set([
|
|
47
|
+
"update_todos_tool_call",
|
|
48
|
+
"create_plan_tool_call",
|
|
49
|
+
]);
|
|
50
|
+
function asRecord(v) {
|
|
51
|
+
return v && typeof v === "object" && !Array.isArray(v) ? v : undefined;
|
|
52
|
+
}
|
|
53
|
+
function findToolVariant(toolCall) {
|
|
54
|
+
for (const key of TOOL_CALL_VARIANTS) {
|
|
55
|
+
if (toolCall[key] != null)
|
|
56
|
+
return key;
|
|
57
|
+
}
|
|
58
|
+
for (const [key, value] of Object.entries(toolCall)) {
|
|
59
|
+
if (!key.endsWith("_tool_call"))
|
|
60
|
+
continue;
|
|
61
|
+
if (value && typeof value === "object")
|
|
62
|
+
return key;
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
function mapTodoStatus(status) {
|
|
67
|
+
if (typeof status === "string" && status.length > 0) {
|
|
68
|
+
const s = status.toLowerCase().replace(/^todo_status_/, "");
|
|
69
|
+
if (s === "unspecified")
|
|
70
|
+
return "pending";
|
|
71
|
+
return s;
|
|
72
|
+
}
|
|
73
|
+
if (typeof status === "number" && TODO_STATUS[status])
|
|
74
|
+
return TODO_STATUS[status];
|
|
75
|
+
return "pending";
|
|
76
|
+
}
|
|
77
|
+
function mapTodos(raw) {
|
|
78
|
+
if (!Array.isArray(raw))
|
|
79
|
+
return [];
|
|
80
|
+
return raw.map((item, index) => {
|
|
81
|
+
const t = asRecord(item) ?? {};
|
|
82
|
+
const content = typeof t.content === "string" ? t.content : "";
|
|
83
|
+
const id = typeof t.id === "string" && t.id.length > 0
|
|
84
|
+
? t.id
|
|
85
|
+
: `todo_${index + 1}`;
|
|
86
|
+
return {
|
|
87
|
+
id,
|
|
88
|
+
content,
|
|
89
|
+
status: mapTodoStatus(t.status),
|
|
90
|
+
priority: typeof t.priority === "string" && t.priority ? t.priority : "medium",
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
function unwrapArgs(variantPayload) {
|
|
95
|
+
const nested = asRecord(variantPayload.args);
|
|
96
|
+
return nested ?? variantPayload;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Decode a Cursor ToolCall oneof into a display tool call we can bridge.
|
|
100
|
+
*/
|
|
101
|
+
export function parseDisplayToolCall(callId, toolCall) {
|
|
102
|
+
if (!toolCall || !callId)
|
|
103
|
+
return undefined;
|
|
104
|
+
const variant = findToolVariant(toolCall);
|
|
105
|
+
if (!variant)
|
|
106
|
+
return undefined;
|
|
107
|
+
const payload = asRecord(toolCall[variant]);
|
|
108
|
+
if (!payload)
|
|
109
|
+
return undefined;
|
|
110
|
+
const args = unwrapArgs(payload);
|
|
111
|
+
if (variant === "mcp_tool_call") {
|
|
112
|
+
const name = mcpRealToolName(args);
|
|
113
|
+
let mcpArgs = asRecord(args.args) ?? {};
|
|
114
|
+
if (Array.isArray(args.args)) {
|
|
115
|
+
mcpArgs = decodeStructEntriesToJson(args.args);
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
callId,
|
|
119
|
+
variant,
|
|
120
|
+
preferredToolName: name,
|
|
121
|
+
args: mcpArgs,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (variant === "update_todos_tool_call" || variant === "create_plan_tool_call") {
|
|
125
|
+
const result = asRecord(payload.result);
|
|
126
|
+
const success = asRecord(result?.success);
|
|
127
|
+
const completedTodos = Array.isArray(success?.todos) ? success.todos : undefined;
|
|
128
|
+
const isMerge = variant === "update_todos_tool_call" && args.merge === true;
|
|
129
|
+
// OpenCode todowrite replaces the whole list. For Cursor merge updates,
|
|
130
|
+
// only bridge when ToolCallCompleted includes the final merged list.
|
|
131
|
+
const sourceTodos = completedTodos ?? (!isMerge ? args.todos : undefined);
|
|
132
|
+
const todos = mapTodos(sourceTodos);
|
|
133
|
+
if (variant === "create_plan_tool_call") {
|
|
134
|
+
const overview = typeof args.overview === "string" ? args.overview.trim() : "";
|
|
135
|
+
const plan = typeof args.plan === "string" ? args.plan.trim() : "";
|
|
136
|
+
const name = typeof args.name === "string" ? args.name.trim() : "";
|
|
137
|
+
if (overview || plan || name) {
|
|
138
|
+
todos.unshift({
|
|
139
|
+
id: "plan",
|
|
140
|
+
content: [name && `Plan: ${name}`, overview, plan].filter(Boolean).join("\n").slice(0, 2000),
|
|
141
|
+
status: "pending",
|
|
142
|
+
priority: "high",
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
callId,
|
|
148
|
+
variant,
|
|
149
|
+
preferredToolName: "todowrite",
|
|
150
|
+
args: { todos },
|
|
151
|
+
bridgeable: variant === "create_plan_tool_call" ? todos.length > 0 : Array.isArray(sourceTodos),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
if (variant === "edit_tool_call") {
|
|
155
|
+
const path = typeof args.path === "string" ? args.path : "";
|
|
156
|
+
const content = typeof args.stream_content === "string" ? args.stream_content : undefined;
|
|
157
|
+
return {
|
|
158
|
+
callId,
|
|
159
|
+
variant,
|
|
160
|
+
preferredToolName: "write",
|
|
161
|
+
args: { path, content },
|
|
162
|
+
bridgeable: path.length > 0 && content !== undefined,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
if (variant === "pi_edit_tool_call") {
|
|
166
|
+
const path = typeof args.path === "string" ? args.path : "";
|
|
167
|
+
const edits = Array.isArray(args.edits) ? args.edits : [];
|
|
168
|
+
const replacement = edits.length === 1 ? asRecord(edits[0]) : undefined;
|
|
169
|
+
const oldText = replacement?.old_text;
|
|
170
|
+
const newText = replacement?.new_text;
|
|
171
|
+
return {
|
|
172
|
+
callId,
|
|
173
|
+
variant,
|
|
174
|
+
preferredToolName: "edit",
|
|
175
|
+
args: { path, old_string: oldText, new_string: newText },
|
|
176
|
+
bridgeable: path.length > 0 &&
|
|
177
|
+
typeof oldText === "string" && oldText.length > 0 &&
|
|
178
|
+
typeof newText === "string",
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
if (variant === "task_tool_call") {
|
|
182
|
+
const subagentType = openCodeSubagentType(args.subagent_type);
|
|
183
|
+
const description = typeof args.description === "string" ? args.description : "";
|
|
184
|
+
const prompt = typeof args.prompt === "string" ? args.prompt : "";
|
|
185
|
+
const taskArgs = {
|
|
186
|
+
description,
|
|
187
|
+
prompt,
|
|
188
|
+
subagent_type: subagentType,
|
|
189
|
+
};
|
|
190
|
+
if (typeof args.resume === "string" && args.resume)
|
|
191
|
+
taskArgs.task_id = args.resume;
|
|
192
|
+
return {
|
|
193
|
+
callId,
|
|
194
|
+
variant,
|
|
195
|
+
preferredToolName: "task",
|
|
196
|
+
args: taskArgs,
|
|
197
|
+
bridgeable: description.length > 0 && prompt.length > 0 && !!subagentType,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
if (variant === "ask_question_tool_call") {
|
|
201
|
+
const title = typeof args.title === "string" ? args.title : "";
|
|
202
|
+
const questions = Array.isArray(args.questions)
|
|
203
|
+
? args.questions.map((q) => {
|
|
204
|
+
const qq = asRecord(q) ?? {};
|
|
205
|
+
const prompt = typeof qq.prompt === "string" ? qq.prompt : "";
|
|
206
|
+
const options = Array.isArray(qq.options)
|
|
207
|
+
? qq.options.map((o) => {
|
|
208
|
+
const oo = asRecord(o) ?? {};
|
|
209
|
+
return {
|
|
210
|
+
label: typeof oo.label === "string" ? oo.label : String(oo.id ?? ""),
|
|
211
|
+
description: typeof oo.description === "string" ? oo.description : "",
|
|
212
|
+
};
|
|
213
|
+
})
|
|
214
|
+
: [];
|
|
215
|
+
return {
|
|
216
|
+
question: prompt,
|
|
217
|
+
header: (typeof qq.id === "string" && qq.id) || title || "Question",
|
|
218
|
+
options,
|
|
219
|
+
multiple: qq.allow_multiple === true,
|
|
220
|
+
};
|
|
221
|
+
})
|
|
222
|
+
: [];
|
|
223
|
+
return {
|
|
224
|
+
callId,
|
|
225
|
+
variant,
|
|
226
|
+
preferredToolName: "question",
|
|
227
|
+
args: { questions },
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
if (variant === "switch_mode_tool_call") {
|
|
231
|
+
const target = typeof args.target_mode_id === "string" ? args.target_mode_id.toLowerCase() : "";
|
|
232
|
+
const preferred = target.includes("plan") || target === "plan" ? "plan_enter" : "plan_exit";
|
|
233
|
+
return {
|
|
234
|
+
callId,
|
|
235
|
+
variant,
|
|
236
|
+
preferredToolName: preferred,
|
|
237
|
+
// OpenCode plan_enter / plan_exit both advertise an empty input schema.
|
|
238
|
+
args: {},
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
if (variant === "delete_tool_call") {
|
|
242
|
+
const path = typeof args.path === "string" ? args.path : "";
|
|
243
|
+
return {
|
|
244
|
+
callId,
|
|
245
|
+
variant,
|
|
246
|
+
preferredToolName: "bash",
|
|
247
|
+
args: path ? { command: `rm -f -- ${shellQuote(path)}` } : { command: "true" },
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
const preferred = VARIANT_TO_OPENCODE[variant] ?? variant.replace(/_tool_call$/, "");
|
|
251
|
+
return {
|
|
252
|
+
callId,
|
|
253
|
+
variant,
|
|
254
|
+
preferredToolName: preferred,
|
|
255
|
+
args,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
function shellQuote(s) {
|
|
259
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Pick an advertised OpenCode tool for a display call, remapping args as needed.
|
|
263
|
+
* Returns undefined when nothing compatible is advertised this turn.
|
|
264
|
+
*/
|
|
265
|
+
export function resolveBridgedOpenCodeToolCall(display, advertised) {
|
|
266
|
+
if (!DISPLAY_STATE_MIRROR_VARIANTS.has(display.variant))
|
|
267
|
+
return undefined;
|
|
268
|
+
if (display.bridgeable === false)
|
|
269
|
+
return undefined;
|
|
270
|
+
const names = new Set([...advertised].filter(Boolean));
|
|
271
|
+
if (names.size === 0)
|
|
272
|
+
return undefined;
|
|
273
|
+
const candidates = candidateToolNames(display, names);
|
|
274
|
+
for (const toolName of candidates) {
|
|
275
|
+
if (!names.has(toolName))
|
|
276
|
+
continue;
|
|
277
|
+
const mapped = mapCursorArgsToOpencode(toolName, display.args);
|
|
278
|
+
if (toolName === "todowrite") {
|
|
279
|
+
mapped.args = {
|
|
280
|
+
todos: mapTodos(display.args.todos ?? mapped.args.todos),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
return {
|
|
284
|
+
toolName: mapped.toolName,
|
|
285
|
+
args: mapped.args,
|
|
286
|
+
callId: display.callId,
|
|
287
|
+
variant: display.variant,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
return undefined;
|
|
291
|
+
}
|
|
292
|
+
function candidateToolNames(display, advertised) {
|
|
293
|
+
const out = [];
|
|
294
|
+
const add = (n) => {
|
|
295
|
+
if (n && !out.includes(n))
|
|
296
|
+
out.push(n);
|
|
297
|
+
};
|
|
298
|
+
add(display.preferredToolName);
|
|
299
|
+
if (display.variant === "create_plan_tool_call")
|
|
300
|
+
add("todowrite");
|
|
301
|
+
if (display.variant === "update_todos_tool_call")
|
|
302
|
+
add("todowrite");
|
|
303
|
+
if (display.variant === "read_todos_tool_call")
|
|
304
|
+
add("todoread");
|
|
305
|
+
if (display.variant === "ask_question_tool_call")
|
|
306
|
+
add("question");
|
|
307
|
+
if (display.variant === "web_search_tool_call")
|
|
308
|
+
add("websearch");
|
|
309
|
+
if (display.variant === "fetch_tool_call" || display.variant === "web_fetch_tool_call") {
|
|
310
|
+
add("webfetch");
|
|
311
|
+
}
|
|
312
|
+
for (const name of advertised) {
|
|
313
|
+
if (name.toLowerCase() === display.preferredToolName.toLowerCase())
|
|
314
|
+
add(name);
|
|
315
|
+
}
|
|
316
|
+
return out;
|
|
317
|
+
}
|
|
318
|
+
/** Map only Cursor subagent variants with a safe OpenCode identity. */
|
|
319
|
+
function openCodeSubagentType(raw) {
|
|
320
|
+
const subtype = asRecord(raw);
|
|
321
|
+
if (!subtype)
|
|
322
|
+
return undefined;
|
|
323
|
+
const custom = asRecord(subtype.custom);
|
|
324
|
+
if (typeof custom?.name === "string" && custom.name.length > 0)
|
|
325
|
+
return custom.name;
|
|
326
|
+
if (subtype.explore != null)
|
|
327
|
+
return "explore";
|
|
328
|
+
return undefined;
|
|
329
|
+
}
|
|
330
|
+
/** Advertised OpenCode tool ids from Cursor McpToolDefinition descriptors. */
|
|
331
|
+
export function advertisedToolNamesFromDescriptors(descriptors) {
|
|
332
|
+
const out = [];
|
|
333
|
+
for (const d of descriptors) {
|
|
334
|
+
const toolName = typeof d.tool_name === "string" ? d.tool_name : undefined;
|
|
335
|
+
const provider = typeof d.provider_identifier === "string" ? d.provider_identifier : "opencode";
|
|
336
|
+
if (!toolName)
|
|
337
|
+
continue;
|
|
338
|
+
if (provider && provider !== "opencode")
|
|
339
|
+
out.push(`${provider}_${toolName}`);
|
|
340
|
+
else
|
|
341
|
+
out.push(toolName);
|
|
342
|
+
}
|
|
343
|
+
return out;
|
|
344
|
+
}
|
|
345
|
+
/** Walk a protobuf message and return top-level field numbers present. */
|
|
346
|
+
export function listProtobufFieldNumbers(buf) {
|
|
347
|
+
const fields = [];
|
|
348
|
+
let i = 0;
|
|
349
|
+
while (i < buf.length) {
|
|
350
|
+
let key = 0;
|
|
351
|
+
let shift = 0;
|
|
352
|
+
while (i < buf.length) {
|
|
353
|
+
const byte = buf[i++];
|
|
354
|
+
key |= (byte & 0x7f) << shift;
|
|
355
|
+
if ((byte & 0x80) === 0)
|
|
356
|
+
break;
|
|
357
|
+
shift += 7;
|
|
358
|
+
if (shift > 35)
|
|
359
|
+
return fields;
|
|
360
|
+
}
|
|
361
|
+
const field = key >>> 3;
|
|
362
|
+
const wire = key & 7;
|
|
363
|
+
fields.push(field);
|
|
364
|
+
if (wire === 0) {
|
|
365
|
+
// varint
|
|
366
|
+
while (i < buf.length && (buf[i++] & 0x80) !== 0) { /* consume */ }
|
|
367
|
+
}
|
|
368
|
+
else if (wire === 1) {
|
|
369
|
+
i += 8;
|
|
370
|
+
}
|
|
371
|
+
else if (wire === 2) {
|
|
372
|
+
let len = 0;
|
|
373
|
+
shift = 0;
|
|
374
|
+
while (i < buf.length) {
|
|
375
|
+
const byte = buf[i++];
|
|
376
|
+
len |= (byte & 0x7f) << shift;
|
|
377
|
+
if ((byte & 0x80) === 0)
|
|
378
|
+
break;
|
|
379
|
+
shift += 7;
|
|
380
|
+
if (shift > 35)
|
|
381
|
+
return fields;
|
|
382
|
+
}
|
|
383
|
+
i += len;
|
|
384
|
+
}
|
|
385
|
+
else if (wire === 5) {
|
|
386
|
+
i += 4;
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
if (i > buf.length)
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
return fields;
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Extract nested bytes for path of field numbers (each must be wire type 2).
|
|
398
|
+
* Returns undefined if any segment is missing.
|
|
399
|
+
*/
|
|
400
|
+
export function extractProtobufSubmessage(buf, path) {
|
|
401
|
+
let cur = buf;
|
|
402
|
+
for (const want of path) {
|
|
403
|
+
if (!cur)
|
|
404
|
+
return undefined;
|
|
405
|
+
let i = 0;
|
|
406
|
+
let found;
|
|
407
|
+
while (i < cur.length) {
|
|
408
|
+
let key = 0;
|
|
409
|
+
let shift = 0;
|
|
410
|
+
while (i < cur.length) {
|
|
411
|
+
const byte = cur[i++];
|
|
412
|
+
key |= (byte & 0x7f) << shift;
|
|
413
|
+
if ((byte & 0x80) === 0)
|
|
414
|
+
break;
|
|
415
|
+
shift += 7;
|
|
416
|
+
if (shift > 35)
|
|
417
|
+
return undefined;
|
|
418
|
+
}
|
|
419
|
+
const field = key >>> 3;
|
|
420
|
+
const wire = key & 7;
|
|
421
|
+
if (wire === 0) {
|
|
422
|
+
while (i < cur.length && (cur[i++] & 0x80) !== 0) { /* consume */ }
|
|
423
|
+
}
|
|
424
|
+
else if (wire === 1) {
|
|
425
|
+
i += 8;
|
|
426
|
+
}
|
|
427
|
+
else if (wire === 2) {
|
|
428
|
+
let len = 0;
|
|
429
|
+
shift = 0;
|
|
430
|
+
while (i < cur.length) {
|
|
431
|
+
const byte = cur[i++];
|
|
432
|
+
len |= (byte & 0x7f) << shift;
|
|
433
|
+
if ((byte & 0x80) === 0)
|
|
434
|
+
break;
|
|
435
|
+
shift += 7;
|
|
436
|
+
if (shift > 35)
|
|
437
|
+
return undefined;
|
|
438
|
+
}
|
|
439
|
+
const slice = cur.subarray(i, i + len);
|
|
440
|
+
i += len;
|
|
441
|
+
if (field === want)
|
|
442
|
+
found = slice;
|
|
443
|
+
}
|
|
444
|
+
else if (wire === 5) {
|
|
445
|
+
i += 4;
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
return undefined;
|
|
449
|
+
}
|
|
450
|
+
if (i > cur.length)
|
|
451
|
+
return undefined;
|
|
452
|
+
}
|
|
453
|
+
cur = found;
|
|
454
|
+
}
|
|
455
|
+
return cur;
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Pull Cursor's tool_call_id out of a raw ExecServerMessage args variant so we
|
|
459
|
+
* can correlate display tool_call_started with the authoritative exec path.
|
|
460
|
+
*/
|
|
461
|
+
export function extractExecDisplayCallId(execMsg) {
|
|
462
|
+
for (const key of [
|
|
463
|
+
"read_args",
|
|
464
|
+
"write_args",
|
|
465
|
+
"pi_write_args",
|
|
466
|
+
"grep_args",
|
|
467
|
+
"ls_args",
|
|
468
|
+
"delete_args",
|
|
469
|
+
"shell_stream_args",
|
|
470
|
+
"mcp_args",
|
|
471
|
+
]) {
|
|
472
|
+
const args = asRecord(execMsg[key]);
|
|
473
|
+
if (!args)
|
|
474
|
+
continue;
|
|
475
|
+
if (typeof args.tool_call_id === "string" && args.tool_call_id.length > 0) {
|
|
476
|
+
return args.tool_call_id;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return undefined;
|
|
480
|
+
}
|