Files
ccs/src/cursor/cursor-protobuf-decoder.ts
T

342 lines
9.4 KiB
TypeScript

/**
* Cursor Protobuf Decoder
* Implements ConnectRPC protobuf wire format decoding
*/
import * as zlib from 'zlib';
import {
WIRE_TYPE,
FIELD,
isCompressedConnectFrame,
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<number, Array<{ wireType: WireType; value: Uint8Array | number }>> {
const fields = new Map<number, Array<{ wireType: WireType; value: Uint8Array | number }>>();
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 (isCompressedConnectFrame(flags)) {
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.ToolCall.ID)) {
const idField = toolCall.get(FIELD.ToolCall.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.ToolCall.NAME)) {
const nameField = toolCall.get(FIELD.ToolCall.NAME);
if (nameField && nameField[0]) {
toolName = new TextDecoder().decode(nameField[0].value as Uint8Array);
}
}
// Extract is_last flag
if (toolCall.has(FIELD.ToolCall.IS_LAST)) {
const lastField = toolCall.get(FIELD.ToolCall.IS_LAST);
if (lastField && lastField[0]) {
isLast = (lastField[0].value as number) !== 0;
}
}
// Extract MCP params - nested real tool info
if (toolCall.has(FIELD.ToolCall.MCP_PARAMS)) {
try {
const mcpField = toolCall.get(FIELD.ToolCall.MCP_PARAMS);
if (!mcpField || !mcpField[0]) return null;
const mcpParams = decodeMessage(mcpField[0].value as Uint8Array);
if (mcpParams.has(FIELD.McpParams.TOOLS_LIST)) {
const toolsList = mcpParams.get(FIELD.McpParams.TOOLS_LIST);
if (!toolsList || !toolsList[0]) return null;
const tool = decodeMessage(toolsList[0].value as Uint8Array);
if (tool.has(FIELD.McpNested.NAME)) {
const nestedName = tool.get(FIELD.McpNested.NAME);
if (nestedName && nestedName[0]) {
toolName = new TextDecoder().decode(nestedName[0].value as Uint8Array);
}
}
if (tool.has(FIELD.McpNested.PARAMS)) {
const nestedParams = tool.get(FIELD.McpNested.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.ToolCall.RAW_ARGS)) {
const rawArgsField = toolCall.get(FIELD.ToolCall.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.ChatResponse.TEXT)) {
const textField = nested.get(FIELD.ChatResponse.TEXT);
if (textField && textField[0]) {
text = new TextDecoder().decode(textField[0].value as Uint8Array);
}
}
// Extract thinking
if (nested.has(FIELD.ChatResponse.THINKING)) {
try {
const thinkingField = nested.get(FIELD.ChatResponse.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.Response.TOOL_CALL)) {
const toolCallField = fields.get(FIELD.Response.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.RESPONSE)) {
const responseField = fields.get(FIELD.Response.RESPONSE);
if (responseField && responseField[0]) {
const { text, thinking } = extractTextAndThinking(responseField[0].value as Uint8Array);
if (text || thinking) {
return { text, error: null, toolCall: null, thinking };
}
}
}
if (payload.length > 0) {
return { text: null, error: 'Malformed protobuf response', toolCall: null, thinking: null };
}
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: 'Malformed protobuf response', toolCall: null, thinking: null };
}
}