/** * Cursor Protobuf Decoder * Implements ConnectRPC protobuf wire format decoding */ import * as zlib from 'zlib'; import { WIRE_TYPE, FIELD, COMPRESS_FLAG, type WireType } from './cursor-protobuf-schema.js'; /** * Decode a varint from buffer * Returns [value, newOffset] */ export function decodeVarint(buffer: Uint8Array, offset: number): [number, number] { let result = 0; let shift = 0; let pos = offset; const maxBytes = 5; while (pos < buffer.length && pos - offset < maxBytes) { const b = buffer[pos]; result |= (b & 0x7f) << shift; pos++; if (!(b & 0x80)) break; shift += 7; } return [result >>> 0, pos]; // Ensure unsigned } /** * Decode a single protobuf field * Returns [fieldNum, wireType, value, newOffset] */ export function decodeField( buffer: Uint8Array, offset: number ): [number | null, WireType | null, Uint8Array | number | null, number] { if (offset >= buffer.length) { return [null, null, null, offset]; } const [tag, pos1] = decodeVarint(buffer, offset); const fieldNum = tag >> 3; const wireType = (tag & 0x07) as WireType; let value: Uint8Array | number | null; 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); if (pos2 + length > buffer.length) { return [null, null, null, buffer.length]; } value = buffer.slice(pos2, pos2 + length); pos = pos2 + length; } else if (wireType === WIRE_TYPE.FIXED64) { if (pos + 8 > buffer.length) { return [null, null, null, buffer.length]; } value = buffer.slice(pos, pos + 8); pos += 8; } else if (wireType === WIRE_TYPE.FIXED32) { if (pos + 4 > buffer.length) { return [null, null, null, buffer.length]; } value = buffer.slice(pos, pos + 4); pos += 4; } else { value = null; } return [fieldNum, wireType, value, pos]; } /** * Decode a protobuf message into a map of fields */ export function decodeMessage( data: Uint8Array ): Map> { const fields = new Map>(); let pos = 0; // NOTE: If two fields share the same field number but different wire types, later values overwrite earlier ones. while (pos < data.length) { const [fieldNum, wireType, value, newPos] = decodeField(data, pos); if (fieldNum === null || wireType === null || value === null) break; if (!fields.has(fieldNum)) { fields.set(fieldNum, []); } const fieldArray = fields.get(fieldNum); if (fieldArray) { fieldArray.push({ wireType, value: value as Uint8Array | number }); } pos = newPos; } return fields; } /** * Parse ConnectRPC frame from buffer * Returns frame data or null if incomplete */ export function parseConnectRPCFrame(buffer: Buffer): { flags: number; length: number; payload: Uint8Array; consumed: number; } | null { 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 === COMPRESS_FLAG.GZIP || flags === COMPRESS_FLAG.GZIP_ALT || flags === COMPRESS_FLAG.GZIP_BOTH ) { try { payload = Buffer.from(zlib.gunzipSync(payload)); } catch (err) { if (process.env.CCS_DEBUG) { console.error('[cursor] parseConnectRPCFrame decompression failed:', err); } // Decompression failed, use raw payload } } return { flags, length, payload: new Uint8Array(payload), consumed: 5 + length, }; } /** * Extract tool call from protobuf data */ function extractToolCall(toolCallData: Uint8Array): { id: string; type: string; function: { name: string; arguments: string }; isLast: boolean; } | null { const toolCall = decodeMessage(toolCallData); let toolCallId = ''; let toolName = ''; let rawArgs = ''; let isLast = false; // Extract tool call ID if (toolCall.has(FIELD.TOOL_ID)) { const idField = toolCall.get(FIELD.TOOL_ID); if (idField && idField[0]) { const fullId = new TextDecoder().decode(idField[0].value as Uint8Array); toolCallId = fullId.split('\n')[0]; // Take first line } } // Extract tool name if (toolCall.has(FIELD.TOOL_NAME)) { const nameField = toolCall.get(FIELD.TOOL_NAME); if (nameField && nameField[0]) { toolName = new TextDecoder().decode(nameField[0].value as Uint8Array); } } // Extract is_last flag if (toolCall.has(FIELD.TOOL_IS_LAST)) { 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 if (toolCall.has(FIELD.TOOL_MCP_PARAMS)) { try { const mcpField = toolCall.get(FIELD.TOOL_MCP_PARAMS); if (!mcpField || !mcpField[0]) return null; const mcpParams = decodeMessage(mcpField[0].value as Uint8Array); if (mcpParams.has(FIELD.MCP_TOOLS_LIST)) { const toolsList = mcpParams.get(FIELD.MCP_TOOLS_LIST); if (!toolsList || !toolsList[0]) return null; const tool = decodeMessage(toolsList[0].value as Uint8Array); if (tool.has(FIELD.MCP_NESTED_NAME)) { const nestedName = tool.get(FIELD.MCP_NESTED_NAME); if (nestedName && nestedName[0]) { toolName = new TextDecoder().decode(nestedName[0].value as Uint8Array); } } if (tool.has(FIELD.MCP_NESTED_PARAMS)) { const nestedParams = tool.get(FIELD.MCP_NESTED_PARAMS); if (nestedParams && nestedParams[0]) { rawArgs = new TextDecoder().decode(nestedParams[0].value as Uint8Array); } } } } catch (err) { if (process.env.CCS_DEBUG) { console.error('[cursor] extractToolCall MCP parsing failed:', err); } // MCP parse error, continue } } // 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 */ function extractTextAndThinking(responseData: Uint8Array): { text: string | null; thinking: string | null; } { const nested = decodeMessage(responseData); let text: string | null = null; let thinking: string | null = null; // Extract text if (nested.has(FIELD.RESPONSE_TEXT)) { const textField = nested.get(FIELD.RESPONSE_TEXT); if (textField && textField[0]) { text = new TextDecoder().decode(textField[0].value as Uint8Array); } } // Extract thinking if (nested.has(FIELD.THINKING)) { try { const thinkingField = nested.get(FIELD.THINKING); if (thinkingField && thinkingField[0]) { const thinkingMsg = decodeMessage(thinkingField[0].value as Uint8Array); if (thinkingMsg.has(FIELD.THINKING_TEXT)) { const thinkingTextField = thinkingMsg.get(FIELD.THINKING_TEXT); if (thinkingTextField && thinkingTextField[0]) { thinking = new TextDecoder().decode(thinkingTextField[0].value as Uint8Array); } } } } catch (err) { if (process.env.CCS_DEBUG) { console.error('[cursor] extractTextAndThinking parsing failed:', err); } // Thinking parse error, continue } } return { text, thinking }; } /** * Extract text and tool calls from response payload */ export function extractTextFromResponse(payload: Uint8Array): { text: string | null; error: string | null; toolCall: { id: string; type: string; function: { name: string; arguments: string }; isLast: boolean; } | null; thinking: string | null; } { try { const fields = decodeMessage(payload); // Field 1: ClientSideToolV2Call if (fields.has(FIELD.TOOL_CALL)) { const toolCallField = fields.get(FIELD.TOOL_CALL); if (toolCallField && toolCallField[0]) { const toolCall = extractToolCall(toolCallField[0].value as Uint8Array); if (toolCall) { return { text: null, error: null, toolCall, thinking: null }; } } } // Field 2: StreamUnifiedChatResponse if (fields.has(FIELD.RESPONSE)) { const responseField = fields.get(FIELD.RESPONSE); if (responseField && responseField[0]) { const { text, thinking } = extractTextAndThinking(responseField[0].value as Uint8Array); if (text || thinking) { return { text, error: null, toolCall: null, thinking }; } } } return { text: null, error: null, toolCall: null, thinking: null }; } catch (err) { if (process.env.CCS_DEBUG) { console.error('[cursor] extractTextFromResponse parsing failed:', err); } return { text: null, error: null, toolCall: null, thinking: null }; } }