feat(cursor): full OpenAI parity (tool calls, streaming, sessions) (#2082)

Merged automatically
This commit is contained in:
payne
2026-05-10 06:00:18 +03:00
committed by GitHub
parent 09733e4906
commit d06d1ae49c
20 changed files with 4885 additions and 2564 deletions

View File

@@ -608,20 +608,107 @@ export const REGISTRY: Record<string, RegistryEntry> = {
headers: getCursorRegistryHeaders(),
clientVersion: CURSOR_REGISTRY_VERSION,
models: [
{ id: "default", name: "Auto (Server Picks)" },
{ id: "claude-4.6-opus-high-thinking", name: "Claude 4.6 Opus High Thinking" },
{ id: "claude-4.6-opus-high", name: "Claude 4.6 Opus High" },
{ id: "claude-4.6-sonnet-high-thinking", name: "Claude 4.6 Sonnet High Thinking" },
{ id: "claude-4.6-sonnet-high", name: "Claude 4.6 Sonnet High" },
{ id: "claude-4.6-haiku", name: "Claude 4.6 Haiku" },
{ id: "claude-4.6-opus", name: "Claude 4.6 Opus" },
{ id: "claude-4.5-opus-high-thinking", name: "Claude 4.5 Opus High Thinking" },
{ id: "claude-4.5-opus-high", name: "Claude 4.5 Opus High" },
{ id: "claude-4.5-sonnet-thinking", name: "Claude 4.5 Sonnet Thinking" },
{ id: "claude-4.5-sonnet", name: "Claude 4.5 Sonnet" },
{ id: "claude-4.5-haiku", name: "Claude 4.5 Haiku" },
{ id: "claude-4.5-opus", name: "Claude 4.5 Opus" },
{ id: "auto", name: "Auto (Server Picks)" },
{ id: "composer-2-fast", name: "Composer 2 Fast" },
{ id: "composer-2", name: "Composer 2" },
{ id: "gpt-5.3-codex-low", name: "GPT 5.3 Codex Low" },
{ id: "gpt-5.3-codex-low-fast", name: "GPT 5.3 Codex Low Fast" },
{ id: "gpt-5.3-codex", name: "GPT 5.3 Codex" },
{ id: "gpt-5.3-codex-fast", name: "GPT 5.3 Codex Fast" },
{ id: "gpt-5.3-codex-high", name: "GPT 5.3 Codex High" },
{ id: "gpt-5.3-codex-high-fast", name: "GPT 5.3 Codex High Fast" },
{ id: "gpt-5.3-codex-xhigh", name: "GPT 5.3 Codex XHigh" },
{ id: "gpt-5.3-codex-xhigh-fast", name: "GPT 5.3 Codex XHigh Fast" },
{ id: "gpt-5.2", name: "GPT 5.2" },
{ id: "gpt-5.3-codex-spark-preview-low", name: "GPT 5.3 Codex Spark Preview Low" },
{ id: "gpt-5.3-codex-spark-preview", name: "GPT 5.3 Codex Spark Preview" },
{ id: "gpt-5.3-codex-spark-preview-high", name: "GPT 5.3 Codex Spark Preview High" },
{ id: "gpt-5.3-codex-spark-preview-xhigh", name: "GPT 5.3 Codex Spark Preview XHigh" },
{ id: "gpt-5.2-codex-low", name: "GPT 5.2 Codex Low" },
{ id: "gpt-5.2-codex-low-fast", name: "GPT 5.2 Codex Low Fast" },
{ id: "gpt-5.2-codex", name: "GPT 5.2 Codex" },
{ id: "gpt-5.2-codex-fast", name: "GPT 5.2 Codex Fast" },
{ id: "gpt-5.2-codex-high", name: "GPT 5.2 Codex High" },
{ id: "gpt-5.2-codex-high-fast", name: "GPT 5.2 Codex High Fast" },
{ id: "gpt-5.2-codex-xhigh", name: "GPT 5.2 Codex XHigh" },
{ id: "gpt-5.2-codex-xhigh-fast", name: "GPT 5.2 Codex XHigh Fast" },
{ id: "gpt-5.1-codex-max-low", name: "GPT 5.1 Codex Max Low" },
{ id: "gpt-5.1-codex-max-low-fast", name: "GPT 5.1 Codex Max Low Fast" },
{ id: "gpt-5.1-codex-max-medium", name: "GPT 5.1 Codex Max Medium" },
{ id: "gpt-5.1-codex-max-medium-fast", name: "GPT 5.1 Codex Max Medium Fast" },
{ id: "gpt-5.1-codex-max-high", name: "GPT 5.1 Codex Max High" },
{ id: "gpt-5.1-codex-max-high-fast", name: "GPT 5.1 Codex Max High Fast" },
{ id: "gpt-5.1-codex-max-xhigh", name: "GPT 5.1 Codex Max XHigh" },
{ id: "gpt-5.1-codex-max-xhigh-fast", name: "GPT 5.1 Codex Max XHigh Fast" },
{ id: "gpt-5.5-high", name: "GPT 5.5 High" },
{ id: "gpt-5.5-high-fast", name: "GPT 5.5 High Fast" },
{ id: "claude-opus-4-7-thinking-high", name: "Claude Opus 4.7 Thinking High" },
{ id: "gpt-5.4-high", name: "GPT 5.4 High" },
{ id: "gpt-5.4-high-fast", name: "GPT 5.4 High Fast" },
{ id: "claude-4.6-opus-high-thinking", name: "Claude 4.6 Opus High Thinking" },
{ id: "claude-4.6-opus-high-thinking-fast", name: "Claude 4.6 Opus High Thinking Fast" },
{ id: "claude-4.6-sonnet-medium", name: "Claude 4.6 Sonnet Medium" },
{ id: "claude-4.6-sonnet-medium-thinking", name: "Claude 4.6 Sonnet Medium Thinking" },
{ id: "gpt-5.5-none", name: "GPT 5.5 None" },
{ id: "gpt-5.5-none-fast", name: "GPT 5.5 None Fast" },
{ id: "gpt-5.5-low", name: "GPT 5.5 Low" },
{ id: "gpt-5.5-low-fast", name: "GPT 5.5 Low Fast" },
{ id: "gpt-5.5-medium", name: "GPT 5.5 Medium" },
{ id: "gpt-5.5-medium-fast", name: "GPT 5.5 Medium Fast" },
{ id: "gpt-5.5-extra-high", name: "GPT 5.5 Extra High" },
{ id: "gpt-5.5-extra-high-fast", name: "GPT 5.5 Extra High Fast" },
{ id: "claude-opus-4-7-low", name: "Claude Opus 4.7 Low" },
{ id: "claude-opus-4-7-medium", name: "Claude Opus 4.7 Medium" },
{ id: "claude-opus-4-7-high", name: "Claude Opus 4.7 High" },
{ id: "claude-opus-4-7-xhigh", name: "Claude Opus 4.7 XHigh" },
{ id: "claude-opus-4-7-max", name: "Claude Opus 4.7 Max" },
{ id: "claude-opus-4-7-thinking-low", name: "Claude Opus 4.7 Thinking Low" },
{ id: "claude-opus-4-7-thinking-medium", name: "Claude Opus 4.7 Thinking Medium" },
{ id: "claude-opus-4-7-thinking-xhigh", name: "Claude Opus 4.7 Thinking XHigh" },
{ id: "claude-opus-4-7-thinking-max", name: "Claude Opus 4.7 Thinking Max" },
{ id: "gpt-5.4-low", name: "GPT 5.4 Low" },
{ id: "gpt-5.4-medium", name: "GPT 5.4 Medium" },
{ id: "gpt-5.4-medium-fast", name: "GPT 5.4 Medium Fast" },
{ id: "gpt-5.4-xhigh", name: "GPT 5.4 XHigh" },
{ id: "gpt-5.4-xhigh-fast", name: "GPT 5.4 XHigh Fast" },
{ id: "claude-4.6-opus-high", name: "Claude 4.6 Opus High" },
{ id: "claude-4.6-opus-max", name: "Claude 4.6 Opus Max" },
{ id: "claude-4.6-opus-max-thinking", name: "Claude 4.6 Opus Max Thinking" },
{ id: "claude-4.6-opus-max-thinking-fast", name: "Claude 4.6 Opus Max Thinking Fast" },
{ id: "claude-4.5-opus-high", name: "Claude 4.5 Opus High" },
{ id: "claude-4.5-opus-high-thinking", name: "Claude 4.5 Opus High Thinking" },
{ id: "gpt-5.2-low", name: "GPT 5.2 Low" },
{ id: "gpt-5.2-low-fast", name: "GPT 5.2 Low Fast" },
{ id: "gpt-5.2-fast", name: "GPT 5.2 Fast" },
{ id: "gpt-5.2-high", name: "GPT 5.2 High" },
{ id: "gpt-5.2-high-fast", name: "GPT 5.2 High Fast" },
{ id: "gpt-5.2-xhigh", name: "GPT 5.2 XHigh" },
{ id: "gpt-5.2-xhigh-fast", name: "GPT 5.2 XHigh Fast" },
{ id: "gemini-3.1-pro", name: "Gemini 3.1 Pro" },
{ id: "gpt-5.4-mini-none", name: "GPT 5.4 Mini None" },
{ id: "gpt-5.4-mini-low", name: "GPT 5.4 Mini Low" },
{ id: "gpt-5.4-mini-medium", name: "GPT 5.4 Mini Medium" },
{ id: "gpt-5.4-mini-high", name: "GPT 5.4 Mini High" },
{ id: "gpt-5.4-mini-xhigh", name: "GPT 5.4 Mini XHigh" },
{ id: "gpt-5.4-nano-none", name: "GPT 5.4 Nano None" },
{ id: "gpt-5.4-nano-low", name: "GPT 5.4 Nano Low" },
{ id: "gpt-5.4-nano-medium", name: "GPT 5.4 Nano Medium" },
{ id: "gpt-5.4-nano-high", name: "GPT 5.4 Nano High" },
{ id: "gpt-5.4-nano-xhigh", name: "GPT 5.4 Nano XHigh" },
{ id: "grok-4.3", name: "Grok 4.3" },
{ id: "claude-4.5-sonnet", name: "Claude 4.5 Sonnet" },
{ id: "claude-4.5-sonnet-thinking", name: "Claude 4.5 Sonnet Thinking" },
{ id: "gpt-5.1-low", name: "GPT 5.1 Low" },
{ id: "gpt-5.1", name: "GPT 5.1" },
{ id: "gpt-5.1-high", name: "GPT 5.1 High" },
{ id: "gemini-3-flash", name: "Gemini 3 Flash" },
{ id: "gpt-5.1-codex-mini-low", name: "GPT 5.1 Codex Mini Low" },
{ id: "gpt-5.1-codex-mini", name: "GPT 5.1 Codex Mini" },
{ id: "gpt-5.1-codex-mini-high", name: "GPT 5.1 Codex Mini High" },
{ id: "claude-4-sonnet", name: "Claude 4 Sonnet" },
{ id: "claude-4-sonnet-thinking", name: "Claude 4 Sonnet Thinking" },
{ id: "gpt-5-mini", name: "GPT 5 Mini" },
{ id: "kimi-k2.5", name: "Kimi K2.5" },
],
},

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,167 @@
/**
* CursorSessionManager — keeps cursor's h2 streams alive across OpenAI calls
* so a tool-using turn can complete inline.
*
* cursor's `agent.v1.AgentService/Run` is bidirectional. When the model
* invokes an MCP tool it pauses and waits for a `ExecClientMessage.McpResult`
* on the SAME stream. Closing the stream between the OpenAI tool_calls
* response and the role:"tool" follow-up loses the exec_id mapping cursor
* needs to resume.
*
* This manager solves that by retaining the open h2 stream (keyed by
* conversation_id) when the executor reports endReason="tool_calls". The
* next OpenAI call with role:"tool" reacquires the session, replies via
* encodeExecMcpResult on the live stream, and continues driving until
* turn_ended.
*
* Multi-instance considerations: sessions live in process memory. If a
* follow-up call lands on a different OmniRoute instance, acquire() returns
* undefined and the executor falls back to cold-resume (fresh RunRequest
* with all history flattened into UserText). Cold-resume is correctness-
* preserving but loses the inline efficiency.
*
* Concurrency: one in-flight call per session. The acquire/release pattern
* keeps a session in "awaiting_tool_result" between calls; if a second call
* arrives while the first is still running, acquire() returns undefined and
* the second falls back to cold-resume.
*
* TTL: sessions evict after CURSOR_SESSION_IDLE_TTL_MS (default 5min). The
* sweep runs lazily on every acquire/release rather than via setInterval to
* keep this module test-friendly.
*/
import type { ClientHttp2Session, ClientHttp2Stream } from "node:http2";
import { encodeExecMcpResult } from "../utils/cursorAgentProtobuf.ts";
const DEFAULT_IDLE_TTL_MS = 5 * 60 * 1000;
export type CursorSession = {
conversationId: string;
h2Client: ClientHttp2Session;
h2Req: ClientHttp2Stream;
blobStore: Map<string, Buffer>;
pendingToolCalls: Map<string, { execMsgId: number; execId: string; toolName: string }>;
state: "running" | "awaiting_tool_result" | "closed";
lastActivityTs: number;
};
export class CursorSessionManager {
private sessions = new Map<string, CursorSession>();
private idleTtlMs: number;
constructor(opts: { idleTtlMs?: number } = {}) {
this.idleTtlMs = opts.idleTtlMs ?? DEFAULT_IDLE_TTL_MS;
}
/**
* Try to reacquire an existing session for this conversation. Returns
* undefined if there isn't one, if it's still running, or if it's idle
* past the TTL (in which case it's closed as a side-effect).
*/
acquire(conversationId: string): CursorSession | undefined {
this.evictExpired();
const session = this.sessions.get(conversationId);
if (!session) return undefined;
if (session.state !== "awaiting_tool_result") return undefined;
session.state = "running";
session.lastActivityTs = Date.now();
return session;
}
/**
* Register a freshly-opened h2 stream as the session for this conversation.
* Any pre-existing session for the same conversation is closed first.
*/
open(
conversationId: string,
h2Client: ClientHttp2Session,
h2Req: ClientHttp2Stream,
blobStore: Map<string, Buffer>
): CursorSession {
const existing = this.sessions.get(conversationId);
if (existing) this.close(existing);
const session: CursorSession = {
conversationId,
h2Client,
h2Req,
blobStore,
pendingToolCalls: new Map(),
state: "running",
lastActivityTs: Date.now(),
};
this.sessions.set(conversationId, session);
return session;
}
/**
* Mark a session as no longer in-flight. If finalState is
* "awaiting_tool_result" the h2 stream stays open and the next acquire()
* for this conversation_id can reuse it. If "idle" or "closed" the
* h2 is torn down here.
*/
release(session: CursorSession, finalState: "awaiting_tool_result" | "idle" | "closed"): void {
session.lastActivityTs = Date.now();
if (finalState === "awaiting_tool_result") {
session.state = "awaiting_tool_result";
return;
}
this.close(session);
}
close(session: CursorSession): void {
session.state = "closed";
try {
session.h2Req.close();
} catch {}
try {
session.h2Client.close();
} catch {}
this.sessions.delete(session.conversationId);
}
/**
* Send an MCP tool result on this session's open h2 stream. Returns true
* if the openAIToolCallId matched a pending call we'd previously seen
* mcp_args for; false otherwise (caller should fall back to cold-resume).
*/
sendToolResult(
session: CursorSession,
openAIToolCallId: string,
content: string,
isError: boolean
): boolean {
const pending = session.pendingToolCalls.get(openAIToolCallId);
if (!pending) return false;
try {
session.h2Req.write(encodeExecMcpResult(pending.execMsgId, pending.execId, content, isError));
session.pendingToolCalls.delete(openAIToolCallId);
session.lastActivityTs = Date.now();
return true;
} catch {
return false;
}
}
private evictExpired(): void {
const now = Date.now();
for (const session of this.sessions.values()) {
if (now - session.lastActivityTs > this.idleTtlMs) {
this.close(session);
}
}
}
// ─── Test / introspection helpers ────────────────────────────────────────
size(): number {
return this.sessions.size;
}
has(conversationId: string): boolean {
return this.sessions.has(conversationId);
}
}
// Module-level singleton — one manager per OmniRoute process. The executor
// imports this directly. For testing, construct a fresh CursorSessionManager.
export const cursorSessionManager = new CursorSessionManager();

File diff suppressed because it is too large Load Diff

View File

@@ -1,897 +0,0 @@
/**
* Cursor Protobuf Encoder/Decoder
* Implements ConnectRPC protobuf wire format for Cursor API
*
* Schema Version: reverse-engineered from Cursor client traffic.
* If Cursor updates their protocol, unknown field warnings will appear
* in the logs — update the FIELD map and bump the version below.
*/
import { v4 as uuidv4 } from "uuid";
import zlib from "zlib";
const DEBUG = process.env.CURSOR_PROTOBUF_DEBUG === "1";
const log = (tag, ...args) => DEBUG && console.log(`[PROTOBUF:${tag}]`, ...args);
const textDecoder = new TextDecoder();
/**
* Schema version — bump when updating field definitions.
* Logged in warnings to help correlate unknown fields with Cursor client versions.
*/
const PROTOBUF_SCHEMA_VERSION = "1.1.3";
// ==================== SCHEMAS ====================
const WIRE_TYPE = { VARINT: 0, FIXED64: 1, LEN: 2, FIXED32: 5 };
const ROLE = { USER: 1, ASSISTANT: 2 };
const UNIFIED_MODE = { CHAT: 1, AGENT: 2 };
const THINKING_LEVEL = { UNSPECIFIED: 0, MEDIUM: 1, HIGH: 2 };
const CLIENT_SIDE_TOOL_V2 = { MCP: 19 };
const FIELD = {
// StreamUnifiedChatRequestWithTools (top level)
REQUEST: 1,
// StreamUnifiedChatRequest
MESSAGES: 1,
UNKNOWN_2: 2,
INSTRUCTION: 3,
UNKNOWN_4: 4,
MODEL: 5,
WEB_TOOL: 8,
UNKNOWN_13: 13,
CURSOR_SETTING: 15,
UNKNOWN_19: 19,
CONVERSATION_ID: 23,
METADATA: 26,
IS_AGENTIC: 27,
SUPPORTED_TOOLS: 29,
MESSAGE_IDS: 30,
MCP_TOOLS: 34,
LARGE_CONTEXT: 35,
UNKNOWN_38: 38,
UNIFIED_MODE: 46,
UNKNOWN_47: 47,
SHOULD_DISABLE_TOOLS: 48,
THINKING_LEVEL: 49,
UNKNOWN_51: 51,
UNKNOWN_53: 53,
UNIFIED_MODE_NAME: 54,
// ConversationMessage
MSG_CONTENT: 1,
MSG_ROLE: 2,
MSG_ID: 13,
MSG_TOOL_RESULTS: 18,
MSG_IS_AGENTIC: 29,
MSG_UNIFIED_MODE: 47,
MSG_SUPPORTED_TOOLS: 51,
// ConversationMessage.ToolResult
TOOL_RESULT_CALL_ID: 1,
TOOL_RESULT_NAME: 2,
TOOL_RESULT_INDEX: 3,
TOOL_RESULT_RAW_ARGS: 5,
TOOL_RESULT_RESULT: 8,
TOOL_RESULT_TOOL_CALL: 11,
TOOL_RESULT_MODEL_CALL_ID: 12,
// ClientSideToolV2Result (nested inside ToolResult.result)
CLIENT_RESULT_TOOL: 1,
CLIENT_RESULT_MCP_RESULT: 28,
CLIENT_RESULT_TOOL_CALL_ID: 35,
CLIENT_RESULT_MODEL_CALL_ID: 48,
CLIENT_RESULT_TOOL_INDEX: 49,
// MCPResult (nested inside ClientSideToolV2Result.mcp_result)
MCP_RESULT_SELECTED_TOOL: 1,
MCP_RESULT_RESULT: 2,
// ClientSideToolV2Call (nested inside ToolResult.tool_call)
CLIENT_CALL_TOOL: 1,
CLIENT_CALL_MCP_PARAMS: 27,
CLIENT_CALL_TOOL_CALL_ID: 3,
CLIENT_CALL_NAME: 9,
CLIENT_CALL_RAW_ARGS: 10,
CLIENT_CALL_TOOL_INDEX: 48,
CLIENT_CALL_MODEL_CALL_ID: 49,
// Model
MODEL_NAME: 1,
MODEL_EMPTY: 4,
// Instruction
INSTRUCTION_TEXT: 1,
// CursorSetting
SETTING_PATH: 1,
SETTING_UNKNOWN_3: 3,
SETTING_UNKNOWN_6: 6,
SETTING_UNKNOWN_8: 8,
SETTING_UNKNOWN_9: 9,
// CursorSetting.Unknown6
SETTING6_FIELD_1: 1,
SETTING6_FIELD_2: 2,
// Metadata
META_PLATFORM: 1,
META_ARCH: 2,
META_VERSION: 3,
META_CWD: 4,
META_TIMESTAMP: 5,
// MessageId
MSGID_ID: 1,
MSGID_SUMMARY: 2,
MSGID_ROLE: 3,
// MCPTool
MCP_TOOL_NAME: 1,
MCP_TOOL_DESC: 2,
MCP_TOOL_PARAMS: 3,
MCP_TOOL_SERVER: 4,
// StreamUnifiedChatResponseWithTools (response)
TOOL_CALL: 1,
RESPONSE: 2,
// ClientSideToolV2Call
TOOL_ID: 3,
TOOL_NAME: 9,
TOOL_RAW_ARGS: 10,
TOOL_IS_LAST: 11,
TOOL_IS_LAST_ALT: 15,
TOOL_MCP_PARAMS: 27,
// MCPParams
MCP_TOOLS_LIST: 1,
// MCPParams.Tool (nested)
MCP_NESTED_NAME: 1,
MCP_NESTED_PARAMS: 3,
// StreamUnifiedChatResponse
RESPONSE_TEXT: 1,
THINKING: 25,
// Thinking
THINKING_TEXT: 1,
};
// Known response field numbers — used to detect unknown fields from protocol updates
const KNOWN_RESPONSE_FIELDS = new Set([
FIELD.TOOL_CALL,
FIELD.RESPONSE,
FIELD.TOOL_ID,
FIELD.TOOL_NAME,
FIELD.TOOL_RAW_ARGS,
FIELD.TOOL_IS_LAST,
FIELD.TOOL_MCP_PARAMS,
FIELD.RESPONSE_TEXT,
FIELD.THINKING,
]);
// ==================== PRIMITIVE ENCODING ====================
export function encodeVarint(value) {
const bytes = [];
while (value >= 0x80) {
bytes.push((value & 0x7f) | 0x80);
value >>>= 7;
}
bytes.push(value & 0x7f);
return new Uint8Array(bytes);
}
export function encodeField(fieldNum, wireType, value) {
const tag = (fieldNum << 3) | wireType;
const tagBytes = encodeVarint(tag);
if (wireType === WIRE_TYPE.VARINT) {
const valueBytes = encodeVarint(value);
return concatArrays(tagBytes, valueBytes);
}
if (wireType === WIRE_TYPE.LEN) {
const dataBytes =
typeof value === "string"
? new TextEncoder().encode(value)
: value instanceof Uint8Array
? value
: Buffer.isBuffer(value)
? new Uint8Array(value)
: new Uint8Array(0);
const lengthBytes = encodeVarint(dataBytes.length);
return concatArrays(tagBytes, lengthBytes, dataBytes);
}
return new Uint8Array(0);
}
function concatArrays(...arrays) {
const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);
const result = new Uint8Array(totalLength);
let offset = 0;
for (const arr of arrays) {
result.set(arr, offset);
offset += arr.length;
}
return result;
}
// ==================== MESSAGE ENCODING ====================
export function encodeToolResult(toolResult) {
const { toolCallId, modelCallId } = parseToolCallId(toolResult.tool_call_id || "");
const rawToolName = toolResult.name || "";
const toolName = formatCursorToolName(rawToolName);
const { selectedTool, serverName } = parseCursorToolName(toolName);
const toolIndex = toolResult.index > 0 ? toolResult.index : 1;
const rawArgs = toolResult.raw_args || "{}";
const resultContent = toolResult.result || "";
const encodedResultMessage = encodeClientSideToolResult(
toolCallId,
modelCallId,
selectedTool,
toolIndex,
resultContent
);
const encodedToolCallMessage = encodeClientSideToolCall(
toolCallId,
modelCallId,
toolName,
selectedTool,
serverName,
rawArgs,
toolIndex
);
return concatArrays(
encodeField(FIELD.TOOL_RESULT_CALL_ID, WIRE_TYPE.LEN, toolCallId),
encodeField(FIELD.TOOL_RESULT_NAME, WIRE_TYPE.LEN, toolName),
encodeField(FIELD.TOOL_RESULT_INDEX, WIRE_TYPE.VARINT, toolIndex),
...(modelCallId
? [encodeField(FIELD.TOOL_RESULT_MODEL_CALL_ID, WIRE_TYPE.LEN, modelCallId)]
: []),
encodeField(FIELD.TOOL_RESULT_RAW_ARGS, WIRE_TYPE.LEN, rawArgs),
...(encodedResultMessage
? [encodeField(FIELD.TOOL_RESULT_RESULT, WIRE_TYPE.LEN, encodedResultMessage)]
: []),
encodeField(FIELD.TOOL_RESULT_TOOL_CALL, WIRE_TYPE.LEN, encodedToolCallMessage)
);
}
function parseToolCallId(toolCallIdRaw) {
if (typeof toolCallIdRaw !== "string" || toolCallIdRaw.length === 0) {
return { toolCallId: "", modelCallId: null };
}
const delimiter = "\nmc_";
const idx = toolCallIdRaw.indexOf(delimiter);
if (idx >= 0) {
return {
toolCallId: toolCallIdRaw.slice(0, idx),
modelCallId: toolCallIdRaw.slice(idx + delimiter.length),
};
}
return { toolCallId: toolCallIdRaw, modelCallId: null };
}
function formatCursorToolName(rawName) {
const base = typeof rawName === "string" && rawName.length > 0 ? rawName : "tool";
if (base.startsWith("mcp__")) {
const rest = base.slice("mcp__".length);
const splitIdx = rest.indexOf("__");
if (splitIdx >= 0) {
const server = rest.slice(0, splitIdx) || "custom";
const name = rest.slice(splitIdx + 2) || "tool";
return `mcp_${server}_${name}`;
}
return `mcp_custom_${rest || "tool"}`;
}
if (base.startsWith("mcp_")) return base;
return `mcp_custom_${base}`;
}
function parseCursorToolName(formattedName) {
if (typeof formattedName !== "string" || !formattedName.startsWith("mcp_")) {
return { serverName: "custom", selectedTool: formattedName || "tool" };
}
const tail = formattedName.slice("mcp_".length);
const splitIdx = tail.indexOf("_");
if (splitIdx < 0) {
return { serverName: "custom", selectedTool: tail || "tool" };
}
return {
serverName: tail.slice(0, splitIdx) || "custom",
selectedTool: tail.slice(splitIdx + 1) || "tool",
};
}
function encodeClientSideToolResult(toolCallId, modelCallId, toolName, toolIndex, resultContent) {
const outputText = typeof resultContent === "string" ? resultContent : "";
const selectedTool = typeof toolName === "string" && toolName.length > 0 ? toolName : "tool";
const mcpResult = concatArrays(
encodeField(FIELD.MCP_RESULT_SELECTED_TOOL, WIRE_TYPE.LEN, selectedTool),
encodeField(FIELD.MCP_RESULT_RESULT, WIRE_TYPE.LEN, outputText)
);
return concatArrays(
encodeField(FIELD.CLIENT_RESULT_TOOL, WIRE_TYPE.VARINT, CLIENT_SIDE_TOOL_V2.MCP),
encodeField(FIELD.CLIENT_RESULT_MCP_RESULT, WIRE_TYPE.LEN, mcpResult),
...(toolCallId
? [encodeField(FIELD.CLIENT_RESULT_TOOL_CALL_ID, WIRE_TYPE.LEN, toolCallId)]
: []),
...(modelCallId
? [encodeField(FIELD.CLIENT_RESULT_MODEL_CALL_ID, WIRE_TYPE.LEN, modelCallId)]
: []),
encodeField(FIELD.CLIENT_RESULT_TOOL_INDEX, WIRE_TYPE.VARINT, toolIndex)
);
}
function encodeMcpParamsForCall(toolName, rawArgs, serverName) {
const tool = concatArrays(
encodeField(FIELD.MCP_TOOL_NAME, WIRE_TYPE.LEN, toolName || "tool"),
encodeField(FIELD.MCP_TOOL_PARAMS, WIRE_TYPE.LEN, rawArgs || "{}"),
encodeField(FIELD.MCP_TOOL_SERVER, WIRE_TYPE.LEN, serverName || "custom")
);
return encodeField(FIELD.MCP_TOOLS_LIST, WIRE_TYPE.LEN, tool);
}
function encodeClientSideToolCall(
toolCallId,
modelCallId,
toolName,
selectedTool,
serverName,
rawArgs,
toolIndex
) {
return concatArrays(
encodeField(FIELD.CLIENT_CALL_TOOL, WIRE_TYPE.VARINT, CLIENT_SIDE_TOOL_V2.MCP),
encodeField(
FIELD.CLIENT_CALL_MCP_PARAMS,
WIRE_TYPE.LEN,
encodeMcpParamsForCall(selectedTool, rawArgs, serverName)
),
...(toolCallId ? [encodeField(FIELD.CLIENT_CALL_TOOL_CALL_ID, WIRE_TYPE.LEN, toolCallId)] : []),
encodeField(FIELD.CLIENT_CALL_NAME, WIRE_TYPE.LEN, toolName || "tool"),
encodeField(FIELD.CLIENT_CALL_RAW_ARGS, WIRE_TYPE.LEN, rawArgs || "{}"),
encodeField(FIELD.CLIENT_CALL_TOOL_INDEX, WIRE_TYPE.VARINT, toolIndex > 0 ? toolIndex : 1),
...(modelCallId
? [encodeField(FIELD.CLIENT_CALL_MODEL_CALL_ID, WIRE_TYPE.LEN, modelCallId)]
: [])
);
}
export function encodeMessage(
content,
role,
messageId,
chatModeEnum = null,
isLast = false,
hasTools = false,
toolResults = []
) {
return concatArrays(
encodeField(FIELD.MSG_CONTENT, WIRE_TYPE.LEN, content),
encodeField(FIELD.MSG_ROLE, WIRE_TYPE.VARINT, role),
encodeField(FIELD.MSG_ID, WIRE_TYPE.LEN, messageId),
...(toolResults.length > 0
? toolResults.map((tr) =>
encodeField(FIELD.MSG_TOOL_RESULTS, WIRE_TYPE.LEN, encodeToolResult(tr))
)
: []),
encodeField(FIELD.MSG_IS_AGENTIC, WIRE_TYPE.VARINT, hasTools ? 1 : 0),
encodeField(
FIELD.MSG_UNIFIED_MODE,
WIRE_TYPE.VARINT,
hasTools ? UNIFIED_MODE.AGENT : UNIFIED_MODE.CHAT
),
...(isLast && hasTools
? [encodeField(FIELD.MSG_SUPPORTED_TOOLS, WIRE_TYPE.LEN, encodeVarint(1))]
: [])
);
}
export function encodeInstruction(text) {
return text ? encodeField(FIELD.INSTRUCTION_TEXT, WIRE_TYPE.LEN, text) : new Uint8Array(0);
}
export function encodeModel(modelName) {
return concatArrays(
encodeField(FIELD.MODEL_NAME, WIRE_TYPE.LEN, modelName),
encodeField(FIELD.MODEL_EMPTY, WIRE_TYPE.LEN, new Uint8Array(0))
);
}
export function encodeCursorSetting() {
const unknown6 = concatArrays(
encodeField(FIELD.SETTING6_FIELD_1, WIRE_TYPE.LEN, new Uint8Array(0)),
encodeField(FIELD.SETTING6_FIELD_2, WIRE_TYPE.LEN, new Uint8Array(0))
);
return concatArrays(
encodeField(FIELD.SETTING_PATH, WIRE_TYPE.LEN, "cursor\\aisettings"),
encodeField(FIELD.SETTING_UNKNOWN_3, WIRE_TYPE.LEN, new Uint8Array(0)),
encodeField(FIELD.SETTING_UNKNOWN_6, WIRE_TYPE.LEN, unknown6),
encodeField(FIELD.SETTING_UNKNOWN_8, WIRE_TYPE.VARINT, 1),
encodeField(FIELD.SETTING_UNKNOWN_9, WIRE_TYPE.VARINT, 1)
);
}
export function encodeMetadata() {
return concatArrays(
encodeField(FIELD.META_PLATFORM, WIRE_TYPE.LEN, process.platform || "linux"),
encodeField(FIELD.META_ARCH, WIRE_TYPE.LEN, process.arch || "x64"),
encodeField(FIELD.META_VERSION, WIRE_TYPE.LEN, process.version || "v20.0.0"),
encodeField(FIELD.META_CWD, WIRE_TYPE.LEN, process.cwd?.() || "/"),
encodeField(FIELD.META_TIMESTAMP, WIRE_TYPE.LEN, new Date().toISOString())
);
}
export function encodeMessageId(messageId, role, summaryId = null) {
return concatArrays(
encodeField(FIELD.MSGID_ID, WIRE_TYPE.LEN, messageId),
...(summaryId ? [encodeField(FIELD.MSGID_SUMMARY, WIRE_TYPE.LEN, summaryId)] : []),
encodeField(FIELD.MSGID_ROLE, WIRE_TYPE.VARINT, role)
);
}
export function encodeMcpTool(tool) {
const toolName = tool.function?.name || tool.name || "";
const toolDesc = tool.function?.description || tool.description || "";
const inputSchema = tool.function?.parameters || tool.input_schema || {};
return concatArrays(
...(toolName ? [encodeField(FIELD.MCP_TOOL_NAME, WIRE_TYPE.LEN, toolName)] : []),
...(toolDesc ? [encodeField(FIELD.MCP_TOOL_DESC, WIRE_TYPE.LEN, toolDesc)] : []),
...(Object.keys(inputSchema).length > 0
? [encodeField(FIELD.MCP_TOOL_PARAMS, WIRE_TYPE.LEN, JSON.stringify(inputSchema))]
: []),
encodeField(FIELD.MCP_TOOL_SERVER, WIRE_TYPE.LEN, "custom")
);
}
// ==================== REQUEST BUILDING ====================
export function encodeRequest(messages, modelName, tools = [], reasoningEffort = null) {
const hasTools = tools?.length > 0;
const isAgentic = hasTools;
const formattedMessages = [];
const messageIds = [];
const normalizedMessages = [];
// Guardrail: split mixed assistant payload into separate assistant messages.
for (let i = 0; i < messages.length; i++) {
const msg = messages[i];
const hasToolCalls = Array.isArray(msg?.tool_calls) && msg.tool_calls.length > 0;
const hasToolResults = Array.isArray(msg?.tool_results) && msg.tool_results.length > 0;
if (msg?.role === "assistant" && hasToolCalls && hasToolResults) {
log(
"ENCODE",
`normalizing mixed assistant tool payload at msg[${i}] (calls=${msg.tool_calls.length}, results=${msg.tool_results.length})`
);
// Keep assistant tool call message without embedded results
normalizedMessages.push({
...msg,
tool_results: [],
});
// Avoid inserting duplicate assistant tool-result message if next one already matches
const nextMsg = messages[i + 1];
const nextHasToolResults =
nextMsg?.role === "assistant" &&
Array.isArray(nextMsg?.tool_results) &&
nextMsg.tool_results.length > 0;
const currentIds = new Set(
msg.tool_results.map((tr) => tr?.tool_call_id).filter((id) => typeof id === "string")
);
const nextIds = new Set(
(nextMsg?.tool_results || [])
.map((tr) => tr?.tool_call_id)
.filter((id) => typeof id === "string")
);
let sameIds = currentIds.size > 0 && currentIds.size === nextIds.size;
if (sameIds) {
for (const id of currentIds) {
if (!nextIds.has(id)) {
sameIds = false;
break;
}
}
}
if (!(nextHasToolResults && sameIds)) {
normalizedMessages.push({
role: "assistant",
content: "",
tool_results: msg.tool_results,
});
}
continue;
}
normalizedMessages.push(msg);
}
// Prepare messages
for (let i = 0; i < normalizedMessages.length; i++) {
const msg = normalizedMessages[i];
const role = msg.role === "user" ? ROLE.USER : ROLE.ASSISTANT;
const msgId = uuidv4();
const isLast = i === normalizedMessages.length - 1;
formattedMessages.push({
content: msg.content,
role,
messageId: msgId,
isLast,
hasTools,
toolResults: msg.tool_results || [],
});
messageIds.push({ messageId: msgId, role });
}
// Map reasoning effort to thinking level
let thinkingLevel = THINKING_LEVEL.UNSPECIFIED;
if (reasoningEffort === "medium") thinkingLevel = THINKING_LEVEL.MEDIUM;
else if (reasoningEffort === "high") thinkingLevel = THINKING_LEVEL.HIGH;
// Build request
return concatArrays(
// Messages
...formattedMessages.map((fm) =>
encodeField(
FIELD.MESSAGES,
WIRE_TYPE.LEN,
encodeMessage(
fm.content,
fm.role,
fm.messageId,
null,
fm.isLast,
fm.hasTools,
fm.toolResults
)
)
),
// Static fields
encodeField(FIELD.UNKNOWN_2, WIRE_TYPE.VARINT, 1),
encodeField(FIELD.INSTRUCTION, WIRE_TYPE.LEN, encodeInstruction("")),
encodeField(FIELD.UNKNOWN_4, WIRE_TYPE.VARINT, 1),
encodeField(FIELD.MODEL, WIRE_TYPE.LEN, encodeModel(modelName)),
encodeField(FIELD.WEB_TOOL, WIRE_TYPE.LEN, ""),
encodeField(FIELD.UNKNOWN_13, WIRE_TYPE.VARINT, 1),
encodeField(FIELD.CURSOR_SETTING, WIRE_TYPE.LEN, encodeCursorSetting()),
encodeField(FIELD.UNKNOWN_19, WIRE_TYPE.VARINT, 1),
encodeField(FIELD.CONVERSATION_ID, WIRE_TYPE.LEN, uuidv4()),
encodeField(FIELD.METADATA, WIRE_TYPE.LEN, encodeMetadata()),
// Tool-related fields
encodeField(FIELD.IS_AGENTIC, WIRE_TYPE.VARINT, isAgentic ? 1 : 0),
...(isAgentic ? [encodeField(FIELD.SUPPORTED_TOOLS, WIRE_TYPE.LEN, encodeVarint(1))] : []),
// Message IDs
...messageIds.map((mid) =>
encodeField(FIELD.MESSAGE_IDS, WIRE_TYPE.LEN, encodeMessageId(mid.messageId, mid.role))
),
// MCP Tools
...(tools?.length > 0
? tools.map((tool) => encodeField(FIELD.MCP_TOOLS, WIRE_TYPE.LEN, encodeMcpTool(tool)))
: []),
// Mode fields
encodeField(FIELD.LARGE_CONTEXT, WIRE_TYPE.VARINT, 0),
encodeField(FIELD.UNKNOWN_38, WIRE_TYPE.VARINT, 0),
encodeField(
FIELD.UNIFIED_MODE,
WIRE_TYPE.VARINT,
isAgentic ? UNIFIED_MODE.AGENT : UNIFIED_MODE.CHAT
),
encodeField(FIELD.UNKNOWN_47, WIRE_TYPE.LEN, ""),
encodeField(FIELD.SHOULD_DISABLE_TOOLS, WIRE_TYPE.VARINT, isAgentic ? 0 : 1),
encodeField(FIELD.THINKING_LEVEL, WIRE_TYPE.VARINT, thinkingLevel),
encodeField(FIELD.UNKNOWN_51, WIRE_TYPE.VARINT, 0),
encodeField(FIELD.UNKNOWN_53, WIRE_TYPE.VARINT, 1),
encodeField(FIELD.UNIFIED_MODE_NAME, WIRE_TYPE.LEN, isAgentic ? "Agent" : "Ask")
);
}
export function buildChatRequest(messages, modelName, tools = [], reasoningEffort = null) {
return encodeField(
FIELD.REQUEST,
WIRE_TYPE.LEN,
encodeRequest(messages, modelName, tools, reasoningEffort)
);
}
export function wrapConnectRPCFrame(payload, compress = false) {
let finalPayload = payload;
let flags = 0x00;
if (compress) {
finalPayload = new Uint8Array(zlib.gzipSync(Buffer.from(payload)));
flags = 0x01;
}
const frame = new Uint8Array(5 + finalPayload.length);
frame[0] = flags;
frame[1] = (finalPayload.length >> 24) & 0xff;
frame[2] = (finalPayload.length >> 16) & 0xff;
frame[3] = (finalPayload.length >> 8) & 0xff;
frame[4] = finalPayload.length & 0xff;
frame.set(finalPayload, 5);
return frame;
}
export function generateCursorBody(messages, modelName, tools = [], reasoningEffort = null) {
log(
"BODY",
`Generating: ${messages.length} msgs, model=${modelName}, tools=${tools.length}, reasoning=${reasoningEffort || "none"}`
);
const protobuf = buildChatRequest(messages, modelName, tools, reasoningEffort);
const framed = wrapConnectRPCFrame(protobuf, false); // Cursor doesn't support compressed requests
log("BODY", `Protobuf=${protobuf.length}B, Framed=${framed.length}B`);
return framed;
}
// ==================== PRIMITIVE DECODING ====================
export function decodeVarint(buffer, offset) {
let result = 0;
let shift = 0;
let pos = offset;
while (pos < buffer.length) {
const b = buffer[pos];
result |= (b & 0x7f) << shift;
pos++;
if (!(b & 0x80)) break;
shift += 7;
}
return [result, pos];
}
export function decodeField(buffer, offset) {
if (offset >= buffer.length) return [null, null, null, offset];
const [tag, pos1] = decodeVarint(buffer, offset);
const fieldNum = tag >> 3;
const wireType = tag & 0x07;
let value;
let pos = pos1;
if (wireType === WIRE_TYPE.VARINT) {
[value, pos] = decodeVarint(buffer, pos);
} else if (wireType === WIRE_TYPE.LEN) {
const [length, pos2] = decodeVarint(buffer, pos);
value = buffer.slice(pos2, pos2 + length);
pos = pos2 + length;
} else if (wireType === WIRE_TYPE.FIXED64) {
value = buffer.slice(pos, pos + 8);
pos += 8;
} else if (wireType === WIRE_TYPE.FIXED32) {
value = buffer.slice(pos, pos + 4);
pos += 4;
} else {
value = null;
}
return [fieldNum, wireType, value, pos];
}
export function decodeMessage(data) {
const fields = new Map();
let pos = 0;
while (pos < data.length) {
const [fieldNum, wireType, value, newPos] = decodeField(data, pos);
if (fieldNum === null) break;
if (!fields.has(fieldNum)) fields.set(fieldNum, []);
fields.get(fieldNum).push({ wireType, value });
pos = newPos;
}
return fields;
}
// ==================== RESPONSE PARSING ====================
export function parseConnectRPCFrame(buffer) {
if (buffer.length < 5) return null;
const flags = buffer[0];
const length = (buffer[1] << 24) | (buffer[2] << 16) | (buffer[3] << 8) | buffer[4];
if (buffer.length < 5 + length) return null;
let payload = buffer.slice(5, 5 + length);
// Decompress if gzip
if (flags === 0x01) {
try {
payload = new Uint8Array(zlib.gunzipSync(Buffer.from(payload)));
} catch (err) {
log("PARSE", `Decompression failed: ${err.message}`);
}
}
return { flags, length, payload, consumed: 5 + length };
}
function extractToolCall(toolCallData) {
const toolCall = decodeMessage(toolCallData);
let toolCallId = "";
let toolName = "";
let rawArgs = "";
let isLast = false;
// Extract tool call ID
if (toolCall.has(FIELD.TOOL_ID)) {
toolCallId = textDecoder.decode(toolCall.get(FIELD.TOOL_ID)[0].value);
}
// Extract tool name
if (toolCall.has(FIELD.TOOL_NAME)) {
toolName = textDecoder.decode(toolCall.get(FIELD.TOOL_NAME)[0].value);
}
// Extract is_last flag
if (toolCall.has(FIELD.TOOL_IS_LAST)) {
isLast = toolCall.get(FIELD.TOOL_IS_LAST)[0].value !== 0;
} else if (toolCall.has(FIELD.TOOL_IS_LAST_ALT)) {
isLast = toolCall.get(FIELD.TOOL_IS_LAST_ALT)[0].value !== 0;
}
// Extract MCP params - nested real tool info
if (toolCall.has(FIELD.TOOL_MCP_PARAMS)) {
try {
const mcpParams = decodeMessage(toolCall.get(FIELD.TOOL_MCP_PARAMS)[0].value);
if (mcpParams.has(FIELD.MCP_TOOLS_LIST)) {
const tool = decodeMessage(mcpParams.get(FIELD.MCP_TOOLS_LIST)[0].value);
if (tool.has(FIELD.MCP_NESTED_NAME)) {
toolName = textDecoder.decode(tool.get(FIELD.MCP_NESTED_NAME)[0].value);
}
if (tool.has(FIELD.MCP_NESTED_PARAMS)) {
rawArgs = textDecoder.decode(tool.get(FIELD.MCP_NESTED_PARAMS)[0].value);
}
}
} catch (err) {
log("EXTRACT", `MCP parse error: ${err.message}`);
}
}
// Fallback to raw_args
if (!rawArgs && toolCall.has(FIELD.TOOL_RAW_ARGS)) {
rawArgs = textDecoder.decode(toolCall.get(FIELD.TOOL_RAW_ARGS)[0].value);
}
if (toolCallId && toolName) {
return {
id: toolCallId,
type: "function",
function: {
name: toolName,
arguments: rawArgs || "{}",
},
isLast,
};
}
return null;
}
function extractTextAndThinking(responseData) {
const nested = decodeMessage(responseData);
let text = null;
let thinking = null;
// Extract text
if (nested.has(FIELD.RESPONSE_TEXT)) {
text = textDecoder.decode(nested.get(FIELD.RESPONSE_TEXT)[0].value);
}
// Extract thinking
if (nested.has(FIELD.THINKING)) {
try {
const thinkingMsg = decodeMessage(nested.get(FIELD.THINKING)[0].value);
if (thinkingMsg.has(FIELD.THINKING_TEXT)) {
thinking = textDecoder.decode(thinkingMsg.get(FIELD.THINKING_TEXT)[0].value);
}
} catch (err) {
log("EXTRACT", `Thinking parse error: ${err.message}`);
}
}
return { text, thinking };
}
export function extractTextFromResponse(payload) {
try {
const fields = decodeMessage(payload);
// Warn about unknown field numbers — may indicate a Cursor protocol update
for (const fieldNum of fields.keys()) {
if (!KNOWN_RESPONSE_FIELDS.has(fieldNum)) {
log(
"SCHEMA",
`Unknown response field #${fieldNum} detected. Schema v${PROTOBUF_SCHEMA_VERSION} may be outdated.`
);
}
}
// Field 1: ClientSideToolV2Call
if (fields.has(FIELD.TOOL_CALL)) {
const toolCall = extractToolCall(fields.get(FIELD.TOOL_CALL)[0].value);
if (toolCall) {
log("EXTRACT", `Tool call: ${toolCall.function.name}`);
return { text: null, error: null, toolCall, thinking: null };
}
}
// Field 2: StreamUnifiedChatResponse
if (fields.has(FIELD.RESPONSE)) {
const { text, thinking } = extractTextAndThinking(fields.get(FIELD.RESPONSE)[0].value);
if (text || thinking) {
return { text, error: null, toolCall: null, thinking };
}
}
return { text: null, error: null, toolCall: null, thinking: null };
} catch (err) {
// Graceful fallback — return raw payload instead of crashing
log("EXTRACT", `Decode failed (schema v${PROTOBUF_SCHEMA_VERSION}): ${err.message}`);
return {
text: null,
error: null,
toolCall: null,
thinking: null,
raw: Buffer.from(payload).toString("base64"),
decodeError: err.message,
};
}
}
// ==================== EXPORTS ====================
const cursorProtobufUtils = {
encodeVarint,
encodeField,
encodeMessage,
buildChatRequest,
wrapConnectRPCFrame,
generateCursorBody,
decodeVarint,
decodeField,
decodeMessage,
parseConnectRPCFrame,
extractTextFromResponse,
};
export default cursorProtobufUtils;

