cursor-opencode-provider 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +200 -0
  3. package/dist/auth.d.ts +48 -0
  4. package/dist/auth.js +200 -0
  5. package/dist/context/agents.d.ts +8 -0
  6. package/dist/context/agents.js +76 -0
  7. package/dist/context/build.d.ts +12 -0
  8. package/dist/context/build.js +83 -0
  9. package/dist/context/env.d.ts +1 -0
  10. package/dist/context/env.js +29 -0
  11. package/dist/context/git.d.ts +20 -0
  12. package/dist/context/git.js +59 -0
  13. package/dist/context/index.d.ts +2 -0
  14. package/dist/context/index.js +2 -0
  15. package/dist/context/layout.d.ts +17 -0
  16. package/dist/context/layout.js +58 -0
  17. package/dist/context/paths.d.ts +3 -0
  18. package/dist/context/paths.js +11 -0
  19. package/dist/context/plugins.d.ts +8 -0
  20. package/dist/context/plugins.js +50 -0
  21. package/dist/context/rules.d.ts +19 -0
  22. package/dist/context/rules.js +198 -0
  23. package/dist/context/skills.d.ts +11 -0
  24. package/dist/context/skills.js +104 -0
  25. package/dist/index.d.ts +15 -0
  26. package/dist/index.js +18 -0
  27. package/dist/language-model.d.ts +45 -0
  28. package/dist/language-model.js +834 -0
  29. package/dist/models.d.ts +49 -0
  30. package/dist/models.js +136 -0
  31. package/dist/plugin-v2.d.ts +2 -0
  32. package/dist/plugin-v2.js +48 -0
  33. package/dist/plugin.d.ts +2 -0
  34. package/dist/plugin.js +201 -0
  35. package/dist/protocol/blob-store.d.ts +15 -0
  36. package/dist/protocol/blob-store.js +52 -0
  37. package/dist/protocol/checkpoint.d.ts +17 -0
  38. package/dist/protocol/checkpoint.js +29 -0
  39. package/dist/protocol/checksum.d.ts +2 -0
  40. package/dist/protocol/checksum.js +23 -0
  41. package/dist/protocol/client-version.d.ts +5 -0
  42. package/dist/protocol/client-version.js +150 -0
  43. package/dist/protocol/device-id.d.ts +8 -0
  44. package/dist/protocol/device-id.js +121 -0
  45. package/dist/protocol/framing.d.ts +10 -0
  46. package/dist/protocol/framing.js +90 -0
  47. package/dist/protocol/kv.d.ts +24 -0
  48. package/dist/protocol/kv.js +81 -0
  49. package/dist/protocol/messages.d.ts +11 -0
  50. package/dist/protocol/messages.js +676 -0
  51. package/dist/protocol/request.d.ts +36 -0
  52. package/dist/protocol/request.js +90 -0
  53. package/dist/protocol/stream.d.ts +38 -0
  54. package/dist/protocol/stream.js +64 -0
  55. package/dist/protocol/struct.d.ts +19 -0
  56. package/dist/protocol/struct.js +186 -0
  57. package/dist/protocol/thinking.d.ts +15 -0
  58. package/dist/protocol/thinking.js +17 -0
  59. package/dist/protocol/tools.d.ts +94 -0
  60. package/dist/protocol/tools.js +631 -0
  61. package/dist/session.d.ts +81 -0
  62. package/dist/session.js +96 -0
  63. package/dist/shared.d.ts +15 -0
  64. package/dist/shared.js +13 -0
  65. package/dist/transport/connect.d.ts +23 -0
  66. package/dist/transport/connect.js +275 -0
  67. package/package.json +65 -0
