@rama_nigg/open-cursor 2.4.5 → 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/index.js +164 -83
- package/dist/plugin-entry.js +156 -75
- package/package.json +1 -1
- package/src/mcp/tool-bridge.ts +4 -2
- package/src/plugin.ts +28 -39
- 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/tools/defaults.ts +0 -4
package/dist/plugin-entry.js
CHANGED
|
@@ -1066,27 +1066,6 @@ var init_perf = __esm(() => {
|
|
|
1066
1066
|
});
|
|
1067
1067
|
|
|
1068
1068
|
// src/proxy/prompt-builder.ts
|
|
1069
|
-
import { appendFileSync as appendFileSync2, existsSync as existsSync4, mkdirSync as mkdirSync2 } from "node:fs";
|
|
1070
|
-
import { homedir as homedir4 } from "node:os";
|
|
1071
|
-
import { join as join5 } from "node:path";
|
|
1072
|
-
function ensureLogDir2() {
|
|
1073
|
-
try {
|
|
1074
|
-
if (!existsSync4(DEBUG_LOG_DIR)) {
|
|
1075
|
-
mkdirSync2(DEBUG_LOG_DIR, { recursive: true });
|
|
1076
|
-
}
|
|
1077
|
-
} catch {}
|
|
1078
|
-
}
|
|
1079
|
-
function debugLogToFile(message, data) {
|
|
1080
|
-
try {
|
|
1081
|
-
ensureLogDir2();
|
|
1082
|
-
const timestamp = new Date().toISOString();
|
|
1083
|
-
const logLine = `[${timestamp}] ${message}: ${JSON.stringify(data, null, 2)}
|
|
1084
|
-
`;
|
|
1085
|
-
appendFileSync2(DEBUG_LOG_FILE, logLine);
|
|
1086
|
-
} catch (err) {
|
|
1087
|
-
log5.debug(message, data);
|
|
1088
|
-
}
|
|
1089
|
-
}
|
|
1090
1069
|
function buildPromptFromMessages(messages, tools, subagentNames = []) {
|
|
1091
1070
|
const messageSummary = messages.map((m, i) => {
|
|
1092
1071
|
const role = m?.role ?? "?";
|
|
@@ -1100,7 +1079,7 @@ function buildPromptFromMessages(messages, tools, subagentNames = []) {
|
|
|
1100
1079
|
const assistantWithToolCalls = messages.filter((m) => m?.role === "assistant" && Array.isArray(m?.tool_calls) && m.tool_calls.length > 0);
|
|
1101
1080
|
const assistantEmpty = messages.filter((m) => m?.role === "assistant" && (!m?.tool_calls || m.tool_calls.length === 0) && (!m?.content || m.content === "" || m.content === null));
|
|
1102
1081
|
const toolResults = messages.filter((m) => m?.role === "tool");
|
|
1103
|
-
|
|
1082
|
+
log5.debug("buildPromptFromMessages", {
|
|
1104
1083
|
totalMessages: messages.length,
|
|
1105
1084
|
totalTools: tools.length,
|
|
1106
1085
|
messageSummary,
|
|
@@ -1195,22 +1174,20 @@ ${toolDescs}`);
|
|
|
1195
1174
|
const finalPrompt = lines.join(`
|
|
1196
1175
|
|
|
1197
1176
|
`);
|
|
1198
|
-
|
|
1177
|
+
log5.debug("buildPromptFromMessages: final prompt", {
|
|
1199
1178
|
lineCount: lines.length,
|
|
1200
1179
|
promptLength: finalPrompt.length,
|
|
1201
1180
|
promptPreview: finalPrompt.slice(0, 500),
|
|
1202
1181
|
hasToolResultFormat: finalPrompt.includes("TOOL_RESULT"),
|
|
1203
1182
|
hasAssistantToolCallFormat: finalPrompt.includes("tool_call(id:"),
|
|
1204
|
-
hasCompletionSignal: finalPrompt.includes("
|
|
1183
|
+
hasCompletionSignal: finalPrompt.includes("The above tool calls have been executed")
|
|
1205
1184
|
});
|
|
1206
1185
|
return finalPrompt;
|
|
1207
1186
|
}
|
|
1208
|
-
var log5
|
|
1187
|
+
var log5;
|
|
1209
1188
|
var init_prompt_builder = __esm(() => {
|
|
1210
1189
|
init_logger();
|
|
1211
1190
|
log5 = createLogger("proxy:prompt-builder");
|
|
1212
|
-
DEBUG_LOG_DIR = join5(homedir4(), ".config", "opencode", "logs");
|
|
1213
|
-
DEBUG_LOG_FILE = join5(DEBUG_LOG_DIR, "tool-loop-debug.log");
|
|
1214
1191
|
});
|
|
1215
1192
|
|
|
1216
1193
|
// src/proxy/tool-loop.ts
|
|
@@ -11999,7 +11976,7 @@ function buildMcpToolDefinitions(tools) {
|
|
|
11999
11976
|
function namespaceMcpTool(serverName, toolName) {
|
|
12000
11977
|
const sanitizedServer = serverName.replace(/[^a-zA-Z0-9]/g, "_");
|
|
12001
11978
|
const sanitizedTool = toolName.replace(/[^a-zA-Z0-9]/g, "_");
|
|
12002
|
-
return
|
|
11979
|
+
return `${MCP_TOOL_PREFIX}${sanitizedServer}__${sanitizedTool}`;
|
|
12003
11980
|
}
|
|
12004
11981
|
function mcpSchemaToZod(inputSchema, z2) {
|
|
12005
11982
|
if (!inputSchema || typeof inputSchema !== "object") {
|
|
@@ -12041,7 +12018,7 @@ function mcpSchemaToZod(inputSchema, z2) {
|
|
|
12041
12018
|
}
|
|
12042
12019
|
return shape;
|
|
12043
12020
|
}
|
|
12044
|
-
var log13;
|
|
12021
|
+
var log13, MCP_TOOL_PREFIX = "mcp__";
|
|
12045
12022
|
var init_tool_bridge = __esm(() => {
|
|
12046
12023
|
init_logger();
|
|
12047
12024
|
log13 = createLogger("mcp:tool-bridge");
|
|
@@ -14046,9 +14023,6 @@ function resolveEditArguments(args) {
|
|
|
14046
14023
|
newString = fallbackContent;
|
|
14047
14024
|
}
|
|
14048
14025
|
}
|
|
14049
|
-
if (oldString === undefined && newString !== undefined) {
|
|
14050
|
-
oldString = "";
|
|
14051
|
-
}
|
|
14052
14026
|
return {
|
|
14053
14027
|
path: path2,
|
|
14054
14028
|
old_string: oldString,
|
|
@@ -14455,7 +14429,10 @@ function applyToolSchemaCompat(toolCall, toolSchemaMap) {
|
|
|
14455
14429
|
const toolSpecificArgs = normalizeToolSpecificArgs(toolCall.function.name, normalizedArgs);
|
|
14456
14430
|
const schema = toolSchemaMap.get(toolCall.function.name);
|
|
14457
14431
|
const sanitization = sanitizeArgumentsForSchema(toolSpecificArgs, schema);
|
|
14458
|
-
const validation = validateToolArguments(toolCall.function.name, sanitization.args, schema, sanitization.unexpected
|
|
14432
|
+
const validation = validateToolArguments(toolCall.function.name, sanitization.args, schema, sanitization.unexpected, {
|
|
14433
|
+
originalArgs: parsedArgs,
|
|
14434
|
+
writeSchema: toolSchemaMap.get("write")
|
|
14435
|
+
});
|
|
14459
14436
|
const normalizedToolCall = {
|
|
14460
14437
|
...toolCall,
|
|
14461
14438
|
function: {
|
|
@@ -14466,12 +14443,59 @@ function applyToolSchemaCompat(toolCall, toolSchemaMap) {
|
|
|
14466
14443
|
return {
|
|
14467
14444
|
toolCall: normalizedToolCall,
|
|
14468
14445
|
normalizedArgs: sanitization.args,
|
|
14446
|
+
originalArgs: parsedArgs,
|
|
14469
14447
|
originalArgKeys,
|
|
14470
14448
|
normalizedArgKeys: Object.keys(sanitization.args),
|
|
14471
14449
|
collisionKeys,
|
|
14472
14450
|
validation
|
|
14473
14451
|
};
|
|
14474
14452
|
}
|
|
14453
|
+
function isFullFileShapedEditValidationFailure(toolName, args, validation, originalArgs, writeSchema) {
|
|
14454
|
+
if (toolName.toLowerCase() !== "edit" || validation.ok) {
|
|
14455
|
+
return false;
|
|
14456
|
+
}
|
|
14457
|
+
return buildEditFullFileHint(args, validation.missing, validation.typeErrors, {
|
|
14458
|
+
originalArgs,
|
|
14459
|
+
writeSchema
|
|
14460
|
+
}) !== null;
|
|
14461
|
+
}
|
|
14462
|
+
function buildWriteArguments(filePath, content, writeSchema) {
|
|
14463
|
+
if (!isRecord3(writeSchema)) {
|
|
14464
|
+
return { path: filePath, content };
|
|
14465
|
+
}
|
|
14466
|
+
const required = Array.isArray(writeSchema.required) ? writeSchema.required.filter((value) => typeof value === "string") : [];
|
|
14467
|
+
if (required.includes("filePath")) {
|
|
14468
|
+
return { filePath, content };
|
|
14469
|
+
}
|
|
14470
|
+
return { path: filePath, content };
|
|
14471
|
+
}
|
|
14472
|
+
function tryRerouteEditToWrite(toolCall, compat, allowedToolNames, toolSchemaMap) {
|
|
14473
|
+
if (toolCall.function.name.toLowerCase() !== "edit") {
|
|
14474
|
+
return null;
|
|
14475
|
+
}
|
|
14476
|
+
if (!allowedToolNames.has("write") || !toolSchemaMap.has("write")) {
|
|
14477
|
+
return null;
|
|
14478
|
+
}
|
|
14479
|
+
const writeSchema = toolSchemaMap.get("write");
|
|
14480
|
+
if (!isFullFileShapedEditValidationFailure(toolCall.function.name, compat.normalizedArgs, compat.validation, compat.originalArgs, writeSchema)) {
|
|
14481
|
+
return null;
|
|
14482
|
+
}
|
|
14483
|
+
const filePath = typeof compat.normalizedArgs.path === "string" && compat.normalizedArgs.path.length > 0 ? compat.normalizedArgs.path : typeof compat.normalizedArgs.filePath === "string" && compat.normalizedArgs.filePath.length > 0 ? compat.normalizedArgs.filePath : null;
|
|
14484
|
+
if (!filePath) {
|
|
14485
|
+
return null;
|
|
14486
|
+
}
|
|
14487
|
+
const content = typeof compat.normalizedArgs.new_string === "string" ? compat.normalizedArgs.new_string : typeof compat.normalizedArgs.newString === "string" ? compat.normalizedArgs.newString : typeof compat.normalizedArgs.content === "string" ? compat.normalizedArgs.content : null;
|
|
14488
|
+
if (content === null) {
|
|
14489
|
+
return null;
|
|
14490
|
+
}
|
|
14491
|
+
return {
|
|
14492
|
+
...toolCall,
|
|
14493
|
+
function: {
|
|
14494
|
+
name: "write",
|
|
14495
|
+
arguments: JSON.stringify(buildWriteArguments(filePath, content, writeSchema))
|
|
14496
|
+
}
|
|
14497
|
+
};
|
|
14498
|
+
}
|
|
14475
14499
|
function parseArguments(rawArguments) {
|
|
14476
14500
|
try {
|
|
14477
14501
|
const parsed = JSON.parse(rawArguments);
|
|
@@ -14654,7 +14678,7 @@ function sanitizeArgumentsForSchema(args, schema) {
|
|
|
14654
14678
|
}
|
|
14655
14679
|
return { args: sanitized, unexpected };
|
|
14656
14680
|
}
|
|
14657
|
-
function validateToolArguments(toolName, args, schema, unexpected) {
|
|
14681
|
+
function validateToolArguments(toolName, args, schema, unexpected, context = {}) {
|
|
14658
14682
|
if (!isRecord3(schema)) {
|
|
14659
14683
|
return {
|
|
14660
14684
|
hasSchema: false,
|
|
@@ -14690,10 +14714,59 @@ function validateToolArguments(toolName, args, schema, unexpected) {
|
|
|
14690
14714
|
missing,
|
|
14691
14715
|
unexpected,
|
|
14692
14716
|
typeErrors,
|
|
14693
|
-
repairHint: ok ? undefined : buildRepairHint(toolName, missing, unexpected, typeErrors)
|
|
14717
|
+
repairHint: ok ? undefined : buildRepairHint(toolName, args, missing, unexpected, typeErrors, context)
|
|
14694
14718
|
};
|
|
14695
14719
|
}
|
|
14696
|
-
function
|
|
14720
|
+
function hadOldStringPropertyInPayload(args) {
|
|
14721
|
+
for (const key of Object.keys(args)) {
|
|
14722
|
+
const token = key.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
14723
|
+
if (token === "oldstring") {
|
|
14724
|
+
return true;
|
|
14725
|
+
}
|
|
14726
|
+
}
|
|
14727
|
+
return false;
|
|
14728
|
+
}
|
|
14729
|
+
function hasEditFilePath(args) {
|
|
14730
|
+
const pathValue = args.path ?? args.filePath;
|
|
14731
|
+
return typeof pathValue === "string" && pathValue.trim().length > 0;
|
|
14732
|
+
}
|
|
14733
|
+
function hasEditBody(args) {
|
|
14734
|
+
const body = args.new_string ?? args.newString ?? args.content;
|
|
14735
|
+
return typeof body === "string" && body.length > 0;
|
|
14736
|
+
}
|
|
14737
|
+
function writeToolExample(writeSchema) {
|
|
14738
|
+
if (!isRecord3(writeSchema)) {
|
|
14739
|
+
return "write with path and content";
|
|
14740
|
+
}
|
|
14741
|
+
const required = Array.isArray(writeSchema.required) ? writeSchema.required.filter((value) => typeof value === "string") : [];
|
|
14742
|
+
if (required.includes("filePath")) {
|
|
14743
|
+
return "write with filePath and content";
|
|
14744
|
+
}
|
|
14745
|
+
return "write with path and content";
|
|
14746
|
+
}
|
|
14747
|
+
function buildEditFullFileHint(args, missing, typeErrors, context) {
|
|
14748
|
+
if (typeErrors.length > 0) {
|
|
14749
|
+
return null;
|
|
14750
|
+
}
|
|
14751
|
+
const missingOldStringOnly = (missing.includes("old_string") || missing.includes("oldString")) && missing.every((key) => key === "old_string" || key === "oldString");
|
|
14752
|
+
if (!missingOldStringOnly) {
|
|
14753
|
+
return null;
|
|
14754
|
+
}
|
|
14755
|
+
const originalArgs = context.originalArgs ?? {};
|
|
14756
|
+
if (hadOldStringPropertyInPayload(originalArgs)) {
|
|
14757
|
+
return null;
|
|
14758
|
+
}
|
|
14759
|
+
if (!hasEditFilePath(args) || !hasEditBody(args)) {
|
|
14760
|
+
return null;
|
|
14761
|
+
}
|
|
14762
|
+
const example = writeToolExample(context.writeSchema);
|
|
14763
|
+
return `For a full file body, use ${example} instead of edit without old_string`;
|
|
14764
|
+
}
|
|
14765
|
+
function buildRepairHint(toolName, args, missing, unexpected, typeErrors, context = {}) {
|
|
14766
|
+
const fullFileHint = toolName.toLowerCase() === "edit" ? buildEditFullFileHint(args, missing, typeErrors, context) : null;
|
|
14767
|
+
if (fullFileHint) {
|
|
14768
|
+
return fullFileHint;
|
|
14769
|
+
}
|
|
14697
14770
|
const hints = [];
|
|
14698
14771
|
if (missing.length > 0) {
|
|
14699
14772
|
hints.push(`missing required: ${missing.join(", ")}`);
|
|
@@ -14704,7 +14777,7 @@ function buildRepairHint(toolName, missing, unexpected, typeErrors) {
|
|
|
14704
14777
|
if (typeErrors.length > 0) {
|
|
14705
14778
|
hints.push(`fix type errors: ${typeErrors.join("; ")}`);
|
|
14706
14779
|
}
|
|
14707
|
-
if (toolName.toLowerCase() === "edit" && (missing.includes("old_string") || missing.includes("new_string"))) {
|
|
14780
|
+
if (toolName.toLowerCase() === "edit" && (missing.includes("old_string") || missing.includes("oldString") || missing.includes("new_string") || missing.includes("newString"))) {
|
|
14708
14781
|
hints.push("edit requires path, old_string, and new_string");
|
|
14709
14782
|
}
|
|
14710
14783
|
return hints.join(" | ");
|
|
@@ -14891,6 +14964,14 @@ async function handleToolLoopEventLegacy(options) {
|
|
|
14891
14964
|
}
|
|
14892
14965
|
return { intercepted: false, skipConverter: true, terminate: validationTermination };
|
|
14893
14966
|
}
|
|
14967
|
+
const reroutedWrite = tryRerouteEditToWrite(normalizedToolCall, compat, allowedToolNames, toolSchemaMap);
|
|
14968
|
+
if (reroutedWrite) {
|
|
14969
|
+
log18.debug("Rerouting malformed edit call to write (legacy)", {
|
|
14970
|
+
missing: compat.validation.missing
|
|
14971
|
+
});
|
|
14972
|
+
await onInterceptedToolCall(reroutedWrite);
|
|
14973
|
+
return { intercepted: true, skipConverter: true };
|
|
14974
|
+
}
|
|
14894
14975
|
if (shouldEmitNonFatalSchemaValidationHint(normalizedToolCall, compat.validation)) {
|
|
14895
14976
|
const hintChunk = createNonFatalSchemaValidationHintChunk(responseMeta, normalizedToolCall, compat.validation);
|
|
14896
14977
|
log18.debug("Emitting non-fatal schema validation hint in legacy and skipping malformed tool execution", {
|
|
@@ -15042,6 +15123,18 @@ async function handleToolLoopEventV1(options) {
|
|
|
15042
15123
|
terminate: createSchemaValidationTermination(normalizedToolCall, compat.validation)
|
|
15043
15124
|
};
|
|
15044
15125
|
}
|
|
15126
|
+
const reroutedWrite = tryRerouteEditToWrite(normalizedToolCall, compat, allowedToolNames, toolSchemaMap);
|
|
15127
|
+
if (reroutedWrite) {
|
|
15128
|
+
log18.debug("Rerouting malformed edit call to write", {
|
|
15129
|
+
missing: compat.validation.missing,
|
|
15130
|
+
typeErrors: compat.validation.typeErrors
|
|
15131
|
+
});
|
|
15132
|
+
await onInterceptedToolCall(reroutedWrite);
|
|
15133
|
+
return {
|
|
15134
|
+
intercepted: true,
|
|
15135
|
+
skipConverter: true
|
|
15136
|
+
};
|
|
15137
|
+
}
|
|
15045
15138
|
if (schemaValidationFailureMode === "pass_through" && shouldEmitNonFatalSchemaValidationHint(normalizedToolCall, compat.validation)) {
|
|
15046
15139
|
const hintChunk = createNonFatalSchemaValidationHintChunk(responseMeta, normalizedToolCall, compat.validation);
|
|
15047
15140
|
log18.debug("Emitting non-fatal schema validation hint and skipping malformed tool execution", {
|
|
@@ -15252,8 +15345,9 @@ function shouldTerminateOnSchemaValidation(toolCall, validation) {
|
|
|
15252
15345
|
}
|
|
15253
15346
|
function createNonFatalSchemaValidationHintChunk(meta, toolCall, validation) {
|
|
15254
15347
|
const termination = createSchemaValidationTermination(toolCall, validation);
|
|
15255
|
-
const
|
|
15256
|
-
const
|
|
15348
|
+
const fallbackHint = "Use write for full-file replacement, or provide path, old_string, and new_string for edit.";
|
|
15349
|
+
const suffix = termination.repairHint ? "" : ` ${fallbackHint}`;
|
|
15350
|
+
const content = `Skipped malformed tool call "${toolCall.function.name}": ${termination.message}${suffix}`.trim();
|
|
15257
15351
|
return {
|
|
15258
15352
|
id: meta.id,
|
|
15259
15353
|
object: "chat.completion.chunk",
|
|
@@ -15894,25 +15988,13 @@ __export(exports_plugin, {
|
|
|
15894
15988
|
CursorPlugin: () => CursorPlugin
|
|
15895
15989
|
});
|
|
15896
15990
|
import { tool as tool2 } from "@opencode-ai/plugin";
|
|
15897
|
-
import {
|
|
15991
|
+
import { realpathSync } from "fs";
|
|
15898
15992
|
import { mkdir } from "fs/promises";
|
|
15899
|
-
import { homedir as
|
|
15900
|
-
import { isAbsolute, join as
|
|
15901
|
-
function
|
|
15902
|
-
|
|
15903
|
-
|
|
15904
|
-
mkdir(DEBUG_LOG_DIR2, { recursive: true }).catch(() => {});
|
|
15905
|
-
}
|
|
15906
|
-
} catch {}
|
|
15907
|
-
}
|
|
15908
|
-
function debugLogToFile2(message, data) {
|
|
15909
|
-
try {
|
|
15910
|
-
ensureDebugLogDir();
|
|
15911
|
-
const timestamp = new Date().toISOString();
|
|
15912
|
-
const logLine = `[${timestamp}] ${message}: ${JSON.stringify(data, null, 2)}
|
|
15913
|
-
`;
|
|
15914
|
-
appendFileSync3(DEBUG_LOG_FILE2, logLine);
|
|
15915
|
-
} catch {}
|
|
15993
|
+
import { homedir as homedir4 } from "os";
|
|
15994
|
+
import { isAbsolute, join as join5, relative, resolve as resolve2 } from "path";
|
|
15995
|
+
function getMcpToolDefinitionName(mcpToolDefs, index) {
|
|
15996
|
+
const name = mcpToolDefs[index]?.function?.name;
|
|
15997
|
+
return typeof name === "string" && name.length > 0 ? name : undefined;
|
|
15916
15998
|
}
|
|
15917
15999
|
function buildAvailableToolsSystemMessage(lastToolNames, lastToolMap, mcpToolDefs, mcpToolSummaries, subagentNames = []) {
|
|
15918
16000
|
const parts = [];
|
|
@@ -15922,27 +16004,27 @@ function buildAvailableToolsSystemMessage(lastToolNames, lastToolMap, mcpToolDef
|
|
|
15922
16004
|
parts.push(`Available OpenCode tools (use via tool calls): ${names}. Original skill ids mapped as: ${mapping}. Aliases include oc_skill_* and oc_superskill_* when applicable.`);
|
|
15923
16005
|
}
|
|
15924
16006
|
if (mcpToolSummaries && mcpToolSummaries.length > 0) {
|
|
16007
|
+
const summariesWithCallNames = mcpToolSummaries.map((summary, index) => ({
|
|
16008
|
+
...summary,
|
|
16009
|
+
callName: summary.callName ?? getMcpToolDefinitionName(mcpToolDefs, index) ?? namespaceMcpTool(summary.serverName, summary.toolName)
|
|
16010
|
+
}));
|
|
15925
16011
|
const servers = new Map;
|
|
15926
|
-
for (const s of
|
|
16012
|
+
for (const s of summariesWithCallNames) {
|
|
15927
16013
|
const list = servers.get(s.serverName) ?? [];
|
|
15928
16014
|
list.push(s);
|
|
15929
16015
|
servers.set(s.serverName, list);
|
|
15930
16016
|
}
|
|
15931
16017
|
const lines = [
|
|
15932
|
-
|
|
15933
|
-
"
|
|
16018
|
+
`MCP TOOLS — Use via direct tool calls (\`${MCP_TOOL_PREFIX}<server>__<tool>\`).`,
|
|
16019
|
+
"These tools are exposed as first-class tool calls (e.g. mcp__filesystem__read_file).",
|
|
15934
16020
|
""
|
|
15935
16021
|
];
|
|
15936
16022
|
for (const [server2, tools] of servers) {
|
|
15937
16023
|
lines.push(`Server: ${server2}`);
|
|
15938
16024
|
for (const t of tools) {
|
|
15939
16025
|
const paramHint = t.params?.length ? ` (params: ${t.params.join(", ")})` : "";
|
|
15940
|
-
|
|
15941
|
-
|
|
15942
|
-
if (tools.length > 0) {
|
|
15943
|
-
const ex = tools[0];
|
|
15944
|
-
const exArgs = ex.params?.length ? ` '{"${ex.params[0]}":"..."}'` : "";
|
|
15945
|
-
lines.push(` Example: mcptool call ${server2} ${ex.toolName}${exArgs}`);
|
|
16026
|
+
const sourceHint = t.callName === t.toolName ? "" : ` (server: ${t.serverName}; tool: ${t.toolName})`;
|
|
16027
|
+
lines.push(` - ${t.callName}${paramHint}${t.description ? " — " + t.description : ""}${sourceHint}`);
|
|
15946
16028
|
}
|
|
15947
16029
|
lines.push("");
|
|
15948
16030
|
}
|
|
@@ -15957,8 +16039,8 @@ function buildAvailableToolsSystemMessage(lastToolNames, lastToolMap, mcpToolDef
|
|
|
15957
16039
|
`) : null;
|
|
15958
16040
|
}
|
|
15959
16041
|
async function ensurePluginDirectory() {
|
|
15960
|
-
const configHome = process.env.XDG_CONFIG_HOME ? resolve2(process.env.XDG_CONFIG_HOME) :
|
|
15961
|
-
const pluginDir =
|
|
16042
|
+
const configHome = process.env.XDG_CONFIG_HOME ? resolve2(process.env.XDG_CONFIG_HOME) : join5(homedir4(), ".config");
|
|
16043
|
+
const pluginDir = join5(configHome, "opencode", "plugin");
|
|
15962
16044
|
try {
|
|
15963
16045
|
await mkdir(pluginDir, { recursive: true });
|
|
15964
16046
|
log20.debug("Plugin directory ensured", { path: pluginDir });
|
|
@@ -15975,8 +16057,8 @@ function getGlobalKey() {
|
|
|
15975
16057
|
return "__opencode_cursor_proxy_server__";
|
|
15976
16058
|
}
|
|
15977
16059
|
function getOpenCodeConfigPrefix() {
|
|
15978
|
-
const configHome = process.env.XDG_CONFIG_HOME ? resolve2(process.env.XDG_CONFIG_HOME) :
|
|
15979
|
-
return
|
|
16060
|
+
const configHome = process.env.XDG_CONFIG_HOME ? resolve2(process.env.XDG_CONFIG_HOME) : join5(homedir4(), ".config");
|
|
16061
|
+
return join5(configHome, "opencode");
|
|
15980
16062
|
}
|
|
15981
16063
|
function canonicalizePathForCompare(pathValue) {
|
|
15982
16064
|
const resolvedPath = resolve2(pathValue);
|
|
@@ -16053,7 +16135,7 @@ function resolveWorkspaceDirectory(worktree, directory) {
|
|
|
16053
16135
|
if (isAcceptableWorkspace(cwd, configPrefix)) {
|
|
16054
16136
|
return cwd;
|
|
16055
16137
|
}
|
|
16056
|
-
const home = resolveCandidate(
|
|
16138
|
+
const home = resolveCandidate(homedir4());
|
|
16057
16139
|
if (home && !isRootPath(home)) {
|
|
16058
16140
|
return home;
|
|
16059
16141
|
}
|
|
@@ -16355,7 +16437,7 @@ async function ensureCursorProxyServer(workspaceDirectory, toolRouter) {
|
|
|
16355
16437
|
const messages = Array.isArray(body?.messages) ? body.messages : [];
|
|
16356
16438
|
const stream = body?.stream === true;
|
|
16357
16439
|
const tools = Array.isArray(body?.tools) ? body.tools : [];
|
|
16358
|
-
|
|
16440
|
+
log20.debug("raw request body", {
|
|
16359
16441
|
model: body?.model,
|
|
16360
16442
|
cursorModel: body?.cursorModel,
|
|
16361
16443
|
stream,
|
|
@@ -17346,7 +17428,7 @@ function buildToolHookEntries(registry, fallbackBaseDir) {
|
|
|
17346
17428
|
}
|
|
17347
17429
|
return entries;
|
|
17348
17430
|
}
|
|
17349
|
-
var log20,
|
|
17431
|
+
var log20, CURSOR_PROVIDER_ID2 = "cursor-acp", CURSOR_PROVIDER_PREFIX, CURSOR_PROXY_HOST = "127.0.0.1", CURSOR_PROXY_DEFAULT_PORT = 32124, CURSOR_PROXY_DEFAULT_BASE_URL, REUSE_EXISTING_PROXY, SESSION_WORKSPACE_CACHE_LIMIT = 200, FORCE_TOOL_MODE, EMIT_TOOL_UPDATES, FORWARD_TOOL_CALLS, TOOL_LOOP_MODE_RAW, TOOL_LOOP_MODE, TOOL_LOOP_MODE_VALID, PROVIDER_BOUNDARY_MODE_RAW, PROVIDER_BOUNDARY_MODE, PROVIDER_BOUNDARY_MODE_VALID, LEGACY_PROVIDER_BOUNDARY, PROVIDER_BOUNDARY, ENABLE_PROVIDER_BOUNDARY_AUTOFALLBACK, TOOL_LOOP_MAX_REPEAT_RAW, TOOL_LOOP_MAX_REPEAT, TOOL_LOOP_MAX_REPEAT_VALID, PROXY_EXECUTE_TOOL_CALLS, SUPPRESS_CONVERTER_TOOL_EVENTS, SHOULD_EMIT_TOOL_UPDATES, NATIVE_TOOL_HOOK_EXCLUSIONS, CursorPlugin = async ({ $, directory, worktree, client: client3, serverUrl }) => {
|
|
17350
17432
|
const workspaceDirectory = resolveWorkspaceDirectory(worktree, directory);
|
|
17351
17433
|
log20.debug("Plugin initializing", {
|
|
17352
17434
|
directory,
|
|
@@ -17402,6 +17484,7 @@ var log20, DEBUG_LOG_DIR2, DEBUG_LOG_FILE2, CURSOR_PROVIDER_ID2 = "cursor-acp",
|
|
|
17402
17484
|
mcpToolSummaries = tools.map((t) => ({
|
|
17403
17485
|
serverName: t.serverName,
|
|
17404
17486
|
toolName: t.name,
|
|
17487
|
+
callName: namespaceMcpTool(t.serverName, t.name),
|
|
17405
17488
|
description: t.description,
|
|
17406
17489
|
params: t.inputSchema ? Object.keys(t.inputSchema.properties ?? {}) : undefined
|
|
17407
17490
|
}));
|
|
@@ -17619,8 +17702,6 @@ var init_plugin = __esm(() => {
|
|
|
17619
17702
|
init_tool_loop_guard();
|
|
17620
17703
|
init_binary();
|
|
17621
17704
|
log20 = createLogger("plugin");
|
|
17622
|
-
DEBUG_LOG_DIR2 = join6(homedir5(), ".config", "opencode", "logs");
|
|
17623
|
-
DEBUG_LOG_FILE2 = join6(DEBUG_LOG_DIR2, "tool-loop-debug.log");
|
|
17624
17705
|
CURSOR_PROVIDER_PREFIX = `${CURSOR_PROVIDER_ID2}/`;
|
|
17625
17706
|
CURSOR_PROXY_DEFAULT_BASE_URL = `http://${CURSOR_PROXY_HOST}:${CURSOR_PROXY_DEFAULT_PORT}/v1`;
|
|
17626
17707
|
REUSE_EXISTING_PROXY = process.env.CURSOR_ACP_REUSE_EXISTING_PROXY !== "false";
|
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/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,7 +1,7 @@
|
|
|
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";
|
|
@@ -33,7 +33,12 @@ import { SkillResolver } from "./tools/skills/resolver.js";
|
|
|
33
33
|
import { autoRefreshModels } from "./models/sync.js";
|
|
34
34
|
import { readMcpConfigs, readSubagentNames } from "./mcp/config.js";
|
|
35
35
|
import { McpClientManager } from "./mcp/client-manager.js";
|
|
36
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
MCP_TOOL_PREFIX,
|
|
38
|
+
buildMcpToolHookEntries,
|
|
39
|
+
buildMcpToolDefinitions,
|
|
40
|
+
namespaceMcpTool,
|
|
41
|
+
} from "./mcp/tool-bridge.js";
|
|
37
42
|
import { createOpencodeClient } from "@opencode-ai/sdk";
|
|
38
43
|
import { ToolRegistry as CoreRegistry } from "./tools/core/registry.js";
|
|
39
44
|
import { LocalExecutor } from "./tools/executors/local.js";
|
|
@@ -62,38 +67,19 @@ import { formatShellCommandForPlatform, resolveCursorAgentBinary } from "./utils
|
|
|
62
67
|
|
|
63
68
|
const log = createLogger("plugin");
|
|
64
69
|
|
|
65
|
-
// Debug log file for tool-loop investigation
|
|
66
|
-
const DEBUG_LOG_DIR = join(homedir(), ".config", "opencode", "logs");
|
|
67
|
-
const DEBUG_LOG_FILE = join(DEBUG_LOG_DIR, "tool-loop-debug.log");
|
|
68
|
-
|
|
69
|
-
function ensureDebugLogDir(): void {
|
|
70
|
-
try {
|
|
71
|
-
if (!existsSync(DEBUG_LOG_DIR)) {
|
|
72
|
-
mkdir(DEBUG_LOG_DIR, { recursive: true }).catch(() => {});
|
|
73
|
-
}
|
|
74
|
-
} catch {
|
|
75
|
-
// Ignore errors creating log directory
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function debugLogToFile(message: string, data: any): void {
|
|
80
|
-
try {
|
|
81
|
-
ensureDebugLogDir();
|
|
82
|
-
const timestamp = new Date().toISOString();
|
|
83
|
-
const logLine = `[${timestamp}] ${message}: ${JSON.stringify(data, null, 2)}\n`;
|
|
84
|
-
appendFileSync(DEBUG_LOG_FILE, logLine);
|
|
85
|
-
} catch {
|
|
86
|
-
// Ignore errors
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
70
|
interface McpToolSummary {
|
|
91
71
|
serverName: string;
|
|
92
72
|
toolName: string;
|
|
73
|
+
callName?: string;
|
|
93
74
|
description?: string;
|
|
94
75
|
params?: string[];
|
|
95
76
|
}
|
|
96
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
|
+
|
|
97
83
|
export function buildAvailableToolsSystemMessage(
|
|
98
84
|
lastToolNames: string[],
|
|
99
85
|
lastToolMap: Array<{ id: string; name: string }>,
|
|
@@ -110,16 +96,23 @@ export function buildAvailableToolsSystemMessage(
|
|
|
110
96
|
}
|
|
111
97
|
|
|
112
98
|
if (mcpToolSummaries && mcpToolSummaries.length > 0) {
|
|
113
|
-
const
|
|
114
|
-
|
|
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) {
|
|
115
108
|
const list = servers.get(s.serverName) ?? [];
|
|
116
109
|
list.push(s);
|
|
117
110
|
servers.set(s.serverName, list);
|
|
118
111
|
}
|
|
119
112
|
|
|
120
113
|
const lines: string[] = [
|
|
121
|
-
|
|
122
|
-
"
|
|
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).",
|
|
123
116
|
"",
|
|
124
117
|
];
|
|
125
118
|
|
|
@@ -127,12 +120,8 @@ export function buildAvailableToolsSystemMessage(
|
|
|
127
120
|
lines.push(`Server: ${server}`);
|
|
128
121
|
for (const t of tools) {
|
|
129
122
|
const paramHint = t.params?.length ? ` (params: ${t.params.join(", ")})` : "";
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (tools.length > 0) {
|
|
133
|
-
const ex = tools[0];
|
|
134
|
-
const exArgs = ex.params?.length ? ` '{"${ex.params[0]}":"..."}'` : "";
|
|
135
|
-
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}`);
|
|
136
125
|
}
|
|
137
126
|
lines.push("");
|
|
138
127
|
}
|
|
@@ -707,8 +696,7 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
|
|
|
707
696
|
const stream = body?.stream === true;
|
|
708
697
|
const tools = Array.isArray(body?.tools) ? body.tools : [];
|
|
709
698
|
|
|
710
|
-
|
|
711
|
-
debugLogToFile("raw_request_body", {
|
|
699
|
+
log.debug("raw request body", {
|
|
712
700
|
model: body?.model,
|
|
713
701
|
cursorModel: body?.cursorModel,
|
|
714
702
|
stream,
|
|
@@ -1903,6 +1891,7 @@ export const CursorPlugin: Plugin = async ({ $, directory, worktree, client, ser
|
|
|
1903
1891
|
mcpToolSummaries = tools.map((t) => ({
|
|
1904
1892
|
serverName: t.serverName,
|
|
1905
1893
|
toolName: t.name,
|
|
1894
|
+
callName: namespaceMcpTool(t.serverName, t.name),
|
|
1906
1895
|
description: t.description,
|
|
1907
1896
|
params: t.inputSchema
|
|
1908
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",
|