176
scripts/cursor-tap.cjs Normal file
View File

@@ -0,0 +1,176 @@
#!/usr/bin/env node
/**
* cursor-tap — capture cursor agent.v1.AgentService/Run wire bytes for tests.
*
* Usage:
* CURSOR_TOKEN=... node scripts/cursor-tap.cjs <fixture-name> <prompt>
*
* Examples:
* node scripts/cursor-tap.cjs single-turn-chat "say only PING"
* node scripts/cursor-tap.cjs system-prompt "be brief|hi" # split on first '|'
* node scripts/cursor-tap.cjs tool-call "weather in Paris" --tools=get_weather
* node scripts/cursor-tap.cjs composer-2-fast "hi" --model=composer-2-fast
*
* Writes the upstream response bytes to tests/fixtures/cursor/<fixture-name>.bin
* and prints decoded summary to stdout. Use these fixtures in unit tests to
* catch schema drift in cursor-agent's protobuf format.
*
* Note: this is a one-time / on-demand tool. The .bin output is gitignored
* by default; commit fixtures explicitly when you want them in the test
* baseline (tests/fixtures/cursor/.gitignore controls this).
*/
const fs = require("fs");
const path = require("path");
const http2 = require("http2");
const crypto = require("crypto");
const args = process.argv.slice(2);
if (args.length < 2) {
console.error("Usage: cursor-tap.cjs <fixture-name> <prompt> [--model=...] [--tools=name1,name2]");
process.exit(1);
}
const [fixtureName, prompt, ...flags] = args;
const flagMap = Object.fromEntries(
flags.map((f) => {
const m = f.match(/^--([^=]+)=(.*)$/);
return m ? [m[1], m[2]] : [f.replace(/^--/, ""), true];
})
);
const token = process.env.CURSOR_TOKEN;
if (!token) {
console.error("Set CURSOR_TOKEN environment variable.");
process.exit(1);
}
const model = flagMap.model || "auto";
const conversationId = crypto.randomUUID();
const requestId = crypto.randomUUID();
const traceParent = `00-${crypto.randomBytes(16).toString("hex")}-${crypto.randomBytes(8).toString("hex")}-01`;
// ─── Minimal protobuf encoder (mirrors open-sse/utils/cursorAgentProtobuf.ts) ─
function encodeVarint(n) {
const out = [];
let v = BigInt(n);
while (v > 0x7fn) {
out.push(Number(v & 0x7fn) | 0x80);
v >>= 7n;
}
out.push(Number(v));
return Buffer.from(out);
}
function tag(field, wt) {
return encodeVarint((field << 3) | wt);
}
function lenField(f, payload) {
return Buffer.concat([tag(f, 2), encodeVarint(payload.length), payload]);
}
function strField(f, s) {
return lenField(f, Buffer.from(s, "utf8"));
}
function varintField(f, n) {
return Buffer.concat([tag(f, 0), encodeVarint(n)]);
}
function wrapConnectFrame(payload) {
const header = Buffer.alloc(5);
header[0] = 0;
header.writeUInt32BE(payload.length, 1);
return Buffer.concat([header, payload]);
}
// AgentRunRequest body
const userMessage = lenField(
1,
Buffer.concat([
strField(1, prompt),
strField(2, crypto.randomUUID()),
lenField(3, Buffer.alloc(0)),
varintField(4, 1),
])
);
const userMessageAction = lenField(1, userMessage);
const action = lenField(2, userMessageAction);
const conversationState = lenField(1, Buffer.alloc(0));
const requestedModel = lenField(9, strField(1, model === "auto" ? "default" : model));
const arr = Buffer.concat([
conversationState,
action,
lenField(4, Buffer.alloc(0)), // mcp_tools
strField(5, conversationId),
requestedModel,
varintField(12, 0),
strField(16, conversationId),
]);
const acm = lenField(1, arr);
const body = wrapConnectFrame(acm);
// ─── h2 request ────────────────────────────────────────────────────────────
const cleanToken = token.includes("::") ? token.split("::")[1] : token;
const client = http2.connect("https://agentn.global.api5.cursor.sh");
const collected = [];
let responseStatus = 0;
const req = client.request({
":method": "POST",
":path": "/agent.v1.AgentService/Run",
":authority": "agentn.global.api5.cursor.sh",
":scheme": "https",
authorization: `Bearer ${cleanToken}`,
"backend-traceparent": traceParent,
"connect-accept-encoding": "gzip,br",
"connect-protocol-version": "1",
"content-type": "application/connect+proto",
traceparent: traceParent,
"user-agent": "connect-es/1.6.1",
"x-cursor-client-type": "cli",
"x-cursor-client-version": "cli-2025.10.21-b2dfaef",
"x-ghost-mode": "true",
"x-original-request-id": requestId,
"x-request-id": requestId,
});
req.on("response", (h) => {
responseStatus = Number(h[":status"]);
});
req.on("data", (chunk) => {
collected.push(Buffer.from(chunk));
});
req.on("end", () => {
const raw = Buffer.concat(collected);
const outDir = path.join(__dirname, "..", "tests", "fixtures", "cursor");
fs.mkdirSync(outDir, { recursive: true });
const outFile = path.join(outDir, `${fixtureName}.bin`);
fs.writeFileSync(outFile, raw);
console.log(`[cursor-tap] status=${responseStatus} bytes=${raw.length}${outFile}`);
client.close();
});
req.on("error", (err) => {
console.error("[cursor-tap] req error:", err);
process.exit(1);
});
req.write(body);
// NOTE: we never end the request; cursor closes the stream itself when the
// turn is done. For tool-using captures, the script may need to write
// follow-up frames before the stream closes — extend as needed.
// Safety timeout: if cursor doesn't close in 60s, dump what we have.
setTimeout(() => {
console.warn("[cursor-tap] safety timeout; closing");
try {
req.close();
client.close();
} catch {}
const raw = Buffer.concat(collected);
if (raw.length > 0) {
const outDir = path.join(__dirname, "..", "tests", "fixtures", "cursor");
fs.mkdirSync(outDir, { recursive: true });
fs.writeFileSync(path.join(outDir, `${fixtureName}.bin`), raw);
}
process.exit(0);
}, 60_000);

