mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 14:16:43 +00:00
225 lines
6.5 KiB
TypeScript
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;
|
|
}
|