@@ -0,0 +1,631 @@
1
+ import { encodeMessage } from "./messages.js";
2
+ import { encodeJsonAsValue, decodeStructEntriesToJson, readAllFields } from "./struct.js";
3
+ // Exec variant field number whose reply is the server-initiated request_context
4
+ // probe (ExecServerMessage #10 → ExecClientMessage #10). request/result share a
5
+ // field number for every exec variant, so this is also the result field.
6
+ export const REQUEST_CONTEXT_RESULT_FIELD = 10;
7
+ /**
8
+ * Convert opencode's per-turn tool list into Cursor `McpToolDefinition`
9
+ * entries for `request_context.tools` (#7) and `AgentRunRequest.mcp_tools`.
10
+ *
11
+ * opencode tools are already namespaced (e.g. `read`, `grep`, or
12
+ * `<server>_<tool>` for MCP). We advertise them all under a synthetic
13
+ * `opencode` provider so Cursor routes their calls back to us as
14
+ * `mcp_args` on the exec channel. The composite `name` is what the model
15
+ * calls and what comes back as `McpArgs.name`.
16
+ */
17
+ export function toolsToDescriptors(tools, providerIdentifier = "opencode") {
18
+ return tools.map((t) => ({
19
+ name: `${providerIdentifier}-${t.name}`,
20
+ description: t.description ?? "",
21
+ input_schema: encodeJsonAsValue(normalizeInputSchema(t.inputSchema)),
22
+ provider_identifier: providerIdentifier,
23
+ tool_name: t.name,
24
+ }));
25
+ }
26
+ function normalizeInputSchema(schema) {
27
+ if (schema && typeof schema === "object" && !Array.isArray(schema)) {
28
+ return schema;
29
+ }
30
+ return { type: "object", properties: {} };
31
+ }
32
+ /**
33
+ * Build the nested McpFileSystemOptions / McpMetaToolOptions shape used by
34
+ * requestContext.#23 / #34. Groups every tool under one synthetic server so
35
+ * Cursor's IDE-style decoder also sees the list.
36
+ */
37
+ export function toolsToMcpDescriptors(tools, providerIdentifier = "opencode") {
38
+ if (tools.length === 0)
39
+ return [];
40
+ return [
41
+ {
42
+ server_name: providerIdentifier,
43
+ server_identifier: providerIdentifier,
44
+ tools: tools.map((t) => ({
45
+ tool_name: t.name,
46
+ description: t.description ?? "",
47
+ input_schema: encodeJsonAsValue(normalizeInputSchema(t.inputSchema)),
48
+ })),
49
+ },
50
+ ];
51
+ }
52
+ /**
53
+ * @deprecated Prefer `buildRequestContext` from `../context/build.js`.
54
+ * Kept as a sync tools-only fallback for unit tests that don't need collectors.
55
+ */
56
+ export function buildLiveRequestContext(tools, providerIdentifier = "opencode") {
57
+ const flat = toolsToDescriptors(tools, providerIdentifier);
58
+ const nested = toolsToMcpDescriptors(tools, providerIdentifier);
59
+ const cwd = process.cwd();
60
+ return {
61
+ env: {
62
+ os_version: process.platform,
63
+ workspace_paths: [cwd],
64
+ shell: process.env.SHELL || "/bin/bash",
65
+ time_zone: "UTC",
66
+ project_folder: cwd,
67
+ process_working_directory: cwd,
68
+ },
69
+ tools: flat,
70
+ mcp_file_system_options: {
71
+ enabled: true,
72
+ workspace_project_dir: cwd,
73
+ mcp_descriptors: nested,
74
+ },
75
+ mcp_meta_tool_options: {
76
+ enabled: true,
77
+ mcp_descriptors: nested,
78
+ },
79
+ rules_info_complete: true,
80
+ env_info_complete: true,
81
+ repository_info_complete: true,
82
+ mcp_file_system_info_complete: true,
83
+ git_status_info_complete: true,
84
+ };
85
+ }
86
+ // ── Cursor exec-variant → opencode tool name ──
87
+ // Native ExecServerMessage variants only (agent.v1). There is no edit_args or
88
+ // glob_args on the exec channel — Cursor's EditToolCall is display-only, and
89
+ // glob/edit from opencode are advertised as MCP tools and arrive as mcp_args.
90
+ //
91
+ // OpenCode has no `ls` / `delete` builtins: ls → read (dirs are readable),
92
+ // delete → bash `rm`. Arg key remapping happens in mapCursorArgsToOpencode.
93
+ const cursorToolToOpencode = {
94
+ read_args: "read",
95
+ write_args: "write",
96
+ pi_write_args: "write",
97
+ grep_args: "grep",
98
+ ls_args: "read",
99
+ delete_args: "bash",
100
+ shell_stream_args: "bash",
101
+ mcp_args: "mcp",
102
+ };
103
+ const opencodeToolToCursor = {
104
+ read: "read_args",
105
+ write: "write_args",
106
+ grep: "grep_args",
107
+ bash: "shell_stream_args",
108
+ mcp: "mcp_args",
109
+ };
110
+ /** Cursor-only fields that must not be forwarded as OpenCode tool input. */
111
+ const CURSOR_INTERNAL_KEYS = new Set([
112
+ "tool_call_id",
113
+ "toolCallId",
114
+ "exec_id",
115
+ "span",
116
+ "sandbox_policy",
117
+ "requested_sandbox_policy",
118
+ "smart_mode_approval",
119
+ "smart_mode_approval_only",
120
+ "skip_approval",
121
+ "parsing_result",
122
+ "classifier_result",
123
+ "hook_approval_requirement",
124
+ "conversation_id",
125
+ "simple_commands",
126
+ "has_input_redirect",
127
+ "has_output_redirect",
128
+ "is_background",
129
+ "timeout_behavior",
130
+ "hard_timeout",
131
+ "close_stdin",
132
+ "output_notification",
133
+ "file_output_threshold_bytes",
134
+ "return_file_content_after_write",
135
+ "file_bytes",
136
+ "encoding_hint",
137
+ "ignore",
138
+ ]);
139
+ export function mapExecServerToToolName(execField) {
140
+ return cursorToolToOpencode[execField];
141
+ }
142
+ export function mapToolNameToExecField(toolName) {
143
+ return opencodeToolToCursor[toolName];
144
+ }
145
+ // Cursor exec-request variant → the ExecClientMessage result field the client
146
+ // must reply with. This is keyed off the REQUEST variant, not the opencode tool
147
+ // name, because an MCP call must always answer with `mcp_result` even though the
148
+ // resolved tool name (e.g. "read") looks like a built-in.
149
+ const execVariantToResultField = {
150
+ read_args: "read_result",
151
+ write_args: "write_result",
152
+ // Pi write: args #48, result #49 (not the same field number).
153
+ pi_write_args: "pi_write_result",
154
+ grep_args: "grep_result",
155
+ ls_args: "ls_result",
156
+ delete_args: "delete_result",
157
+ shell_stream_args: "shell_stream",
158
+ mcp_args: "mcp_result",
159
+ };
160
+ const MCP_PREFIX = "opencode-";
161
+ export function parseExecServerMessage(msg) {
162
+ const id = msg.id;
163
+ if (id === undefined)
164
+ return undefined;
165
+ // Find which args variant is set
166
+ const execVariant = findOneOfVariant(msg, [
167
+ "read_args", "write_args", "pi_write_args",
168
+ "grep_args", "ls_args",
169
+ "delete_args", "shell_stream_args", "mcp_args",
170
+ ]);
171
+ if (!execVariant)
172
+ return undefined;
173
+ const resultField = execVariantToResultField[execVariant];
174
+ const execId = msg.exec_id ?? "";
175
+ if (execVariant === "mcp_args") {
176
+ // An MCP call to one of the tools we advertised. Resolve the real opencode
177
+ // tool name (Cursor's model may have shortened "opencode-read" → "read") and
178
+ // decode the argument map back into JSON for opencode to execute.
179
+ const m = msg.mcp_args ?? {};
180
+ const mapped = mapCursorArgsToOpencode(mcpRealToolName(m), decodeMcpArgs(m.args), "mcp_args");
181
+ return {
182
+ id,
183
+ execId,
184
+ toolName: mapped.toolName,
185
+ args: mapped.args,
186
+ resultField,
187
+ };
188
+ }
189
+ const toolName = cursorToolToOpencode[execVariant];
190
+ if (!toolName)
191
+ return undefined;
192
+ const mapped = mapCursorArgsToOpencode(toolName, msg[execVariant] ?? {}, execVariant);
193
+ return {
194
+ id,
195
+ execId,
196
+ toolName: mapped.toolName,
197
+ args: mapped.args,
198
+ resultField,
199
+ };
200
+ }
201
+ /**
202
+ * Remap Cursor exec / MCP arg shapes onto OpenCode's Effect Schema keys.
203
+ * Without this, OpenCode rejects calls with InvalidArgumentsError
204
+ * (e.g. `path` instead of `filePath`, `file_text` instead of `content`).
205
+ */
206
+ export function mapCursorArgsToOpencode(toolName, raw, execVariant) {
207
+ const cleaned = {};
208
+ for (const [k, v] of Object.entries(raw)) {
209
+ if (v === undefined || v === null)
210
+ continue;
211
+ if (CURSOR_INTERNAL_KEYS.has(k))
212
+ continue;
213
+ // Drop empty strings from optional protobuf defaults.
214
+ if (typeof v === "string" && v.length === 0)
215
+ continue;
216
+ cleaned[k] = v;
217
+ }
218
+ // Native ls_args → OpenCode read (directory listing via read).
219
+ if (execVariant === "ls_args") {
220
+ const filePath = str(cleaned.path) ?? str(cleaned.filePath);
221
+ return { toolName: "read", args: filePath ? { filePath } : {} };
222
+ }
223
+ // Native delete_args → bash rm (OpenCode has no delete builtin).
224
+ if (execVariant === "delete_args") {
225
+ const target = str(cleaned.path) ?? str(cleaned.filePath);
226
+ return {
227
+ toolName: "bash",
228
+ args: target ? { command: `rm -f -- ${shellQuote(target)}` } : { command: "true" },
229
+ };
230
+ }
231
+ switch (toolName) {
232
+ case "read": {
233
+ const args = {};
234
+ const filePath = str(cleaned.filePath) ?? str(cleaned.path) ?? str(cleaned.file_path);
235
+ if (filePath)
236
+ args.filePath = filePath;
237
+ // Cursor often sends offset=0/limit=0 as protobuf defaults. OpenCode's
238
+ // read treats limit=0 as "read zero lines" (empty content) — omit zeros.
239
+ const offset = num(cleaned.offset);
240
+ if (offset !== undefined && offset > 0)
241
+ args.offset = offset;
242
+ const limit = num(cleaned.limit);
243
+ if (limit !== undefined && limit > 0)
244
+ args.limit = limit;
245
+ return { toolName: "read", args };
246
+ }
247
+ case "write": {
248
+ const args = {};
249
+ const filePath = str(cleaned.filePath) ?? str(cleaned.path) ?? str(cleaned.file_path);
250
+ if (filePath)
251
+ args.filePath = filePath;
252
+ const content = str(cleaned.content) ?? str(cleaned.file_text) ?? str(cleaned.fileText);
253
+ if (content !== undefined)
254
+ args.content = content;
255
+ return { toolName: "write", args };
256
+ }
257
+ case "edit": {
258
+ const args = {};
259
+ const filePath = str(cleaned.filePath) ?? str(cleaned.path) ?? str(cleaned.file_path);
260
+ if (filePath)
261
+ args.filePath = filePath;
262
+ const oldString = str(cleaned.oldString) ?? str(cleaned.old_string);
263
+ if (oldString !== undefined)
264
+ args.oldString = oldString;
265
+ const newString = str(cleaned.newString) ?? str(cleaned.new_string);
266
+ if (newString !== undefined)
267
+ args.newString = newString;
268
+ if (typeof cleaned.replaceAll === "boolean")
269
+ args.replaceAll = cleaned.replaceAll;
270
+ return { toolName: "edit", args };
271
+ }
272
+ case "bash": {
273
+ const args = {};
274
+ const command = str(cleaned.command);
275
+ if (command)
276
+ args.command = command;
277
+ const workdir = str(cleaned.workdir) ?? str(cleaned.working_directory);
278
+ if (workdir)
279
+ args.workdir = workdir;
280
+ const timeout = num(cleaned.timeout);
281
+ if (timeout !== undefined)
282
+ args.timeout = timeout;
283
+ return { toolName: "bash", args };
284
+ }
285
+ case "grep": {
286
+ const pattern = str(cleaned.pattern);
287
+ const path = str(cleaned.path);
288
+ const include = str(cleaned.include) ?? str(cleaned.glob);
289
+ // Cursor's native Grep often arrives with an empty pattern + a glob
290
+ // (e.g. "**/*") when the model is really listing files. OpenCode's grep
291
+ // requires a non-empty string pattern — remap to glob instead of
292
+ // forwarding a call that will fail and loop.
293
+ if (!pattern) {
294
+ const args = { pattern: include ?? "**/*" };
295
+ if (path)
296
+ args.path = path;
297
+ return { toolName: "glob", args };
298
+ }
299
+ const args = { pattern };
300
+ if (path)
301
+ args.path = path;
302
+ if (include)
303
+ args.include = include;
304
+ return { toolName: "grep", args };
305
+ }
306
+ case "glob": {
307
+ const args = {};
308
+ const pattern = str(cleaned.pattern) ?? str(cleaned.glob_pattern) ?? str(cleaned.globPattern);
309
+ if (pattern)
310
+ args.pattern = pattern;
311
+ const path = str(cleaned.path) ?? str(cleaned.target_directory) ?? str(cleaned.targetDirectory);
312
+ if (path)
313
+ args.path = path;
314
+ return { toolName: "glob", args };
315
+ }
316
+ default:
317
+ return { toolName, args: cleaned };
318
+ }
319
+ }
320
+ function str(v) {
321
+ return typeof v === "string" && v.length > 0 ? v : undefined;
322
+ }
323
+ function num(v) {
324
+ if (typeof v === "number" && Number.isFinite(v))
325
+ return v;
326
+ if (typeof v === "string" && v.trim() !== "" && Number.isFinite(Number(v)))
327
+ return Number(v);
328
+ return undefined;
329
+ }
330
+ function shellQuote(s) {
331
+ return `'${s.replace(/'/g, `'\\''`)}'`;
332
+ }
333
+ function mcpRealToolName(mcpArgs) {
334
+ const toolName = mcpArgs.tool_name;
335
+ if (toolName)
336
+ return toolName;
337
+ const name = mcpArgs.name ?? "";
338
+ return name.startsWith(MCP_PREFIX) ? name.slice(MCP_PREFIX.length) : name || "mcp";
339
+ }
340
+ function decodeMcpArgs(raw) {
341
+ if (!Array.isArray(raw))
342
+ return {};
343
+ const entries = raw.map((a) => (typeof a === "string" ? b64ToBytes(a) : a));
344
+ try {
345
+ return decodeStructEntriesToJson(entries);
346
+ }
347
+ catch {
348
+ return {};
349
+ }
350
+ }
351
+ function b64ToBytes(s) {
352
+ const bin = atob(s);
353
+ const out = new Uint8Array(bin.length);
354
+ for (let i = 0; i < bin.length; i++)
355
+ out[i] = bin.charCodeAt(i);
356
+ return out;
357
+ }
358
+ function findOneOfVariant(msg, candidates) {
359
+ for (const key of candidates) {
360
+ if (msg[key] !== undefined && msg[key] !== null) {
361
+ return key;
362
+ }
363
+ }
364
+ return undefined;
365
+ }
366
+ /**
367
+ * Build one or more ExecClientMessage frames for a tool result.
368
+ * Shell replies are a sequence of ShellStream oneofs under the same id —
369
+ * Start → stdout/stderr → exit — then an ACM #5 stream_close so the server
370
+ * knows the client finished streaming (CLI always sends this; without it
371
+ * shell execs hang on heartbeats forever).
372
+ */
373
+ export function buildExecClientMessages(input) {
374
+ const resultField = input.resultField || "mcp_result";
375
+ const frames = [];
376
+ if (resultField === "shell_stream") {
377
+ // Real clients always emit Start → Stdout/Stderr* → Exit (capture/tests).
378
+ frames.push(encodeShellStream(input.execId, undefined, { start: {} }));
379
+ if (input.error) {
380
+ frames.push(encodeShellStream(input.execId, undefined, { stderr: { data: input.error } }));
381
+ frames.push(encodeShellStream(input.execId, input.executionTimeMs, { exit: { code: 1, aborted: false } }));
382
+ }
383
+ else {
384
+ if (input.output) {
385
+ frames.push(encodeShellStream(input.execId, undefined, { stdout: { data: input.output } }));
386
+ }
387
+ frames.push(encodeShellStream(input.execId, input.executionTimeMs, {
388
+ exit: { code: 0, aborted: false },
389
+ }));
390
+ }
391
+ }
392
+ else {
393
+ const clientMsg = {
394
+ id: input.execId,
395
+ local_execution_time_ms: input.executionTimeMs ?? 0,
396
+ };
397
+ clientMsg[resultField] = buildTypedExecResult(resultField, input.output, input.error);
398
+ frames.push(encodeMessage("AgentClientMessage", {
399
+ exec_client_message: clientMsg,
400
+ }));
401
+ }
402
+ // Always close the exec stream — mirrors CLI agent-exec after every handler.
403
+ frames.push(buildExecStreamClose(input.execId));
404
+ return frames;
405
+ }
406
+ /** ACM #5 exec_client_control_message { stream_close { id } }. */
407
+ export function buildExecStreamClose(execId) {
408
+ return encodeMessage("AgentClientMessage", {
409
+ exec_client_control_message: {
410
+ stream_close: { id: execId },
411
+ },
412
+ });
413
+ }
414
+ /**
415
+ * Map OpenCode tool text into the agent.v1 result oneof for each exec variant.
416
+ * OpenCode returns free-form text; we wrap it in the minimal success shape the
417
+ * server accepts (verified against agent.v1 wire captures).
418
+ */
419
+ export function buildTypedExecResult(resultField, output, error) {
420
+ switch (resultField) {
421
+ case "read_result":
422
+ if (error)
423
+ return { error: { path: "", error } };
424
+ return {
425
+ success: {
426
+ path: extractPathTag(output) ?? "",
427
+ content: output,
428
+ total_lines: countLines(output),
429
+ },
430
+ };
431
+ case "grep_result": {
432
+ if (error)
433
+ return { error: { error } };
434
+ // Prefer files_with_matches: OpenCode glob/grep often returns path lists.
435
+ // Content-mode GrepSuccess also works but needs GrepFileMatch nesting.
436
+ const files = extractPathLines(output);
437
+ const cwd = process.cwd();
438
+ return {
439
+ success: {
440
+ pattern: "",
441
+ path: cwd,
442
+ output_mode: "files_with_matches",
443
+ workspace_results: {
444
+ [cwd]: {
445
+ files: {
446
+ files,
447
+ total_files: files.length,
448
+ client_truncated: false,
449
+ },
450
+ },
451
+ },
452
+ },
453
+ };
454
+ }
455
+ case "write_result":
456
+ if (error)
457
+ return { error: { path: "", error } };
458
+ return {
459
+ success: {
460
+ path: extractPathTag(output) ?? "",
461
+ lines_created: countLines(output),
462
+ file_size: output.length,
463
+ },
464
+ };
465
+ case "pi_write_result":
466
+ // PiWriteExecSuccess is just { output }; error is { error }.
467
+ if (error)
468
+ return { error: { error } };
469
+ return { success: { output: output || "Wrote file successfully." } };
470
+ case "delete_result":
471
+ if (error)
472
+ return { error: { path: "", error } };
473
+ return { success: { path: "", deleted_file: "" } };
474
+ case "ls_result": {
475
+ if (error)
476
+ return { error: { path: "", error } };
477
+ const rootPath = process.cwd();
478
+ const entries = extractPathLines(output);
479
+ return {
480
+ success: {
481
+ directory_tree_root: {
482
+ abs_path: rootPath,
483
+ children_dirs: [],
484
+ children_files: entries.map((name) => ({
485
+ name: name.includes("/") ? name.slice(name.lastIndexOf("/") + 1) : name,
486
+ })),
487
+ num_files: entries.length,
488
+ },
489
+ },
490
+ };
491
+ }
492
+ case "mcp_result":
493
+ if (error)
494
+ return { error: { error } };
495
+ return {
496
+ success: {
497
+ content: [{ text: { text: output } }],
498
+ is_error: false,
499
+ },
500
+ };
501
+ default:
502
+ // Unknown variant: best-effort success wrapper so the server sees a oneof.
503
+ if (error)
504
+ return { error: { error } };
505
+ return { success: { content: output } };
506
+ }
507
+ }
508
+ function extractPathTag(output) {
509
+ const m = output.match(/<path>([^<]+)<\/path>/);
510
+ return m?.[1];
511
+ }
512
+ function extractPathLines(output) {
513
+ const lines = output.split("\n").map((l) => l.trim()).filter(Boolean);
514
+ // Prefer absolute / relative path-looking lines; fall back to all non-empty.
515
+ const paths = lines.filter((l) => l.startsWith("/") || l.startsWith("./") || l.includes("/"));
516
+ return (paths.length > 0 ? paths : lines).slice(0, 2000);
517
+ }
518
+ function countLines(s) {
519
+ if (!s)
520
+ return 0;
521
+ let n = 1;
522
+ for (let i = 0; i < s.length; i++)
523
+ if (s.charCodeAt(i) === 10)
524
+ n++;
525
+ return n;
526
+ }
527
+ function encodeShellStream(execId, executionTimeMs, shellStream) {
528
+ const clientMsg = {
529
+ id: execId,
530
+ shell_stream: shellStream,
531
+ };
532
+ if (executionTimeMs !== undefined) {
533
+ clientMsg.local_execution_time_ms = executionTimeMs;
534
+ }
535
+ return encodeMessage("AgentClientMessage", {
536
+ exec_client_message: clientMsg,
537
+ });
538
+ }
539
+ // ── Map a Cursor tool call to the opencode tool-call struct ──
540
+ export function buildToolCallPart(execMsg, sessionId) {
541
+ // Tag the toolCallId with the originating session's id so the result-bearing
542
+ // doStream call can disambiguate the Run stream — Cursor resets exec ids per
543
+ // stream, so two concurrent conversations would otherwise collide on `id`.
544
+ return {
545
+ toolCallId: `cursor_${sessionId}_${execMsg.id}`,
546
+ toolName: execMsg.toolName,
547
+ // LanguageModelV3ToolCall.input is a *stringified* JSON object. The AI SDK
548
+ // does `input.trim()` before JSON.parse; emitting a plain object crashes
549
+ // with "input.trim is not a function" and the model retries forever.
550
+ // OpenCode's processor then receives the parsed object from the SDK.
551
+ input: JSON.stringify(execMsg.args ?? {}),
552
+ };
553
+ }
554
+ // ── Extract exec id from tool call id ──
555
+ export function parseExecIdFromToolCallId(toolCallId) {
556
+ // Format: cursor_<sessionId>_<execId>. sessionId may itself contain
557
+ // underscores (e.g. UUIDs), so anchor on the trailing _<digits>.
558
+ const match = toolCallId.match(/^cursor_(.+)_(\d+)$/);
559
+ if (!match)
560
+ return undefined;
561
+ const execId = parseInt(match[2], 10);
562
+ if (!Number.isFinite(execId))
563
+ return undefined;
564
+ return { sessionId: match[1], execId };
565
+ }
566
+ // ── Safety net: reply to exec variants we don't map to an opencode tool ──
567
+ //
568
+ // Cursor's real server sends server-initiated exec probes (request_context #10,
569
+ // and potentially diagnostics/smart-mode-classifier/etc.) that are NOT tool
570
+ // calls. opencode owns the tool loop, so these have no opencode tool to route
571
+ // to — but we MUST still reply on the Run stream or the server blocks forever
572
+ // (endless heartbeats, no response). For any unmapped variant we emit an empty
573
+ // result at the SAME field number (request/result field numbers are identical
574
+ // for every exec variant: read #7→#7, mcp #11→#11, request_context #10→#10…).
575
+ /**
576
+ * Find the exec variant field number from the raw (gunzipped) AgentServerMessage
577
+ * payload: peel field #2 (exec_server_message), then return the first
578
+ * message-typed field that isn't id(#1)/exec_id(#15)/span_context(#19).
579
+ * Returns undefined if there is no exec_server_message or no variant set.
580
+ */
581
+ export function detectExecVariantField(agentServerPayload) {
582
+ const execBytes = readAllFields(agentServerPayload).find((f) => f.fn === 2 && f.wt === 2)?.bytes;
583
+ if (!execBytes)
584
+ return undefined;
585
+ for (const f of readAllFields(execBytes)) {
586
+ if (f.wt !== 2)
587
+ continue; // message-typed only (wire type 2)
588
+ if (f.fn === 1 || f.fn === 15 || f.fn === 19)
589
+ continue;
590
+ return f.fn;
591
+ }
592
+ return undefined;
593
+ }
594
+ /** Build ExecClientMessage{1:id, <resultField>: empty} as raw bytes. */
595
+ export function buildRawEmptyExecReply(execId, resultField) {
596
+ const inner = [];
597
+ writeVarintRaw(inner, (1 << 3) | 0); // field 1 (id), wire 0 (varint)
598
+ writeVarintRaw(inner, execId >>> 0);
599
+ writeVarintRaw(inner, (resultField << 3) | 2); // field N, wire 2 (length-delimited)
600
+ writeVarintRaw(inner, 0); // empty submessage
601
+ // Wrap as AgentClientMessage.exec_client_message (field #2).
602
+ const acm = [];
603
+ writeVarintRaw(acm, (2 << 3) | 2);
604
+ writeVarintRaw(acm, inner.length);
605
+ for (const b of inner)
606
+ acm.push(b);
607
+ return new Uint8Array(acm);
608
+ }
609
+ function writeVarintRaw(out, n) {
610
+ let v = n >>> 0;
611
+ while (v > 0x7f) {
612
+ out.push((v & 0x7f) | 0x80);
613
+ v >>>= 7;
614
+ }
615
+ out.push(v);
616
+ }
617
+ /**
618
+ * Encode exec #10 request_context_result from a prebuilt RequestContext payload.
619
+ */
620
+ export function buildRequestContextResult(execId, requestContext) {
621
+ return encodeMessage("AgentClientMessage", {
622
+ exec_client_message: {
623
+ id: execId,
624
+ request_context_result: {
625
+ success: {
626
+ request_context: requestContext,
627
+ },
628
+ },
629
+ },
630
+ });
631
+ }