View File

@@ -0,0 +1,130 @@
#!/usr/bin/env node
// Sync the cursor models list in open-sse/config/providerRegistry.ts from
// cursor-agent's runtime model list. Triggers an intentional invalid --model
// invocation so cursor-agent prints "Available models: ..." on stderr.
//
// Usage:
// node scripts/sync-cursor-models.mjs # spawn cursor-agent and apply
// node scripts/sync-cursor-models.mjs --dry-run # print proposed block, don't write
// node scripts/sync-cursor-models.mjs --from-stdin # read the error message from stdin
import { spawnSync } from "node:child_process";
import { readFileSync, writeFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";
const __dirname = dirname(fileURLToPath(import.meta.url));
const REGISTRY_PATH = resolve(__dirname, "..", "open-sse", "config", "providerRegistry.ts");
const args = new Set(process.argv.slice(2));
const DRY_RUN = args.has("--dry-run");
const FROM_STDIN = args.has("--from-stdin");
function readSource() {
if (FROM_STDIN) return readFileSync(0, "utf8");
// cursor-agent prints "Available models: ..." to stderr and exits non-zero.
const r = spawnSync("cursor-agent", ["--model", "--help"], { encoding: "utf8" });
return `${r.stdout || ""}\n${r.stderr || ""}`;
}
// `auto` is a CLI-side abstraction (cursor-agent resolves it locally before
// sending) and `composer-*` targets cursor's edit/composer endpoint, neither
// of which work via the chat RPC. Filter them so the dashboard never offers
// them to the chat path. Pass --include-unsupported to keep them.
function isUnsupportedChatModel(id) {
if (id === "auto") return true;
if (id.startsWith("composer-")) return true;
return false;
}
function parseModelIds(text) {
const m = text.match(/Available models:\s*([^\n]+)/);
if (!m) throw new Error("Could not find 'Available models:' line in cursor-agent output");
const includeUnsupported = args.has("--include-unsupported");
return m[1]
.split(",")
.map((s) => s.trim())
.filter(Boolean)
.filter((id) => includeUnsupported || !isUnsupportedChatModel(id));
}
const SEGMENT_OVERRIDES = {
gpt: "GPT",
claude: "Claude",
gemini: "Gemini",
grok: "Grok",
kimi: "Kimi",
composer: "Composer",
opus: "Opus",
sonnet: "Sonnet",
haiku: "Haiku",
codex: "Codex",
mini: "Mini",
nano: "Nano",
max: "Max",
high: "High",
low: "Low",
medium: "Medium",
xhigh: "XHigh",
none: "None",
fast: "Fast",
thinking: "Thinking",
extra: "Extra",
spark: "Spark",
preview: "Preview",
flash: "Flash",
pro: "Pro",
};
// Pretty-print an id by:
// 1) collapsing claude-NAME-X-Y dotted version (e.g. claude-opus-4-7 → claude-opus-4.7)
// 2) splitting on '-'
// 3) applying SEGMENT_OVERRIDES; falling back to capitalize-first
function humanize(id) {
if (id === "auto") return "Auto (Server Picks)";
// Collapse "X-Y" numeric suffix in claude-foo-X-Y- patterns into "X.Y"
const collapsed = id.replace(/(\d+)-(\d+)(?=-|$)/g, "$1.$2");
const parts = collapsed.split("-");
const labelled = parts.map((p) => {
if (SEGMENT_OVERRIDES[p]) return SEGMENT_OVERRIDES[p];
if (/^\d/.test(p)) return p; // leave version numbers / "k2.5" alone
return p.charAt(0).toUpperCase() + p.slice(1);
});
return labelled.join(" ");
}
function buildModelsArrayLines(ids) {
const seen = new Set();
const out = [];
for (const id of ids) {
if (seen.has(id)) continue;
seen.add(id);
out.push(` { id: ${JSON.stringify(id)}, name: ${JSON.stringify(humanize(id))} },`);
}
return out.join("\n");
}
function replaceCursorModels(source, modelsBlock) {
// Match the `cursor:` provider entry and replace just its `models: [ ... ],` array.
const re = /(\n cursor:\s*\{[\s\S]*?\n models:\s*\[)([\s\S]*?)(\n \],)/;
if (!re.test(source)) throw new Error("Could not locate cursor.models array in registry");
return source.replace(re, `$1\n${modelsBlock}$3`);
}
const ids = parseModelIds(readSource());
const block = buildModelsArrayLines(ids);
if (DRY_RUN) {
console.log(block);
process.exit(0);
}
const before = readFileSync(REGISTRY_PATH, "utf8");
const after = replaceCursorModels(before, block);
if (before === after) {
console.log("No changes — cursor models already in sync.");
process.exit(0);
}
writeFileSync(REGISTRY_PATH, after);
console.log(`Updated ${ids.length} cursor models in ${REGISTRY_PATH}`);

View File

@@ -63,6 +63,7 @@ import {
isAutoFetchModelsEnabled,
persistDiscoveredModels,
} from "@/lib/providerModels/modelDiscovery";
import { fetchCursorAgentModels } from "@/lib/providerModels/cursorAgent";
type JsonRecord = Record<string, unknown>;
type LocalCatalogModel = {
@@ -1525,6 +1526,31 @@ export async function GET(
});
}
if (provider === "cursor") {
const cachedResponse = maybeReturnCachedDiscovery();
if (cachedResponse) return cachedResponse;
const autoFetchDisabledResponse = maybeReturnAutoFetchDisabled();
if (autoFetchDisabledResponse) return autoFetchDisabledResponse;
try {
const models = await fetchCursorAgentModels();
return buildApiDiscoveryResponse(models);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.log("[models] cursor-agent fetch failed:", message);
const fallback = buildDiscoveryFallbackResponse({
cacheWarning: `cursor-agent unavailable (${message}) — using cached catalog`,
localWarning: `cursor-agent unavailable (${message}) — using local catalog`,
});
if (fallback) return fallback;
return NextResponse.json(
{ error: `Failed to fetch Cursor models: ${message}` },
{ status: 502 }
);
}
}
if (provider === "glm" || provider === "glmt") {
const cachedResponse = maybeReturnCachedDiscovery();
if (cachedResponse) return cachedResponse;

View File

@@ -0,0 +1,166 @@
import { spawn } from "node:child_process";
import { existsSync } from "node:fs";
import { homedir } from "node:os";
import { delimiter, join } from "node:path";
// cursor-agent waits on stdin when given a piped fd, so we always launch it
// with stdin closed ("ignore") so it exits as soon as it prints the model list.
function runCursorAgent(
binary: string,
args: string[],
timeoutMs: number
): Promise<{ stdout: string; stderr: string; code: number | null; signal: NodeJS.Signals | null }> {
return new Promise((resolve, reject) => {
let child;
try {
child = spawn(binary, args, { stdio: ["ignore", "pipe", "pipe"] });
} catch (err) {
reject(err);
return;
}
let stdout = "";
let stderr = "";
child.stdout.setEncoding("utf8");
child.stderr.setEncoding("utf8");
child.stdout.on("data", (chunk) => {
stdout += chunk;
});
child.stderr.on("data", (chunk) => {
stderr += chunk;
});
const killTimer = setTimeout(() => child.kill("SIGTERM"), timeoutMs);
child.on("error", (err) => {
clearTimeout(killTimer);
reject(err);
});
child.on("close", (code, signal) => {
clearTimeout(killTimer);
resolve({ stdout, stderr, code, signal });
});
});
}
// Resolve cursor-agent across common install locations, since the standalone
// Next.js server may run with a PATH that doesn't include the user's local bin.
function resolveCursorAgentBinary(): string | null {
const home = homedir();
const candidates = [
join(home, ".local", "bin", "cursor-agent"),
"/root/.local/bin/cursor-agent",
"/usr/local/bin/cursor-agent",
"/usr/bin/cursor-agent",
];
for (const candidate of candidates) {
if (existsSync(candidate)) return candidate;
}
// Fallback: PATH-based lookup (lets execFile do the resolution).
const pathDirs = (process.env.PATH || "").split(delimiter).filter(Boolean);
for (const dir of pathDirs) {
const candidate = join(dir, "cursor-agent");
if (existsSync(candidate)) return candidate;
}
return null;
}
const SEGMENT_OVERRIDES: Record<string, string> = {
gpt: "GPT",
claude: "Claude",
gemini: "Gemini",
grok: "Grok",
kimi: "Kimi",
composer: "Composer",
opus: "Opus",
sonnet: "Sonnet",
haiku: "Haiku",
codex: "Codex",
mini: "Mini",
nano: "Nano",
max: "Max",
high: "High",
low: "Low",
medium: "Medium",
xhigh: "XHigh",
none: "None",
fast: "Fast",
thinking: "Thinking",
extra: "Extra",
spark: "Spark",
preview: "Preview",
flash: "Flash",
pro: "Pro",
};
export function humanizeCursorModelId(id: string): string {
if (id === "auto") return "Auto (Server Picks)";
// Collapse digit-dash-digit suffixes (e.g. claude-opus-4-7 → claude-opus-4.7)
// so version numbers read naturally.
const collapsed = id.replace(/(\d+)-(\d+)(?=-|$)/g, "$1.$2");
return collapsed
.split("-")
.map((part) => {
if (SEGMENT_OVERRIDES[part]) return SEGMENT_OVERRIDES[part];
if (/^\d/.test(part)) return part;
return part.charAt(0).toUpperCase() + part.slice(1);
})
.join(" ");
}
export function parseCursorAgentModels(text: string): string[] {
const match = text.match(/Available models:\s*([^\n]+)/);
if (!match) return [];
const seen = new Set<string>();
const out: string[] = [];
for (const raw of match[1].split(",")) {
const id = raw.trim();
if (!id || seen.has(id)) continue;
seen.add(id);
out.push(id);
}
return out;
}
export type CursorAgentModelEntry = {
id: string;
name: string;
owned_by: "cursor";
};
export async function fetchCursorAgentModels(
options: { binary?: string; timeoutMs?: number } = {}
): Promise<CursorAgentModelEntry[]> {
const binary = options.binary || resolveCursorAgentBinary();
const timeoutMs = options.timeoutMs ?? 5000;
if (!binary) {
throw new Error(
"cursor-agent binary not found. Install it (curl https://cursor.com/install -fsS | bash) so ~/.local/bin/cursor-agent exists, or pass a binary path explicitly."
);
}
// cursor-agent prints "Available models: ..." to stderr and exits non-zero
// when given an unknown model id, so we intentionally pass `--help` as the
// model value to coerce it into listing.
let result: { stdout: string; stderr: string };
try {
result = await runCursorAgent(binary, ["--model", "--help"], timeoutMs);
} catch (err: unknown) {
const e = err as NodeJS.ErrnoException;
if (e?.code === "ENOENT") {
throw new Error(`cursor-agent binary not executable at ${binary}`);
}
throw err;
}
const combined = `${result.stdout}\n${result.stderr}`;
const ids = parseCursorAgentModels(combined);
if (ids.length === 0) {
throw new Error("cursor-agent did not return an 'Available models:' line");
}
return ids.map((id) => ({
id,
name: humanizeCursorModelId(id),
owned_by: "cursor" as const,
}));
}

5
tests/fixtures/cursor/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
# Captured cursor wire bytes from scripts/cursor-tap.cjs.
# By default these are local-only — uncomment a specific fixture to
# include it in the repo as a regression baseline.
*.bin
!.gitignore

View File

@@ -0,0 +1,147 @@
/**
* Cursor end-to-end integration test.
*
* Skipped unless `CURSOR_E2E_TOKEN` env var is set. Exercises the full
* OpenAI-compatible flow against cursor's real `agent.v1.AgentService/Run`
* endpoint:
*
* 1. Single-turn chat with system prompt
* 2. Tool-use round trip (request → tool_calls → role:"tool" follow-up)
* 3. Streaming SSE incremental delivery
* 4. Inline-session reuse across two consecutive calls
* 5. Cold-resume fallback when session is missing/evicted
*
* To run:
* CURSOR_E2E_TOKEN=$(cat ~/.cursor/access-token) \
* node --import tsx/esm --test tests/integration/cursor-e2e.test.ts
*
* Capturing wire fixtures (separate workflow):
* CURSOR_TOKEN=... node scripts/cursor-tap.cjs single-turn-chat "say PING"
*/
import test from "node:test";
import assert from "node:assert/strict";
const TOKEN = process.env.CURSOR_E2E_TOKEN;
const skipReason = TOKEN ? undefined : "CURSOR_E2E_TOKEN not set";
test(
"[cursor-e2e] single-turn plain chat returns assistant text",
{ skip: skipReason },
async () => {
const { CursorExecutor } = await import("../../open-sse/executors/cursor.ts");
const exec = new CursorExecutor();
const result = await exec.execute({
model: "auto",
body: { messages: [{ role: "user", content: "say only PING" }] },
stream: false,
credentials: { accessToken: TOKEN },
signal: undefined,
log: () => {},
upstreamExtraHeaders: undefined,
});
assert.equal(result.response.status, 200);
const json = await result.response.json();
assert.equal(json.choices[0].finish_reason, "stop");
assert.match(json.choices[0].message.content, /PING/i);
}
);
test("[cursor-e2e] system prompt biases the response", { skip: skipReason }, async () => {
const { CursorExecutor } = await import("../../open-sse/executors/cursor.ts");
const exec = new CursorExecutor();
const result = await exec.execute({
model: "auto",
body: {
messages: [
{ role: "system", content: "Reply with exactly the word HAIKU and nothing else." },
{ role: "user", content: "hi" },
],
},
stream: false,
credentials: { accessToken: TOKEN },
signal: undefined,
log: () => {},
upstreamExtraHeaders: undefined,
});
assert.equal(result.response.status, 200);
const json = await result.response.json();
assert.match(json.choices[0].message.content, /HAIKU/);
});
test("[cursor-e2e] tool-use single-turn returns tool_calls", { skip: skipReason }, async () => {
const { CursorExecutor } = await import("../../open-sse/executors/cursor.ts");
const exec = new CursorExecutor();
const result = await exec.execute({
model: "claude-4.6-sonnet-medium",
body: {
messages: [{ role: "user", content: "What's the weather in Paris? Use the tool." }],
tools: [
{
type: "function",
function: {
name: "get_weather",
description: "Get current weather for a city",
parameters: {
type: "object",
properties: { city: { type: "string" } },
required: ["city"],
},
},
},
],
},
stream: false,
credentials: { accessToken: TOKEN },
signal: undefined,
log: () => {},
upstreamExtraHeaders: undefined,
});
assert.equal(result.response.status, 200);
const json = await result.response.json();
assert.equal(json.choices[0].finish_reason, "tool_calls");
const toolCall = json.choices[0].message.tool_calls?.[0];
assert.ok(toolCall, "expected a tool_call");
assert.equal(toolCall.function.name, "get_weather");
assert.match(toolCall.function.arguments, /Paris/);
});
test(
"[cursor-e2e] streaming SSE delivers chunks before the upstream closes",
{ skip: skipReason },
async () => {
const { CursorExecutor } = await import("../../open-sse/executors/cursor.ts");
const exec = new CursorExecutor();
const result = await exec.execute({
model: "auto",
body: { messages: [{ role: "user", content: "count from 1 to 5" }] },
stream: true,
credentials: { accessToken: TOKEN },
signal: undefined,
log: () => {},
upstreamExtraHeaders: undefined,
});
assert.equal(result.response.status, 200);
const reader = (result.response.body as ReadableStream<Uint8Array>).getReader();
const decoder = new TextDecoder();
let chunks = 0;
let totalText = "";
let firstChunkTime: number | null = null;
const startTime = Date.now();
while (true) {
const { done, value } = await reader.read();
if (done) break;
if (firstChunkTime == null) firstChunkTime = Date.now();
const text = decoder.decode(value);
chunks++;
totalText += text;
}
void firstChunkTime;
void startTime;
// Multiple SSE chunks (not one big buffered blob) proves we're streaming
// emit-as-decoded. The exact latency ratio depends on cursor's pacing
// and isn't worth asserting tightly.
assert.ok(chunks > 1, `expected multiple chunks; got ${chunks}`);
assert.match(totalText, /data: \[DONE\]/);
}
);

View File

@@ -0,0 +1,219 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
decodeExecServerEvent,
iterateConnectFrames,
wrapConnectFrame,
} from "../../open-sse/utils/cursorAgentProtobuf";
// ─── Wire-format helpers (match the encoder's primitives) ──────────────────
//
// We synthesize ExecServerMessage payloads for each variant and assert
// decodeExecServerEvent returns the right kind + extracted fields. This
// validates the decoder against the field-number contract independently of
// the encoder (which runs the same code path in different test files).
function v(n: number): Buffer {
const out: number[] = [];
while (n > 0x7f) {
out.push((n & 0x7f) | 0x80);
n >>>= 7;
}
out.push(n);
return Buffer.from(out);
}
function tag(field: number, wireType: number): Buffer {
return v((field << 3) | wireType);
}
function lenPrefixed(field: number, payload: Buffer): Buffer {
return Buffer.concat([tag(field, 2), v(payload.length), payload]);
}
function varintField(field: number, value: number): Buffer {
return Buffer.concat([tag(field, 0), v(value)]);
}
function stringField(field: number, value: string): Buffer {
return lenPrefixed(field, Buffer.from(value, "utf8"));
}
// AgentServerMessage { exec_server_message (2): ExecServerMessage }
function buildAgentServerMessage(esmInner: Buffer): Buffer {
return lenPrefixed(2, esmInner);
}
// ExecServerMessage { id (1): execMsgId, exec_id (15): execId, <variant>: bytes }
function buildExecServerMessage(
execMsgId: number,
execId: string,
variantField: number,
variantPayload: Buffer
): Buffer {
return Buffer.concat([
varintField(1, execMsgId),
stringField(15, execId),
lenPrefixed(variantField, variantPayload),
]);
}
test("decodeExecServerEvent returns null for unrelated AgentServerMessage", () => {
// text_delta InteractionUpdate, no exec_server_message
const interactionUpdate = lenPrefixed(1, lenPrefixed(1, Buffer.from("hi", "utf8")));
const asm = lenPrefixed(1, interactionUpdate);
assert.equal(decodeExecServerEvent(asm), null);
});
test("decodeExecServerEvent recognizes request_context (field 10)", () => {
const variant = Buffer.alloc(0); // RequestContextArgs has no fields we care about
const esm = buildExecServerMessage(1, "exec-rc", 10, variant);
const asm = buildAgentServerMessage(esm);
const event = decodeExecServerEvent(asm);
assert.deepEqual(event, { kind: "exec_request_context", execMsgId: 1, execId: "exec-rc" });
});
test("decodeExecServerEvent recognizes read_args (field 7) with path", () => {
const variant = stringField(1, "/etc/passwd");
const esm = buildExecServerMessage(2, "exec-r", 7, variant);
const event = decodeExecServerEvent(buildAgentServerMessage(esm));
assert.deepEqual(event, {
kind: "exec_read",
execMsgId: 2,
execId: "exec-r",
path: "/etc/passwd",
});
});
test("decodeExecServerEvent recognizes write_args (field 3)", () => {
const variant = stringField(1, "/tmp/x");
const esm = buildExecServerMessage(3, "exec-w", 3, variant);
const event = decodeExecServerEvent(buildAgentServerMessage(esm));
assert.deepEqual(event, { kind: "exec_write", execMsgId: 3, execId: "exec-w", path: "/tmp/x" });
});
test("decodeExecServerEvent recognizes delete_args (field 4)", () => {
const variant = stringField(1, "/tmp/y");
const esm = buildExecServerMessage(4, "exec-d", 4, variant);
const event = decodeExecServerEvent(buildAgentServerMessage(esm));
assert.deepEqual(event, { kind: "exec_delete", execMsgId: 4, execId: "exec-d", path: "/tmp/y" });
});
test("decodeExecServerEvent recognizes ls_args (field 8)", () => {
const variant = stringField(1, "/home");
const esm = buildExecServerMessage(5, "exec-l", 8, variant);
const event = decodeExecServerEvent(buildAgentServerMessage(esm));
assert.deepEqual(event, { kind: "exec_ls", execMsgId: 5, execId: "exec-l", path: "/home" });
});
test("decodeExecServerEvent recognizes grep_args (field 5)", () => {
const variant = stringField(1, "pattern");
const esm = buildExecServerMessage(6, "exec-g", 5, variant);
const event = decodeExecServerEvent(buildAgentServerMessage(esm));
assert.deepEqual(event, { kind: "exec_grep", execMsgId: 6, execId: "exec-g" });
});
test("decodeExecServerEvent recognizes diagnostics_args (field 9)", () => {
const esm = buildExecServerMessage(7, "exec-diag", 9, Buffer.alloc(0));
const event = decodeExecServerEvent(buildAgentServerMessage(esm));
assert.deepEqual(event, { kind: "exec_diagnostics", execMsgId: 7, execId: "exec-diag" });
});
test("decodeExecServerEvent recognizes shell_args (field 2) with command + working_dir", () => {
const variant = Buffer.concat([stringField(1, "ls -la"), stringField(2, "/tmp")]);
const esm = buildExecServerMessage(8, "exec-s", 2, variant);
const event = decodeExecServerEvent(buildAgentServerMessage(esm));
assert.deepEqual(event, {
kind: "exec_shell",
execMsgId: 8,
execId: "exec-s",
command: "ls -la",
workingDir: "/tmp",
});
});
test("decodeExecServerEvent recognizes shell_stream_args (field 14)", () => {
const variant = Buffer.concat([stringField(1, "tail -f x"), stringField(2, "/var/log")]);
const esm = buildExecServerMessage(9, "exec-ss", 14, variant);
const event = decodeExecServerEvent(buildAgentServerMessage(esm));
assert.deepEqual(event, {
kind: "exec_shell_stream",
execMsgId: 9,
execId: "exec-ss",
command: "tail -f x",
workingDir: "/var/log",
});
});
test("decodeExecServerEvent recognizes background_shell_spawn (field 16)", () => {
const variant = Buffer.concat([stringField(1, "node server"), stringField(2, "/app")]);
const esm = buildExecServerMessage(10, "exec-bg", 16, variant);
const event = decodeExecServerEvent(buildAgentServerMessage(esm));
assert.deepEqual(event, {
kind: "exec_bg_shell",
execMsgId: 10,
execId: "exec-bg",
command: "node server",
workingDir: "/app",
});
});
test("decodeExecServerEvent recognizes fetch_args (field 20) with url", () => {
const variant = stringField(1, "https://example.com/x");
const esm = buildExecServerMessage(11, "exec-f", 20, variant);
const event = decodeExecServerEvent(buildAgentServerMessage(esm));
assert.deepEqual(event, {
kind: "exec_fetch",
execMsgId: 11,
execId: "exec-f",
url: "https://example.com/x",
});
});
test("decodeExecServerEvent recognizes write_shell_stdin_args (field 23)", () => {
const esm = buildExecServerMessage(12, "exec-stdin", 23, Buffer.alloc(0));
const event = decodeExecServerEvent(buildAgentServerMessage(esm));
assert.deepEqual(event, { kind: "exec_write_shell_stdin", execMsgId: 12, execId: "exec-stdin" });
});
test("decodeExecServerEvent recognizes mcp_args (field 11) with name + tool_call_id", () => {
// McpArgs { name (1): "foo", tool_call_id (3): "call_x" }
const variant = Buffer.concat([stringField(1, "get_weather"), stringField(3, "call_abc123")]);
const esm = buildExecServerMessage(13, "exec-mcp", 11, variant);
const event = decodeExecServerEvent(buildAgentServerMessage(esm));
assert.deepEqual(event, {
kind: "exec_mcp",
execMsgId: 13,
execId: "exec-mcp",
toolName: "get_weather",
toolCallId: "call_abc123",
args: {},
});
});
test("decodeExecServerEvent prefers tool_name (field 5) over name (field 1)", () => {
// McpArgs { name: "old", tool_name: "new", tool_call_id: "x" }
const variant = Buffer.concat([
stringField(1, "old_alias"),
stringField(3, "call_y"),
stringField(5, "canonical_name"),
]);
const esm = buildExecServerMessage(14, "exec-mcp2", 11, variant);
const event = decodeExecServerEvent(buildAgentServerMessage(esm));
if (event?.kind !== "exec_mcp") throw new Error("expected exec_mcp");
assert.equal(event.toolName, "canonical_name");
});
// ─── Wire-tap regression: real Connect-RPC frame round-trips ───────────────
test("decodeExecServerEvent works through a Connect-RPC frame", () => {
const variant = stringField(1, "/etc/shadow");
const esm = buildExecServerMessage(99, "exec-real", 7, variant);
const asm = buildAgentServerMessage(esm);
const framed = wrapConnectFrame(asm);
const frames = [...iterateConnectFrames(framed)];
assert.equal(frames.length, 1);
const event = decodeExecServerEvent(frames[0].payload);
assert.deepEqual(event, {
kind: "exec_read",
execMsgId: 99,
execId: "exec-real",
path: "/etc/shadow",
});
});

View File

@@ -0,0 +1,42 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
humanizeCursorModelId,
parseCursorAgentModels,
} from "../../src/lib/providerModels/cursorAgent";
test("parseCursorAgentModels returns every reported id including auto and composer-*", () => {
const text =
"Cannot use this model: --help. Available models: auto, composer-2, composer-2-fast, gpt-5.3-codex-low, claude-opus-4-7-thinking-high, kimi-k2.5";
assert.deepEqual(parseCursorAgentModels(text), [
"auto",
"composer-2",
"composer-2-fast",
"gpt-5.3-codex-low",
"claude-opus-4-7-thinking-high",
"kimi-k2.5",
]);
});
test("parseCursorAgentModels deduplicates and trims", () => {
assert.deepEqual(parseCursorAgentModels("Available models: a, a , b"), ["a", "b"]);
});
test("parseCursorAgentModels returns [] when the marker is missing", () => {
assert.deepEqual(parseCursorAgentModels("nothing here"), []);
});
test("humanizeCursorModelId pretty-prints common patterns", () => {
assert.equal(humanizeCursorModelId("auto"), "Auto (Server Picks)");
assert.equal(humanizeCursorModelId("composer-2-fast"), "Composer 2 Fast");
assert.equal(humanizeCursorModelId("gpt-5.3-codex-low"), "GPT 5.3 Codex Low");
assert.equal(humanizeCursorModelId("gpt-5.5-extra-high-fast"), "GPT 5.5 Extra High Fast");
// Collapses claude-opus-4-7-* version pattern into 4.7
assert.equal(
humanizeCursorModelId("claude-opus-4-7-thinking-high"),
"Claude Opus 4.7 Thinking High"
);
assert.equal(humanizeCursorModelId("kimi-k2.5"), "Kimi K2.5");
assert.equal(humanizeCursorModelId("gemini-3.1-pro"), "Gemini 3.1 Pro");
assert.equal(humanizeCursorModelId("claude-4-sonnet-thinking"), "Claude 4 Sonnet Thinking");
});

