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

225 lines
6.5 KiB
TypeScript

/**
* Cursor Protobuf Encoder
* Implements ConnectRPC protobuf wire format encoding
*/
import * as zlib from 'zlib';
import {
WIRE_TYPE,
FIELD,
COMPRESS_FLAG,
UNIFIED_MODE,
type WireType,
type RoleType,
type CursorTool,
type CursorToolResult,
} from './cursor-protobuf-schema.js';
/**
* Encode a varint (variable-length integer)
*/
export function encodeVarint(value: number): Uint8Array {
const bytes: number[] = [];
let val = value >>> 0; // Ensure unsigned
while (val >= 0x80) {
bytes.push((val & 0x7f) | 0x80);
val >>>= 7;
}
bytes.push(val & 0x7f);
return new Uint8Array(bytes);
}
/**
* Encode a protobuf field (tag + value)
*/
export function encodeField(
fieldNum: number,
wireType: WireType,
value: number | string | Uint8Array
): Uint8Array {
const tag = (fieldNum << 3) | wireType;
const tagBytes = encodeVarint(tag);
if (wireType === WIRE_TYPE.VARINT) {
const valueBytes = encodeVarint(value as number);
return concatArrays(tagBytes, valueBytes);
}
if (wireType === WIRE_TYPE.LEN) {
const dataBytes =
typeof value === 'string'
? new TextEncoder().encode(value)
: value instanceof Uint8Array
? value
: new Uint8Array(0);
const lengthBytes = encodeVarint(dataBytes.length);
return concatArrays(tagBytes, lengthBytes, dataBytes);
}
return new Uint8Array(0);
}
/**
* Concatenate multiple Uint8Arrays
*/
export 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;
}
/**
* Encode a tool result
*/
export function encodeToolResult(toolResult: CursorToolResult): Uint8Array {
const toolCallId = toolResult.tool_call_id || '';
const toolName = toolResult.name || '';
const toolIndex = toolResult.index || 0;
const rawArgs = toolResult.raw_args || '{}';
return concatArrays(
encodeField(FIELD.ToolResult.CALL_ID, WIRE_TYPE.LEN, toolCallId),
encodeField(FIELD.ToolResult.NAME, WIRE_TYPE.LEN, toolName),
encodeField(FIELD.ToolResult.INDEX, WIRE_TYPE.VARINT, toolIndex),
encodeField(FIELD.ToolResult.RAW_ARGS, WIRE_TYPE.LEN, rawArgs)
);
}
/**
* Encode a conversation message
*/
export function encodeMessage(
content: string,
role: RoleType,
messageId: string,
isLast: boolean,
hasTools: boolean,
toolResults: CursorToolResult[]
): Uint8Array {
return concatArrays(
encodeField(FIELD.Message.CONTENT, WIRE_TYPE.LEN, content),
encodeField(FIELD.Message.ROLE, WIRE_TYPE.VARINT, role),
encodeField(FIELD.Message.ID, WIRE_TYPE.LEN, messageId),
...(toolResults.length > 0
? toolResults.map((tr) =>
encodeField(FIELD.Message.TOOL_RESULTS, WIRE_TYPE.LEN, encodeToolResult(tr))
)
: []),
encodeField(FIELD.Message.IS_AGENTIC, WIRE_TYPE.VARINT, hasTools ? 1 : 0),
encodeField(
FIELD.Message.UNIFIED_MODE,
WIRE_TYPE.VARINT,
hasTools ? UNIFIED_MODE.AGENT : UNIFIED_MODE.CHAT
),
...(isLast && hasTools
? [encodeField(FIELD.Message.SUPPORTED_TOOLS, WIRE_TYPE.LEN, encodeVarint(1))]
: [])
);
}
/**
* Encode instruction text
*/
export function encodeInstruction(text: string): Uint8Array {
return text ? encodeField(FIELD.Instruction.TEXT, WIRE_TYPE.LEN, text) : new Uint8Array(0);
}
/**
* Encode model information
*/
export function encodeModel(modelName: string): Uint8Array {
return concatArrays(
encodeField(FIELD.Model.NAME, WIRE_TYPE.LEN, modelName),
encodeField(FIELD.Model.EMPTY, WIRE_TYPE.LEN, new Uint8Array(0))
);
}
/**
* Encode cursor settings
*/
export function encodeCursorSetting(): Uint8Array {
const unknown6 = concatArrays(
encodeField(FIELD.Setting.U6_FIELD_1, WIRE_TYPE.LEN, new Uint8Array(0)),
encodeField(FIELD.Setting.U6_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)
);
}
/**
* Encode metadata
*/
export function encodeMetadata(): Uint8Array {
return concatArrays(
encodeField(FIELD.Metadata.PLATFORM, WIRE_TYPE.LEN, process.platform || 'linux'),
encodeField(FIELD.Metadata.ARCH, WIRE_TYPE.LEN, process.arch || 'x64'),
encodeField(FIELD.Metadata.VERSION, WIRE_TYPE.LEN, process.version || 'v20.0.0'),
encodeField(FIELD.Metadata.CWD, WIRE_TYPE.LEN, process.cwd() || '/'),
encodeField(FIELD.Metadata.TIMESTAMP, WIRE_TYPE.LEN, new Date().toISOString())
);
}
/**
* Encode message ID
*/
export function encodeMessageId(messageId: string, role: RoleType, summaryId?: string): Uint8Array {
return concatArrays(
encodeField(FIELD.MessageId.ID, WIRE_TYPE.LEN, messageId),
...(summaryId ? [encodeField(FIELD.MessageId.SUMMARY, WIRE_TYPE.LEN, summaryId)] : []),
encodeField(FIELD.MessageId.ROLE, WIRE_TYPE.VARINT, role)
);
}
/**
* Encode MCP tool
*/
export function encodeMcpTool(tool: CursorTool): Uint8Array {
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.McpTool.NAME, WIRE_TYPE.LEN, toolName)] : []),
...(toolDesc ? [encodeField(FIELD.McpTool.DESC, WIRE_TYPE.LEN, toolDesc)] : []),
...(Object.keys(inputSchema).length > 0
? [encodeField(FIELD.McpTool.PARAMS, WIRE_TYPE.LEN, JSON.stringify(inputSchema))]
: []),
encodeField(FIELD.McpTool.SERVER, WIRE_TYPE.LEN, 'custom')
);
}
/**
* Wrap payload in ConnectRPC frame (5-byte header + payload)
*/
export function wrapConnectRPCFrame(payload: Uint8Array, compress = false): Uint8Array {
let finalPayload = payload;
let flags: number = COMPRESS_FLAG.NONE;
if (compress) {
finalPayload = new Uint8Array(zlib.gzipSync(Buffer.from(payload)));
flags = COMPRESS_FLAG.GZIP;
}
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;
}