mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-07-09 17:18:24 +00:00
7080 lines
231 KiB
JavaScript
7080 lines
231 KiB
JavaScript
"use strict";
|
|
|
|
const http = require("node:http");
|
|
const https = require("node:https");
|
|
const fs = require("node:fs");
|
|
const os = require("node:os");
|
|
const path = require("node:path");
|
|
const zlib = require("node:zlib");
|
|
|
|
const DEFAULT_CURSOR_HOSTS = [
|
|
"api.cursor.sh",
|
|
"api2.cursor.sh",
|
|
"api3.cursor.sh",
|
|
"api4.cursor.sh",
|
|
"api.cursor.com",
|
|
"api2.cursor.com",
|
|
"*.cursor.sh",
|
|
"*.cursor.com",
|
|
"*.cursorapi.com"
|
|
];
|
|
|
|
const HOP_BY_HOP_HEADERS = new Set([
|
|
"connection",
|
|
"keep-alive",
|
|
"proxy-authenticate",
|
|
"proxy-authorization",
|
|
"proxy-connection",
|
|
"te",
|
|
"trailer",
|
|
"transfer-encoding",
|
|
"upgrade"
|
|
]);
|
|
|
|
const INTERNAL_HEADER_PREFIX = "x-ccr-";
|
|
const DEFAULT_GATEWAY_URL = "http://127.0.0.1:3456";
|
|
const MAX_JSON_SCAN_DEPTH = 8;
|
|
const DEFAULT_BIDI_WAIT_MS = 15000;
|
|
const DEFAULT_BIDI_SETTLE_MS = 500;
|
|
const DEFAULT_BIDI_MAX_SETTLE_MS = 2000;
|
|
const DEFAULT_BIDI_MISSING_CONTEXT_WAIT_MS = 6000;
|
|
const DEFAULT_BIDI_SESSION_TTL_MS = 10 * 60 * 1000;
|
|
const DEFAULT_CURSOR_CONTEXT_TTL_MS = 2 * 60 * 1000;
|
|
const DEFAULT_CURSOR_CONTEXT_MAX_ENTRIES = 64;
|
|
const DEFAULT_GATEWAY_TIMEOUT_MS = 10 * 60 * 1000;
|
|
const DEFAULT_CURSOR_TOOL_BRIDGE_TIMEOUT_MS = 2 * 60 * 1000;
|
|
const DEFAULT_CURSOR_TOOL_BRIDGE_MAX_ROUNDS = 8;
|
|
const DEFAULT_PASSTHROUGH_LOG_LIMIT = 3;
|
|
const DEFAULT_DECODE_DIAGNOSTIC_SAMPLE_LIMIT = 8;
|
|
const DEFAULT_DECODE_DIAGNOSTIC_SAMPLE_CHARS = 180;
|
|
const DEFAULT_DECODE_DUMP_DIR = path.join(os.tmpdir(), "ccr-cursor-decode-dumps");
|
|
const DEFAULT_DECODE_TREE_DEPTH = 12;
|
|
const CURSOR_PROXY_ROUTE_TYPES = new Set(["always", "image", "long-context", "model", "model-prefix", "thinking", "web-search"]);
|
|
const CURSOR_NATIVE_RPC_PATH_PATTERN = /^\/(?:aiserver|agent)\.v\d+\.[^/]+\/[^/]+$/i;
|
|
const CURSOR_NATIVE_LLM_SERVICE_PATTERN =
|
|
/^(?:AiService|AgentService|ChatService|ComposerService|CppService|TerminalService)$/i;
|
|
const CURSOR_NATIVE_LLM_METHOD_PATTERN =
|
|
/^(?:(?:Stream|Run|Generate|Create|Submit|Start).*(?:Agent|Chat|Completion|Completions|Composer|Cpp|Edit|Edits|Inline|Message|Prompt|Response|Terminal)|Complete(?:Chat|Completion|Edit|Inline|Terminal).*)$/i;
|
|
const CURSOR_NATIVE_SUPPORTED_TOOL_FIELDS = new Set([29, 51]);
|
|
const CURSOR_NATIVE_SUPPORTED_TOOL_ENUM_NAMES = new Map([
|
|
[1, "READ_SEMSEARCH_FILES"],
|
|
[3, "RIPGREP_SEARCH"],
|
|
[5, "READ_FILE"],
|
|
[6, "LIST_DIR"],
|
|
[8, "FILE_SEARCH"],
|
|
[9, "SEMANTIC_SEARCH_FULL"],
|
|
[39, "LIST_DIR_V2"],
|
|
[40, "READ_FILE_V2"],
|
|
[41, "RIPGREP_RAW_SEARCH"],
|
|
[42, "GLOB_FILE_SEARCH"]
|
|
]);
|
|
const CURSOR_NATIVE_SUPPORTED_TOOL_TO_SPEC_NAME = new Map([
|
|
[3, "grep_search"],
|
|
[5, "read_file"],
|
|
[6, "list_dir"],
|
|
[8, "glob_file_search"],
|
|
[9, "codebase_search"],
|
|
[39, "list_dir"],
|
|
[40, "read_file"],
|
|
[41, "grep_search"],
|
|
[42, "glob_file_search"]
|
|
]);
|
|
const CURSOR_NATIVE_BUILTIN_TOOL_NAMES = [
|
|
"read_file",
|
|
"list_dir",
|
|
"grep_search",
|
|
"glob_file_search",
|
|
"codebase_search",
|
|
"shell",
|
|
"delete_file",
|
|
"edit_file",
|
|
"read_lints",
|
|
"web_search",
|
|
"web_fetch",
|
|
"task",
|
|
"await_task",
|
|
"todo_read",
|
|
"todo_write",
|
|
"ask_question",
|
|
"switch_mode",
|
|
"generate_image",
|
|
"list_mcp_resources",
|
|
"read_mcp_resource",
|
|
"get_mcp_tools",
|
|
"call_mcp_tool",
|
|
"set_active_branch"
|
|
];
|
|
const CURSOR_SYSTEM_PROMPT_KEYS = [
|
|
"customInstruction",
|
|
"customInstructions",
|
|
"developer",
|
|
"developerInstruction",
|
|
"developerInstructions",
|
|
"developerPrompt",
|
|
"globalInstruction",
|
|
"globalInstructions",
|
|
"instructions",
|
|
"roleInstruction",
|
|
"roleInstructions",
|
|
"system",
|
|
"systemInstruction",
|
|
"systemInstructions",
|
|
"systemPrompt",
|
|
"system_prompt"
|
|
];
|
|
const CURSOR_TOOL_KEYS = [
|
|
"availableTools",
|
|
"available_tools",
|
|
"availableToolSchemas",
|
|
"available_tool_schemas",
|
|
"clientSideToolDefinitions",
|
|
"clientSideTools",
|
|
"functionDefinitions",
|
|
"function_definitions",
|
|
"functions",
|
|
"mcpTools",
|
|
"mcp_tools",
|
|
"toolSchemas",
|
|
"tool_schemas",
|
|
"toolDefinitions",
|
|
"tool_definitions",
|
|
"tools"
|
|
];
|
|
const CURSOR_TOOL_CHOICE_KEYS = [
|
|
"toolChoice",
|
|
"toolSelection",
|
|
"tool_choice",
|
|
"tool_selection"
|
|
];
|
|
const UUID_PATTERN = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
|
|
|
|
module.exports = {
|
|
async setup(ctx) {
|
|
const options = isRecord(ctx.pluginConfig) ? ctx.pluginConfig : {};
|
|
const gatewayUrl = trimTrailingSlash(stringValue(options.gatewayUrl) || configuredGatewayUrl(ctx.config));
|
|
const gatewayApiKey = stringValue(options.gatewayApiKey) || configuredGatewayApiKey(ctx.config);
|
|
const hosts = normalizeStringList(options.hosts, options.host) || DEFAULT_CURSOR_HOSTS;
|
|
const paths = options.paths === false ? undefined : normalizePathList(options.paths);
|
|
|
|
const runtime = {
|
|
bidiSessionTtlMs: numberOption(options.bidiSessionTtlMs, DEFAULT_BIDI_SESSION_TTL_MS),
|
|
bidiMaxSettleMs: numberOption(options.bidiMaxSettleMs, DEFAULT_BIDI_MAX_SETTLE_MS),
|
|
bidiMissingContextWaitMs: numberOption(options.bidiMissingContextWaitMs, DEFAULT_BIDI_MISSING_CONTEXT_WAIT_MS),
|
|
bidiSettleMs: numberOption(options.bidiSettleMs, DEFAULT_BIDI_SETTLE_MS),
|
|
bidiSessions: new Map(),
|
|
bidiWaitMs: numberOption(options.bidiWaitMs, DEFAULT_BIDI_WAIT_MS),
|
|
cursorContextCollector: options.cursorContextCollector !== false,
|
|
cursorContextMaxEntries: numberOption(options.cursorContextMaxEntries, DEFAULT_CURSOR_CONTEXT_MAX_ENTRIES),
|
|
cursorContextTtlMs: numberOption(options.cursorContextTtlMs, DEFAULT_CURSOR_CONTEXT_TTL_MS),
|
|
cursorContexts: new Map(),
|
|
cursorBidiProto: options.cursorBidiProto !== false,
|
|
cursorConnectJson: options.cursorConnectJson !== false,
|
|
cursorNativeProto: options.cursorNativeProto !== false,
|
|
cursorNativeLlmMethods: normalizeStringList(options.cursorNativeLlmMethods, options.cursorNativeLlmMethod) || [],
|
|
decodeDiagnosticSampleChars: numberOption(
|
|
options.decodeDiagnosticSampleChars ?? options.cursorDecodeDiagnosticSampleChars,
|
|
DEFAULT_DECODE_DIAGNOSTIC_SAMPLE_CHARS
|
|
),
|
|
decodeDiagnosticSampleLimit: numberOption(
|
|
options.decodeDiagnosticSampleLimit ?? options.cursorDecodeDiagnosticSampleLimit,
|
|
DEFAULT_DECODE_DIAGNOSTIC_SAMPLE_LIMIT
|
|
),
|
|
decodeDumpDir: stringValue(options.decodeDumpDir) ||
|
|
stringValue(options.cursorDecodeDumpDir) ||
|
|
stringValue(process.env.CCR_CURSOR_DECODE_DUMP_DIR) ||
|
|
DEFAULT_DECODE_DUMP_DIR,
|
|
decodeDumpFull: options.decodeDumpFull !== false && options.cursorDecodeDumpFull !== false,
|
|
decodeDiagnostics: options.decodeDiagnostics !== false && options.cursorDecodeDiagnostics !== false,
|
|
decodeTreeDepth: numberOption(options.decodeTreeDepth ?? options.cursorDecodeTreeDepth, DEFAULT_DECODE_TREE_DEPTH),
|
|
forwardDecodedCursorTools: options.forwardDecodedCursorTools !== false && options.cursorForwardDecodedTools !== false,
|
|
forwardCursorNativeBuiltinTools: options.forwardCursorNativeBuiltinTools !== false &&
|
|
options.cursorForwardNativeBuiltinTools !== false,
|
|
bridgeOpenAIToolCalls: options.bridgeOpenAIToolCalls !== false && options.cursorBridgeOpenAIToolCalls !== false,
|
|
showUnbridgedToolCallWarning: options.showUnbridgedToolCallWarning === true ||
|
|
options.cursorShowUnbridgedToolCallWarning === true,
|
|
toolCallBridgeTimeoutMs: numberOption(
|
|
options.toolCallBridgeTimeoutMs ?? options.cursorToolCallBridgeTimeoutMs,
|
|
DEFAULT_CURSOR_TOOL_BRIDGE_TIMEOUT_MS
|
|
),
|
|
maxToolCallRounds: numberOption(
|
|
options.maxToolCallRounds ?? options.cursorMaxToolCallRounds,
|
|
DEFAULT_CURSOR_TOOL_BRIDGE_MAX_ROUNDS
|
|
),
|
|
inlineToolCallContinuation: options.inlineToolCallContinuation === true ||
|
|
options.cursorInlineToolCallContinuation === true,
|
|
cursorMcpSkipApproval: options.cursorMcpSkipApproval === true || options.mcpSkipApproval === true,
|
|
chatCompletionSystemPrompt: stringValue(options.systemPrompt) ||
|
|
stringValue(options.openaiSystemPrompt) ||
|
|
stringValue(options.defaultSystemPrompt),
|
|
chatCompletionToolChoice: normalizeToolChoice(
|
|
options.toolChoice ?? options.openaiToolChoice ?? options.defaultToolChoice
|
|
),
|
|
chatCompletionTools: normalizeConfiguredTools(options.tools ?? options.openaiTools ?? options.defaultTools),
|
|
defaultModel: stringValue(options.defaultModel) || configuredDefaultModel(ctx.config),
|
|
fallbackToCursor: options.fallbackToCursor !== false,
|
|
gatewayApiKey,
|
|
gatewayTimeoutMs: numberOption(options.gatewayTimeoutMs, DEFAULT_GATEWAY_TIMEOUT_MS),
|
|
gatewayUrl,
|
|
logger: ctx.logger,
|
|
options,
|
|
passthroughLogCounts: new Map(),
|
|
passthroughLogLimit: numberOption(options.passthroughLogLimit, DEFAULT_PASSTHROUGH_LOG_LIMIT),
|
|
routing: normalizeCursorProxyRouting(options.routing, options),
|
|
targetModel: stringValue(options.targetModel),
|
|
targetProvider: stringValue(options.targetProvider),
|
|
targetProviders: stringValue(options.targetProviders),
|
|
warnedMissingChatContext: false,
|
|
warnedSuppressedDecodedCursorTools: false
|
|
};
|
|
|
|
const backend = await ctx.registerHttpBackend({
|
|
id: "cursor-proxy-adapter",
|
|
async handler(request, response) {
|
|
await handleCursorProxyRequest(runtime, request, response);
|
|
}
|
|
});
|
|
|
|
hosts.forEach((host, index) => {
|
|
ctx.registerProxyRoute({
|
|
host,
|
|
id: `cursor-proxy-${host.replace(/[^a-z0-9]+/gi, "-") || index}`,
|
|
paths,
|
|
preserveHost: true,
|
|
upstream: backend.url
|
|
});
|
|
});
|
|
|
|
ctx.registerGatewayRoute({
|
|
auth: "none",
|
|
handler(_request, response, helpers) {
|
|
helpers.sendJson(response, 200, {
|
|
backend: backend.url,
|
|
bidiMaxSettleMs: runtime.bidiMaxSettleMs,
|
|
bidiMissingContextWaitMs: runtime.bidiMissingContextWaitMs,
|
|
bidiSettleMs: runtime.bidiSettleMs,
|
|
cursorBidiProto: runtime.cursorBidiProto,
|
|
cursorConnectJson: runtime.cursorConnectJson,
|
|
cursorNativeProto: runtime.cursorNativeProto,
|
|
fallbackToCursor: runtime.fallbackToCursor,
|
|
gatewayUrl,
|
|
hosts,
|
|
collector: {
|
|
auth: options.collectorAuth === "gateway" ? "gateway" : "none",
|
|
contexts: runtime.cursorContexts.size,
|
|
enabled: runtime.cursorContextCollector,
|
|
endpoints: runtime.cursorContextCollector ? ["POST /plugins/cursor-proxy/collector"] : [],
|
|
maxEntries: runtime.cursorContextMaxEntries,
|
|
ttlMs: runtime.cursorContextTtlMs
|
|
},
|
|
debug: {
|
|
auth: options.debugAuth === "gateway" ? "gateway" : "none",
|
|
endpoints: ["GET /plugins/cursor-proxy/debug/sessions"]
|
|
},
|
|
openaiCompatContext: {
|
|
systemPrompt: Boolean(runtime.chatCompletionSystemPrompt),
|
|
toolChoice: runtime.chatCompletionToolChoice !== undefined,
|
|
tools: runtime.chatCompletionTools.length
|
|
},
|
|
agentRunToolForwarding: {
|
|
bridgeOpenAIToolCalls: runtime.bridgeOpenAIToolCalls,
|
|
cursorMcpSkipApproval: runtime.cursorMcpSkipApproval,
|
|
forwardDecodedCursorTools: runtime.forwardDecodedCursorTools,
|
|
forwardCursorNativeBuiltinTools: runtime.forwardCursorNativeBuiltinTools,
|
|
inlineToolCallContinuation: runtime.inlineToolCallContinuation,
|
|
maxToolCallRounds: runtime.maxToolCallRounds,
|
|
toolCallBridgeTimeoutMs: runtime.toolCallBridgeTimeoutMs,
|
|
showUnbridgedToolCallWarning: runtime.showUnbridgedToolCallWarning
|
|
},
|
|
paths: paths || ["*"],
|
|
plugin: "cursor-proxy",
|
|
routing: {
|
|
defaultTarget: runtime.routing.defaultTarget || undefined,
|
|
enabled: runtime.routing.enabled,
|
|
rules: runtime.routing.rules.length
|
|
},
|
|
sessions: runtime.bidiSessions.size,
|
|
targetModel: runtime.targetModel || undefined,
|
|
targetProvider: runtime.targetProvider || undefined,
|
|
targetProviders: runtime.targetProviders || undefined
|
|
});
|
|
},
|
|
id: "cursor-proxy-status",
|
|
method: "GET",
|
|
path: "/plugins/cursor-proxy"
|
|
});
|
|
|
|
ctx.registerGatewayRoute({
|
|
auth: options.debugAuth === "gateway" ? "gateway" : "none",
|
|
handler(_request, response, helpers) {
|
|
helpers.sendJson(response, 200, {
|
|
collector: {
|
|
contexts: runtime.cursorContexts.size,
|
|
ttlMs: runtime.cursorContextTtlMs
|
|
},
|
|
plugin: "cursor-proxy",
|
|
sessions: describeBidiSessions(runtime)
|
|
});
|
|
},
|
|
id: "cursor-proxy-debug-sessions",
|
|
method: "GET",
|
|
path: "/plugins/cursor-proxy/debug/sessions"
|
|
});
|
|
|
|
if (runtime.cursorContextCollector) {
|
|
ctx.registerGatewayRoute({
|
|
auth: options.collectorAuth === "gateway" ? "gateway" : "none",
|
|
handler(request, response, helpers) {
|
|
return handleCursorContextCollector(runtime, request, response, helpers);
|
|
},
|
|
id: "cursor-proxy-context-collector",
|
|
method: "POST",
|
|
pathPrefix: "/plugins/cursor-proxy/collector"
|
|
});
|
|
}
|
|
|
|
ctx.logger.info(
|
|
`Cursor proxy adapter listening at ${backend.url} for ${hosts.join(", ")} ` +
|
|
`(${paths?.join(", ") || "all paths"}) and forwarding JSON and Cursor Agent LLM traffic to ${gatewayUrl}`
|
|
);
|
|
}
|
|
};
|
|
|
|
async function handleCursorProxyRequest(runtime, request, response) {
|
|
const originalUrl = originalRequestUrl(request);
|
|
const url = new URL(request.url || "/", "http://cursor-proxy.local");
|
|
const method = (request.method || "GET").toUpperCase();
|
|
|
|
if (method === "OPTIONS") {
|
|
response.writeHead(204, corsHeaders());
|
|
response.end();
|
|
return;
|
|
}
|
|
|
|
const requestBody = await readRequestBody(request);
|
|
if (runtime.cursorBidiProto) {
|
|
const handled = await handleCursorBidiProtoRequest(runtime, request, response, url, requestBody, originalUrl);
|
|
if (handled) {
|
|
return;
|
|
}
|
|
}
|
|
if (runtime.cursorNativeProto) {
|
|
const handled = await handleCursorNativeProtoRequest(runtime, request, response, url, requestBody);
|
|
if (handled) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
const route = resolveGatewayRoute(runtime, request, url, requestBody);
|
|
|
|
if (!route) {
|
|
if (runtime.fallbackToCursor && originalUrl) {
|
|
logCursorPassthrough(runtime, method, url.pathname, request.headers);
|
|
await forwardToUrl({
|
|
body: requestBody,
|
|
headers: buildPassthroughHeaders(request.headers, originalUrl),
|
|
method,
|
|
response,
|
|
url: originalUrl
|
|
});
|
|
return;
|
|
}
|
|
|
|
sendJson(response, 415, {
|
|
error: {
|
|
code: "unsupported_cursor_request",
|
|
message:
|
|
"Cursor proxy could not map this request to a CCR gateway protocol. " +
|
|
"OpenAI, Anthropic, Gemini JSON requests and Cursor Agent RunSSE protobuf requests are supported; other native Cursor RPC is passed through when fallbackToCursor is enabled."
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
await forwardToUrl({
|
|
body: route.body,
|
|
headers: buildGatewayHeaders(runtime, request.headers, route, route.body),
|
|
method: route.method,
|
|
response,
|
|
url: new URL(route.path, runtime.gatewayUrl)
|
|
});
|
|
}
|
|
|
|
async function handleCursorContextCollector(runtime, request, response, helpers) {
|
|
const body = await helpers.readBody(request);
|
|
const payload = readJsonLikePayload(body);
|
|
if (!isRecord(payload)) {
|
|
helpers.sendJson(response, 400, {
|
|
error: {
|
|
code: "invalid_cursor_context",
|
|
message: "Cursor context collector expects a JSON object."
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
const chatRequest = normalizeCollectedCursorChatRequest(runtime, payload);
|
|
if (!chatRequest) {
|
|
helpers.sendJson(response, 400, {
|
|
error: {
|
|
code: "unsupported_cursor_context",
|
|
message:
|
|
"Cursor context collector could not find chat messages, system instructions, or tools in this payload."
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
const keys = collectCursorContextKeys(payload, chatRequest);
|
|
const stored = storeCursorContext(runtime, {
|
|
chatRequest,
|
|
createdAt: Date.now(),
|
|
keys,
|
|
summary: summarizeChatRequest(chatRequest)
|
|
});
|
|
|
|
helpers.sendJson(response, 200, {
|
|
contextKeys: stored.keys,
|
|
stored: true,
|
|
summary: formatChatSummary(stored.summary)
|
|
});
|
|
}
|
|
|
|
async function handleCursorBidiProtoRequest(runtime, request, response, url, requestBody) {
|
|
const method = (request.method || "GET").toUpperCase();
|
|
const path = normalizePath(url.pathname);
|
|
if (method !== "POST") {
|
|
return false;
|
|
}
|
|
|
|
if (path === "/aiserver.v1.BidiService/BidiAppend") {
|
|
const append = decodeBidiAppendRequest(requestBody);
|
|
if (!append.requestId) {
|
|
const waitingSession = findSingleWaitingBidiSession(runtime);
|
|
if (!waitingSession) {
|
|
return false;
|
|
}
|
|
append.requestId = waitingSession.requestId;
|
|
runtime.logger?.debug?.(
|
|
`Cursor proxy inferred BidiAppend request id ${append.requestId} from the only waiting RunSSE session ` +
|
|
`(seq=${append.appendSeqno}, body=${requestBody.length}, body_hex=${append.bodyHex}, decoded_body=${append.rpcBodyEncoding}, ` +
|
|
`decoded_hex=${append.rpcBodyHex}, fields=${formatProtoFieldSummary(append.rpcBody)}).`
|
|
);
|
|
}
|
|
|
|
const session = getBidiSession(runtime, append.requestId, true);
|
|
const runRequest = appendBidiSessionRequest(session, append);
|
|
if (runRequest) {
|
|
setBidiSessionRunRequest(session, runRequest);
|
|
runtime.logger?.debug?.(
|
|
`Cursor proxy captured AgentRunRequest for Bidi request ${append.requestId} ` +
|
|
`(seq=${append.appendSeqno}, appends=${session.appends.size}, ${formatAgentRunRequestSummary(runtime, runRequest)}).`
|
|
);
|
|
} else if (session.waiters.length > 0) {
|
|
runtime.logger?.debug?.(
|
|
`Cursor proxy buffered BidiAppend for ${append.requestId} but has not decoded AgentRunRequest yet ` +
|
|
`(seq=${append.appendSeqno}, body=${requestBody.length}, body_hex=${append.bodyHex}, decoded_body=${append.rpcBodyEncoding}, ` +
|
|
`decoded_hex=${append.rpcBodyHex}, data=${append.data.length}, ` +
|
|
`data_bytes=${append.dataBytes.length}, data_hex=${append.dataHex}, data_binary=${append.dataBinary.length}, appends=${session.appends.size}, ` +
|
|
`combined_data=${combinedAppendDataLength(session)}, combined_data_bytes=${combinedAppendDataBytesLength(session)}, combined_data_binary=${combinedAppendDataBinaryLength(session)}, ` +
|
|
`fields=${formatProtoFieldSummary(append.rpcBody)}, data_binary_fields=${formatProtoFieldSummary(append.dataBinary)}).`
|
|
);
|
|
logCursorDecodeDiagnostics(
|
|
runtime,
|
|
"debug",
|
|
`Cursor proxy BidiAppend decode diagnostics for ${append.requestId}: ` +
|
|
formatBidiSessionDecodeDiagnostic(runtime, session)
|
|
);
|
|
}
|
|
|
|
sendBinary(response, 200, Buffer.alloc(0), protoHeaders());
|
|
return true;
|
|
}
|
|
|
|
if (path === "/agent.v1.AgentService/RunSSE") {
|
|
const runSse = decodeRunSseRequest(requestBody);
|
|
const requestId = runSse.requestId || (runSse.runRequest ? createSyntheticBidiRequestId() : "");
|
|
if (!requestId) {
|
|
logCursorDecodeDiagnostics(
|
|
runtime,
|
|
"warn",
|
|
`Cursor proxy could not identify AgentService/RunSSE request id ` +
|
|
`(body=${requestBody.length}, decoded_body=${runSse.rpcBodyEncoding}, fields=${formatProtoFieldSummary(runSse.rpcBody)}, ` +
|
|
`candidates=${formatCandidateBufferSummary(runtime, runSse.candidates)}).`
|
|
);
|
|
return false;
|
|
}
|
|
|
|
const session = getBidiSession(runtime, requestId, true);
|
|
session.lastSeen = Date.now();
|
|
if (runSse.runRequest) {
|
|
setBidiSessionRunRequest(session, runSse.runRequest);
|
|
runtime.logger?.debug?.(
|
|
`Cursor proxy captured AgentRunRequest from AgentService/RunSSE body for Bidi request ${requestId} ` +
|
|
`(body=${requestBody.length}, fields=${formatProtoFieldSummary(runSse.rpcBody)}, ` +
|
|
`${formatAgentRunRequestSummary(runtime, runSse.runRequest)}).`
|
|
);
|
|
const chatRequest = convertAgentRunRequestToOpenAIChat(runtime, runSse.runRequest);
|
|
if (chatRequest && isSimplifiedAgentChatRequest(chatRequest)) {
|
|
logCursorDecodeDiagnostics(
|
|
runtime,
|
|
"warn",
|
|
`Cursor proxy RunSSE AgentRunRequest decoded without Cursor context for ${requestId}: ` +
|
|
formatAgentRunRequestDecodeDiagnostic(runtime, runSse.runRequest, chatRequest) +
|
|
`; runsse_candidates=${formatCandidateBufferSummary(runtime, runSse.candidates)}`
|
|
);
|
|
} else {
|
|
logCursorDecodeDiagnostics(
|
|
runtime,
|
|
"debug",
|
|
`Cursor proxy RunSSE decode diagnostics for ${requestId}: ` +
|
|
formatAgentRunRequestDecodeDiagnostic(runtime, runSse.runRequest, chatRequest) +
|
|
`; runsse_candidates=${formatCandidateBufferSummary(runtime, runSse.candidates)}`
|
|
);
|
|
}
|
|
}
|
|
if (!session.runRequest) {
|
|
const runRequest = extractAgentRunRequestFromBidiSession(session);
|
|
if (runRequest) {
|
|
setBidiSessionRunRequest(session, runRequest);
|
|
}
|
|
}
|
|
runtime.logger?.debug?.(`Cursor proxy handling AgentService/RunSSE for Bidi request ${requestId}.`);
|
|
await handleAgentRunSse(runtime, request, response, session);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
async function handleAgentRunSse(runtime, request, response, session) {
|
|
response.writeHead(200, corsHeaders(connectProtoHeaders()));
|
|
|
|
const initialRunRequest = session.runRequest || await waitForBidiRunRequest(session, runtime.bidiWaitMs);
|
|
if (!initialRunRequest) {
|
|
runtime.logger?.warn?.(
|
|
`Cursor proxy timed out waiting for BidiAppend AgentRunRequest for ${session.requestId} ` +
|
|
`(${formatBidiSessionDiagnostic(session)}).`
|
|
);
|
|
writeCursorConnectMessage(response, encodeCursorAgentTextDelta(
|
|
"Cursor proxy did not receive the AgentRunRequest payload for this RunSSE request."
|
|
));
|
|
writeCursorConnectMessage(response, encodeCursorAgentTurnEnded());
|
|
finishCursorConnectStream(response);
|
|
return;
|
|
}
|
|
|
|
await waitForBidiRunRequestSettle(session, runtime.bidiSettleMs, runtime.bidiMaxSettleMs);
|
|
const runRequest = session.runRequest || initialRunRequest;
|
|
let chatRequest = convertAgentRunRequestToOpenAIChat(runtime, runRequest);
|
|
if (!chatRequest) {
|
|
runtime.logger?.warn?.(`Cursor proxy could not decode AgentRunRequest ${session.requestId} into chat messages.`);
|
|
writeCursorConnectMessage(response, encodeCursorAgentTextDelta(
|
|
"Cursor proxy could not decode this Cursor Agent request into a gateway chat request."
|
|
));
|
|
writeCursorConnectMessage(response, encodeCursorAgentTurnEnded());
|
|
finishCursorConnectStream(response);
|
|
return;
|
|
}
|
|
|
|
chatRequest = applyCollectedCursorContext(runtime, session, runRequest, chatRequest);
|
|
|
|
if (isSimplifiedAgentChatRequest(chatRequest) && runtime.bidiMissingContextWaitMs > 0) {
|
|
const richer = await waitForRicherBidiChatRequest(runtime, session, runtime.bidiMissingContextWaitMs, chatRequest);
|
|
if (richer) {
|
|
chatRequest = richer.chatRequest;
|
|
}
|
|
}
|
|
|
|
chatRequest = applyCollectedCursorContext(runtime, session, session.runRequest || runRequest, chatRequest);
|
|
logAgentChatConversion(runtime, session, chatRequest);
|
|
await streamAgentChatWithCursorToolBridge(runtime, request, response, session, chatRequest);
|
|
}
|
|
|
|
async function streamAgentChatWithCursorToolBridge(runtime, request, response, session, initialChatRequest) {
|
|
let chatRequest = initialChatRequest;
|
|
let runRequest = session.runRequest;
|
|
let observedRunRequestVersion = session.runRequestVersion || 0;
|
|
const maxRounds = Math.max(1, Math.trunc(Number(runtime.maxToolCallRounds) || DEFAULT_CURSOR_TOOL_BRIDGE_MAX_ROUNDS));
|
|
|
|
for (let round = 0; round < maxRounds; round += 1) {
|
|
const bridgeContext = buildCursorToolBridgeContext(runtime, session, runRequest, chatRequest, round);
|
|
const outcome = await streamGatewayChatToCursor(runtime, request, response, chatRequest, bridgeContext);
|
|
if (!outcome || outcome.type !== "tool_calls") {
|
|
return;
|
|
}
|
|
if (runtime.inlineToolCallContinuation !== true) {
|
|
runtime.logger?.debug?.(
|
|
`Cursor proxy handed bridged tool_calls to Cursor Agent for ${session.requestId}; ` +
|
|
`ending this RunSSE so Cursor can execute tools and send the next AgentRunRequest ` +
|
|
`(${summarizeGatewayToolCalls(outcome.toolCalls)}).`
|
|
);
|
|
writeCursorConnectMessage(response, encodeCursorAgentTurnEnded());
|
|
finishCursorConnectStream(response);
|
|
return;
|
|
}
|
|
|
|
const nextRunRequest = await waitForBidiRunRequestAfter(
|
|
session,
|
|
observedRunRequestVersion,
|
|
runtime.toolCallBridgeTimeoutMs
|
|
);
|
|
if (!nextRunRequest) {
|
|
runtime.logger?.warn?.(
|
|
`Cursor proxy bridged upstream tool_calls to Cursor Agent for ${session.requestId}, ` +
|
|
`but did not receive a follow-up AgentRunRequest with tool results within ` +
|
|
`${runtime.toolCallBridgeTimeoutMs}ms (${summarizeGatewayToolCalls(outcome.toolCalls)}).`
|
|
);
|
|
writeCursorConnectMessage(response, encodeCursorAgentTextDelta(
|
|
"\n\n[Cursor proxy] Cursor did not return tool results after the proxy emitted Cursor Agent tool events."
|
|
));
|
|
writeCursorConnectMessage(response, encodeCursorAgentTurnEnded());
|
|
finishCursorConnectStream(response);
|
|
return;
|
|
}
|
|
|
|
observedRunRequestVersion = session.runRequestVersion || observedRunRequestVersion;
|
|
await waitForBidiRunRequestSettle(session, runtime.bidiSettleMs, runtime.bidiMaxSettleMs);
|
|
runRequest = session.runRequest || nextRunRequest;
|
|
|
|
let nextChatRequest = convertAgentRunRequestToOpenAIChat(runtime, runRequest);
|
|
if (!nextChatRequest) {
|
|
runtime.logger?.warn?.(
|
|
`Cursor proxy received follow-up AgentRunRequest for ${session.requestId}, ` +
|
|
"but could not decode it into OpenAI chat messages after tool execution."
|
|
);
|
|
writeCursorConnectMessage(response, encodeCursorAgentTextDelta(
|
|
"\n\n[Cursor proxy] Cursor returned a follow-up payload, but the proxy could not decode the tool result context."
|
|
));
|
|
writeCursorConnectMessage(response, encodeCursorAgentTurnEnded());
|
|
finishCursorConnectStream(response);
|
|
return;
|
|
}
|
|
|
|
nextChatRequest = applyCollectedCursorContext(runtime, session, runRequest, nextChatRequest);
|
|
const resultMessages = findToolResultMessages(nextChatRequest?.messages, outcome.toolCalls);
|
|
logCursorToolBridgeFollowUp(
|
|
runtime,
|
|
session,
|
|
runRequest,
|
|
nextChatRequest,
|
|
outcome.toolCalls,
|
|
resultMessages,
|
|
round + 1
|
|
);
|
|
if (resultMessages.length === 0) {
|
|
runtime.logger?.warn?.(
|
|
`Cursor proxy received a follow-up AgentRunRequest for ${session.requestId} after bridging ` +
|
|
`tool_calls, but decoded no matching tool result messages; stopping instead of repeating the ` +
|
|
`same tool call (${summarizeGatewayToolCalls(outcome.toolCalls)}, ${formatChatRequestSummary(nextChatRequest)}).`
|
|
);
|
|
writeCursorConnectMessage(response, encodeCursorAgentTextDelta(
|
|
"\n\n[Cursor proxy] Cursor returned a follow-up request, but the proxy decoded no matching tool result message. " +
|
|
"Stopped before repeating the same tool call; check cursor-proxy decode dumps/logs for the follow-up payload."
|
|
));
|
|
writeCursorConnectMessage(response, encodeCursorAgentTurnEnded());
|
|
finishCursorConnectStream(response);
|
|
return;
|
|
}
|
|
writeCursorMcpToolResultEvents(runtime, response, outcome.toolCalls, bridgeContext, nextChatRequest, outcome.modelCallId);
|
|
chatRequest = mergeToolCallContinuationChatRequest(chatRequest, outcome.toolCalls, nextChatRequest);
|
|
logAgentToolContinuation(runtime, session, outcome.toolCalls, chatRequest, round + 1);
|
|
}
|
|
|
|
runtime.logger?.warn?.(
|
|
`Cursor proxy stopped tool-call bridge for ${session.requestId} after ${maxRounds} rounds ` +
|
|
`(${formatChatRequestSummary(chatRequest)}).`
|
|
);
|
|
writeCursorConnectMessage(response, encodeCursorAgentTextDelta(
|
|
`\n\n[Cursor proxy] Stopped after ${maxRounds} tool-call bridge rounds.`
|
|
));
|
|
writeCursorConnectMessage(response, encodeCursorAgentTurnEnded());
|
|
finishCursorConnectStream(response);
|
|
}
|
|
|
|
async function handleCursorNativeProtoRequest(runtime, request, response, url, requestBody) {
|
|
const method = (request.method || "GET").toUpperCase();
|
|
const path = normalizePath(url.pathname);
|
|
if (method !== "POST" || !isCursorNativeLlmRpcPath(runtime, path) || requestBody.length === 0) {
|
|
return false;
|
|
}
|
|
|
|
const chatRequest = convertCursorNativeRequestToOpenAIChat(runtime, path, requestBody);
|
|
if (!chatRequest) {
|
|
return false;
|
|
}
|
|
|
|
runtime.logger?.debug?.(
|
|
`Cursor proxy converted native Cursor RPC ${path} to OpenAI chat ` +
|
|
`(messages=${Array.isArray(chatRequest.messages) ? chatRequest.messages.length : 0}, ` +
|
|
`tools=${Array.isArray(chatRequest.tools) ? chatRequest.tools.length : 0}).`
|
|
);
|
|
if (isSimplifiedAgentChatRequest(chatRequest)) {
|
|
logCursorDecodeDiagnostics(
|
|
runtime,
|
|
"warn",
|
|
`Cursor proxy native Cursor RPC ${path} decoded without system/tools: ` +
|
|
formatCursorNativeProtoDecodeDiagnostic(runtime, path, requestBody, chatRequest)
|
|
);
|
|
}
|
|
response.writeHead(200, corsHeaders(connectProtoHeaders()));
|
|
await streamGatewayChatToCursor(runtime, request, response, chatRequest);
|
|
return true;
|
|
}
|
|
|
|
function convertCursorNativeRequestToOpenAIChat(runtime, path, requestBody) {
|
|
const candidateBuffers = cursorNativeCandidateBuffers(requestBody);
|
|
const runRequest = extractAgentRunRequestFromCandidates(candidateBuffers);
|
|
if (runRequest) {
|
|
return convertAgentRunRequestToOpenAIChat(runtime, runRequest);
|
|
}
|
|
|
|
const decodedValues = collectCursorNativeDecodedValues(candidateBuffers);
|
|
const fromDecodedValues = convertCursorDecodedValuesToOpenAIChat(runtime, path, decodedValues, {
|
|
loosePromptFallback: false,
|
|
requireInteractiveMessage: true
|
|
});
|
|
if (fromDecodedValues) {
|
|
return fromDecodedValues;
|
|
}
|
|
|
|
const jsonPayload = readJsonLikePayload(requestBody);
|
|
if (jsonPayload !== undefined) {
|
|
return convertCursorJsonPayloadToOpenAIChat(runtime, path, jsonPayload);
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
function cursorNativeCandidateBuffers(body) {
|
|
const candidates = [];
|
|
const decoded = decodeCursorProtoBody(body);
|
|
candidates.push(decoded.body);
|
|
candidates.push(...binaryCandidateBuffers(body));
|
|
if (decoded.body && !decoded.body.equals?.(body)) {
|
|
candidates.push(...binaryCandidateBuffers(decoded.body));
|
|
}
|
|
return uniqueBuffers(candidates);
|
|
}
|
|
|
|
function collectCursorNativeDecodedValues(candidateBuffers) {
|
|
const values = [];
|
|
for (const buffer of candidateBuffers) {
|
|
if (!Buffer.isBuffer(buffer) || buffer.length === 0) {
|
|
continue;
|
|
}
|
|
if (looksLikeTextBuffer(buffer)) {
|
|
values.push(buffer.toString("utf8"));
|
|
const parsed = readJsonText(buffer.toString("utf8"));
|
|
if (parsed !== undefined) {
|
|
values.push(parsed);
|
|
}
|
|
}
|
|
for (const item of decodeAllProtoStrings(buffer)) {
|
|
if (!item.value || looksLikeNoiseString(item.value)) {
|
|
continue;
|
|
}
|
|
values.push(item.value);
|
|
const parsed = readJsonText(item.value);
|
|
if (parsed !== undefined) {
|
|
values.push(parsed);
|
|
}
|
|
for (const decoded of decodeEmbeddedStringValues(item.value)) {
|
|
values.push(decoded);
|
|
}
|
|
}
|
|
}
|
|
return uniqueDecodedValues(values);
|
|
}
|
|
|
|
function convertCursorDecodedValuesToOpenAIChat(runtime, path, values, options = {}) {
|
|
if (!Array.isArray(values) || values.length === 0) {
|
|
return undefined;
|
|
}
|
|
|
|
const messages = extractMessagesFromValues(values);
|
|
if (messages.length === 0) {
|
|
const prompt = findFirstStringFromValues(values, [
|
|
"input",
|
|
"instruction",
|
|
"message",
|
|
"prompt",
|
|
"query",
|
|
"text",
|
|
"userMessage",
|
|
"user_message"
|
|
]) || (options.loosePromptFallback === false ? undefined : findBestPromptStringFromValues(values));
|
|
if (prompt) {
|
|
messages.push({ content: prompt, role: "user" });
|
|
}
|
|
}
|
|
if (messages.length === 0) {
|
|
return undefined;
|
|
}
|
|
|
|
const systemPrompt = selectPreferredSystemPrompt(
|
|
extractSystemPromptFromValues(values),
|
|
findBestSystemPromptStringFromValues(values)
|
|
);
|
|
if (systemPrompt && !messages.some((message) => message.role === "system")) {
|
|
messages.unshift({ content: systemPrompt, role: "system" });
|
|
}
|
|
|
|
const compactedMessages = compactChatMessages(messages);
|
|
if (compactedMessages.length === 0) {
|
|
return undefined;
|
|
}
|
|
if (options.requireInteractiveMessage !== false && !hasInteractiveChatMessage(compactedMessages)) {
|
|
return undefined;
|
|
}
|
|
|
|
const tools = extractToolsFromValues(values);
|
|
const toolChoice = extractToolChoiceFromValues(values);
|
|
const model =
|
|
findFirstStringFromValues(values, ["model", "modelName", "selectedModel", "intentModel", "chatModel"]) ||
|
|
runtime.defaultModel ||
|
|
"cursor-proxy";
|
|
|
|
return compactObject({
|
|
frequency_penalty: findFirstNumberFromValues(values, ["frequency_penalty", "frequencyPenalty"]),
|
|
max_tokens: findFirstNumberFromValues(values, ["max_tokens", "maxTokens", "maxOutputTokens", "output_tokens"]),
|
|
messages: compactedMessages,
|
|
model,
|
|
presence_penalty: findFirstNumberFromValues(values, ["presence_penalty", "presencePenalty"]),
|
|
stream: path.toLowerCase().includes("stream") || findFirstBooleanFromValues(values, ["stream", "shouldStream"]) !== false,
|
|
temperature: findFirstNumberFromValues(values, ["temperature"]),
|
|
tool_choice: toolChoice,
|
|
tools: tools.length > 0 ? tools : undefined,
|
|
top_p: findFirstNumberFromValues(values, ["top_p", "topP"])
|
|
});
|
|
}
|
|
|
|
function hasInteractiveChatMessage(messages) {
|
|
return messages.some((message) => ["assistant", "tool", "user"].includes(message.role));
|
|
}
|
|
|
|
function normalizeCollectedCursorChatRequest(runtime, payload) {
|
|
const candidates = collectCursorContextPayloadCandidates(payload);
|
|
for (const candidate of candidates) {
|
|
const direct = normalizeCollectedOpenAIChatPayload(runtime, candidate);
|
|
if (direct) {
|
|
return direct;
|
|
}
|
|
|
|
const converted = convertCursorDecodedValuesToOpenAIChat(runtime, "/collector", [candidate], {
|
|
loosePromptFallback: false,
|
|
requireInteractiveMessage: false
|
|
});
|
|
if (converted) {
|
|
return converted;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function collectCursorContextPayloadCandidates(payload) {
|
|
const candidates = [];
|
|
const add = (value) => {
|
|
if (value === undefined || value === null) {
|
|
return;
|
|
}
|
|
if (typeof value === "string") {
|
|
const parsed = readJsonText(value);
|
|
if (parsed !== undefined) {
|
|
candidates.push(parsed);
|
|
}
|
|
return;
|
|
}
|
|
candidates.push(value);
|
|
};
|
|
|
|
if (isRecord(payload)) {
|
|
for (const key of [
|
|
"openai",
|
|
"openaiRequest",
|
|
"openai_request",
|
|
"chat",
|
|
"chatRequest",
|
|
"chat_request",
|
|
"request",
|
|
"body",
|
|
"payload",
|
|
"message"
|
|
]) {
|
|
add(payload[key]);
|
|
}
|
|
}
|
|
add(payload);
|
|
return uniqueDecodedValues(candidates);
|
|
}
|
|
|
|
function normalizeCollectedOpenAIChatPayload(runtime, payload) {
|
|
if (!isRecord(payload)) {
|
|
return undefined;
|
|
}
|
|
|
|
const messages = Array.isArray(payload.messages)
|
|
? compactChatMessages(payload.messages.map((message) => isRecord(message) ? message : undefined).filter(Boolean))
|
|
: extractMessages(payload);
|
|
const systemPrompt = extractSystemPrompt(payload);
|
|
if (systemPrompt && !messages.some((message) => message.role === "system")) {
|
|
messages.unshift({ content: systemPrompt, role: "system" });
|
|
}
|
|
|
|
const compactedMessages = compactChatMessages(messages);
|
|
const tools = Array.isArray(payload.tools) ? normalizeToolList(payload.tools) : extractTools(payload);
|
|
const toolChoice = normalizeToolChoice(payload.tool_choice ?? payload.toolChoice) ?? extractToolChoice(payload);
|
|
|
|
if (
|
|
compactedMessages.length === 0 &&
|
|
tools.length === 0 &&
|
|
!systemPrompt &&
|
|
toolChoice === undefined
|
|
) {
|
|
return undefined;
|
|
}
|
|
|
|
return compactObject({
|
|
frequency_penalty: numberFromParameter(payload.frequency_penalty ?? payload.frequencyPenalty),
|
|
max_tokens: numberFromParameter(payload.max_tokens ?? payload.maxTokens ?? payload.maxOutputTokens ?? payload.output_tokens),
|
|
messages: compactedMessages.length > 0 ? compactedMessages : undefined,
|
|
model: stringValue(payload.model) ||
|
|
stringValue(payload.modelName) ||
|
|
stringValue(payload.selectedModel) ||
|
|
runtime.defaultModel ||
|
|
"cursor-proxy",
|
|
parallel_tool_calls: typeof payload.parallel_tool_calls === "boolean" ? payload.parallel_tool_calls : undefined,
|
|
presence_penalty: numberFromParameter(payload.presence_penalty ?? payload.presencePenalty),
|
|
reasoning_effort: payload.reasoning_effort,
|
|
reasoning_split: payload.reasoning_split,
|
|
response_format: isRecord(payload.response_format) ? payload.response_format : undefined,
|
|
stream: payload.stream === undefined ? true : Boolean(payload.stream),
|
|
temperature: numberFromParameter(payload.temperature),
|
|
tool_choice: toolChoice,
|
|
tools: tools.length > 0 ? tools : undefined,
|
|
top_p: numberFromParameter(payload.top_p ?? payload.topP)
|
|
});
|
|
}
|
|
|
|
function convertCursorJsonPayloadToOpenAIChat(runtime, path, payload) {
|
|
const unwrapped = unwrapCursorJson(payload);
|
|
if (unwrapped === undefined) {
|
|
return undefined;
|
|
}
|
|
const converted = convertCursorDecodedValuesToOpenAIChat(runtime, path, [unwrapped], {
|
|
loosePromptFallback: false,
|
|
requireInteractiveMessage: true
|
|
});
|
|
if (converted) {
|
|
return converted;
|
|
}
|
|
const body = Buffer.from(`${JSON.stringify(unwrapped)}\n`, "utf8");
|
|
return convertCursorJsonToOpenAIChat(runtime, path, body);
|
|
}
|
|
|
|
function decodeBidiAppendRequest(body) {
|
|
const decodedBody = decodeCursorProtoBody(body);
|
|
const rpcBody = decodedBody.body;
|
|
const dataBytes = concatProtoBytesFields(rpcBody, 1);
|
|
const dataBinary = concatProtoBytesFields(rpcBody, 4);
|
|
return {
|
|
appendSeqno: decodeProtoIntField(rpcBody, 3),
|
|
bodyHex: body.subarray(0, 32).toString("hex"),
|
|
data: dataBytes.length > 0 ? dataBytes.toString("utf8") : "",
|
|
dataBytes,
|
|
dataHex: dataBytes.subarray(0, 32).toString("hex"),
|
|
dataBinary,
|
|
rawBody: body,
|
|
rpcBody,
|
|
rpcBodyEncoding: decodedBody.encoding,
|
|
rpcBodyHex: rpcBody.subarray(0, 32).toString("hex"),
|
|
requestId: decodeBidiAppendRequestId(rpcBody)
|
|
};
|
|
}
|
|
|
|
function decodeBidiAppendRequestId(rpcBody) {
|
|
const requestIdMessage = decodeProtoMessageFields(rpcBody, 2)[0];
|
|
return decodeProtoStringField(requestIdMessage, 1) ||
|
|
decodeProtoStringField(rpcBody, 2) ||
|
|
findFirstUuidString(requestIdMessage) ||
|
|
findFirstUuidString(rpcBody) ||
|
|
"";
|
|
}
|
|
|
|
function decodeRunSseRequest(body) {
|
|
const decodedBody = decodeCursorProtoBody(body);
|
|
const rpcBody = decodedBody.body;
|
|
const candidates = runSseCandidateBuffers(body, rpcBody);
|
|
return {
|
|
candidates,
|
|
rpcBodyEncoding: decodedBody.encoding,
|
|
requestId: decodeBidiRequestId(rpcBody),
|
|
rpcBody,
|
|
runRequest: extractAgentRunRequestFromCandidates(candidates)
|
|
};
|
|
}
|
|
|
|
function decodeBidiRequestId(body) {
|
|
return decodeProtoStringField(body, 1) ||
|
|
decodeProtoStringField(decodeProtoMessageFields(body, 1)[0], 1) ||
|
|
findFirstUuidString(body) ||
|
|
"";
|
|
}
|
|
|
|
function findFirstUuidString(buffer) {
|
|
for (const item of decodeAllProtoStrings(buffer)) {
|
|
const match = item.value.match(UUID_PATTERN);
|
|
if (match) {
|
|
return match[0];
|
|
}
|
|
}
|
|
return "";
|
|
}
|
|
|
|
function createSyntheticBidiRequestId() {
|
|
return `runsse-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
}
|
|
|
|
function decodeCursorProtoBody(body) {
|
|
const fallbackBody = decodeConnectEnvelope(body);
|
|
const candidates = [];
|
|
const addCandidate = (buffer, encoding) => {
|
|
if (Buffer.isBuffer(buffer) && buffer.length > 0) {
|
|
candidates.push({ body: buffer, encoding });
|
|
}
|
|
};
|
|
|
|
addCandidate(fallbackBody, "connect");
|
|
for (const frame of decodeConnectMessages(body)) {
|
|
addCandidate(frame, "connect-frame");
|
|
}
|
|
|
|
const decompressedBody = decompressConnectPayload(body);
|
|
if (decompressedBody) {
|
|
addCandidate(decompressedBody, "compressed");
|
|
for (const frame of decodeConnectMessages(decompressedBody)) {
|
|
addCandidate(frame, "compressed-connect-frame");
|
|
}
|
|
}
|
|
|
|
for (const candidate of candidates) {
|
|
if (hasReadableProtoFields(candidate.body)) {
|
|
return candidate;
|
|
}
|
|
}
|
|
return { body: fallbackBody, encoding: "raw" };
|
|
}
|
|
|
|
function appendBidiSessionRequest(session, append) {
|
|
session.lastSeen = Date.now();
|
|
session.appendSeqno = append.appendSeqno;
|
|
let key = append.appendSeqno || session.appends.size + 1;
|
|
while (session.appends.has(key)) {
|
|
key += 1e-6;
|
|
}
|
|
session.appends.set(key, append);
|
|
while (session.appends.size > 128) {
|
|
const firstKey = session.appends.keys().next().value;
|
|
session.appends.delete(firstKey);
|
|
}
|
|
return extractAgentRunRequestFromBidiSession(session);
|
|
}
|
|
|
|
function extractAgentRunRequestFromBidiSession(session) {
|
|
return extractAgentRunRequestFromCandidates(bidiSessionCandidateBuffers(session));
|
|
}
|
|
|
|
function bidiSessionCandidateBuffers(session) {
|
|
const appends = sortedBidiAppends(session);
|
|
const candidates = [];
|
|
|
|
const combinedDataBinary = Buffer.concat(appends.map((append) => append.dataBinary).filter((buffer) => buffer.length > 0));
|
|
if (combinedDataBinary.length > 0) {
|
|
candidates.push(combinedDataBinary);
|
|
candidates.push(...decodeConnectMessages(combinedDataBinary));
|
|
}
|
|
|
|
const combinedDataBytes = Buffer.concat(appends.map((append) => append.dataBytes).filter((buffer) => buffer.length > 0));
|
|
if (combinedDataBytes.length > 0) {
|
|
candidates.push(combinedDataBytes);
|
|
candidates.push(...decodeConnectMessages(combinedDataBytes));
|
|
}
|
|
|
|
const combinedData = appends.map((append) => append.data || "").join("");
|
|
if (combinedData) {
|
|
candidates.push(...stringCandidateBuffers(combinedData));
|
|
candidates.push(...stringCandidateBuffers(combinedData.replace(/\s+/g, "")));
|
|
}
|
|
|
|
for (const append of appends) {
|
|
candidates.push(...bidiAppendCandidateBuffers(append));
|
|
}
|
|
|
|
return candidates;
|
|
}
|
|
|
|
function runSseCandidateBuffers(body, rpcBody) {
|
|
const candidates = [];
|
|
candidates.push(...binaryCandidateBuffers(rpcBody));
|
|
candidates.push(...binaryCandidateBuffers(body));
|
|
return candidates;
|
|
}
|
|
|
|
function sortedBidiAppends(session) {
|
|
return [...session.appends.entries()]
|
|
.sort((left, right) => Number(left[0]) - Number(right[0]))
|
|
.map((entry) => entry[1]);
|
|
}
|
|
|
|
function combinedAppendDataLength(session) {
|
|
return sortedBidiAppends(session).reduce((total, append) => total + append.data.length, 0);
|
|
}
|
|
|
|
function combinedAppendDataBytesLength(session) {
|
|
return sortedBidiAppends(session).reduce((total, append) => total + append.dataBytes.length, 0);
|
|
}
|
|
|
|
function combinedAppendDataBinaryLength(session) {
|
|
return sortedBidiAppends(session).reduce((total, append) => total + append.dataBinary.length, 0);
|
|
}
|
|
|
|
function formatBidiSessionDiagnostic(session) {
|
|
const appends = sortedBidiAppends(session);
|
|
const lastAppend = appends[appends.length - 1];
|
|
return [
|
|
`appends=${session.appends.size}`,
|
|
`last_seq=${lastAppend?.appendSeqno || session.appendSeqno || 0}`,
|
|
`combined_data=${combinedAppendDataLength(session)}`,
|
|
`combined_data_bytes=${combinedAppendDataBytesLength(session)}`,
|
|
`combined_data_binary=${combinedAppendDataBinaryLength(session)}`,
|
|
`last_data=${lastAppend?.data.length || 0}`,
|
|
`last_data_bytes=${lastAppend?.dataBytes.length || 0}`,
|
|
`last_data_hex=${lastAppend?.dataHex || ""}`,
|
|
`last_data_binary=${lastAppend?.dataBinary.length || 0}`,
|
|
`last_fields=${lastAppend ? formatProtoFieldSummary(lastAppend.rpcBody) : "empty"}`,
|
|
`last_data_binary_fields=${lastAppend ? formatProtoFieldSummary(lastAppend.dataBinary) : "empty"}`,
|
|
`append_details=${formatBidiAppendDetails(appends)}`
|
|
].join(", ");
|
|
}
|
|
|
|
function formatBidiAppendDetails(appends) {
|
|
if (!appends.length) {
|
|
return "empty";
|
|
}
|
|
return appends.slice(-16).map((append) => {
|
|
return [
|
|
`seq:${append.appendSeqno || 0}`,
|
|
`data:${append.data.length}`,
|
|
`data_bytes:${append.dataBytes.length}`,
|
|
`data_hex:${append.dataHex}`,
|
|
`data_binary:${append.dataBinary.length}`,
|
|
`fields:${formatProtoFieldSummary(append.rpcBody)}`,
|
|
`data_binary_fields:${formatProtoFieldSummary(append.dataBinary)}`
|
|
].join("/");
|
|
}).join("|");
|
|
}
|
|
|
|
function extractAgentRunRequestFromBidiAppend(append) {
|
|
return extractAgentRunRequestFromCandidates(bidiAppendCandidateBuffers(append));
|
|
}
|
|
|
|
function bidiAppendCandidateBuffers(append) {
|
|
const candidates = [];
|
|
candidates.push(...binaryCandidateBuffers(append.rawBody));
|
|
candidates.push(...binaryCandidateBuffers(append.rpcBody));
|
|
if (append.dataBinary.length > 0) {
|
|
candidates.push(...binaryCandidateBuffers(append.dataBinary));
|
|
}
|
|
if (append.dataBytes.length > 0) {
|
|
candidates.push(...binaryCandidateBuffers(append.dataBytes));
|
|
}
|
|
if (append.data) {
|
|
candidates.push(...stringCandidateBuffers(append.data));
|
|
}
|
|
return candidates;
|
|
}
|
|
|
|
function binaryCandidateBuffers(buffer) {
|
|
if (!Buffer.isBuffer(buffer) || buffer.length === 0) {
|
|
return [];
|
|
}
|
|
const candidates = [buffer];
|
|
const framed = decodeConnectMessages(buffer);
|
|
if (framed.length > 0 && !(framed.length === 1 && framed[0] === buffer)) {
|
|
candidates.push(...framed);
|
|
}
|
|
const decompressed = decompressConnectPayload(buffer);
|
|
if (decompressed && decompressed.length > 0 && !decompressed.equals(buffer)) {
|
|
candidates.push(decompressed);
|
|
candidates.push(...decodeConnectMessages(decompressed));
|
|
}
|
|
candidates.push(...lengthPrefixedCandidateBuffers(buffer));
|
|
|
|
if (looksLikeTextBuffer(buffer)) {
|
|
candidates.push(...stringCandidateBuffers(buffer.toString("utf8")));
|
|
}
|
|
return candidates;
|
|
}
|
|
|
|
function lengthPrefixedCandidateBuffers(buffer) {
|
|
const candidates = [];
|
|
const varintLength = readProtoVarint(buffer, 0);
|
|
if (varintLength) {
|
|
const start = varintLength.offset;
|
|
const end = start + Number(varintLength.value);
|
|
if (end > start && end <= buffer.length) {
|
|
candidates.push(buffer.subarray(start, end));
|
|
}
|
|
}
|
|
|
|
if (buffer.length >= 4) {
|
|
const bigEndianLength = buffer.readUInt32BE(0);
|
|
if (bigEndianLength > 0 && bigEndianLength <= buffer.length - 4) {
|
|
candidates.push(buffer.subarray(4, 4 + bigEndianLength));
|
|
}
|
|
const littleEndianLength = buffer.readUInt32LE(0);
|
|
if (littleEndianLength > 0 && littleEndianLength <= buffer.length - 4) {
|
|
candidates.push(buffer.subarray(4, 4 + littleEndianLength));
|
|
}
|
|
}
|
|
|
|
return candidates;
|
|
}
|
|
|
|
function looksLikeTextBuffer(buffer) {
|
|
const sampleLength = Math.min(buffer.length, 512);
|
|
if (sampleLength === 0) {
|
|
return false;
|
|
}
|
|
let textLike = 0;
|
|
for (let index = 0; index < sampleLength; index += 1) {
|
|
const byte = buffer[index];
|
|
if (byte === 9 || byte === 10 || byte === 13 || (byte >= 32 && byte <= 126)) {
|
|
textLike += 1;
|
|
}
|
|
}
|
|
return textLike / sampleLength > 0.9;
|
|
}
|
|
|
|
function stringCandidateBuffers(value) {
|
|
const candidates = [];
|
|
const text = String(value || "");
|
|
if (!text) {
|
|
return candidates;
|
|
}
|
|
candidates.push(Buffer.from(text, "utf8"));
|
|
const trimmed = text.trim();
|
|
if (!trimmed) {
|
|
return candidates;
|
|
}
|
|
for (const candidate of base64CandidateBuffers(trimmed)) {
|
|
candidates.push(candidate);
|
|
}
|
|
if (/^(?:[0-9a-f]{2})+$/i.test(trimmed)) {
|
|
candidates.push(Buffer.from(trimmed, "hex"));
|
|
}
|
|
if (/%[0-9a-f]{2}/i.test(trimmed)) {
|
|
try {
|
|
candidates.push(...stringCandidateBuffers(decodeURIComponent(trimmed)));
|
|
} catch {
|
|
// Not URI encoded.
|
|
}
|
|
}
|
|
const parsed = readJsonText(trimmed);
|
|
if (parsed) {
|
|
for (const item of findStringValuesByKeys(parsed, ["data", "dataBinary", "data_binary", "payload", "message"])) {
|
|
candidates.push(...stringCandidateBuffers(item));
|
|
}
|
|
}
|
|
return candidates;
|
|
}
|
|
|
|
function decodeEmbeddedStringValues(value, depth = 0) {
|
|
if (depth > 3 || typeof value !== "string") {
|
|
return [];
|
|
}
|
|
const trimmed = value.trim();
|
|
if (!trimmed || looksLikeNoiseString(trimmed)) {
|
|
return [];
|
|
}
|
|
|
|
const decoded = [];
|
|
const parsed = readJsonText(trimmed);
|
|
if (parsed !== undefined) {
|
|
decoded.push(parsed);
|
|
for (const item of findStringValuesByKeys(parsed, ["body", "data", "dataBinary", "data_binary", "message", "payload", "request"])) {
|
|
decoded.push(...decodeEmbeddedStringValues(item, depth + 1));
|
|
}
|
|
}
|
|
|
|
if (/%[0-9a-f]{2}/i.test(trimmed)) {
|
|
try {
|
|
const uriDecoded = decodeURIComponent(trimmed);
|
|
if (uriDecoded !== trimmed) {
|
|
decoded.push(uriDecoded);
|
|
decoded.push(...decodeEmbeddedStringValues(uriDecoded, depth + 1));
|
|
}
|
|
} catch {
|
|
// Not URI encoded.
|
|
}
|
|
}
|
|
|
|
for (const buffer of base64CandidateBuffers(trimmed)) {
|
|
if (!Buffer.isBuffer(buffer) || buffer.length === 0) {
|
|
continue;
|
|
}
|
|
if (looksLikeTextBuffer(buffer)) {
|
|
const text = buffer.toString("utf8").trim();
|
|
if (text && text !== trimmed && !looksLikeNoiseString(text)) {
|
|
decoded.push(text);
|
|
decoded.push(...decodeEmbeddedStringValues(text, depth + 1));
|
|
}
|
|
}
|
|
for (const item of decodeAllProtoStrings(buffer)) {
|
|
if (item.value && !looksLikeNoiseString(item.value)) {
|
|
decoded.push(item.value);
|
|
decoded.push(...decodeEmbeddedStringValues(item.value, depth + 1));
|
|
}
|
|
}
|
|
}
|
|
|
|
return uniqueDecodedValues(decoded);
|
|
}
|
|
|
|
function uniqueBuffers(buffers) {
|
|
const result = [];
|
|
const seen = new Set();
|
|
for (const buffer of buffers) {
|
|
if (!Buffer.isBuffer(buffer) || buffer.length === 0) {
|
|
continue;
|
|
}
|
|
const key = `${buffer.length}:${buffer.subarray(0, 32).toString("hex")}`;
|
|
if (seen.has(key)) {
|
|
continue;
|
|
}
|
|
seen.add(key);
|
|
result.push(buffer);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function uniqueDecodedValues(values) {
|
|
const result = [];
|
|
const seen = new Set();
|
|
for (const value of values) {
|
|
if (value === undefined || value === null) {
|
|
continue;
|
|
}
|
|
const key = typeof value === "string" ? `s:${value}` : `j:${safeJsonStringify(value)}`;
|
|
if (seen.has(key)) {
|
|
continue;
|
|
}
|
|
seen.add(key);
|
|
result.push(value);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function safeJsonStringify(value) {
|
|
try {
|
|
return JSON.stringify(value);
|
|
} catch {
|
|
return String(value);
|
|
}
|
|
}
|
|
|
|
function base64CandidateBuffers(text) {
|
|
const candidates = [];
|
|
const compact = text.replace(/\s+/g, "");
|
|
if (!compact || compact.length < 4) {
|
|
return candidates;
|
|
}
|
|
if (/^[A-Za-z0-9+/]+={0,2}$/.test(compact)) {
|
|
candidates.push(Buffer.from(padBase64(compact), "base64"));
|
|
}
|
|
if (/^[A-Za-z0-9_-]+={0,2}$/.test(compact)) {
|
|
candidates.push(Buffer.from(padBase64(compact), "base64url"));
|
|
}
|
|
return candidates;
|
|
}
|
|
|
|
function padBase64(value) {
|
|
const remainder = value.length % 4;
|
|
return remainder === 0 ? value : `${value}${"=".repeat(4 - remainder)}`;
|
|
}
|
|
|
|
function extractAgentRunRequestFromCandidates(candidates) {
|
|
const uniqueCandidates = [];
|
|
const seen = new Set();
|
|
for (const candidate of candidates) {
|
|
if (!Buffer.isBuffer(candidate) || candidate.length === 0) {
|
|
continue;
|
|
}
|
|
const key = `${candidate.length}:${candidate.subarray(0, 16).toString("hex")}`;
|
|
if (seen.has(key)) {
|
|
continue;
|
|
}
|
|
seen.add(key);
|
|
uniqueCandidates.push(candidate);
|
|
const runRequest = extractAgentRunRequest(candidate, 0);
|
|
if (runRequest) {
|
|
return runRequest;
|
|
}
|
|
}
|
|
for (const candidate of uniqueCandidates) {
|
|
const runRequest = extractAgentRunRequestFromOffsets(candidate);
|
|
if (runRequest) {
|
|
return runRequest;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function extractAgentRunRequest(message, depth) {
|
|
if (!Buffer.isBuffer(message) || message.length === 0 || depth > 8) {
|
|
return undefined;
|
|
}
|
|
|
|
if (looksLikeAgentRunRequest(message)) {
|
|
return message;
|
|
}
|
|
|
|
const runRequest = decodeProtoMessageFields(message, 1)[0];
|
|
if (looksLikeAgentRunRequest(runRequest)) {
|
|
return runRequest;
|
|
}
|
|
|
|
for (const field of readLengthDelimitedProtoFields(message)) {
|
|
const nested = extractAgentRunRequest(field.value, depth + 1);
|
|
if (nested) {
|
|
return nested;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function extractAgentRunRequestFromOffsets(message) {
|
|
if (!Buffer.isBuffer(message) || message.length < 2) {
|
|
return undefined;
|
|
}
|
|
const maxOffset = Math.min(16, message.length - 1);
|
|
for (let offset = 1; offset <= maxOffset; offset += 1) {
|
|
const nested = extractAgentRunRequest(message.subarray(offset), 1);
|
|
if (nested) {
|
|
return nested;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function looksLikeAgentRunRequest(message) {
|
|
if (!Buffer.isBuffer(message) || message.length === 0) {
|
|
return false;
|
|
}
|
|
return Boolean(
|
|
looksLikeConversationState(decodeProtoMessageFields(message, 1)[0]) ||
|
|
looksLikeConversationAction(decodeProtoMessageFields(message, 2)[0]) ||
|
|
looksLikeModelDetails(decodeProtoMessageFields(message, 3)[0]) ||
|
|
looksLikeRequestedModel(decodeProtoMessageFields(message, 9)[0]) ||
|
|
decodeProtoStringField(message, 5) ||
|
|
decodeProtoStringField(message, 8) ||
|
|
decodeProtoStringField(message, 18)
|
|
);
|
|
}
|
|
|
|
function looksLikeConversationState(message) {
|
|
return Buffer.isBuffer(message) && (
|
|
decodeProtoStringFields(message, 1).length > 0 ||
|
|
decodeProtoMessageFields(message, 8).length > 0
|
|
);
|
|
}
|
|
|
|
function looksLikeConversationAction(message) {
|
|
if (!Buffer.isBuffer(message)) {
|
|
return false;
|
|
}
|
|
const userMessageAction = decodeProtoMessageFields(message, 1)[0];
|
|
const startPlanAction = decodeProtoMessageFields(message, 6)[0];
|
|
return Boolean(
|
|
decodeUserMessage(decodeProtoMessageFields(userMessageAction, 1)[0]) ||
|
|
decodeProtoMessageFields(userMessageAction, 4).some((item) => decodeUserMessage(item)) ||
|
|
decodeUserMessage(decodeProtoMessageFields(startPlanAction, 1)[0])
|
|
);
|
|
}
|
|
|
|
function looksLikeModelDetails(message) {
|
|
return Buffer.isBuffer(message) && Boolean(decodeProtoStringField(message, 1));
|
|
}
|
|
|
|
function looksLikeRequestedModel(message) {
|
|
return Buffer.isBuffer(message) && Boolean(decodeProtoStringField(message, 1));
|
|
}
|
|
|
|
function getBidiSession(runtime, requestId, create) {
|
|
cleanupBidiSessions(runtime);
|
|
let session = runtime.bidiSessions.get(requestId);
|
|
if (!session && create) {
|
|
session = {
|
|
createdAt: Date.now(),
|
|
lastSeen: Date.now(),
|
|
lastRunRequestAt: 0,
|
|
requestId,
|
|
runRequest: undefined,
|
|
appends: new Map(),
|
|
waiters: []
|
|
};
|
|
runtime.bidiSessions.set(requestId, session);
|
|
}
|
|
return session;
|
|
}
|
|
|
|
function findSingleWaitingBidiSession(runtime) {
|
|
cleanupBidiSessions(runtime);
|
|
const waitingSessions = [...runtime.bidiSessions.values()].filter((session) => session.waiters.length > 0);
|
|
return waitingSessions.length === 1 ? waitingSessions[0] : undefined;
|
|
}
|
|
|
|
function cleanupBidiSessions(runtime) {
|
|
const now = Date.now();
|
|
for (const [requestId, session] of runtime.bidiSessions) {
|
|
if (now - session.lastSeen > runtime.bidiSessionTtlMs) {
|
|
runtime.bidiSessions.delete(requestId);
|
|
}
|
|
}
|
|
}
|
|
|
|
function describeBidiSessions(runtime) {
|
|
cleanupBidiSessions(runtime);
|
|
return [...runtime.bidiSessions.values()]
|
|
.map((session) => describeBidiSession(runtime, session))
|
|
.sort((left, right) => right.lastSeenAt - left.lastSeenAt)
|
|
.slice(0, 25);
|
|
}
|
|
|
|
function describeBidiSession(runtime, session) {
|
|
const appends = sortedBidiAppends(session);
|
|
const chatRequest = session.runRequest ? convertAgentRunRequestToOpenAIChat(runtime, session.runRequest) : undefined;
|
|
return {
|
|
ageMs: Date.now() - session.createdAt,
|
|
appendDetails: formatBidiAppendDetails(appends),
|
|
appendSeqno: session.appendSeqno || 0,
|
|
appends: appends.length,
|
|
chat: chatRequest ? summarizeChatRequest(chatRequest) : undefined,
|
|
combinedDataBinaryBytes: combinedAppendDataBinaryLength(session),
|
|
combinedDataBytes: combinedAppendDataBytesLength(session),
|
|
combinedDataTextBytes: combinedAppendDataLength(session),
|
|
decodedHints: summarizeBidiDecodedHints(session),
|
|
lastRunRequestAt: session.lastRunRequestAt || 0,
|
|
lastSeenAgoMs: Date.now() - session.lastSeen,
|
|
lastSeenAt: session.lastSeen,
|
|
requestId: session.requestId,
|
|
waiters: session.waiters.length
|
|
};
|
|
}
|
|
|
|
function summarizeBidiDecodedHints(session) {
|
|
const strings = [];
|
|
const addStringsFromBuffer = (buffer) => {
|
|
if (!Buffer.isBuffer(buffer) || buffer.length === 0) {
|
|
return;
|
|
}
|
|
if (looksLikeTextBuffer(buffer)) {
|
|
strings.push(buffer.toString("utf8"));
|
|
}
|
|
for (const item of decodeAllProtoStrings(buffer)) {
|
|
strings.push(item.value);
|
|
}
|
|
};
|
|
|
|
for (const append of sortedBidiAppends(session)) {
|
|
addStringsFromBuffer(append.rpcBody);
|
|
addStringsFromBuffer(append.dataBytes);
|
|
addStringsFromBuffer(append.dataBinary);
|
|
}
|
|
|
|
const uniqueStrings = [...new Set(strings.filter((value) => value && !looksLikeNoiseString(value)))];
|
|
const toolKeyPattern = new RegExp(`\\b(?:${CURSOR_TOOL_KEYS.map(escapeRegExp).join("|")})\\b`, "i");
|
|
return {
|
|
jsonLikeStrings: uniqueStrings.filter((value) => readJsonText(value) !== undefined).length,
|
|
maxStringLength: uniqueStrings.reduce((max, value) => Math.max(max, value.length), 0),
|
|
strings: uniqueStrings.length,
|
|
systemLikeStrings: uniqueStrings.filter(looksLikeSystemPromptText).length,
|
|
toolLikeStrings: uniqueStrings.filter((value) => toolKeyPattern.test(value)).length
|
|
};
|
|
}
|
|
|
|
function setBidiSessionRunRequest(session, runRequest) {
|
|
session.runRequest = runRequest;
|
|
session.lastRunRequestAt = Date.now();
|
|
session.runRequestVersion = (session.runRequestVersion || 0) + 1;
|
|
const waiters = session.waiters.splice(0);
|
|
waiters.forEach((resolve) => resolve(runRequest));
|
|
}
|
|
|
|
function waitForBidiRunRequest(session, timeoutMs) {
|
|
if (session.runRequest) {
|
|
return Promise.resolve(session.runRequest);
|
|
}
|
|
return new Promise((resolve) => {
|
|
const timer = setTimeout(() => {
|
|
const index = session.waiters.indexOf(done);
|
|
if (index >= 0) {
|
|
session.waiters.splice(index, 1);
|
|
}
|
|
resolve(undefined);
|
|
}, timeoutMs);
|
|
const done = (runRequest) => {
|
|
clearTimeout(timer);
|
|
resolve(runRequest);
|
|
};
|
|
session.waiters.push(done);
|
|
});
|
|
}
|
|
|
|
function waitForBidiRunRequestAfter(session, runRequestVersion, timeoutMs) {
|
|
const observedVersion = Math.max(0, Number(runRequestVersion) || 0);
|
|
if ((session.runRequestVersion || 0) > observedVersion && session.runRequest) {
|
|
return Promise.resolve(session.runRequest);
|
|
}
|
|
|
|
return new Promise((resolve) => {
|
|
const timer = setTimeout(() => {
|
|
const index = session.waiters.indexOf(done);
|
|
if (index >= 0) {
|
|
session.waiters.splice(index, 1);
|
|
}
|
|
resolve(undefined);
|
|
}, timeoutMs);
|
|
const done = (runRequest) => {
|
|
clearTimeout(timer);
|
|
if ((session.runRequestVersion || 0) > observedVersion) {
|
|
resolve(runRequest);
|
|
} else {
|
|
resolve(undefined);
|
|
}
|
|
};
|
|
session.waiters.push(done);
|
|
});
|
|
}
|
|
|
|
async function waitForBidiRunRequestSettle(session, settleMs, maxSettleMs) {
|
|
const settleWindow = Math.max(0, Number(settleMs) || 0);
|
|
const maxWindow = Math.max(settleWindow, Number(maxSettleMs) || 0);
|
|
if (!session.runRequest || settleWindow <= 0 || maxWindow <= 0) {
|
|
return;
|
|
}
|
|
|
|
const startedAt = Date.now();
|
|
while (Date.now() - startedAt < maxWindow) {
|
|
const updatedAt = session.lastRunRequestAt || startedAt;
|
|
const quietFor = Date.now() - updatedAt;
|
|
if (quietFor >= settleWindow) {
|
|
return;
|
|
}
|
|
const remainingSettle = settleWindow - quietFor;
|
|
const remainingMax = maxWindow - (Date.now() - startedAt);
|
|
await sleep(Math.max(1, Math.min(remainingSettle, remainingMax)));
|
|
}
|
|
}
|
|
|
|
function sleep(ms) {
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
}
|
|
|
|
async function waitForRicherBidiChatRequest(runtime, session, timeoutMs, baseChatRequest) {
|
|
const startedAt = Date.now();
|
|
const deadline = startedAt + Math.max(0, Number(timeoutMs) || 0);
|
|
let observedRunRequestAt = session.lastRunRequestAt || startedAt;
|
|
let currentChatRequest = baseChatRequest;
|
|
|
|
while (Date.now() < deadline) {
|
|
if (session.runRequest) {
|
|
const decoded = convertAgentRunRequestToOpenAIChat(runtime, session.runRequest);
|
|
if (decoded) {
|
|
currentChatRequest = decoded;
|
|
}
|
|
}
|
|
|
|
const collected = findCollectedCursorContext(runtime, session, session.runRequest, currentChatRequest);
|
|
if (collected) {
|
|
runtime.logger?.debug?.(
|
|
`Cursor proxy found collected Cursor context for ${session.requestId} after ` +
|
|
`${Date.now() - startedAt}ms (${formatChatRequestSummary(collected.chatRequest)}).`
|
|
);
|
|
return { chatRequest: collected.chatRequest, runRequest: session.runRequest };
|
|
}
|
|
|
|
if ((session.lastRunRequestAt || 0) > observedRunRequestAt && session.runRequest) {
|
|
observedRunRequestAt = session.lastRunRequestAt;
|
|
const chatRequest = currentChatRequest || convertAgentRunRequestToOpenAIChat(runtime, session.runRequest);
|
|
if (chatRequest && !isSimplifiedAgentChatRequest(chatRequest)) {
|
|
runtime.logger?.debug?.(
|
|
`Cursor proxy found richer AgentRunRequest for ${session.requestId} after ` +
|
|
`${Date.now() - startedAt}ms (${formatChatRequestSummary(chatRequest)}).`
|
|
);
|
|
return { chatRequest, runRequest: session.runRequest };
|
|
}
|
|
}
|
|
await sleep(Math.min(100, Math.max(1, deadline - Date.now())));
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
function collectCursorContextKeys(payload, chatRequest) {
|
|
const keys = new Set();
|
|
const add = (value) => {
|
|
const text = stringValue(value);
|
|
if (!text) {
|
|
return;
|
|
}
|
|
keys.add(text);
|
|
for (const match of text.matchAll(new RegExp(UUID_PATTERN.source, "ig"))) {
|
|
keys.add(match[0]);
|
|
}
|
|
};
|
|
|
|
if (isRecord(payload)) {
|
|
for (const key of [
|
|
"requestId",
|
|
"request_id",
|
|
"bidiRequestId",
|
|
"bidi_request_id",
|
|
"runId",
|
|
"run_id",
|
|
"conversationId",
|
|
"conversation_id",
|
|
"clientRequestId",
|
|
"client_request_id",
|
|
"chatId",
|
|
"chat_id",
|
|
"threadId",
|
|
"thread_id",
|
|
"sessionId",
|
|
"session_id",
|
|
"traceId",
|
|
"trace_id"
|
|
]) {
|
|
add(payload[key]);
|
|
}
|
|
}
|
|
|
|
for (const value of findStringValuesByKeys(payload, [
|
|
"requestId",
|
|
"request_id",
|
|
"bidiRequestId",
|
|
"bidi_request_id",
|
|
"conversationId",
|
|
"conversation_id",
|
|
"clientRequestId",
|
|
"client_request_id"
|
|
])) {
|
|
add(value);
|
|
}
|
|
|
|
add(lastUserMessageText(chatRequest));
|
|
if (keys.size === 0) {
|
|
keys.add("__latest__");
|
|
}
|
|
keys.add("__latest__");
|
|
return [...keys];
|
|
}
|
|
|
|
function storeCursorContext(runtime, context) {
|
|
cleanupCursorContexts(runtime);
|
|
const keys = context.keys.length > 0 ? context.keys : ["__latest__"];
|
|
const stored = { ...context, keys };
|
|
for (const key of keys) {
|
|
runtime.cursorContexts.set(key, stored);
|
|
}
|
|
trimCursorContexts(runtime);
|
|
return stored;
|
|
}
|
|
|
|
function cleanupCursorContexts(runtime) {
|
|
const ttlMs = Math.max(0, Number(runtime.cursorContextTtlMs) || 0);
|
|
if (ttlMs <= 0) {
|
|
return;
|
|
}
|
|
const now = Date.now();
|
|
for (const [key, context] of runtime.cursorContexts) {
|
|
if (now - context.createdAt > ttlMs) {
|
|
runtime.cursorContexts.delete(key);
|
|
}
|
|
}
|
|
}
|
|
|
|
function trimCursorContexts(runtime) {
|
|
const maxEntries = Math.max(1, Number(runtime.cursorContextMaxEntries) || DEFAULT_CURSOR_CONTEXT_MAX_ENTRIES);
|
|
while (runtime.cursorContexts.size > maxEntries) {
|
|
let oldestKey;
|
|
let oldestAt = Infinity;
|
|
for (const [key, context] of runtime.cursorContexts) {
|
|
if (context.createdAt < oldestAt) {
|
|
oldestAt = context.createdAt;
|
|
oldestKey = key;
|
|
}
|
|
}
|
|
if (!oldestKey) {
|
|
return;
|
|
}
|
|
runtime.cursorContexts.delete(oldestKey);
|
|
}
|
|
}
|
|
|
|
function findCollectedCursorContext(runtime, session, runRequest, chatRequest) {
|
|
if (!runtime.cursorContextCollector || !runtime.cursorContexts?.size) {
|
|
return undefined;
|
|
}
|
|
cleanupCursorContexts(runtime);
|
|
|
|
const keys = new Set([session?.requestId]);
|
|
if (runRequest) {
|
|
for (const item of decodeAllProtoStrings(runRequest)) {
|
|
for (const match of item.value.matchAll(new RegExp(UUID_PATTERN.source, "ig"))) {
|
|
keys.add(match[0]);
|
|
}
|
|
}
|
|
}
|
|
for (const key of keys) {
|
|
const context = key ? runtime.cursorContexts.get(key) : undefined;
|
|
if (context) {
|
|
return context;
|
|
}
|
|
}
|
|
|
|
if (chatRequest) {
|
|
const matchedByPrompt = findCollectedCursorContextByPrompt(runtime, chatRequest);
|
|
if (matchedByPrompt) {
|
|
return matchedByPrompt;
|
|
}
|
|
}
|
|
|
|
if (!chatRequest) {
|
|
const latest = runtime.cursorContexts.get("__latest__");
|
|
return latest && Date.now() - latest.createdAt <= runtime.cursorContextTtlMs ? latest : undefined;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function findCollectedCursorContextByPrompt(runtime, chatRequest) {
|
|
const prompt = lastUserMessageText(chatRequest);
|
|
if (!prompt) {
|
|
return undefined;
|
|
}
|
|
|
|
const uniqueContexts = uniqueContextsNewestFirst(runtime.cursorContexts);
|
|
return uniqueContexts.find((context) => chatRequestContainsUserText(context.chatRequest, prompt));
|
|
}
|
|
|
|
function uniqueContextsNewestFirst(contextsByKey) {
|
|
const seen = new Set();
|
|
const contexts = [];
|
|
for (const context of contextsByKey.values()) {
|
|
if (seen.has(context)) {
|
|
continue;
|
|
}
|
|
seen.add(context);
|
|
contexts.push(context);
|
|
}
|
|
return contexts.sort((left, right) => right.createdAt - left.createdAt);
|
|
}
|
|
|
|
function chatRequestContainsUserText(chatRequest, text) {
|
|
const normalizedNeedle = normalizePromptFingerprint(text);
|
|
if (!normalizedNeedle) {
|
|
return false;
|
|
}
|
|
return (Array.isArray(chatRequest?.messages) ? chatRequest.messages : []).some((message) => {
|
|
if (message.role !== "user") {
|
|
return false;
|
|
}
|
|
const normalizedContent = normalizePromptFingerprint(stringifyContent(message.content));
|
|
return normalizedContent === normalizedNeedle ||
|
|
normalizedContent.includes(normalizedNeedle) ||
|
|
normalizedNeedle.includes(normalizedContent);
|
|
});
|
|
}
|
|
|
|
function normalizePromptFingerprint(value) {
|
|
return String(value || "").replace(/\s+/g, " ").trim().slice(-4000);
|
|
}
|
|
|
|
function lastUserMessageText(chatRequest) {
|
|
const messages = Array.isArray(chatRequest?.messages) ? chatRequest.messages : [];
|
|
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
if (messages[index]?.role === "user") {
|
|
return stringifyContent(messages[index].content) || "";
|
|
}
|
|
}
|
|
return "";
|
|
}
|
|
|
|
function applyCollectedCursorContext(runtime, session, runRequest, chatRequest) {
|
|
const collected = findCollectedCursorContext(runtime, session, runRequest, chatRequest);
|
|
if (!collected) {
|
|
return chatRequest;
|
|
}
|
|
|
|
const merged = mergeCollectedCursorChatRequest(chatRequest, collected.chatRequest);
|
|
runtime.logger?.debug?.(
|
|
`Cursor proxy applied collected Cursor context for ${session.requestId} ` +
|
|
`(${formatChatRequestSummary(merged)}).`
|
|
);
|
|
return merged;
|
|
}
|
|
|
|
function mergeCollectedCursorChatRequest(baseRequest, collectedRequest) {
|
|
const baseMessages = Array.isArray(baseRequest?.messages) ? baseRequest.messages : [];
|
|
const collectedMessages = Array.isArray(collectedRequest?.messages) ? collectedRequest.messages : [];
|
|
const collectedHasInteractiveMessage = hasInteractiveChatMessage(collectedMessages);
|
|
const messages = collectedHasInteractiveMessage
|
|
? collectedMessages
|
|
: compactChatMessages([
|
|
...collectedMessages,
|
|
...baseMessages
|
|
]);
|
|
|
|
return compactObject({
|
|
...baseRequest,
|
|
...collectedRequest,
|
|
messages: messages.length > 0 ? messages : baseMessages,
|
|
model: stringValue(collectedRequest?.model) || stringValue(baseRequest?.model) || "cursor-proxy",
|
|
stream: true,
|
|
tool_choice: collectedRequest?.tool_choice ?? baseRequest?.tool_choice,
|
|
tools: Array.isArray(collectedRequest?.tools) && collectedRequest.tools.length > 0
|
|
? collectedRequest.tools
|
|
: baseRequest?.tools
|
|
});
|
|
}
|
|
|
|
function convertAgentRunRequestToOpenAIChat(runtime, runRequest) {
|
|
const messages = [];
|
|
const contextValues = decodeAgentRunRequestContextValues(runRequest);
|
|
const fallbackSystemPrompt = decodeProtoStringField(runRequest, 5);
|
|
const systemPrompt = composeAgentSystemPrompt(
|
|
contextValues,
|
|
selectPreferredSystemPrompt(
|
|
decodeProtoStringField(runRequest, 8),
|
|
extractSystemPromptFromValues(contextValues),
|
|
findBestSystemPromptStringFromValues(contextValues),
|
|
looksLikeSystemPromptText(fallbackSystemPrompt) ? fallbackSystemPrompt : undefined
|
|
)
|
|
);
|
|
if (systemPrompt) {
|
|
messages.push({ content: systemPrompt, role: "system" });
|
|
}
|
|
|
|
const conversationState = decodeProtoMessageFields(runRequest, 1)[0];
|
|
if (conversationState) {
|
|
messages.push(...decodeConversationStateMessages(conversationState));
|
|
}
|
|
|
|
const action = decodeProtoMessageFields(runRequest, 2)[0];
|
|
if (action) {
|
|
messages.push(...decodeConversationActionMessages(action));
|
|
}
|
|
|
|
const compactedMessages = compactChatMessages(messages);
|
|
if (compactedMessages.length === 0) {
|
|
const fallbackPrompt = decodeAllProtoStrings(runRequest)
|
|
.map((item) => item.value)
|
|
.filter((value) => value.length > 3 && !looksLikeNoiseString(value))
|
|
.sort((a, b) => b.length - a.length)[0];
|
|
if (fallbackPrompt) {
|
|
compactedMessages.push({ content: fallbackPrompt, role: "user" });
|
|
}
|
|
}
|
|
if (compactedMessages.length === 0) {
|
|
return undefined;
|
|
}
|
|
|
|
const requestedModel = decodeProtoMessageFields(runRequest, 9)[0];
|
|
const modelDetails = decodeProtoMessageFields(runRequest, 3)[0];
|
|
const parameters = decodeRequestedModelParameters(requestedModel);
|
|
const decodedTools = uniqueTools([
|
|
...extractToolsFromValues(contextValues),
|
|
...extractCursorNativeToolsFromRunRequest(runtime, runRequest, contextValues, systemPrompt)
|
|
]);
|
|
const tools = shouldForwardDecodedCursorTools(runtime) ? decodedTools : [];
|
|
const toolChoice = tools.length > 0 ? extractToolChoiceFromValues(contextValues) : undefined;
|
|
const model =
|
|
decodeProtoStringField(requestedModel, 1) ||
|
|
decodeProtoStringField(modelDetails, 1) ||
|
|
decodeProtoStringField(runRequest, 18) ||
|
|
runtime.defaultModel ||
|
|
"cursor-proxy";
|
|
|
|
return compactObject({
|
|
max_tokens: numberFromParameter(parameters.max_tokens || parameters.maxTokens || parameters.output_tokens),
|
|
messages: compactedMessages,
|
|
model,
|
|
stream: true,
|
|
temperature: numberFromParameter(parameters.temperature),
|
|
tool_choice: toolChoice,
|
|
tools: tools.length > 0 ? tools : undefined,
|
|
top_p: numberFromParameter(parameters.top_p || parameters.topP)
|
|
});
|
|
}
|
|
|
|
function logAgentChatConversion(runtime, session, chatRequest) {
|
|
const summary = summarizeChatRequest(chatRequest);
|
|
const decodedToolSummary = summarizeDecodedCursorToolForwarding(runtime, session?.runRequest, chatRequest);
|
|
const diagnostic =
|
|
`${formatChatSummary(summary)}, ` +
|
|
`decoded_tools=${decodedToolSummary.decodedTools}, ` +
|
|
`native_tool_enums=${decodedToolSummary.nativeToolEnums}, ` +
|
|
`suppressed_decoded_tools=${decodedToolSummary.suppressedTools}, ` +
|
|
`appends=${session.appends.size}, last_seq=${session.appendSeqno || 0}`;
|
|
const dumpPath = writeAgentRunRequestDecodeDump(runtime, session, session.runRequest, chatRequest, "initial");
|
|
const dumpSuffix = dumpPath ? ` decode_dump=${dumpPath}` : "";
|
|
const missingSystem = summary.system === 0;
|
|
const missingTools = summary.tools === 0 && decodedToolSummary.suppressedTools === 0;
|
|
|
|
if (decodedToolSummary.suppressedTools > 0 && runtime.warnedSuppressedDecodedCursorTools !== true) {
|
|
runtime.warnedSuppressedDecodedCursorTools = true;
|
|
runtime.logger?.warn?.(
|
|
`Cursor proxy decoded ${decodedToolSummary.suppressedTools} Cursor tools from AgentRunRequest, ` +
|
|
"but did not forward them to the upstream model because config.forwardDecodedCursorTools=false. " +
|
|
"OpenAI tool_calls -> Cursor Agent tool events bridge is available when decoded tools are forwarded." +
|
|
dumpSuffix
|
|
);
|
|
}
|
|
|
|
if (summary.simplified || missingSystem || missingTools) {
|
|
const missingContext = [
|
|
missingSystem ? "system" : "",
|
|
missingTools ? "tools" : ""
|
|
].filter(Boolean).join("/") || "context";
|
|
runtime.logger?.warn?.(
|
|
`Cursor proxy decoded AgentRunRequest ${session.requestId} without ${missingContext} (${diagnostic}). ` +
|
|
"The decoded Cursor payload does not expose Agent context; post full Cursor hook context to " +
|
|
"/plugins/cursor-proxy/collector, or configure cursor-proxy config.systemPrompt/config.tools " +
|
|
`for static fallback context if this Cursor flow omits it.${dumpSuffix}`
|
|
);
|
|
logCursorDecodeDiagnostics(
|
|
runtime,
|
|
"warn",
|
|
`Cursor proxy AgentRunRequest detailed decode diagnostics for ${session.requestId}: ` +
|
|
formatAgentRunRequestDecodeDiagnostic(runtime, session.runRequest, chatRequest) +
|
|
`; bidi_session=${formatBidiSessionDecodeDiagnostic(runtime, session)}`
|
|
);
|
|
return;
|
|
}
|
|
|
|
runtime.logger?.debug?.(`Cursor proxy decoded AgentRunRequest ${session.requestId} (${diagnostic}).${dumpSuffix}`);
|
|
if (Buffer.isBuffer(session.runRequest)) {
|
|
const contextValues = decodeAgentRunRequestContextValues(session.runRequest);
|
|
const systemPrompt = chatRequest?.messages?.find((message) => message?.role === "system")?.content;
|
|
const nativeExtraction = describeCursorNativeToolExtraction(runtime, session.runRequest, contextValues, systemPrompt);
|
|
runtime.logger?.debug?.(
|
|
`Cursor proxy native tool extraction for ${session.requestId}: ` +
|
|
safeJsonStringify(nativeExtraction)
|
|
);
|
|
}
|
|
}
|
|
|
|
function formatAgentRunRequestSummary(runtime, runRequest) {
|
|
const chatRequest = convertAgentRunRequestToOpenAIChat(runtime, runRequest);
|
|
return chatRequest ? formatChatRequestSummary(chatRequest) : "chat=unavailable";
|
|
}
|
|
|
|
function formatChatRequestSummary(chatRequest) {
|
|
return formatChatSummary(summarizeChatRequest(chatRequest));
|
|
}
|
|
|
|
function formatChatSummary(summary) {
|
|
return [
|
|
`messages=${summary.messages}`,
|
|
`system=${summary.system}`,
|
|
`user=${summary.user}`,
|
|
`assistant=${summary.assistant}`,
|
|
`tools=${summary.tools}`,
|
|
`roles=${summary.roles.join(",") || "none"}`
|
|
].join(", ");
|
|
}
|
|
|
|
function summarizeChatRequest(chatRequest) {
|
|
const messages = Array.isArray(chatRequest?.messages) ? chatRequest.messages : [];
|
|
const roles = messages.map((message) => normalizeRole(stringValue(message.role)) || "unknown");
|
|
const system = roles.filter((role) => role === "system").length;
|
|
const user = roles.filter((role) => role === "user").length;
|
|
const assistant = roles.filter((role) => role === "assistant").length;
|
|
const tools = Array.isArray(chatRequest?.tools) ? chatRequest.tools.length : 0;
|
|
return {
|
|
assistant,
|
|
messages: messages.length,
|
|
roles,
|
|
simplified: messages.length > 0 && system === 0 && tools === 0 && roles.every((role) => role === "user"),
|
|
system,
|
|
tools,
|
|
user
|
|
};
|
|
}
|
|
|
|
function shouldForwardDecodedCursorTools(runtime) {
|
|
return runtime?.forwardDecodedCursorTools === true;
|
|
}
|
|
|
|
function summarizeDecodedCursorToolForwarding(runtime, runRequest, chatRequest) {
|
|
const contextValues = Buffer.isBuffer(runRequest) ? decodeAgentRunRequestContextValues(runRequest) : [];
|
|
const decodedTools = Buffer.isBuffer(runRequest)
|
|
? uniqueTools([
|
|
...extractToolsFromValues(contextValues),
|
|
...extractCursorNativeToolsFromRunRequest(runtime, runRequest, contextValues)
|
|
])
|
|
: [];
|
|
const forwardedTools = Array.isArray(chatRequest?.tools) ? chatRequest.tools.length : 0;
|
|
const forwardDecodedCursorTools = shouldForwardDecodedCursorTools(runtime);
|
|
return {
|
|
decodedTools: decodedTools.length,
|
|
forwardDecodedCursorTools,
|
|
forwardedTools,
|
|
nativeToolEnums: Buffer.isBuffer(runRequest) ? extractCursorSupportedToolEnums(runRequest).length : 0,
|
|
suppressedTools: forwardDecodedCursorTools ? 0 : decodedTools.length
|
|
};
|
|
}
|
|
|
|
function shouldLogDecodeDiagnostics(runtime) {
|
|
return runtime?.decodeDiagnostics !== false;
|
|
}
|
|
|
|
function logCursorDecodeDiagnostics(runtime, level, message) {
|
|
if (!shouldLogDecodeDiagnostics(runtime)) {
|
|
return;
|
|
}
|
|
const logger = runtime?.logger;
|
|
if (level === "warn") {
|
|
logger?.warn?.(message);
|
|
return;
|
|
}
|
|
logger?.debug?.(message);
|
|
}
|
|
|
|
function formatAgentRunRequestDecodeDiagnostic(runtime, runRequest, chatRequest) {
|
|
if (!Buffer.isBuffer(runRequest)) {
|
|
return safeJsonStringify({ error: "missing-run-request" });
|
|
}
|
|
|
|
const embeddedPayloads = decodeJsonPayloadsFromProtoStrings(runRequest);
|
|
const contextValues = decodeAgentRunRequestContextValues(runRequest);
|
|
const fallbackSystemPrompt = decodeProtoStringField(runRequest, 5);
|
|
const systemPrompt = composeAgentSystemPrompt(
|
|
contextValues,
|
|
selectPreferredSystemPrompt(
|
|
decodeProtoStringField(runRequest, 8),
|
|
extractSystemPromptFromValues(contextValues),
|
|
findBestSystemPromptStringFromValues(contextValues),
|
|
looksLikeSystemPromptText(fallbackSystemPrompt) ? fallbackSystemPrompt : undefined
|
|
)
|
|
);
|
|
const tools = extractToolsFromValues(contextValues);
|
|
const toolChoice = extractToolChoiceFromValues(contextValues);
|
|
const toolFilePaths = extractCursorToolFilePaths(contextValues);
|
|
const instructionFilePaths = extractCursorInstructionFilePaths(contextValues);
|
|
const instructionTexts = extractCursorInstructionTexts(contextValues);
|
|
const nativeSupportedTools = extractCursorSupportedToolEnums(runRequest);
|
|
const nativeTools = extractCursorNativeToolsFromRunRequest(runtime, runRequest, contextValues, systemPrompt);
|
|
const decodedTools = uniqueTools([...tools, ...nativeTools]);
|
|
const toolForwarding = summarizeDecodedCursorToolForwarding(runtime, runRequest, chatRequest);
|
|
const nativeExtraction = describeCursorNativeToolExtraction(runtime, runRequest, contextValues, systemPrompt);
|
|
|
|
return safeJsonStringify(compactObject({
|
|
bytes: runRequest.length,
|
|
chat: chatRequest ? summarizeChatRequest(chatRequest) : undefined,
|
|
contextValues: summarizeDecodedValuesForDiagnostics(runtime, contextValues),
|
|
directStrings: compactObject({
|
|
field5: summarizeLogString(runtime, fallbackSystemPrompt),
|
|
field8: summarizeLogString(runtime, decodeProtoStringField(runRequest, 8)),
|
|
field18: summarizeLogString(runtime, decodeProtoStringField(runRequest, 18))
|
|
}),
|
|
embeddedJson: summarizeJsonPayloadsForDiagnostics(runtime, embeddedPayloads),
|
|
extracted: compactObject({
|
|
instructionFiles: instructionFilePaths.length,
|
|
instructionFileSamples: instructionFilePaths.slice(0, decodeDiagnosticSampleLimit(runtime)),
|
|
instructionTexts: instructionTexts.length,
|
|
nativeExtraction,
|
|
nativeSupportedToolEnums: summarizeCursorSupportedToolEnums(runtime, nativeSupportedTools),
|
|
nativeToolNames: summarizeToolNamesForDiagnostics(runtime, nativeTools),
|
|
nativeTools: nativeTools.length,
|
|
systemPrompt: summarizeLogString(runtime, systemPrompt),
|
|
toolFiles: toolFilePaths.length,
|
|
toolFileSamples: toolFilePaths.slice(0, decodeDiagnosticSampleLimit(runtime)),
|
|
toolChoice,
|
|
toolForwarding,
|
|
toolNames: summarizeToolNamesForDiagnostics(runtime, decodedTools),
|
|
tools: decodedTools.length
|
|
}),
|
|
fields: formatProtoFieldSummary(runRequest),
|
|
shapeReasons: agentRunRequestShapeReasons(runRequest),
|
|
strings: summarizeProtoStringsForDiagnostics(runtime, decodeAllProtoStrings(runRequest)),
|
|
topLevel: summarizeAgentRunRequestTopLevel(runRequest)
|
|
}));
|
|
}
|
|
|
|
function formatBidiSessionDecodeDiagnostic(runtime, session) {
|
|
if (!session) {
|
|
return safeJsonStringify({ error: "missing-session" });
|
|
}
|
|
const appends = sortedBidiAppends(session);
|
|
return safeJsonStringify({
|
|
appendDetails: formatBidiAppendDetails(appends),
|
|
appends: appends.length,
|
|
candidates: summarizeCandidateBuffersForDiagnostics(runtime, bidiSessionCandidateBuffers(session)),
|
|
combinedDataBinaryBytes: combinedAppendDataBinaryLength(session),
|
|
combinedDataBytes: combinedAppendDataBytesLength(session),
|
|
combinedDataTextBytes: combinedAppendDataLength(session),
|
|
decodedHints: summarizeBidiDecodedHints(session),
|
|
lastRunRequestAt: session.lastRunRequestAt || 0,
|
|
requestId: session.requestId
|
|
});
|
|
}
|
|
|
|
function formatCursorNativeProtoDecodeDiagnostic(runtime, path, requestBody, chatRequest) {
|
|
const candidates = cursorNativeCandidateBuffers(requestBody);
|
|
const runRequest = extractAgentRunRequestFromCandidates(candidates);
|
|
return safeJsonStringify(compactObject({
|
|
candidates: summarizeCandidateBuffersForDiagnostics(runtime, candidates),
|
|
chat: summarizeChatRequest(chatRequest),
|
|
path,
|
|
requestBytes: Buffer.isBuffer(requestBody) ? requestBody.length : 0,
|
|
runRequest: runRequest ? JSON.parse(formatAgentRunRequestDecodeDiagnostic(runtime, runRequest, chatRequest)) : undefined
|
|
}));
|
|
}
|
|
|
|
function writeAgentRunRequestDecodeDump(runtime, session, runRequest, chatRequest, label = "") {
|
|
if (!shouldLogDecodeDiagnostics(runtime) || runtime?.decodeDumpFull === false || !Buffer.isBuffer(runRequest)) {
|
|
return "";
|
|
}
|
|
const dumpDir = stringValue(runtime.decodeDumpDir) || DEFAULT_DECODE_DUMP_DIR;
|
|
try {
|
|
fs.mkdirSync(dumpDir, { recursive: true });
|
|
const labelSuffix = label ? `-${sanitizeRouteId(label)}` : "";
|
|
const fileName = `${sanitizeRouteId(session?.requestId || "agent-run")}${labelSuffix}-${Date.now()}.json`;
|
|
const filePath = path.join(dumpDir, fileName);
|
|
fs.writeFileSync(
|
|
filePath,
|
|
`${JSON.stringify(buildAgentRunRequestDecodeDump(runtime, session, runRequest, chatRequest, label), null, 2)}\n`,
|
|
"utf8"
|
|
);
|
|
return filePath;
|
|
} catch (error) {
|
|
runtime?.logger?.warn?.(`Cursor proxy failed to write full decode dump: ${formatError(error)}`);
|
|
return "";
|
|
}
|
|
}
|
|
|
|
function buildAgentRunRequestDecodeDump(runtime, session, runRequest, chatRequest, label = "") {
|
|
const contextValues = decodeAgentRunRequestContextValues(runRequest);
|
|
const embeddedPayloads = decodeJsonPayloadsFromProtoStrings(runRequest);
|
|
const toolFilePaths = extractCursorToolFilePaths(contextValues);
|
|
const instructionFilePaths = extractCursorInstructionFilePaths(contextValues);
|
|
const tools = extractToolsFromValues(contextValues);
|
|
const nativeSupportedTools = extractCursorSupportedToolEnums(runRequest);
|
|
const nativeTools = extractCursorNativeToolsFromRunRequest(
|
|
runtime,
|
|
runRequest,
|
|
contextValues,
|
|
chatRequest?.messages?.find((message) => message?.role === "system")?.content
|
|
);
|
|
const appends = session ? sortedBidiAppends(session) : [];
|
|
const toolForwarding = summarizeDecodedCursorToolForwarding(runtime, runRequest, chatRequest);
|
|
|
|
return {
|
|
appends: appends.map((append) => ({
|
|
appendSeqno: append.appendSeqno,
|
|
dataBase64: append.dataBytes.length > 0 ? append.dataBytes.toString("base64") : undefined,
|
|
dataBinaryBase64: append.dataBinary.length > 0 ? append.dataBinary.toString("base64") : undefined,
|
|
dataBytes: append.dataBytes.length,
|
|
dataText: append.data || undefined,
|
|
requestId: append.requestId,
|
|
rpcBodyBase64: append.rpcBody.length > 0 ? append.rpcBody.toString("base64") : undefined,
|
|
rpcBodyBytes: append.rpcBody.length,
|
|
rpcBodyEncoding: append.rpcBodyEncoding
|
|
})),
|
|
chatRequest,
|
|
contextValues: contextValues.map((value) => serializeDiagnosticValue(value)),
|
|
embeddedJsonPayloads: embeddedPayloads.map((value) => serializeDiagnosticValue(value)),
|
|
extracted: {
|
|
instructionFileContents: instructionFilePaths.map((filePath) => ({
|
|
content: readTextFileIfSmall(filePath, 256 * 1024),
|
|
path: filePath
|
|
})),
|
|
instructionFilePaths,
|
|
nativeSupportedTools,
|
|
nativeExtraction: describeCursorNativeToolExtraction(
|
|
runtime,
|
|
runRequest,
|
|
contextValues,
|
|
chatRequest?.messages?.find((message) => message?.role === "system")?.content
|
|
),
|
|
nativeToolNames: nativeTools.map((tool) => stringValue(tool?.function?.name) || stringValue(tool?.name) || "unknown"),
|
|
nativeTools,
|
|
systemPrompt: chatRequest?.messages?.find((message) => message?.role === "system")?.content,
|
|
toolFileContents: toolFilePaths.map((filePath) => ({
|
|
json: readJsonText(readTextFileIfSmall(filePath, 256 * 1024)),
|
|
path: filePath
|
|
})),
|
|
toolFilePaths,
|
|
toolForwarding,
|
|
toolNames: uniqueTools([...tools, ...nativeTools])
|
|
.map((tool) => stringValue(tool?.function?.name) || stringValue(tool?.name) || stringValue(tool?.type) || "unknown"),
|
|
tools: uniqueTools([...tools, ...nativeTools])
|
|
},
|
|
label,
|
|
proto: decodeProtoTree(runRequest, {
|
|
maxDepth: Math.max(1, Math.trunc(Number(runtime?.decodeTreeDepth) || DEFAULT_DECODE_TREE_DEPTH)),
|
|
path: ""
|
|
}),
|
|
raw: {
|
|
base64: runRequest.toString("base64"),
|
|
bytes: runRequest.length,
|
|
fieldSummary: formatProtoFieldSummary(runRequest),
|
|
hex: runRequest.toString("hex")
|
|
},
|
|
requestId: session?.requestId || "",
|
|
strings: decodeAllProtoStringsWithPaths(runRequest),
|
|
summary: {
|
|
chat: chatRequest ? summarizeChatRequest(chatRequest) : undefined,
|
|
contextValues: summarizeDecodedValuesForDiagnostics(runtime, contextValues),
|
|
embeddedJson: summarizeJsonPayloadsForDiagnostics(runtime, embeddedPayloads),
|
|
fields: formatProtoFieldSummary(runRequest),
|
|
shapeReasons: agentRunRequestShapeReasons(runRequest)
|
|
},
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
}
|
|
|
|
function decodeProtoTree(buffer, options = {}, depth = 0, seen = new Set()) {
|
|
if (!Buffer.isBuffer(buffer)) {
|
|
return { error: "not-buffer" };
|
|
}
|
|
const maxDepth = Math.max(0, Math.trunc(Number(options.maxDepth) || DEFAULT_DECODE_TREE_DEPTH));
|
|
const currentPath = stringValue(options.path) || "";
|
|
const key = `${buffer.length}:${buffer.subarray(0, 32).toString("hex")}`;
|
|
if (seen.has(key)) {
|
|
return { bytes: buffer.length, circular: true, path: currentPath };
|
|
}
|
|
seen.add(key);
|
|
|
|
const fields = [];
|
|
forEachProtoField(buffer, (field) => {
|
|
const fieldPath = currentPath ? `${currentPath}.${field.fieldNumber}` : String(field.fieldNumber);
|
|
fields.push(decodeProtoFieldForDump(field, fieldPath, maxDepth, depth, seen));
|
|
});
|
|
seen.delete(key);
|
|
|
|
return {
|
|
bytes: buffer.length,
|
|
fields,
|
|
path: currentPath
|
|
};
|
|
}
|
|
|
|
function decodeProtoFieldForDump(field, fieldPath, maxDepth, depth, seen) {
|
|
const base = {
|
|
fieldNumber: field.fieldNumber,
|
|
path: fieldPath,
|
|
wireType: field.wireType,
|
|
wireTypeName: protoWireTypeName(field.wireType)
|
|
};
|
|
if (field.wireType === 0) {
|
|
return { ...base, value: field.value.toString() };
|
|
}
|
|
if (field.wireType === 1 || field.wireType === 5) {
|
|
return {
|
|
...base,
|
|
base64: field.value.toString("base64"),
|
|
bytes: field.value.length,
|
|
hex: field.value.toString("hex")
|
|
};
|
|
}
|
|
if (field.wireType !== 2) {
|
|
return base;
|
|
}
|
|
|
|
const text = field.value.length > 0 && looksLikeTextBuffer(field.value) ? field.value.toString("utf8") : undefined;
|
|
const parsedJson = text !== undefined ? readJsonText(text) : undefined;
|
|
const canNest = depth < maxDepth && field.value.length > 0 && hasReadableProtoFields(field.value);
|
|
return compactObject({
|
|
...base,
|
|
base64: field.value.toString("base64"),
|
|
bytes: field.value.length,
|
|
children: canNest ? decodeProtoTree(field.value, { maxDepth, path: fieldPath }, depth + 1, seen).fields : undefined,
|
|
json: parsedJson !== undefined ? serializeDiagnosticValue(parsedJson) : undefined,
|
|
text
|
|
});
|
|
}
|
|
|
|
function decodeAllProtoStringsWithPaths(buffer, pathPrefix = "", depth = 0) {
|
|
if (!Buffer.isBuffer(buffer) || depth > 12) {
|
|
return [];
|
|
}
|
|
const values = [];
|
|
forEachProtoField(buffer, (field) => {
|
|
const fieldPath = pathPrefix ? `${pathPrefix}.${field.fieldNumber}` : String(field.fieldNumber);
|
|
if (field.wireType !== 2 || field.value.length === 0) {
|
|
return;
|
|
}
|
|
if (looksLikeTextBuffer(field.value)) {
|
|
const text = field.value.toString("utf8");
|
|
values.push({
|
|
fieldNumber: field.fieldNumber,
|
|
length: text.length,
|
|
path: fieldPath,
|
|
value: text
|
|
});
|
|
}
|
|
values.push(...decodeAllProtoStringsWithPaths(field.value, fieldPath, depth + 1));
|
|
});
|
|
return values;
|
|
}
|
|
|
|
function protoWireTypeName(wireType) {
|
|
if (wireType === 0) {
|
|
return "varint";
|
|
}
|
|
if (wireType === 1) {
|
|
return "fixed64";
|
|
}
|
|
if (wireType === 2) {
|
|
return "length-delimited";
|
|
}
|
|
if (wireType === 5) {
|
|
return "fixed32";
|
|
}
|
|
return "unknown";
|
|
}
|
|
|
|
function serializeDiagnosticValue(value, seen = new Set()) {
|
|
if (typeof value === "bigint") {
|
|
return value.toString();
|
|
}
|
|
if (Buffer.isBuffer(value)) {
|
|
return {
|
|
base64: value.toString("base64"),
|
|
bytes: value.length,
|
|
hex: value.toString("hex")
|
|
};
|
|
}
|
|
if (Array.isArray(value)) {
|
|
return value.map((item) => serializeDiagnosticValue(item, seen));
|
|
}
|
|
if (isRecord(value)) {
|
|
if (seen.has(value)) {
|
|
return "[Circular]";
|
|
}
|
|
seen.add(value);
|
|
const result = {};
|
|
for (const [key, rawValue] of Object.entries(value)) {
|
|
result[key] = serializeDiagnosticValue(rawValue, seen);
|
|
}
|
|
seen.delete(value);
|
|
return result;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function formatCandidateBufferSummary(runtime, candidates) {
|
|
return safeJsonStringify(summarizeCandidateBuffersForDiagnostics(runtime, candidates));
|
|
}
|
|
|
|
function summarizeCandidateBuffersForDiagnostics(runtime, candidates) {
|
|
const unique = uniqueBuffers(Array.isArray(candidates) ? candidates : []);
|
|
return {
|
|
samples: unique
|
|
.slice(0, decodeDiagnosticSampleLimit(runtime))
|
|
.map((buffer, index) => summarizeBufferForDiagnostics(runtime, buffer, index)),
|
|
total: Array.isArray(candidates) ? candidates.length : 0,
|
|
unique: unique.length
|
|
};
|
|
}
|
|
|
|
function summarizeBufferForDiagnostics(runtime, buffer, index) {
|
|
const strings = decodeAllProtoStrings(buffer);
|
|
return compactObject({
|
|
bytes: Buffer.isBuffer(buffer) ? buffer.length : 0,
|
|
fields: formatProtoFieldSummary(buffer),
|
|
headHex: Buffer.isBuffer(buffer) ? buffer.subarray(0, 32).toString("hex") : undefined,
|
|
index,
|
|
shapeReasons: agentRunRequestShapeReasons(buffer),
|
|
strings: summarizeProtoStringsForDiagnostics(runtime, strings),
|
|
textSample: Buffer.isBuffer(buffer) && looksLikeTextBuffer(buffer)
|
|
? truncateForLog(runtime, buffer.toString("utf8"))
|
|
: undefined
|
|
});
|
|
}
|
|
|
|
function summarizeAgentRunRequestTopLevel(runRequest) {
|
|
const conversationState = decodeProtoMessageFields(runRequest, 1)[0];
|
|
const action = decodeProtoMessageFields(runRequest, 2)[0];
|
|
const modelDetails = decodeProtoMessageFields(runRequest, 3)[0];
|
|
const requestedModel = decodeProtoMessageFields(runRequest, 9)[0];
|
|
return compactObject({
|
|
action: summarizeProtoBufferForDiagnostics(action),
|
|
conversationActionMessages: action ? decodeConversationActionMessages(action).length : 0,
|
|
conversationState: summarizeProtoBufferForDiagnostics(conversationState),
|
|
conversationStateMessages: conversationState ? decodeConversationStateMessages(conversationState).length : 0,
|
|
modelDetails: summarizeProtoBufferForDiagnostics(modelDetails),
|
|
requestedModel: summarizeProtoBufferForDiagnostics(requestedModel)
|
|
});
|
|
}
|
|
|
|
function summarizeProtoBufferForDiagnostics(buffer) {
|
|
if (!Buffer.isBuffer(buffer) || buffer.length === 0) {
|
|
return undefined;
|
|
}
|
|
return {
|
|
bytes: buffer.length,
|
|
fields: formatProtoFieldSummary(buffer),
|
|
strings: decodeAllProtoStrings(buffer).length
|
|
};
|
|
}
|
|
|
|
function agentRunRequestShapeReasons(buffer) {
|
|
if (!Buffer.isBuffer(buffer) || buffer.length === 0) {
|
|
return [];
|
|
}
|
|
const reasons = [];
|
|
const conversationState = decodeProtoMessageFields(buffer, 1)[0];
|
|
const conversationAction = decodeProtoMessageFields(buffer, 2)[0];
|
|
const modelDetails = decodeProtoMessageFields(buffer, 3)[0];
|
|
const requestedModel = decodeProtoMessageFields(buffer, 9)[0];
|
|
const field5 = decodeProtoStringField(buffer, 5);
|
|
const field8 = decodeProtoStringField(buffer, 8);
|
|
const field18 = decodeProtoStringField(buffer, 18);
|
|
if (looksLikeConversationState(conversationState)) {
|
|
reasons.push("field1:conversation_state");
|
|
}
|
|
if (looksLikeConversationAction(conversationAction)) {
|
|
reasons.push("field2:conversation_action");
|
|
}
|
|
if (looksLikeModelDetails(modelDetails)) {
|
|
reasons.push("field3:model_details");
|
|
}
|
|
if (looksLikeRequestedModel(requestedModel)) {
|
|
reasons.push("field9:requested_model");
|
|
}
|
|
if (field5) {
|
|
reasons.push(`field5:string:${field5.length}`);
|
|
}
|
|
if (field8) {
|
|
reasons.push(`field8:string:${field8.length}`);
|
|
}
|
|
if (field18) {
|
|
reasons.push(`field18:string:${field18.length}`);
|
|
}
|
|
return reasons;
|
|
}
|
|
|
|
function summarizeProtoStringsForDiagnostics(runtime, strings) {
|
|
const values = Array.isArray(strings)
|
|
? strings.filter((item) => item && typeof item.value === "string" && item.value.length > 0)
|
|
: [];
|
|
const unique = [];
|
|
const seen = new Set();
|
|
const toolKeyPattern = new RegExp(`\\b(?:${CURSOR_TOOL_KEYS.map(escapeRegExp).join("|")})\\b`, "i");
|
|
for (const item of values) {
|
|
const key = `${item.fieldNumber}:${item.value}`;
|
|
if (seen.has(key)) {
|
|
continue;
|
|
}
|
|
seen.add(key);
|
|
unique.push(item);
|
|
}
|
|
return {
|
|
count: values.length,
|
|
jsonLike: values.filter((item) => readJsonText(item.value) !== undefined).length,
|
|
jsonLikeSamples: sampleProtoStringItemsForDiagnostics(
|
|
runtime,
|
|
unique.filter((item) => readJsonText(item.value) !== undefined)
|
|
),
|
|
maxLength: values.reduce((max, item) => Math.max(max, item.value.length), 0),
|
|
samples: unique
|
|
.filter((item) => !looksLikeNoiseString(item.value))
|
|
.slice(0, decodeDiagnosticSampleLimit(runtime))
|
|
.map((item) => ({
|
|
field: item.fieldNumber,
|
|
length: item.value.length,
|
|
sample: truncateForLog(runtime, item.value)
|
|
})),
|
|
systemLike: values.filter((item) => looksLikeSystemPromptText(item.value)).length,
|
|
systemLikeSamples: sampleProtoStringItemsForDiagnostics(
|
|
runtime,
|
|
unique.filter((item) => looksLikeSystemPromptText(item.value))
|
|
),
|
|
toolKeyLike: values.filter((item) => toolKeyPattern.test(item.value)).length,
|
|
toolKeyLikeSamples: sampleProtoStringItemsForDiagnostics(
|
|
runtime,
|
|
unique.filter((item) => toolKeyPattern.test(item.value))
|
|
),
|
|
unique: unique.length
|
|
};
|
|
}
|
|
|
|
function sampleProtoStringItemsForDiagnostics(runtime, items) {
|
|
return (Array.isArray(items) ? items : [])
|
|
.filter((item) => item && typeof item.value === "string" && !looksLikeNoiseString(item.value))
|
|
.slice(0, decodeDiagnosticSampleLimit(runtime))
|
|
.map((item) => ({
|
|
field: item.fieldNumber,
|
|
length: item.value.length,
|
|
sample: truncateForLog(runtime, item.value)
|
|
}));
|
|
}
|
|
|
|
function summarizeDecodedValuesForDiagnostics(runtime, values) {
|
|
const list = Array.isArray(values) ? values : [];
|
|
const strings = list.filter((value) => typeof value === "string");
|
|
const records = list.filter(isRecord);
|
|
const arrays = list.filter(Array.isArray);
|
|
const toolKeyPattern = new RegExp(`\\b(?:${CURSOR_TOOL_KEYS.map(escapeRegExp).join("|")})\\b`, "i");
|
|
return {
|
|
arrays: arrays.length,
|
|
records: records.length,
|
|
samples: list
|
|
.filter((value) => typeof value === "string" && !looksLikeNoiseString(value))
|
|
.slice(0, decodeDiagnosticSampleLimit(runtime))
|
|
.map((value) => ({
|
|
length: value.length,
|
|
sample: truncateForLog(runtime, value)
|
|
})),
|
|
strings: strings.length,
|
|
systemLike: strings.filter(looksLikeSystemPromptText).length,
|
|
systemLikeSamples: strings
|
|
.filter(looksLikeSystemPromptText)
|
|
.slice(0, decodeDiagnosticSampleLimit(runtime))
|
|
.map((value) => ({
|
|
length: value.length,
|
|
sample: truncateForLog(runtime, value)
|
|
})),
|
|
toolKeyLike: strings.filter((value) => toolKeyPattern.test(value)).length,
|
|
toolKeyLikeSamples: strings
|
|
.filter((value) => toolKeyPattern.test(value))
|
|
.slice(0, decodeDiagnosticSampleLimit(runtime))
|
|
.map((value) => ({
|
|
length: value.length,
|
|
sample: truncateForLog(runtime, value)
|
|
})),
|
|
total: list.length
|
|
};
|
|
}
|
|
|
|
function summarizeJsonPayloadsForDiagnostics(runtime, payloads) {
|
|
const values = Array.isArray(payloads) ? payloads : [];
|
|
return {
|
|
count: values.length,
|
|
samples: values
|
|
.slice(0, decodeDiagnosticSampleLimit(runtime))
|
|
.map((value) => summarizeJsonPayloadForDiagnostics(runtime, value))
|
|
};
|
|
}
|
|
|
|
function summarizeJsonPayloadForDiagnostics(runtime, value) {
|
|
const tools = extractTools(value);
|
|
const messages = extractMessages(value);
|
|
return compactObject({
|
|
keys: isRecord(value) ? Object.keys(value).slice(0, 24) : undefined,
|
|
messageRoles: messages.map((message) => normalizeRole(stringValue(message.role)) || "unknown").slice(0, 16),
|
|
messages: messages.length,
|
|
systemPrompt: summarizeLogString(runtime, extractSystemPrompt(value)),
|
|
toolChoice: extractToolChoice(value),
|
|
toolNames: summarizeToolNamesForDiagnostics(runtime, tools),
|
|
tools: tools.length,
|
|
type: Array.isArray(value) ? "array" : typeof value
|
|
});
|
|
}
|
|
|
|
function summarizeToolNamesForDiagnostics(runtime, tools) {
|
|
if (!Array.isArray(tools) || tools.length === 0) {
|
|
return [];
|
|
}
|
|
return tools
|
|
.slice(0, decodeDiagnosticSampleLimit(runtime))
|
|
.map((tool) => stringValue(tool?.function?.name) || stringValue(tool?.name) || stringValue(tool?.type) || "unknown");
|
|
}
|
|
|
|
function summarizeLogString(runtime, value) {
|
|
if (typeof value !== "string" || value.length === 0) {
|
|
return undefined;
|
|
}
|
|
return {
|
|
length: value.length,
|
|
sample: truncateForLog(runtime, value)
|
|
};
|
|
}
|
|
|
|
function truncateForLog(runtime, value) {
|
|
const text = String(value || "").replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f]/g, "?");
|
|
const maxLength = Math.max(16, Math.trunc(Number(runtime?.decodeDiagnosticSampleChars) || DEFAULT_DECODE_DIAGNOSTIC_SAMPLE_CHARS));
|
|
return text.length > maxLength ? `${text.slice(0, maxLength)}...<${text.length}>` : text;
|
|
}
|
|
|
|
function decodeDiagnosticSampleLimit(runtime) {
|
|
return Math.max(1, Math.trunc(Number(runtime?.decodeDiagnosticSampleLimit) || DEFAULT_DECODE_DIAGNOSTIC_SAMPLE_LIMIT));
|
|
}
|
|
|
|
function isSimplifiedAgentChatRequest(chatRequest) {
|
|
return summarizeChatRequest(chatRequest).simplified;
|
|
}
|
|
|
|
function decodeConversationStateMessages(conversationState) {
|
|
const messages = [];
|
|
for (const rawJson of decodeProtoStringFields(conversationState, 1)) {
|
|
const parsed = readJsonText(rawJson);
|
|
const normalized = normalizeMessages(parsed);
|
|
if (normalized.length > 0) {
|
|
messages.push(...normalized);
|
|
} else if (parsed !== undefined && typeof parsed !== "string") {
|
|
const embeddedMessages = extractMessages(parsed);
|
|
const systemPrompt = extractSystemPrompt(parsed);
|
|
if (systemPrompt) {
|
|
messages.push({ content: systemPrompt, role: "system" });
|
|
}
|
|
if (embeddedMessages.length > 0) {
|
|
messages.push(...embeddedMessages);
|
|
}
|
|
} else if (typeof parsed === "string" && parsed.trim()) {
|
|
messages.push({ content: parsed.trim(), role: "system" });
|
|
} else if (rawJson.trim()) {
|
|
messages.push({ content: rawJson.trim(), role: "system" });
|
|
}
|
|
}
|
|
|
|
for (const turn of decodeProtoMessageFields(conversationState, 8)) {
|
|
messages.push(...decodeConversationTurnMessages(turn));
|
|
}
|
|
return messages;
|
|
}
|
|
|
|
function decodeConversationTurnMessages(turn) {
|
|
const messages = [];
|
|
const agentTurn = decodeProtoMessageFields(turn, 1)[0];
|
|
if (!agentTurn) {
|
|
return messages;
|
|
}
|
|
|
|
const userMessage = decodeUserMessage(decodeProtoMessageFields(agentTurn, 1)[0]);
|
|
if (userMessage) {
|
|
messages.push({ content: userMessage, role: "user" });
|
|
}
|
|
|
|
const assistantChunks = [];
|
|
for (const step of decodeProtoMessageFields(agentTurn, 2)) {
|
|
const assistantMessage = decodeProtoMessageFields(step, 1)[0];
|
|
const text = decodeProtoStringField(assistantMessage, 1);
|
|
if (text) {
|
|
assistantChunks.push(text);
|
|
}
|
|
}
|
|
if (assistantChunks.length > 0) {
|
|
messages.push({ content: assistantChunks.join("\n"), role: "assistant" });
|
|
}
|
|
return messages;
|
|
}
|
|
|
|
function decodeConversationActionMessages(action) {
|
|
const messages = [];
|
|
const userMessageAction = decodeProtoMessageFields(action, 1)[0];
|
|
if (userMessageAction) {
|
|
for (const prepend of decodeProtoMessageFields(userMessageAction, 4)) {
|
|
const text = decodeUserMessage(prepend);
|
|
if (text) {
|
|
messages.push({ content: text, role: "user" });
|
|
}
|
|
}
|
|
const text = decodeUserMessage(decodeProtoMessageFields(userMessageAction, 1)[0]);
|
|
if (text) {
|
|
messages.push({ content: text, role: "user" });
|
|
}
|
|
}
|
|
|
|
const startPlanAction = decodeProtoMessageFields(action, 6)[0];
|
|
const startPlanUserMessage = decodeUserMessage(decodeProtoMessageFields(startPlanAction, 1)[0]);
|
|
if (startPlanUserMessage) {
|
|
messages.push({ content: startPlanUserMessage, role: "user" });
|
|
}
|
|
|
|
return messages;
|
|
}
|
|
|
|
function decodeUserMessage(message) {
|
|
return decodeProtoStringField(message, 1) || decodeProtoStringField(message, 8) || "";
|
|
}
|
|
|
|
function decodeRequestedModelParameters(requestedModel) {
|
|
const result = {};
|
|
for (const parameter of decodeProtoMessageFields(requestedModel, 3)) {
|
|
const id = decodeProtoStringField(parameter, 1);
|
|
const value = decodeProtoStringField(parameter, 2);
|
|
if (id && value) {
|
|
result[id] = value;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function compactChatMessages(messages) {
|
|
const result = [];
|
|
for (const message of messages) {
|
|
if (!message?.role) {
|
|
continue;
|
|
}
|
|
const content = normalizeChatMessageContent(message.content);
|
|
const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls.filter(Boolean) : undefined;
|
|
const functionCall = isRecord(message.function_call) ? message.function_call : undefined;
|
|
if (!hasChatMessageContent(content) && (!toolCalls || toolCalls.length === 0) && !functionCall) {
|
|
continue;
|
|
}
|
|
const compacted = compactObject({
|
|
...message,
|
|
content: hasChatMessageContent(content) ? content : message.role === "assistant" ? "" : undefined,
|
|
function_call: functionCall,
|
|
tool_calls: toolCalls && toolCalls.length > 0 ? toolCalls : undefined
|
|
});
|
|
const last = result[result.length - 1];
|
|
if (isDuplicateSystemMessage(last, compacted)) {
|
|
continue;
|
|
}
|
|
if (canMergeChatMessages(last, compacted)) {
|
|
last.content = `${last.content}\n\n${compacted.content.trim()}`;
|
|
} else {
|
|
result.push(compacted);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function isDuplicateSystemMessage(left, right) {
|
|
return left?.role === "system" &&
|
|
right?.role === "system" &&
|
|
typeof left.content === "string" &&
|
|
typeof right.content === "string" &&
|
|
left.content.trim() === right.content.trim() &&
|
|
!hasChatToolFields(left) &&
|
|
!hasChatToolFields(right);
|
|
}
|
|
|
|
function canMergeChatMessages(left, right) {
|
|
return left?.role === right?.role &&
|
|
typeof left.content === "string" &&
|
|
typeof right.content === "string" &&
|
|
!hasChatToolFields(left) &&
|
|
!hasChatToolFields(right);
|
|
}
|
|
|
|
function hasChatToolFields(message) {
|
|
return Boolean(
|
|
message?.function_call ||
|
|
message?.name ||
|
|
message?.tool_call_id ||
|
|
(Array.isArray(message?.tool_calls) && message.tool_calls.length > 0)
|
|
);
|
|
}
|
|
|
|
function hasChatMessageContent(content) {
|
|
if (typeof content === "string") {
|
|
return content.trim().length > 0;
|
|
}
|
|
if (Array.isArray(content)) {
|
|
return content.length > 0;
|
|
}
|
|
return content !== undefined && content !== null;
|
|
}
|
|
|
|
function normalizeChatMessageContent(content) {
|
|
if (typeof content === "string") {
|
|
return content.trim();
|
|
}
|
|
if (Array.isArray(content)) {
|
|
return content.filter((item) => item !== undefined && item !== null);
|
|
}
|
|
return content;
|
|
}
|
|
|
|
function numberFromParameter(value) {
|
|
if (value === undefined) {
|
|
return undefined;
|
|
}
|
|
const number = Number(value);
|
|
return Number.isFinite(number) ? number : undefined;
|
|
}
|
|
|
|
function looksLikeNoiseString(value) {
|
|
return /^[0-9a-f-]{16,}$/i.test(value) ||
|
|
/^cursor[-_a-z0-9.]*$/i.test(value) ||
|
|
value.startsWith("data:") ||
|
|
value.startsWith("file:") ||
|
|
value.startsWith("vscode:");
|
|
}
|
|
|
|
function looksLikeSystemPromptText(value) {
|
|
if (!value || value.length < 20 || looksLikeNoiseString(value)) {
|
|
return false;
|
|
}
|
|
return /\b(system|developer|instruction|assistant|rules?|tools?|you are|you should|must)\b/i.test(value) ||
|
|
/你是|系统|规则|工具|助手|必须|请遵守/.test(value);
|
|
}
|
|
|
|
function routeJsonRequestBody(runtime, requestBody) {
|
|
const payload = readJsonLikePayload(requestBody);
|
|
if (!isRecord(payload)) {
|
|
return { body: requestBody, routed: false };
|
|
}
|
|
|
|
const routedPayload = { ...payload };
|
|
const decision = applyCursorProxyRouting(runtime, routedPayload);
|
|
if (!decision.routed) {
|
|
return { body: requestBody, routed: false };
|
|
}
|
|
|
|
return {
|
|
body: Buffer.from(`${JSON.stringify(routedPayload)}\n`, "utf8"),
|
|
routed: true
|
|
};
|
|
}
|
|
|
|
function prepareOpenAIChatRequestBody(runtime, requestBody) {
|
|
const payload = readJsonLikePayload(requestBody);
|
|
if (!isRecord(payload)) {
|
|
return { body: requestBody, routed: false };
|
|
}
|
|
|
|
const preparedPayload = { ...payload };
|
|
const contextChanged = applyOpenAICompatChatContext(runtime, preparedPayload);
|
|
const usageChanged = ensureOpenAIStreamUsage(preparedPayload);
|
|
const changed = contextChanged || usageChanged;
|
|
const decision = applyCursorProxyRouting(runtime, preparedPayload);
|
|
if (!changed && !decision.routed) {
|
|
return { body: requestBody, routed: false };
|
|
}
|
|
|
|
return {
|
|
body: Buffer.from(`${JSON.stringify(preparedPayload)}\n`, "utf8"),
|
|
routed: decision.routed
|
|
};
|
|
}
|
|
|
|
function ensureOpenAIStreamUsage(body) {
|
|
if (body.stream !== true) {
|
|
return false;
|
|
}
|
|
|
|
const streamOptions = isRecord(body.stream_options) ? { ...body.stream_options } : {};
|
|
if (streamOptions.include_usage === true) {
|
|
return false;
|
|
}
|
|
body.stream_options = { ...streamOptions, include_usage: true };
|
|
return true;
|
|
}
|
|
|
|
function applyOpenAICompatChatContext(runtime, body) {
|
|
if (!Array.isArray(body.messages)) {
|
|
return false;
|
|
}
|
|
|
|
let changed = applyCollectedOpenAICompatChatContext(runtime, body);
|
|
const messages = body.messages.filter((message) => isRecord(message));
|
|
const missingSystem = !hasSystemInstruction(body, messages);
|
|
const missingTools = !Array.isArray(body.tools) || body.tools.length === 0;
|
|
const missingToolChoice = body.tool_choice === undefined && body.toolChoice === undefined;
|
|
const simplified = isSimplifiedOpenAICompatChat(body, messages);
|
|
|
|
if (missingSystem && runtime.chatCompletionSystemPrompt) {
|
|
body.messages = [
|
|
{ content: runtime.chatCompletionSystemPrompt, role: "system" },
|
|
...body.messages
|
|
];
|
|
changed = true;
|
|
}
|
|
|
|
if (missingTools && runtime.chatCompletionTools.length > 0) {
|
|
body.tools = runtime.chatCompletionTools;
|
|
changed = true;
|
|
}
|
|
|
|
if (missingToolChoice && body.tools && runtime.chatCompletionToolChoice !== undefined) {
|
|
body.tool_choice = runtime.chatCompletionToolChoice;
|
|
changed = true;
|
|
}
|
|
|
|
if (simplified && !changed && runtime.warnedMissingChatContext !== true) {
|
|
runtime.warnedMissingChatContext = true;
|
|
runtime.logger?.warn?.(
|
|
"Cursor proxy received an OpenAI-compatible chat request with only user messages and no system/tools. " +
|
|
"The incoming request does not contain Cursor Agent context; configure cursor-proxy config.systemPrompt/config.tools " +
|
|
"or use Cursor's native Agent traffic so the proxy can forward tools and system prompts."
|
|
);
|
|
}
|
|
|
|
return changed;
|
|
}
|
|
|
|
function applyCollectedOpenAICompatChatContext(runtime, body) {
|
|
const collected = findCollectedCursorContext(runtime, undefined, undefined, body);
|
|
if (!collected) {
|
|
return false;
|
|
}
|
|
|
|
const merged = mergeCollectedCursorChatRequest(body, collected.chatRequest);
|
|
for (const key of Object.keys(body)) {
|
|
delete body[key];
|
|
}
|
|
Object.assign(body, merged);
|
|
runtime.logger?.debug?.(
|
|
`Cursor proxy applied collected Cursor context to OpenAI-compatible request ` +
|
|
`(${formatChatRequestSummary(body)}).`
|
|
);
|
|
return true;
|
|
}
|
|
|
|
function hasSystemInstruction(body, messages) {
|
|
return Boolean(
|
|
body.system !== undefined ||
|
|
body.systemPrompt !== undefined ||
|
|
body.instructions !== undefined ||
|
|
messages.some((message) => normalizeRole(stringValue(message.role)) === "system")
|
|
);
|
|
}
|
|
|
|
function isSimplifiedOpenAICompatChat(body, messages) {
|
|
return messages.length > 0 &&
|
|
messages.every((message) => normalizeRole(stringValue(message.role)) === "user") &&
|
|
!hasSystemInstruction(body, messages) &&
|
|
(!Array.isArray(body.tools) || body.tools.length === 0);
|
|
}
|
|
|
|
function applyCursorProxyRouting(runtime, body) {
|
|
if (!isRecord(body)) {
|
|
return { requestedModel: "", routed: false };
|
|
}
|
|
|
|
const requestedModel = stringValue(body.model) || runtime.defaultModel || "cursor-proxy";
|
|
const routing = runtime.routing;
|
|
if (routing && routing.enabled !== false) {
|
|
const route = resolveCursorProxyRoute(routing, body, requestedModel);
|
|
if (route?.target) {
|
|
body.model = route.target;
|
|
runtime.logger?.debug?.(
|
|
`Cursor proxy routed model ${requestedModel || "<empty>"} to ${route.target} (${route.reason}).`
|
|
);
|
|
return {
|
|
requestedModel,
|
|
reason: route.reason,
|
|
routed: true,
|
|
routedModel: route.target
|
|
};
|
|
}
|
|
}
|
|
|
|
const legacyTarget = normalizeRouteTarget(
|
|
composeRouteTarget(runtime.targetProvider, runtime.targetModel) ||
|
|
stringValue(runtime.targetModel)
|
|
);
|
|
if (legacyTarget) {
|
|
body.model = legacyTarget;
|
|
return {
|
|
requestedModel,
|
|
reason: "legacy-target",
|
|
routed: true,
|
|
routedModel: legacyTarget
|
|
};
|
|
}
|
|
|
|
if (isCursorDefaultModel(requestedModel) && runtime.defaultModel && runtime.defaultModel !== requestedModel) {
|
|
body.model = runtime.defaultModel;
|
|
return {
|
|
requestedModel,
|
|
reason: "configured-default-model",
|
|
routed: true,
|
|
routedModel: runtime.defaultModel
|
|
};
|
|
}
|
|
|
|
return { requestedModel, routed: false };
|
|
}
|
|
|
|
function resolveCursorProxyRoute(routing, body, requestedModel) {
|
|
for (const rule of routing.rules || []) {
|
|
if (rule.enabled === false || !rule.target) {
|
|
continue;
|
|
}
|
|
if (matchesCursorProxyRouteRule(rule, body, requestedModel)) {
|
|
return {
|
|
reason: rule.id ? `plugin-rule:${rule.id}` : `plugin-rule:${rule.type}`,
|
|
target: rule.target
|
|
};
|
|
}
|
|
}
|
|
|
|
if (routing.defaultTarget) {
|
|
return {
|
|
reason: "plugin-default",
|
|
target: routing.defaultTarget
|
|
};
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
function matchesCursorProxyRouteRule(rule, body, requestedModel) {
|
|
switch (rule.type) {
|
|
case "always":
|
|
return true;
|
|
case "image":
|
|
return hasImageContent(body?.messages) || hasImageContent(body?.input);
|
|
case "long-context":
|
|
return estimateTokenCount(body) > (rule.threshold || 200000);
|
|
case "model":
|
|
return Boolean(rule.model && requestedModel === rule.model);
|
|
case "model-prefix":
|
|
return Boolean(rule.pattern && requestedModel?.startsWith(rule.pattern));
|
|
case "thinking":
|
|
return Boolean(body?.thinking || body?.reasoning || body?.reasoning_effort);
|
|
case "web-search":
|
|
return hasWebSearchTool(body?.tools);
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function normalizeCursorProxyRouting(value, options = {}) {
|
|
const fallbackTarget = normalizeRouteTarget(
|
|
composeRouteTarget(options.targetProvider, options.targetModel) ||
|
|
stringValue(options.targetModel)
|
|
);
|
|
const record = isRecord(value) ? value : {};
|
|
const rules = [];
|
|
|
|
if (isRecord(record.modelMap)) {
|
|
for (const [model, target] of Object.entries(record.modelMap)) {
|
|
const normalizedTarget = normalizeRouteTarget(stringValue(target));
|
|
const normalizedModel = stringValue(model);
|
|
if (!normalizedModel || !normalizedTarget) {
|
|
continue;
|
|
}
|
|
rules.push({
|
|
enabled: true,
|
|
id: `model-${sanitizeRouteId(normalizedModel)}`,
|
|
model: normalizedModel,
|
|
name: normalizedModel,
|
|
target: normalizedTarget,
|
|
type: "model"
|
|
});
|
|
}
|
|
}
|
|
|
|
if (Array.isArray(record.rules)) {
|
|
record.rules.forEach((rule, index) => {
|
|
const normalized = normalizeCursorProxyRoutingRule(rule, index);
|
|
if (normalized) {
|
|
rules.push(normalized);
|
|
}
|
|
});
|
|
}
|
|
|
|
return {
|
|
defaultTarget: normalizeRouteTarget(stringValue(record.default) || stringValue(record.defaultTarget)) || fallbackTarget,
|
|
enabled: value === false ? false : record.enabled !== false,
|
|
rules
|
|
};
|
|
}
|
|
|
|
function normalizeCursorProxyRoutingRule(value, index) {
|
|
if (!isRecord(value)) {
|
|
return undefined;
|
|
}
|
|
|
|
const type = stringValue(value.type) || "model";
|
|
if (!CURSOR_PROXY_ROUTE_TYPES.has(type)) {
|
|
return undefined;
|
|
}
|
|
|
|
const target = normalizeRouteTarget(
|
|
stringValue(value.target) ||
|
|
composeRouteTarget(value.targetProvider, value.targetModel) ||
|
|
stringValue(value.targetModel)
|
|
);
|
|
if (!target) {
|
|
return undefined;
|
|
}
|
|
|
|
const model = stringValue(value.model) || stringValue(value.sourceModel);
|
|
const pattern = stringValue(value.pattern) || (type === "model-prefix" ? model : undefined);
|
|
const threshold = positiveNumber(value.threshold) || positiveNumber(value.tokenThreshold);
|
|
const id = stringValue(value.id) || `${type}-${index + 1}`;
|
|
return {
|
|
enabled: value.enabled !== false,
|
|
id,
|
|
name: stringValue(value.name) || id,
|
|
...(model ? { model } : {}),
|
|
...(pattern ? { pattern } : {}),
|
|
target,
|
|
...(threshold ? { threshold } : {}),
|
|
type
|
|
};
|
|
}
|
|
|
|
function normalizeRouteTarget(value) {
|
|
const raw = stringValue(value);
|
|
if (!raw) {
|
|
return undefined;
|
|
}
|
|
|
|
const commaIndex = raw.indexOf(",");
|
|
if (commaIndex > 0 && commaIndex < raw.length - 1) {
|
|
const provider = raw.slice(0, commaIndex).trim();
|
|
const model = raw.slice(commaIndex + 1).trim();
|
|
return provider && model ? `${provider}/${model}` : undefined;
|
|
}
|
|
|
|
return raw;
|
|
}
|
|
|
|
function composeRouteTarget(providerValue, modelValue) {
|
|
const provider = stringValue(providerValue);
|
|
const model = stringValue(modelValue);
|
|
if (provider && model) {
|
|
return `${provider}/${model}`;
|
|
}
|
|
return model || provider;
|
|
}
|
|
|
|
function positiveNumber(value) {
|
|
const number = Number(value);
|
|
return Number.isFinite(number) && number > 0 ? number : undefined;
|
|
}
|
|
|
|
function hasWebSearchTool(tools) {
|
|
return Array.isArray(tools) && tools.some((tool) => isRecord(tool) && stringValue(tool.type)?.startsWith("web_search"));
|
|
}
|
|
|
|
function hasImageContent(value) {
|
|
return value !== undefined && JSON.stringify(value).includes("\"image\"");
|
|
}
|
|
|
|
function estimateTokenCount(body) {
|
|
return Math.max(1, Math.ceil(JSON.stringify(body || {}).length / 4));
|
|
}
|
|
|
|
function isCursorDefaultModel(model) {
|
|
const normalized = stringValue(model).toLowerCase();
|
|
return normalized === "default" || normalized === "auto" || normalized === "cursor-default";
|
|
}
|
|
|
|
function sanitizeRouteId(value) {
|
|
return String(value || "")
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9._-]+/g, "-")
|
|
.replace(/^-+|-+$/g, "")
|
|
.slice(0, 80) || "route";
|
|
}
|
|
|
|
function escapeRegExp(value) {
|
|
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
}
|
|
|
|
async function streamGatewayChatToCursor(runtime, request, response, chatRequest, bridgeContext) {
|
|
const requestBody = { ...chatRequest, stream: true };
|
|
ensureOpenAIStreamUsage(requestBody);
|
|
const routingDecision = applyCursorProxyRouting(runtime, requestBody);
|
|
const body = Buffer.from(`${JSON.stringify(requestBody)}\n`, "utf8");
|
|
const headers = buildGatewayHeaders(
|
|
runtime,
|
|
{},
|
|
{ body, method: "POST", path: "/v1/chat/completions", protocol: "openai", skipRuntimeTargetHeaders: routingDecision.routed },
|
|
body
|
|
);
|
|
const targetUrl = new URL("/v1/chat/completions", runtime.gatewayUrl);
|
|
const transport = targetUrl.protocol === "https:" ? https : http;
|
|
|
|
return await new Promise((resolve) => {
|
|
let settled = false;
|
|
const streamState = createGatewayStreamState();
|
|
const finishTurn = (outcome = { type: "done" }) => {
|
|
if (!settled) {
|
|
settled = true;
|
|
writeCursorConnectMessage(response, encodeCursorAgentTurnEnded());
|
|
finishCursorConnectStream(response);
|
|
resolve(outcome);
|
|
}
|
|
};
|
|
const finishToolBridge = () => {
|
|
if (settled) {
|
|
return;
|
|
}
|
|
|
|
const toolCalls = finalizeGatewayToolCalls(streamState);
|
|
if (toolCalls.length === 0 && streamState.finishReason !== "tool_calls") {
|
|
finishTurn({ type: "done" });
|
|
return;
|
|
}
|
|
|
|
const summary = summarizeGatewayToolCalls(toolCalls);
|
|
const canBridge = bridgeContext && runtime.bridgeOpenAIToolCalls === true && toolCalls.length > 0;
|
|
if (!canBridge) {
|
|
handleUnbridgedGatewayToolCalls(runtime, response, toolCalls, streamState, bridgeContext);
|
|
finishTurn({ finishReason: streamState.finishReason, toolCalls, type: "done" });
|
|
return;
|
|
}
|
|
|
|
const bridgeResult = bridgeGatewayToolCallsToCursor(runtime, response, toolCalls, bridgeContext, streamState);
|
|
if (bridgeResult.sent !== toolCalls.length || bridgeResult.missing.length > 0) {
|
|
handleUnbridgedGatewayToolCalls(runtime, response, toolCalls, streamState, bridgeContext, bridgeResult);
|
|
finishTurn({ finishReason: streamState.finishReason, toolCalls, type: "done" });
|
|
return;
|
|
}
|
|
|
|
settled = true;
|
|
runtime.logger?.debug?.(
|
|
`Cursor proxy bridged upstream tool_calls to Cursor Agent ` +
|
|
`(${summary || "tool_calls requested"}, model_call_id=${streamState.modelCallId || "unknown"}).`
|
|
);
|
|
resolve({
|
|
finishReason: streamState.finishReason,
|
|
modelCallId: streamState.modelCallId,
|
|
toolCalls,
|
|
type: "tool_calls"
|
|
});
|
|
};
|
|
|
|
const upstreamRequest = transport.request(
|
|
{
|
|
headers,
|
|
hostname: targetUrl.hostname,
|
|
method: "POST",
|
|
path: `${targetUrl.pathname}${targetUrl.search}`,
|
|
port: targetUrl.port || (targetUrl.protocol === "https:" ? 443 : 80),
|
|
protocol: targetUrl.protocol
|
|
},
|
|
(upstreamResponse) => {
|
|
const chunks = [];
|
|
let sseBuffer = "";
|
|
const statusCode = upstreamResponse.statusCode || 502;
|
|
const contentType = readHeader(upstreamResponse.headers["content-type"]).toLowerCase();
|
|
|
|
upstreamResponse.on("data", (chunk) => {
|
|
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
if (statusCode >= 400 || !contentType.includes("event-stream")) {
|
|
chunks.push(buffer);
|
|
return;
|
|
}
|
|
sseBuffer += buffer.toString("utf8");
|
|
const lines = sseBuffer.split(/\r?\n/);
|
|
sseBuffer = lines.pop() || "";
|
|
for (const line of lines) {
|
|
handleGatewayStreamLine(response, line, streamState);
|
|
}
|
|
});
|
|
|
|
upstreamResponse.once("end", () => {
|
|
if (statusCode >= 400) {
|
|
const message = Buffer.concat(chunks).toString("utf8").trim() || `Gateway returned HTTP ${statusCode}.`;
|
|
writeCursorConnectMessage(response, encodeCursorAgentTextDelta(message));
|
|
finishTurn({ statusCode, type: "done" });
|
|
return;
|
|
} else if (!contentType.includes("event-stream")) {
|
|
const text = Buffer.concat(chunks).toString("utf8");
|
|
collectGatewayToolCallsFromJsonText(text, streamState);
|
|
const message = extractTextFromGatewayJson(text);
|
|
if (message) {
|
|
writeCursorConnectMessage(response, encodeCursorAgentTextDelta(message));
|
|
}
|
|
finishToolBridge();
|
|
return;
|
|
} else if (sseBuffer.trim()) {
|
|
handleGatewayStreamLine(response, sseBuffer, streamState);
|
|
}
|
|
finishToolBridge();
|
|
});
|
|
|
|
upstreamResponse.once("error", (error) => {
|
|
writeCursorConnectMessage(response, encodeCursorAgentTextDelta(formatError(error)));
|
|
finishTurn({ error, type: "done" });
|
|
});
|
|
}
|
|
);
|
|
|
|
upstreamRequest.once("error", (error) => {
|
|
writeCursorConnectMessage(response, encodeCursorAgentTextDelta(formatError(error)));
|
|
finishTurn({ error, type: "done" });
|
|
});
|
|
upstreamRequest.setTimeout(runtime.gatewayTimeoutMs, () => {
|
|
upstreamRequest.destroy(new Error(`Cursor proxy gateway request to ${targetUrl.toString()} timed out.`));
|
|
});
|
|
upstreamRequest.end(body);
|
|
});
|
|
}
|
|
|
|
function createGatewayStreamState() {
|
|
return {
|
|
finishReason: "",
|
|
modelCallId: "",
|
|
toolCallIndexKeys: new Map(),
|
|
toolCallParts: new Map(),
|
|
toolCalls: []
|
|
};
|
|
}
|
|
|
|
function handleGatewayStreamLine(response, line, streamState) {
|
|
const trimmed = line.trim();
|
|
if (!trimmed.startsWith("data:")) {
|
|
return;
|
|
}
|
|
const data = trimmed.slice(5).trim();
|
|
if (!data || data === "[DONE]") {
|
|
return;
|
|
}
|
|
const payload = readJsonText(data);
|
|
if (!payload) {
|
|
return;
|
|
}
|
|
const text = extractTextFromGatewayPayload(payload);
|
|
if (text) {
|
|
writeCursorConnectMessage(response, encodeCursorAgentTextDelta(text));
|
|
}
|
|
collectGatewayToolCallsFromPayload(payload, streamState);
|
|
}
|
|
|
|
function extractTextFromGatewayPayload(payload) {
|
|
return stringifyContent(payload.choices?.[0]?.delta?.content) ||
|
|
stringifyContent(payload.choices?.[0]?.message?.content) ||
|
|
stringifyContent(payload.delta) ||
|
|
stringifyContent(payload.output_text) ||
|
|
"";
|
|
}
|
|
|
|
function collectGatewayToolCallsFromJsonText(text, streamState) {
|
|
const payload = readJsonText(text);
|
|
if (!payload) {
|
|
return;
|
|
}
|
|
collectGatewayToolCallsFromPayload(payload, streamState);
|
|
}
|
|
|
|
function collectGatewayToolCallsFromPayload(payload, streamState) {
|
|
if (!isRecord(payload) || !streamState) {
|
|
return;
|
|
}
|
|
if (typeof payload.id === "string" && payload.id) {
|
|
streamState.modelCallId = payload.id;
|
|
}
|
|
const choices = Array.isArray(payload.choices) ? payload.choices : [];
|
|
choices.forEach((choice) => {
|
|
if (!isRecord(choice)) {
|
|
return;
|
|
}
|
|
const finishReason = stringValue(choice.finish_reason);
|
|
if (finishReason) {
|
|
streamState.finishReason = finishReason;
|
|
}
|
|
const indexOffset = streamState.toolCallParts.size;
|
|
collectGatewayToolCallDeltas(choice.delta?.tool_calls, streamState, indexOffset);
|
|
collectGatewayToolCallDeltas(choice.message?.tool_calls, streamState, indexOffset);
|
|
collectGatewayToolCallDeltas(choice.tool_calls, streamState, indexOffset);
|
|
});
|
|
collectGatewayToolCallDeltas(payload.tool_calls, streamState, streamState.toolCallParts.size);
|
|
}
|
|
|
|
function collectGatewayToolCallDeltas(value, streamState, indexOffset) {
|
|
const values = Array.isArray(value) ? value : value ? [value] : [];
|
|
values.forEach((item, index) => {
|
|
applyGatewayToolCallDelta(item, streamState, indexOffset + index);
|
|
});
|
|
}
|
|
|
|
function applyGatewayToolCallDelta(value, streamState, fallbackIndex) {
|
|
if (!isRecord(value)) {
|
|
return;
|
|
}
|
|
const raw = unwrapToolCall(value);
|
|
if (!isRecord(raw)) {
|
|
return;
|
|
}
|
|
const fn = isRecord(raw.function) ? raw.function : raw;
|
|
const rawIndex = Number(raw.index);
|
|
const index = Number.isInteger(rawIndex) ? rawIndex : fallbackIndex;
|
|
const id = stringValue(raw.id) || stringValue(raw.tool_call_id) || stringValue(raw.toolCallId) || stringValue(raw.callId);
|
|
const indexKey = `index:${index}`;
|
|
const mappedKey = streamState.toolCallIndexKeys.get(indexKey);
|
|
let key = id || mappedKey || indexKey;
|
|
let part = streamState.toolCallParts.get(key);
|
|
if (id && mappedKey && mappedKey !== id && streamState.toolCallParts.has(mappedKey)) {
|
|
part = streamState.toolCallParts.get(mappedKey);
|
|
streamState.toolCallParts.delete(mappedKey);
|
|
} else if (id && !mappedKey && streamState.toolCallParts.has(indexKey)) {
|
|
part = streamState.toolCallParts.get(indexKey);
|
|
streamState.toolCallParts.delete(indexKey);
|
|
}
|
|
if (id) {
|
|
key = id;
|
|
streamState.toolCallIndexKeys.set(indexKey, id);
|
|
}
|
|
part = part || {
|
|
arguments: "",
|
|
id: id || `call_${index + 1}`,
|
|
index,
|
|
name: "",
|
|
type: stringValue(raw.type) || "function"
|
|
};
|
|
|
|
if (id) {
|
|
part.id = id;
|
|
}
|
|
const name =
|
|
stringValue(fn.name) ||
|
|
stringValue(raw.name) ||
|
|
stringValue(raw.toolName) ||
|
|
stringValue(raw.functionName);
|
|
if (name) {
|
|
part.name = name;
|
|
}
|
|
const rawArguments = firstDefined(fn.arguments, raw.arguments, fn.input, raw.input, raw.args, fn.parameters, raw.parameters);
|
|
if (typeof rawArguments === "string") {
|
|
part.arguments += rawArguments;
|
|
} else if (rawArguments !== undefined) {
|
|
part.arguments = normalizeToolArguments(rawArguments);
|
|
}
|
|
if (part.type !== "function") {
|
|
part.type = "function";
|
|
}
|
|
streamState.toolCallParts.set(key, part);
|
|
}
|
|
|
|
function finalizeGatewayToolCalls(streamState) {
|
|
const parts = [...(streamState?.toolCallParts?.values?.() || [])]
|
|
.sort((left, right) => (left.index || 0) - (right.index || 0));
|
|
const calls = parts
|
|
.filter((part) => part.name)
|
|
.map((part, index) => ({
|
|
function: {
|
|
arguments: part.arguments || "{}",
|
|
name: part.name
|
|
},
|
|
id: part.id || `call_${index + 1}`,
|
|
type: "function"
|
|
}));
|
|
const normalized = uniqueToolCalls([
|
|
...normalizeToolCallList(streamState?.toolCalls),
|
|
...calls
|
|
]);
|
|
if (streamState) {
|
|
streamState.toolCalls = normalized;
|
|
}
|
|
return normalized;
|
|
}
|
|
|
|
function summarizeGatewayToolCalls(toolCalls) {
|
|
const calls = Array.isArray(toolCalls) ? toolCalls : [];
|
|
return calls
|
|
.slice(0, 5)
|
|
.map((call) => {
|
|
const name = stringValue(call?.function?.name) || stringValue(call?.name) || "unknown";
|
|
const args = stringValue(call?.function?.arguments) || stringValue(call?.arguments) || "";
|
|
return args ? `${name}(${truncateForLog(undefined, args)})` : name;
|
|
})
|
|
.join(", ");
|
|
}
|
|
|
|
function buildCursorToolBridgeContext(runtime, session, runRequest, chatRequest, round) {
|
|
const contextValues = Buffer.isBuffer(runRequest) ? decodeAgentRunRequestContextValues(runRequest) : [];
|
|
const toolIndex = buildCursorMcpToolIndex(contextValues);
|
|
const decodedTools = Array.isArray(chatRequest?.tools) ? chatRequest.tools.length : 0;
|
|
const nativeToolNames = (Array.isArray(chatRequest?.tools) ? chatRequest.tools : [])
|
|
.map((tool) => stringValue(tool?.function?.name) || stringValue(tool?.name))
|
|
.filter((name) => lookupCursorNativeToolSpec(name));
|
|
runtime.logger?.debug?.(
|
|
`Cursor proxy prepared tool bridge context for ${session.requestId} ` +
|
|
`(round=${round}, decoded_tools=${decodedTools}, cursor_native_tools=${nativeToolNames.length}, ` +
|
|
`cursor_native_names=${nativeToolNames.slice(0, decodeDiagnosticSampleLimit(runtime)).join(",") || "none"}, ` +
|
|
`cursor_mcp_tools=${toolIndex.tools.length}, ` +
|
|
`providers=${[...new Set(toolIndex.tools.map((tool) => tool.providerIdentifier))].join(",") || "none"}).`
|
|
);
|
|
return {
|
|
chatRequest,
|
|
round,
|
|
runRequest,
|
|
session,
|
|
toolIndex
|
|
};
|
|
}
|
|
|
|
function buildCursorMcpToolIndex(values) {
|
|
const byName = new Map();
|
|
const tools = [];
|
|
for (const filePath of extractCursorToolFilePaths(values)) {
|
|
const metadata = readCursorMcpToolMetadata(filePath);
|
|
if (!metadata) {
|
|
continue;
|
|
}
|
|
tools.push(metadata);
|
|
addCursorMcpToolIndexEntry(byName, metadata.name, metadata);
|
|
addCursorMcpToolIndexEntry(byName, metadata.toolName, metadata);
|
|
addCursorMcpToolIndexEntry(byName, `${metadata.providerIdentifier}.${metadata.toolName}`, metadata);
|
|
addCursorMcpToolIndexEntry(byName, `${metadata.providerIdentifier}_${metadata.toolName}`, metadata);
|
|
addCursorMcpToolIndexEntry(byName, `${metadata.providerIdentifier}:${metadata.toolName}`, metadata);
|
|
}
|
|
return { byName, tools };
|
|
}
|
|
|
|
function addCursorMcpToolIndexEntry(index, name, metadata) {
|
|
const key = normalizeToolBridgeName(name);
|
|
if (!key || index.has(key)) {
|
|
return;
|
|
}
|
|
index.set(key, metadata);
|
|
}
|
|
|
|
function lookupCursorMcpToolMetadata(bridgeContext, name) {
|
|
const index = bridgeContext?.toolIndex?.byName;
|
|
if (!(index instanceof Map)) {
|
|
return undefined;
|
|
}
|
|
return index.get(normalizeToolBridgeName(name));
|
|
}
|
|
|
|
function normalizeToolBridgeName(value) {
|
|
return String(value || "").trim().toLowerCase();
|
|
}
|
|
|
|
function readCursorMcpToolMetadata(filePath) {
|
|
const pathMetadata = parseCursorMcpToolFilePath(filePath);
|
|
if (!pathMetadata) {
|
|
return undefined;
|
|
}
|
|
const text = readTextFileIfSmall(filePath, 512 * 1024);
|
|
const parsed = readJsonText(text);
|
|
if (!isRecord(parsed)) {
|
|
return {
|
|
...pathMetadata,
|
|
name: pathMetadata.toolName
|
|
};
|
|
}
|
|
const name = stringValue(parsed.name) || pathMetadata.toolName;
|
|
return {
|
|
...pathMetadata,
|
|
description: stringValue(parsed.description),
|
|
name,
|
|
schema: isRecord(parsed.arguments) ? parsed.arguments : isRecord(parsed.parameters) ? parsed.parameters : undefined
|
|
};
|
|
}
|
|
|
|
function parseCursorMcpToolFilePath(filePath) {
|
|
const normalized = normalizeCursorMcpFilePath(filePath);
|
|
if (!normalized) {
|
|
return undefined;
|
|
}
|
|
const parts = normalized.split(path.sep);
|
|
const markerIndex = parts.lastIndexOf("mcps");
|
|
if (markerIndex < 0 || parts[markerIndex + 2] !== "tools") {
|
|
return undefined;
|
|
}
|
|
const providerIdentifier = parts[markerIndex + 1];
|
|
const fileName = parts[markerIndex + 3] || "";
|
|
const toolName = fileName.endsWith(".json") ? fileName.slice(0, -".json".length) : path.basename(fileName, ".json");
|
|
if (!providerIdentifier || !toolName) {
|
|
return undefined;
|
|
}
|
|
return {
|
|
filePath: normalized,
|
|
providerIdentifier,
|
|
toolName
|
|
};
|
|
}
|
|
|
|
function bridgeGatewayToolCallsToCursor(runtime, response, toolCalls, bridgeContext, streamState) {
|
|
const missing = [];
|
|
let sent = 0;
|
|
toolCalls.forEach((call, index) => {
|
|
const name = stringValue(call?.function?.name) || stringValue(call?.name);
|
|
const nativeSpec = lookupCursorNativeToolSpec(name);
|
|
if (nativeSpec) {
|
|
runtime.logger?.debug?.(
|
|
`Cursor proxy bridging OpenAI tool_call ${name || `call_${index + 1}`} ` +
|
|
`to Cursor native ${nativeSpec.name} (field=${nativeSpec.toolCallField}).`
|
|
);
|
|
writeCursorConnectMessage(
|
|
response,
|
|
encodeCursorAgentNativeToolCallStarted(call, nativeSpec, {
|
|
modelCallId: streamState?.modelCallId
|
|
})
|
|
);
|
|
sent += 1;
|
|
return;
|
|
}
|
|
const metadata = lookupCursorMcpToolMetadata(bridgeContext, name);
|
|
if (!metadata) {
|
|
missing.push(name || `call_${index + 1}`);
|
|
return;
|
|
}
|
|
runtime.logger?.debug?.(
|
|
`Cursor proxy bridging OpenAI tool_call ${name || `call_${index + 1}`} ` +
|
|
`to Cursor MCP ${metadata.providerIdentifier}/${metadata.toolName}.`
|
|
);
|
|
writeCursorConnectMessage(
|
|
response,
|
|
encodeCursorAgentMcpToolCallStarted(call, metadata, {
|
|
modelCallId: streamState?.modelCallId,
|
|
skipApproval: runtime.cursorMcpSkipApproval
|
|
})
|
|
);
|
|
sent += 1;
|
|
});
|
|
return { missing, sent };
|
|
}
|
|
|
|
function handleUnbridgedGatewayToolCalls(runtime, response, toolCalls, streamState, bridgeContext, bridgeResult) {
|
|
const summary = summarizeGatewayToolCalls(toolCalls);
|
|
const missing = Array.isArray(bridgeResult?.missing) ? bridgeResult.missing : [];
|
|
const reason = runtime.bridgeOpenAIToolCalls !== true
|
|
? "tool bridge disabled"
|
|
: !bridgeContext
|
|
? "no Cursor Agent bridge context"
|
|
: toolCalls.length === 0
|
|
? "finish_reason=tool_calls but no complete tool_calls were decoded"
|
|
: missing.length > 0
|
|
? `missing Cursor tool metadata for ${missing.join(", ")}`
|
|
: "tool bridge could not emit all tool events";
|
|
runtime.logger?.warn?.(
|
|
`Cursor proxy received upstream tool_calls but could not bridge them to Cursor Agent ` +
|
|
`(${reason}; ${summary || "tool_calls requested"}; model_call_id=${streamState?.modelCallId || "unknown"}).`
|
|
);
|
|
if (runtime?.showUnbridgedToolCallWarning === true) {
|
|
writeCursorConnectMessage(response, encodeCursorAgentTextDelta(
|
|
`\n\n[Cursor proxy] Upstream requested tool execution (${summary || "tool_calls"}), ` +
|
|
`but the proxy could not emit Cursor Agent tool events: ${reason}.`
|
|
));
|
|
}
|
|
}
|
|
|
|
function writeCursorMcpToolResultEvents(runtime, response, toolCalls, bridgeContext, chatRequest, modelCallId) {
|
|
const resultMessages = findToolResultMessages(chatRequest?.messages, toolCalls);
|
|
if (resultMessages.length === 0) {
|
|
runtime.logger?.debug?.(
|
|
`Cursor proxy did not find decoded tool result messages for completed Cursor MCP tool events ` +
|
|
`(${summarizeGatewayToolCalls(toolCalls)}).`
|
|
);
|
|
return;
|
|
}
|
|
|
|
for (const call of toolCalls) {
|
|
const name = stringValue(call?.function?.name);
|
|
const metadata = lookupCursorMcpToolMetadata(bridgeContext, name);
|
|
if (!metadata) {
|
|
continue;
|
|
}
|
|
const resultMessage = findToolResultMessageForCall(resultMessages, call);
|
|
if (!resultMessage) {
|
|
continue;
|
|
}
|
|
writeCursorConnectMessage(
|
|
response,
|
|
encodeCursorAgentMcpToolCallCompleted(call, metadata, resultMessage, {
|
|
modelCallId,
|
|
skipApproval: runtime.cursorMcpSkipApproval
|
|
})
|
|
);
|
|
}
|
|
}
|
|
|
|
function findToolResultMessages(messages, toolCalls) {
|
|
const ids = new Set((Array.isArray(toolCalls) ? toolCalls : []).map((call) => call.id).filter(Boolean));
|
|
const names = new Set((Array.isArray(toolCalls) ? toolCalls : [])
|
|
.map((call) => stringValue(call?.function?.name))
|
|
.filter(Boolean));
|
|
return (Array.isArray(messages) ? messages : [])
|
|
.filter((message) => message?.role === "tool")
|
|
.filter((message) => {
|
|
const id = stringValue(message.tool_call_id);
|
|
const name = stringValue(message.name);
|
|
return (id && ids.has(id)) || (name && names.has(name)) || (ids.size === 0 && names.size === 0);
|
|
});
|
|
}
|
|
|
|
function findToolResultMessageForCall(messages, call) {
|
|
const id = stringValue(call?.id);
|
|
const name = stringValue(call?.function?.name);
|
|
return (Array.isArray(messages) ? messages : []).find((message) => {
|
|
const messageId = stringValue(message.tool_call_id);
|
|
const messageName = stringValue(message.name);
|
|
return (id && messageId === id) || (name && messageName === name);
|
|
}) || (Array.isArray(messages) ? messages[0] : undefined);
|
|
}
|
|
|
|
function mergeToolCallContinuationChatRequest(previousRequest, toolCalls, nextRequest) {
|
|
const previousMessages = Array.isArray(previousRequest?.messages) ? previousRequest.messages : [];
|
|
const nextMessages = Array.isArray(nextRequest?.messages) ? nextRequest.messages : [];
|
|
const assistantToolCallMessage = createAssistantToolCallMessage(toolCalls);
|
|
const mergedMessages = [];
|
|
appendUniqueChatMessages(mergedMessages, previousMessages);
|
|
if (!hasAssistantToolCallsForIds(mergedMessages, toolCalls)) {
|
|
appendUniqueChatMessages(mergedMessages, [assistantToolCallMessage]);
|
|
}
|
|
appendUniqueChatMessages(mergedMessages, nextMessages);
|
|
|
|
return compactObject({
|
|
...previousRequest,
|
|
...nextRequest,
|
|
messages: compactChatMessages(mergedMessages),
|
|
stream: true,
|
|
tool_choice: nextRequest?.tool_choice ?? previousRequest?.tool_choice,
|
|
tools: Array.isArray(nextRequest?.tools) && nextRequest.tools.length > 0
|
|
? nextRequest.tools
|
|
: previousRequest?.tools
|
|
});
|
|
}
|
|
|
|
function createAssistantToolCallMessage(toolCalls) {
|
|
return {
|
|
content: "",
|
|
role: "assistant",
|
|
tool_calls: normalizeToolCallList(toolCalls)
|
|
};
|
|
}
|
|
|
|
function appendUniqueChatMessages(target, messages) {
|
|
const seen = new Set(target.map(chatMessageFingerprint));
|
|
for (const message of Array.isArray(messages) ? messages : []) {
|
|
const fingerprint = chatMessageFingerprint(message);
|
|
if (!fingerprint || seen.has(fingerprint)) {
|
|
continue;
|
|
}
|
|
seen.add(fingerprint);
|
|
target.push(message);
|
|
}
|
|
}
|
|
|
|
function chatMessageFingerprint(message) {
|
|
if (!isRecord(message)) {
|
|
return "";
|
|
}
|
|
return safeJsonStringify(compactObject({
|
|
content: message.content,
|
|
function_call: message.function_call,
|
|
name: message.name,
|
|
role: message.role,
|
|
tool_call_id: message.tool_call_id,
|
|
tool_calls: message.tool_calls
|
|
}));
|
|
}
|
|
|
|
function hasAssistantToolCallsForIds(messages, toolCalls) {
|
|
const ids = new Set((Array.isArray(toolCalls) ? toolCalls : []).map((call) => call.id).filter(Boolean));
|
|
if (ids.size === 0) {
|
|
return false;
|
|
}
|
|
return (Array.isArray(messages) ? messages : []).some((message) => {
|
|
if (message?.role !== "assistant" || !Array.isArray(message.tool_calls)) {
|
|
return false;
|
|
}
|
|
return message.tool_calls.some((call) => ids.has(call.id));
|
|
});
|
|
}
|
|
|
|
function logAgentToolContinuation(runtime, session, toolCalls, chatRequest, round) {
|
|
runtime.logger?.debug?.(
|
|
`Cursor proxy continuing Agent RunSSE after Cursor tool results for ${session.requestId} ` +
|
|
`(round=${round}, tool_calls=${summarizeGatewayToolCalls(toolCalls) || "none"}, ` +
|
|
`${formatChatRequestSummary(chatRequest)}).`
|
|
);
|
|
}
|
|
|
|
function logCursorToolBridgeFollowUp(runtime, session, runRequest, chatRequest, toolCalls, resultMessages, round) {
|
|
const dumpPath = writeAgentRunRequestDecodeDump(
|
|
runtime,
|
|
session,
|
|
runRequest,
|
|
chatRequest,
|
|
`tool-bridge-round-${round}`
|
|
);
|
|
const resultSummary = (Array.isArray(resultMessages) ? resultMessages : [])
|
|
.slice(0, decodeDiagnosticSampleLimit(runtime))
|
|
.map((message) => {
|
|
const id = stringValue(message.tool_call_id) || "no-id";
|
|
const name = stringValue(message.name) || "no-name";
|
|
const content = truncateForLog(runtime, stringifyContent(message.content) || "");
|
|
return `${name}/${id}:${content}`;
|
|
})
|
|
.join(" | ") || "none";
|
|
runtime.logger?.debug?.(
|
|
`Cursor proxy decoded tool bridge follow-up for ${session.requestId} ` +
|
|
`(round=${round}, tool_calls=${summarizeGatewayToolCalls(toolCalls) || "none"}, ` +
|
|
`result_messages=${Array.isArray(resultMessages) ? resultMessages.length : 0}, ` +
|
|
`results=${resultSummary}, ${formatChatRequestSummary(chatRequest)}` +
|
|
`${dumpPath ? `, decode_dump=${dumpPath}` : ""}).`
|
|
);
|
|
if (!Array.isArray(resultMessages) || resultMessages.length > 0) {
|
|
return;
|
|
}
|
|
logCursorDecodeDiagnostics(
|
|
runtime,
|
|
"warn",
|
|
`Cursor proxy tool bridge follow-up for ${session.requestId} did not decode any matching tool result messages; ` +
|
|
`tool_calls=${summarizeGatewayToolCalls(toolCalls) || "none"}; ` +
|
|
`run_request=${formatAgentRunRequestDecodeDiagnostic(runtime, runRequest, chatRequest)}; ` +
|
|
`bidi_session=${formatBidiSessionDecodeDiagnostic(runtime, session)}`
|
|
);
|
|
}
|
|
|
|
function extractTextFromGatewayJson(text) {
|
|
const payload = readJsonText(text);
|
|
if (!payload) {
|
|
return text.trim();
|
|
}
|
|
const choice = Array.isArray(payload.choices) ? payload.choices[0] : undefined;
|
|
const hasToolCalls = normalizeToolCallList(choice?.message?.tool_calls).length > 0 ||
|
|
normalizeToolCallList(choice?.delta?.tool_calls).length > 0 ||
|
|
normalizeToolCallList(payload.tool_calls).length > 0;
|
|
return stringifyContent(payload.choices?.[0]?.message?.content) ||
|
|
stringifyContent(payload.choices?.[0]?.delta?.content) ||
|
|
stringifyContent(payload.output_text) ||
|
|
stringifyContent(payload.content) ||
|
|
(hasToolCalls ? "" : text.trim());
|
|
}
|
|
|
|
function writeCursorConnectMessage(response, message) {
|
|
if (!response.writableEnded) {
|
|
response.write(connectEnvelope(0, message));
|
|
}
|
|
}
|
|
|
|
function finishCursorConnectStream(response) {
|
|
if (!response.writableEnded) {
|
|
response.end(connectEnvelope(0x02, Buffer.from("{}", "utf8")));
|
|
}
|
|
}
|
|
|
|
function encodeCursorAgentTextDelta(text) {
|
|
return protoMessage(1, protoMessage(1, protoRawString(1, text)));
|
|
}
|
|
|
|
function encodeCursorAgentTurnEnded() {
|
|
return protoMessage(1, protoMessage(14, Buffer.alloc(0)));
|
|
}
|
|
|
|
function encodeCursorAgentMcpToolCallStarted(call, metadata, options = {}) {
|
|
const update = Buffer.concat([
|
|
protoString(1, call.id),
|
|
protoMessage(2, encodeCursorMcpToolCall(call, metadata, undefined, options)),
|
|
protoString(3, options.modelCallId)
|
|
]);
|
|
return protoMessage(1, protoMessage(2, update));
|
|
}
|
|
|
|
function encodeCursorAgentMcpToolCallCompleted(call, metadata, resultMessage, options = {}) {
|
|
const update = Buffer.concat([
|
|
protoString(1, call.id),
|
|
protoMessage(2, encodeCursorMcpToolCall(call, metadata, resultMessage, options)),
|
|
protoString(3, options.modelCallId)
|
|
]);
|
|
return protoMessage(1, protoMessage(3, update));
|
|
}
|
|
|
|
function encodeCursorAgentNativeToolCallStarted(call, spec, options = {}) {
|
|
const update = Buffer.concat([
|
|
protoString(1, call.id),
|
|
protoMessage(2, encodeCursorNativeToolCall(call, spec)),
|
|
protoString(3, options.modelCallId)
|
|
]);
|
|
return protoMessage(1, protoMessage(2, update));
|
|
}
|
|
|
|
function encodeCursorNativeToolCall(call, spec) {
|
|
const args = parseToolArgumentsObject(call?.function?.arguments);
|
|
return protoMessage(spec.toolCallField, protoMessage(1, spec.encodeArgs(args, call)));
|
|
}
|
|
|
|
function encodeCursorMcpToolCall(call, metadata, resultMessage, options = {}) {
|
|
const mcpToolCall = Buffer.concat([
|
|
protoMessage(1, encodeCursorMcpArgs(call, metadata, options)),
|
|
resultMessage ? protoMessage(2, encodeCursorMcpResult(resultMessage)) : Buffer.alloc(0)
|
|
]);
|
|
return protoMessage(15, mcpToolCall);
|
|
}
|
|
|
|
function encodeCursorMcpArgs(call, metadata, options = {}) {
|
|
const args = parseToolArgumentsObject(call?.function?.arguments);
|
|
return Buffer.concat([
|
|
protoString(1, stringValue(call?.function?.name) || metadata.name || metadata.toolName),
|
|
encodeProtoStringValueMapField(2, args),
|
|
protoString(3, call?.id),
|
|
protoString(4, metadata.providerIdentifier),
|
|
protoString(5, metadata.toolName || metadata.name),
|
|
options.skipApproval ? protoBool(8, true) : Buffer.alloc(0)
|
|
]);
|
|
}
|
|
|
|
function encodeCursorMcpResult(resultMessage) {
|
|
const text = stringifyContent(resultMessage?.content) || "";
|
|
const textContent = protoRawString(1, text);
|
|
const contentItem = protoMessage(1, textContent);
|
|
const success = protoMessage(1, contentItem);
|
|
return protoMessage(1, success);
|
|
}
|
|
|
|
function parseToolArgumentsObject(value) {
|
|
if (isRecord(value)) {
|
|
return value;
|
|
}
|
|
if (typeof value === "string") {
|
|
const parsed = readJsonText(value);
|
|
if (isRecord(parsed)) {
|
|
return parsed;
|
|
}
|
|
if (parsed !== undefined) {
|
|
return { value: parsed };
|
|
}
|
|
return value.trim() ? { input: value } : {};
|
|
}
|
|
if (value === undefined || value === null) {
|
|
return {};
|
|
}
|
|
return { value };
|
|
}
|
|
|
|
function encodeCursorNativeReadArgs(args) {
|
|
return Buffer.concat([
|
|
protoString(1, stringArg(args, ["path", "file", "file_path", "target_file"])),
|
|
optionalProtoInt(2, numberArg(args, ["offset", "start_line"])),
|
|
optionalProtoInt(3, numberArg(args, ["limit", "num_lines", "line_count"])),
|
|
optionalProtoBool(5, booleanArg(args, ["include_line_numbers", "includeLineNumbers"]))
|
|
]);
|
|
}
|
|
|
|
function encodeCursorNativeLsArgs(args, call) {
|
|
const ignore = arrayArg(args, ["ignore", "ignores", "exclude"]).filter((item) => typeof item === "string");
|
|
return Buffer.concat([
|
|
protoString(1, stringArg(args, ["path", "directory", "dir", "target_directory"]) || "."),
|
|
Buffer.concat(ignore.map((item) => protoString(2, item))),
|
|
protoString(3, call?.id)
|
|
]);
|
|
}
|
|
|
|
function encodeCursorNativeGrepArgs(args, call) {
|
|
return Buffer.concat([
|
|
protoString(1, stringArg(args, ["pattern", "query", "regex"])),
|
|
protoString(2, stringArg(args, ["path", "target_directory", "directory"])),
|
|
protoString(3, stringArg(args, ["glob", "include", "include_pattern"])),
|
|
protoString(4, stringArg(args, ["output_mode", "outputMode"])),
|
|
optionalProtoInt(5, numberArg(args, ["context_before", "contextBefore"])),
|
|
optionalProtoInt(6, numberArg(args, ["context_after", "contextAfter"])),
|
|
optionalProtoInt(7, numberArg(args, ["context"])),
|
|
optionalProtoBool(8, booleanArg(args, ["case_insensitive", "caseInsensitive", "ignore_case"])),
|
|
protoString(9, stringArg(args, ["type"])),
|
|
optionalProtoInt(10, numberArg(args, ["head_limit", "headLimit", "limit"])),
|
|
optionalProtoBool(11, booleanArg(args, ["multiline"])),
|
|
protoString(12, stringArg(args, ["sort"])),
|
|
optionalProtoBool(13, booleanArg(args, ["sort_ascending", "sortAscending"])),
|
|
protoString(14, call?.id),
|
|
optionalProtoInt(16, numberArg(args, ["offset"]))
|
|
]);
|
|
}
|
|
|
|
function encodeCursorNativeGlobArgs(args) {
|
|
return Buffer.concat([
|
|
protoString(1, stringArg(args, ["target_directory", "targetDirectory", "path", "directory"])),
|
|
protoString(2, stringArg(args, ["glob_pattern", "globPattern", "pattern", "query"]) || "*")
|
|
]);
|
|
}
|
|
|
|
function encodeCursorNativeSemSearchArgs(args) {
|
|
const targetDirectories = arrayArg(args, ["target_directories", "targetDirectories", "directories"])
|
|
.filter((item) => typeof item === "string");
|
|
return Buffer.concat([
|
|
protoString(1, stringArg(args, ["query", "question"])),
|
|
Buffer.concat(targetDirectories.map((item) => protoString(2, item))),
|
|
protoString(3, stringArg(args, ["explanation", "reason"]))
|
|
]);
|
|
}
|
|
|
|
function encodeCursorNativeShellArgs(args, call) {
|
|
const simpleCommands = arrayArg(args, ["simple_commands", "simpleCommands"])
|
|
.filter((item) => typeof item === "string");
|
|
return Buffer.concat([
|
|
protoString(1, stringArg(args, ["command", "cmd"])),
|
|
protoString(2, stringArg(args, ["working_directory", "workingDirectory", "cwd", "path"])),
|
|
optionalProtoInt(3, numberArg(args, ["timeout", "timeout_ms", "timeoutMs"])),
|
|
protoString(4, call?.id),
|
|
Buffer.concat(simpleCommands.map((item) => protoString(5, item))),
|
|
optionalProtoBool(11, booleanArg(args, ["is_background", "isBackground", "background"])),
|
|
optionalProtoBool(12, booleanArg(args, ["skip_approval", "skipApproval"])),
|
|
optionalProtoInt(13, timeoutBehaviorArg(args)),
|
|
optionalProtoInt(14, numberArg(args, ["hard_timeout", "hardTimeout"])),
|
|
protoString(15, stringArg(args, ["description", "reason"])),
|
|
optionalProtoBool(17, booleanArg(args, ["close_stdin", "closeStdin"]))
|
|
]);
|
|
}
|
|
|
|
function encodeCursorNativeDeleteArgs(args, call) {
|
|
return Buffer.concat([
|
|
protoString(1, stringArg(args, ["path", "file", "file_path", "target_file"])),
|
|
protoString(2, call?.id)
|
|
]);
|
|
}
|
|
|
|
function encodeCursorNativeEditArgs(args) {
|
|
return Buffer.concat([
|
|
protoString(1, stringArg(args, ["path", "file", "file_path", "target_file"])),
|
|
protoRawString(6, stringArg(args, ["stream_content", "streamContent", "content", "file_text", "text", "new_content"]))
|
|
]);
|
|
}
|
|
|
|
function encodeCursorNativeReadLintsArgs(args) {
|
|
const paths = arrayArg(args, ["paths", "path", "files", "file"])
|
|
.filter((item) => typeof item === "string");
|
|
return Buffer.concat(paths.map((item) => protoString(1, item)));
|
|
}
|
|
|
|
function encodeCursorNativeWebSearchArgs(args, call) {
|
|
return Buffer.concat([
|
|
protoString(1, stringArg(args, ["search_term", "searchTerm", "query", "q"])),
|
|
protoString(2, call?.id)
|
|
]);
|
|
}
|
|
|
|
function encodeCursorNativeUrlArgs(args, call) {
|
|
return Buffer.concat([
|
|
protoString(1, stringArg(args, ["url", "uri"])),
|
|
protoString(2, call?.id)
|
|
]);
|
|
}
|
|
|
|
function encodeCursorNativeTaskArgs(args) {
|
|
const attachments = arrayArg(args, ["attachments", "files", "paths"])
|
|
.filter((item) => typeof item === "string");
|
|
const respondingToMessageIds = arrayArg(args, ["responding_to_message_ids", "respondingToMessageIds"])
|
|
.filter((item) => typeof item === "string");
|
|
return Buffer.concat([
|
|
protoString(1, stringArg(args, ["description", "title", "summary"])),
|
|
protoRawString(2, stringArg(args, ["prompt", "task", "input", "instructions"])),
|
|
protoString(4, stringArg(args, ["model"])),
|
|
protoString(5, stringArg(args, ["resume", "resume_from_id", "resumeFromId"])),
|
|
protoString(6, stringArg(args, ["agent_id", "agentId"])),
|
|
Buffer.concat(attachments.map((item) => protoString(7, item))),
|
|
optionalProtoInt(8, taskModeArg(args)),
|
|
Buffer.concat(respondingToMessageIds.map((item) => protoString(9, item))),
|
|
optionalProtoInt(10, taskEnvironmentArg(args))
|
|
]);
|
|
}
|
|
|
|
function encodeCursorNativeAwaitArgs(args) {
|
|
return Buffer.concat([
|
|
protoString(1, stringArg(args, ["task_id", "taskId", "id"])),
|
|
optionalProtoInt(2, numberArg(args, ["block_until_ms", "blockUntilMs", "timeout_ms", "timeoutMs"])),
|
|
protoString(3, stringArg(args, ["regex", "pattern"]))
|
|
]);
|
|
}
|
|
|
|
function encodeCursorNativeReadTodosArgs(args) {
|
|
const statuses = arrayArg(args, ["status_filter", "statusFilter", "statuses", "status"])
|
|
.map(todoStatusArg)
|
|
.filter((value) => value > 0);
|
|
const ids = arrayArg(args, ["id_filter", "idFilter", "ids", "id"])
|
|
.filter((item) => typeof item === "string");
|
|
return Buffer.concat([
|
|
Buffer.concat(statuses.map((item) => optionalProtoInt(1, item))),
|
|
Buffer.concat(ids.map((item) => protoString(2, item)))
|
|
]);
|
|
}
|
|
|
|
function encodeCursorNativeUpdateTodosArgs(args) {
|
|
const todos = arrayArg(args, ["todos", "items"])
|
|
.filter(isRecord)
|
|
.map(encodeCursorNativeTodoItem);
|
|
return Buffer.concat([
|
|
Buffer.concat(todos.map((item) => protoMessage(1, item))),
|
|
optionalProtoBool(2, booleanArg(args, ["merge"]))
|
|
]);
|
|
}
|
|
|
|
function encodeCursorNativeTodoItem(item) {
|
|
const dependencies = arrayArg(item, ["dependencies", "depends_on", "dependsOn"])
|
|
.filter((value) => typeof value === "string");
|
|
return Buffer.concat([
|
|
protoString(1, stringArg(item, ["id"])),
|
|
protoRawString(2, stringArg(item, ["content", "text", "title"])),
|
|
optionalProtoInt(3, todoStatusArg(item?.status)),
|
|
optionalProtoInt(4, numberArg(item, ["created_at", "createdAt"])),
|
|
optionalProtoInt(5, numberArg(item, ["updated_at", "updatedAt"])),
|
|
Buffer.concat(dependencies.map((dependency) => protoString(6, dependency)))
|
|
]);
|
|
}
|
|
|
|
function encodeCursorNativeAskQuestionArgs(args, call) {
|
|
const questions = arrayArg(args, ["questions", "items"])
|
|
.filter(isRecord)
|
|
.map(encodeCursorNativeQuestion);
|
|
return Buffer.concat([
|
|
protoString(1, stringArg(args, ["title", "header"])),
|
|
Buffer.concat(questions.map((question) => protoMessage(2, question))),
|
|
optionalProtoBool(5, booleanArg(args, ["run_async", "runAsync"])),
|
|
protoString(6, stringArg(args, ["async_original_tool_call_id", "asyncOriginalToolCallId"]))
|
|
]);
|
|
}
|
|
|
|
function encodeCursorNativeQuestion(question) {
|
|
const options = arrayArg(question, ["options", "choices"])
|
|
.filter(isRecord)
|
|
.map(encodeCursorNativeQuestionOption);
|
|
return Buffer.concat([
|
|
protoString(1, stringArg(question, ["id"])),
|
|
protoRawString(2, stringArg(question, ["prompt", "question", "text"])),
|
|
Buffer.concat(options.map((option) => protoMessage(3, option))),
|
|
optionalProtoBool(4, booleanArg(question, ["allow_multiple", "allowMultiple", "multiple"]))
|
|
]);
|
|
}
|
|
|
|
function encodeCursorNativeQuestionOption(option) {
|
|
return Buffer.concat([
|
|
protoString(1, stringArg(option, ["id", "value"])),
|
|
protoRawString(2, stringArg(option, ["label", "text", "description"]))
|
|
]);
|
|
}
|
|
|
|
function encodeCursorNativeSwitchModeArgs(args, call) {
|
|
return Buffer.concat([
|
|
protoString(1, stringArg(args, ["target_mode_id", "targetModeId", "mode", "mode_id"])),
|
|
protoRawString(2, stringArg(args, ["explanation", "reason"])),
|
|
protoString(3, call?.id)
|
|
]);
|
|
}
|
|
|
|
function encodeCursorNativeGenerateImageArgs(args) {
|
|
const references = arrayArg(args, ["reference_image_paths", "referenceImagePaths", "references"])
|
|
.filter((item) => typeof item === "string");
|
|
return Buffer.concat([
|
|
protoRawString(1, stringArg(args, ["description", "prompt"])),
|
|
protoString(2, stringArg(args, ["file_path", "filePath", "path"])),
|
|
Buffer.concat(references.map((item) => protoString(5, item)))
|
|
]);
|
|
}
|
|
|
|
function encodeCursorNativeListMcpResourcesArgs(args) {
|
|
return protoString(1, stringArg(args, ["server", "provider", "provider_identifier", "providerIdentifier"]));
|
|
}
|
|
|
|
function encodeCursorNativeReadMcpResourceArgs(args, call) {
|
|
return Buffer.concat([
|
|
protoString(1, stringArg(args, ["server", "provider", "provider_identifier", "providerIdentifier"])),
|
|
protoString(2, stringArg(args, ["uri", "url"])),
|
|
protoString(3, stringArg(args, ["download_path", "downloadPath", "path"])),
|
|
protoString(4, call?.id)
|
|
]);
|
|
}
|
|
|
|
function encodeCursorNativeGetMcpToolsArgs(args, call) {
|
|
return Buffer.concat([
|
|
protoString(1, stringArg(args, ["server", "provider", "provider_identifier", "providerIdentifier"])),
|
|
protoString(2, stringArg(args, ["tool_name", "toolName", "name"])),
|
|
protoString(3, stringArg(args, ["pattern", "query"])),
|
|
protoString(4, call?.id)
|
|
]);
|
|
}
|
|
|
|
function encodeCursorNativeCallMcpToolArgs(args, call) {
|
|
const server = stringArg(args, ["server", "provider", "provider_identifier", "providerIdentifier"]);
|
|
const toolName = stringArg(args, ["tool_name", "toolName", "name"]);
|
|
const rawArgs = firstDefined(args?.arguments, args?.args, args?.parameters, args?.input);
|
|
const toolArgs = isRecord(rawArgs) ? rawArgs : {};
|
|
return Buffer.concat([
|
|
protoString(1, toolName),
|
|
encodeProtoStringValueMapField(2, toolArgs),
|
|
protoString(3, call?.id),
|
|
protoString(4, server),
|
|
protoString(5, toolName),
|
|
optionalProtoBool(8, booleanArg(args, ["skip_approval", "skipApproval"]))
|
|
]);
|
|
}
|
|
|
|
function encodeCursorNativeSetActiveBranchArgs(args) {
|
|
return Buffer.concat([
|
|
protoString(1, stringArg(args, ["path", "repo_path", "repoPath"])),
|
|
protoString(2, stringArg(args, ["branch_name", "branchName", "branch"]))
|
|
]);
|
|
}
|
|
|
|
function stringArg(args, keys) {
|
|
for (const key of keys) {
|
|
if (typeof args?.[key] === "string" && args[key].trim()) {
|
|
return args[key].trim();
|
|
}
|
|
}
|
|
return "";
|
|
}
|
|
|
|
function numberArg(args, keys) {
|
|
for (const key of keys) {
|
|
const value = args?.[key];
|
|
if (value === undefined || value === null || value === "") {
|
|
continue;
|
|
}
|
|
const number = Number(value);
|
|
if (Number.isFinite(number)) {
|
|
return number;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function booleanArg(args, keys) {
|
|
for (const key of keys) {
|
|
const value = args?.[key];
|
|
if (typeof value === "boolean") {
|
|
return value;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function arrayArg(args, keys) {
|
|
for (const key of keys) {
|
|
const value = args?.[key];
|
|
if (Array.isArray(value)) {
|
|
return value;
|
|
}
|
|
if (typeof value === "string" && value.trim()) {
|
|
return [value.trim()];
|
|
}
|
|
}
|
|
return [];
|
|
}
|
|
|
|
function timeoutBehaviorArg(args) {
|
|
const value = stringArg(args, ["timeout_behavior", "timeoutBehavior"]).toLowerCase();
|
|
if (value === "cancel") {
|
|
return 1;
|
|
}
|
|
if (value === "background") {
|
|
return 2;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function taskModeArg(args) {
|
|
const value = stringArg(args, ["mode"]).toLowerCase();
|
|
if (value === "agent" || value === "default") {
|
|
return 1;
|
|
}
|
|
if (value === "ask" || value === "plan") {
|
|
return 2;
|
|
}
|
|
const number = numberArg(args, ["mode"]);
|
|
return Number.isFinite(number) ? number : undefined;
|
|
}
|
|
|
|
function taskEnvironmentArg(args) {
|
|
const value = stringArg(args, ["environment", "env"]).toLowerCase();
|
|
if (value === "local" || value === "ide") {
|
|
return 1;
|
|
}
|
|
if (value === "cloud" || value === "remote") {
|
|
return 2;
|
|
}
|
|
const number = numberArg(args, ["environment", "env"]);
|
|
return Number.isFinite(number) ? number : undefined;
|
|
}
|
|
|
|
function todoStatusArg(value) {
|
|
const normalized = typeof value === "string" ? value.trim().toLowerCase().replace(/[-\s]+/g, "_") : "";
|
|
if (normalized === "pending" || normalized === "todo") {
|
|
return 1;
|
|
}
|
|
if (normalized === "in_progress" || normalized === "inprogress" || normalized === "doing") {
|
|
return 2;
|
|
}
|
|
if (normalized === "completed" || normalized === "complete" || normalized === "done") {
|
|
return 3;
|
|
}
|
|
if (normalized === "cancelled" || normalized === "canceled") {
|
|
return 4;
|
|
}
|
|
const number = Number(value);
|
|
return Number.isFinite(number) && number > 0 ? number : undefined;
|
|
}
|
|
|
|
function encodeProtoStringValueMapField(fieldNumber, value) {
|
|
if (!isRecord(value)) {
|
|
return Buffer.alloc(0);
|
|
}
|
|
return Buffer.concat(Object.entries(value).map(([key, rawValue]) => {
|
|
const entry = Buffer.concat([
|
|
protoString(1, key),
|
|
protoMessage(2, encodeGoogleValue(rawValue))
|
|
]);
|
|
return protoMessage(fieldNumber, entry);
|
|
}));
|
|
}
|
|
|
|
function encodeGoogleStruct(value) {
|
|
if (!isRecord(value)) {
|
|
return Buffer.alloc(0);
|
|
}
|
|
return Buffer.concat(Object.entries(value).map(([key, rawValue]) => {
|
|
const field = Buffer.concat([
|
|
protoString(1, key),
|
|
protoMessage(2, encodeGoogleValue(rawValue))
|
|
]);
|
|
return protoMessage(1, field);
|
|
}));
|
|
}
|
|
|
|
function encodeGoogleListValue(value) {
|
|
const items = Array.isArray(value) ? value : [];
|
|
return Buffer.concat(items.map((item) => protoMessage(1, encodeGoogleValue(item))));
|
|
}
|
|
|
|
function encodeGoogleValue(value) {
|
|
if (value === null || value === undefined) {
|
|
return Buffer.concat([protoTag(1, 0), protoVarint(0)]);
|
|
}
|
|
if (typeof value === "number") {
|
|
return Number.isFinite(value) ? protoDouble(2, value) : protoRawString(3, String(value));
|
|
}
|
|
if (typeof value === "string") {
|
|
return protoRawString(3, value);
|
|
}
|
|
if (typeof value === "boolean") {
|
|
return protoBool(4, value, true);
|
|
}
|
|
if (Array.isArray(value)) {
|
|
return protoMessage(6, encodeGoogleListValue(value));
|
|
}
|
|
if (isRecord(value)) {
|
|
return protoMessage(5, encodeGoogleStruct(value));
|
|
}
|
|
return protoRawString(3, String(value));
|
|
}
|
|
|
|
function resolveGatewayRoute(runtime, request, url, requestBody) {
|
|
const method = (request.method || "GET").toUpperCase();
|
|
const path = normalizePath(url.pathname);
|
|
const search = url.search || "";
|
|
|
|
if (method === "GET" || method === "HEAD") {
|
|
if (isModelsPath(path)) {
|
|
return { body: undefined, method, path: `/v1/models${search}`, protocol: "openai" };
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
if (method !== "POST" && method !== "PUT" && method !== "PATCH") {
|
|
return undefined;
|
|
}
|
|
|
|
if (isOpenAIChatPath(path)) {
|
|
const routed = prepareOpenAIChatRequestBody(runtime, requestBody);
|
|
return {
|
|
body: routed.body,
|
|
method,
|
|
path: `/v1/chat/completions${search}`,
|
|
protocol: "openai",
|
|
skipRuntimeTargetHeaders: routed.routed
|
|
};
|
|
}
|
|
if (isOpenAIResponsesPath(path)) {
|
|
const routed = routeJsonRequestBody(runtime, requestBody);
|
|
return {
|
|
body: routed.body,
|
|
method,
|
|
path: `/v1/responses${search}`,
|
|
protocol: "openai",
|
|
skipRuntimeTargetHeaders: routed.routed
|
|
};
|
|
}
|
|
if (isAnthropicMessagesPath(path)) {
|
|
const routed = routeJsonRequestBody(runtime, requestBody);
|
|
return {
|
|
body: routed.body,
|
|
method,
|
|
path: `/v1/messages${search}`,
|
|
protocol: "anthropic",
|
|
skipRuntimeTargetHeaders: routed.routed
|
|
};
|
|
}
|
|
if (isAnthropicCountTokensPath(path)) {
|
|
const routed = routeJsonRequestBody(runtime, requestBody);
|
|
return {
|
|
body: routed.body,
|
|
method,
|
|
path: `/v1/messages/count_tokens${search}`,
|
|
protocol: "anthropic",
|
|
skipRuntimeTargetHeaders: routed.routed
|
|
};
|
|
}
|
|
if (isGeminiPath(path)) {
|
|
const routed = routeJsonRequestBody(runtime, requestBody);
|
|
return {
|
|
body: routed.body,
|
|
method,
|
|
path: `${path}${search}`,
|
|
protocol: "gemini",
|
|
skipRuntimeTargetHeaders: routed.routed
|
|
};
|
|
}
|
|
|
|
if (runtime.cursorConnectJson && isCursorConnectJsonRequest(request.headers, path)) {
|
|
const converted = convertCursorJsonToOpenAIChat(runtime, path, requestBody);
|
|
if (converted) {
|
|
const routingDecision = applyCursorProxyRouting(runtime, converted);
|
|
return {
|
|
body: Buffer.from(`${JSON.stringify(converted)}\n`, "utf8"),
|
|
method: "POST",
|
|
path: "/v1/chat/completions",
|
|
protocol: "openai",
|
|
skipRuntimeTargetHeaders: routingDecision.routed
|
|
};
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
function buildGatewayHeaders(runtime, sourceHeaders, route, body) {
|
|
const headers = copyForwardHeaders(sourceHeaders);
|
|
delete headers.host;
|
|
delete headers.authorization;
|
|
delete headers["x-api-key"];
|
|
delete headers["content-length"];
|
|
delete headers["content-encoding"];
|
|
delete headers["accept-encoding"];
|
|
for (const key of Object.keys(headers)) {
|
|
if (key.startsWith(INTERNAL_HEADER_PREFIX)) {
|
|
delete headers[key];
|
|
}
|
|
}
|
|
|
|
headers["content-type"] = route.body ? (headers["content-type"] || "application/json") : "application/json";
|
|
headers.accept = headers.accept || "*/*";
|
|
headers["x-ccr-client"] = "Cursor";
|
|
headers["x-ccr-cursor-proxy"] = "1";
|
|
|
|
if (body) {
|
|
headers["content-length"] = String(Buffer.byteLength(body));
|
|
}
|
|
if (runtime.gatewayApiKey) {
|
|
headers.authorization = `Bearer ${runtime.gatewayApiKey}`;
|
|
headers["x-api-key"] = runtime.gatewayApiKey;
|
|
}
|
|
const useRuntimeTargetHeaders = route.skipRuntimeTargetHeaders !== true;
|
|
if (route.targetProvider || (useRuntimeTargetHeaders && runtime.targetProvider)) {
|
|
headers["x-target-provider"] = route.targetProvider || runtime.targetProvider;
|
|
}
|
|
if (route.targetProviders || (useRuntimeTargetHeaders && runtime.targetProviders)) {
|
|
headers["x-target-providers"] = route.targetProviders || runtime.targetProviders;
|
|
}
|
|
if (route.targetModel || (useRuntimeTargetHeaders && runtime.targetModel)) {
|
|
headers["x-target-model"] = route.targetModel || runtime.targetModel;
|
|
}
|
|
|
|
if (route.protocol === "anthropic") {
|
|
headers["anthropic-version"] = headers["anthropic-version"] || "2023-06-01";
|
|
}
|
|
|
|
return headers;
|
|
}
|
|
|
|
function buildPassthroughHeaders(sourceHeaders, originalUrl) {
|
|
const headers = copyForwardHeaders(sourceHeaders);
|
|
for (const key of Object.keys(headers)) {
|
|
if (key.startsWith(INTERNAL_HEADER_PREFIX)) {
|
|
delete headers[key];
|
|
}
|
|
}
|
|
delete headers["content-length"];
|
|
headers.host = originalUrl.host;
|
|
return headers;
|
|
}
|
|
|
|
function logCursorPassthrough(runtime, method, path, headers) {
|
|
const normalizedPath = normalizePath(path);
|
|
const key = `${method} ${normalizedPath}`;
|
|
const count = (runtime.passthroughLogCounts.get(key) || 0) + 1;
|
|
runtime.passthroughLogCounts.set(key, count);
|
|
if (count > runtime.passthroughLogLimit) {
|
|
return;
|
|
}
|
|
|
|
const suffix = count === runtime.passthroughLogLimit
|
|
? "; suppressing repeated logs for this RPC"
|
|
: "";
|
|
runtime.logger?.debug?.(
|
|
`Cursor proxy passing through non-chat Cursor RPC ${method} ${normalizedPath} ` +
|
|
`(${readHeader(headers["content-type"]) || "no content-type"})${suffix}.`
|
|
);
|
|
}
|
|
|
|
function copyForwardHeaders(sourceHeaders) {
|
|
const headers = {};
|
|
for (const [key, value] of Object.entries(sourceHeaders)) {
|
|
const normalized = key.toLowerCase();
|
|
if (HOP_BY_HOP_HEADERS.has(normalized) || value === undefined) {
|
|
continue;
|
|
}
|
|
headers[normalized] = Array.isArray(value) ? value.join(",") : String(value);
|
|
}
|
|
return headers;
|
|
}
|
|
|
|
async function forwardToUrl({ body, headers, method, response, url }) {
|
|
const targetUrl = url instanceof URL ? url : new URL(url);
|
|
const transport = targetUrl.protocol === "https:" ? https : http;
|
|
|
|
await new Promise((resolve) => {
|
|
const upstreamRequest = transport.request(
|
|
{
|
|
headers,
|
|
hostname: targetUrl.hostname,
|
|
method,
|
|
path: `${targetUrl.pathname}${targetUrl.search}`,
|
|
port: targetUrl.port || (targetUrl.protocol === "https:" ? 443 : 80),
|
|
protocol: targetUrl.protocol
|
|
},
|
|
(upstreamResponse) => {
|
|
const statusCode = upstreamResponse.statusCode || 502;
|
|
response.writeHead(statusCode, filterResponseHeaders(upstreamResponse.headers));
|
|
upstreamResponse.pipe(response);
|
|
upstreamResponse.once("end", resolve);
|
|
upstreamResponse.once("error", (error) => {
|
|
if (!response.headersSent) {
|
|
sendJson(response, 502, { error: { message: formatError(error) } });
|
|
} else {
|
|
response.destroy(error);
|
|
}
|
|
resolve();
|
|
});
|
|
}
|
|
);
|
|
|
|
upstreamRequest.once("error", (error) => {
|
|
if (!response.headersSent) {
|
|
sendJson(response, 502, { error: { message: formatError(error) } });
|
|
} else {
|
|
response.destroy(error);
|
|
}
|
|
resolve();
|
|
});
|
|
upstreamRequest.setTimeout(120000, () => {
|
|
upstreamRequest.destroy(new Error(`Cursor proxy upstream request to ${targetUrl.toString()} timed out.`));
|
|
});
|
|
|
|
if (body && Buffer.byteLength(body) > 0 && method !== "GET" && method !== "HEAD") {
|
|
upstreamRequest.end(body);
|
|
} else {
|
|
upstreamRequest.end();
|
|
}
|
|
});
|
|
}
|
|
|
|
function convertCursorJsonToOpenAIChat(runtime, path, body) {
|
|
const payload = unwrapCursorJson(readJsonLikePayload(body));
|
|
if (!payload) {
|
|
return undefined;
|
|
}
|
|
|
|
const messages = extractMessages(payload);
|
|
if (messages.length === 0) {
|
|
const prompt = findFirstStringByKeys(payload, ["prompt", "query", "instruction", "text"]);
|
|
if (prompt) {
|
|
messages.push({ content: prompt, role: "user" });
|
|
}
|
|
}
|
|
if (messages.length === 0) {
|
|
return undefined;
|
|
}
|
|
|
|
const systemPrompt = extractSystemPrompt(payload);
|
|
if (systemPrompt && !messages.some((message) => message.role === "system")) {
|
|
messages.unshift({ content: systemPrompt, role: "system" });
|
|
}
|
|
const compactedMessages = compactChatMessages(messages);
|
|
if (compactedMessages.length === 0) {
|
|
return undefined;
|
|
}
|
|
const tools = extractTools(payload);
|
|
const toolChoice = extractToolChoice(payload);
|
|
|
|
const model =
|
|
findFirstStringByKeys(payload, ["model", "modelName", "selectedModel", "intentModel", "chatModel"]) ||
|
|
runtime.defaultModel ||
|
|
"cursor-proxy";
|
|
const stream = path.toLowerCase().includes("stream") || Boolean(findFirstBooleanByKeys(payload, ["stream", "shouldStream"]));
|
|
|
|
return compactObject({
|
|
frequency_penalty: findFirstNumberByKeys(payload, ["frequency_penalty", "frequencyPenalty"]),
|
|
max_tokens: findFirstNumberByKeys(payload, ["max_tokens", "maxTokens", "maxOutputTokens"]),
|
|
messages: compactedMessages,
|
|
model,
|
|
presence_penalty: findFirstNumberByKeys(payload, ["presence_penalty", "presencePenalty"]),
|
|
stream,
|
|
temperature: findFirstNumberByKeys(payload, ["temperature"]),
|
|
tool_choice: toolChoice,
|
|
tools: tools.length > 0 ? tools : undefined,
|
|
top_p: findFirstNumberByKeys(payload, ["top_p", "topP"])
|
|
});
|
|
}
|
|
|
|
function extractMessages(value) {
|
|
const candidates = [];
|
|
const rootMessages = normalizeMessages(value);
|
|
if (rootMessages.length > 0) {
|
|
candidates.push(rootMessages);
|
|
}
|
|
walkJson(value, (item) => {
|
|
if (!Array.isArray(item) || item.length === 0) {
|
|
return;
|
|
}
|
|
const normalizedItems = item.map(normalizeMessages);
|
|
const messages = normalizedItems.flat();
|
|
const matchedItems = normalizedItems.filter((messagesForItem) => messagesForItem.length > 0).length;
|
|
if (messages.length > 0 && matchedItems >= Math.ceil(item.length * 0.5)) {
|
|
candidates.push(messages);
|
|
}
|
|
});
|
|
candidates.sort((a, b) => scoreMessages(b) - scoreMessages(a));
|
|
return candidates[0] || [];
|
|
}
|
|
|
|
function extractMessagesFromValues(values) {
|
|
const candidates = [];
|
|
for (const value of values) {
|
|
const messages = extractMessages(value);
|
|
if (messages.length > 0) {
|
|
candidates.push(messages);
|
|
}
|
|
}
|
|
candidates.sort((a, b) => scoreMessages(b) - scoreMessages(a));
|
|
return [...(candidates[0] || [])];
|
|
}
|
|
|
|
function scoreMessages(messages) {
|
|
return messages.length * 10 +
|
|
messages.filter((message) => message.role === "system").length * 4 +
|
|
messages.filter((message) => message.role === "tool").length * 3 +
|
|
messages.filter((message) => Array.isArray(message.tool_calls) && message.tool_calls.length > 0).length * 3;
|
|
}
|
|
|
|
function normalizeMessages(value) {
|
|
const messages = [];
|
|
const message = normalizeMessage(value);
|
|
if (message) {
|
|
messages.push(message);
|
|
}
|
|
const toolResults = normalizeToolResultMessages(value);
|
|
if (toolResults.length > 0) {
|
|
messages.push(...toolResults);
|
|
}
|
|
return messages;
|
|
}
|
|
|
|
function normalizeMessage(value) {
|
|
if (!isRecord(value)) {
|
|
return undefined;
|
|
}
|
|
|
|
const role = normalizeRole(
|
|
stringValue(value.role) ||
|
|
stringValue(value.speaker) ||
|
|
stringValue(value.author) ||
|
|
stringValue(value.type) ||
|
|
stringValue(value.kind)
|
|
);
|
|
if (!role) {
|
|
return undefined;
|
|
}
|
|
|
|
const content = normalizeMessageContent(value);
|
|
const toolCalls = normalizeToolCallsFromMessage(value);
|
|
const functionCall = normalizeFunctionCall(value.function_call || value.functionCall);
|
|
if (!hasChatMessageContent(content) && toolCalls.length === 0 && !functionCall) {
|
|
return undefined;
|
|
}
|
|
|
|
return compactObject({
|
|
content,
|
|
function_call: functionCall,
|
|
name: role === "tool" ? stringValue(value.name) || stringValue(value.toolName) || stringValue(value.functionName) : undefined,
|
|
role,
|
|
tool_call_id: role === "tool" ? toolCallIdFromRecord(value) : undefined,
|
|
tool_calls: toolCalls.length > 0 ? toolCalls : undefined
|
|
});
|
|
}
|
|
|
|
function normalizeRole(value) {
|
|
if (!value) {
|
|
return undefined;
|
|
}
|
|
const normalized = value.toLowerCase().replace(/^role_/, "").replace(/[-\s]+/g, "_");
|
|
if (["customer", "human", "human_message", "request", "user", "user_message"].includes(normalized)) {
|
|
return "user";
|
|
}
|
|
if (["agent", "ai", "assistant", "assistant_message", "bot", "model", "model_message"].includes(normalized)) {
|
|
return "assistant";
|
|
}
|
|
if (["developer", "developer_message", "system", "system_message"].includes(normalized)) {
|
|
return "system";
|
|
}
|
|
if (["function", "function_result", "tool", "tool_result", "tool_result_error", "tool_response"].includes(normalized)) {
|
|
return "tool";
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function normalizeMessageContent(value) {
|
|
for (const key of [
|
|
"bubbleText",
|
|
"content",
|
|
"displayText",
|
|
"markdown",
|
|
"message",
|
|
"observation",
|
|
"output",
|
|
"plainText",
|
|
"prompt",
|
|
"rawText",
|
|
"result",
|
|
"text",
|
|
"userMessage",
|
|
"value",
|
|
"error"
|
|
]) {
|
|
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
const content = normalizeContentValue(value[key]);
|
|
if (hasChatMessageContent(content)) {
|
|
return content;
|
|
}
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function normalizeContentValue(value) {
|
|
if (typeof value === "string") {
|
|
return value;
|
|
}
|
|
if (typeof value === "number" || typeof value === "boolean") {
|
|
return String(value);
|
|
}
|
|
if (Array.isArray(value)) {
|
|
const parts = [];
|
|
const text = [];
|
|
for (const item of value) {
|
|
if (isToolCallContentBlock(item) || isToolResultContentBlock(item)) {
|
|
continue;
|
|
}
|
|
const part = normalizeOpenAIContentPart(item);
|
|
if (part) {
|
|
parts.push(part);
|
|
continue;
|
|
}
|
|
const itemText = stringifyContent(item);
|
|
if (itemText) {
|
|
text.push(itemText);
|
|
}
|
|
}
|
|
if (parts.length > 0 && parts.every((part) => part.type === "text")) {
|
|
return [...text, ...parts.map((part) => part.text)].filter(Boolean).join("\n");
|
|
}
|
|
if (parts.length > 0 && text.length === 0) {
|
|
return parts;
|
|
}
|
|
if (parts.length > 0 || text.length > 0) {
|
|
return [
|
|
...text.map((item) => ({ text: item, type: "text" })),
|
|
...parts
|
|
];
|
|
}
|
|
return undefined;
|
|
}
|
|
if (isRecord(value)) {
|
|
if (isToolCallContentBlock(value) || isToolResultContentBlock(value)) {
|
|
return undefined;
|
|
}
|
|
const part = normalizeOpenAIContentPart(value);
|
|
if (part?.type === "text") {
|
|
return part.text;
|
|
}
|
|
if (part) {
|
|
return [part];
|
|
}
|
|
const text = stringifyContent(value);
|
|
if (text) {
|
|
return text;
|
|
}
|
|
try {
|
|
return JSON.stringify(value);
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function normalizeOpenAIContentPart(value) {
|
|
if (!isRecord(value)) {
|
|
return undefined;
|
|
}
|
|
const type = stringValue(value.type)?.toLowerCase();
|
|
if ((type === "text" || type === "input_text") && typeof value.text === "string") {
|
|
return { text: value.text, type: "text" };
|
|
}
|
|
if (type === "image_url" && (isRecord(value.image_url) || typeof value.image_url === "string")) {
|
|
return {
|
|
image_url: typeof value.image_url === "string" ? { url: value.image_url } : value.image_url,
|
|
type: "image_url"
|
|
};
|
|
}
|
|
if ((type === "image" || type === "input_image") && typeof value.url === "string") {
|
|
return { image_url: { url: value.url }, type: "image_url" };
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function normalizeToolCallsFromMessage(value) {
|
|
const calls = [];
|
|
for (const key of ["tool_calls", "toolCalls", "function_calls", "functionCalls", "calls"]) {
|
|
calls.push(...normalizeToolCallList(value[key]));
|
|
}
|
|
calls.push(...normalizeToolCallList(value.tool_call || value.toolCall));
|
|
if (Array.isArray(value.content)) {
|
|
for (const block of value.content) {
|
|
if (isToolCallContentBlock(block)) {
|
|
calls.push(...normalizeToolCallList(block));
|
|
}
|
|
}
|
|
}
|
|
return uniqueToolCalls(calls);
|
|
}
|
|
|
|
function normalizeToolCallList(value) {
|
|
const values = Array.isArray(value) ? value : value ? [value] : [];
|
|
return values.map(normalizeToolCall).filter(Boolean);
|
|
}
|
|
|
|
function normalizeToolCall(value, index) {
|
|
const raw = unwrapToolCall(value);
|
|
if (!isRecord(raw)) {
|
|
return undefined;
|
|
}
|
|
const fn = isRecord(raw.function) ? raw.function : raw;
|
|
const name =
|
|
stringValue(fn.name) ||
|
|
stringValue(raw.name) ||
|
|
stringValue(raw.toolName) ||
|
|
stringValue(raw.functionName);
|
|
if (!name) {
|
|
return undefined;
|
|
}
|
|
const args = firstDefined(fn.arguments, raw.arguments, fn.input, raw.input, raw.args, fn.parameters, raw.parameters);
|
|
return {
|
|
function: {
|
|
arguments: normalizeToolArguments(args),
|
|
name
|
|
},
|
|
id: stringValue(raw.id) || stringValue(raw.tool_call_id) || stringValue(raw.toolCallId) || stringValue(raw.callId) || `call_${index + 1}`,
|
|
type: "function"
|
|
};
|
|
}
|
|
|
|
function unwrapToolCall(value) {
|
|
if (!isRecord(value)) {
|
|
return value;
|
|
}
|
|
if (isRecord(value.toolCall)) {
|
|
return value.toolCall;
|
|
}
|
|
if (isRecord(value.tool_call)) {
|
|
return value.tool_call;
|
|
}
|
|
if (isRecord(value.functionCall)) {
|
|
return value.functionCall;
|
|
}
|
|
if (isRecord(value.function_call)) {
|
|
return value.function_call;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function normalizeFunctionCall(value) {
|
|
if (!isRecord(value)) {
|
|
return undefined;
|
|
}
|
|
const name = stringValue(value.name) || stringValue(value.functionName);
|
|
if (!name) {
|
|
return undefined;
|
|
}
|
|
return {
|
|
arguments: normalizeToolArguments(firstDefined(value.arguments, value.input, value.args, value.parameters)),
|
|
name
|
|
};
|
|
}
|
|
|
|
function normalizeToolResultMessages(value) {
|
|
if (!isRecord(value)) {
|
|
return [];
|
|
}
|
|
const blocks = [];
|
|
if (Array.isArray(value.content)) {
|
|
blocks.push(...value.content);
|
|
}
|
|
if (isToolResultContentBlock(value)) {
|
|
blocks.push(value);
|
|
}
|
|
return blocks.map(normalizeToolResultBlock).filter(Boolean);
|
|
}
|
|
|
|
function normalizeToolResultBlock(value) {
|
|
const raw = unwrapToolResult(value);
|
|
if (!isRecord(raw)) {
|
|
return undefined;
|
|
}
|
|
const toolCallId = toolCallIdFromRecord(raw) || stringValue(raw.tool_use_id) || stringValue(raw.toolUseId);
|
|
const name = stringValue(raw.name) || stringValue(raw.toolName) || stringValue(raw.functionName);
|
|
const content = normalizeContentValue(firstDefined(raw.content, raw.result, raw.output, raw.text, raw.error)) || "";
|
|
if (!toolCallId && !name) {
|
|
return undefined;
|
|
}
|
|
return compactObject({
|
|
content,
|
|
name,
|
|
role: "tool",
|
|
tool_call_id: toolCallId
|
|
});
|
|
}
|
|
|
|
function unwrapToolResult(value) {
|
|
if (!isRecord(value)) {
|
|
return value;
|
|
}
|
|
if (isRecord(value.toolResult)) {
|
|
return value.toolResult;
|
|
}
|
|
if (isRecord(value.tool_result)) {
|
|
return value.tool_result;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function isToolCallContentBlock(value) {
|
|
const raw = unwrapToolCall(value);
|
|
if (!isRecord(raw)) {
|
|
return false;
|
|
}
|
|
const type = stringValue(raw.type)?.toLowerCase().replace(/[-\s]+/g, "_");
|
|
return Boolean(
|
|
type === "tool_call" ||
|
|
type === "tool_use" ||
|
|
type === "function_call" ||
|
|
isRecord(value.toolCall) ||
|
|
isRecord(value.tool_call) ||
|
|
isRecord(value.functionCall) ||
|
|
isRecord(value.function_call)
|
|
);
|
|
}
|
|
|
|
function isToolResultContentBlock(value) {
|
|
const raw = unwrapToolResult(value);
|
|
if (!isRecord(raw)) {
|
|
return false;
|
|
}
|
|
const type = stringValue(raw.type)?.toLowerCase().replace(/[-\s]+/g, "_");
|
|
return Boolean(
|
|
type === "tool_result" ||
|
|
type === "tool_result_error" ||
|
|
type === "function_result" ||
|
|
isRecord(value.toolResult) ||
|
|
isRecord(value.tool_result)
|
|
);
|
|
}
|
|
|
|
function toolCallIdFromRecord(value) {
|
|
return stringValue(value.tool_call_id) ||
|
|
stringValue(value.toolCallId) ||
|
|
stringValue(value.callId) ||
|
|
stringValue(value.id);
|
|
}
|
|
|
|
function uniqueToolCalls(calls) {
|
|
const result = [];
|
|
const seen = new Set();
|
|
for (const call of calls) {
|
|
const key = call.id || `${call.function?.name}:${call.function?.arguments}`;
|
|
if (seen.has(key)) {
|
|
continue;
|
|
}
|
|
seen.add(key);
|
|
result.push(call);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function normalizeToolArguments(value) {
|
|
if (typeof value === "string") {
|
|
return value.trim() || "{}";
|
|
}
|
|
if (value === undefined) {
|
|
return "{}";
|
|
}
|
|
try {
|
|
return JSON.stringify(value);
|
|
} catch {
|
|
return "{}";
|
|
}
|
|
}
|
|
|
|
function stringifyContent(value) {
|
|
if (typeof value === "string") {
|
|
return value;
|
|
}
|
|
if (typeof value === "number" || typeof value === "boolean") {
|
|
return String(value);
|
|
}
|
|
if (Array.isArray(value)) {
|
|
return value.map(stringifyContent).filter(Boolean).join("\n");
|
|
}
|
|
if (isRecord(value)) {
|
|
if (typeof value.text === "string") {
|
|
return value.text;
|
|
}
|
|
if (typeof value.content === "string") {
|
|
return value.content;
|
|
}
|
|
if (typeof value.markdown === "string") {
|
|
return value.markdown;
|
|
}
|
|
if (typeof value.value === "string") {
|
|
return value.value;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function extractSystemPrompt(value) {
|
|
return findFirstByKeys(value, CURSOR_SYSTEM_PROMPT_KEYS, stringifyPromptCandidate);
|
|
}
|
|
|
|
function extractSystemPromptFromValues(values) {
|
|
for (const value of values) {
|
|
const systemPrompt = extractSystemPrompt(value);
|
|
if (systemPrompt) {
|
|
return systemPrompt;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function selectPreferredSystemPrompt(...candidates) {
|
|
for (const candidate of candidates) {
|
|
const content = typeof candidate === "string" ? candidate.trim() : "";
|
|
if (!content || isLikelyCursorInstructionText(content) || looksLikeCursorToolDescriptionText(content)) {
|
|
continue;
|
|
}
|
|
return content;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function composeAgentSystemPrompt(values, preferredSystemPrompt) {
|
|
const sections = [];
|
|
const toolDescriptionFingerprints = new Set(
|
|
extractToolsFromValues(values)
|
|
.map((tool) => normalizePromptFingerprint(tool?.function?.description || tool?.description))
|
|
.filter(Boolean)
|
|
);
|
|
const addSection = (text) => {
|
|
const content = typeof text === "string" ? text.trim() : "";
|
|
if (!content || looksLikeNoiseString(content) || isCursorMcpToolFilePath(content)) {
|
|
return;
|
|
}
|
|
if (looksLikeCursorToolDescriptionText(content) || toolDescriptionFingerprints.has(normalizePromptFingerprint(content))) {
|
|
return;
|
|
}
|
|
if (sections.some((item) => normalizePromptFingerprint(item) === normalizePromptFingerprint(content))) {
|
|
return;
|
|
}
|
|
sections.push(content);
|
|
};
|
|
|
|
addSection(preferredSystemPrompt);
|
|
|
|
const workspaceContext = composeCursorWorkspaceContext(values);
|
|
if (workspaceContext) {
|
|
addSection(workspaceContext);
|
|
}
|
|
|
|
if (sections.length === 0) {
|
|
return undefined;
|
|
}
|
|
return truncateComposedSystemPrompt(sections.join("\n\n---\n\n"));
|
|
}
|
|
|
|
function extractCursorInstructionTexts(values) {
|
|
const instructions = [];
|
|
const addInstruction = (text) => {
|
|
const content = typeof text === "string" ? text.trim() : "";
|
|
if (!content || !isLikelyCursorInstructionText(content)) {
|
|
return;
|
|
}
|
|
if (instructions.some((item) => normalizePromptFingerprint(item) === normalizePromptFingerprint(content))) {
|
|
return;
|
|
}
|
|
instructions.push(content);
|
|
};
|
|
|
|
for (const value of Array.isArray(values) ? values : [values]) {
|
|
if (typeof value !== "string") {
|
|
continue;
|
|
}
|
|
addInstruction(value);
|
|
}
|
|
|
|
for (const filePath of extractCursorInstructionFilePaths(values)) {
|
|
const content = readTextFileIfSmall(filePath, 128 * 1024);
|
|
if (content) {
|
|
addInstruction(content);
|
|
}
|
|
}
|
|
|
|
return instructions
|
|
.sort((left, right) => scoreCursorInstructionText(right) - scoreCursorInstructionText(left))
|
|
.slice(0, 12);
|
|
}
|
|
|
|
function extractCursorInstructionFilePaths(values) {
|
|
const result = [];
|
|
const seen = new Set();
|
|
const addPath = (filePath) => {
|
|
const normalized = typeof filePath === "string" ? filePath.trim() : "";
|
|
if (!normalized || seen.has(normalized)) {
|
|
return;
|
|
}
|
|
seen.add(normalized);
|
|
result.push(normalized);
|
|
};
|
|
|
|
for (const toolPath of extractCursorToolFilePaths(values)) {
|
|
const marker = "/tools/";
|
|
const markerIndex = toolPath.lastIndexOf(marker);
|
|
if (markerIndex > 0) {
|
|
addPath(`${toolPath.slice(0, markerIndex)}/INSTRUCTIONS.md`);
|
|
}
|
|
}
|
|
|
|
for (const value of Array.isArray(values) ? values : [values]) {
|
|
if (typeof value !== "string") {
|
|
continue;
|
|
}
|
|
if (/\/mcps\/[^/]+\/INSTRUCTIONS\.md$/.test(value.trim())) {
|
|
addPath(value.trim());
|
|
}
|
|
for (const match of value.matchAll(/\/[^\s"'`<>]+\/mcps\/[^\s"'`<>]+\/INSTRUCTIONS\.md/g)) {
|
|
addPath(match[0]);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function isLikelyCursorInstructionText(value) {
|
|
const text = String(value || "").trim();
|
|
if (text.length < 250 || text.length > 120000 || looksLikeNoiseString(text) || looksLikeJsonText(text)) {
|
|
return false;
|
|
}
|
|
if (isCursorMcpToolFilePath(text) || looksLikeGitStatusText(text)) {
|
|
return false;
|
|
}
|
|
return /\bMCP\b|\bCORE WORKFLOW\b|\bCDP USAGE\b|\bCRITICAL\b|\bYou MUST\b|\bUse [a-zA-Z0-9_]+ when\b|\ballows you to\b|A Cursor Canvas/.test(text);
|
|
}
|
|
|
|
function scoreCursorInstructionText(value) {
|
|
const text = String(value || "");
|
|
let score = Math.min(text.length, 10000) / 100;
|
|
if (/\bMCP\b/.test(text)) {
|
|
score += 50;
|
|
}
|
|
if (/\bCORE WORKFLOW\b|\bCDP USAGE\b/.test(text)) {
|
|
score += 35;
|
|
}
|
|
if (/A Cursor Canvas/.test(text)) {
|
|
score += 25;
|
|
}
|
|
if (/\bUse [a-zA-Z0-9_]+ when\b/.test(text)) {
|
|
score += 20;
|
|
}
|
|
return score;
|
|
}
|
|
|
|
function composeCursorWorkspaceContext(values) {
|
|
const strings = (Array.isArray(values) ? values : [values]).filter((value) => typeof value === "string");
|
|
const workspaceRoot = strings.find((value) =>
|
|
/^\/.+/.test(value) &&
|
|
!value.includes("/.cursor/") &&
|
|
!value.includes("/mcps/") &&
|
|
!value.endsWith(".json") &&
|
|
value.length < 500
|
|
);
|
|
const os = strings.find((value) => /^(?:darwin|linux|win32)\s+\S+/i.test(value.trim()));
|
|
const shell = strings.find((value) => /^(?:zsh|bash|fish|pwsh|powershell|cmd)$/i.test(value.trim()));
|
|
const timezone = strings.find((value) => /^[A-Za-z_]+\/[A-Za-z0-9_+\-]+$/.test(value.trim()));
|
|
const gitStatus = strings.find(looksLikeGitStatusText);
|
|
const lines = [];
|
|
if (workspaceRoot) {
|
|
lines.push(`Workspace root: ${workspaceRoot}`);
|
|
}
|
|
if (os) {
|
|
lines.push(`OS: ${os.trim()}`);
|
|
}
|
|
if (shell) {
|
|
lines.push(`Shell: ${shell.trim()}`);
|
|
}
|
|
if (timezone) {
|
|
lines.push(`Timezone: ${timezone.trim()}`);
|
|
}
|
|
if (gitStatus) {
|
|
lines.push(`Git status:\n${gitStatus.trim()}`);
|
|
}
|
|
return lines.length > 0 ? `Cursor workspace context:\n${lines.join("\n")}` : "";
|
|
}
|
|
|
|
function looksLikeGitStatusText(value) {
|
|
const text = String(value || "").trim();
|
|
if (!text || text.length > 20000) {
|
|
return false;
|
|
}
|
|
const lines = text.split(/\r?\n/).filter(Boolean);
|
|
if (lines.length === 0) {
|
|
return false;
|
|
}
|
|
return lines.filter((line) => /^(?:[ MADRCU?!]{1,2}|\?\?)\s+/.test(line)).length >= Math.ceil(lines.length * 0.5);
|
|
}
|
|
|
|
function truncateComposedSystemPrompt(value) {
|
|
const maxLength = 32000;
|
|
return value.length > maxLength ? `${value.slice(0, maxLength)}\n\n[Cursor context truncated at ${maxLength} characters]` : value;
|
|
}
|
|
|
|
function readTextFileIfSmall(filePath, maxBytes) {
|
|
try {
|
|
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
|
|
return "";
|
|
}
|
|
const stat = fs.statSync(filePath);
|
|
if (stat.size > maxBytes) {
|
|
return "";
|
|
}
|
|
return fs.readFileSync(filePath, "utf8");
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
function stringifyPromptCandidate(value) {
|
|
const content = normalizeContentValue(value);
|
|
if (typeof content === "string") {
|
|
return content.trim() || undefined;
|
|
}
|
|
if (Array.isArray(content)) {
|
|
const text = stringifyContent(content).trim();
|
|
return text || undefined;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function extractCursorNativeToolsFromValues(values) {
|
|
const names = extractCursorNativeToolNames(values);
|
|
return names.map((name) => cursorNativeToolSpecToOpenAITool(lookupCursorNativeToolSpec(name))).filter(Boolean);
|
|
}
|
|
|
|
function extractCursorNativeToolsFromRunRequest(runtime, runRequest, values, systemPrompt) {
|
|
const extraction = describeCursorNativeToolExtraction(runtime, runRequest, values, systemPrompt);
|
|
return extraction.names.map((name) => cursorNativeToolSpecToOpenAITool(lookupCursorNativeToolSpec(name))).filter(Boolean);
|
|
}
|
|
|
|
function describeCursorNativeToolExtraction(runtime, runRequest, values, systemPrompt) {
|
|
const sourceValues = [...(Array.isArray(values) ? values : [values]), systemPrompt].filter(Boolean);
|
|
const textNames = extractCursorNativeToolNames(sourceValues);
|
|
const nativeSupportedTools = extractCursorSupportedToolEnums(runRequest);
|
|
const enumNames = nativeSupportedTools
|
|
.map((item) => CURSOR_NATIVE_SUPPORTED_TOOL_TO_SPEC_NAME.get(item.value))
|
|
.filter(Boolean);
|
|
const fallbackApplied = shouldForwardCursorNativeBuiltinTools(runtime) &&
|
|
enumNames.length === 0 &&
|
|
hasCursorWorkspaceContext(values);
|
|
const names = uniqueStrings([
|
|
...textNames,
|
|
...enumNames,
|
|
...(fallbackApplied ? CURSOR_NATIVE_BUILTIN_TOOL_NAMES : [])
|
|
]);
|
|
const unsupportedBuiltins = CURSOR_NATIVE_BUILTIN_TOOL_NAMES.filter((name) => !lookupCursorNativeToolSpec(name));
|
|
return {
|
|
enumNames: uniqueStrings(enumNames),
|
|
enumValues: summarizeCursorSupportedToolEnums(runtime, nativeSupportedTools),
|
|
fallbackApplied,
|
|
fallbackReason: fallbackApplied
|
|
? "AgentRunRequest exposed workspace context and MCP descriptors but no Cursor native tool enum list"
|
|
: undefined,
|
|
forwardCursorNativeBuiltinTools: shouldForwardCursorNativeBuiltinTools(runtime),
|
|
hasWorkspaceContext: hasCursorWorkspaceContext(values),
|
|
names,
|
|
textNames: uniqueStrings(textNames),
|
|
unsupportedBuiltins
|
|
};
|
|
}
|
|
|
|
function shouldForwardCursorNativeBuiltinTools(runtime) {
|
|
return runtime?.forwardCursorNativeBuiltinTools !== false;
|
|
}
|
|
|
|
function hasCursorWorkspaceContext(values) {
|
|
return Boolean(composeCursorWorkspaceContext(values));
|
|
}
|
|
|
|
function extractCursorSupportedToolEnums(buffer) {
|
|
if (!Buffer.isBuffer(buffer) || buffer.length === 0) {
|
|
return [];
|
|
}
|
|
const results = [];
|
|
const seenBuffers = new Set();
|
|
collectCursorSupportedToolEnums(buffer, "", 0, seenBuffers, results);
|
|
const seenValues = new Set();
|
|
return results.filter((item) => {
|
|
const key = `${item.path}:${item.value}`;
|
|
if (seenValues.has(key)) {
|
|
return false;
|
|
}
|
|
seenValues.add(key);
|
|
return true;
|
|
});
|
|
}
|
|
|
|
function collectCursorSupportedToolEnums(buffer, currentPath, depth, seenBuffers, results) {
|
|
if (!Buffer.isBuffer(buffer) || buffer.length === 0 || depth > 14) {
|
|
return;
|
|
}
|
|
const key = `${buffer.length}:${buffer.subarray(0, 24).toString("hex")}`;
|
|
if (seenBuffers.has(key)) {
|
|
return;
|
|
}
|
|
seenBuffers.add(key);
|
|
|
|
forEachProtoField(buffer, (field) => {
|
|
const fieldPath = currentPath ? `${currentPath}.${field.fieldNumber}` : String(field.fieldNumber);
|
|
if (CURSOR_NATIVE_SUPPORTED_TOOL_FIELDS.has(field.fieldNumber)) {
|
|
for (const value of decodeCursorSupportedToolEnumField(field)) {
|
|
const name = CURSOR_NATIVE_SUPPORTED_TOOL_ENUM_NAMES.get(value);
|
|
if (name) {
|
|
results.push({
|
|
name,
|
|
path: fieldPath,
|
|
value
|
|
});
|
|
}
|
|
}
|
|
}
|
|
if (field.wireType === 2 && shouldRecurseIntoCursorProtoField(field.value, depth)) {
|
|
collectCursorSupportedToolEnums(field.value, fieldPath, depth + 1, seenBuffers, results);
|
|
}
|
|
});
|
|
}
|
|
|
|
function decodeCursorSupportedToolEnumField(field) {
|
|
if (field.wireType === 0) {
|
|
return [Number(field.value)].filter((value) => CURSOR_NATIVE_SUPPORTED_TOOL_ENUM_NAMES.has(value));
|
|
}
|
|
if (field.wireType !== 2 || field.value.length === 0) {
|
|
return [];
|
|
}
|
|
const values = decodePackedCursorSupportedToolEnums(field.value);
|
|
return values.length > 0 ? values : [];
|
|
}
|
|
|
|
function decodePackedCursorSupportedToolEnums(buffer) {
|
|
const values = [];
|
|
let offset = 0;
|
|
while (offset < buffer.length) {
|
|
const value = readProtoVarint(buffer, offset);
|
|
if (!value) {
|
|
return [];
|
|
}
|
|
offset = value.offset;
|
|
const number = Number(value.value);
|
|
if (!CURSOR_NATIVE_SUPPORTED_TOOL_ENUM_NAMES.has(number)) {
|
|
return [];
|
|
}
|
|
values.push(number);
|
|
}
|
|
return values;
|
|
}
|
|
|
|
function shouldRecurseIntoCursorProtoField(buffer, depth) {
|
|
if (!Buffer.isBuffer(buffer) || buffer.length === 0 || depth >= 14) {
|
|
return false;
|
|
}
|
|
if (looksLikeTextBuffer(buffer) && !hasReadableProtoFields(buffer)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function summarizeCursorSupportedToolEnums(runtime, tools) {
|
|
if (!Array.isArray(tools) || tools.length === 0) {
|
|
return [];
|
|
}
|
|
return tools.slice(0, decodeDiagnosticSampleLimit(runtime)).map((item) => ({
|
|
name: item.name,
|
|
path: item.path,
|
|
tool: CURSOR_NATIVE_SUPPORTED_TOOL_TO_SPEC_NAME.get(item.value),
|
|
value: item.value
|
|
}));
|
|
}
|
|
|
|
function extractCursorNativeToolNames(values) {
|
|
const names = new Set();
|
|
const text = (Array.isArray(values) ? values : [values])
|
|
.filter((value) => typeof value === "string")
|
|
.join("\n");
|
|
for (const spec of cursorNativeToolSpecs()) {
|
|
for (const alias of [spec.name, ...(spec.aliases || [])]) {
|
|
if (new RegExp(`(^|[^A-Za-z0-9_])${escapeRegExp(alias)}([^A-Za-z0-9_]|$)`).test(text)) {
|
|
names.add(spec.name);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return [...names];
|
|
}
|
|
|
|
function cursorNativeToolSpecToOpenAITool(spec) {
|
|
if (!spec) {
|
|
return undefined;
|
|
}
|
|
return {
|
|
function: {
|
|
description: spec.description,
|
|
name: spec.name,
|
|
parameters: spec.parameters
|
|
},
|
|
type: "function"
|
|
};
|
|
}
|
|
|
|
function lookupCursorNativeToolSpec(name) {
|
|
const normalized = normalizeToolBridgeName(name);
|
|
return cursorNativeToolSpecs().find((spec) =>
|
|
normalizeToolBridgeName(spec.name) === normalized ||
|
|
(spec.aliases || []).some((alias) => normalizeToolBridgeName(alias) === normalized)
|
|
);
|
|
}
|
|
|
|
function cursorNativeToolSpecs() {
|
|
return [
|
|
{
|
|
aliases: ["Read", "read", "readFile"],
|
|
description: "Read a file through Cursor's native read_file tool. Use this for known project files.",
|
|
encodeArgs: encodeCursorNativeReadArgs,
|
|
name: "read_file",
|
|
parameters: {
|
|
additionalProperties: false,
|
|
properties: {
|
|
include_line_numbers: { type: "boolean" },
|
|
limit: { type: "integer" },
|
|
offset: { type: "integer" },
|
|
path: { type: "string" }
|
|
},
|
|
required: ["path"],
|
|
type: "object"
|
|
},
|
|
toolCallField: 8
|
|
},
|
|
{
|
|
aliases: ["Ls", "ListDir", "list_directory", "ls"],
|
|
description: "List a directory through Cursor's native list_dir tool.",
|
|
encodeArgs: encodeCursorNativeLsArgs,
|
|
name: "list_dir",
|
|
parameters: {
|
|
additionalProperties: false,
|
|
properties: {
|
|
ignore: { items: { type: "string" }, type: "array" },
|
|
path: { type: "string" }
|
|
},
|
|
required: ["path"],
|
|
type: "object"
|
|
},
|
|
toolCallField: 13
|
|
},
|
|
{
|
|
aliases: ["Grep", "grep", "ripgrep_search", "ripgrep_raw_search"],
|
|
description: "Search exact text or regular expressions through Cursor's native grep_search tool.",
|
|
encodeArgs: encodeCursorNativeGrepArgs,
|
|
name: "grep_search",
|
|
parameters: {
|
|
additionalProperties: false,
|
|
properties: {
|
|
case_insensitive: { type: "boolean" },
|
|
context: { type: "integer" },
|
|
context_after: { type: "integer" },
|
|
context_before: { type: "integer" },
|
|
glob: { type: "string" },
|
|
head_limit: { type: "integer" },
|
|
multiline: { type: "boolean" },
|
|
offset: { type: "integer" },
|
|
output_mode: { type: "string" },
|
|
path: { type: "string" },
|
|
pattern: { type: "string" },
|
|
sort: { type: "string" },
|
|
sort_ascending: { type: "boolean" },
|
|
type: { type: "string" }
|
|
},
|
|
required: ["pattern"],
|
|
type: "object"
|
|
},
|
|
toolCallField: 5
|
|
},
|
|
{
|
|
aliases: ["Glob", "glob", "file_search", "FileSearch"],
|
|
description: "Find files by glob pattern through Cursor's native glob/file search tool.",
|
|
encodeArgs: encodeCursorNativeGlobArgs,
|
|
name: "glob_file_search",
|
|
parameters: {
|
|
additionalProperties: false,
|
|
properties: {
|
|
glob_pattern: { type: "string" },
|
|
pattern: { type: "string" },
|
|
target_directory: { type: "string" }
|
|
},
|
|
required: ["glob_pattern"],
|
|
type: "object"
|
|
},
|
|
toolCallField: 4
|
|
},
|
|
{
|
|
aliases: ["SemanticSearch", "semantic_search", "sem_search"],
|
|
description: "Run Cursor's native semantic codebase search tool.",
|
|
encodeArgs: encodeCursorNativeSemSearchArgs,
|
|
name: "codebase_search",
|
|
parameters: {
|
|
additionalProperties: false,
|
|
properties: {
|
|
explanation: { type: "string" },
|
|
query: { type: "string" },
|
|
target_directories: { items: { type: "string" }, type: "array" }
|
|
},
|
|
required: ["query"],
|
|
type: "object"
|
|
},
|
|
toolCallField: 16
|
|
},
|
|
{
|
|
aliases: ["Shell", "run_terminal_cmd", "run_terminal_command", "run_terminal_command_v2"],
|
|
description: "Run a shell command through Cursor's native shell tool.",
|
|
encodeArgs: encodeCursorNativeShellArgs,
|
|
name: "shell",
|
|
parameters: {
|
|
additionalProperties: false,
|
|
properties: {
|
|
close_stdin: { type: "boolean" },
|
|
command: { type: "string" },
|
|
description: { type: "string" },
|
|
hard_timeout: { type: "integer" },
|
|
is_background: { type: "boolean" },
|
|
simple_commands: { items: { type: "string" }, type: "array" },
|
|
skip_approval: { type: "boolean" },
|
|
timeout: { type: "integer" },
|
|
timeout_behavior: { enum: ["cancel", "background"], type: "string" },
|
|
working_directory: { type: "string" }
|
|
},
|
|
required: ["command"],
|
|
type: "object"
|
|
},
|
|
toolCallField: 1
|
|
},
|
|
{
|
|
aliases: ["Delete", "delete", "delete_file"],
|
|
description: "Delete a file through Cursor's native delete tool.",
|
|
encodeArgs: encodeCursorNativeDeleteArgs,
|
|
name: "delete_file",
|
|
parameters: {
|
|
additionalProperties: false,
|
|
properties: {
|
|
path: { type: "string" }
|
|
},
|
|
required: ["path"],
|
|
type: "object"
|
|
},
|
|
toolCallField: 3
|
|
},
|
|
{
|
|
aliases: ["Edit", "edit", "edit_file"],
|
|
description: "Apply a full-file edit through Cursor's native edit tool.",
|
|
encodeArgs: encodeCursorNativeEditArgs,
|
|
name: "edit_file",
|
|
parameters: {
|
|
additionalProperties: false,
|
|
properties: {
|
|
content: { type: "string" },
|
|
path: { type: "string" },
|
|
stream_content: { type: "string" }
|
|
},
|
|
required: ["path", "content"],
|
|
type: "object"
|
|
},
|
|
toolCallField: 12
|
|
},
|
|
{
|
|
aliases: ["ReadLints", "read_lints"],
|
|
description: "Read linter diagnostics through Cursor's native read_lints tool.",
|
|
encodeArgs: encodeCursorNativeReadLintsArgs,
|
|
name: "read_lints",
|
|
parameters: {
|
|
additionalProperties: false,
|
|
properties: {
|
|
paths: { items: { type: "string" }, type: "array" }
|
|
},
|
|
required: ["paths"],
|
|
type: "object"
|
|
},
|
|
toolCallField: 14
|
|
},
|
|
{
|
|
aliases: ["WebSearch", "web_search"],
|
|
description: "Run a web search through Cursor's native web_search tool.",
|
|
encodeArgs: encodeCursorNativeWebSearchArgs,
|
|
name: "web_search",
|
|
parameters: {
|
|
additionalProperties: false,
|
|
properties: {
|
|
query: { type: "string" },
|
|
search_term: { type: "string" }
|
|
},
|
|
required: ["query"],
|
|
type: "object"
|
|
},
|
|
toolCallField: 18
|
|
},
|
|
{
|
|
aliases: ["WebFetch", "web_fetch", "fetch_url"],
|
|
description: "Fetch and convert a web page through Cursor's native web_fetch tool.",
|
|
encodeArgs: encodeCursorNativeUrlArgs,
|
|
name: "web_fetch",
|
|
parameters: {
|
|
additionalProperties: false,
|
|
properties: {
|
|
url: { type: "string" }
|
|
},
|
|
required: ["url"],
|
|
type: "object"
|
|
},
|
|
toolCallField: 37
|
|
},
|
|
{
|
|
aliases: ["Task", "task"],
|
|
description: "Start a Cursor native subagent task.",
|
|
encodeArgs: encodeCursorNativeTaskArgs,
|
|
name: "task",
|
|
parameters: {
|
|
additionalProperties: false,
|
|
properties: {
|
|
agent_id: { type: "string" },
|
|
attachments: { items: { type: "string" }, type: "array" },
|
|
description: { type: "string" },
|
|
model: { type: "string" },
|
|
prompt: { type: "string" },
|
|
resume: { type: "string" }
|
|
},
|
|
required: ["description", "prompt"],
|
|
type: "object"
|
|
},
|
|
toolCallField: 19
|
|
},
|
|
{
|
|
aliases: ["Await", "await", "await_task"],
|
|
description: "Await a Cursor native background task.",
|
|
encodeArgs: encodeCursorNativeAwaitArgs,
|
|
name: "await_task",
|
|
parameters: {
|
|
additionalProperties: false,
|
|
properties: {
|
|
block_until_ms: { type: "integer" },
|
|
regex: { type: "string" },
|
|
task_id: { type: "string" }
|
|
},
|
|
required: ["task_id"],
|
|
type: "object"
|
|
},
|
|
toolCallField: 42
|
|
},
|
|
{
|
|
aliases: ["TodoRead", "ReadTodos", "todo_read"],
|
|
description: "Read Cursor's native todo list.",
|
|
encodeArgs: encodeCursorNativeReadTodosArgs,
|
|
name: "todo_read",
|
|
parameters: {
|
|
additionalProperties: false,
|
|
properties: {
|
|
id_filter: { items: { type: "string" }, type: "array" },
|
|
status_filter: {
|
|
items: { enum: ["pending", "in_progress", "completed", "cancelled"], type: "string" },
|
|
type: "array"
|
|
}
|
|
},
|
|
type: "object"
|
|
},
|
|
toolCallField: 10
|
|
},
|
|
{
|
|
aliases: ["TodoWrite", "todo_write"],
|
|
description: "Update Cursor's native todo list.",
|
|
encodeArgs: encodeCursorNativeUpdateTodosArgs,
|
|
name: "todo_write",
|
|
parameters: {
|
|
additionalProperties: false,
|
|
properties: {
|
|
merge: { type: "boolean" },
|
|
todos: {
|
|
items: {
|
|
additionalProperties: false,
|
|
properties: {
|
|
content: { type: "string" },
|
|
dependencies: { items: { type: "string" }, type: "array" },
|
|
id: { type: "string" },
|
|
status: { enum: ["pending", "in_progress", "completed", "cancelled"], type: "string" }
|
|
},
|
|
required: ["content", "status"],
|
|
type: "object"
|
|
},
|
|
type: "array"
|
|
}
|
|
},
|
|
required: ["todos"],
|
|
type: "object"
|
|
},
|
|
toolCallField: 9
|
|
},
|
|
{
|
|
aliases: ["AskQuestion", "ask_question"],
|
|
description: "Ask the user a structured question through Cursor.",
|
|
encodeArgs: encodeCursorNativeAskQuestionArgs,
|
|
name: "ask_question",
|
|
parameters: {
|
|
additionalProperties: false,
|
|
properties: {
|
|
questions: {
|
|
items: {
|
|
additionalProperties: false,
|
|
properties: {
|
|
allow_multiple: { type: "boolean" },
|
|
id: { type: "string" },
|
|
options: {
|
|
items: {
|
|
additionalProperties: false,
|
|
properties: {
|
|
id: { type: "string" },
|
|
label: { type: "string" }
|
|
},
|
|
required: ["id", "label"],
|
|
type: "object"
|
|
},
|
|
type: "array"
|
|
},
|
|
prompt: { type: "string" }
|
|
},
|
|
required: ["id", "prompt"],
|
|
type: "object"
|
|
},
|
|
type: "array"
|
|
},
|
|
run_async: { type: "boolean" },
|
|
title: { type: "string" }
|
|
},
|
|
required: ["title", "questions"],
|
|
type: "object"
|
|
},
|
|
toolCallField: 23
|
|
},
|
|
{
|
|
aliases: ["SwitchMode", "switch_mode"],
|
|
description: "Switch Cursor agent mode.",
|
|
encodeArgs: encodeCursorNativeSwitchModeArgs,
|
|
name: "switch_mode",
|
|
parameters: {
|
|
additionalProperties: false,
|
|
properties: {
|
|
explanation: { type: "string" },
|
|
target_mode_id: { type: "string" }
|
|
},
|
|
required: ["target_mode_id"],
|
|
type: "object"
|
|
},
|
|
toolCallField: 25
|
|
},
|
|
{
|
|
aliases: ["GenerateImage", "generate_image"],
|
|
description: "Generate an image through Cursor's native generate_image tool.",
|
|
encodeArgs: encodeCursorNativeGenerateImageArgs,
|
|
name: "generate_image",
|
|
parameters: {
|
|
additionalProperties: false,
|
|
properties: {
|
|
description: { type: "string" },
|
|
file_path: { type: "string" },
|
|
reference_image_paths: { items: { type: "string" }, type: "array" }
|
|
},
|
|
required: ["description"],
|
|
type: "object"
|
|
},
|
|
toolCallField: 28
|
|
},
|
|
{
|
|
aliases: ["ListMcpResources", "list_mcp_resources"],
|
|
description: "List MCP resources through Cursor.",
|
|
encodeArgs: encodeCursorNativeListMcpResourcesArgs,
|
|
name: "list_mcp_resources",
|
|
parameters: {
|
|
additionalProperties: false,
|
|
properties: {
|
|
server: { type: "string" }
|
|
},
|
|
type: "object"
|
|
},
|
|
toolCallField: 20
|
|
},
|
|
{
|
|
aliases: ["FetchMcpResource", "ReadMcpResource", "read_mcp_resource"],
|
|
description: "Fetch an MCP resource through Cursor.",
|
|
encodeArgs: encodeCursorNativeReadMcpResourceArgs,
|
|
name: "read_mcp_resource",
|
|
parameters: {
|
|
additionalProperties: false,
|
|
properties: {
|
|
download_path: { type: "string" },
|
|
server: { type: "string" },
|
|
uri: { type: "string" }
|
|
},
|
|
required: ["server", "uri"],
|
|
type: "object"
|
|
},
|
|
toolCallField: 21
|
|
},
|
|
{
|
|
aliases: ["GetMcpTools", "get_mcp_tools"],
|
|
description: "Discover MCP tool schemas through Cursor.",
|
|
encodeArgs: encodeCursorNativeGetMcpToolsArgs,
|
|
name: "get_mcp_tools",
|
|
parameters: {
|
|
additionalProperties: false,
|
|
properties: {
|
|
pattern: { type: "string" },
|
|
server: { type: "string" },
|
|
tool_name: { type: "string" }
|
|
},
|
|
type: "object"
|
|
},
|
|
toolCallField: 44
|
|
},
|
|
{
|
|
aliases: ["CallMcpTool", "call_mcp_tool"],
|
|
description: "Call an MCP tool through Cursor's native MCP bridge.",
|
|
encodeArgs: encodeCursorNativeCallMcpToolArgs,
|
|
name: "call_mcp_tool",
|
|
parameters: {
|
|
additionalProperties: false,
|
|
properties: {
|
|
arguments: { additionalProperties: true, type: "object" },
|
|
args: { additionalProperties: true, type: "object" },
|
|
server: { type: "string" },
|
|
tool_name: { type: "string" }
|
|
},
|
|
required: ["server", "tool_name"],
|
|
type: "object"
|
|
},
|
|
toolCallField: 15
|
|
},
|
|
{
|
|
aliases: ["SetActiveBranch", "set_active_branch"],
|
|
description: "Set Cursor's active branch metadata.",
|
|
encodeArgs: encodeCursorNativeSetActiveBranchArgs,
|
|
name: "set_active_branch",
|
|
parameters: {
|
|
additionalProperties: false,
|
|
properties: {
|
|
branch_name: { type: "string" },
|
|
path: { type: "string" }
|
|
},
|
|
required: ["path", "branch_name"],
|
|
type: "object"
|
|
},
|
|
toolCallField: 46
|
|
}
|
|
];
|
|
}
|
|
|
|
function extractToolsFromValues(values) {
|
|
const tools = [];
|
|
for (const value of values) {
|
|
tools.push(...extractTools(value));
|
|
}
|
|
tools.push(...extractToolsFromCursorToolFiles(values));
|
|
return uniqueTools(tools);
|
|
}
|
|
|
|
function extractTools(value) {
|
|
const tools = [];
|
|
if (typeof value === "string") {
|
|
tools.push(...extractToolsFromCursorToolFiles([value]));
|
|
return uniqueTools(tools);
|
|
}
|
|
const keySet = new Set(CURSOR_TOOL_KEYS.map((key) => key.toLowerCase()));
|
|
walkJson(value, (item) => {
|
|
if (!isRecord(item)) {
|
|
return;
|
|
}
|
|
for (const [key, rawValue] of Object.entries(item)) {
|
|
if (!keySet.has(key.toLowerCase())) {
|
|
continue;
|
|
}
|
|
tools.push(...normalizeToolList(rawValue));
|
|
}
|
|
});
|
|
return uniqueTools(tools);
|
|
}
|
|
|
|
function extractToolsFromCursorToolFiles(values) {
|
|
const tools = [];
|
|
for (const filePath of extractCursorToolFilePaths(values)) {
|
|
const tool = readCursorToolDefinition(filePath);
|
|
if (tool) {
|
|
tools.push(tool);
|
|
}
|
|
}
|
|
return uniqueTools(tools);
|
|
}
|
|
|
|
function extractCursorToolFilePaths(values) {
|
|
const result = [];
|
|
const seen = new Set();
|
|
const addPath = (value) => {
|
|
const filePath = normalizeCursorMcpFilePath(value);
|
|
if (!filePath || seen.has(filePath)) {
|
|
return;
|
|
}
|
|
seen.add(filePath);
|
|
result.push(filePath);
|
|
};
|
|
|
|
for (const value of Array.isArray(values) ? values : [values]) {
|
|
if (typeof value !== "string") {
|
|
continue;
|
|
}
|
|
addPath(value);
|
|
for (const match of value.matchAll(/\/[^\s"'`<>]+\/mcps\/[^\s"'`<>]+\/tools\/[^\s"'`<>]+\.json/g)) {
|
|
addPath(match[0]);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function normalizeCursorMcpFilePath(value) {
|
|
const text = typeof value === "string" ? value.trim() : "";
|
|
if (!text || !text.includes("/mcps/") || !text.endsWith(".json")) {
|
|
return "";
|
|
}
|
|
if (!/\/mcps\/[^/]+\/tools\/[^/]+\.json$/.test(text)) {
|
|
return "";
|
|
}
|
|
return text;
|
|
}
|
|
|
|
function isCursorMcpToolFilePath(value) {
|
|
return Boolean(normalizeCursorMcpFilePath(value));
|
|
}
|
|
|
|
function readCursorToolDefinition(filePath) {
|
|
try {
|
|
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
|
|
return undefined;
|
|
}
|
|
const parsed = readJsonText(fs.readFileSync(filePath, "utf8"));
|
|
if (!isRecord(parsed)) {
|
|
return undefined;
|
|
}
|
|
return normalizeTool(parsed);
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
function normalizeToolList(value) {
|
|
if (Array.isArray(value)) {
|
|
return value.map((item) => normalizeTool(item)).filter(Boolean);
|
|
}
|
|
if (isRecord(value)) {
|
|
return Object.entries(value).map(([name, item]) => {
|
|
if (!isRecord(item)) {
|
|
return normalizeTool({ description: stringifyContent(item), name });
|
|
}
|
|
const tool = { ...item };
|
|
if (!stringValue(tool.name)) {
|
|
tool.name = name;
|
|
}
|
|
return normalizeTool(tool);
|
|
}).filter(Boolean);
|
|
}
|
|
return [];
|
|
}
|
|
|
|
function normalizeConfiguredTools(value) {
|
|
if (isRecord(value) && (Array.isArray(value.tools) || isRecord(value.tools))) {
|
|
return normalizeToolList(value.tools);
|
|
}
|
|
return normalizeToolList(value);
|
|
}
|
|
|
|
function normalizeTool(value) {
|
|
if (!isRecord(value)) {
|
|
return undefined;
|
|
}
|
|
const type = stringValue(value.type);
|
|
if (type && type.toLowerCase().startsWith("web_search")) {
|
|
return { ...value, type };
|
|
}
|
|
|
|
const fn = isRecord(value.function) ? value.function : value;
|
|
const name =
|
|
stringValue(fn.name) ||
|
|
stringValue(value.name) ||
|
|
stringValue(value.toolName) ||
|
|
stringValue(value.functionName);
|
|
if (!name) {
|
|
return undefined;
|
|
}
|
|
|
|
const parameters = normalizeToolParameters(firstDefined(
|
|
fn.parameters,
|
|
value.parameters,
|
|
fn.arguments,
|
|
value.arguments,
|
|
fn.input_schema,
|
|
value.input_schema,
|
|
fn.inputSchema,
|
|
value.inputSchema,
|
|
fn.schema,
|
|
value.schema
|
|
));
|
|
return {
|
|
function: compactObject({
|
|
description: stringValue(fn.description) || stringValue(value.description),
|
|
name,
|
|
parameters
|
|
}),
|
|
type: "function"
|
|
};
|
|
}
|
|
|
|
function normalizeToolParameters(value) {
|
|
if (isRecord(value)) {
|
|
return value;
|
|
}
|
|
if (typeof value === "string") {
|
|
const parsed = readJsonText(value);
|
|
if (isRecord(parsed)) {
|
|
return parsed;
|
|
}
|
|
}
|
|
return { properties: {}, type: "object" };
|
|
}
|
|
|
|
function uniqueTools(tools) {
|
|
const result = [];
|
|
const seen = new Set();
|
|
for (const tool of tools) {
|
|
const key = tool.type === "function" ? `function:${tool.function?.name || ""}` : `${tool.type}:${tool.name || ""}`;
|
|
if (!key || seen.has(key)) {
|
|
continue;
|
|
}
|
|
seen.add(key);
|
|
result.push(tool);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function extractToolChoiceFromValues(values) {
|
|
for (const value of values) {
|
|
const toolChoice = extractToolChoice(value);
|
|
if (toolChoice !== undefined) {
|
|
return toolChoice;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function extractToolChoice(value) {
|
|
return findFirstByKeys(value, CURSOR_TOOL_CHOICE_KEYS, normalizeToolChoice);
|
|
}
|
|
|
|
function normalizeToolChoice(value) {
|
|
if (typeof value === "string" && value.trim()) {
|
|
const normalized = value.trim().toLowerCase();
|
|
if (["auto", "none", "required"].includes(normalized)) {
|
|
return normalized;
|
|
}
|
|
return { function: { name: value.trim() }, type: "function" };
|
|
}
|
|
if (!isRecord(value)) {
|
|
return undefined;
|
|
}
|
|
const type = stringValue(value.type);
|
|
if (type && ["auto", "none", "required"].includes(type.toLowerCase())) {
|
|
return type.toLowerCase();
|
|
}
|
|
const fn = isRecord(value.function) ? value.function : value;
|
|
const name = stringValue(fn.name) || stringValue(value.name) || stringValue(value.toolName);
|
|
return name ? { function: { name }, type: "function" } : undefined;
|
|
}
|
|
|
|
function decodeJsonPayloadsFromProtoStrings(buffer) {
|
|
const payloads = [];
|
|
for (const item of decodeAllProtoStrings(buffer)) {
|
|
const parsed = readJsonText(item.value);
|
|
if (parsed !== undefined) {
|
|
payloads.push(parsed);
|
|
}
|
|
}
|
|
return payloads;
|
|
}
|
|
|
|
function decodeAgentRunRequestContextValues(buffer) {
|
|
const values = [];
|
|
for (const item of decodeAllProtoStrings(buffer)) {
|
|
const text = typeof item.value === "string" ? item.value.trim() : "";
|
|
if (!text || looksLikeNoiseString(text)) {
|
|
continue;
|
|
}
|
|
values.push(text);
|
|
const parsed = readJsonText(text);
|
|
if (parsed !== undefined) {
|
|
values.push(parsed);
|
|
}
|
|
values.push(...decodeEmbeddedStringValues(text));
|
|
}
|
|
return uniqueDecodedValues(values);
|
|
}
|
|
|
|
function firstDefined(...values) {
|
|
return values.find((value) => value !== undefined);
|
|
}
|
|
|
|
function readJsonLikePayload(body) {
|
|
if (!body || body.length === 0) {
|
|
return undefined;
|
|
}
|
|
const text = body.toString("utf8").trim();
|
|
if (!text) {
|
|
return undefined;
|
|
}
|
|
try {
|
|
return JSON.parse(text);
|
|
} catch {
|
|
for (const line of text.split(/\r?\n/)) {
|
|
const trimmed = line.trim();
|
|
if (!trimmed.startsWith("{")) {
|
|
continue;
|
|
}
|
|
try {
|
|
return JSON.parse(trimmed);
|
|
} catch {
|
|
// Try the next line.
|
|
}
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function unwrapCursorJson(value) {
|
|
if (!isRecord(value)) {
|
|
return value;
|
|
}
|
|
if (isRecord(value.message)) {
|
|
return value.message;
|
|
}
|
|
if (isRecord(value.request)) {
|
|
return value.request;
|
|
}
|
|
if (isRecord(value.payload)) {
|
|
return value.payload;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function findFirstStringByKeys(value, keys) {
|
|
return findFirstByKeys(value, keys, (item) => typeof item === "string" && item.trim() ? item.trim() : undefined);
|
|
}
|
|
|
|
function findFirstStringFromValues(values, keys) {
|
|
for (const value of values) {
|
|
const found = findFirstStringByKeys(value, keys);
|
|
if (found) {
|
|
return found;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function findStringValuesByKeys(value, keys) {
|
|
const keySet = new Set(keys.map((key) => key.toLowerCase()));
|
|
const found = [];
|
|
walkJson(value, (item) => {
|
|
if (!isRecord(item)) {
|
|
return;
|
|
}
|
|
for (const [key, rawValue] of Object.entries(item)) {
|
|
if (keySet.has(key.toLowerCase()) && typeof rawValue === "string" && rawValue.trim()) {
|
|
found.push(rawValue.trim());
|
|
}
|
|
}
|
|
});
|
|
return found;
|
|
}
|
|
|
|
function findFirstNumberByKeys(value, keys) {
|
|
return findFirstByKeys(value, keys, (item) => {
|
|
const number = Number(item);
|
|
return Number.isFinite(number) ? number : undefined;
|
|
});
|
|
}
|
|
|
|
function findFirstNumberFromValues(values, keys) {
|
|
for (const value of values) {
|
|
const found = findFirstNumberByKeys(value, keys);
|
|
if (found !== undefined) {
|
|
return found;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function findFirstBooleanByKeys(value, keys) {
|
|
return findFirstByKeys(value, keys, (item) => typeof item === "boolean" ? item : undefined);
|
|
}
|
|
|
|
function findFirstBooleanFromValues(values, keys) {
|
|
for (const value of values) {
|
|
const found = findFirstBooleanByKeys(value, keys);
|
|
if (found !== undefined) {
|
|
return found;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function findBestSystemPromptStringFromValues(values) {
|
|
return values
|
|
.filter((value) => typeof value === "string")
|
|
.map((value) => value.trim())
|
|
.filter((value) =>
|
|
looksLikeSystemPromptText(value) &&
|
|
!isLikelyCursorInstructionText(value) &&
|
|
!looksLikeCursorToolDescriptionText(value)
|
|
)
|
|
.sort((left, right) => right.length - left.length)[0];
|
|
}
|
|
|
|
function looksLikeCursorToolDescriptionText(value) {
|
|
const text = String(value || "").trim();
|
|
if (!text || text.length > 1600 || looksLikeJsonText(text)) {
|
|
return false;
|
|
}
|
|
const firstLine = text.split(/\r?\n/, 1)[0] || text;
|
|
if (/\bChrome DevTools Protocol\b|\bCDP\b.*\bInput\.\*|\btarget browser tab\b/i.test(text)) {
|
|
return true;
|
|
}
|
|
if (
|
|
/^(?:Use|Call|Invoke)\s+[`'"]?[A-Za-z0-9_.:-]+[`'"]?\s+(?:to|when|for)\b/i.test(firstLine) ||
|
|
/\b(?:browser|move_agent|cursor|mcp)_[a-z0-9_]+\b/i.test(text)
|
|
) {
|
|
return true;
|
|
}
|
|
const startsLikeToolDescription =
|
|
/^(?:Move|Open|Create|Rename|Navigate|Capture|Click|Type|Set|Select|Press|Scroll|Drag|Get|Highlight|List|Send|Take|Lock|Read|Write|Search|Delete|Run|Fetch|Find|Inspect|Execute|Apply)\b/.test(firstLine);
|
|
const hasToolUsageLanguage =
|
|
/\bUse (?:this|ONLY|instead|by default|browser_|move_agent_|cursor|the )\b/i.test(text) ||
|
|
/\bDo not use\b|\bDo not call\b|\bMust be called\b|\bCall this\b/i.test(text) ||
|
|
/\bDefaults? to\b|\bRequired for\b|\bOptional\b/i.test(text);
|
|
const referencesToolName = /\b[a-z][a-z0-9]+(?:_[a-z0-9]+)+\b/.test(text);
|
|
return startsLikeToolDescription && hasToolUsageLanguage && (referencesToolName || text.length < 900);
|
|
}
|
|
|
|
function findBestPromptStringFromValues(values) {
|
|
return values
|
|
.filter((value) => typeof value === "string")
|
|
.map((value) => value.trim())
|
|
.filter((value) =>
|
|
value.length > 2 &&
|
|
value.length < 200000 &&
|
|
!looksLikeNoiseString(value) &&
|
|
!looksLikeSystemPromptText(value) &&
|
|
!looksLikeJsonText(value)
|
|
)
|
|
.sort((left, right) => scorePromptString(right) - scorePromptString(left))[0];
|
|
}
|
|
|
|
function scorePromptString(value) {
|
|
const hasNaturalLanguage = /[\p{L}\p{N}]/u.test(value) ? 20 : 0;
|
|
const hasWhitespace = /\s/.test(value) ? 5 : 0;
|
|
const lengthScore = Math.min(value.length, 4000) / 100;
|
|
return hasNaturalLanguage + hasWhitespace + lengthScore;
|
|
}
|
|
|
|
function looksLikeJsonText(value) {
|
|
const trimmed = String(value || "").trim();
|
|
return (trimmed.startsWith("{") && trimmed.endsWith("}")) ||
|
|
(trimmed.startsWith("[") && trimmed.endsWith("]"));
|
|
}
|
|
|
|
function findFirstByKeys(value, keys, read) {
|
|
const keySet = new Set(keys.map((key) => key.toLowerCase()));
|
|
let found;
|
|
walkJson(value, (item) => {
|
|
if (found !== undefined || !isRecord(item)) {
|
|
return;
|
|
}
|
|
for (const [key, rawValue] of Object.entries(item)) {
|
|
if (!keySet.has(key.toLowerCase())) {
|
|
continue;
|
|
}
|
|
const readValue = read(rawValue);
|
|
if (readValue !== undefined) {
|
|
found = readValue;
|
|
return;
|
|
}
|
|
}
|
|
});
|
|
return found;
|
|
}
|
|
|
|
function walkJson(value, visit, depth = 0, seen = new Set()) {
|
|
if (depth > MAX_JSON_SCAN_DEPTH || value === undefined || value === null) {
|
|
return;
|
|
}
|
|
if (typeof value === "object") {
|
|
if (seen.has(value)) {
|
|
return;
|
|
}
|
|
seen.add(value);
|
|
}
|
|
|
|
visit(value);
|
|
|
|
if (Array.isArray(value)) {
|
|
value.forEach((item) => walkJson(item, visit, depth + 1, seen));
|
|
return;
|
|
}
|
|
if (isRecord(value)) {
|
|
Object.values(value).forEach((item) => walkJson(item, visit, depth + 1, seen));
|
|
}
|
|
}
|
|
|
|
function isCursorConnectJsonRequest(headers, path) {
|
|
const contentType = readHeader(headers["content-type"]).toLowerCase();
|
|
return isCursorNativeRpcPath(path) && (
|
|
contentType.includes("json") ||
|
|
contentType.includes("connect+json") ||
|
|
contentType === ""
|
|
);
|
|
}
|
|
|
|
function isCursorNativeLlmRpcPath(runtime, path) {
|
|
if (!isCursorNativeRpcPath(path)) {
|
|
return false;
|
|
}
|
|
const normalized = normalizePath(path);
|
|
const lower = normalized.toLowerCase();
|
|
if (lower === "/aiserver.v1.bidiservice/bidiappend") {
|
|
return false;
|
|
}
|
|
if (lower === "/agent.v1.agentservice/runsse") {
|
|
return true;
|
|
}
|
|
if (matchesConfiguredNativeLlmMethod(runtime, normalized)) {
|
|
return true;
|
|
}
|
|
const parsed = parseCursorNativeRpcPath(normalized);
|
|
if (!parsed) {
|
|
return false;
|
|
}
|
|
return CURSOR_NATIVE_LLM_SERVICE_PATTERN.test(parsed.service) &&
|
|
CURSOR_NATIVE_LLM_METHOD_PATTERN.test(parsed.method);
|
|
}
|
|
|
|
function isCursorNativeRpcPath(path) {
|
|
return CURSOR_NATIVE_RPC_PATH_PATTERN.test(normalizePath(path));
|
|
}
|
|
|
|
function parseCursorNativeRpcPath(path) {
|
|
const match = normalizePath(path).match(/^\/(?:aiserver|agent)\.v\d+\.([^/]+)\/([^/]+)$/i);
|
|
return match ? { method: match[2], service: match[1] } : undefined;
|
|
}
|
|
|
|
function matchesConfiguredNativeLlmMethod(runtime, path) {
|
|
const configured = Array.isArray(runtime?.cursorNativeLlmMethods) ? runtime.cursorNativeLlmMethods : [];
|
|
if (configured.length === 0) {
|
|
return false;
|
|
}
|
|
const normalized = normalizePath(path).toLowerCase();
|
|
const parsed = parseCursorNativeRpcPath(path);
|
|
const method = parsed?.method.toLowerCase();
|
|
return configured.some((item) => {
|
|
const value = String(item || "").trim().toLowerCase();
|
|
if (!value) {
|
|
return false;
|
|
}
|
|
return normalized === normalizePath(value).toLowerCase() || method === value;
|
|
});
|
|
}
|
|
|
|
function isOpenAIChatPath(path) {
|
|
return path === "/chat/completions" || path === "/v1/chat/completions" || path.endsWith("/chat/completions");
|
|
}
|
|
|
|
function isOpenAIResponsesPath(path) {
|
|
return path === "/responses" || path === "/v1/responses" || path.endsWith("/responses");
|
|
}
|
|
|
|
function isAnthropicMessagesPath(path) {
|
|
return path === "/messages" || path === "/v1/messages" || path.endsWith("/v1/messages");
|
|
}
|
|
|
|
function isAnthropicCountTokensPath(path) {
|
|
return path === "/v1/messages/count_tokens" || path.endsWith("/messages/count_tokens");
|
|
}
|
|
|
|
function isModelsPath(path) {
|
|
return path === "/models" || path === "/v1/models" || path.startsWith("/v1/models/");
|
|
}
|
|
|
|
function isGeminiPath(path) {
|
|
return /^\/v1(?:beta)?\/models\/[^/]+:(?:generateContent|streamGenerateContent)$/i.test(path);
|
|
}
|
|
|
|
function originalRequestUrl(request) {
|
|
const explicit = readHeader(request.headers["x-ccr-original-url"]);
|
|
if (explicit) {
|
|
try {
|
|
return new URL(explicit);
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
const host = readHeader(request.headers.host);
|
|
if (!host) {
|
|
return undefined;
|
|
}
|
|
return new URL(`https://${host}${request.url || "/"}`);
|
|
}
|
|
|
|
function readRequestBody(request) {
|
|
return new Promise((resolve, reject) => {
|
|
const chunks = [];
|
|
request.on("data", (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
|
|
request.on("end", () => resolve(Buffer.concat(chunks)));
|
|
request.on("error", reject);
|
|
});
|
|
}
|
|
|
|
function filterResponseHeaders(headers) {
|
|
const filtered = {};
|
|
for (const [key, value] of Object.entries(headers)) {
|
|
if (!HOP_BY_HOP_HEADERS.has(key.toLowerCase()) && value !== undefined) {
|
|
filtered[key] = Array.isArray(value) ? value.map(String) : String(value);
|
|
}
|
|
}
|
|
return filtered;
|
|
}
|
|
|
|
function corsHeaders(extra = {}) {
|
|
return {
|
|
"access-control-allow-headers": "authorization,connect-protocol-version,content-type,x-api-key,x-client-version",
|
|
"access-control-allow-methods": "GET,POST,PUT,PATCH,OPTIONS",
|
|
"access-control-allow-origin": "*",
|
|
...extra
|
|
};
|
|
}
|
|
|
|
function sendJson(response, statusCode, payload) {
|
|
response.writeHead(statusCode, corsHeaders({ "content-type": "application/json" }));
|
|
response.end(`${JSON.stringify(payload)}\n`);
|
|
}
|
|
|
|
function sendBinary(response, statusCode, body, headers) {
|
|
response.writeHead(statusCode, corsHeaders(headers || { "content-type": "application/octet-stream" }));
|
|
response.end(body || Buffer.alloc(0));
|
|
}
|
|
|
|
function protoHeaders(extra = {}) {
|
|
return {
|
|
"cache-control": "no-store",
|
|
"connect-protocol-version": "1",
|
|
"content-type": "application/proto",
|
|
...extra
|
|
};
|
|
}
|
|
|
|
function connectProtoHeaders(extra = {}) {
|
|
return {
|
|
"cache-control": "no-store",
|
|
"connect-content-encoding": "identity",
|
|
"connect-protocol-version": "1",
|
|
"content-type": "application/connect+proto",
|
|
...extra
|
|
};
|
|
}
|
|
|
|
function connectEnvelope(flags, data) {
|
|
const payload = Buffer.isBuffer(data) ? data : Buffer.from(data || []);
|
|
const header = Buffer.alloc(5);
|
|
header[0] = flags;
|
|
header.writeUInt32BE(payload.length, 1);
|
|
return Buffer.concat([header, payload]);
|
|
}
|
|
|
|
function decodeConnectEnvelope(buffer) {
|
|
const messages = decodeConnectMessages(buffer);
|
|
if (messages.length === 0) {
|
|
return Buffer.isBuffer(buffer) ? buffer : Buffer.alloc(0);
|
|
}
|
|
return messages.length === 1 ? messages[0] : Buffer.concat(messages);
|
|
}
|
|
|
|
function decodeConnectMessages(buffer) {
|
|
if (!Buffer.isBuffer(buffer) || buffer.length < 5) {
|
|
return Buffer.isBuffer(buffer) && buffer.length > 0 ? [buffer] : [];
|
|
}
|
|
|
|
const frames = [];
|
|
let offset = 0;
|
|
while (offset + 5 <= buffer.length) {
|
|
const flags = buffer[offset];
|
|
if ((flags & ~0x03) !== 0) {
|
|
return frames.length ? frames : [buffer];
|
|
}
|
|
const length = buffer.readUInt32BE(offset + 1);
|
|
const start = offset + 5;
|
|
const end = start + length;
|
|
if (end > buffer.length) {
|
|
return frames.length ? frames : [buffer];
|
|
}
|
|
if ((flags & 0x02) === 0 && length > 0) {
|
|
const payload = buffer.subarray(start, end);
|
|
frames.push((flags & 0x01) === 0 ? payload : decompressConnectPayload(payload) || payload);
|
|
}
|
|
offset = end;
|
|
}
|
|
if (offset === buffer.length) {
|
|
return frames;
|
|
}
|
|
return frames.length > 0 && buffer.length - offset <= 4 ? frames : [buffer];
|
|
}
|
|
|
|
function decompressConnectPayload(payload) {
|
|
for (const decompress of [zlib.gunzipSync, zlib.inflateSync, zlib.brotliDecompressSync]) {
|
|
try {
|
|
return decompress(payload);
|
|
} catch {
|
|
// Try the next compression format.
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function readJsonText(text) {
|
|
if (typeof text !== "string") {
|
|
return undefined;
|
|
}
|
|
const trimmed = text.trim();
|
|
if (!trimmed) {
|
|
return undefined;
|
|
}
|
|
try {
|
|
return JSON.parse(trimmed);
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
function protoString(fieldNumber, value) {
|
|
const text = stringValue(value);
|
|
if (!text) {
|
|
return Buffer.alloc(0);
|
|
}
|
|
return protoBytes(fieldNumber, Buffer.from(text, "utf8"), true);
|
|
}
|
|
|
|
function protoRawString(fieldNumber, value) {
|
|
if (typeof value !== "string" || value.length === 0) {
|
|
return Buffer.alloc(0);
|
|
}
|
|
return protoBytes(fieldNumber, Buffer.from(value, "utf8"), true);
|
|
}
|
|
|
|
function protoBytes(fieldNumber, value, includeEmpty) {
|
|
const payload = Buffer.isBuffer(value) ? value : Buffer.from(value || []);
|
|
if (!includeEmpty && payload.length === 0) {
|
|
return Buffer.alloc(0);
|
|
}
|
|
return Buffer.concat([protoTag(fieldNumber, 2), protoVarint(payload.length), payload]);
|
|
}
|
|
|
|
function protoMessage(fieldNumber, body) {
|
|
const payload = Buffer.isBuffer(body) ? body : Buffer.from(body || []);
|
|
return Buffer.concat([protoTag(fieldNumber, 2), protoVarint(payload.length), payload]);
|
|
}
|
|
|
|
function protoBool(fieldNumber, value, includeFalse) {
|
|
if (!value && !includeFalse) {
|
|
return Buffer.alloc(0);
|
|
}
|
|
return Buffer.concat([protoTag(fieldNumber, 0), protoVarint(value ? 1 : 0)]);
|
|
}
|
|
|
|
function optionalProtoBool(fieldNumber, value) {
|
|
return typeof value === "boolean" ? protoBool(fieldNumber, value, true) : Buffer.alloc(0);
|
|
}
|
|
|
|
function optionalProtoInt(fieldNumber, value) {
|
|
if (!Number.isFinite(Number(value))) {
|
|
return Buffer.alloc(0);
|
|
}
|
|
return Buffer.concat([protoTag(fieldNumber, 0), protoVarint(value)]);
|
|
}
|
|
|
|
function protoDouble(fieldNumber, value) {
|
|
const buffer = Buffer.alloc(8);
|
|
buffer.writeDoubleLE(Number(value) || 0, 0);
|
|
return Buffer.concat([protoTag(fieldNumber, 1), buffer]);
|
|
}
|
|
|
|
function protoTag(fieldNumber, wireType) {
|
|
return protoVarint((Number(fieldNumber) << 3) | Number(wireType));
|
|
}
|
|
|
|
function protoVarint(value) {
|
|
let number = typeof value === "bigint" ? value : BigInt(Math.max(0, Math.trunc(Number(value) || 0)));
|
|
const bytes = [];
|
|
while (number >= 0x80n) {
|
|
bytes.push(Number((number & 0x7fn) | 0x80n));
|
|
number >>= 7n;
|
|
}
|
|
bytes.push(Number(number));
|
|
return Buffer.from(bytes);
|
|
}
|
|
|
|
function decodeProtoStringField(buffer, fieldNumber) {
|
|
const field = readProtoField(buffer, fieldNumber);
|
|
return field?.wireType === 2 ? stringValue(field.value.toString("utf8")) : undefined;
|
|
}
|
|
|
|
function decodeProtoStringFields(buffer, fieldNumber) {
|
|
return readProtoFields(buffer, fieldNumber)
|
|
.filter((field) => field.wireType === 2)
|
|
.map((field) => field.value.toString("utf8"))
|
|
.filter((value) => value.length > 0);
|
|
}
|
|
|
|
function decodeAllProtoStrings(buffer, depth = 0) {
|
|
if (!Buffer.isBuffer(buffer) || depth > 6) {
|
|
return [];
|
|
}
|
|
const values = [];
|
|
forEachProtoField(buffer, (field) => {
|
|
if (field.wireType === 2 && field.value.length > 0) {
|
|
const text = field.value.toString("utf8");
|
|
if (/^[\t\n\r -~\u00a0-\uffff]+$/.test(text)) {
|
|
values.push({ fieldNumber: field.fieldNumber, value: text });
|
|
}
|
|
values.push(...decodeAllProtoStrings(field.value, depth + 1));
|
|
}
|
|
});
|
|
return values;
|
|
}
|
|
|
|
function decodeProtoBytesField(buffer, fieldNumber) {
|
|
const field = readProtoField(buffer, fieldNumber);
|
|
return field?.wireType === 2 ? field.value : undefined;
|
|
}
|
|
|
|
function decodeProtoBytesFields(buffer, fieldNumber) {
|
|
return readProtoFields(buffer, fieldNumber)
|
|
.filter((field) => field.wireType === 2)
|
|
.map((field) => field.value);
|
|
}
|
|
|
|
function concatProtoBytesFields(buffer, fieldNumber) {
|
|
const fields = decodeProtoBytesFields(buffer, fieldNumber).filter((value) => value.length > 0);
|
|
return fields.length > 0 ? Buffer.concat(fields) : Buffer.alloc(0);
|
|
}
|
|
|
|
function decodeProtoMessageFields(buffer, fieldNumber) {
|
|
return readProtoFields(buffer, fieldNumber)
|
|
.filter((field) => field.wireType === 2)
|
|
.map((field) => field.value);
|
|
}
|
|
|
|
function readLengthDelimitedProtoFields(buffer) {
|
|
const fields = [];
|
|
forEachProtoField(buffer, (field) => {
|
|
if (field.wireType === 2 && field.value.length > 0) {
|
|
fields.push(field);
|
|
}
|
|
});
|
|
return fields;
|
|
}
|
|
|
|
function hasReadableProtoFields(buffer) {
|
|
let found = false;
|
|
forEachProtoField(buffer, () => {
|
|
found = true;
|
|
});
|
|
return found;
|
|
}
|
|
|
|
function decodeProtoIntField(buffer, fieldNumber) {
|
|
const field = readProtoField(buffer, fieldNumber);
|
|
return field?.wireType === 0 ? Number(field.value) : 0;
|
|
}
|
|
|
|
function readProtoFields(buffer, targetFieldNumber) {
|
|
if (!Buffer.isBuffer(buffer)) {
|
|
return [];
|
|
}
|
|
const matches = [];
|
|
forEachProtoField(buffer, (field) => {
|
|
if (field.fieldNumber === targetFieldNumber) {
|
|
matches.push({
|
|
value: field.value,
|
|
wireType: field.wireType
|
|
});
|
|
}
|
|
});
|
|
return matches;
|
|
}
|
|
|
|
function readProtoField(buffer, targetFieldNumber) {
|
|
return readProtoFields(buffer, targetFieldNumber)[0];
|
|
}
|
|
|
|
function forEachProtoField(buffer, visitor) {
|
|
if (!Buffer.isBuffer(buffer)) {
|
|
return;
|
|
}
|
|
|
|
let offset = 0;
|
|
while (offset < buffer.length) {
|
|
const tag = readProtoVarint(buffer, offset);
|
|
if (!tag) {
|
|
return;
|
|
}
|
|
|
|
offset = tag.offset;
|
|
const fieldNumber = Number(tag.value >> 3n);
|
|
const wireType = Number(tag.value & 0x07n);
|
|
if (fieldNumber <= 0) {
|
|
return;
|
|
}
|
|
|
|
if (wireType === 0) {
|
|
const value = readProtoVarint(buffer, offset);
|
|
if (!value) {
|
|
return;
|
|
}
|
|
offset = value.offset;
|
|
visitor({ fieldNumber, value: value.value, wireType });
|
|
continue;
|
|
}
|
|
|
|
if (wireType === 1) {
|
|
if (offset + 8 > buffer.length) {
|
|
return;
|
|
}
|
|
const value = buffer.subarray(offset, offset + 8);
|
|
offset += 8;
|
|
visitor({ fieldNumber, value, wireType });
|
|
continue;
|
|
}
|
|
|
|
if (wireType === 2) {
|
|
const length = readProtoVarint(buffer, offset);
|
|
if (!length) {
|
|
return;
|
|
}
|
|
offset = length.offset;
|
|
const end = offset + Number(length.value);
|
|
if (end > buffer.length) {
|
|
return;
|
|
}
|
|
const value = buffer.subarray(offset, end);
|
|
offset = end;
|
|
visitor({ fieldNumber, value, wireType });
|
|
continue;
|
|
}
|
|
|
|
if (wireType === 5) {
|
|
if (offset + 4 > buffer.length) {
|
|
return;
|
|
}
|
|
const value = buffer.subarray(offset, offset + 4);
|
|
offset += 4;
|
|
visitor({ fieldNumber, value, wireType });
|
|
continue;
|
|
}
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
function readProtoVarint(buffer, offset) {
|
|
let result = 0n;
|
|
let shift = 0n;
|
|
for (let index = offset; index < buffer.length; index += 1) {
|
|
const byte = buffer[index];
|
|
result |= BigInt(byte & 0x7f) << shift;
|
|
if ((byte & 0x80) === 0) {
|
|
return {
|
|
offset: index + 1,
|
|
value: result
|
|
};
|
|
}
|
|
shift += 7n;
|
|
if (shift > 70n) {
|
|
return undefined;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function formatProtoFieldSummary(buffer) {
|
|
if (!Buffer.isBuffer(buffer) || buffer.length === 0) {
|
|
return "empty";
|
|
}
|
|
const fields = [];
|
|
forEachProtoField(buffer, (field) => {
|
|
if (fields.length >= 12) {
|
|
return;
|
|
}
|
|
fields.push(`${field.fieldNumber}:${field.wireType}:${field.wireType === 2 ? field.value.length : String(field.value)}`);
|
|
});
|
|
return fields.length ? fields.join(",") : `unreadable:${buffer.length}`;
|
|
}
|
|
|
|
function configuredGatewayUrl(config) {
|
|
if (typeof config?.routerEndpoint === "string" && config.routerEndpoint.trim()) {
|
|
return config.routerEndpoint.trim();
|
|
}
|
|
const host = config?.gateway?.host && config.gateway.host !== "0.0.0.0" ? config.gateway.host : "127.0.0.1";
|
|
const port = config?.gateway?.port || config?.PORT || 3456;
|
|
return `http://${host}:${port}`;
|
|
}
|
|
|
|
function configuredGatewayApiKey(config) {
|
|
const apiKeys = Array.isArray(config?.APIKEYS) ? config.APIKEYS : [];
|
|
return apiKeys.map((item) => stringValue(item?.key)).find(Boolean) || stringValue(config?.APIKEY);
|
|
}
|
|
|
|
function configuredDefaultModel(config) {
|
|
const routerDefault = stringValue(config?.Router?.default);
|
|
if (routerDefault) {
|
|
return routeTargetModel(routerDefault) || routerDefault;
|
|
}
|
|
|
|
const preferredProvider = stringValue(config?.preferredProvider);
|
|
const providers = Array.isArray(config?.Providers) ? config.Providers : [];
|
|
const preferred = preferredProvider
|
|
? providers.find((provider) => provider?.name === preferredProvider)
|
|
: undefined;
|
|
return firstProviderModel(preferred) || providers.map(firstProviderModel).find(Boolean);
|
|
}
|
|
|
|
function routeTargetModel(value) {
|
|
const parts = value.split(",");
|
|
return parts.length > 1 ? parts.slice(1).join(",").trim() : undefined;
|
|
}
|
|
|
|
function firstProviderModel(provider) {
|
|
return Array.isArray(provider?.models) ? provider.models.map(stringValue).find(Boolean) : undefined;
|
|
}
|
|
|
|
function normalizeStringList(...values) {
|
|
const result = [];
|
|
for (const value of values) {
|
|
if (Array.isArray(value)) {
|
|
value.forEach((item) => {
|
|
const normalized = stringValue(item);
|
|
if (normalized) {
|
|
result.push(normalized.toLowerCase());
|
|
}
|
|
});
|
|
continue;
|
|
}
|
|
const normalized = stringValue(value);
|
|
if (normalized) {
|
|
result.push(normalized.toLowerCase());
|
|
}
|
|
}
|
|
return result.length ? uniqueStrings(result) : undefined;
|
|
}
|
|
|
|
function normalizePathList(value) {
|
|
if (!Array.isArray(value)) {
|
|
return undefined;
|
|
}
|
|
const result = value
|
|
.map((item) => normalizePath(stringValue(item)))
|
|
.filter(Boolean);
|
|
return result.length ? uniqueStrings(result) : undefined;
|
|
}
|
|
|
|
function normalizePath(value) {
|
|
if (!value) {
|
|
return "";
|
|
}
|
|
const trimmed = value.trim();
|
|
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
}
|
|
|
|
function trimTrailingSlash(value) {
|
|
return (value || DEFAULT_GATEWAY_URL).replace(/\/+$/, "");
|
|
}
|
|
|
|
function numberOption(value, fallback) {
|
|
const number = Number(value);
|
|
return Number.isFinite(number) && number > 0 ? number : fallback;
|
|
}
|
|
|
|
function uniqueStrings(values) {
|
|
return [...new Set(values.filter(Boolean))];
|
|
}
|
|
|
|
function compactObject(value) {
|
|
const result = {};
|
|
for (const [key, rawValue] of Object.entries(value)) {
|
|
if (rawValue !== undefined) {
|
|
result[key] = rawValue;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function readHeader(value) {
|
|
if (Array.isArray(value)) {
|
|
return value[0]?.trim() || "";
|
|
}
|
|
return typeof value === "string" ? value.trim() : "";
|
|
}
|
|
|
|
function stringValue(value) {
|
|
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
}
|
|
|
|
function isRecord(value) {
|
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
}
|
|
|
|
function formatError(error) {
|
|
return error instanceof Error ? error.message : String(error);
|
|
}
|