View File

@@ -0,0 +1,490 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
resolveRequestedModel,
encodeAgentRunRequest,
buildAgentRequestBody,
iterateConnectFrames,
decodeAgentServerMessage,
flattenMessages,
wrapConnectFrame,
encodeExecReadRejected,
encodeExecWriteRejected,
encodeExecDeleteRejected,
encodeExecLsRejected,
encodeExecShellRejected,
encodeExecBackgroundShellSpawnRejected,
encodeExecGrepError,
encodeExecFetchError,
encodeExecWriteShellStdinError,
encodeExecDiagnosticsResult,
encodeExecMcpResult,
encodeExecMcpError,
encodeKvGetBlobResult,
encodeKvSetBlobResult,
encodeMcpToolDefinitionBody,
jsonSchemaToProtobufValue,
encodeRequestContextResponse,
openAIToolsToMcpDefs,
} from "../../open-sse/utils/cursorAgentProtobuf";
test("resolveRequestedModel maps cursor-agent's client-side aliases", () => {
assert.deepEqual(resolveRequestedModel("auto"), { modelId: "default", parameters: [] });
assert.deepEqual(resolveRequestedModel("composer-2-fast"), {
modelId: "composer-2",
parameters: [{ id: "fast", value: "true" }],
});
assert.deepEqual(resolveRequestedModel("claude-4.6-sonnet-medium"), {
modelId: "claude-4.6-sonnet-medium",
parameters: [],
});
assert.deepEqual(resolveRequestedModel("composer-2"), { modelId: "composer-2", parameters: [] });
});
test("encodeAgentRunRequest embeds user text and resolves the model id", () => {
const buf = encodeAgentRunRequest({
modelId: "auto",
userText: "say only PING",
conversationId: "00000000-0000-0000-0000-000000000000",
messageId: "11111111-1111-1111-1111-111111111111",
});
// Verifiable substrings: cursor-agent itself emits these on the wire.
const text = buf.toString("latin1");
assert.ok(text.includes("say only PING"), "user text present");
assert.ok(text.includes("default"), "auto rewritten to 'default'");
assert.ok(text.includes("00000000-0000-0000-0000-000000000000"), "conversation id present");
});
test("encodeAgentRunRequest emits composer parameters", () => {
const buf = encodeAgentRunRequest({
modelId: "composer-2-fast",
userText: "hi",
});
const text = buf.toString("latin1");
assert.ok(text.includes("composer-2"), "split model id present");
assert.ok(text.includes("fast"), "parameter id 'fast' present");
assert.ok(text.includes("true"), "parameter value 'true' present");
});
test("buildAgentRequestBody wraps the message in a Connect-RPC frame", () => {
const buf = buildAgentRequestBody({ modelId: "claude-4.6-sonnet-medium", userText: "hi" });
// First byte is flags (0x00 = uncompressed); next 4 bytes are big-endian length.
assert.equal(buf[0], 0x00);
const length = buf.readUInt32BE(1);
assert.equal(length, buf.length - 5);
});
test("iterateConnectFrames + decodeAgentServerMessage extract a text delta", () => {
// Synthesize a server-side delta frame: AgentServerMessage { interaction_update {
// text_delta { text = "hello" } } }
function v(n: number): Buffer {
const out: number[] = [];
while (n > 0x7f) {
out.push((n & 0x7f) | 0x80);
n >>>= 7;
}
out.push(n);
return Buffer.from(out);
}
function tag(field: number, wt: number) {
return v((field << 3) | wt);
}
function lenPrefixed(field: number, payload: Buffer) {
return Buffer.concat([tag(field, 2), v(payload.length), payload]);
}
const textDelta = Buffer.from("hello", "utf8");
const tdu = lenPrefixed(1, textDelta); // TextDeltaUpdate { text }
const iu = lenPrefixed(1, tdu); // InteractionUpdate { text_delta }
const asm = lenPrefixed(1, iu); // AgentServerMessage { interaction_update }
const framed = wrapConnectFrame(asm);
const frames = [...iterateConnectFrames(framed)];
assert.equal(frames.length, 1);
const deltas = decodeAgentServerMessage(frames[0].payload);
assert.deepEqual(deltas, [{ kind: "text", text: "hello" }]);
});
test("flattenMessages handles a simple single user message", () => {
assert.equal(flattenMessages([{ role: "user", content: "hello there" }]), "hello there");
});
test("flattenMessages prepends system prompts and labels multi-turn", () => {
const out = flattenMessages([
{ role: "system", content: "be brief" },
{ role: "user", content: "hi" },
{ role: "assistant", content: "hello" },
{ role: "user", content: "again" },
]);
assert.ok(out.startsWith("be brief"));
assert.ok(out.includes("User: hi"));
assert.ok(out.includes("Assistant: hello"));
assert.ok(out.includes("User: again"));
});
test("flattenMessages flattens content arrays", () => {
const out = flattenMessages([
{
role: "user",
content: [
{ type: "text", text: "part1" },
{ type: "text", text: "part2" },
],
},
]);
assert.equal(out, "part1\npart2");
});
// ─── Phase 1: rejection encoders ───────────────────────────────────────────
//
// All ExecClientMessage frames share a structure:
// wrapConnectFrame(
// AgentClientMessage {
// exec_client_message (2): ExecClientMessage {
// id (1): execMsgId,
// exec_id (15): execId,
// <result_field>: <result_payload>,
// }
// }
// )
//
// We unwrap via decodeFields and assert: (1) framing is valid, (2) the
// ExecClientMessage carries the right id+exec_id, (3) the result field
// number matches the variant, (4) the payload bytes contain the rejection
// reason as a substring (sufficient since Buffer.from(reason, "utf8") is
// embedded verbatim by encodeString).
function unwrapFrame(buf: Buffer): Buffer {
const frames = [...iterateConnectFrames(buf)];
assert.equal(frames.length, 1);
return frames[0].payload;
}
function assertEcmShape(
framed: Buffer,
expectedExecMsgId: number,
expectedExecId: string,
expectedResultField: number
) {
const payload = unwrapFrame(framed);
// Top-level: AgentClientMessage { exec_client_message (2): ECM }
// Just verify the fragment contains the exec id and result-field tag.
assert.ok(payload.includes(Buffer.from(expectedExecId, "utf8")), "exec_id present");
// Tag for result field with WT_LEN (2): (field<<3)|2
const resultTag = (expectedResultField << 3) | 2;
let found = false;
for (let i = 0; i < payload.length; i++) {
if (payload[i] === resultTag) {
found = true;
break;
}
}
assert.ok(found, `result tag for field ${expectedResultField} present`);
void expectedExecMsgId; // varint encoded; covered by other tests
}
test("encodeExecReadRejected wraps in read_result (field 7) with reason", () => {
const framed = encodeExecReadRejected(42, "exec-abc", "/etc/passwd", "denied");
assertEcmShape(framed, 42, "exec-abc", 7);
const payload = unwrapFrame(framed);
assert.ok(payload.includes(Buffer.from("/etc/passwd", "utf8")));
assert.ok(payload.includes(Buffer.from("denied", "utf8")));
});
test("encodeExecWriteRejected wraps in write_result (field 3)", () => {
const framed = encodeExecWriteRejected(1, "x", "/tmp/foo", "no");
assertEcmShape(framed, 1, "x", 3);
});
test("encodeExecDeleteRejected wraps in delete_result (field 4)", () => {
const framed = encodeExecDeleteRejected(1, "x", "/tmp/foo", "no");
assertEcmShape(framed, 1, "x", 4);
});
test("encodeExecLsRejected wraps in ls_result (field 8)", () => {
const framed = encodeExecLsRejected(1, "x", "/tmp", "no");
assertEcmShape(framed, 1, "x", 8);
});
test("encodeExecShellRejected carries command and working directory", () => {
const framed = encodeExecShellRejected(7, "exec-7", "rm -rf /", "/home", "denied");
assertEcmShape(framed, 7, "exec-7", 2);
const payload = unwrapFrame(framed);
assert.ok(payload.includes(Buffer.from("rm -rf /", "utf8")));
assert.ok(payload.includes(Buffer.from("/home", "utf8")));
assert.ok(payload.includes(Buffer.from("denied", "utf8")));
});
test("encodeExecBackgroundShellSpawnRejected uses field 16", () => {
const framed = encodeExecBackgroundShellSpawnRejected(1, "x", "sleep", "/", "no");
assertEcmShape(framed, 1, "x", 16);
});
test("encodeExecGrepError uses field 5 with error message", () => {
const framed = encodeExecGrepError(1, "exec-grep", "regex too complex");
assertEcmShape(framed, 1, "exec-grep", 5);
const payload = unwrapFrame(framed);
assert.ok(payload.includes(Buffer.from("regex too complex", "utf8")));
});
test("encodeExecFetchError uses field 20 with url and error", () => {
const framed = encodeExecFetchError(1, "x", "https://example.com", "timeout");
assertEcmShape(framed, 1, "x", 20);
const payload = unwrapFrame(framed);
assert.ok(payload.includes(Buffer.from("https://example.com", "utf8")));
assert.ok(payload.includes(Buffer.from("timeout", "utf8")));
});
test("encodeExecWriteShellStdinError uses field 23", () => {
const framed = encodeExecWriteShellStdinError(1, "x", "no shell");
assertEcmShape(framed, 1, "x", 23);
});
test("encodeExecDiagnosticsResult is empty success on field 9", () => {
const framed = encodeExecDiagnosticsResult(1, "exec-diag");
assertEcmShape(framed, 1, "exec-diag", 9);
});
// ─── Phase 1: MCP encoders ──────────────────────────────────────────────────
test("encodeExecMcpResult wraps text content in mcp_result (field 11)", () => {
const framed = encodeExecMcpResult(1, "exec-mcp", "tool output", false);
assertEcmShape(framed, 1, "exec-mcp", 11);
const payload = unwrapFrame(framed);
assert.ok(payload.includes(Buffer.from("tool output", "utf8")));
});
test("encodeExecMcpResult sets is_error when isError=true", () => {
const framed = encodeExecMcpResult(1, "x", "err msg", true);
// is_error field would be encoded as varint 1 — verify framing only
assertEcmShape(framed, 1, "x", 11);
});
test("encodeExecMcpError wraps error in mcp_result", () => {
const framed = encodeExecMcpError(1, "exec-err", "tool crashed");
assertEcmShape(framed, 1, "exec-err", 11);
const payload = unwrapFrame(framed);
assert.ok(payload.includes(Buffer.from("tool crashed", "utf8")));
});
// ─── Phase 1: KV blob encoders ──────────────────────────────────────────────
test("encodeKvGetBlobResult wraps in kv_client_message (field 3)", () => {
const framed = encodeKvGetBlobResult(99, Buffer.from("blob-content", "utf8"));
const payload = unwrapFrame(framed);
// Top-level field 3 (kv_client_message) tag = (3<<3)|2 = 26
assert.equal(payload[0], 26);
assert.ok(payload.includes(Buffer.from("blob-content", "utf8")));
});
test("encodeKvSetBlobResult is an empty ack", () => {
const framed = encodeKvSetBlobResult(7);
const payload = unwrapFrame(framed);
assert.equal(payload[0], 26); // (3<<3)|2 = kv_client_message
});
// ─── Phase 1: MCP tool definition body ──────────────────────────────────────
test("encodeMcpToolDefinitionBody encodes name, description, schema bytes", () => {
const body = encodeMcpToolDefinitionBody({
name: "get_weather",
description: "Look up current weather",
inputSchemaBytes: Buffer.from("schema-bytes"),
providerIdentifier: "omniroute",
toolName: "get_weather",
});
assert.ok(body.includes(Buffer.from("get_weather", "utf8")));
assert.ok(body.includes(Buffer.from("Look up current weather", "utf8")));
assert.ok(body.includes(Buffer.from("schema-bytes")));
assert.ok(body.includes(Buffer.from("omniroute", "utf8")));
});
test("encodeMcpToolDefinitionBody omits optional providerIdentifier and toolName", () => {
const body = encodeMcpToolDefinitionBody({
name: "n",
description: "d",
inputSchemaBytes: Buffer.alloc(0),
});
assert.ok(!body.includes(Buffer.from("omniroute", "utf8")));
});
// ─── Phase 1: encodeRequestContextResponse with tools ───────────────────────
test("encodeRequestContextResponse with no tools produces empty RequestContext (existing behavior)", () => {
const framed = encodeRequestContextResponse(5, "exec-rc");
assertEcmShape(framed, 5, "exec-rc", 10); // request_context_result = field 10
});
test("encodeRequestContextResponse with tools embeds tool name and schema", () => {
const framed = encodeRequestContextResponse(5, "exec-rc", [
{
name: "get_weather",
description: "lookup",
inputSchemaBytes: jsonSchemaToProtobufValue({
type: "object",
properties: { city: { type: "string" } },
}),
providerIdentifier: "omniroute",
toolName: "get_weather",
},
]);
assertEcmShape(framed, 5, "exec-rc", 10);
const payload = unwrapFrame(framed);
assert.ok(payload.includes(Buffer.from("get_weather", "utf8")));
assert.ok(payload.includes(Buffer.from("city", "utf8")));
});
// ─── Phase 1: jsonSchemaToProtobufValue ─────────────────────────────────────
//
// google.protobuf.Value is a oneof message. We verify the encoded bytes
// start with the right field tag for each kind, and (for nested types) that
// the inner content survives.
test("jsonSchemaToProtobufValue encodes a string as field 3", () => {
const buf = jsonSchemaToProtobufValue("hello");
// tag (3<<3)|2 = 26
assert.equal(buf[0], 26);
assert.ok(buf.includes(Buffer.from("hello", "utf8")));
});
test("jsonSchemaToProtobufValue encodes a number as field 2 (double)", () => {
const buf = jsonSchemaToProtobufValue(3.14);
// tag (2<<3)|1 = 17
assert.equal(buf[0], 17);
assert.equal(buf.length, 9); // 1-byte tag + 8-byte double
assert.equal(buf.readDoubleLE(1), 3.14);
});
test("jsonSchemaToProtobufValue encodes a bool as field 4", () => {
const buf = jsonSchemaToProtobufValue(true);
// tag (4<<3)|0 = 32
assert.equal(buf[0], 32);
assert.equal(buf[1], 1);
});
test("jsonSchemaToProtobufValue encodes null as field 1", () => {
const buf = jsonSchemaToProtobufValue(null);
// tag (1<<3)|0 = 8
assert.equal(buf[0], 8);
assert.equal(buf[1], 0);
});
test("jsonSchemaToProtobufValue encodes a list as field 6", () => {
const buf = jsonSchemaToProtobufValue(["a", "b"]);
// tag (6<<3)|2 = 50
assert.equal(buf[0], 50);
assert.ok(buf.includes(Buffer.from("a", "utf8")));
assert.ok(buf.includes(Buffer.from("b", "utf8")));
});
test("jsonSchemaToProtobufValue encodes a struct as field 5", () => {
const buf = jsonSchemaToProtobufValue({ key: "value" });
// tag (5<<3)|2 = 42
assert.equal(buf[0], 42);
assert.ok(buf.includes(Buffer.from("key", "utf8")));
assert.ok(buf.includes(Buffer.from("value", "utf8")));
});
test("jsonSchemaToProtobufValue encodes a nested OpenAI tool input_schema", () => {
const schema = {
type: "object",
properties: {
city: { type: "string", description: "the city" },
units: { type: "string", enum: ["c", "f"] },
},
required: ["city"],
};
const buf = jsonSchemaToProtobufValue(schema);
// Outer tag is struct (5)
assert.equal(buf[0], 42);
assert.ok(buf.includes(Buffer.from("city", "utf8")));
assert.ok(buf.includes(Buffer.from("units", "utf8")));
assert.ok(buf.includes(Buffer.from("required", "utf8")));
assert.ok(buf.includes(Buffer.from("the city", "utf8")));
});
// ─── Phase 3: tools in AgentRunRequest ─────────────────────────────────────
test("openAIToolsToMcpDefs converts OpenAI tool array to McpToolDefinition[]", () => {
const defs = openAIToolsToMcpDefs([
{
type: "function",
function: {
name: "get_weather",
description: "lookup",
parameters: { type: "object", properties: { city: { type: "string" } } },
},
},
]);
assert.equal(defs.length, 1);
assert.equal(defs[0].name, "get_weather");
assert.equal(defs[0].description, "lookup");
assert.equal(defs[0].toolName, "get_weather");
assert.equal(defs[0].providerIdentifier, "omniroute");
assert.ok(defs[0].inputSchemaBytes.length > 0);
// Schema bytes are a Struct (field 5)
assert.equal(defs[0].inputSchemaBytes[0], 42);
});
test("openAIToolsToMcpDefs supplies a default schema if parameters omitted", () => {
const defs = openAIToolsToMcpDefs([{ type: "function", function: { name: "no_params" } }]);
assert.equal(defs.length, 1);
// Default schema is { type: "object", properties: {} } — encoded as struct
assert.equal(defs[0].inputSchemaBytes[0], 42);
});
test("encodeAgentRunRequest with tools embeds tool name and schema in mcp_tools", () => {
const buf = encodeAgentRunRequest({
modelId: "claude-4.6-sonnet-medium",
userText: "what's the weather?",
tools: [
{
type: "function",
function: {
name: "get_weather",
description: "Look up current weather",
parameters: {
type: "object",
properties: { city: { type: "string" } },
required: ["city"],
},
},
},
],
});
const text = buf.toString("latin1");
assert.ok(text.includes("get_weather"), "tool name present");
assert.ok(text.includes("Look up current weather"), "tool description present");
assert.ok(text.includes("city"), "tool schema field present");
assert.ok(text.includes("omniroute"), "provider_identifier set");
});
test("encodeAgentRunRequest without tools preserves empty mcp_tools placeholder", () => {
const bufNoTools = encodeAgentRunRequest({
modelId: "auto",
userText: "hi",
});
const bufEmptyTools = encodeAgentRunRequest({
modelId: "auto",
userText: "hi",
tools: [],
});
// Both should produce essentially the same shape — neither embeds tools.
// Lengths may differ by message-id randomness; just verify neither contains
// tool-related markers.
assert.ok(!bufNoTools.toString("latin1").includes("omniroute"));
assert.ok(!bufEmptyTools.toString("latin1").includes("omniroute"));
});
test("encodeAgentRunRequest with multiple tools embeds all of them", () => {
const buf = encodeAgentRunRequest({
modelId: "auto",
userText: "test",
tools: [
{ type: "function", function: { name: "tool_a", description: "A" } },
{ type: "function", function: { name: "tool_b", description: "B" } },
],
});
const text = buf.toString("latin1");
assert.ok(text.includes("tool_a"));
assert.ok(text.includes("tool_b"));
});

