@rama_nigg/open-cursor 2.4.4 → 2.4.6
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 +2 -0
- package/dist/cli/discover.js +9 -0
- package/dist/cli/opencode-cursor.js +9 -0
- package/dist/index.js +335 -301
- package/dist/plugin-entry.js +318 -264
- package/package.json +1 -1
- package/src/auth.ts +2 -2
- package/src/client/simple.ts +3 -3
- package/src/mcp/tool-bridge.ts +4 -2
- package/src/plugin.ts +118 -181
- package/src/provider/runtime-interception.ts +40 -5
- package/src/provider/tool-schema-compat.ts +180 -2
- package/src/proxy/prompt-builder.ts +4 -35
- package/src/streaming/ai-sdk-parts.ts +8 -30
- package/src/streaming/delta-tracker.ts +42 -0
- package/src/streaming/openai-sse.ts +8 -33
- package/src/tools/defaults.ts +0 -4
- package/src/utils/binary.ts +17 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rama_nigg/open-cursor",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.6",
|
|
4
4
|
"description": "No prompt limits. No broken streams. Full thinking + tool support. Your Cursor subscription, properly integrated.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/plugin-entry.js",
|
package/src/auth.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { homedir, platform } from "os";
|
|
|
6
6
|
import { join } from "path";
|
|
7
7
|
import { createLogger } from "./utils/logger";
|
|
8
8
|
import { stripAnsi } from "./utils/errors";
|
|
9
|
-
import { resolveCursorAgentBinary } from "./utils/binary.js";
|
|
9
|
+
import { formatShellCommandForPlatform, resolveCursorAgentBinary } from "./utils/binary.js";
|
|
10
10
|
|
|
11
11
|
const log = createLogger("auth");
|
|
12
12
|
|
|
@@ -76,7 +76,7 @@ export async function startCursorOAuth(): Promise<{
|
|
|
76
76
|
return new Promise((resolve, reject) => {
|
|
77
77
|
log.info("Starting cursor-cli login process");
|
|
78
78
|
|
|
79
|
-
const proc = spawn(resolveCursorAgentBinary(), ["login"], {
|
|
79
|
+
const proc = spawn(formatShellCommandForPlatform(resolveCursorAgentBinary()), ["login"], {
|
|
80
80
|
stdio: ["pipe", "pipe", "pipe"],
|
|
81
81
|
shell: process.platform === "win32",
|
|
82
82
|
});
|
package/src/client/simple.ts
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
type StreamJsonEvent,
|
|
8
8
|
} from '../streaming/types.js';
|
|
9
9
|
import { createLogger } from '../utils/logger.js';
|
|
10
|
-
import { resolveCursorAgentBinary } from '../utils/binary.js';
|
|
10
|
+
import { formatShellCommandForPlatform, resolveCursorAgentBinary } from '../utils/binary.js';
|
|
11
11
|
|
|
12
12
|
export interface CursorClientConfig {
|
|
13
13
|
timeout?: number;
|
|
@@ -77,7 +77,7 @@ export class SimpleCursorClient {
|
|
|
77
77
|
|
|
78
78
|
this.log.debug('Executing prompt stream', { promptLength: prompt.length, mode, model });
|
|
79
79
|
|
|
80
|
-
const child = spawn(this.config.cursorAgentPath, args, {
|
|
80
|
+
const child = spawn(formatShellCommandForPlatform(this.config.cursorAgentPath), args, {
|
|
81
81
|
cwd,
|
|
82
82
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
83
83
|
shell: process.platform === 'win32',
|
|
@@ -189,7 +189,7 @@ export class SimpleCursorClient {
|
|
|
189
189
|
this.log.debug('Executing prompt', { promptLength: prompt.length, mode, model });
|
|
190
190
|
|
|
191
191
|
return new Promise((resolve, reject) => {
|
|
192
|
-
const child = spawn(this.config.cursorAgentPath, args, {
|
|
192
|
+
const child = spawn(formatShellCommandForPlatform(this.config.cursorAgentPath), args, {
|
|
193
193
|
cwd,
|
|
194
194
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
195
195
|
shell: process.platform === 'win32',
|
package/src/mcp/tool-bridge.ts
CHANGED
|
@@ -4,6 +4,8 @@ import type { McpClientManager } from "./client-manager.js";
|
|
|
4
4
|
|
|
5
5
|
const log = createLogger("mcp:tool-bridge");
|
|
6
6
|
|
|
7
|
+
export const MCP_TOOL_PREFIX = "mcp__";
|
|
8
|
+
|
|
7
9
|
interface DiscoveredMcpTool {
|
|
8
10
|
name: string;
|
|
9
11
|
serverName: string;
|
|
@@ -76,10 +78,10 @@ export function buildMcpToolDefinitions(tools: DiscoveredMcpTool[]): any[] {
|
|
|
76
78
|
return defs;
|
|
77
79
|
}
|
|
78
80
|
|
|
79
|
-
function namespaceMcpTool(serverName: string, toolName: string): string {
|
|
81
|
+
export function namespaceMcpTool(serverName: string, toolName: string): string {
|
|
80
82
|
const sanitizedServer = serverName.replace(/[^a-zA-Z0-9]/g, "_");
|
|
81
83
|
const sanitizedTool = toolName.replace(/[^a-zA-Z0-9]/g, "_");
|
|
82
|
-
return
|
|
84
|
+
return `${MCP_TOOL_PREFIX}${sanitizedServer}__${sanitizedTool}`;
|
|
83
85
|
}
|
|
84
86
|
|
|
85
87
|
function mcpSchemaToZod(inputSchema: Record<string, unknown> | undefined, z: any): any {
|
package/src/plugin.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import type { Plugin, PluginInput } from "@opencode-ai/plugin";
|
|
2
2
|
import { tool } from "@opencode-ai/plugin";
|
|
3
3
|
import type { Auth } from "@opencode-ai/sdk";
|
|
4
|
-
import {
|
|
4
|
+
import { realpathSync } from "fs";
|
|
5
5
|
import { mkdir } from "fs/promises";
|
|
6
6
|
import { homedir } from "os";
|
|
7
7
|
import { isAbsolute, join, relative, resolve } from "path";
|
|
8
8
|
import { ToolMapper, type ToolUpdate } from "./acp/tools.js";
|
|
9
9
|
import { startCursorOAuth } from "./auth";
|
|
10
10
|
import { LineBuffer } from "./streaming/line-buffer.js";
|
|
11
|
+
import { MixedDeltaTracker } from "./streaming/delta-tracker.js";
|
|
11
12
|
import { StreamToSseConverter, formatSseDone } from "./streaming/openai-sse.js";
|
|
12
13
|
import { parseStreamJsonLine } from "./streaming/parser.js";
|
|
13
14
|
import { extractText, extractThinking, isAssistantText, isResult, isThinking } from "./streaming/types.js";
|
|
@@ -32,7 +33,12 @@ import { SkillResolver } from "./tools/skills/resolver.js";
|
|
|
32
33
|
import { autoRefreshModels } from "./models/sync.js";
|
|
33
34
|
import { readMcpConfigs, readSubagentNames } from "./mcp/config.js";
|
|
34
35
|
import { McpClientManager } from "./mcp/client-manager.js";
|
|
35
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
MCP_TOOL_PREFIX,
|
|
38
|
+
buildMcpToolHookEntries,
|
|
39
|
+
buildMcpToolDefinitions,
|
|
40
|
+
namespaceMcpTool,
|
|
41
|
+
} from "./mcp/tool-bridge.js";
|
|
36
42
|
import { createOpencodeClient } from "@opencode-ai/sdk";
|
|
37
43
|
import { ToolRegistry as CoreRegistry } from "./tools/core/registry.js";
|
|
38
44
|
import { LocalExecutor } from "./tools/executors/local.js";
|
|
@@ -57,42 +63,23 @@ import {
|
|
|
57
63
|
parseToolLoopMaxRepeat,
|
|
58
64
|
type ToolLoopGuard,
|
|
59
65
|
} from "./provider/tool-loop-guard.js";
|
|
60
|
-
import { resolveCursorAgentBinary } from "./utils/binary.js";
|
|
66
|
+
import { formatShellCommandForPlatform, resolveCursorAgentBinary } from "./utils/binary.js";
|
|
61
67
|
|
|
62
68
|
const log = createLogger("plugin");
|
|
63
69
|
|
|
64
|
-
// Debug log file for tool-loop investigation
|
|
65
|
-
const DEBUG_LOG_DIR = join(homedir(), ".config", "opencode", "logs");
|
|
66
|
-
const DEBUG_LOG_FILE = join(DEBUG_LOG_DIR, "tool-loop-debug.log");
|
|
67
|
-
|
|
68
|
-
function ensureDebugLogDir(): void {
|
|
69
|
-
try {
|
|
70
|
-
if (!existsSync(DEBUG_LOG_DIR)) {
|
|
71
|
-
mkdir(DEBUG_LOG_DIR, { recursive: true }).catch(() => {});
|
|
72
|
-
}
|
|
73
|
-
} catch {
|
|
74
|
-
// Ignore errors creating log directory
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function debugLogToFile(message: string, data: any): void {
|
|
79
|
-
try {
|
|
80
|
-
ensureDebugLogDir();
|
|
81
|
-
const timestamp = new Date().toISOString();
|
|
82
|
-
const logLine = `[${timestamp}] ${message}: ${JSON.stringify(data, null, 2)}\n`;
|
|
83
|
-
appendFileSync(DEBUG_LOG_FILE, logLine);
|
|
84
|
-
} catch {
|
|
85
|
-
// Ignore errors
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
70
|
interface McpToolSummary {
|
|
90
71
|
serverName: string;
|
|
91
72
|
toolName: string;
|
|
73
|
+
callName?: string;
|
|
92
74
|
description?: string;
|
|
93
75
|
params?: string[];
|
|
94
76
|
}
|
|
95
77
|
|
|
78
|
+
function getMcpToolDefinitionName(mcpToolDefs: any[], index: number): string | undefined {
|
|
79
|
+
const name = mcpToolDefs[index]?.function?.name;
|
|
80
|
+
return typeof name === "string" && name.length > 0 ? name : undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
96
83
|
export function buildAvailableToolsSystemMessage(
|
|
97
84
|
lastToolNames: string[],
|
|
98
85
|
lastToolMap: Array<{ id: string; name: string }>,
|
|
@@ -109,16 +96,23 @@ export function buildAvailableToolsSystemMessage(
|
|
|
109
96
|
}
|
|
110
97
|
|
|
111
98
|
if (mcpToolSummaries && mcpToolSummaries.length > 0) {
|
|
112
|
-
const
|
|
113
|
-
|
|
99
|
+
const summariesWithCallNames = mcpToolSummaries.map((summary, index) => ({
|
|
100
|
+
...summary,
|
|
101
|
+
callName: summary.callName
|
|
102
|
+
?? getMcpToolDefinitionName(mcpToolDefs, index)
|
|
103
|
+
?? namespaceMcpTool(summary.serverName, summary.toolName),
|
|
104
|
+
}));
|
|
105
|
+
|
|
106
|
+
const servers = new Map<string, Array<McpToolSummary & { callName: string }>>();
|
|
107
|
+
for (const s of summariesWithCallNames) {
|
|
114
108
|
const list = servers.get(s.serverName) ?? [];
|
|
115
109
|
list.push(s);
|
|
116
110
|
servers.set(s.serverName, list);
|
|
117
111
|
}
|
|
118
112
|
|
|
119
113
|
const lines: string[] = [
|
|
120
|
-
|
|
121
|
-
"
|
|
114
|
+
`MCP TOOLS — Use via direct tool calls (\`${MCP_TOOL_PREFIX}<server>__<tool>\`).`,
|
|
115
|
+
"These tools are exposed as first-class tool calls (e.g. mcp__filesystem__read_file).",
|
|
122
116
|
"",
|
|
123
117
|
];
|
|
124
118
|
|
|
@@ -126,12 +120,8 @@ export function buildAvailableToolsSystemMessage(
|
|
|
126
120
|
lines.push(`Server: ${server}`);
|
|
127
121
|
for (const t of tools) {
|
|
128
122
|
const paramHint = t.params?.length ? ` (params: ${t.params.join(", ")})` : "";
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (tools.length > 0) {
|
|
132
|
-
const ex = tools[0];
|
|
133
|
-
const exArgs = ex.params?.length ? ` '{"${ex.params[0]}":"..."}'` : "";
|
|
134
|
-
lines.push(` Example: mcptool call ${server} ${ex.toolName}${exArgs}`);
|
|
123
|
+
const sourceHint = t.callName === t.toolName ? "" : ` (server: ${t.serverName}; tool: ${t.toolName})`;
|
|
124
|
+
lines.push(` - ${t.callName}${paramHint}${t.description ? " — " + t.description : ""}${sourceHint}`);
|
|
135
125
|
}
|
|
136
126
|
lines.push("");
|
|
137
127
|
}
|
|
@@ -444,6 +434,7 @@ export function extractCompletionFromStream(output: string): {
|
|
|
444
434
|
let usage: OpenAiUsage | undefined;
|
|
445
435
|
let sawAssistantPartials = false;
|
|
446
436
|
let sawThinkingPartials = false;
|
|
437
|
+
const tracker = new MixedDeltaTracker();
|
|
447
438
|
|
|
448
439
|
for (const line of lines) {
|
|
449
440
|
const event = parseStreamJsonLine(line);
|
|
@@ -457,8 +448,8 @@ export function extractCompletionFromStream(output: string): {
|
|
|
457
448
|
|
|
458
449
|
const isPartial = typeof (event as any).timestamp_ms === "number";
|
|
459
450
|
if (isPartial) {
|
|
460
|
-
assistantText += text;
|
|
461
451
|
sawAssistantPartials = true;
|
|
452
|
+
assistantText += tracker.nextText(text);
|
|
462
453
|
} else if (!sawAssistantPartials) {
|
|
463
454
|
assistantText = text;
|
|
464
455
|
}
|
|
@@ -469,8 +460,8 @@ export function extractCompletionFromStream(output: string): {
|
|
|
469
460
|
if (thinking) {
|
|
470
461
|
const isPartial = typeof (event as any).timestamp_ms === "number";
|
|
471
462
|
if (isPartial) {
|
|
472
|
-
reasoningText += thinking;
|
|
473
463
|
sawThinkingPartials = true;
|
|
464
|
+
reasoningText += tracker.nextThinking(thinking);
|
|
474
465
|
} else if (!sawThinkingPartials) {
|
|
475
466
|
reasoningText = thinking;
|
|
476
467
|
}
|
|
@@ -705,8 +696,7 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
|
|
|
705
696
|
const stream = body?.stream === true;
|
|
706
697
|
const tools = Array.isArray(body?.tools) ? body.tools : [];
|
|
707
698
|
|
|
708
|
-
|
|
709
|
-
debugLogToFile("raw_request_body", {
|
|
699
|
+
log.debug("raw request body", {
|
|
710
700
|
model: body?.model,
|
|
711
701
|
cursorModel: body?.cursorModel,
|
|
712
702
|
stream,
|
|
@@ -1238,7 +1228,7 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
|
|
|
1238
1228
|
cmd.push("--force");
|
|
1239
1229
|
}
|
|
1240
1230
|
|
|
1241
|
-
const child = spawn(cmd[0], cmd.slice(1), {
|
|
1231
|
+
const child = spawn(formatShellCommandForPlatform(cmd[0]), cmd.slice(1), {
|
|
1242
1232
|
stdio: ["pipe", "pipe", "pipe"],
|
|
1243
1233
|
shell: process.platform === "win32",
|
|
1244
1234
|
});
|
|
@@ -1415,19 +1405,17 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
|
|
|
1415
1405
|
}
|
|
1416
1406
|
};
|
|
1417
1407
|
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1408
|
+
const chunkQueue: Buffer[] = [];
|
|
1409
|
+
let draining = false;
|
|
1410
|
+
let childClosed = false;
|
|
1411
|
+
let childCloseHandled = false;
|
|
1412
|
+
let childExitCode: number | null = null;
|
|
1413
|
+
|
|
1414
|
+
const processLines = async (lines: string[]) => {
|
|
1415
|
+
for (const line of lines) {
|
|
1416
|
+
if (streamTerminated || res.writableEnded) break;
|
|
1427
1417
|
const event = parseStreamJsonLine(line);
|
|
1428
|
-
if (!event)
|
|
1429
|
-
continue;
|
|
1430
|
-
}
|
|
1418
|
+
if (!event) continue;
|
|
1431
1419
|
|
|
1432
1420
|
if (isResult(event)) {
|
|
1433
1421
|
usage = extractOpenAiUsageFromResult(event) ?? usage;
|
|
@@ -1469,156 +1457,104 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
|
|
|
1469
1457
|
if (!result.terminate.silent) {
|
|
1470
1458
|
emitTerminalAssistantErrorAndTerminate(result.terminate.message);
|
|
1471
1459
|
} else {
|
|
1472
|
-
// Silent termination: just end the stream without an error message
|
|
1473
1460
|
streamTerminated = true;
|
|
1474
1461
|
try { child.kill(); } catch { /* ignore */ }
|
|
1475
1462
|
}
|
|
1476
1463
|
break;
|
|
1477
1464
|
}
|
|
1478
|
-
if (result.intercepted)
|
|
1479
|
-
|
|
1480
|
-
}
|
|
1481
|
-
if (result.skipConverter) {
|
|
1482
|
-
continue;
|
|
1483
|
-
}
|
|
1465
|
+
if (result.intercepted) break;
|
|
1466
|
+
if (result.skipConverter) continue;
|
|
1484
1467
|
}
|
|
1485
1468
|
|
|
1486
|
-
if (streamTerminated || res.writableEnded)
|
|
1487
|
-
break;
|
|
1488
|
-
}
|
|
1469
|
+
if (streamTerminated || res.writableEnded) break;
|
|
1489
1470
|
for (const sse of converter.handleEvent(event)) {
|
|
1490
1471
|
res.write(sse);
|
|
1491
1472
|
}
|
|
1492
1473
|
}
|
|
1493
|
-
}
|
|
1494
|
-
|
|
1495
|
-
child.on("close", async (code) => {
|
|
1496
|
-
if (streamTerminated || res.writableEnded) {
|
|
1497
|
-
return;
|
|
1498
|
-
}
|
|
1499
|
-
for (const line of lineBuffer.flush()) {
|
|
1500
|
-
if (streamTerminated || res.writableEnded) {
|
|
1501
|
-
break;
|
|
1502
|
-
}
|
|
1503
|
-
const event = parseStreamJsonLine(line);
|
|
1504
|
-
if (!event) {
|
|
1505
|
-
continue;
|
|
1506
|
-
}
|
|
1474
|
+
};
|
|
1507
1475
|
|
|
1508
|
-
|
|
1509
|
-
|
|
1476
|
+
const drainQueue = async () => {
|
|
1477
|
+
if (draining) return;
|
|
1478
|
+
draining = true;
|
|
1479
|
+
try {
|
|
1480
|
+
while (chunkQueue.length > 0) {
|
|
1481
|
+
if (streamTerminated || res.writableEnded) break;
|
|
1482
|
+
const chunk = chunkQueue.shift()!;
|
|
1483
|
+
if (!firstTokenReceived) { perf.mark("first-token"); firstTokenReceived = true; }
|
|
1484
|
+
await processLines(lineBuffer.push(chunk));
|
|
1510
1485
|
}
|
|
1511
1486
|
|
|
1512
|
-
if (
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
toolSessionId,
|
|
1524
|
-
shouldEmitToolUpdates: SHOULD_EMIT_TOOL_UPDATES,
|
|
1525
|
-
proxyExecuteToolCalls: PROXY_EXECUTE_TOOL_CALLS,
|
|
1526
|
-
suppressConverterToolEvents: SUPPRESS_CONVERTER_TOOL_EVENTS,
|
|
1527
|
-
toolRouter,
|
|
1528
|
-
responseMeta: { id, created, model },
|
|
1529
|
-
passThroughTracker,
|
|
1530
|
-
onToolUpdate: (update) => {
|
|
1531
|
-
res.write(formatToolUpdateEvent(update));
|
|
1532
|
-
},
|
|
1533
|
-
onToolResult: (toolResult) => {
|
|
1534
|
-
res.write(`data: ${JSON.stringify(toolResult)}\n\n`);
|
|
1535
|
-
},
|
|
1536
|
-
onInterceptedToolCall: (toolCall) => {
|
|
1537
|
-
emitToolCallAndTerminate(toolCall);
|
|
1538
|
-
},
|
|
1539
|
-
onFallbackToLegacy: (error) => {
|
|
1540
|
-
boundaryContext.activateLegacyFallback("handleToolLoopEvent.close", error);
|
|
1541
|
-
},
|
|
1487
|
+
if (childClosed && !childCloseHandled && !streamTerminated && !res.writableEnded) {
|
|
1488
|
+
childCloseHandled = true;
|
|
1489
|
+
await processLines(lineBuffer.flush());
|
|
1490
|
+
if (streamTerminated || res.writableEnded) return;
|
|
1491
|
+
|
|
1492
|
+
perf.mark("request:done");
|
|
1493
|
+
perf.summarize();
|
|
1494
|
+
const stderrText = Buffer.concat(stderrChunks).toString().trim();
|
|
1495
|
+
log.debug("cursor-agent completed (node stream)", {
|
|
1496
|
+
code: childExitCode,
|
|
1497
|
+
stderrChars: stderrText.length,
|
|
1542
1498
|
});
|
|
1543
|
-
if (
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
}
|
|
1551
|
-
|
|
1499
|
+
if (childExitCode !== 0) {
|
|
1500
|
+
const errSource =
|
|
1501
|
+
stderrText
|
|
1502
|
+
|| `cursor-agent exited with code ${String(childExitCode ?? "unknown")} and no output`;
|
|
1503
|
+
const parsed = parseAgentError(errSource);
|
|
1504
|
+
const msg = formatErrorForUser(parsed);
|
|
1505
|
+
const errChunk = createChatCompletionChunk(id, created, model, msg, true);
|
|
1506
|
+
res.write(`data: ${JSON.stringify(errChunk)}\n\n`);
|
|
1507
|
+
res.write(formatSseDone());
|
|
1508
|
+
streamTerminated = true;
|
|
1509
|
+
res.end();
|
|
1510
|
+
return;
|
|
1552
1511
|
}
|
|
1553
|
-
|
|
1554
|
-
|
|
1512
|
+
|
|
1513
|
+
const passThroughSummary = passThroughTracker.getSummary();
|
|
1514
|
+
if (passThroughSummary.hasActivity) {
|
|
1515
|
+
await toastService.showPassThroughSummary(passThroughSummary.tools);
|
|
1555
1516
|
}
|
|
1556
|
-
if (
|
|
1557
|
-
|
|
1517
|
+
if (passThroughSummary.errors.length > 0) {
|
|
1518
|
+
await toastService.showErrorSummary(passThroughSummary.errors);
|
|
1558
1519
|
}
|
|
1559
|
-
}
|
|
1560
1520
|
|
|
1561
|
-
|
|
1562
|
-
|
|
1521
|
+
const doneChunk = {
|
|
1522
|
+
id,
|
|
1523
|
+
object: "chat.completion.chunk",
|
|
1524
|
+
created,
|
|
1525
|
+
model,
|
|
1526
|
+
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
|
|
1527
|
+
};
|
|
1528
|
+
res.write(`data: ${JSON.stringify(doneChunk)}\n\n`);
|
|
1529
|
+
if (usage) {
|
|
1530
|
+
const usageChunk = createChatCompletionUsageChunk(id, created, model, usage);
|
|
1531
|
+
res.write(`data: ${JSON.stringify(usageChunk)}\n\n`);
|
|
1532
|
+
}
|
|
1533
|
+
res.write(formatSseDone());
|
|
1534
|
+
streamTerminated = true;
|
|
1535
|
+
res.end();
|
|
1563
1536
|
}
|
|
1564
|
-
|
|
1565
|
-
|
|
1537
|
+
} finally {
|
|
1538
|
+
draining = false;
|
|
1539
|
+
if (
|
|
1540
|
+
!streamTerminated
|
|
1541
|
+
&& !res.writableEnded
|
|
1542
|
+
&& (chunkQueue.length > 0 || (childClosed && !childCloseHandled))
|
|
1543
|
+
) {
|
|
1544
|
+
drainQueue();
|
|
1566
1545
|
}
|
|
1567
1546
|
}
|
|
1568
|
-
|
|
1569
|
-
return;
|
|
1570
|
-
}
|
|
1571
|
-
|
|
1572
|
-
perf.mark("request:done");
|
|
1573
|
-
perf.summarize();
|
|
1574
|
-
const stderrText = Buffer.concat(stderrChunks).toString().trim();
|
|
1575
|
-
log.debug("cursor-agent completed (node stream)", {
|
|
1576
|
-
code,
|
|
1577
|
-
stderrChars: stderrText.length,
|
|
1578
|
-
});
|
|
1579
|
-
if (code !== 0) {
|
|
1580
|
-
const errSource =
|
|
1581
|
-
stderrText
|
|
1582
|
-
|| `cursor-agent exited with code ${String(code ?? "unknown")} and no output`;
|
|
1583
|
-
const parsed = parseAgentError(errSource);
|
|
1584
|
-
const msg = formatErrorForUser(parsed);
|
|
1585
|
-
const errChunk = createChatCompletionChunk(id, created, model, msg, true);
|
|
1586
|
-
res.write(`data: ${JSON.stringify(errChunk)}\n\n`);
|
|
1587
|
-
res.write(formatSseDone());
|
|
1588
|
-
streamTerminated = true;
|
|
1589
|
-
res.end();
|
|
1590
|
-
return;
|
|
1591
|
-
}
|
|
1547
|
+
};
|
|
1592
1548
|
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
}
|
|
1598
|
-
if (passThroughSummary.errors.length > 0) {
|
|
1599
|
-
await toastService.showErrorSummary(passThroughSummary.errors);
|
|
1600
|
-
}
|
|
1549
|
+
child.stdout.on("data", (chunk) => {
|
|
1550
|
+
chunkQueue.push(Buffer.from(chunk));
|
|
1551
|
+
drainQueue();
|
|
1552
|
+
});
|
|
1601
1553
|
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
model,
|
|
1607
|
-
choices: [
|
|
1608
|
-
{
|
|
1609
|
-
index: 0,
|
|
1610
|
-
delta: {},
|
|
1611
|
-
finish_reason: "stop",
|
|
1612
|
-
},
|
|
1613
|
-
],
|
|
1614
|
-
};
|
|
1615
|
-
res.write(`data: ${JSON.stringify(doneChunk)}\n\n`);
|
|
1616
|
-
if (usage) {
|
|
1617
|
-
const usageChunk = createChatCompletionUsageChunk(id, created, model, usage);
|
|
1618
|
-
res.write(`data: ${JSON.stringify(usageChunk)}\n\n`);
|
|
1619
|
-
}
|
|
1620
|
-
res.write(formatSseDone());
|
|
1621
|
-
res.end();
|
|
1554
|
+
child.on("close", (code) => {
|
|
1555
|
+
childClosed = true;
|
|
1556
|
+
childExitCode = code;
|
|
1557
|
+
drainQueue();
|
|
1622
1558
|
});
|
|
1623
1559
|
}
|
|
1624
1560
|
} catch (error) {
|
|
@@ -1955,6 +1891,7 @@ export const CursorPlugin: Plugin = async ({ $, directory, worktree, client, ser
|
|
|
1955
1891
|
mcpToolSummaries = tools.map((t) => ({
|
|
1956
1892
|
serverName: t.serverName,
|
|
1957
1893
|
toolName: t.name,
|
|
1894
|
+
callName: namespaceMcpTool(t.serverName, t.name),
|
|
1958
1895
|
description: t.description,
|
|
1959
1896
|
params: t.inputSchema
|
|
1960
1897
|
? Object.keys((t.inputSchema as any).properties ?? {})
|
|
@@ -3,7 +3,11 @@ import { extractOpenAiToolCall, type OpenAiToolCall, type ToolCallExtractionResu
|
|
|
3
3
|
import type { StreamJsonToolCallEvent } from "../streaming/types.js";
|
|
4
4
|
import type { ToolRouter } from "../tools/router.js";
|
|
5
5
|
import { createLogger } from "../utils/logger.js";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
applyToolSchemaCompat,
|
|
8
|
+
tryRerouteEditToWrite,
|
|
9
|
+
type ToolSchemaValidationResult,
|
|
10
|
+
} from "./tool-schema-compat.js";
|
|
7
11
|
import type { ToolLoopGuard } from "./tool-loop-guard.js";
|
|
8
12
|
import type { ProviderBoundaryMode, ToolLoopMode } from "./boundary.js";
|
|
9
13
|
import type { ProviderBoundary } from "./boundary.js";
|
|
@@ -190,6 +194,20 @@ export async function handleToolLoopEventLegacy(
|
|
|
190
194
|
return { intercepted: false, skipConverter: true, terminate: validationTermination };
|
|
191
195
|
}
|
|
192
196
|
|
|
197
|
+
const reroutedWrite = tryRerouteEditToWrite(
|
|
198
|
+
normalizedToolCall,
|
|
199
|
+
compat,
|
|
200
|
+
allowedToolNames,
|
|
201
|
+
toolSchemaMap,
|
|
202
|
+
);
|
|
203
|
+
if (reroutedWrite) {
|
|
204
|
+
log.debug("Rerouting malformed edit call to write (legacy)", {
|
|
205
|
+
missing: compat.validation.missing,
|
|
206
|
+
});
|
|
207
|
+
await onInterceptedToolCall(reroutedWrite);
|
|
208
|
+
return { intercepted: true, skipConverter: true };
|
|
209
|
+
}
|
|
210
|
+
|
|
193
211
|
if (shouldEmitNonFatalSchemaValidationHint(normalizedToolCall, compat.validation)) {
|
|
194
212
|
const hintChunk = createNonFatalSchemaValidationHintChunk(
|
|
195
213
|
responseMeta,
|
|
@@ -383,6 +401,23 @@ export async function handleToolLoopEventV1(
|
|
|
383
401
|
terminate: createSchemaValidationTermination(normalizedToolCall, compat.validation),
|
|
384
402
|
};
|
|
385
403
|
}
|
|
404
|
+
const reroutedWrite = tryRerouteEditToWrite(
|
|
405
|
+
normalizedToolCall,
|
|
406
|
+
compat,
|
|
407
|
+
allowedToolNames,
|
|
408
|
+
toolSchemaMap,
|
|
409
|
+
);
|
|
410
|
+
if (reroutedWrite) {
|
|
411
|
+
log.debug("Rerouting malformed edit call to write", {
|
|
412
|
+
missing: compat.validation.missing,
|
|
413
|
+
typeErrors: compat.validation.typeErrors,
|
|
414
|
+
});
|
|
415
|
+
await onInterceptedToolCall(reroutedWrite);
|
|
416
|
+
return {
|
|
417
|
+
intercepted: true,
|
|
418
|
+
skipConverter: true,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
386
421
|
if (
|
|
387
422
|
schemaValidationFailureMode === "pass_through"
|
|
388
423
|
&& shouldEmitNonFatalSchemaValidationHint(normalizedToolCall, compat.validation)
|
|
@@ -668,11 +703,11 @@ function createNonFatalSchemaValidationHintChunk(
|
|
|
668
703
|
validation: ToolSchemaValidationResult,
|
|
669
704
|
): NonFatalSchemaValidationResultChunk {
|
|
670
705
|
const termination = createSchemaValidationTermination(toolCall, validation);
|
|
671
|
-
const
|
|
672
|
-
|
|
673
|
-
|
|
706
|
+
const fallbackHint =
|
|
707
|
+
"Use write for full-file replacement, or provide path, old_string, and new_string for edit.";
|
|
708
|
+
const suffix = termination.repairHint ? "" : ` ${fallbackHint}`;
|
|
674
709
|
const content =
|
|
675
|
-
`Skipped malformed tool call "${toolCall.function.name}": ${termination.message}
|
|
710
|
+
`Skipped malformed tool call "${toolCall.function.name}": ${termination.message}${suffix}`.trim();
|
|
676
711
|
return {
|
|
677
712
|
id: meta.id,
|
|
678
713
|
object: "chat.completion.chunk",
|