@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.
@@ -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
- debugLogToFile("buildPromptFromMessages", {
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
- debugLogToFile("buildPromptFromMessages: final prompt", {
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("Based on the tool results")
1183
+ hasCompletionSignal: finalPrompt.includes("The above tool calls have been executed")
1205
1184
  });
1206
1185
  return finalPrompt;
1207
1186
  }
1208
- var log5, DEBUG_LOG_DIR, DEBUG_LOG_FILE;
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 `mcp__${sanitizedServer}__${sanitizedTool}`;
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 buildRepairHint(toolName, missing, unexpected, typeErrors) {
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 hint = termination.repairHint || "Use write for full-file replacement, or provide path, old_string, and new_string for edit.";
15256
- const content = `Skipped malformed tool call "${toolCall.function.name}": ${termination.message} ${hint}`.trim();
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 { appendFileSync as appendFileSync3, existsSync as existsSync5, realpathSync } from "fs";
15991
+ import { realpathSync } from "fs";
15898
15992
  import { mkdir } from "fs/promises";
15899
- import { homedir as homedir5 } from "os";
15900
- import { isAbsolute, join as join6, relative, resolve as resolve2 } from "path";
15901
- function ensureDebugLogDir() {
15902
- try {
15903
- if (!existsSync5(DEBUG_LOG_DIR2)) {
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 mcpToolSummaries) {
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
- "MCP TOOLS — Use via Shell with the `mcptool` CLI.",
15933
- "Syntax: mcptool call <server> <tool> [json-args]",
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
- lines.push(` - ${t.toolName}${paramHint}${t.description ? " " + t.description : ""}`);
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) : join6(homedir5(), ".config");
15961
- const pluginDir = join6(configHome, "opencode", "plugin");
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) : join6(homedir5(), ".config");
15979
- return join6(configHome, "opencode");
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(homedir5());
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
- debugLogToFile2("raw_request_body", {
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, DEBUG_LOG_DIR2, DEBUG_LOG_FILE2, 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 }) => {
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.5",
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",
@@ -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 `mcp__${sanitizedServer}__${sanitizedTool}`;
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 { appendFileSync, existsSync, mkdirSync, realpathSync } from "fs";
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 { buildMcpToolHookEntries, buildMcpToolDefinitions } from "./mcp/tool-bridge.js";
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 servers = new Map<string, McpToolSummary[]>();
114
- for (const s of mcpToolSummaries) {
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
- "MCP TOOLS — Use via Shell with the `mcptool` CLI.",
122
- "Syntax: mcptool call <server> <tool> [json-args]",
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
- lines.push(` - ${t.toolName}${paramHint}${t.description ? " " + t.description : ""}`);
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
- // DEBUG: Log raw request structure for tool-loop investigation
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 { applyToolSchemaCompat, type ToolSchemaValidationResult } from "./tool-schema-compat.js";
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 hint =
672
- termination.repairHint
673
- || "Use write for full-file replacement, or provide path, old_string, and new_string for edit.";
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} ${hint}`.trim();
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",