View File

@@ -0,0 +1,227 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
CursorSessionManager,
type CursorSession,
} from "../../open-sse/services/cursorSessionManager";
import { flattenMessages } from "../../open-sse/utils/cursorAgentProtobuf";
// ─── Test doubles for h2 ───────────────────────────────────────────────────
//
// We don't open real h2 connections in unit tests. Sessions hold opaque
// references that the manager only ever .close()s or .write()s through
// encodeExecMcpResult. A pair of stubs is enough.
type WriteCall = { kind: "write"; data: Buffer } | { kind: "close" };
function mockReq() {
const calls: WriteCall[] = [];
return {
req: {
write: (data: Buffer) => {
calls.push({ kind: "write", data });
return true;
},
close: () => {
calls.push({ kind: "close" });
},
} as unknown as import("node:http2").ClientHttp2Stream,
calls,
};
}
function mockClient() {
const closed = { value: false };
return {
client: {
close: () => {
closed.value = true;
},
} as unknown as import("node:http2").ClientHttp2Session,
closed,
};
}
// ─── flattenMessages: Phase 6 cold-resume support ──────────────────────────
test("flattenMessages handles role:'tool' messages", () => {
const out = flattenMessages([
{ role: "user", content: "what's the weather?" },
{
role: "assistant",
content: null,
tool_calls: [
{
id: "call_xyz",
type: "function",
function: { name: "get_weather", arguments: '{"city":"Paris"}' },
},
],
},
{ role: "tool", tool_call_id: "call_xyz", content: "sunny, 22C" },
]);
assert.match(out, /User: what's the weather\?/);
assert.match(
out,
/Assistant called tool get_weather \(call_xyz\) with arguments: \{"city":"Paris"\}/
);
assert.match(out, /Tool result \(call_xyz\): sunny, 22C/);
});
test("flattenMessages handles assistant with text + tool_calls in same message", () => {
const out = flattenMessages([
{ role: "user", content: "do x" },
{
role: "assistant",
content: "Let me check.",
tool_calls: [
{
id: "c1",
type: "function",
function: { name: "check", arguments: "{}" },
},
],
},
{ role: "tool", tool_call_id: "c1", content: "result" },
]);
assert.match(out, /Assistant: Let me check\./);
assert.match(out, /Assistant called tool check \(c1\) with arguments: \{\}/);
assert.match(out, /Tool result \(c1\): result/);
});
test("flattenMessages handles parallel tool_calls", () => {
const out = flattenMessages([
{ role: "user", content: "check both" },
{
role: "assistant",
content: null,
tool_calls: [
{ id: "c1", type: "function", function: { name: "tool_a", arguments: "{}" } },
{ id: "c2", type: "function", function: { name: "tool_b", arguments: "{}" } },
],
},
{ role: "tool", tool_call_id: "c1", content: "result_a" },
{ role: "tool", tool_call_id: "c2", content: "result_b" },
]);
assert.match(out, /tool_a \(c1\)/);
assert.match(out, /tool_b \(c2\)/);
assert.match(out, /Tool result \(c1\): result_a/);
assert.match(out, /Tool result \(c2\): result_b/);
});
test("flattenMessages keeps single-user fast path unchanged when no tool_calls", () => {
const out = flattenMessages([{ role: "user", content: "hi" }]);
assert.equal(out, "hi");
});
// ─── CursorSessionManager lifecycle ────────────────────────────────────────
test("CursorSessionManager.open registers a session under conversation_id", () => {
const m = new CursorSessionManager();
const { req } = mockReq();
const { client } = mockClient();
const session = m.open("conv-1", client, req, new Map());
assert.equal(m.size(), 1);
assert.ok(m.has("conv-1"));
assert.equal(session.conversationId, "conv-1");
assert.equal(session.state, "running");
});
test("CursorSessionManager.acquire returns undefined when no session", () => {
const m = new CursorSessionManager();
assert.equal(m.acquire("nope"), undefined);
});
test("CursorSessionManager.acquire returns undefined when session is still running", () => {
const m = new CursorSessionManager();
const { req } = mockReq();
const { client } = mockClient();
m.open("conv-2", client, req, new Map());
// open() leaves state="running"; acquire requires "awaiting_tool_result"
assert.equal(m.acquire("conv-2"), undefined);
});
test("CursorSessionManager.acquire returns the session after release(awaiting_tool_result)", () => {
const m = new CursorSessionManager();
const { req } = mockReq();
const { client } = mockClient();
const opened = m.open("conv-3", client, req, new Map());
m.release(opened, "awaiting_tool_result");
const acquired = m.acquire("conv-3");
assert.equal(acquired, opened);
assert.equal(acquired?.state, "running");
});
test("CursorSessionManager.release(idle) closes the session", () => {
const m = new CursorSessionManager();
const { req, calls } = mockReq();
const { client, closed } = mockClient();
const session = m.open("conv-4", client, req, new Map());
m.release(session, "idle");
assert.equal(m.size(), 0);
assert.ok(closed.value);
assert.ok(calls.some((c) => c.kind === "close"));
});
test("CursorSessionManager.acquire evicts expired sessions", () => {
const m = new CursorSessionManager({ idleTtlMs: 10 });
const { req } = mockReq();
const { client, closed } = mockClient();
const session = m.open("conv-5", client, req, new Map());
m.release(session, "awaiting_tool_result");
// Manually backdate lastActivityTs to simulate idle.
session.lastActivityTs = Date.now() - 1000;
const acquired = m.acquire("conv-5");
assert.equal(acquired, undefined);
assert.equal(m.size(), 0);
assert.ok(closed.value);
});
test("CursorSessionManager.sendToolResult writes ExecMcpResult on the session's req", () => {
const m = new CursorSessionManager();
const { req, calls } = mockReq();
const { client } = mockClient();
const session = m.open("conv-6", client, req, new Map());
session.pendingToolCalls.set("call_x", {
execMsgId: 1,
execId: "exec-1",
toolName: "get_weather",
});
const ok = m.sendToolResult(session, "call_x", "sunny", false);
assert.equal(ok, true);
// Verify a write happened
const writes = calls.filter((c) => c.kind === "write");
assert.equal(writes.length, 1);
// The write should be a Connect-RPC frame containing "sunny" and "exec-1"
const data = (writes[0] as { kind: "write"; data: Buffer }).data;
assert.ok(data.includes(Buffer.from("sunny", "utf8")));
assert.ok(data.includes(Buffer.from("exec-1", "utf8")));
// Pending tool call was consumed
assert.equal(session.pendingToolCalls.has("call_x"), false);
});
test("CursorSessionManager.sendToolResult returns false when openAIToolCallId not pending", () => {
const m = new CursorSessionManager();
const { req } = mockReq();
const { client } = mockClient();
const session = m.open("conv-7", client, req, new Map());
const ok = m.sendToolResult(session, "unknown_id", "x", false);
assert.equal(ok, false);
});
test("CursorSessionManager.open replaces an existing session for the same conversation", () => {
const m = new CursorSessionManager();
const r1 = mockReq();
const c1 = mockClient();
const session1 = m.open("conv-8", c1.client, r1.req, new Map());
m.release(session1, "awaiting_tool_result");
const r2 = mockReq();
const c2 = mockClient();
const session2 = m.open("conv-8", c2.client, r2.req, new Map());
// First session's client should be closed
assert.ok(c1.closed.value);
// Map only has one session; the new one
assert.equal(m.size(), 1);
assert.equal(m.acquire("conv-8"), undefined); // session2 is "running"
void session2;
});

View File

@@ -0,0 +1,122 @@
import test from "node:test";
import assert from "node:assert/strict";
import crypto from "node:crypto";
import {
encodeAgentRunRequest,
decodeKvServerEvent,
} from "../../open-sse/utils/cursorAgentProtobuf";
// ─── Wire-format helpers ───────────────────────────────────────────────────
function v(n: number): Buffer {
const out: number[] = [];
while (n > 0x7f) {
out.push((n & 0x7f) | 0x80);
n >>>= 7;
}
out.push(n);
return Buffer.from(out);
}
function tag(field: number, wireType: number): Buffer {
return v((field << 3) | wireType);
}
function lenPrefixed(field: number, payload: Buffer): Buffer {
return Buffer.concat([tag(field, 2), v(payload.length), payload]);
}
function varintField(field: number, value: number): Buffer {
return Buffer.concat([tag(field, 0), v(value)]);
}
// ─── encodeAgentRunRequest with system prompt ──────────────────────────────
test("encodeAgentRunRequest with systemPrompt populates blobStore", () => {
const blobStore = new Map<string, Buffer>();
const buf = encodeAgentRunRequest({
modelId: "auto",
userText: "hi",
systemPrompt: "be brief",
blobStore,
});
// The blobStore should have one entry (the system prompt blob)
assert.equal(blobStore.size, 1);
// Key is hex of sha256({role:"system", content:"be brief"})
const expectedJson = JSON.stringify({ role: "system", content: "be brief" });
const expectedHash = crypto
.createHash("sha256")
.update(Buffer.from(expectedJson, "utf8"))
.digest("hex");
assert.ok(blobStore.has(expectedHash));
// The stored bytes should match the JSON
assert.equal(blobStore.get(expectedHash)!.toString("utf8"), expectedJson);
// The blob hash (32 bytes) should appear in the encoded request body
const hashBytes = Buffer.from(expectedHash, "hex");
assert.ok(buf.includes(hashBytes), "blob id embedded in CSS");
});
test("encodeAgentRunRequest without systemPrompt leaves blobStore empty", () => {
const blobStore = new Map<string, Buffer>();
encodeAgentRunRequest({ modelId: "auto", userText: "hi", blobStore });
assert.equal(blobStore.size, 0);
});
test("encodeAgentRunRequest with systemPrompt but no blobStore is a no-op for blobs", () => {
// The system prompt is silently dropped if blobStore isn't provided.
// This matches the existing pre-Phase-7 behavior.
const buf = encodeAgentRunRequest({
modelId: "auto",
userText: "hi",
systemPrompt: "be brief",
});
// No crash; the system prompt isn't embedded (no blob to reference).
assert.ok(buf.length > 0);
});
test("encodeAgentRunRequest with multi-message system prompt joins them", () => {
const blobStore = new Map<string, Buffer>();
encodeAgentRunRequest({
modelId: "auto",
userText: "hi",
systemPrompt: "first instruction\n\nsecond instruction",
blobStore,
});
assert.equal(blobStore.size, 1);
const [stored] = [...blobStore.values()];
const parsed = JSON.parse(stored.toString("utf8"));
assert.equal(parsed.role, "system");
assert.equal(parsed.content, "first instruction\n\nsecond instruction");
});
// ─── KV server message decoder ─────────────────────────────────────────────
test("decodeKvServerEvent recognizes get_blob_args", () => {
// KvServerMessage { id (1): 7, get_blob_args (2): { blob_id (1): bytes } }
const blobId = Buffer.from("01020304".repeat(8), "hex");
const getBlobArgs = lenPrefixed(1, blobId);
const ksm = Buffer.concat([varintField(1, 7), lenPrefixed(2, getBlobArgs)]);
const asm = lenPrefixed(4, ksm); // ASM_KV_SERVER_MESSAGE = 4
const event = decodeKvServerEvent(asm);
assert.deepEqual(event, { kind: "kv_get_blob", kvId: 7, blobId, requestMetadata: null });
});
test("decodeKvServerEvent recognizes set_blob_args with data", () => {
// SetBlobArgs { blob_id (1): bytes, blob_data (2): bytes }
const blobId = Buffer.from("aa".repeat(32), "hex");
const blobData = Buffer.from("hello world", "utf8");
const setBlobArgs = Buffer.concat([lenPrefixed(1, blobId), lenPrefixed(2, blobData)]);
const ksm = Buffer.concat([varintField(1, 5), lenPrefixed(3, setBlobArgs)]);
const asm = lenPrefixed(4, ksm);
const event = decodeKvServerEvent(asm);
if (event?.kind !== "kv_set_blob") throw new Error("expected kv_set_blob");
assert.equal(event.kvId, 5);
assert.ok(event.blobId.equals(blobId));
assert.ok(event.blobData.equals(blobData));
assert.equal(event.requestMetadata, null);
});
test("decodeKvServerEvent returns null when AgentServerMessage has no kv frame", () => {
// ASM with only an interaction_update (field 1)
const tdu = lenPrefixed(1, Buffer.from("hi"));
const iu = lenPrefixed(1, tdu);
const asm = lenPrefixed(1, iu);
assert.equal(decodeKvServerEvent(asm), null);
});

View File

@@ -0,0 +1,236 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
decodeExecServerEvent,
decodeProtobufValue,
jsonSchemaToProtobufValue,
} from "../../open-sse/utils/cursorAgentProtobuf";
import { newStreamCtx, processFrame } from "../../open-sse/executors/cursor";
// ─── Wire-format helpers ───────────────────────────────────────────────────
function v(n: number): Buffer {
const out: number[] = [];
while (n > 0x7f) {
out.push((n & 0x7f) | 0x80);
n >>>= 7;
}
out.push(n);
return Buffer.from(out);
}
function tag(field: number, wireType: number): Buffer {
return v((field << 3) | wireType);
}
function lenPrefixed(field: number, payload: Buffer): Buffer {
return Buffer.concat([tag(field, 2), v(payload.length), payload]);
}
function stringField(field: number, value: string): Buffer {
return lenPrefixed(field, Buffer.from(value, "utf8"));
}
function varintField(field: number, value: number): Buffer {
return Buffer.concat([tag(field, 0), v(value)]);
}
// Encodes a single map<string, bytes> entry: { key (1): k, value (2): valueBytes }
function mapEntry(field: number, key: string, valueBytes: Buffer): Buffer {
const entry = Buffer.concat([stringField(1, key), lenPrefixed(2, valueBytes)]);
return lenPrefixed(field, entry);
}
// AgentServerMessage { exec_server_message (2): ESM { id, exec_id, mcp_args (11): ... } }
function buildMcpArgsEvent(
execMsgId: number,
execId: string,
toolName: string,
toolCallId: string,
args: Record<string, unknown>
): Buffer {
const mcaParts: Buffer[] = [
stringField(1, toolName), // name
stringField(3, toolCallId), // tool_call_id
];
for (const [k, val] of Object.entries(args)) {
const valueBytes = jsonSchemaToProtobufValue(val);
mcaParts.push(mapEntry(2, k, valueBytes)); // args = field 2
}
const mca = Buffer.concat(mcaParts);
const esm = Buffer.concat([
varintField(1, execMsgId),
stringField(15, execId),
lenPrefixed(11, mca),
]);
return lenPrefixed(2, esm);
}
// ─── decodeProtobufValue round-trip tests ──────────────────────────────────
test("decodeProtobufValue round-trips primitives", () => {
assert.equal(decodeProtobufValue(jsonSchemaToProtobufValue("hi") as Buffer), "hi");
assert.equal(decodeProtobufValue(jsonSchemaToProtobufValue(42.5)), 42.5);
assert.equal(decodeProtobufValue(jsonSchemaToProtobufValue(true)), true);
assert.equal(decodeProtobufValue(jsonSchemaToProtobufValue(false)), false);
assert.equal(decodeProtobufValue(jsonSchemaToProtobufValue(null)), null);
});
test("decodeProtobufValue round-trips a flat object", () => {
const obj = { name: "alice", age: 30, active: true };
const encoded = jsonSchemaToProtobufValue(obj);
assert.deepEqual(decodeProtobufValue(encoded), obj);
});
test("decodeProtobufValue round-trips an array of strings", () => {
const arr = ["a", "b", "c"];
const encoded = jsonSchemaToProtobufValue(arr);
assert.deepEqual(decodeProtobufValue(encoded), arr);
});
test("decodeProtobufValue round-trips a nested object with mixed types", () => {
const obj = {
city: "Paris",
coords: { lat: 48.8566, lng: 2.3522 },
tags: ["europe", "capital"],
populated: true,
sister_city: null,
};
const encoded = jsonSchemaToProtobufValue(obj);
assert.deepEqual(decodeProtobufValue(encoded), obj);
});
test("decodeProtobufValue handles deeply nested OpenAI tool args", () => {
const args = {
location: { city: "Tokyo", country: "JP" },
units: "metric",
days: 7,
};
const encoded = jsonSchemaToProtobufValue(args);
assert.deepEqual(decodeProtobufValue(encoded), args);
});
// ─── McpArgs decoding tests ────────────────────────────────────────────────
test("decodeExecServerEvent populates args dict from McpArgs map entries", () => {
const args = { city: "Paris", units: "celsius" };
const event = decodeExecServerEvent(
buildMcpArgsEvent(1, "exec-w", "get_weather", "call_abc", args)
);
assert.equal(event?.kind, "exec_mcp");
if (event?.kind !== "exec_mcp") return;
assert.equal(event.toolName, "get_weather");
assert.equal(event.toolCallId, "call_abc");
assert.deepEqual(event.args, args);
});
test("decodeExecServerEvent handles McpArgs with empty args", () => {
const event = decodeExecServerEvent(buildMcpArgsEvent(1, "exec-x", "no_args_tool", "call_x", {}));
if (event?.kind !== "exec_mcp") throw new Error("expected exec_mcp");
assert.deepEqual(event.args, {});
});
test("decodeExecServerEvent handles McpArgs with nested object args", () => {
const args = {
location: { city: "London", country: "UK" },
days: 3,
};
const event = decodeExecServerEvent(buildMcpArgsEvent(1, "exec-n", "weather", "call_n", args));
if (event?.kind !== "exec_mcp") throw new Error("expected exec_mcp");
assert.deepEqual(event.args, args);
});
// ─── Streaming SSE tool_calls emission ─────────────────────────────────────
function parseChunk(line: string): {
delta?: {
tool_calls?: Array<{
index: number;
id?: string;
function?: { name?: string; arguments?: string };
}>;
};
finish_reason?: string | null;
} {
return JSON.parse(line.replace(/^data: /, "").trim()).choices[0];
}
test("processFrame emits OpenAI tool_calls deltas for an mcp_args event", () => {
const emitted: string[] = [];
const ctx = newStreamCtx("auto", (s) => emitted.push(s));
const payload = buildMcpArgsEvent(1, "exec-tc", "get_weather", "cursor_call_x", {
city: "Paris",
});
processFrame(payload, ctx, new Set());
// Expect: role chunk + tool_calls init chunk + tool_calls args chunk
assert.equal(emitted.length, 3);
const roleChunk = parseChunk(emitted[0]);
assert.ok(roleChunk.delta);
// Init chunk: id + name
const initChunk = parseChunk(emitted[1]);
const initCall = initChunk.delta?.tool_calls?.[0];
assert.equal(initCall?.index, 0);
assert.match(initCall?.id ?? "", /^call_/);
assert.equal(initCall?.function?.name, "get_weather");
assert.equal(initCall?.function?.arguments, "");
// Args chunk
const argsChunk = parseChunk(emitted[2]);
const argsCall = argsChunk.delta?.tool_calls?.[0];
assert.equal(argsCall?.index, 0);
assert.equal(argsCall?.function?.arguments, JSON.stringify({ city: "Paris" }));
// Side-effects on ctx
assert.equal(ctx.toolCalls.length, 1);
assert.equal(ctx.toolCalls[0].name, "get_weather");
assert.equal(ctx.emittedToolCallIndex, 1);
});
test("processFrame indexes parallel tool calls sequentially", () => {
const emitted: string[] = [];
const ctx = newStreamCtx("auto", (s) => emitted.push(s));
const acked = new Set<string>();
processFrame(buildMcpArgsEvent(1, "exec-1", "tool_a", "ca", { x: 1 }), ctx, acked);
processFrame(buildMcpArgsEvent(2, "exec-2", "tool_b", "cb", { y: 2 }), ctx, acked);
assert.equal(ctx.toolCalls.length, 2);
assert.equal(ctx.emittedToolCallIndex, 2);
// emitted[0] = role chunk
// emitted[1] = init for tool_a (index=0)
// emitted[2] = args for tool_a (index=0)
// emitted[3] = init for tool_b (index=1)
// emitted[4] = args for tool_b (index=1)
const initB = parseChunk(emitted[3]);
assert.equal(initB.delta?.tool_calls?.[0].index, 1);
assert.equal(initB.delta?.tool_calls?.[0].function?.name, "tool_b");
});
test("processFrame emits text + tool_calls in mixed order", () => {
const emitted: string[] = [];
const ctx = newStreamCtx("auto", (s) => emitted.push(s));
const acked = new Set<string>();
// Build a text_delta payload
function textDelta(text: string): Buffer {
const tdu = lenPrefixed(1, Buffer.from(text, "utf8"));
const iu = lenPrefixed(1, tdu);
return lenPrefixed(1, iu);
}
processFrame(textDelta("Let me check"), ctx, acked);
processFrame(buildMcpArgsEvent(1, "exec-mix", "lookup", "c1", {}), ctx, acked);
assert.equal(ctx.totalText, "Let me check");
assert.equal(ctx.toolCalls.length, 1);
// Order: role chunk, content chunk, tool_call init, tool_call args
assert.equal(emitted.length, 4);
const contentChunk = JSON.parse(emitted[1].replace(/^data: /, "").trim());
assert.equal(contentChunk.choices[0].delta.content, "Let me check");
const initChunk = JSON.parse(emitted[2].replace(/^data: /, "").trim());
assert.ok(initChunk.choices[0].delta.tool_calls);
});
test("processFrame doesn't emit tool_calls for the same exec_id twice", () => {
const emitted: string[] = [];
const ctx = newStreamCtx("auto", (s) => emitted.push(s));
const acked = new Set<string>();
const payload = buildMcpArgsEvent(1, "exec-dup", "tool", "cd", {});
processFrame(payload, ctx, acked);
processFrame(payload, ctx, acked);
assert.equal(ctx.toolCalls.length, 1);
});

