Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@bitkyc08/opencodex",
"version": "2.7.35",
"version": "2.7.39",
"description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code",
"type": "module",
"main": "./bin/package-main.mjs",
Expand Down
7 changes: 5 additions & 2 deletions src/adapters/cursor/live-transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ import {
buildCursorToolDefinitions,
cursorRequestAdvertisesApplyPatch,
cursorRequestHasShellAlias,
cursorToolInputSchema,
cursorToolArgNormalizeSchema,
cursorToolWireName,
cursorToolsForActivePrompt,
isGenericToolUseCountDemoPrompt,
Expand Down Expand Up @@ -506,7 +506,10 @@ class LiveCursorTransport implements CursorTransport {
const cursorToolNameMap = new Map<string, string>();
for (const tool of cursorVisibleTools ?? []) {
const cursorWireName = cursorToolWireName(tool);
toolSchemas.set(cursorWireName, cursorToolInputSchema(tool));
// Normalize against Responses/Codex field names, not the Cursor advertisement schema.
// Advertising `cmd` while also storing that schema here left `cmd` unmapped and Codex
// rejected shell_command with "missing field `command`" (#399).
toolSchemas.set(cursorWireName, cursorToolArgNormalizeSchema(tool));
cursorToolNameMap.set(cursorWireName, namespacedToolName(tool.namespace, tool.name));
}
state = createCursorProtobufEventState({
Expand Down
2 changes: 1 addition & 1 deletion src/adapters/cursor/native-exec-fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ function codexNativeMutationRefusal(operation: "write" | "delete"): string {
}

const NATIVE_LOCAL_EXEC_DISABLED =
"Cursor native local filesystem execution is not available for this request. Use the exec_command tool with equivalent shell commands (cat, head, ls, rg, grep) for file reads and searches, or apply_patch for file edits.";
"Route filesystem work through the Codex Responses bridge shell tool (`shell_command` / `exec_command`, or the listed `mcp_opencodex-responses_*` display alias) with equivalent shell commands (cat, head, ls, rg, grep), or `apply_patch` for file edits. Do not tell the user shell or read access is blocked, disabled, or denied — silently call that bridge tool.";

export function rejectReadExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
if (execMsg.message.case !== "readArgs") throw new Error("invalid read exec");
Expand Down
2 changes: 1 addition & 1 deletion src/adapters/cursor/native-exec-network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export interface CursorNativeNetworkDeps {
}

const NATIVE_FETCH_DISABLED =
"Cursor native fetch execution is not available for this request. Use the exec_command tool with curl or wget to make network requests instead.";
"Route this through the Codex shell bridge tool `shell_command` (aliases: `exec_command`, `mcp_opencodex-responses_shell_command`, `mcp_opencodex-responses_exec_command`) with curl or wget. Do not tell the user network or shell access is blocked, disabled, or denied — silently call that bridge tool.";

export function rejectFetchExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
if (execMsg.message.case !== "fetchArgs") throw new Error("invalid fetch exec");
Expand Down
2 changes: 1 addition & 1 deletion src/adapters/cursor/native-exec-shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const backgroundShells = new Map<number, { child: ChildProcessWithoutNullStreams
let nextShellId = 1;

const NATIVE_SHELL_DISABLED =
"Cursor native shell execution is not available for this request. Use the exec_command tool to run shell commands instead.";
"Route this through the Codex bridge shell tool from the current catalog (`shell_command` or `exec_command`, including the long `mcp_opencodex-responses_*` display name if listed). Do not tell the user shell access is blocked, disabled, or denied — silently call that bridge tool with the same command.";

function rejectedShellResult(command: string, cwd: string, started: number) {
return create(ShellResultSchema, {
Expand Down
40 changes: 32 additions & 8 deletions src/adapters/cursor/protobuf-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import type { OcxUsage } from "../../types";
import type { AgentServerMessage, McpArgs, ToolCall } from "./gen/agent_pb";
import { decodeCursorArgsMap } from "./arg-codec";
import { normalizeArgKeys } from "./arg-normalize";
import { OCX_RESPONSES_TOOL_PROVIDER, normalizeCursorWireName, responsesToolNameFromCursorWire } from "./tool-definitions";
import {
normalizeCursorWireName,
OCX_RESPONSES_TOOL_PROVIDER,
resolveShellBridgeAliasKey,
responsesToolNameFromCursorWire,
} from "./tool-definitions";
import type { CursorServerMessage } from "./types";

const DEFAULT_CONTEXT_USAGE_MAX_ENTRIES = 200;
Expand Down Expand Up @@ -212,12 +217,26 @@ function decodeMcpArgs(args: McpArgs | undefined): string {
return JSON.stringify(decodeCursorArgsMap(args?.args));
}

/** Resolve an advertised client-tool wire name, including shell_command/exec_command aliases (#399). */
function resolveAdvertisedClientToolName(
state: CursorProtobufEventState,
cursorWireName: string,
): string | undefined {
const normalized = normalizeCursorWireName(cursorWireName);
if (!state.clientToolNames) return normalized;
return resolveShellBridgeAliasKey(normalized, alias => (state.clientToolNames!.has(alias) ? alias : undefined));
}

function toolSchemaForWireName(state: CursorProtobufEventState, toolName: string | undefined): unknown | undefined {
if (!toolName || !state.toolSchemas) return undefined;
return resolveShellBridgeAliasKey(toolName, alias => state.toolSchemas!.get(alias));
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

function decodeMcpArgsNormalized(args: McpArgs | undefined, state: CursorProtobufEventState): string {
const decoded = decodeCursorArgsMap(args?.args);
const toolName = mcpWireNameFromArgs(args);
if (toolName && state.toolSchemas?.has(toolName)) {
return JSON.stringify(normalizeArgKeys(decoded, state.toolSchemas.get(toolName)));
}
const schema = toolSchemaForWireName(state, toolName);
if (schema) return JSON.stringify(normalizeArgKeys(decoded, schema));
return JSON.stringify(decoded);
}

Expand All @@ -237,11 +256,12 @@ function isCompleteJson(text: string): boolean {

/** Schema-normalize a JSON-text argument blob for a named tool, if a schema is known. */
function normalizeJsonText(text: string, toolName: string | undefined, state: CursorProtobufEventState): string {
if (!toolName || !state.toolSchemas?.has(toolName)) return text;
const schema = toolSchemaForWireName(state, toolName);
if (!schema) return text;
try {
const parsed = JSON.parse(text);
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
return JSON.stringify(normalizeArgKeys(parsed as Record<string, unknown>, state.toolSchemas.get(toolName)));
return JSON.stringify(normalizeArgKeys(parsed as Record<string, unknown>, schema));
}
} catch {
// Not parseable as an object: leave as-is.
Expand Down Expand Up @@ -305,10 +325,14 @@ export function mapSyntheticMcpExecToToolEvents(
function recordToolCall(state: CursorProtobufEventState, callId: string, cursorWireName: string): CursorServerMessage[] {
if (state.completedToolCalls.has(callId)) return [];
if (state.openToolCalls.has(callId)) return [];
if (state.clientToolNames && !state.clientToolNames.has(cursorWireName)) {
const advertisedName = resolveAdvertisedClientToolName(state, cursorWireName);
if (state.clientToolNames && !advertisedName) {
return [{ type: "error", message: `Cursor requested unknown Responses tool: ${cursorWireName}` }];
}
state.openToolCalls.set(callId, { name: responsesToolNameFromCursorWire(cursorWireName, state.cursorToolNameMap), args: "" });
// Prefer the advertised catalog name for Responses mapping so shell_command/exec_command aliases
// land on the tool Codex actually exposed this turn (#399).
const mapKey = advertisedName ?? normalizeCursorWireName(cursorWireName);
state.openToolCalls.set(callId, { name: responsesToolNameFromCursorWire(mapKey, state.cursorToolNameMap), args: "" });
state.startedClientToolCalls++;
return [];
}
Expand Down
42 changes: 32 additions & 10 deletions src/adapters/cursor/request-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ import {
cursorMcpToolEncodedSize,
cursorMcpToolsEncodedSize,
cursorToolAllowedByChoice,
cursorToolChoiceAliases,
cursorToolWireName,
cursorToolsForActivePrompt,
isBareCodexShellBridgeTool,
} from "./tool-definitions";
import { lookupCursorThreadConversation } from "./thread-continuity";

Expand All @@ -35,10 +37,17 @@ function explicitlySelectedNames(choice: OcxToolChoice | undefined): Set<string>
}

function toolPriority(tool: OcxTool, selectedNames: ReadonlySet<string>): number {
if (toolChoiceAliases(tool).some(name => selectedNames.has(name))) return 0;
if (tool.loadedFromToolSearch) return 1;
if (!tool.namespace) return 2;
return 3;
if (cursorToolChoiceAliases(tool).some(name => selectedNames.has(name))) return 0;
// Shell bridge before apply_patch so a large patch schema cannot starve the only execution path (#399).
if (isBareCodexShellBridgeTool(tool)) return 1;
if (!tool.namespace && tool.name === "apply_patch") return 2;
if (tool.loadedFromToolSearch) return 3;
if (!tool.namespace) return 4;
return 5;
}

function isPinnedCursorTool(tool: OcxTool, selectedNames: ReadonlySet<string>): boolean {
return toolPriority(tool, selectedNames) <= 2;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
Expand All @@ -64,15 +73,28 @@ export function applyCursorToolBudget(
const keptSet = new Set<OcxTool>();
let keptBytes = 0;

for (const candidate of candidates) {
if (kept.length >= CURSOR_TOOL_COUNT_LIMIT) continue;
const tryKeep = (tool: OcxTool): boolean => {
if (keptSet.has(tool) || kept.length >= CURSOR_TOOL_COUNT_LIMIT) return keptSet.has(tool);
// Repeated protobuf message fields serialize as concatenated tag/length/value entries,
// so each one-entry wrapper size is the exact additive contribution to McpTools.
const candidateBytes = cursorMcpToolEncodedSize(candidate.tool, toolChoice);
if (keptBytes + candidateBytes > CURSOR_TOOL_BYTES_LIMIT) continue;
kept.push(candidate.tool);
keptSet.add(candidate.tool);
const candidateBytes = cursorMcpToolEncodedSize(tool, toolChoice);
if (keptBytes + candidateBytes > CURSOR_TOOL_BYTES_LIMIT) return false;
kept.push(tool);
keptSet.add(tool);
keptBytes += candidateBytes;
return true;
};

// Phase 1: selected tools + shell bridge + apply_patch (priority <= 2).
// Pins are admitted before filler so a crowded catalog cannot drop the Codex execution path (#399).
for (const candidate of candidates) {
if (!isPinnedCursorTool(candidate.tool, selectedNames)) continue;
tryKeep(candidate.tool);
}

// Phase 2: remaining tools by priority.
for (const candidate of candidates) {
tryKeep(candidate.tool);
}

return {
Expand Down
Loading
Loading