fix(cursor): address code review edge cases in protobuf and executor

CRITICAL FIX:
- CursorCredentials interface now matches types.ts (machineId, ghostMode as top-level)
- Fixes runtime error when cursor-auth saves credentials and cursor-executor reads them

HIGH:
- Replace 18+ non-null assertions with guard clauses across executor and decoder
- Prefix unused params in translator (_model, _stream, _credentials)
- HTTP/2 client closes on connection error to prevent leak
- AbortSignal listener leak documented with TODO (inline arrow prevents cleanup)

MEDIUM:
- Export concatArrays from encoder, remove duplicate from protobuf.ts
- Varint decoder now enforces 5-byte max to prevent overflow
- Buffer slice bounds check prevents out-of-range read
- Empty messages array validation with explicit error
- Buffered streaming limitation documented with TODO comment

All edge cases from code review now addressed.
This commit is contained in:
Tam Nhu Tran
2026-02-11 19:13:58 +07:00
parent 9daf9430bb
commit cc5a9039e4
6 changed files with 1472 additions and 1532 deletions
File diff suppressed because it is too large Load Diff
+229 -217
View File
@@ -3,34 +3,28 @@
* Implements ConnectRPC protobuf wire format decoding * Implements ConnectRPC protobuf wire format decoding
*/ */
import * as zlib from "zlib"; import * as zlib from 'zlib';
import { import { WIRE_TYPE, FIELD, type WireType } from './cursor-protobuf-schema.js';
WIRE_TYPE,
FIELD,
type WireType,
} from "./cursor-protobuf-schema.js";
/** /**
* Decode a varint from buffer * Decode a varint from buffer
* Returns [value, newOffset] * Returns [value, newOffset]
*/ */
export function decodeVarint( export function decodeVarint(buffer: Uint8Array, offset: number): [number, number] {
buffer: Uint8Array, let result = 0;
offset: number let shift = 0;
): [number, number] { let pos = offset;
let result = 0; const maxBytes = 5;
let shift = 0;
let pos = offset;
while (pos < buffer.length) { while (pos < buffer.length && pos - offset < maxBytes) {
const b = buffer[pos]; const b = buffer[pos];
result |= (b & 0x7f) << shift; result |= (b & 0x7f) << shift;
pos++; pos++;
if (!(b & 0x80)) break; if (!(b & 0x80)) break;
shift += 7; shift += 7;
} }
return [result, pos]; return [result, pos];
} }
/** /**
@@ -38,63 +32,66 @@ export function decodeVarint(
* Returns [fieldNum, wireType, value, newOffset] * Returns [fieldNum, wireType, value, newOffset]
*/ */
export function decodeField( export function decodeField(
buffer: Uint8Array, buffer: Uint8Array,
offset: number offset: number
): [number | null, WireType | null, Uint8Array | number | null, number] { ): [number | null, WireType | null, Uint8Array | number | null, number] {
if (offset >= buffer.length) { if (offset >= buffer.length) {
return [null, null, null, offset]; return [null, null, null, offset];
} }
const [tag, pos1] = decodeVarint(buffer, offset); const [tag, pos1] = decodeVarint(buffer, offset);
const fieldNum = tag >> 3; const fieldNum = tag >> 3;
const wireType = (tag & 0x07) as WireType; const wireType = (tag & 0x07) as WireType;
let value: Uint8Array | number | null; let value: Uint8Array | number | null;
let pos = pos1; let pos = pos1;
if (wireType === WIRE_TYPE.VARINT) { if (wireType === WIRE_TYPE.VARINT) {
[value, pos] = decodeVarint(buffer, pos); [value, pos] = decodeVarint(buffer, pos);
} else if (wireType === WIRE_TYPE.LEN) { } else if (wireType === WIRE_TYPE.LEN) {
const [length, pos2] = decodeVarint(buffer, pos); const [length, pos2] = decodeVarint(buffer, pos);
value = buffer.slice(pos2, pos2 + length); if (pos2 + length > buffer.length) {
pos = pos2 + length; return [null, null, null, buffer.length];
} else if (wireType === WIRE_TYPE.FIXED64) { }
value = buffer.slice(pos, pos + 8); value = buffer.slice(pos2, pos2 + length);
pos += 8; pos = pos2 + length;
} else if (wireType === WIRE_TYPE.FIXED32) { } else if (wireType === WIRE_TYPE.FIXED64) {
value = buffer.slice(pos, pos + 4); value = buffer.slice(pos, pos + 8);
pos += 4; pos += 8;
} else { } else if (wireType === WIRE_TYPE.FIXED32) {
value = null; value = buffer.slice(pos, pos + 4);
} pos += 4;
} else {
value = null;
}
return [fieldNum, wireType, value, pos]; return [fieldNum, wireType, value, pos];
} }
/** /**
* Decode a protobuf message into a map of fields * Decode a protobuf message into a map of fields
*/ */
export function decodeMessage( export function decodeMessage(
data: Uint8Array data: Uint8Array
): Map<number, Array<{ wireType: WireType; value: Uint8Array | number }>> { ): Map<number, Array<{ wireType: WireType; value: Uint8Array | number }>> {
const fields = new Map< const fields = new Map<number, Array<{ wireType: WireType; value: Uint8Array | number }>>();
number, let pos = 0;
Array<{ wireType: WireType; value: Uint8Array | number }>
>();
let pos = 0;
while (pos < data.length) { while (pos < data.length) {
const [fieldNum, wireType, value, newPos] = decodeField(data, pos); const [fieldNum, wireType, value, newPos] = decodeField(data, pos);
if (fieldNum === null || wireType === null || value === null) break; if (fieldNum === null || wireType === null || value === null) break;
if (!fields.has(fieldNum)) { if (!fields.has(fieldNum)) {
fields.set(fieldNum, []); fields.set(fieldNum, []);
} }
fields.get(fieldNum)!.push({ wireType, value: value as Uint8Array | number }); const fieldArray = fields.get(fieldNum);
pos = newPos; if (fieldArray) {
} fieldArray.push({ wireType, value: value as Uint8Array | number });
}
pos = newPos;
}
return fields; return fields;
} }
/** /**
@@ -102,200 +99,215 @@ export function decodeMessage(
* Returns frame data or null if incomplete * Returns frame data or null if incomplete
*/ */
export function parseConnectRPCFrame(buffer: Buffer): { export function parseConnectRPCFrame(buffer: Buffer): {
flags: number; flags: number;
length: number; length: number;
payload: Uint8Array; payload: Uint8Array;
consumed: number; consumed: number;
} | null { } | null {
if (buffer.length < 5) return null; if (buffer.length < 5) return null;
const flags = buffer[0]; const flags = buffer[0];
const length = const length = (buffer[1] << 24) | (buffer[2] << 16) | (buffer[3] << 8) | buffer[4];
(buffer[1] << 24) | (buffer[2] << 16) | (buffer[3] << 8) | buffer[4];
if (buffer.length < 5 + length) return null; if (buffer.length < 5 + length) return null;
let payload = buffer.slice(5, 5 + length); let payload = buffer.slice(5, 5 + length);
// Decompress if gzip // Decompress if gzip
if (flags === 0x01 || flags === 0x02 || flags === 0x03) { if (flags === 0x01 || flags === 0x02 || flags === 0x03) {
try { try {
payload = Buffer.from(zlib.gunzipSync(payload)); payload = Buffer.from(zlib.gunzipSync(payload));
} catch { } catch {
// Decompression failed, use raw payload // Decompression failed, use raw payload
} }
} }
return { return {
flags, flags,
length, length,
payload: new Uint8Array(payload), payload: new Uint8Array(payload),
consumed: 5 + length, consumed: 5 + length,
}; };
} }
/** /**
* Extract tool call from protobuf data * Extract tool call from protobuf data
*/ */
function extractToolCall(toolCallData: Uint8Array): { function extractToolCall(toolCallData: Uint8Array): {
id: string; id: string;
type: string; type: string;
function: { name: string; arguments: string }; function: { name: string; arguments: string };
isLast: boolean; isLast: boolean;
} | null { } | null {
const toolCall = decodeMessage(toolCallData); const toolCall = decodeMessage(toolCallData);
let toolCallId = ""; let toolCallId = '';
let toolName = ""; let toolName = '';
let rawArgs = ""; let rawArgs = '';
let isLast = false; let isLast = false;
// Extract tool call ID // Extract tool call ID
if (toolCall.has(FIELD.TOOL_ID)) { if (toolCall.has(FIELD.TOOL_ID)) {
const fullId = new TextDecoder().decode( const idField = toolCall.get(FIELD.TOOL_ID);
toolCall.get(FIELD.TOOL_ID)![0].value as Uint8Array if (idField && idField[0]) {
); const fullId = new TextDecoder().decode(idField[0].value as Uint8Array);
toolCallId = fullId.split("\n")[0]; // Take first line toolCallId = fullId.split('\n')[0]; // Take first line
} }
}
// Extract tool name // Extract tool name
if (toolCall.has(FIELD.TOOL_NAME)) { if (toolCall.has(FIELD.TOOL_NAME)) {
toolName = new TextDecoder().decode( const nameField = toolCall.get(FIELD.TOOL_NAME);
toolCall.get(FIELD.TOOL_NAME)![0].value as Uint8Array if (nameField && nameField[0]) {
); toolName = new TextDecoder().decode(nameField[0].value as Uint8Array);
} }
}
// Extract is_last flag // Extract is_last flag
if (toolCall.has(FIELD.TOOL_IS_LAST)) { if (toolCall.has(FIELD.TOOL_IS_LAST)) {
isLast = (toolCall.get(FIELD.TOOL_IS_LAST)![0].value as number) !== 0; const lastField = toolCall.get(FIELD.TOOL_IS_LAST);
} if (lastField && lastField[0]) {
isLast = (lastField[0].value as number) !== 0;
}
}
// Extract MCP params - nested real tool info // Extract MCP params - nested real tool info
if (toolCall.has(FIELD.TOOL_MCP_PARAMS)) { if (toolCall.has(FIELD.TOOL_MCP_PARAMS)) {
try { try {
const mcpParams = decodeMessage( const mcpField = toolCall.get(FIELD.TOOL_MCP_PARAMS);
toolCall.get(FIELD.TOOL_MCP_PARAMS)![0].value as Uint8Array if (!mcpField || !mcpField[0]) return null;
);
if (mcpParams.has(FIELD.MCP_TOOLS_LIST)) { const mcpParams = decodeMessage(mcpField[0].value as Uint8Array);
const tool = decodeMessage(
mcpParams.get(FIELD.MCP_TOOLS_LIST)![0].value as Uint8Array
);
if (tool.has(FIELD.MCP_NESTED_NAME)) { if (mcpParams.has(FIELD.MCP_TOOLS_LIST)) {
toolName = new TextDecoder().decode( const toolsList = mcpParams.get(FIELD.MCP_TOOLS_LIST);
tool.get(FIELD.MCP_NESTED_NAME)![0].value as Uint8Array if (!toolsList || !toolsList[0]) return null;
);
}
if (tool.has(FIELD.MCP_NESTED_PARAMS)) { const tool = decodeMessage(toolsList[0].value as Uint8Array);
rawArgs = new TextDecoder().decode(
tool.get(FIELD.MCP_NESTED_PARAMS)![0].value as Uint8Array
);
}
}
} catch {
// MCP parse error, continue
}
}
// Fallback to raw_args if (tool.has(FIELD.MCP_NESTED_NAME)) {
if (!rawArgs && toolCall.has(FIELD.TOOL_RAW_ARGS)) { const nestedName = tool.get(FIELD.MCP_NESTED_NAME);
rawArgs = new TextDecoder().decode( if (nestedName && nestedName[0]) {
toolCall.get(FIELD.TOOL_RAW_ARGS)![0].value as Uint8Array toolName = new TextDecoder().decode(nestedName[0].value as Uint8Array);
); }
} }
if (toolCallId && toolName) { if (tool.has(FIELD.MCP_NESTED_PARAMS)) {
return { const nestedParams = tool.get(FIELD.MCP_NESTED_PARAMS);
id: toolCallId, if (nestedParams && nestedParams[0]) {
type: "function", rawArgs = new TextDecoder().decode(nestedParams[0].value as Uint8Array);
function: { }
name: toolName, }
arguments: rawArgs || "{}", }
}, } catch {
isLast, // MCP parse error, continue
}; }
} }
return null; // Fallback to raw_args
if (!rawArgs && toolCall.has(FIELD.TOOL_RAW_ARGS)) {
const rawArgsField = toolCall.get(FIELD.TOOL_RAW_ARGS);
if (rawArgsField && rawArgsField[0]) {
rawArgs = new TextDecoder().decode(rawArgsField[0].value as Uint8Array);
}
}
if (toolCallId && toolName) {
return {
id: toolCallId,
type: 'function',
function: {
name: toolName,
arguments: rawArgs || '{}',
},
isLast,
};
}
return null;
} }
/** /**
* Extract text and thinking from response data * Extract text and thinking from response data
*/ */
function extractTextAndThinking( function extractTextAndThinking(responseData: Uint8Array): {
responseData: Uint8Array text: string | null;
): { text: string | null; thinking: string | null } { thinking: string | null;
const nested = decodeMessage(responseData); } {
let text: string | null = null; const nested = decodeMessage(responseData);
let thinking: string | null = null; let text: string | null = null;
let thinking: string | null = null;
// Extract text // Extract text
if (nested.has(FIELD.RESPONSE_TEXT)) { if (nested.has(FIELD.RESPONSE_TEXT)) {
text = new TextDecoder().decode( const textField = nested.get(FIELD.RESPONSE_TEXT);
nested.get(FIELD.RESPONSE_TEXT)![0].value as Uint8Array if (textField && textField[0]) {
); text = new TextDecoder().decode(textField[0].value as Uint8Array);
} }
}
// Extract thinking // Extract thinking
if (nested.has(FIELD.THINKING)) { if (nested.has(FIELD.THINKING)) {
try { try {
const thinkingMsg = decodeMessage( const thinkingField = nested.get(FIELD.THINKING);
nested.get(FIELD.THINKING)![0].value as Uint8Array if (thinkingField && thinkingField[0]) {
); const thinkingMsg = decodeMessage(thinkingField[0].value as Uint8Array);
if (thinkingMsg.has(FIELD.THINKING_TEXT)) { if (thinkingMsg.has(FIELD.THINKING_TEXT)) {
thinking = new TextDecoder().decode( const thinkingTextField = thinkingMsg.get(FIELD.THINKING_TEXT);
thinkingMsg.get(FIELD.THINKING_TEXT)![0].value as Uint8Array if (thinkingTextField && thinkingTextField[0]) {
); thinking = new TextDecoder().decode(thinkingTextField[0].value as Uint8Array);
} }
} catch { }
// Thinking parse error, continue }
} } catch {
} // Thinking parse error, continue
}
}
return { text, thinking }; return { text, thinking };
} }
/** /**
* Extract text and tool calls from response payload * Extract text and tool calls from response payload
*/ */
export function extractTextFromResponse(payload: Uint8Array): { export function extractTextFromResponse(payload: Uint8Array): {
text: string | null; text: string | null;
error: string | null; error: string | null;
toolCall: { toolCall: {
id: string; id: string;
type: string; type: string;
function: { name: string; arguments: string }; function: { name: string; arguments: string };
isLast: boolean; isLast: boolean;
} | null; } | null;
thinking: string | null; thinking: string | null;
} { } {
try { try {
const fields = decodeMessage(payload); const fields = decodeMessage(payload);
// Field 1: ClientSideToolV2Call // Field 1: ClientSideToolV2Call
if (fields.has(FIELD.TOOL_CALL)) { if (fields.has(FIELD.TOOL_CALL)) {
const toolCall = extractToolCall( const toolCallField = fields.get(FIELD.TOOL_CALL);
fields.get(FIELD.TOOL_CALL)![0].value as Uint8Array if (toolCallField && toolCallField[0]) {
); const toolCall = extractToolCall(toolCallField[0].value as Uint8Array);
if (toolCall) { if (toolCall) {
return { text: null, error: null, toolCall, thinking: null }; return { text: null, error: null, toolCall, thinking: null };
} }
} }
}
// Field 2: StreamUnifiedChatResponse // Field 2: StreamUnifiedChatResponse
if (fields.has(FIELD.RESPONSE)) { if (fields.has(FIELD.RESPONSE)) {
const { text, thinking } = extractTextAndThinking( const responseField = fields.get(FIELD.RESPONSE);
fields.get(FIELD.RESPONSE)![0].value as Uint8Array if (responseField && responseField[0]) {
); const { text, thinking } = extractTextAndThinking(responseField[0].value as Uint8Array);
if (text || thinking) { if (text || thinking) {
return { text, error: null, toolCall: null, thinking }; return { text, error: null, toolCall: null, thinking };
} }
} }
}
return { text: null, error: null, toolCall: null, thinking: null }; return { text: null, error: null, toolCall: null, thinking: null };
} catch { } catch {
return { text: null, error: null, toolCall: null, thinking: null }; return { text: null, error: null, toolCall: null, thinking: null };
} }
} }
+137 -175
View File
@@ -3,260 +3,222 @@
* Implements ConnectRPC protobuf wire format encoding * Implements ConnectRPC protobuf wire format encoding
*/ */
import { randomUUID } from "crypto"; import * as zlib from 'zlib';
import * as zlib from "zlib";
import { import {
WIRE_TYPE, WIRE_TYPE,
ROLE, FIELD,
UNIFIED_MODE, COMPRESS_FLAG,
THINKING_LEVEL, UNIFIED_MODE,
FIELD, type WireType,
COMPRESS_FLAG, type RoleType,
type WireType, type CursorTool,
type RoleType, type CursorToolResult,
type ThinkingLevelType, } from './cursor-protobuf-schema.js';
type CursorTool,
type CursorToolResult,
type CursorMessage,
type FormattedMessage,
type MessageId,
} from "./cursor-protobuf-schema.js";
/** /**
* Encode a varint (variable-length integer) * Encode a varint (variable-length integer)
*/ */
export function encodeVarint(value: number): Uint8Array { export function encodeVarint(value: number): Uint8Array {
const bytes: number[] = []; const bytes: number[] = [];
let val = value >>> 0; // Ensure unsigned let val = value >>> 0; // Ensure unsigned
while (val >= 0x80) { while (val >= 0x80) {
bytes.push((val & 0x7f) | 0x80); bytes.push((val & 0x7f) | 0x80);
val >>>= 7; val >>>= 7;
} }
bytes.push(val & 0x7f); bytes.push(val & 0x7f);
return new Uint8Array(bytes); return new Uint8Array(bytes);
} }
/** /**
* Encode a protobuf field (tag + value) * Encode a protobuf field (tag + value)
*/ */
export function encodeField( export function encodeField(
fieldNum: number, fieldNum: number,
wireType: WireType, wireType: WireType,
value: number | string | Uint8Array value: number | string | Uint8Array
): Uint8Array { ): Uint8Array {
const tag = (fieldNum << 3) | wireType; const tag = (fieldNum << 3) | wireType;
const tagBytes = encodeVarint(tag); const tagBytes = encodeVarint(tag);
if (wireType === WIRE_TYPE.VARINT) { if (wireType === WIRE_TYPE.VARINT) {
const valueBytes = encodeVarint(value as number); const valueBytes = encodeVarint(value as number);
return concatArrays(tagBytes, valueBytes); return concatArrays(tagBytes, valueBytes);
} }
if (wireType === WIRE_TYPE.LEN) { if (wireType === WIRE_TYPE.LEN) {
const dataBytes = const dataBytes =
typeof value === "string" typeof value === 'string'
? new TextEncoder().encode(value) ? new TextEncoder().encode(value)
: value instanceof Uint8Array : value instanceof Uint8Array
? value ? value
: new Uint8Array(0); : new Uint8Array(0);
const lengthBytes = encodeVarint(dataBytes.length); const lengthBytes = encodeVarint(dataBytes.length);
return concatArrays(tagBytes, lengthBytes, dataBytes); return concatArrays(tagBytes, lengthBytes, dataBytes);
} }
return new Uint8Array(0); return new Uint8Array(0);
} }
/** /**
* Concatenate multiple Uint8Arrays * Concatenate multiple Uint8Arrays
*/ */
function concatArrays(...arrays: Uint8Array[]): Uint8Array { export function concatArrays(...arrays: Uint8Array[]): Uint8Array {
const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0); const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);
const result = new Uint8Array(totalLength); const result = new Uint8Array(totalLength);
let offset = 0; let offset = 0;
for (const arr of arrays) { for (const arr of arrays) {
result.set(arr, offset); result.set(arr, offset);
offset += arr.length; offset += arr.length;
} }
return result; return result;
} }
/** /**
* Encode a tool result * Encode a tool result
*/ */
export function encodeToolResult(toolResult: CursorToolResult): Uint8Array { export function encodeToolResult(toolResult: CursorToolResult): Uint8Array {
const toolCallId = toolResult.tool_call_id || ""; const toolCallId = toolResult.tool_call_id || '';
const toolName = toolResult.name || ""; const toolName = toolResult.name || '';
const toolIndex = toolResult.index || 0; const toolIndex = toolResult.index || 0;
const rawArgs = toolResult.raw_args || "{}"; const rawArgs = toolResult.raw_args || '{}';
return concatArrays( return concatArrays(
encodeField(FIELD.TOOL_RESULT_CALL_ID, WIRE_TYPE.LEN, toolCallId), encodeField(FIELD.TOOL_RESULT_CALL_ID, WIRE_TYPE.LEN, toolCallId),
encodeField(FIELD.TOOL_RESULT_NAME, WIRE_TYPE.LEN, toolName), encodeField(FIELD.TOOL_RESULT_NAME, WIRE_TYPE.LEN, toolName),
encodeField(FIELD.TOOL_RESULT_INDEX, WIRE_TYPE.VARINT, toolIndex), encodeField(FIELD.TOOL_RESULT_INDEX, WIRE_TYPE.VARINT, toolIndex),
encodeField(FIELD.TOOL_RESULT_RAW_ARGS, WIRE_TYPE.LEN, rawArgs) encodeField(FIELD.TOOL_RESULT_RAW_ARGS, WIRE_TYPE.LEN, rawArgs)
); );
} }
/** /**
* Encode a conversation message * Encode a conversation message
*/ */
export function encodeMessage( export function encodeMessage(
content: string, content: string,
role: RoleType, role: RoleType,
messageId: string, messageId: string,
isLast: boolean, isLast: boolean,
hasTools: boolean, hasTools: boolean,
toolResults: CursorToolResult[] toolResults: CursorToolResult[]
): Uint8Array { ): Uint8Array {
return concatArrays( return concatArrays(
encodeField(FIELD.MSG_CONTENT, WIRE_TYPE.LEN, content), encodeField(FIELD.MSG_CONTENT, WIRE_TYPE.LEN, content),
encodeField(FIELD.MSG_ROLE, WIRE_TYPE.VARINT, role), encodeField(FIELD.MSG_ROLE, WIRE_TYPE.VARINT, role),
encodeField(FIELD.MSG_ID, WIRE_TYPE.LEN, messageId), encodeField(FIELD.MSG_ID, WIRE_TYPE.LEN, messageId),
...(toolResults.length > 0 ...(toolResults.length > 0
? toolResults.map((tr) => ? toolResults.map((tr) =>
encodeField( encodeField(FIELD.MSG_TOOL_RESULTS, WIRE_TYPE.LEN, encodeToolResult(tr))
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,
encodeField(FIELD.MSG_IS_AGENTIC, WIRE_TYPE.VARINT, hasTools ? 1 : 0), hasTools ? UNIFIED_MODE.AGENT : UNIFIED_MODE.CHAT
encodeField( ),
FIELD.MSG_UNIFIED_MODE, ...(isLast && hasTools
WIRE_TYPE.VARINT, ? [encodeField(FIELD.MSG_SUPPORTED_TOOLS, WIRE_TYPE.LEN, encodeVarint(1))]
hasTools ? UNIFIED_MODE.AGENT : UNIFIED_MODE.CHAT : [])
), );
...(isLast && hasTools
? [
encodeField(
FIELD.MSG_SUPPORTED_TOOLS,
WIRE_TYPE.LEN,
encodeVarint(1)
),
]
: [])
);
} }
/** /**
* Encode instruction text * Encode instruction text
*/ */
export function encodeInstruction(text: string): Uint8Array { export function encodeInstruction(text: string): Uint8Array {
return text return text ? encodeField(FIELD.INSTRUCTION_TEXT, WIRE_TYPE.LEN, text) : new Uint8Array(0);
? encodeField(FIELD.INSTRUCTION_TEXT, WIRE_TYPE.LEN, text)
: new Uint8Array(0);
} }
/** /**
* Encode model information * Encode model information
*/ */
export function encodeModel(modelName: string): Uint8Array { export function encodeModel(modelName: string): Uint8Array {
return concatArrays( return concatArrays(
encodeField(FIELD.MODEL_NAME, WIRE_TYPE.LEN, modelName), encodeField(FIELD.MODEL_NAME, WIRE_TYPE.LEN, modelName),
encodeField(FIELD.MODEL_EMPTY, WIRE_TYPE.LEN, new Uint8Array(0)) encodeField(FIELD.MODEL_EMPTY, WIRE_TYPE.LEN, new Uint8Array(0))
); );
} }
/** /**
* Encode cursor settings * Encode cursor settings
*/ */
export function encodeCursorSetting(): Uint8Array { export function encodeCursorSetting(): Uint8Array {
const unknown6 = concatArrays( const unknown6 = concatArrays(
encodeField(FIELD.SETTING6_FIELD_1, WIRE_TYPE.LEN, new Uint8Array(0)), encodeField(FIELD.SETTING6_FIELD_1, WIRE_TYPE.LEN, new Uint8Array(0)),
encodeField(FIELD.SETTING6_FIELD_2, WIRE_TYPE.LEN, new Uint8Array(0)) encodeField(FIELD.SETTING6_FIELD_2, WIRE_TYPE.LEN, new Uint8Array(0))
); );
return concatArrays( return concatArrays(
encodeField(FIELD.SETTING_PATH, WIRE_TYPE.LEN, "cursor\\aisettings"), encodeField(FIELD.SETTING_PATH, WIRE_TYPE.LEN, 'cursor\\aisettings'),
encodeField(FIELD.SETTING_UNKNOWN_3, WIRE_TYPE.LEN, new Uint8Array(0)), encodeField(FIELD.SETTING_UNKNOWN_3, WIRE_TYPE.LEN, new Uint8Array(0)),
encodeField(FIELD.SETTING_UNKNOWN_6, WIRE_TYPE.LEN, unknown6), encodeField(FIELD.SETTING_UNKNOWN_6, WIRE_TYPE.LEN, unknown6),
encodeField(FIELD.SETTING_UNKNOWN_8, WIRE_TYPE.VARINT, 1), encodeField(FIELD.SETTING_UNKNOWN_8, WIRE_TYPE.VARINT, 1),
encodeField(FIELD.SETTING_UNKNOWN_9, WIRE_TYPE.VARINT, 1) encodeField(FIELD.SETTING_UNKNOWN_9, WIRE_TYPE.VARINT, 1)
); );
} }
/** /**
* Encode metadata * Encode metadata
*/ */
export function encodeMetadata(): Uint8Array { export function encodeMetadata(): Uint8Array {
return concatArrays( return concatArrays(
encodeField(FIELD.META_PLATFORM, WIRE_TYPE.LEN, process.platform || "linux"), encodeField(FIELD.META_PLATFORM, WIRE_TYPE.LEN, process.platform || 'linux'),
encodeField(FIELD.META_ARCH, WIRE_TYPE.LEN, process.arch || "x64"), 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_VERSION, WIRE_TYPE.LEN, process.version || 'v20.0.0'),
encodeField(FIELD.META_CWD, WIRE_TYPE.LEN, process.cwd() || "/"), encodeField(FIELD.META_CWD, WIRE_TYPE.LEN, process.cwd() || '/'),
encodeField(FIELD.META_TIMESTAMP, WIRE_TYPE.LEN, new Date().toISOString()) encodeField(FIELD.META_TIMESTAMP, WIRE_TYPE.LEN, new Date().toISOString())
); );
} }
/** /**
* Encode message ID * Encode message ID
*/ */
export function encodeMessageId( export function encodeMessageId(messageId: string, role: RoleType, summaryId?: string): Uint8Array {
messageId: string, return concatArrays(
role: RoleType, encodeField(FIELD.MSGID_ID, WIRE_TYPE.LEN, messageId),
summaryId?: string ...(summaryId ? [encodeField(FIELD.MSGID_SUMMARY, WIRE_TYPE.LEN, summaryId)] : []),
): Uint8Array { encodeField(FIELD.MSGID_ROLE, WIRE_TYPE.VARINT, role)
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)
);
} }
/** /**
* Encode MCP tool * Encode MCP tool
*/ */
export function encodeMcpTool(tool: CursorTool): Uint8Array { export function encodeMcpTool(tool: CursorTool): Uint8Array {
const toolName = tool.function?.name || tool.name || ""; const toolName = tool.function?.name || tool.name || '';
const toolDesc = tool.function?.description || tool.description || ""; const toolDesc = tool.function?.description || tool.description || '';
const inputSchema = tool.function?.parameters || tool.input_schema || {}; const inputSchema = tool.function?.parameters || tool.input_schema || {};
return concatArrays( return concatArrays(
...(toolName ...(toolName ? [encodeField(FIELD.MCP_TOOL_NAME, WIRE_TYPE.LEN, 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
...(toolDesc ? [encodeField(FIELD.MCP_TOOL_PARAMS, WIRE_TYPE.LEN, JSON.stringify(inputSchema))]
? [encodeField(FIELD.MCP_TOOL_DESC, WIRE_TYPE.LEN, toolDesc)] : []),
: []), encodeField(FIELD.MCP_TOOL_SERVER, WIRE_TYPE.LEN, 'custom')
...(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")
);
} }
/** /**
* Wrap payload in ConnectRPC frame (5-byte header + payload) * Wrap payload in ConnectRPC frame (5-byte header + payload)
*/ */
export function wrapConnectRPCFrame( export function wrapConnectRPCFrame(payload: Uint8Array, compress = false): Uint8Array {
payload: Uint8Array, let finalPayload = payload;
compress = false let flags: number = COMPRESS_FLAG.NONE;
): Uint8Array {
let finalPayload = payload;
let flags: number = COMPRESS_FLAG.NONE;
if (compress) { if (compress) {
finalPayload = new Uint8Array(zlib.gzipSync(Buffer.from(payload))); finalPayload = new Uint8Array(zlib.gzipSync(Buffer.from(payload)));
flags = COMPRESS_FLAG.GZIP; flags = COMPRESS_FLAG.GZIP;
} }
const frame = new Uint8Array(5 + finalPayload.length); const frame = new Uint8Array(5 + finalPayload.length);
frame[0] = flags; frame[0] = flags;
frame[1] = (finalPayload.length >> 24) & 0xff; frame[1] = (finalPayload.length >> 24) & 0xff;
frame[2] = (finalPayload.length >> 16) & 0xff; frame[2] = (finalPayload.length >> 16) & 0xff;
frame[3] = (finalPayload.length >> 8) & 0xff; frame[3] = (finalPayload.length >> 8) & 0xff;
frame[4] = finalPayload.length & 0xff; frame[4] = finalPayload.length & 0xff;
frame.set(finalPayload, 5); frame.set(finalPayload, 5);
return frame; return frame;
} }
+134 -135
View File
@@ -5,201 +5,200 @@
/** Wire types for protobuf encoding */ /** Wire types for protobuf encoding */
export const WIRE_TYPE = { export const WIRE_TYPE = {
VARINT: 0, VARINT: 0,
FIXED64: 1, FIXED64: 1,
LEN: 2, LEN: 2,
FIXED32: 5, FIXED32: 5,
} as const; } as const;
/** Message role constants */ /** Message role constants */
export const ROLE = { export const ROLE = {
USER: 1, USER: 1,
ASSISTANT: 2, ASSISTANT: 2,
} as const; } as const;
/** Unified mode constants */ /** Unified mode constants */
export const UNIFIED_MODE = { export const UNIFIED_MODE = {
CHAT: 1, CHAT: 1,
AGENT: 2, AGENT: 2,
} as const; } as const;
/** Thinking level constants */ /** Thinking level constants */
export const THINKING_LEVEL = { export const THINKING_LEVEL = {
UNSPECIFIED: 0, UNSPECIFIED: 0,
MEDIUM: 1, MEDIUM: 1,
HIGH: 2, HIGH: 2,
} as const; } as const;
/** Field numbers for all protobuf messages */ /** Field numbers for all protobuf messages */
export const FIELD = { export const FIELD = {
// StreamUnifiedChatRequestWithTools (top level) // StreamUnifiedChatRequestWithTools (top level)
REQUEST: 1, REQUEST: 1,
// StreamUnifiedChatRequest // StreamUnifiedChatRequest
MESSAGES: 1, MESSAGES: 1,
UNKNOWN_2: 2, UNKNOWN_2: 2,
INSTRUCTION: 3, INSTRUCTION: 3,
UNKNOWN_4: 4, UNKNOWN_4: 4,
MODEL: 5, MODEL: 5,
WEB_TOOL: 8, WEB_TOOL: 8,
UNKNOWN_13: 13, UNKNOWN_13: 13,
CURSOR_SETTING: 15, CURSOR_SETTING: 15,
UNKNOWN_19: 19, UNKNOWN_19: 19,
CONVERSATION_ID: 23, CONVERSATION_ID: 23,
METADATA: 26, METADATA: 26,
IS_AGENTIC: 27, IS_AGENTIC: 27,
SUPPORTED_TOOLS: 29, SUPPORTED_TOOLS: 29,
MESSAGE_IDS: 30, MESSAGE_IDS: 30,
MCP_TOOLS: 34, MCP_TOOLS: 34,
LARGE_CONTEXT: 35, LARGE_CONTEXT: 35,
UNKNOWN_38: 38, UNKNOWN_38: 38,
UNIFIED_MODE: 46, UNIFIED_MODE: 46,
UNKNOWN_47: 47, UNKNOWN_47: 47,
SHOULD_DISABLE_TOOLS: 48, SHOULD_DISABLE_TOOLS: 48,
THINKING_LEVEL: 49, THINKING_LEVEL: 49,
UNKNOWN_51: 51, UNKNOWN_51: 51,
UNKNOWN_53: 53, UNKNOWN_53: 53,
UNIFIED_MODE_NAME: 54, UNIFIED_MODE_NAME: 54,
// ConversationMessage // ConversationMessage
MSG_CONTENT: 1, MSG_CONTENT: 1,
MSG_ROLE: 2, MSG_ROLE: 2,
MSG_ID: 13, MSG_ID: 13,
MSG_TOOL_RESULTS: 18, MSG_TOOL_RESULTS: 18,
MSG_IS_AGENTIC: 29, MSG_IS_AGENTIC: 29,
MSG_UNIFIED_MODE: 47, MSG_UNIFIED_MODE: 47,
MSG_SUPPORTED_TOOLS: 51, MSG_SUPPORTED_TOOLS: 51,
// ConversationMessage.ToolResult // ConversationMessage.ToolResult
TOOL_RESULT_CALL_ID: 1, TOOL_RESULT_CALL_ID: 1,
TOOL_RESULT_NAME: 2, TOOL_RESULT_NAME: 2,
TOOL_RESULT_INDEX: 3, TOOL_RESULT_INDEX: 3,
TOOL_RESULT_RAW_ARGS: 5, TOOL_RESULT_RAW_ARGS: 5,
TOOL_RESULT_RESULT: 8, TOOL_RESULT_RESULT: 8,
// Model // Model
MODEL_NAME: 1, MODEL_NAME: 1,
MODEL_EMPTY: 4, MODEL_EMPTY: 4,
// Instruction // Instruction
INSTRUCTION_TEXT: 1, INSTRUCTION_TEXT: 1,
// CursorSetting // CursorSetting
SETTING_PATH: 1, SETTING_PATH: 1,
SETTING_UNKNOWN_3: 3, SETTING_UNKNOWN_3: 3,
SETTING_UNKNOWN_6: 6, SETTING_UNKNOWN_6: 6,
SETTING_UNKNOWN_8: 8, SETTING_UNKNOWN_8: 8,
SETTING_UNKNOWN_9: 9, SETTING_UNKNOWN_9: 9,
// CursorSetting.Unknown6 // CursorSetting.Unknown6
SETTING6_FIELD_1: 1, SETTING6_FIELD_1: 1,
SETTING6_FIELD_2: 2, SETTING6_FIELD_2: 2,
// Metadata // Metadata
META_PLATFORM: 1, META_PLATFORM: 1,
META_ARCH: 2, META_ARCH: 2,
META_VERSION: 3, META_VERSION: 3,
META_CWD: 4, META_CWD: 4,
META_TIMESTAMP: 5, META_TIMESTAMP: 5,
// MessageId // MessageId
MSGID_ID: 1, MSGID_ID: 1,
MSGID_SUMMARY: 2, MSGID_SUMMARY: 2,
MSGID_ROLE: 3, MSGID_ROLE: 3,
// MCPTool // MCPTool
MCP_TOOL_NAME: 1, MCP_TOOL_NAME: 1,
MCP_TOOL_DESC: 2, MCP_TOOL_DESC: 2,
MCP_TOOL_PARAMS: 3, MCP_TOOL_PARAMS: 3,
MCP_TOOL_SERVER: 4, MCP_TOOL_SERVER: 4,
// StreamUnifiedChatResponseWithTools (response) // StreamUnifiedChatResponseWithTools (response)
TOOL_CALL: 1, TOOL_CALL: 1,
RESPONSE: 2, RESPONSE: 2,
// ClientSideToolV2Call // ClientSideToolV2Call
TOOL_ID: 3, TOOL_ID: 3,
TOOL_NAME: 9, TOOL_NAME: 9,
TOOL_RAW_ARGS: 10, TOOL_RAW_ARGS: 10,
TOOL_IS_LAST: 11, TOOL_IS_LAST: 11,
TOOL_MCP_PARAMS: 27, TOOL_MCP_PARAMS: 27,
// MCPParams // MCPParams
MCP_TOOLS_LIST: 1, MCP_TOOLS_LIST: 1,
// MCPParams.Tool (nested) // MCPParams.Tool (nested)
MCP_NESTED_NAME: 1, MCP_NESTED_NAME: 1,
MCP_NESTED_PARAMS: 3, MCP_NESTED_PARAMS: 3,
// StreamUnifiedChatResponse // StreamUnifiedChatResponse
RESPONSE_TEXT: 1, RESPONSE_TEXT: 1,
THINKING: 25, THINKING: 25,
// Thinking // Thinking
THINKING_TEXT: 1, THINKING_TEXT: 1,
} as const; } as const;
/** Type definitions */ /** Type definitions */
export type WireType = (typeof WIRE_TYPE)[keyof typeof WIRE_TYPE]; export type WireType = (typeof WIRE_TYPE)[keyof typeof WIRE_TYPE];
export type RoleType = (typeof ROLE)[keyof typeof ROLE]; export type RoleType = (typeof ROLE)[keyof typeof ROLE];
export type UnifiedModeType = (typeof UNIFIED_MODE)[keyof typeof UNIFIED_MODE]; export type UnifiedModeType = (typeof UNIFIED_MODE)[keyof typeof UNIFIED_MODE];
export type ThinkingLevelType = export type ThinkingLevelType = (typeof THINKING_LEVEL)[keyof typeof THINKING_LEVEL];
(typeof THINKING_LEVEL)[keyof typeof THINKING_LEVEL];
export type FieldNumber = (typeof FIELD)[keyof typeof FIELD]; export type FieldNumber = (typeof FIELD)[keyof typeof FIELD];
/** Cursor tool definition */ /** Cursor tool definition */
export interface CursorTool { export interface CursorTool {
function?: { function?: {
name?: string; name?: string;
description?: string; description?: string;
parameters?: Record<string, unknown>; parameters?: Record<string, unknown>;
}; };
name?: string; name?: string;
description?: string; description?: string;
input_schema?: Record<string, unknown>; input_schema?: Record<string, unknown>;
} }
/** Cursor tool result */ /** Cursor tool result */
export interface CursorToolResult { export interface CursorToolResult {
tool_call_id?: string; tool_call_id?: string;
name?: string; name?: string;
index?: number; index?: number;
raw_args?: string; raw_args?: string;
} }
/** Cursor message format */ /** Cursor message format */
export interface CursorMessage { export interface CursorMessage {
role: string; role: string;
content: string; content: string;
tool_results?: CursorToolResult[]; tool_results?: CursorToolResult[];
tool_calls?: Array<{ tool_calls?: Array<{
id: string; id: string;
type: string; type: string;
function: { function: {
name: string; name: string;
arguments: string; arguments: string;
}; };
}>; }>;
} }
/** Formatted message for encoding */ /** Formatted message for encoding */
export interface FormattedMessage { export interface FormattedMessage {
content: string; content: string;
role: RoleType; role: RoleType;
messageId: string; messageId: string;
isLast: boolean; isLast: boolean;
hasTools: boolean; hasTools: boolean;
toolResults: CursorToolResult[]; toolResults: CursorToolResult[];
} }
/** Message ID structure */ /** Message ID structure */
export interface MessageId { export interface MessageId {
messageId: string; messageId: string;
role: RoleType; role: RoleType;
} }
/** Compression flags for ConnectRPC frames */ /** Compression flags for ConnectRPC frames */
export const COMPRESS_FLAG = { export const COMPRESS_FLAG = {
NONE: 0x00, NONE: 0x00,
GZIP: 0x01, GZIP: 0x01,
} as const; } as const;
+143 -169
View File
@@ -3,210 +3,184 @@
* Exports encoder/decoder functions and builds complete requests * Exports encoder/decoder functions and builds complete requests
*/ */
import { randomUUID } from "crypto"; import { randomUUID } from 'crypto';
import { import {
ROLE, ROLE,
UNIFIED_MODE, UNIFIED_MODE,
THINKING_LEVEL, THINKING_LEVEL,
FIELD, FIELD,
type CursorMessage, type CursorMessage,
type CursorTool, type CursorTool,
type FormattedMessage, type FormattedMessage,
type MessageId, type MessageId,
type ThinkingLevelType, type ThinkingLevelType,
} from "./cursor-protobuf-schema.js"; } from './cursor-protobuf-schema.js';
import { import {
encodeField, encodeField,
encodeVarint, encodeVarint,
encodeMessage, encodeMessage,
encodeInstruction, encodeInstruction,
encodeModel, encodeModel,
encodeCursorSetting, encodeCursorSetting,
encodeMetadata, encodeMetadata,
encodeMessageId, encodeMessageId,
encodeMcpTool, encodeMcpTool,
wrapConnectRPCFrame, wrapConnectRPCFrame,
} from "./cursor-protobuf-encoder.js"; concatArrays,
} from './cursor-protobuf-encoder.js';
import { import {
decodeVarint, decodeVarint,
decodeField, decodeField,
decodeMessage, decodeMessage,
parseConnectRPCFrame, parseConnectRPCFrame,
extractTextFromResponse, extractTextFromResponse,
} from "./cursor-protobuf-decoder.js"; } from './cursor-protobuf-decoder.js';
import { WIRE_TYPE } from "./cursor-protobuf-schema.js"; import { WIRE_TYPE } from './cursor-protobuf-schema.js';
/** /**
* Build complete chat request protobuf * Build complete chat request protobuf
*/ */
export function encodeRequest( export function encodeRequest(
messages: CursorMessage[], messages: CursorMessage[],
modelName: string, modelName: string,
tools: CursorTool[] = [], tools: CursorTool[] = [],
reasoningEffort: string | null = null reasoningEffort: string | null = null
): Uint8Array { ): Uint8Array {
const hasTools = tools?.length > 0; if (messages.length === 0) {
const isAgentic = hasTools; throw new Error('Messages array must not be empty');
const formattedMessages: FormattedMessage[] = []; }
const messageIds: MessageId[] = [];
// Prepare messages const hasTools = tools?.length > 0;
for (let i = 0; i < messages.length; i++) { const isAgentic = hasTools;
const msg = messages[i]; const formattedMessages: FormattedMessage[] = [];
const role = msg.role === "user" ? ROLE.USER : ROLE.ASSISTANT; const messageIds: MessageId[] = [];
const msgId = randomUUID();
const isLast = i === messages.length - 1;
formattedMessages.push({ // Prepare messages
content: msg.content, for (let i = 0; i < messages.length; i++) {
role, const msg = messages[i];
messageId: msgId, const role = msg.role === 'user' ? ROLE.USER : ROLE.ASSISTANT;
isLast, const msgId = randomUUID();
hasTools, const isLast = i === messages.length - 1;
toolResults: msg.tool_results || [],
});
messageIds.push({ messageId: msgId, role }); formattedMessages.push({
} content: msg.content,
role,
messageId: msgId,
isLast,
hasTools,
toolResults: msg.tool_results || [],
});
// Map reasoning effort to thinking level messageIds.push({ messageId: msgId, role });
let thinkingLevel: ThinkingLevelType = THINKING_LEVEL.UNSPECIFIED; }
if (reasoningEffort === "medium") thinkingLevel = THINKING_LEVEL.MEDIUM;
else if (reasoningEffort === "high") thinkingLevel = THINKING_LEVEL.HIGH;
// Build arrays for messages and tools // Map reasoning effort to thinking level
const messageFields = formattedMessages.map((fm) => let thinkingLevel: ThinkingLevelType = THINKING_LEVEL.UNSPECIFIED;
encodeField( if (reasoningEffort === 'medium') thinkingLevel = THINKING_LEVEL.MEDIUM;
FIELD.MESSAGES, else if (reasoningEffort === 'high') thinkingLevel = THINKING_LEVEL.HIGH;
WIRE_TYPE.LEN,
encodeMessage(
fm.content,
fm.role,
fm.messageId,
fm.isLast,
fm.hasTools,
fm.toolResults
)
)
);
const messageIdFields = messageIds.map((mid) => // Build arrays for messages and tools
encodeField( const messageFields = formattedMessages.map((fm) =>
FIELD.MESSAGE_IDS, encodeField(
WIRE_TYPE.LEN, FIELD.MESSAGES,
encodeMessageId(mid.messageId, mid.role) WIRE_TYPE.LEN,
) encodeMessage(fm.content, fm.role, fm.messageId, fm.isLast, fm.hasTools, fm.toolResults)
); )
);
const toolFields = const messageIdFields = messageIds.map((mid) =>
tools?.length > 0 encodeField(FIELD.MESSAGE_IDS, WIRE_TYPE.LEN, encodeMessageId(mid.messageId, mid.role))
? tools.map((tool) => );
encodeField(FIELD.MCP_TOOLS, WIRE_TYPE.LEN, encodeMcpTool(tool))
)
: [];
const supportedToolsField = isAgentic const toolFields =
? [encodeField(FIELD.SUPPORTED_TOOLS, WIRE_TYPE.LEN, encodeVarint(1))] tools?.length > 0
: []; ? tools.map((tool) => encodeField(FIELD.MCP_TOOLS, WIRE_TYPE.LEN, encodeMcpTool(tool)))
: [];
// Concatenate all parts const supportedToolsField = isAgentic
const parts: Uint8Array[] = [ ? [encodeField(FIELD.SUPPORTED_TOOLS, WIRE_TYPE.LEN, encodeVarint(1))]
...messageFields, : [];
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, randomUUID()),
encodeField(FIELD.METADATA, WIRE_TYPE.LEN, encodeMetadata()),
encodeField(FIELD.IS_AGENTIC, WIRE_TYPE.VARINT, isAgentic ? 1 : 0),
...supportedToolsField,
...messageIdFields,
...toolFields,
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"
),
];
return concatArrays(...parts); // Concatenate all parts
const parts: Uint8Array[] = [
...messageFields,
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, randomUUID()),
encodeField(FIELD.METADATA, WIRE_TYPE.LEN, encodeMetadata()),
encodeField(FIELD.IS_AGENTIC, WIRE_TYPE.VARINT, isAgentic ? 1 : 0),
...supportedToolsField,
...messageIdFields,
...toolFields,
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'),
];
return concatArrays(...parts);
} }
/** /**
* Build chat request wrapped in top-level message * Build chat request wrapped in top-level message
*/ */
export function buildChatRequest( export function buildChatRequest(
messages: CursorMessage[], messages: CursorMessage[],
modelName: string, modelName: string,
tools: CursorTool[] = [], tools: CursorTool[] = [],
reasoningEffort: string | null = null reasoningEffort: string | null = null
): Uint8Array { ): Uint8Array {
return encodeField( return encodeField(
FIELD.REQUEST, FIELD.REQUEST,
WIRE_TYPE.LEN, WIRE_TYPE.LEN,
encodeRequest(messages, modelName, tools, reasoningEffort) encodeRequest(messages, modelName, tools, reasoningEffort)
); );
} }
/** /**
* Generate complete Cursor request body with ConnectRPC framing * Generate complete Cursor request body with ConnectRPC framing
*/ */
export function generateCursorBody( export function generateCursorBody(
messages: CursorMessage[], messages: CursorMessage[],
modelName: string, modelName: string,
tools: CursorTool[] = [], tools: CursorTool[] = [],
reasoningEffort: string | null = null reasoningEffort: string | null = null
): Uint8Array { ): Uint8Array {
const protobuf = buildChatRequest(messages, modelName, tools, reasoningEffort); const protobuf = buildChatRequest(messages, modelName, tools, reasoningEffort);
const framed = wrapConnectRPCFrame(protobuf, false); // Cursor doesn't support compressed requests const framed = wrapConnectRPCFrame(protobuf, false); // Cursor doesn't support compressed requests
return framed; return framed;
}
/**
* Concatenate multiple Uint8Arrays
*/
function concatArrays(...arrays: Uint8Array[]): Uint8Array {
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;
} }
// Re-export all functions // Re-export all functions
export { export {
encodeVarint, encodeVarint,
encodeField, encodeField,
encodeMessage, encodeMessage,
encodeInstruction, encodeInstruction,
encodeModel, encodeModel,
encodeCursorSetting, encodeCursorSetting,
encodeMetadata, encodeMetadata,
encodeMessageId, encodeMessageId,
encodeMcpTool, encodeMcpTool,
wrapConnectRPCFrame, wrapConnectRPCFrame,
decodeVarint, decodeVarint,
decodeField, decodeField,
decodeMessage, decodeMessage,
parseConnectRPCFrame, parseConnectRPCFrame,
extractTextFromResponse, extractTextFromResponse,
}; };
+96 -100
View File
@@ -3,30 +3,26 @@
* Converts OpenAI messages to Cursor format * Converts OpenAI messages to Cursor format
*/ */
import type { import type { CursorMessage, CursorToolResult, CursorTool } from './cursor-protobuf-schema.js';
CursorMessage,
CursorToolResult,
CursorTool,
} from "./cursor-protobuf-schema.js";
/** OpenAI message format */ /** OpenAI message format */
interface OpenAIMessage { interface OpenAIMessage {
role: string; role: string;
content: string | Array<{ type: string; text?: string }>; content: string | Array<{ type: string; text?: string }>;
name?: string; name?: string;
tool_call_id?: string; tool_call_id?: string;
tool_calls?: Array<{ tool_calls?: Array<{
id: string; id: string;
type: string; type: string;
function: { name: string; arguments: string }; function: { name: string; arguments: string };
}>; }>;
} }
/** OpenAI request body */ /** OpenAI request body */
interface OpenAIRequestBody { interface OpenAIRequestBody {
messages: OpenAIMessage[]; messages: OpenAIMessage[];
tools?: CursorTool[]; tools?: CursorTool[];
reasoning_effort?: string; reasoning_effort?: string;
} }
/** /**
@@ -36,91 +32,91 @@ interface OpenAIRequestBody {
* - assistant with tool_calls → keep tool_calls structure (Cursor supports it natively) * - assistant with tool_calls → keep tool_calls structure (Cursor supports it natively)
*/ */
function convertMessages(messages: OpenAIMessage[]): CursorMessage[] { function convertMessages(messages: OpenAIMessage[]): CursorMessage[] {
const result: CursorMessage[] = []; const result: CursorMessage[] = [];
let pendingToolResults: CursorToolResult[] = []; let pendingToolResults: CursorToolResult[] = [];
for (let i = 0; i < messages.length; i++) { for (let i = 0; i < messages.length; i++) {
const msg = messages[i]; const msg = messages[i];
if (msg.role === "system") { if (msg.role === 'system') {
result.push({ result.push({
role: "user", role: 'user',
content: `[System Instructions]\n${msg.content}`, content: `[System Instructions]\n${msg.content}`,
}); });
continue; continue;
} }
if (msg.role === "tool") { if (msg.role === 'tool') {
let toolContent = ""; let toolContent = '';
if (typeof msg.content === "string") { if (typeof msg.content === 'string') {
toolContent = msg.content; toolContent = msg.content;
} else if (Array.isArray(msg.content)) { } else if (Array.isArray(msg.content)) {
for (const part of msg.content) { for (const part of msg.content) {
if (part.type === "text" && part.text) { if (part.type === 'text' && part.text) {
toolContent += part.text; toolContent += part.text;
} }
} }
} }
const toolName = msg.name || "tool"; const toolName = msg.name || 'tool';
const toolCallId = msg.tool_call_id || ""; const toolCallId = msg.tool_call_id || '';
// Accumulate tool result // Accumulate tool result
pendingToolResults.push({ pendingToolResults.push({
tool_call_id: toolCallId, tool_call_id: toolCallId,
name: toolName, name: toolName,
index: pendingToolResults.length, index: pendingToolResults.length,
raw_args: toolContent, raw_args: toolContent,
}); });
continue; continue;
} }
if (msg.role === "user" || msg.role === "assistant") { if (msg.role === 'user' || msg.role === 'assistant') {
let content = ""; let content = '';
if (typeof msg.content === "string") { if (typeof msg.content === 'string') {
content = msg.content; content = msg.content;
} else if (Array.isArray(msg.content)) { } else if (Array.isArray(msg.content)) {
for (const part of msg.content) { for (const part of msg.content) {
if (part.type === "text" && part.text) { if (part.type === 'text' && part.text) {
content += part.text; content += part.text;
} }
} }
} }
// Keep tool_calls structure for assistant messages // Keep tool_calls structure for assistant messages
if (msg.role === "assistant" && msg.tool_calls && msg.tool_calls.length > 0) { if (msg.role === 'assistant' && msg.tool_calls && msg.tool_calls.length > 0) {
const assistantMsg: CursorMessage = { role: "assistant", content: "" }; const assistantMsg: CursorMessage = { role: 'assistant', content: '' };
if (content) { if (content) {
assistantMsg.content = content; assistantMsg.content = content;
} }
assistantMsg.tool_calls = msg.tool_calls; assistantMsg.tool_calls = msg.tool_calls;
// Attach pending tool results to assistant message with tool_calls // Attach pending tool results to assistant message with tool_calls
if (pendingToolResults.length > 0) { if (pendingToolResults.length > 0) {
assistantMsg.tool_results = pendingToolResults; assistantMsg.tool_results = pendingToolResults;
pendingToolResults = []; pendingToolResults = [];
} }
result.push(assistantMsg); result.push(assistantMsg);
} else if (content || pendingToolResults.length > 0) { } else if (content || pendingToolResults.length > 0) {
const msgObj: CursorMessage = { const msgObj: CursorMessage = {
role: msg.role, role: msg.role,
content: content || "", content: content || '',
}; };
// Attach pending tool results to this message // Attach pending tool results to this message
if (pendingToolResults.length > 0) { if (pendingToolResults.length > 0) {
msgObj.tool_results = pendingToolResults; msgObj.tool_results = pendingToolResults;
pendingToolResults = []; pendingToolResults = [];
} }
result.push(msgObj); result.push(msgObj);
} }
} }
} }
return result; return result;
} }
/** /**
@@ -128,18 +124,18 @@ function convertMessages(messages: OpenAIMessage[]): CursorMessage[] {
* Returns modified body with converted messages * Returns modified body with converted messages
*/ */
export function buildCursorRequest( export function buildCursorRequest(
model: string, _model: string,
body: OpenAIRequestBody, body: OpenAIRequestBody,
stream: boolean, _stream: boolean,
credentials: unknown _credentials: unknown
): { ): {
messages: CursorMessage[]; messages: CursorMessage[];
tools?: CursorTool[]; tools?: CursorTool[];
} { } {
const messages = convertMessages(body.messages || []); const messages = convertMessages(body.messages || []);
return { return {
...body, ...body,
messages, messages,
}; };
} }