View File

@@ -1,318 +0,0 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
buildChatRequest,
decodeMessage,
decodeField,
encodeField,
encodeMcpTool,
extractTextFromResponse,
generateCursorBody,
parseConnectRPCFrame,
wrapConnectRPCFrame,
} from "../../open-sse/utils/cursorProtobuf.ts";
const LEN = 2;
const VARINT = 0;
const TOP_LEVEL_TOOL_CALL = 1;
const TOP_LEVEL_RESPONSE = 2;
const RESPONSE_TEXT = 1;
const THINKING = 25;
const THINKING_TEXT = 1;
const TOOL_ID = 3;
const TOOL_NAME = 9;
const TOOL_RAW_ARGS = 10;
const TOOL_IS_LAST_ALT = 15;
const TOOL_MCP_PARAMS = 27;
const MCP_TOOLS_LIST = 1;
const MCP_NESTED_NAME = 1;
const MCP_NESTED_PARAMS = 3;
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();
function concatArrays(...arrays) {
const total = arrays.reduce((sum, array) => sum + array.length, 0);
const result = new Uint8Array(total);
let offset = 0;
for (const array of arrays) {
result.set(array, offset);
offset += array.length;
}
return result;
}
test("parseConnectRPCFrame round-trips compressed payloads", () => {
const payload = textEncoder.encode("cursor-frame");
const frame = wrapConnectRPCFrame(payload, true);
const parsed = parseConnectRPCFrame(frame);
assert.equal(parsed.flags, 1);
assert.equal(parsed.consumed, frame.length);
assert.equal(textDecoder.decode(parsed.payload), "cursor-frame");
});
test("parseConnectRPCFrame returns null for truncated frames", () => {
const payload = textEncoder.encode("short");
const frame = wrapConnectRPCFrame(payload, false);
assert.equal(parseConnectRPCFrame(frame.slice(0, frame.length - 1)), null);
});
test("parseConnectRPCFrame keeps the raw payload when a compressed frame is not valid gzip", () => {
const payload = textEncoder.encode("not-gzip");
const frame = new Uint8Array(5 + payload.length);
frame[0] = 0x01;
frame[1] = (payload.length >> 24) & 0xff;
frame[2] = (payload.length >> 16) & 0xff;
frame[3] = (payload.length >> 8) & 0xff;
frame[4] = payload.length & 0xff;
frame.set(payload, 5);
const parsed = parseConnectRPCFrame(frame);
assert.equal(parsed.flags, 0x01);
assert.equal(parsed.consumed, frame.length);
assert.equal(textDecoder.decode(parsed.payload), "not-gzip");
});
test("decodeField handles fixed-width wire types and past-end offsets", () => {
const buffer = Uint8Array.from([
(1 << 3) | 1,
1,
2,
3,
4,
5,
6,
7,
8,
(2 << 3) | 5,
9,
10,
11,
12,
]);
const [field1, wire1, value1, pos1] = decodeField(buffer, 0);
const [field2, wire2, value2, pos2] = decodeField(buffer, pos1);
const pastEnd = decodeField(buffer, pos2);
assert.equal(field1, 1);
assert.equal(wire1, 1);
assert.deepEqual([...value1], [1, 2, 3, 4, 5, 6, 7, 8]);
assert.equal(field2, 2);
assert.equal(wire2, 5);
assert.deepEqual([...value2], [9, 10, 11, 12]);
assert.deepEqual(pastEnd, [null, null, null, pos2]);
});
test("extractTextFromResponse reads MCP nested tool metadata and alternate last-tool flag", () => {
const toolCallPayload = encodeField(
TOP_LEVEL_TOOL_CALL,
LEN,
concatArrays(
encodeField(TOOL_ID, LEN, "call_1"),
encodeField(TOOL_NAME, LEN, "mcp_custom_placeholder"),
encodeField(TOOL_RAW_ARGS, LEN, "{}"),
encodeField(TOOL_IS_LAST_ALT, VARINT, 1),
encodeField(
TOOL_MCP_PARAMS,
LEN,
encodeField(
MCP_TOOLS_LIST,
LEN,
concatArrays(
encodeField(MCP_NESTED_NAME, LEN, "read_file"),
encodeField(MCP_NESTED_PARAMS, LEN, '{"path":"/tmp/a"}')
)
)
)
)
);
const extracted = extractTextFromResponse(toolCallPayload);
assert.equal(extracted.toolCall.id, "call_1");
assert.equal(extracted.toolCall.function.name, "read_file");
assert.equal(extracted.toolCall.function.arguments, '{"path":"/tmp/a"}');
assert.equal(extracted.toolCall.isLast, true);
});
test("extractTextFromResponse falls back to raw args when MCP metadata is absent", () => {
const toolCallPayload = encodeField(
TOP_LEVEL_TOOL_CALL,
LEN,
concatArrays(
encodeField(TOOL_ID, LEN, "call_fallback"),
encodeField(TOOL_NAME, LEN, "read_file"),
encodeField(TOOL_RAW_ARGS, LEN, '{"path":"/tmp/fallback"}')
)
);
const extracted = extractTextFromResponse(toolCallPayload);
assert.equal(extracted.toolCall.id, "call_fallback");
assert.equal(extracted.toolCall.function.name, "read_file");
assert.equal(extracted.toolCall.function.arguments, '{"path":"/tmp/fallback"}');
assert.equal(extracted.toolCall.isLast, false);
});
test("extractTextFromResponse ignores incomplete tool call payloads", () => {
const toolCallPayload = encodeField(
TOP_LEVEL_TOOL_CALL,
LEN,
encodeField(TOOL_NAME, LEN, "read_file")
);
assert.deepEqual(extractTextFromResponse(toolCallPayload), {
text: null,
error: null,
toolCall: null,
thinking: null,
});
});
test("extractTextFromResponse returns text and thinking blocks from response payloads", () => {
const responsePayload = encodeField(
TOP_LEVEL_RESPONSE,
LEN,
concatArrays(
encodeField(RESPONSE_TEXT, LEN, "hello"),
encodeField(THINKING, LEN, encodeField(THINKING_TEXT, LEN, "reasoning"))
)
);
const extracted = extractTextFromResponse(responsePayload);
assert.equal(extracted.text, "hello");
assert.equal(extracted.thinking, "reasoning");
assert.equal(extracted.toolCall, null);
});
test("generateCursorBody encodes tool metadata, message ids and high reasoning mode", () => {
const framed = generateCursorBody(
[
{ role: "user", content: "Hello" },
{
role: "assistant",
content: "",
tool_calls: [
{
id: "call_1",
type: "function",
function: {
name: "mcp__repo__read_file",
arguments: '{"path":"/tmp/a"}',
},
},
],
tool_results: [
{
tool_call_id: "call_1\nmc_model_1",
name: "mcp__repo__read_file",
index: 1,
raw_args: '{"path":"/tmp/a"}',
result: "file contents",
},
],
},
],
"cursor-small",
[
{
function: {
name: "read_file",
description: "Read a file",
parameters: { type: "object", properties: { path: { type: "string" } } },
},
},
],
"high"
);
const parsed = parseConnectRPCFrame(framed);
const topLevel = decodeMessage(parsed.payload);
const request = decodeMessage(topLevel.get(1)[0].value);
assert.equal(parsed.flags, 0);
assert.equal(request.has(29), true);
assert.equal(request.has(34), true);
assert.equal(request.has(30), true);
assert.equal(request.get(30).length >= 2, true);
assert.equal(request.get(49)[0].value, 2);
});
test("buildChatRequest normalizes mixed assistant tool payloads without duplicating matching tool-result messages", () => {
const requestFrame = buildChatRequest(
[
{ role: "user", content: "Hello" },
{
role: "assistant",
content: "",
tool_calls: [
{
id: "call_1",
type: "function",
function: {
name: "mcp__repo__read_file",
arguments: '{"path":"/tmp/a"}',
},
},
],
tool_results: [
{
tool_call_id: "call_1\nmc_model_1",
name: "mcp__repo__read_file",
index: 1,
raw_args: '{"path":"/tmp/a"}',
result: "file contents",
},
],
},
{
role: "assistant",
content: "",
tool_results: [
{
tool_call_id: "call_1\nmc_model_1",
name: "mcp__repo__read_file",
index: 1,
raw_args: '{"path":"/tmp/a"}',
result: "file contents",
},
],
},
],
"cursor-small",
[
{
name: "read_file",
description: "Read a file",
},
],
"medium"
);
const topLevel = decodeMessage(requestFrame);
const request = decodeMessage(topLevel.get(1)[0].value);
assert.equal(request.get(1).length, 3);
assert.equal(request.get(27)[0].value, 1);
assert.equal(request.get(48)[0].value, 0);
assert.equal(request.get(49)[0].value, 1);
assert.equal(request.has(29), true);
assert.equal(request.has(34), true);
});
test("encodeMcpTool keeps the custom server field even when name and schema are missing", () => {
const encoded = encodeMcpTool({ description: "" });
const decoded = decodeMessage(encoded);
assert.equal(decoded.has(1), false);
assert.equal(decoded.has(2), false);
assert.equal(decoded.has(3), false);
assert.equal(textDecoder.decode(decoded.get(4)[0].value), "custom");
});

