cursor-opencode-provider 0.2.3 → 0.2.5
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 +36 -17
- package/SECURITY.md +42 -0
- package/dist/activity.d.ts +15 -0
- package/dist/activity.js +76 -0
- package/dist/auth.d.ts +2 -1
- package/dist/auth.js +19 -13
- package/dist/context/build.js +0 -7
- package/dist/context/rules.d.ts +4 -0
- package/dist/context/rules.js +53 -17
- package/dist/debug.d.ts +9 -0
- package/dist/debug.js +44 -4
- package/dist/errors.d.ts +59 -0
- package/dist/errors.js +199 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +4 -0
- package/dist/language-model.d.ts +54 -7
- package/dist/language-model.js +730 -197
- package/dist/models.d.ts +7 -0
- package/dist/models.js +259 -79
- package/dist/plugin.js +38 -11
- package/dist/protocol/exec-variants.d.ts +24 -0
- package/dist/protocol/exec-variants.js +55 -0
- package/dist/protocol/interactions.d.ts +8 -1
- package/dist/protocol/interactions.js +15 -2
- package/dist/protocol/messages.js +109 -26
- package/dist/protocol/request.d.ts +2 -4
- package/dist/protocol/request.js +0 -8
- package/dist/protocol/struct.js +1 -1
- package/dist/protocol/tool-call-bridge.js +13 -1
- package/dist/protocol/tools.d.ts +8 -1
- package/dist/protocol/tools.js +384 -39
- package/dist/session.d.ts +115 -8
- package/dist/session.js +362 -31
- package/dist/shell-timeout.d.ts +57 -0
- package/dist/shell-timeout.js +186 -0
- package/dist/transport/connect.d.ts +37 -1
- package/dist/transport/connect.js +679 -115
- package/package.json +8 -2
package/dist/protocol/tools.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
1
2
|
import { encodeMessage } from "./messages.js";
|
|
2
3
|
import { encodeJsonAsValue, decodeStructEntriesToJson, readAllFields } from "./struct.js";
|
|
3
4
|
import { trace } from "../debug.js";
|
|
5
|
+
import { cursorExecVariantByRequestName } from "./exec-variants.js";
|
|
4
6
|
// Exec variant field number whose reply is the server-initiated request_context
|
|
5
7
|
// probe (ExecServerMessage #10 → ExecClientMessage #10). request/result share a
|
|
6
8
|
// field number for every exec variant, so this is also the result field.
|
|
@@ -132,8 +134,13 @@ export function buildLiveRequestContext(tools, providerIdentifier = "opencode",
|
|
|
132
134
|
// glob_args on the exec channel — Cursor's EditToolCall is display-only, and
|
|
133
135
|
// glob/edit from opencode are advertised as MCP tools and arrive as mcp_args.
|
|
134
136
|
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
+
// Design note (F11): Cursor has native delete / background-shell tools; OpenCode
|
|
138
|
+
// does not. This provider intentionally remaps:
|
|
139
|
+
// - delete_args → bash `rm -f -- <quoted-path>`
|
|
140
|
+
// - background_shell_spawn_args → bash + nohup detach wrapper
|
|
141
|
+
// Permissions still flow through OpenCode's advertised `bash` tool. Soft-
|
|
142
|
+
// background / detached children can outlive the OpenCode tool call; leftover
|
|
143
|
+
// process cleanup is left to the user / OS. Arg key remapping happens below.
|
|
137
144
|
const cursorToolToOpencode = {
|
|
138
145
|
read_args: "read",
|
|
139
146
|
write_args: "write",
|
|
@@ -148,6 +155,8 @@ const cursorToolToOpencode = {
|
|
|
148
155
|
ls_args: "read",
|
|
149
156
|
delete_args: "bash",
|
|
150
157
|
shell_stream_args: "bash",
|
|
158
|
+
background_shell_spawn_args: "bash",
|
|
159
|
+
subagent_args: "task",
|
|
151
160
|
mcp_args: "mcp",
|
|
152
161
|
};
|
|
153
162
|
const opencodeToolToCursor = {
|
|
@@ -155,6 +164,7 @@ const opencodeToolToCursor = {
|
|
|
155
164
|
write: "write_args",
|
|
156
165
|
grep: "grep_args",
|
|
157
166
|
bash: "shell_stream_args",
|
|
167
|
+
task: "subagent_args",
|
|
158
168
|
mcp: "mcp_args",
|
|
159
169
|
};
|
|
160
170
|
/** Cursor-only fields that must not be forwarded as OpenCode tool input. */
|
|
@@ -203,26 +213,7 @@ export function mapExecServerToToolName(execField) {
|
|
|
203
213
|
export function mapToolNameToExecField(toolName) {
|
|
204
214
|
return opencodeToolToCursor[toolName];
|
|
205
215
|
}
|
|
206
|
-
|
|
207
|
-
// must reply with. This is keyed off the REQUEST variant, not the opencode tool
|
|
208
|
-
// name, because an MCP call must always answer with `mcp_result` even though the
|
|
209
|
-
// resolved tool name (e.g. "read") looks like a built-in.
|
|
210
|
-
const execVariantToResultField = {
|
|
211
|
-
read_args: "read_result",
|
|
212
|
-
write_args: "write_result",
|
|
213
|
-
pi_read_args: "pi_read_result",
|
|
214
|
-
pi_bash_args: "pi_bash_result",
|
|
215
|
-
pi_edit_args: "pi_edit_result",
|
|
216
|
-
pi_write_args: "pi_write_result",
|
|
217
|
-
pi_grep_args: "pi_grep_result",
|
|
218
|
-
pi_find_args: "pi_find_result",
|
|
219
|
-
pi_ls_args: "pi_ls_result",
|
|
220
|
-
grep_args: "grep_result",
|
|
221
|
-
ls_args: "ls_result",
|
|
222
|
-
delete_args: "delete_result",
|
|
223
|
-
shell_stream_args: "shell_stream",
|
|
224
|
-
mcp_args: "mcp_result",
|
|
225
|
-
};
|
|
216
|
+
const BACKGROUND_SHELL_MARKER = "__CURSOR_BACKGROUND_SHELL__";
|
|
226
217
|
export function parseExecServerMessage(msg) {
|
|
227
218
|
const id = msg.id;
|
|
228
219
|
if (id === undefined)
|
|
@@ -233,12 +224,74 @@ export function parseExecServerMessage(msg) {
|
|
|
233
224
|
"pi_read_args", "pi_bash_args", "pi_edit_args", "pi_write_args",
|
|
234
225
|
"pi_grep_args", "pi_find_args", "pi_ls_args",
|
|
235
226
|
"grep_args", "ls_args",
|
|
236
|
-
"delete_args", "shell_stream_args", "mcp_args",
|
|
227
|
+
"delete_args", "shell_stream_args", "background_shell_spawn_args", "mcp_args",
|
|
228
|
+
"subagent_args",
|
|
237
229
|
]);
|
|
238
230
|
if (!execVariant)
|
|
239
231
|
return undefined;
|
|
240
|
-
|
|
232
|
+
// Use the complete canonical request/result table. In particular, Pi request
|
|
233
|
+
// fields #45..#51 pair with result fields #46..#52 rather than matching ids.
|
|
234
|
+
const resultField = cursorExecVariantByRequestName(execVariant)?.resultName;
|
|
235
|
+
if (!resultField)
|
|
236
|
+
return undefined;
|
|
241
237
|
const execId = msg.exec_id ?? "";
|
|
238
|
+
// F11: Cursor native background shell → OpenCode bash. The wrapper detaches
|
|
239
|
+
// via nohup immediately; OpenCode only sees the spawn marker (pid + log path).
|
|
240
|
+
// Residual: the child can keep running after this tool call completes.
|
|
241
|
+
if (execVariant === "background_shell_spawn_args") {
|
|
242
|
+
const raw = msg.background_shell_spawn_args ?? {};
|
|
243
|
+
const command = str(raw.command);
|
|
244
|
+
const workingDirectory = str(raw.working_directory) ?? "";
|
|
245
|
+
const args = {};
|
|
246
|
+
if (command)
|
|
247
|
+
args.command = buildBackgroundShellCommand(command);
|
|
248
|
+
if (workingDirectory)
|
|
249
|
+
args.workdir = workingDirectory;
|
|
250
|
+
return {
|
|
251
|
+
id,
|
|
252
|
+
execId,
|
|
253
|
+
toolName: "bash",
|
|
254
|
+
args,
|
|
255
|
+
resultField,
|
|
256
|
+
resultMetadata: { command: command ?? "", working_directory: workingDirectory },
|
|
257
|
+
localError: raw.enable_write_shell_stdin_tool === true
|
|
258
|
+
? "Interactive background shells are not available through OpenCode's bash tool."
|
|
259
|
+
: command
|
|
260
|
+
? undefined
|
|
261
|
+
: "Cursor background shell request is missing a command.",
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
if (execVariant === "subagent_args") {
|
|
265
|
+
const raw = msg.subagent_args ?? {};
|
|
266
|
+
const prompt = str(raw.prompt);
|
|
267
|
+
const cursorSubagentType = str(raw.subagent_type);
|
|
268
|
+
const subagentType = cursorSubagentType
|
|
269
|
+
? mapCursorSubagentTypeToOpenCode(cursorSubagentType)
|
|
270
|
+
: undefined;
|
|
271
|
+
const args = {
|
|
272
|
+
description: describeSubagentTask(prompt, subagentType),
|
|
273
|
+
prompt: prompt ?? "",
|
|
274
|
+
subagent_type: subagentType ?? "",
|
|
275
|
+
};
|
|
276
|
+
const resumeAgentId = str(raw.resume_agent_id);
|
|
277
|
+
if (resumeAgentId)
|
|
278
|
+
args.task_id = resumeAgentId;
|
|
279
|
+
// protobufjs materializes an absent proto3 optional bool as false in this
|
|
280
|
+
// reflection schema. OpenCode's foreground default is already false, so
|
|
281
|
+
// only forward the meaningful opt-in value.
|
|
282
|
+
if (raw.run_in_background === true)
|
|
283
|
+
args.background = true;
|
|
284
|
+
return {
|
|
285
|
+
id,
|
|
286
|
+
execId,
|
|
287
|
+
toolName: "task",
|
|
288
|
+
args,
|
|
289
|
+
resultField,
|
|
290
|
+
localError: prompt && cursorSubagentType
|
|
291
|
+
? undefined
|
|
292
|
+
: "Cursor subagent request is missing a required prompt or subagent type.",
|
|
293
|
+
};
|
|
294
|
+
}
|
|
242
295
|
if (execVariant === "mcp_args") {
|
|
243
296
|
// An MCP call to one of the tools we advertised. Resolve the real opencode
|
|
244
297
|
// tool name (Cursor's model may have shortened "opencode-read" → "read") and
|
|
@@ -284,12 +337,50 @@ export function parseExecServerMessage(msg) {
|
|
|
284
337
|
if (!toolName)
|
|
285
338
|
return undefined;
|
|
286
339
|
const mapped = mapCursorArgsToOpencode(toolName, msg[execVariant] ?? {}, execVariant);
|
|
340
|
+
const rawArgs = msg[execVariant] ?? {};
|
|
341
|
+
const resultMetadata = execVariant === "shell_stream_args"
|
|
342
|
+
? shellStreamResultMetadata(rawArgs)
|
|
343
|
+
: execVariant === "read_args"
|
|
344
|
+
? {
|
|
345
|
+
path: str(rawArgs.path) ?? str(rawArgs.file_path) ?? "",
|
|
346
|
+
...(typeof mapped.args.offset === "number" ? { offset: mapped.args.offset } : {}),
|
|
347
|
+
...(typeof mapped.args.limit === "number" ? { limit: mapped.args.limit } : {}),
|
|
348
|
+
}
|
|
349
|
+
: undefined;
|
|
350
|
+
if (resultMetadata
|
|
351
|
+
&& resultMetadata.timeout_behavior !== 2
|
|
352
|
+
&& typeof resultMetadata.timeout_ms === "number") {
|
|
353
|
+
// Cursor's protobuf default (timeout=0) means 30 seconds for an ordinary
|
|
354
|
+
// foreground shell. OpenCode instead treats zero literally, so pass the
|
|
355
|
+
// effective native value rather than the raw protobuf default.
|
|
356
|
+
mapped.args.timeout = resultMetadata.timeout_ms;
|
|
357
|
+
}
|
|
287
358
|
return {
|
|
288
359
|
id,
|
|
289
360
|
execId,
|
|
290
361
|
toolName: mapped.toolName,
|
|
291
362
|
args: mapped.args,
|
|
292
363
|
resultField,
|
|
364
|
+
...(resultMetadata ? { resultMetadata } : {}),
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
function shellStreamResultMetadata(raw) {
|
|
368
|
+
const timeout = num(raw.timeout) ?? 0;
|
|
369
|
+
const timeoutBehavior = num(raw.timeout_behavior) ?? 0;
|
|
370
|
+
const hardTimeout = num(raw.hard_timeout);
|
|
371
|
+
// Cursor CLI: a nonzero timeout is used verbatim. A zero foreground timeout
|
|
372
|
+
// defaults to 30s; zero with background/hard-timeout semantics means an
|
|
373
|
+
// immediate soft handoff governed by the separate hard deadline.
|
|
374
|
+
const effectiveTimeout = timeout !== 0
|
|
375
|
+
? timeout
|
|
376
|
+
: (timeoutBehavior === 2 || (hardTimeout !== undefined && hardTimeout > 0) ? 0 : 30_000);
|
|
377
|
+
return {
|
|
378
|
+
shell_stream: true,
|
|
379
|
+
command: str(raw.command) ?? "",
|
|
380
|
+
working_directory: str(raw.working_directory) ?? "",
|
|
381
|
+
timeout_ms: effectiveTimeout,
|
|
382
|
+
timeout_behavior: timeoutBehavior,
|
|
383
|
+
...(hardTimeout !== undefined && hardTimeout > 0 ? { hard_timeout_ms: hardTimeout } : {}),
|
|
293
384
|
};
|
|
294
385
|
}
|
|
295
386
|
/**
|
|
@@ -315,7 +406,9 @@ export function mapCursorArgsToOpencode(toolName, raw, execVariant) {
|
|
|
315
406
|
const filePath = str(cleaned.path) ?? str(cleaned.filePath);
|
|
316
407
|
return { toolName: "read", args: filePath ? { filePath } : {} };
|
|
317
408
|
}
|
|
318
|
-
//
|
|
409
|
+
// F11: Cursor native delete_args → OpenCode bash. No delete builtin exists, so
|
|
410
|
+
// we emulate with `rm -f -- <path>` (shell-quoted). Same permission boundary
|
|
411
|
+
// as any other bash tool call.
|
|
319
412
|
if (execVariant === "delete_args") {
|
|
320
413
|
const target = str(cleaned.path) ?? str(cleaned.filePath);
|
|
321
414
|
return {
|
|
@@ -425,9 +518,47 @@ function num(v) {
|
|
|
425
518
|
return Number(v);
|
|
426
519
|
return undefined;
|
|
427
520
|
}
|
|
521
|
+
function describeSubagentTask(prompt, subagentType) {
|
|
522
|
+
const words = prompt?.replace(/\s+/g, " ").trim().split(" ").filter(Boolean).slice(0, 5);
|
|
523
|
+
if (words?.length)
|
|
524
|
+
return words.join(" ");
|
|
525
|
+
return `${subagentType || "Delegated"} task`;
|
|
526
|
+
}
|
|
527
|
+
function mapCursorSubagentTypeToOpenCode(subagentType) {
|
|
528
|
+
// Cursor's built-in general agent uses a camelCase protocol identifier, and
|
|
529
|
+
// its native Bugbot reviewer has no OpenCode-specific agent definition.
|
|
530
|
+
// OpenCode `general` is the general-purpose equivalent; `explore` preserves
|
|
531
|
+
// Bugbot's review-only/read-oriented semantics while retaining grep/read/bash.
|
|
532
|
+
// Preserve every other value so `explore` and configured custom agents keep
|
|
533
|
+
// their exact advertised names.
|
|
534
|
+
if (subagentType === "generalPurpose")
|
|
535
|
+
return "general";
|
|
536
|
+
if (subagentType === "bugbot")
|
|
537
|
+
return "explore";
|
|
538
|
+
return subagentType;
|
|
539
|
+
}
|
|
428
540
|
function shellQuote(s) {
|
|
429
541
|
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
430
542
|
}
|
|
543
|
+
/**
|
|
544
|
+
* F11 / background_shell_spawn_args helper.
|
|
545
|
+
*
|
|
546
|
+
* OpenCode's bash tool is foreground-only. Detach the requested command inside
|
|
547
|
+
* that one foreground call (`nohup … &`) and print a private marker containing
|
|
548
|
+
* the spawned PID and log path. With stdin and all output redirected, the host
|
|
549
|
+
* shell can return immediately instead of retaining OpenCode's tool pipe.
|
|
550
|
+
*
|
|
551
|
+
* Residual: the detached child is not reaped by this provider after OpenCode
|
|
552
|
+
* completes the tool call; cleanup is left to the user / OS.
|
|
553
|
+
*/
|
|
554
|
+
function buildBackgroundShellCommand(command) {
|
|
555
|
+
return [
|
|
556
|
+
'bg_log="$(mktemp "${TMPDIR:-/tmp}/cursor-opencode-bg.XXXXXX")" || exit 1',
|
|
557
|
+
`nohup sh -c ${shellQuote(command)} >"$bg_log" 2>&1 </dev/null &`,
|
|
558
|
+
"bg_pid=$!",
|
|
559
|
+
`printf '${BACKGROUND_SHELL_MARKER}%s:%s\\n' "$bg_pid" "$bg_log"`,
|
|
560
|
+
].join("\n");
|
|
561
|
+
}
|
|
431
562
|
/**
|
|
432
563
|
* Map Cursor McpArgs back to the OpenCode tool id.
|
|
433
564
|
* Prefers provider_identifier + bare tool_name (github + create_pull_request
|
|
@@ -505,9 +636,32 @@ export function buildExecClientMessages(input) {
|
|
|
505
636
|
if (input.output) {
|
|
506
637
|
frames.push(encodeShellStream(input.execId, undefined, { stdout: { data: input.output } }));
|
|
507
638
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
639
|
+
if (input.shellOutcome?.kind === "backgrounded") {
|
|
640
|
+
frames.push(encodeShellStream(input.execId, input.executionTimeMs, {
|
|
641
|
+
backgrounded: {
|
|
642
|
+
shell_id: input.shellOutcome.shellId,
|
|
643
|
+
command: input.shellOutcome.command,
|
|
644
|
+
working_directory: input.shellOutcome.workingDirectory,
|
|
645
|
+
pid: input.shellOutcome.pid,
|
|
646
|
+
ms_to_wait: input.shellOutcome.msToWait,
|
|
647
|
+
reason: input.shellOutcome.reason,
|
|
648
|
+
},
|
|
649
|
+
}));
|
|
650
|
+
}
|
|
651
|
+
else if (input.shellOutcome?.kind === "timeout") {
|
|
652
|
+
frames.push(encodeShellStream(input.execId, input.executionTimeMs, {
|
|
653
|
+
// Native CLI represents timeout structurally. ShellAbortReason.TIMEOUT=2.
|
|
654
|
+
exit: { code: 0, aborted: true, abort_reason: 2 },
|
|
655
|
+
}));
|
|
656
|
+
}
|
|
657
|
+
else {
|
|
658
|
+
const exitCode = input.shellOutcome?.kind === "exit"
|
|
659
|
+
? Math.max(0, Math.min(0xffff_ffff, input.shellOutcome.code))
|
|
660
|
+
: 0;
|
|
661
|
+
frames.push(encodeShellStream(input.execId, input.executionTimeMs, {
|
|
662
|
+
exit: { code: exitCode, aborted: false },
|
|
663
|
+
}));
|
|
664
|
+
}
|
|
511
665
|
}
|
|
512
666
|
}
|
|
513
667
|
else {
|
|
@@ -515,7 +669,7 @@ export function buildExecClientMessages(input) {
|
|
|
515
669
|
id: input.execId,
|
|
516
670
|
local_execution_time_ms: input.executionTimeMs ?? 0,
|
|
517
671
|
};
|
|
518
|
-
clientMsg[resultField] = buildTypedExecResult(resultField, input.output, input.error, input.toolName);
|
|
672
|
+
clientMsg[resultField] = buildTypedExecResult(resultField, input.output, input.error, input.toolName, input.resultMetadata);
|
|
519
673
|
frames.push(encodeMessage("AgentClientMessage", {
|
|
520
674
|
exec_client_message: clientMsg,
|
|
521
675
|
}));
|
|
@@ -602,22 +756,31 @@ export function unwrapReadOutput(output) {
|
|
|
602
756
|
* OpenCode returns free-form text; we wrap it in the minimal success shape the
|
|
603
757
|
* server accepts (verified against agent.v1 wire captures).
|
|
604
758
|
*/
|
|
605
|
-
export function buildTypedExecResult(resultField, output, error, toolName) {
|
|
759
|
+
export function buildTypedExecResult(resultField, output, error, toolName, resultMetadata) {
|
|
606
760
|
switch (resultField) {
|
|
607
|
-
case "read_result":
|
|
761
|
+
case "read_result": {
|
|
762
|
+
const readPath = str(resultMetadata?.path) ?? extractPathTag(output) ?? "";
|
|
608
763
|
if (error)
|
|
609
|
-
return { error: { path:
|
|
764
|
+
return { error: { path: readPath, error } };
|
|
610
765
|
// Strip opencode's <path>/<content> envelope so Cursor's model receives
|
|
611
766
|
// raw file content and can't echo the wrapper into subsequent writes.
|
|
612
767
|
// reads route here only for native read_args; most arrive via mcp_result.
|
|
613
768
|
const content = unwrapReadOutput(output);
|
|
769
|
+
const statPath = extractPathTag(output) ?? readPath;
|
|
770
|
+
const outputMetadata = parseOpenCodeReadMetadata(output);
|
|
771
|
+
const totalLines = outputMetadata.totalLines ?? readFileLineCount(statPath) ?? countLines(content);
|
|
772
|
+
const rangeApplied = readRangeApplied(resultMetadata, totalLines);
|
|
614
773
|
return {
|
|
615
774
|
success: {
|
|
616
|
-
path:
|
|
775
|
+
path: readPath,
|
|
617
776
|
content,
|
|
618
|
-
total_lines:
|
|
777
|
+
total_lines: totalLines,
|
|
778
|
+
file_size: readFileSize(statPath),
|
|
779
|
+
truncated: readOutputTruncated(resultMetadata, outputMetadata, totalLines),
|
|
780
|
+
range_applied: rangeApplied,
|
|
619
781
|
},
|
|
620
782
|
};
|
|
783
|
+
}
|
|
621
784
|
case "grep_result": {
|
|
622
785
|
if (error)
|
|
623
786
|
return { error: { error } };
|
|
@@ -673,6 +836,31 @@ export function buildTypedExecResult(resultField, output, error, toolName) {
|
|
|
673
836
|
if (error)
|
|
674
837
|
return { error: { path: "", error } };
|
|
675
838
|
return { success: { path: "", deleted_file: "" } };
|
|
839
|
+
case "background_shell_spawn_result": {
|
|
840
|
+
const command = str(resultMetadata?.command) ?? "";
|
|
841
|
+
const workingDirectory = str(resultMetadata?.working_directory) ?? "";
|
|
842
|
+
if (error)
|
|
843
|
+
return { error: { command, working_directory: workingDirectory, error } };
|
|
844
|
+
const match = new RegExp(`${BACKGROUND_SHELL_MARKER}(\\d+):([^\\r\\n]+)`).exec(output);
|
|
845
|
+
const pid = match ? Number(match[1]) : 0;
|
|
846
|
+
if (!Number.isSafeInteger(pid) || pid <= 0 || pid > 0xffff_ffff) {
|
|
847
|
+
return {
|
|
848
|
+
error: {
|
|
849
|
+
command,
|
|
850
|
+
working_directory: workingDirectory,
|
|
851
|
+
error: "OpenCode did not return a valid background shell process id.",
|
|
852
|
+
},
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
return {
|
|
856
|
+
success: {
|
|
857
|
+
shell_id: pid,
|
|
858
|
+
command,
|
|
859
|
+
working_directory: workingDirectory,
|
|
860
|
+
pid,
|
|
861
|
+
},
|
|
862
|
+
};
|
|
863
|
+
}
|
|
676
864
|
case "ls_result": {
|
|
677
865
|
if (error)
|
|
678
866
|
return { error: { path: "", error } };
|
|
@@ -706,6 +894,27 @@ export function buildTypedExecResult(resultField, output, error, toolName) {
|
|
|
706
894
|
is_error: false,
|
|
707
895
|
},
|
|
708
896
|
};
|
|
897
|
+
case "subagent_result": {
|
|
898
|
+
const task = parseOpenCodeTaskOutput(output);
|
|
899
|
+
if (error || task.state === "error") {
|
|
900
|
+
return {
|
|
901
|
+
error: {
|
|
902
|
+
...(task.agentId ? { agent_id: task.agentId } : {}),
|
|
903
|
+
error: error ?? task.message ?? output,
|
|
904
|
+
},
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
return {
|
|
908
|
+
success: {
|
|
909
|
+
agent_id: task.agentId ?? "",
|
|
910
|
+
...(task.message !== undefined ? { final_message: task.message } : {}),
|
|
911
|
+
tool_call_count: 0,
|
|
912
|
+
// OpenCode marks an asynchronous launch as state="running". Cursor's
|
|
913
|
+
// canonical USER_REQUEST enum value is 2; foreground/default is 0.
|
|
914
|
+
background_reason: task.state === "running" ? 2 : 0,
|
|
915
|
+
},
|
|
916
|
+
};
|
|
917
|
+
}
|
|
709
918
|
default:
|
|
710
919
|
// Unknown variant: best-effort success wrapper so the server sees a oneof.
|
|
711
920
|
if (error)
|
|
@@ -713,10 +922,116 @@ export function buildTypedExecResult(resultField, output, error, toolName) {
|
|
|
713
922
|
return { success: { content: output } };
|
|
714
923
|
}
|
|
715
924
|
}
|
|
925
|
+
function parseOpenCodeTaskOutput(output) {
|
|
926
|
+
// Attribute order is not guaranteed; accept id/state in either order and
|
|
927
|
+
// ignore additional attributes OpenCode may emit on the <task> open tag.
|
|
928
|
+
const open = /<task\b([^>]*)>/i.exec(output);
|
|
929
|
+
if (!open)
|
|
930
|
+
return { message: output };
|
|
931
|
+
const attrs = open[1];
|
|
932
|
+
const agentId = /\bid="([^"]+)"/i.exec(attrs)?.[1];
|
|
933
|
+
const state = /\bstate="(running|completed|error)"/i.exec(attrs)?.[1];
|
|
934
|
+
if (!agentId || !state)
|
|
935
|
+
return { message: output };
|
|
936
|
+
const tag = state === "error" ? "task_error" : "task_result";
|
|
937
|
+
const body = new RegExp(`<${tag}>\\n?([\\s\\S]*?)\\n?</${tag}>`, "i").exec(output);
|
|
938
|
+
return {
|
|
939
|
+
agentId,
|
|
940
|
+
state,
|
|
941
|
+
message: body?.[1] ?? output,
|
|
942
|
+
};
|
|
943
|
+
}
|
|
716
944
|
function extractPathTag(output) {
|
|
717
945
|
const m = output.match(/<path>([^<]+)<\/path>/);
|
|
718
946
|
return m?.[1];
|
|
719
947
|
}
|
|
948
|
+
/** Recover full-file metadata before unwrapReadOutput removes OpenCode's footer. */
|
|
949
|
+
function parseOpenCodeReadMetadata(output) {
|
|
950
|
+
const showing = /Showing lines (\d+)-(\d+)(?: of (\d+))?\./.exec(output);
|
|
951
|
+
if (showing) {
|
|
952
|
+
return {
|
|
953
|
+
startLine: Number(showing[1]),
|
|
954
|
+
endLine: Number(showing[2]),
|
|
955
|
+
...(showing[3] ? { totalLines: Number(showing[3]) } : {}),
|
|
956
|
+
outputCapped: output.includes("(Output capped at "),
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
const complete = /\(End of file - total (\d+) lines?\)/.exec(output);
|
|
960
|
+
if (complete)
|
|
961
|
+
return { totalLines: Number(complete[1]) };
|
|
962
|
+
return {};
|
|
963
|
+
}
|
|
964
|
+
function readRangeApplied(resultMetadata, totalLines) {
|
|
965
|
+
const offset = num(resultMetadata?.offset);
|
|
966
|
+
const limit = num(resultMetadata?.limit);
|
|
967
|
+
if (offset === undefined && limit === undefined)
|
|
968
|
+
return false;
|
|
969
|
+
if (totalLines === 0)
|
|
970
|
+
return false;
|
|
971
|
+
const startLine = offset ?? 1;
|
|
972
|
+
return startLine < 0 || startLine <= totalLines;
|
|
973
|
+
}
|
|
974
|
+
function readOutputTruncated(resultMetadata, outputMetadata, totalLines) {
|
|
975
|
+
if (outputMetadata.outputCapped)
|
|
976
|
+
return true;
|
|
977
|
+
const returnedEnd = outputMetadata.endLine;
|
|
978
|
+
if (returnedEnd === undefined || totalLines === 0)
|
|
979
|
+
return false;
|
|
980
|
+
const offset = num(resultMetadata?.offset);
|
|
981
|
+
const limit = num(resultMetadata?.limit);
|
|
982
|
+
const startLine = offset ?? 1;
|
|
983
|
+
if (startLine < 0)
|
|
984
|
+
return false;
|
|
985
|
+
const expectedEnd = limit === undefined
|
|
986
|
+
? totalLines
|
|
987
|
+
: Math.min(totalLines, Math.max(1, startLine) + limit - 1);
|
|
988
|
+
return returnedEnd < expectedEnd;
|
|
989
|
+
}
|
|
990
|
+
function readFileSize(filePath) {
|
|
991
|
+
if (!filePath)
|
|
992
|
+
return 0;
|
|
993
|
+
try {
|
|
994
|
+
return fs.statSync(filePath).size;
|
|
995
|
+
}
|
|
996
|
+
catch {
|
|
997
|
+
return 0;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
function readFileLineCount(filePath) {
|
|
1001
|
+
if (!filePath)
|
|
1002
|
+
return undefined;
|
|
1003
|
+
let fd;
|
|
1004
|
+
try {
|
|
1005
|
+
fd = fs.openSync(filePath, "r");
|
|
1006
|
+
const buffer = Buffer.allocUnsafe(64 * 1024);
|
|
1007
|
+
let totalBytes = 0;
|
|
1008
|
+
let lines = 1;
|
|
1009
|
+
while (true) {
|
|
1010
|
+
const bytesRead = fs.readSync(fd, buffer, 0, buffer.length, null);
|
|
1011
|
+
if (bytesRead === 0)
|
|
1012
|
+
break;
|
|
1013
|
+
totalBytes += bytesRead;
|
|
1014
|
+
for (let index = 0; index < bytesRead; index++) {
|
|
1015
|
+
if (buffer[index] === 0x0a)
|
|
1016
|
+
lines++;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
return totalBytes === 0 ? 0 : lines;
|
|
1020
|
+
}
|
|
1021
|
+
catch {
|
|
1022
|
+
return undefined;
|
|
1023
|
+
}
|
|
1024
|
+
finally {
|
|
1025
|
+
if (fd !== undefined) {
|
|
1026
|
+
try {
|
|
1027
|
+
fs.closeSync(fd);
|
|
1028
|
+
}
|
|
1029
|
+
catch {
|
|
1030
|
+
/* best effort */
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
720
1035
|
function extractPathLines(output) {
|
|
721
1036
|
const lines = output.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
722
1037
|
// Prefer absolute / relative path-looking lines; fall back to all non-empty.
|
|
@@ -824,16 +1139,28 @@ export function buildMcpStateResult(execId, args, requestContext) {
|
|
|
824
1139
|
? fsOptions.mcp_descriptors.map(recordValue).filter((d) => !!d)
|
|
825
1140
|
: [];
|
|
826
1141
|
const descriptors = nested.length > 0 ? nested : descriptorsFromFlatTools(requestContext.tools);
|
|
1142
|
+
const flatTools = Array.isArray(requestContext.tools)
|
|
1143
|
+
? requestContext.tools.map(recordValue).filter((tool) => !!tool)
|
|
1144
|
+
: [];
|
|
827
1145
|
const servers = descriptors
|
|
828
1146
|
.filter((descriptor) => {
|
|
829
1147
|
const id = stringValue(descriptor.server_identifier);
|
|
830
1148
|
return requested.size === 0 || (id !== undefined && requested.has(id));
|
|
831
1149
|
})
|
|
832
|
-
.map((descriptor) =>
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
1150
|
+
.map((descriptor) => {
|
|
1151
|
+
const serverIdentifier = stringValue(descriptor.server_identifier) ?? stringValue(descriptor.server_name) ?? "";
|
|
1152
|
+
const tools = Array.isArray(descriptor.tools)
|
|
1153
|
+
? descriptor.tools
|
|
1154
|
+
.map(recordValue)
|
|
1155
|
+
.filter((tool) => !!tool)
|
|
1156
|
+
.map((tool) => mcpStateToolDefinition(serverIdentifier, tool, flatTools))
|
|
1157
|
+
: [];
|
|
1158
|
+
return {
|
|
1159
|
+
server_name: stringValue(descriptor.server_name) ?? serverIdentifier,
|
|
1160
|
+
server_identifier: serverIdentifier,
|
|
1161
|
+
tools,
|
|
1162
|
+
};
|
|
1163
|
+
});
|
|
837
1164
|
return encodeMessage("AgentClientMessage", {
|
|
838
1165
|
exec_client_message: {
|
|
839
1166
|
id: execId,
|
|
@@ -841,6 +1168,24 @@ export function buildMcpStateResult(execId, args, requestContext) {
|
|
|
841
1168
|
},
|
|
842
1169
|
});
|
|
843
1170
|
}
|
|
1171
|
+
/**
|
|
1172
|
+
* Exec #36 uses McpToolDefinition, not the narrower McpToolDescriptor used by
|
|
1173
|
+
* RequestContext's filesystem/meta-tool catalogs. Rehydrate the full identity
|
|
1174
|
+
* from RequestContext.tools so Cursor's native get_mcp_tools can correlate the
|
|
1175
|
+
* discovered definition with the later provider_identifier/tool_name request.
|
|
1176
|
+
*/
|
|
1177
|
+
function mcpStateToolDefinition(serverIdentifier, descriptor, flatTools) {
|
|
1178
|
+
const toolName = stringValue(descriptor.tool_name) ?? "";
|
|
1179
|
+
const advertised = flatTools.find((tool) => stringValue(tool.provider_identifier) === serverIdentifier
|
|
1180
|
+
&& stringValue(tool.tool_name) === toolName);
|
|
1181
|
+
return {
|
|
1182
|
+
name: stringValue(advertised?.name) ?? `${serverIdentifier}-${toolName}`,
|
|
1183
|
+
description: stringValue(advertised?.description) ?? stringValue(descriptor.description) ?? "",
|
|
1184
|
+
input_schema: advertised?.input_schema ?? descriptor.input_schema,
|
|
1185
|
+
provider_identifier: serverIdentifier,
|
|
1186
|
+
tool_name: toolName,
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
844
1189
|
function recordValue(value) {
|
|
845
1190
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
846
1191
|
? value
|