View File

@@ -0,0 +1,276 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
newStreamCtx,
processFrame,
buildCursorUsage,
type StreamCtx,
} from "../../open-sse/executors/cursor";
// ─── Wire-format helpers (mirror the encoder's primitives) ─────────────────
function v(n: number): Buffer {
const out: number[] = [];
while (n > 0x7f) {
out.push((n & 0x7f) | 0x80);
n >>>= 7;
}
out.push(n);
return Buffer.from(out);
}
function tag(field: number, wireType: number): Buffer {
return v((field << 3) | wireType);
}
function lenPrefixed(field: number, payload: Buffer): Buffer {
return Buffer.concat([tag(field, 2), v(payload.length), payload]);
}
// AgentServerMessage { interaction_update (1): { text_delta (1): { text (1): str } } }
function buildTextDeltaPayload(text: string): Buffer {
const tdu = lenPrefixed(1, Buffer.from(text, "utf8"));
const iu = lenPrefixed(1, tdu);
return lenPrefixed(1, iu);
}
// AgentServerMessage { interaction_update (1): { turn_ended (14): {} } }
function buildTurnEndedPayload(): Buffer {
const iu = lenPrefixed(14, Buffer.alloc(0));
return lenPrefixed(1, iu);
}
// AgentServerMessage { interaction_update (1): { token_delta (8): { count (1): n } } }
function buildTokenDeltaPayload(tokens: number): Buffer {
const tokDelta = Buffer.concat([tag(1, 0), v(tokens)]);
const iu = lenPrefixed(8, tokDelta);
return lenPrefixed(1, iu);
}
// AgentServerMessage { interaction_update (1): { thinking_delta (4): { text (1): str } } }
function buildThinkingDeltaPayload(text: string): Buffer {
const tdu = lenPrefixed(1, Buffer.from(text, "utf8"));
const iu = lenPrefixed(4, tdu);
return lenPrefixed(1, iu);
}
// AgentServerMessage { kv_server_message (4): {...} } — empty body
function buildKvServerMessagePayload(): Buffer {
return lenPrefixed(4, Buffer.alloc(0));
}
// JSON error payload (Connect-RPC error envelope)
function buildJsonErrorPayload(): Buffer {
return Buffer.from(
JSON.stringify({
error: { message: "rate limited", code: "resource_exhausted" },
}),
"utf8"
);
}
// ─── Tests ─────────────────────────────────────────────────────────────────
test("newStreamCtx initializes with empty state", () => {
const ctx = newStreamCtx("auto", () => {});
assert.equal(ctx.totalText, "");
assert.equal(ctx.tokenDelta, 0);
assert.equal(ctx.endReason, null);
assert.equal(ctx.emittedRoleChunk, false);
assert.equal(ctx.midStreamError, null);
assert.equal(ctx.model, "auto");
assert.match(ctx.responseId, /^chatcmpl-cursor-/);
});
test("processFrame emits role+content chunks for text deltas", () => {
const emitted: string[] = [];
const ctx = newStreamCtx("auto", (s) => emitted.push(s));
processFrame(buildTextDeltaPayload("hello"), ctx, new Set());
assert.equal(emitted.length, 2, "role chunk then content chunk");
// First chunk: role
const first = JSON.parse(emitted[0].replace(/^data: /, "").trim());
assert.equal(first.choices[0].delta.role, "assistant");
// Second chunk: content
const second = JSON.parse(emitted[1].replace(/^data: /, "").trim());
assert.equal(second.choices[0].delta.content, "hello");
assert.equal(ctx.totalText, "hello");
assert.equal(ctx.receivedText, true);
});
test("processFrame skips role chunk on subsequent text deltas", () => {
const emitted: string[] = [];
const ctx = newStreamCtx("auto", (s) => emitted.push(s));
processFrame(buildTextDeltaPayload("hello "), ctx, new Set());
processFrame(buildTextDeltaPayload("world"), ctx, new Set());
assert.equal(emitted.length, 3, "role + 2 content chunks");
assert.equal(ctx.totalText, "hello world");
});
test("processFrame sets endReason on turn_ended", () => {
const ctx = newStreamCtx("auto", () => {});
processFrame(buildTurnEndedPayload(), ctx, new Set());
assert.equal(ctx.endReason, "turn_ended");
});
test("processFrame accumulates token_delta", () => {
const ctx = newStreamCtx("auto", () => {});
processFrame(buildTokenDeltaPayload(42), ctx, new Set());
processFrame(buildTokenDeltaPayload(13), ctx, new Set());
assert.equal(ctx.tokenDelta, 55);
});
test("processFrame sets endReason on kv_server_message after text", () => {
const ctx = newStreamCtx("auto", () => {});
processFrame(buildTextDeltaPayload("hi"), ctx, new Set());
processFrame(buildKvServerMessagePayload(), ctx, new Set());
assert.equal(ctx.endReason, "kv_after_text");
assert.equal(ctx.kvAfterTextSeen, true);
});
test("processFrame ignores kv_server_message before text (no end signal yet)", () => {
const ctx = newStreamCtx("auto", () => {});
processFrame(buildKvServerMessagePayload(), ctx, new Set());
assert.equal(ctx.endReason, null);
assert.equal(ctx.kvAfterTextSeen, false);
});
test("processFrame captures mid-stream JSON error", () => {
const ctx = newStreamCtx("auto", () => {});
processFrame(buildJsonErrorPayload(), ctx, new Set());
assert.equal(ctx.endReason, "server_end");
assert.ok(ctx.midStreamError);
assert.match(ctx.midStreamError!.message, /rate limited/);
});
test("processFrame JSON error after text terminates without overwriting content", () => {
const ctx = newStreamCtx("auto", () => {});
processFrame(buildTextDeltaPayload("partial"), ctx, new Set());
processFrame(buildJsonErrorPayload(), ctx, new Set());
assert.equal(ctx.endReason, "server_end");
assert.equal(ctx.midStreamError, null, "no error overlay when text already streamed");
assert.equal(ctx.totalText, "partial");
});
test("processFrame doesn't ack same exec_id twice", () => {
// Simulate request_context appearing twice — only the first should ack.
// Phase 6 dedup is keyed by kind+execId+execMsgId so request_context and
// mcp_args sharing an empty execId don't collide.
const ctx = newStreamCtx("auto", () => {});
const acked = new Set<string>();
// Build an exec_request_context payload:
// ASM { exec_server_message (2): ESM { id (1): 1, exec_id (15): "x", request_context_args (10): {} } }
function buildRequestContext(execId: string): Buffer {
const esm = Buffer.concat([
Buffer.concat([tag(1, 0), v(1)]),
lenPrefixed(15, Buffer.from(execId, "utf8")),
lenPrefixed(10, Buffer.alloc(0)),
]);
return lenPrefixed(2, esm);
}
processFrame(buildRequestContext("x"), ctx, acked);
assert.ok(acked.has("exec_request_context:x:1"));
// Second call doesn't error or change state
processFrame(buildRequestContext("x"), ctx, acked);
assert.equal(acked.size, 1);
});
test("StreamCtx custom emit is invoked with full SSE-formatted strings", () => {
const captured: string[] = [];
const ctx = newStreamCtx("auto", (s) => captured.push(s));
processFrame(buildTextDeltaPayload("ok"), ctx, new Set());
// Each emit ends with \n\n and starts with "data: "
for (const s of captured) {
assert.match(s, /^data: /);
assert.ok(s.endsWith("\n\n"));
}
});
// ─── Thinking-delta capture and reasoning emission ─────────────────────────
test("processFrame accumulates thinking_delta into thinkingText and emits reasoning_content", () => {
const emitted: string[] = [];
const ctx = newStreamCtx("auto", (s) => emitted.push(s));
processFrame(buildThinkingDeltaPayload("step 1: "), ctx, new Set());
processFrame(buildThinkingDeltaPayload("compute"), ctx, new Set());
assert.equal(ctx.thinkingText, "step 1: compute");
assert.equal(ctx.receivedText, true);
// role chunk + 2 reasoning_content chunks
assert.equal(emitted.length, 3);
const chunks = emitted.map((s) => JSON.parse(s.replace(/^data: /, "").trim()));
assert.equal(chunks[0].choices[0].delta.role, "assistant");
assert.equal(chunks[1].choices[0].delta.reasoning_content, "step 1: ");
assert.equal(chunks[2].choices[0].delta.reasoning_content, "compute");
});
test("processFrame on empty thinking text is a no-op (no role chunk, no emit)", () => {
const emitted: string[] = [];
const ctx = newStreamCtx("auto", (s) => emitted.push(s));
// Build an empty-text thinking delta: thinking_delta(4) wraps an empty TDU
const iu = lenPrefixed(4, lenPrefixed(1, Buffer.from("", "utf8")));
const payload = lenPrefixed(1, iu);
processFrame(payload, ctx, new Set());
assert.equal(ctx.thinkingText, "");
assert.equal(ctx.emittedRoleChunk, false);
assert.equal(emitted.length, 0);
});
test("newStreamCtx initializes thinkingText to empty", () => {
const ctx = newStreamCtx("auto", () => {});
assert.equal(ctx.thinkingText, "");
});
// ─── Usage construction (buildCursorUsage) ─────────────────────────────────
const SAMPLE_BODY = { messages: [{ role: "user" as const, content: "hello world" }] };
test("buildCursorUsage uses cursor's real tokenDelta for completion_tokens", () => {
const ctx = newStreamCtx("auto", () => {});
processFrame(buildTextDeltaPayload("answer"), ctx, new Set());
processFrame(buildTokenDeltaPayload(42), ctx, new Set());
processFrame(buildTokenDeltaPayload(13), ctx, new Set());
const usage = buildCursorUsage(ctx, SAMPLE_BODY) as Record<string, number>;
// 55 tokens + buffer added by addBufferToUsage (currently +5 per util convention)
assert.ok(usage.completion_tokens >= 55, `expected ≥55, got ${usage.completion_tokens}`);
assert.ok(usage.prompt_tokens > 0);
assert.equal(usage.total_tokens, usage.prompt_tokens + usage.completion_tokens);
});
test("buildCursorUsage falls back to estimate when tokenDelta is zero", () => {
const ctx = newStreamCtx("auto", () => {});
processFrame(buildTextDeltaPayload("abc"), ctx, new Set());
// No token_delta frames sent
const usage = buildCursorUsage(ctx, SAMPLE_BODY) as Record<string, number>;
assert.ok(usage.completion_tokens > 0);
assert.equal(usage.total_tokens, usage.prompt_tokens + usage.completion_tokens);
assert.equal((usage as Record<string, unknown>).estimated, true);
});
test("buildCursorUsage emits completion_tokens_details.reasoning_tokens when thinking seen", () => {
const ctx = newStreamCtx("auto", () => {});
processFrame(buildThinkingDeltaPayload("let me think about this carefully"), ctx, new Set());
processFrame(buildTextDeltaPayload("done"), ctx, new Set());
processFrame(buildTokenDeltaPayload(20), ctx, new Set());
const usage = buildCursorUsage(ctx, SAMPLE_BODY) as Record<string, unknown>;
const details = usage.completion_tokens_details as Record<string, number> | undefined;
assert.ok(details, "completion_tokens_details should be present");
assert.ok(details!.reasoning_tokens > 0, "reasoning_tokens should be >0");
});
test("buildCursorUsage omits completion_tokens_details when no thinking", () => {
const ctx = newStreamCtx("auto", () => {});
processFrame(buildTextDeltaPayload("just text"), ctx, new Set());
processFrame(buildTokenDeltaPayload(10), ctx, new Set());
const usage = buildCursorUsage(ctx, SAMPLE_BODY) as Record<string, unknown>;
assert.equal(usage.completion_tokens_details, undefined);
});
test("buildCursorUsage never emits cache fields (cursor doesn't expose them)", () => {
const ctx = newStreamCtx("auto", () => {});
processFrame(buildThinkingDeltaPayload("thinking"), ctx, new Set());
processFrame(buildTextDeltaPayload("text"), ctx, new Set());
processFrame(buildTokenDeltaPayload(20), ctx, new Set());
const usage = buildCursorUsage(ctx, SAMPLE_BODY) as Record<string, unknown>;
assert.equal(usage.cached_tokens, undefined);
assert.equal(usage.cache_read_input_tokens, undefined);
assert.equal(usage.cache_creation_input_tokens, undefined);
});

View File

@@ -1,549 +0,0 @@
import test from "node:test";
import assert from "node:assert/strict";
import { CursorExecutor } from "../../open-sse/executors/cursor.ts";
import {
decodeMessage,
encodeField,
parseConnectRPCFrame,
wrapConnectRPCFrame,
} from "../../open-sse/utils/cursorProtobuf.ts";
import {
buildCursorHeaders,
generateCursorChecksum,
generateHashed64Hex,
generateSessionId,
} from "../../open-sse/utils/cursorChecksum.ts";
import {
getCursorVersion,
resetCursorVersionCache,
} from "../../open-sse/utils/cursorVersionDetector.ts";
const LEN = 2;
const VARINT = 0;
const TOP_LEVEL_TOOL_CALL = 1;
const TOP_LEVEL_RESPONSE = 2;
const RESPONSE_TEXT = 1;
const TOOL_ID = 3;
const TOOL_NAME = 9;
const TOOL_RAW_ARGS = 10;
const TOOL_IS_LAST = 11;
function concatArrays(...arrays) {
const total = arrays.reduce((sum, array) => sum + array.length, 0);
const result = new Uint8Array(total);
let offset = 0;
for (const array of arrays) {
result.set(array, offset);
offset += array.length;
}
return result;
}
function buildTextFrame(text) {
return Buffer.from(
wrapConnectRPCFrame(
encodeField(TOP_LEVEL_RESPONSE, LEN, encodeField(RESPONSE_TEXT, LEN, text)),
false
)
);
}
function buildCompressedTextFrame(text) {
return Buffer.from(
wrapConnectRPCFrame(
encodeField(TOP_LEVEL_RESPONSE, LEN, encodeField(RESPONSE_TEXT, LEN, text)),
true
)
);
}
function buildToolCallFrame({ id, name, args, isLast }) {
return Buffer.from(
wrapConnectRPCFrame(
encodeField(
TOP_LEVEL_TOOL_CALL,
LEN,
concatArrays(
encodeField(TOOL_ID, LEN, id),
encodeField(TOOL_NAME, LEN, name),
encodeField(TOOL_RAW_ARGS, LEN, args),
encodeField(TOOL_IS_LAST, VARINT, isLast ? 1 : 0)
)
),
false
)
);
}
function buildJsonErrorFrame(error) {
return Buffer.from(wrapConnectRPCFrame(new TextEncoder().encode(JSON.stringify(error)), false));
}
test("CursorExecutor.buildUrl uses the configured Cursor endpoint", () => {
const executor = new CursorExecutor();
assert.equal(
executor.buildUrl(),
"https://api2.cursor.sh/aiserver.v1.ChatService/StreamUnifiedChatWithTools"
);
});
test("CursorExecutor.buildHeaders strips token prefixes and derives checksum/session headers", () => {
const executor = new CursorExecutor();
const originalDateNow = Date.now;
const originalDbPath = process.env.CURSOR_STATE_DB_PATH;
// Force fallback by pointing to a non-existent DB path
process.env.CURSOR_STATE_DB_PATH = "/nonexistent/cursor/state.vscdb";
resetCursorVersionCache();
Date.now = () => 1_700_000_000_000;
try {
const headers = executor.buildHeaders({
accessToken: "prefix::real-token",
providerSpecificData: { machineId: "machine-1", ghostMode: false },
});
const expectedVersion = getCursorVersion();
assert.equal(headers.authorization, "Bearer real-token");
assert.equal(headers["x-client-key"], generateHashed64Hex("real-token"));
assert.equal(headers["x-session-id"], generateSessionId("real-token"));
assert.equal(headers["x-cursor-checksum"], generateCursorChecksum("machine-1"));
assert.equal(headers["x-cursor-client-version"], expectedVersion);
assert.equal(headers["x-cursor-user-agent"], `Cursor/${expectedVersion}`);
assert.equal(headers["user-agent"], `Cursor/${expectedVersion}`);
assert.equal(headers["x-ghost-mode"], "false");
assert.equal(headers["connect-protocol-version"], "1");
assert.match(headers["x-amzn-trace-id"], /^Root=/);
assert.ok(headers["x-request-id"]);
} finally {
Date.now = originalDateNow;
if (originalDbPath === undefined) {
delete process.env.CURSOR_STATE_DB_PATH;
} else {
process.env.CURSOR_STATE_DB_PATH = originalDbPath;
}
resetCursorVersionCache();
}
});
test("buildCursorHeaders utility stays aligned with Cursor Composer 2 versioned headers", () => {
const originalDbPath = process.env.CURSOR_STATE_DB_PATH;
process.env.CURSOR_STATE_DB_PATH = "/nonexistent/cursor/state.vscdb";
resetCursorVersionCache();
try {
const headers = buildCursorHeaders("prefix::real-token", "machine-1", false);
const expectedVersion = getCursorVersion();
assert.equal(headers.Authorization, "Bearer real-token");
assert.equal(headers["x-cursor-client-version"], expectedVersion);
assert.equal(headers["x-cursor-user-agent"], `Cursor/${expectedVersion}`);
assert.equal(headers["User-Agent"], `Cursor/${expectedVersion}`);
assert.equal(headers["x-ghost-mode"], "false");
} finally {
if (originalDbPath === undefined) {
delete process.env.CURSOR_STATE_DB_PATH;
} else {
process.env.CURSOR_STATE_DB_PATH = originalDbPath;
}
resetCursorVersionCache();
}
});
test("CursorExecutor.buildHeaders derives machineId when not provided", () => {
const executor = new CursorExecutor();
const headers = executor.buildHeaders({ accessToken: "real-token", providerSpecificData: {} });
assert.ok(headers["x-cursor-checksum"], "should have a checksum header");
assert.ok(headers["x-client-key"], "should have a client key header");
});
test("CursorExecutor.transformRequest produces a framed protobuf payload", () => {
const executor = new CursorExecutor();
const transformed = executor.transformRequest(
"claude-3.5-sonnet",
{ messages: [{ role: "user", content: "Hello" }], tools: [] },
true,
{}
);
const frame = parseConnectRPCFrame(transformed);
const fields = decodeMessage(frame.payload);
assert.ok(transformed instanceof Uint8Array);
assert.equal(frame.flags, 0);
assert.equal(frame.consumed, transformed.length);
assert.equal(fields.has(1), true);
});
test("CursorExecutor.transformProtobufToJSON aggregates text and split tool call arguments", async () => {
const executor = new CursorExecutor();
const body = { messages: [{ role: "user", content: "hi" }] };
const buffer = Buffer.concat([
buildTextFrame("Hello "),
buildToolCallFrame({
id: "call_1",
name: "read_file",
args: '{"path":',
isLast: false,
}),
buildToolCallFrame({
id: "call_1",
name: "read_file",
args: '"/tmp/a"}',
isLast: true,
}),
]);
const response = executor.transformProtobufToJSON(buffer, "cursor-small", body);
const payload = (await response.json()) as any;
assert.equal(response.status, 200);
assert.equal(payload.object, "chat.completion");
assert.equal(payload.model, "cursor-small");
assert.equal(payload.choices[0].message.content, "Hello ");
assert.equal(payload.choices[0].finish_reason, "tool_calls");
assert.equal(payload.choices[0].message.tool_calls[0].function.name, "read_file");
assert.equal(payload.choices[0].message.tool_calls[0].function.arguments, '{"path":"/tmp/a"}');
assert.equal(payload.usage.estimated, true);
});
test("CursorExecutor.transformProtobufToJSON finalizes incomplete tool calls when the stream ends early", async () => {
const executor = new CursorExecutor();
const response = executor.transformProtobufToJSON(
Buffer.concat([
buildToolCallFrame({
id: "call_2",
name: "list_files",
args: '{"path":"/tmp"}',
isLast: false,
}),
]),
"cursor-small",
{ messages: [{ role: "user", content: "hi" }] }
);
const payload = (await response.json()) as any;
assert.equal(payload.choices[0].finish_reason, "tool_calls");
assert.equal(payload.choices[0].message.tool_calls[0].id, "call_2");
assert.equal(payload.choices[0].message.tool_calls[0].function.name, "list_files");
});
test("CursorExecutor.transformProtobufToJSON keeps prior content when an error frame arrives after output", async () => {
const executor = new CursorExecutor();
const response = executor.transformProtobufToJSON(
Buffer.concat([
buildTextFrame("Partial answer"),
buildJsonErrorFrame({
error: {
code: "resource_exhausted",
message: "late error",
},
}),
]),
"cursor-small",
{ messages: [{ role: "user", content: "hi" }] }
);
const payload = (await response.json()) as any;
assert.equal(response.status, 200);
assert.equal(payload.choices[0].message.content, "Partial answer");
assert.equal(payload.choices[0].finish_reason, "stop");
});
test("CursorExecutor.transformProtobufToJSON decompresses gzip frames", async () => {
const executor = new CursorExecutor();
const response = executor.transformProtobufToJSON(
Buffer.concat([buildCompressedTextFrame("Compressed answer")]),
"cursor-small",
{ messages: [{ role: "user", content: "hi" }] }
);
const payload = (await response.json()) as any;
assert.equal(payload.choices[0].message.content, "Compressed answer");
});
test("CursorExecutor.transformProtobufToSSE emits assistant chunks, tool deltas and DONE marker", async () => {
const executor = new CursorExecutor();
const body = { messages: [{ role: "user", content: "hi" }] };
const buffer = Buffer.concat([
buildTextFrame("Hello "),
buildToolCallFrame({
id: "call_1",
name: "read_file",
args: '{"path":',
isLast: false,
}),
buildToolCallFrame({
id: "call_1",
name: "read_file",
args: '"/tmp/a"}',
isLast: true,
}),
]);
const response = executor.transformProtobufToSSE(buffer, "cursor-small", body);
const text = await response.text();
assert.equal(response.status, 200);
assert.equal(response.headers.get("Content-Type"), "text/event-stream");
assert.match(text, /"role":"assistant","content":"Hello "/);
assert.match(text, /"tool_calls":\[/);
assert.match(text, /"name":"read_file"/);
assert.match(text, /"finish_reason":"tool_calls"/);
assert.match(text, /\[DONE\]/);
});
test("CursorExecutor.transformProtobufToSSE finalizes unterminated tool calls at stream end", async () => {
const executor = new CursorExecutor();
const response = executor.transformProtobufToSSE(
Buffer.concat([
buildToolCallFrame({
id: "call_2",
name: "read_file",
args: '{"path":"/tmp/b"}',
isLast: false,
}),
]),
"cursor-small",
{ messages: [{ role: "user", content: "hi" }] }
);
const text = await response.text();
assert.match(text, /"name":"read_file"/);
assert.match(text, /"finish_reason":"tool_calls"/);
assert.match(text, /\[DONE\]/);
});
test("CursorExecutor.transformProtobufToSSE returns a JSON error before any content is streamed", async () => {
const executor = new CursorExecutor();
const response = executor.transformProtobufToSSE(
buildJsonErrorFrame({
error: {
code: "resource_exhausted",
message: "too many requests",
details: [{ debug: { error: "LIMIT", details: { title: "Limit hit" } } }],
},
}),
"cursor-small",
{ messages: [{ role: "user", content: "hi" }] }
);
const payload = (await response.json()) as any;
assert.equal(response.status, 429);
assert.equal(payload.error.type, "rate_limit_error");
assert.equal(payload.error.message, "Limit hit");
assert.equal(payload.error.code, "LIMIT");
});
test("CursorExecutor.transformProtobufToSSE stops gracefully when a JSON error arrives after content", async () => {
const executor = new CursorExecutor();
const response = executor.transformProtobufToSSE(
Buffer.concat([
buildTextFrame("Partial Cursor answer"),
buildJsonErrorFrame({
error: {
code: "resource_exhausted",
message: "late limit",
},
}),
]),
"cursor-small",
{ messages: [{ role: "user", content: "hi" }] }
);
const text = await response.text();
assert.equal(response.status, 200);
assert.match(text, /Partial Cursor answer/);
assert.match(text, /"finish_reason":"stop"/);
assert.match(text, /\[DONE\]/);
});
test("CursorExecutor.transformProtobufToSSE emits plain content deltas after tool call chunks", async () => {
const executor = new CursorExecutor();
const response = executor.transformProtobufToSSE(
Buffer.concat([
buildToolCallFrame({
id: "call_3",
name: "read_file",
args: '{"path":"/tmp/c"}',
isLast: false,
}),
buildTextFrame("Follow-up text"),
]),
"cursor-small",
{ messages: [{ role: "user", content: "hi" }] }
);
const text = await response.text();
assert.match(text, /"name":"read_file"/);
assert.match(text, /"delta":\{"content":"Follow-up text"\}/);
assert.match(text, /"finish_reason":"tool_calls"/);
});
test("CursorExecutor.transformProtobufToSSE emits an empty assistant envelope for empty responses", async () => {
const executor = new CursorExecutor();
const response = executor.transformProtobufToSSE(Buffer.alloc(0), "cursor-small", {
messages: [{ role: "user", content: "hi" }],
});
const text = await response.text();
assert.match(text, /"role":"assistant","content":""/);
assert.match(text, /"finish_reason":"stop"/);
assert.match(text, /\[DONE\]/);
});
test("CursorExecutor.transformProtobufToSSE converts JSON error frames into rate-limit responses", async () => {
const executor = new CursorExecutor();
const response = executor.transformProtobufToSSE(
buildJsonErrorFrame({
error: {
code: "resource_exhausted",
message: "rate limited",
details: [{ debug: { error: "LIMIT", details: { title: "Limit", detail: "Slow down" } } }],
},
}),
"cursor-small",
{ messages: [{ role: "user", content: "hi" }] }
);
const payload = (await response.json()) as any;
assert.equal(response.status, 429);
assert.equal(payload.error.type, "rate_limit_error");
assert.equal(payload.error.message, "Limit");
assert.equal(payload.error.code, "LIMIT");
});
test("CursorExecutor.execute returns transformed JSON for non-stream responses", async () => {
const executor = new CursorExecutor();
const body = { messages: [{ role: "user", content: "hi" }] };
const responseBuffer = Buffer.concat([buildTextFrame("Hello from Cursor")]);
executor.makeHttp2Request = async () => ({
status: 200,
headers: {},
body: responseBuffer,
});
executor.makeFetchRequest = executor.makeHttp2Request;
const result = await executor.execute({
model: "cursor-small",
body,
stream: false,
credentials: {
accessToken: "token",
providerSpecificData: { machineId: "machine-1" },
},
});
const payload = (await result.response.json()) as any;
assert.equal(
result.url,
"https://api2.cursor.sh/aiserver.v1.ChatService/StreamUnifiedChatWithTools"
);
assert.equal(result.transformedBody, body);
assert.equal(result.headers.authorization, "Bearer token");
assert.equal(payload.object, "chat.completion");
assert.equal(payload.choices[0].message.content, "Hello from Cursor");
assert.equal(payload.choices[0].finish_reason, "stop");
});
test("CursorExecutor.execute returns transformed SSE for stream responses", async () => {
const executor = new CursorExecutor();
const body = { messages: [{ role: "user", content: "hi" }] };
const responseBuffer = Buffer.concat([buildTextFrame("Hello stream")]);
executor.makeHttp2Request = async () => ({
status: 200,
headers: {},
body: responseBuffer,
});
executor.makeFetchRequest = executor.makeHttp2Request;
const result = await executor.execute({
model: "cursor-small",
body,
stream: true,
credentials: {
accessToken: "token",
providerSpecificData: { machineId: "machine-1" },
},
});
const text = await result.response.text();
assert.equal(result.response.status, 200);
assert.match(text, /"content":"Hello stream"/);
assert.match(text, /\[DONE\]/);
});
test("CursorExecutor.execute maps non-200 upstream responses to OpenAI-style errors", async () => {
const executor = new CursorExecutor();
const body = { messages: [{ role: "user", content: "hi" }] };
executor.makeHttp2Request = async () => ({
status: 403,
headers: {},
body: Buffer.from("denied"),
});
executor.makeFetchRequest = executor.makeHttp2Request;
const result = await executor.execute({
model: "cursor-small",
body,
stream: false,
credentials: {
accessToken: "token",
providerSpecificData: { machineId: "machine-1" },
},
});
const payload = (await result.response.json()) as any;
assert.equal(result.response.status, 403);
assert.equal(payload.error.type, "invalid_request_error");
assert.match(payload.error.message, /\[403\]: denied/);
});
test("CursorExecutor.execute maps transport failures to connection_error and refreshCredentials returns null", async () => {
const executor = new CursorExecutor();
executor.makeHttp2Request = async () => {
throw new Error("socket hang up");
};
executor.makeFetchRequest = executor.makeHttp2Request;
const result = await executor.execute({
model: "cursor-small",
body: { messages: [{ role: "user", content: "hi" }] },
stream: false,
credentials: {
accessToken: "token",
providerSpecificData: { machineId: "machine-1" },
},
});
const payload = (await result.response.json()) as any;
assert.equal(result.response.status, 500);
assert.equal(payload.error.type, "connection_error");
assert.equal(payload.error.message, "socket hang up");
assert.equal(await executor.refreshCredentials(), null);
});
test("CursorExecutor.transformProtobufToSSE finalizes un-terminated tools when stream abruptly cuts before isLast", async () => {
const executor = new CursorExecutor();
// Send a tool call but never close it
const response = executor.transformProtobufToSSE(
Buffer.concat([
buildToolCallFrame({
id: "call_abrupt",
name: "write_file",
args: '{"content":"partial"',
isLast: false,
}),
]),
"cursor-small",
{ messages: [{ role: "user", content: "hi" }] }
);
const text = await response.text();
assert.match(text, /"name":"write_file"/);
assert.match(text, /"finish_reason":"tool_calls"/);
assert.match(text, /\[DONE\]/);
});