fix(cursor): fix test isolation and daemon exit handling

- Convert PID_FILE constant to getPidFilePath() function to respect CCS_HOME changes at runtime
- Add proc.on('exit') handler to clear interval on silent process crashes
- Ensures test isolation by computing paths dynamically
This commit is contained in:
Tam Nhu Tran
2026-02-11 19:06:59 +07:00
parent aaa31c6427
commit fe97d720d4
8 changed files with 1458 additions and 1538 deletions
+26 -7
View File
@@ -25,7 +25,13 @@ function getCursorDir(): string {
return path.join(getCcsDir(), 'cursor');
}
const PID_FILE = path.join(getCursorDir(), 'daemon.pid');
/**
* Get PID file path.
* Computed at runtime to respect CCS_HOME changes (e.g., in tests).
*/
function getPidFilePath(): string {
return path.join(getCursorDir(), 'daemon.pid');
}
/**
* Check if cursor daemon is running on the specified port.
@@ -77,9 +83,10 @@ export async function getDaemonStatus(port: number): Promise<CursorDaemonStatus>
* Read PID from file.
*/
function getPidFromFile(): number | null {
const pidFile = getPidFilePath();
try {
if (fs.existsSync(PID_FILE)) {
const content = fs.readFileSync(PID_FILE, 'utf8').trim();
if (fs.existsSync(pidFile)) {
const content = fs.readFileSync(pidFile, 'utf8').trim();
const pid = parseInt(content, 10);
return isNaN(pid) ? null : pid;
}
@@ -93,12 +100,13 @@ function getPidFromFile(): number | null {
* Write PID to file.
*/
function writePidToFile(pid: number): void {
const pidFile = getPidFilePath();
try {
const dir = path.dirname(PID_FILE);
const dir = path.dirname(pidFile);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
}
fs.writeFileSync(PID_FILE, pid.toString(), { mode: 0o600 });
fs.writeFileSync(pidFile, pid.toString(), { mode: 0o600 });
} catch {
// Ignore errors
}
@@ -108,9 +116,10 @@ function writePidToFile(pid: number): void {
* Remove PID file.
*/
function removePidFile(): void {
const pidFile = getPidFilePath();
try {
if (fs.existsSync(PID_FILE)) {
fs.unlinkSync(PID_FILE);
if (fs.existsSync(pidFile)) {
fs.unlinkSync(pidFile);
}
} catch {
// Ignore errors
@@ -197,6 +206,16 @@ export async function startDaemon(
error: `Failed to start daemon: ${err.message}`,
});
});
proc.on('exit', (code) => {
if (code !== 0 && code !== null) {
clearInterval(checkInterval);
resolve({
success: false,
error: `Daemon process exited with code ${code}`,
});
}
});
} catch (err) {
resolve({
success: false,
File diff suppressed because it is too large Load Diff
+191 -217
View File
@@ -3,34 +3,27 @@
* Implements ConnectRPC protobuf wire format decoding
*/
import * as zlib from "zlib";
import {
WIRE_TYPE,
FIELD,
type WireType,
} from "./cursor-protobuf-schema.js";
import * as zlib from 'zlib';
import { WIRE_TYPE, FIELD, 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;
export function decodeVarint(buffer: Uint8Array, offset: number): [number, number] {
let result = 0;
let shift = 0;
let pos = offset;
while (pos < buffer.length) {
const b = buffer[pos];
result |= (b & 0x7f) << shift;
pos++;
if (!(b & 0x80)) break;
shift += 7;
}
while (pos < buffer.length) {
const b = buffer[pos];
result |= (b & 0x7f) << shift;
pos++;
if (!(b & 0x80)) break;
shift += 7;
}
return [result, pos];
return [result, pos];
}
/**
@@ -38,63 +31,60 @@ export function decodeVarint(
* Returns [fieldNum, wireType, value, newOffset]
*/
export function decodeField(
buffer: Uint8Array,
offset: number
buffer: Uint8Array,
offset: number
): [number | null, WireType | null, Uint8Array | number | null, number] {
if (offset >= buffer.length) {
return [null, null, null, offset];
}
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;
const [tag, pos1] = decodeVarint(buffer, offset);
const fieldNum = tag >> 3;
const wireType = (tag & 0x07) as WireType;
let value: Uint8Array | number | null;
let pos = pos1;
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);
value = buffer.slice(pos2, pos2 + length);
pos = pos2 + length;
} else if (wireType === WIRE_TYPE.FIXED64) {
value = buffer.slice(pos, pos + 8);
pos += 8;
} else if (wireType === WIRE_TYPE.FIXED32) {
value = buffer.slice(pos, pos + 4);
pos += 4;
} else {
value = null;
}
if (wireType === WIRE_TYPE.VARINT) {
[value, pos] = decodeVarint(buffer, pos);
} else if (wireType === WIRE_TYPE.LEN) {
const [length, pos2] = decodeVarint(buffer, pos);
value = buffer.slice(pos2, pos2 + length);
pos = pos2 + length;
} else if (wireType === WIRE_TYPE.FIXED64) {
value = buffer.slice(pos, pos + 8);
pos += 8;
} else if (wireType === WIRE_TYPE.FIXED32) {
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
*/
export function decodeMessage(
data: Uint8Array
data: Uint8Array
): Map<number, Array<{ wireType: WireType; value: Uint8Array | number }>> {
const fields = new Map<
number,
Array<{ wireType: WireType; value: Uint8Array | number }>
>();
let pos = 0;
const fields = new Map<number, Array<{ wireType: WireType; value: Uint8Array | number }>>();
let pos = 0;
while (pos < data.length) {
const [fieldNum, wireType, value, newPos] = decodeField(data, pos);
if (fieldNum === null || wireType === null || value === null) break;
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, []);
}
fields.get(fieldNum)!.push({ wireType, value: value as Uint8Array | number });
pos = newPos;
}
if (!fields.has(fieldNum)) {
fields.set(fieldNum, []);
}
fields.get(fieldNum)!.push({ wireType, value: value as Uint8Array | number });
pos = newPos;
}
return fields;
return fields;
}
/**
@@ -102,200 +92,184 @@ export function decodeMessage(
* Returns frame data or null if incomplete
*/
export function parseConnectRPCFrame(buffer: Buffer): {
flags: number;
length: number;
payload: Uint8Array;
consumed: number;
flags: number;
length: number;
payload: Uint8Array;
consumed: number;
} | null {
if (buffer.length < 5) return null;
if (buffer.length < 5) return null;
const flags = buffer[0];
const length =
(buffer[1] << 24) | (buffer[2] << 16) | (buffer[3] << 8) | buffer[4];
const flags = buffer[0];
const length = (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
if (flags === 0x01 || flags === 0x02 || flags === 0x03) {
try {
payload = Buffer.from(zlib.gunzipSync(payload));
} catch {
// Decompression failed, use raw payload
}
}
// Decompress if gzip
if (flags === 0x01 || flags === 0x02 || flags === 0x03) {
try {
payload = Buffer.from(zlib.gunzipSync(payload));
} catch {
// Decompression failed, use raw payload
}
}
return {
flags,
length,
payload: new Uint8Array(payload),
consumed: 5 + length,
};
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;
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;
const toolCall = decodeMessage(toolCallData);
let toolCallId = '';
let toolName = '';
let rawArgs = '';
let isLast = false;
// Extract tool call ID
if (toolCall.has(FIELD.TOOL_ID)) {
const fullId = new TextDecoder().decode(
toolCall.get(FIELD.TOOL_ID)![0].value as Uint8Array
);
toolCallId = fullId.split("\n")[0]; // Take first line
}
// Extract tool call ID
if (toolCall.has(FIELD.TOOL_ID)) {
const fullId = new TextDecoder().decode(toolCall.get(FIELD.TOOL_ID)![0].value as Uint8Array);
toolCallId = fullId.split('\n')[0]; // Take first line
}
// Extract tool name
if (toolCall.has(FIELD.TOOL_NAME)) {
toolName = new TextDecoder().decode(
toolCall.get(FIELD.TOOL_NAME)![0].value as Uint8Array
);
}
// Extract tool name
if (toolCall.has(FIELD.TOOL_NAME)) {
toolName = new TextDecoder().decode(toolCall.get(FIELD.TOOL_NAME)![0].value as Uint8Array);
}
// Extract is_last flag
if (toolCall.has(FIELD.TOOL_IS_LAST)) {
isLast = (toolCall.get(FIELD.TOOL_IS_LAST)![0].value as number) !== 0;
}
// Extract is_last flag
if (toolCall.has(FIELD.TOOL_IS_LAST)) {
isLast = (toolCall.get(FIELD.TOOL_IS_LAST)![0].value as number) !== 0;
}
// Extract MCP params - nested real tool info
if (toolCall.has(FIELD.TOOL_MCP_PARAMS)) {
try {
const mcpParams = decodeMessage(
toolCall.get(FIELD.TOOL_MCP_PARAMS)![0].value as Uint8Array
);
// Extract MCP params - nested real tool info
if (toolCall.has(FIELD.TOOL_MCP_PARAMS)) {
try {
const mcpParams = decodeMessage(toolCall.get(FIELD.TOOL_MCP_PARAMS)![0].value as Uint8Array);
if (mcpParams.has(FIELD.MCP_TOOLS_LIST)) {
const tool = decodeMessage(
mcpParams.get(FIELD.MCP_TOOLS_LIST)![0].value as Uint8Array
);
if (mcpParams.has(FIELD.MCP_TOOLS_LIST)) {
const tool = decodeMessage(mcpParams.get(FIELD.MCP_TOOLS_LIST)![0].value as Uint8Array);
if (tool.has(FIELD.MCP_NESTED_NAME)) {
toolName = new TextDecoder().decode(
tool.get(FIELD.MCP_NESTED_NAME)![0].value as Uint8Array
);
}
if (tool.has(FIELD.MCP_NESTED_NAME)) {
toolName = new TextDecoder().decode(
tool.get(FIELD.MCP_NESTED_NAME)![0].value as Uint8Array
);
}
if (tool.has(FIELD.MCP_NESTED_PARAMS)) {
rawArgs = new TextDecoder().decode(
tool.get(FIELD.MCP_NESTED_PARAMS)![0].value as Uint8Array
);
}
}
} catch {
// MCP parse error, continue
}
}
if (tool.has(FIELD.MCP_NESTED_PARAMS)) {
rawArgs = new TextDecoder().decode(
tool.get(FIELD.MCP_NESTED_PARAMS)![0].value as Uint8Array
);
}
}
} catch {
// MCP parse error, continue
}
}
// Fallback to raw_args
if (!rawArgs && toolCall.has(FIELD.TOOL_RAW_ARGS)) {
rawArgs = new TextDecoder().decode(
toolCall.get(FIELD.TOOL_RAW_ARGS)![0].value as Uint8Array
);
}
// Fallback to raw_args
if (!rawArgs && toolCall.has(FIELD.TOOL_RAW_ARGS)) {
rawArgs = new TextDecoder().decode(toolCall.get(FIELD.TOOL_RAW_ARGS)![0].value as Uint8Array);
}
if (toolCallId && toolName) {
return {
id: toolCallId,
type: "function",
function: {
name: toolName,
arguments: rawArgs || "{}",
},
isLast,
};
}
if (toolCallId && toolName) {
return {
id: toolCallId,
type: 'function',
function: {
name: toolName,
arguments: rawArgs || '{}',
},
isLast,
};
}
return null;
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;
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)) {
text = new TextDecoder().decode(
nested.get(FIELD.RESPONSE_TEXT)![0].value as Uint8Array
);
}
// Extract text
if (nested.has(FIELD.RESPONSE_TEXT)) {
text = new TextDecoder().decode(nested.get(FIELD.RESPONSE_TEXT)![0].value as Uint8Array);
}
// Extract thinking
if (nested.has(FIELD.THINKING)) {
try {
const thinkingMsg = decodeMessage(
nested.get(FIELD.THINKING)![0].value as Uint8Array
);
if (thinkingMsg.has(FIELD.THINKING_TEXT)) {
thinking = new TextDecoder().decode(
thinkingMsg.get(FIELD.THINKING_TEXT)![0].value as Uint8Array
);
}
} catch {
// Thinking parse error, continue
}
}
// Extract thinking
if (nested.has(FIELD.THINKING)) {
try {
const thinkingMsg = decodeMessage(nested.get(FIELD.THINKING)![0].value as Uint8Array);
if (thinkingMsg.has(FIELD.THINKING_TEXT)) {
thinking = new TextDecoder().decode(
thinkingMsg.get(FIELD.THINKING_TEXT)![0].value as Uint8Array
);
}
} catch {
// Thinking parse error, continue
}
}
return { text, thinking };
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;
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);
try {
const fields = decodeMessage(payload);
// Field 1: ClientSideToolV2Call
if (fields.has(FIELD.TOOL_CALL)) {
const toolCall = extractToolCall(
fields.get(FIELD.TOOL_CALL)![0].value as Uint8Array
);
if (toolCall) {
return { text: null, error: null, toolCall, thinking: null };
}
}
// Field 1: ClientSideToolV2Call
if (fields.has(FIELD.TOOL_CALL)) {
const toolCall = extractToolCall(fields.get(FIELD.TOOL_CALL)![0].value as Uint8Array);
if (toolCall) {
return { text: null, error: null, toolCall, thinking: null };
}
}
// Field 2: StreamUnifiedChatResponse
if (fields.has(FIELD.RESPONSE)) {
const { text, thinking } = extractTextAndThinking(
fields.get(FIELD.RESPONSE)![0].value as Uint8Array
);
// Field 2: StreamUnifiedChatResponse
if (fields.has(FIELD.RESPONSE)) {
const { text, thinking } = extractTextAndThinking(
fields.get(FIELD.RESPONSE)![0].value as Uint8Array
);
if (text || thinking) {
return { text, error: null, toolCall: null, thinking };
}
}
if (text || thinking) {
return { text, error: null, toolCall: null, thinking };
}
}
return { text: null, error: null, toolCall: null, thinking: null };
} catch {
return { text: null, error: null, toolCall: null, thinking: null };
}
return { text: null, error: null, toolCall: null, thinking: null };
} catch {
return { text: null, error: null, toolCall: null, thinking: null };
}
}
+143 -174
View File
@@ -3,260 +3,229 @@
* Implements ConnectRPC protobuf wire format encoding
*/
import { randomUUID } from "crypto";
import * as zlib from "zlib";
import { randomUUID } from 'crypto';
import * as zlib from 'zlib';
import {
WIRE_TYPE,
ROLE,
UNIFIED_MODE,
THINKING_LEVEL,
FIELD,
COMPRESS_FLAG,
type WireType,
type RoleType,
type ThinkingLevelType,
type CursorTool,
type CursorToolResult,
type CursorMessage,
type FormattedMessage,
type MessageId,
} from "./cursor-protobuf-schema.js";
WIRE_TYPE,
ROLE,
UNIFIED_MODE,
THINKING_LEVEL,
FIELD,
COMPRESS_FLAG,
type WireType,
type RoleType,
type ThinkingLevelType,
type CursorTool,
type CursorToolResult,
type CursorMessage,
type FormattedMessage,
type MessageId,
} 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);
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
fieldNum: number,
wireType: WireType,
value: number | string | Uint8Array
): Uint8Array {
const tag = (fieldNum << 3) | wireType;
const tagBytes = encodeVarint(tag);
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.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);
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);
}
const lengthBytes = encodeVarint(dataBytes.length);
return concatArrays(tagBytes, lengthBytes, dataBytes);
}
return new Uint8Array(0);
return new Uint8Array(0);
}
/**
* 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;
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 || "{}";
const toolCallId = toolResult.tool_call_id || '';
const toolName = toolResult.name || '';
const toolIndex = toolResult.index || 0;
const rawArgs = toolResult.raw_args || '{}';
return concatArrays(
encodeField(FIELD.TOOL_RESULT_CALL_ID, WIRE_TYPE.LEN, toolCallId),
encodeField(FIELD.TOOL_RESULT_NAME, WIRE_TYPE.LEN, toolName),
encodeField(FIELD.TOOL_RESULT_INDEX, WIRE_TYPE.VARINT, toolIndex),
encodeField(FIELD.TOOL_RESULT_RAW_ARGS, WIRE_TYPE.LEN, rawArgs)
);
return concatArrays(
encodeField(FIELD.TOOL_RESULT_CALL_ID, WIRE_TYPE.LEN, toolCallId),
encodeField(FIELD.TOOL_RESULT_NAME, WIRE_TYPE.LEN, toolName),
encodeField(FIELD.TOOL_RESULT_INDEX, WIRE_TYPE.VARINT, toolIndex),
encodeField(FIELD.TOOL_RESULT_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[]
content: string,
role: RoleType,
messageId: string,
isLast: boolean,
hasTools: boolean,
toolResults: CursorToolResult[]
): Uint8Array {
return concatArrays(
encodeField(FIELD.MSG_CONTENT, WIRE_TYPE.LEN, content),
encodeField(FIELD.MSG_ROLE, WIRE_TYPE.VARINT, role),
encodeField(FIELD.MSG_ID, WIRE_TYPE.LEN, messageId),
...(toolResults.length > 0
? toolResults.map((tr) =>
encodeField(
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,
hasTools ? UNIFIED_MODE.AGENT : UNIFIED_MODE.CHAT
),
...(isLast && hasTools
? [
encodeField(
FIELD.MSG_SUPPORTED_TOOLS,
WIRE_TYPE.LEN,
encodeVarint(1)
),
]
: [])
);
return concatArrays(
encodeField(FIELD.MSG_CONTENT, WIRE_TYPE.LEN, content),
encodeField(FIELD.MSG_ROLE, WIRE_TYPE.VARINT, role),
encodeField(FIELD.MSG_ID, WIRE_TYPE.LEN, messageId),
...(toolResults.length > 0
? toolResults.map((tr) =>
encodeField(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,
hasTools ? UNIFIED_MODE.AGENT : UNIFIED_MODE.CHAT
),
...(isLast && hasTools
? [encodeField(FIELD.MSG_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);
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))
);
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.SETTING6_FIELD_1, WIRE_TYPE.LEN, new Uint8Array(0)),
encodeField(FIELD.SETTING6_FIELD_2, WIRE_TYPE.LEN, new Uint8Array(0))
);
const unknown6 = concatArrays(
encodeField(FIELD.SETTING6_FIELD_1, WIRE_TYPE.LEN, new Uint8Array(0)),
encodeField(FIELD.SETTING6_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)
);
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.META_PLATFORM, WIRE_TYPE.LEN, process.platform || "linux"),
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_CWD, WIRE_TYPE.LEN, process.cwd() || "/"),
encodeField(FIELD.META_TIMESTAMP, WIRE_TYPE.LEN, new Date().toISOString())
);
return concatArrays(
encodeField(FIELD.META_PLATFORM, WIRE_TYPE.LEN, process.platform || 'linux'),
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_CWD, WIRE_TYPE.LEN, process.cwd() || '/'),
encodeField(FIELD.META_TIMESTAMP, WIRE_TYPE.LEN, new Date().toISOString())
);
}
/**
* Encode message ID
*/
export function encodeMessageId(
messageId: string,
role: RoleType,
summaryId?: string
): Uint8Array {
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)
);
export function encodeMessageId(messageId: string, role: RoleType, summaryId?: string): Uint8Array {
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
*/
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 || {};
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.MCP_TOOL_NAME, WIRE_TYPE.LEN, toolName)]
: []),
...(toolDesc
? [encodeField(FIELD.MCP_TOOL_DESC, WIRE_TYPE.LEN, toolDesc)]
: []),
...(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")
);
return concatArrays(
...(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
? [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)
*/
export function wrapConnectRPCFrame(
payload: Uint8Array,
compress = false
): Uint8Array {
let finalPayload = payload;
let flags: number = COMPRESS_FLAG.NONE;
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;
}
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);
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;
return frame;
}
+134 -135
View File
@@ -5,201 +5,200 @@
/** Wire types for protobuf encoding */
export const WIRE_TYPE = {
VARINT: 0,
FIXED64: 1,
LEN: 2,
FIXED32: 5,
VARINT: 0,
FIXED64: 1,
LEN: 2,
FIXED32: 5,
} as const;
/** Message role constants */
export const ROLE = {
USER: 1,
ASSISTANT: 2,
USER: 1,
ASSISTANT: 2,
} as const;
/** Unified mode constants */
export const UNIFIED_MODE = {
CHAT: 1,
AGENT: 2,
CHAT: 1,
AGENT: 2,
} as const;
/** Thinking level constants */
export const THINKING_LEVEL = {
UNSPECIFIED: 0,
MEDIUM: 1,
HIGH: 2,
UNSPECIFIED: 0,
MEDIUM: 1,
HIGH: 2,
} as const;
/** Field numbers for all protobuf messages */
export const FIELD = {
// StreamUnifiedChatRequestWithTools (top level)
REQUEST: 1,
// StreamUnifiedChatRequestWithTools (top level)
REQUEST: 1,
// StreamUnifiedChatRequest
MESSAGES: 1,
UNKNOWN_2: 2,
INSTRUCTION: 3,
UNKNOWN_4: 4,
MODEL: 5,
WEB_TOOL: 8,
UNKNOWN_13: 13,
CURSOR_SETTING: 15,
UNKNOWN_19: 19,
CONVERSATION_ID: 23,
METADATA: 26,
IS_AGENTIC: 27,
SUPPORTED_TOOLS: 29,
MESSAGE_IDS: 30,
MCP_TOOLS: 34,
LARGE_CONTEXT: 35,
UNKNOWN_38: 38,
UNIFIED_MODE: 46,
UNKNOWN_47: 47,
SHOULD_DISABLE_TOOLS: 48,
THINKING_LEVEL: 49,
UNKNOWN_51: 51,
UNKNOWN_53: 53,
UNIFIED_MODE_NAME: 54,
// StreamUnifiedChatRequest
MESSAGES: 1,
UNKNOWN_2: 2,
INSTRUCTION: 3,
UNKNOWN_4: 4,
MODEL: 5,
WEB_TOOL: 8,
UNKNOWN_13: 13,
CURSOR_SETTING: 15,
UNKNOWN_19: 19,
CONVERSATION_ID: 23,
METADATA: 26,
IS_AGENTIC: 27,
SUPPORTED_TOOLS: 29,
MESSAGE_IDS: 30,
MCP_TOOLS: 34,
LARGE_CONTEXT: 35,
UNKNOWN_38: 38,
UNIFIED_MODE: 46,
UNKNOWN_47: 47,
SHOULD_DISABLE_TOOLS: 48,
THINKING_LEVEL: 49,
UNKNOWN_51: 51,
UNKNOWN_53: 53,
UNIFIED_MODE_NAME: 54,
// ConversationMessage
MSG_CONTENT: 1,
MSG_ROLE: 2,
MSG_ID: 13,
MSG_TOOL_RESULTS: 18,
MSG_IS_AGENTIC: 29,
MSG_UNIFIED_MODE: 47,
MSG_SUPPORTED_TOOLS: 51,
// ConversationMessage
MSG_CONTENT: 1,
MSG_ROLE: 2,
MSG_ID: 13,
MSG_TOOL_RESULTS: 18,
MSG_IS_AGENTIC: 29,
MSG_UNIFIED_MODE: 47,
MSG_SUPPORTED_TOOLS: 51,
// ConversationMessage.ToolResult
TOOL_RESULT_CALL_ID: 1,
TOOL_RESULT_NAME: 2,
TOOL_RESULT_INDEX: 3,
TOOL_RESULT_RAW_ARGS: 5,
TOOL_RESULT_RESULT: 8,
// ConversationMessage.ToolResult
TOOL_RESULT_CALL_ID: 1,
TOOL_RESULT_NAME: 2,
TOOL_RESULT_INDEX: 3,
TOOL_RESULT_RAW_ARGS: 5,
TOOL_RESULT_RESULT: 8,
// Model
MODEL_NAME: 1,
MODEL_EMPTY: 4,
// Model
MODEL_NAME: 1,
MODEL_EMPTY: 4,
// Instruction
INSTRUCTION_TEXT: 1,
// Instruction
INSTRUCTION_TEXT: 1,
// CursorSetting
SETTING_PATH: 1,
SETTING_UNKNOWN_3: 3,
SETTING_UNKNOWN_6: 6,
SETTING_UNKNOWN_8: 8,
SETTING_UNKNOWN_9: 9,
// CursorSetting
SETTING_PATH: 1,
SETTING_UNKNOWN_3: 3,
SETTING_UNKNOWN_6: 6,
SETTING_UNKNOWN_8: 8,
SETTING_UNKNOWN_9: 9,
// CursorSetting.Unknown6
SETTING6_FIELD_1: 1,
SETTING6_FIELD_2: 2,
// CursorSetting.Unknown6
SETTING6_FIELD_1: 1,
SETTING6_FIELD_2: 2,
// Metadata
META_PLATFORM: 1,
META_ARCH: 2,
META_VERSION: 3,
META_CWD: 4,
META_TIMESTAMP: 5,
// Metadata
META_PLATFORM: 1,
META_ARCH: 2,
META_VERSION: 3,
META_CWD: 4,
META_TIMESTAMP: 5,
// MessageId
MSGID_ID: 1,
MSGID_SUMMARY: 2,
MSGID_ROLE: 3,
// MessageId
MSGID_ID: 1,
MSGID_SUMMARY: 2,
MSGID_ROLE: 3,
// MCPTool
MCP_TOOL_NAME: 1,
MCP_TOOL_DESC: 2,
MCP_TOOL_PARAMS: 3,
MCP_TOOL_SERVER: 4,
// MCPTool
MCP_TOOL_NAME: 1,
MCP_TOOL_DESC: 2,
MCP_TOOL_PARAMS: 3,
MCP_TOOL_SERVER: 4,
// StreamUnifiedChatResponseWithTools (response)
TOOL_CALL: 1,
RESPONSE: 2,
// StreamUnifiedChatResponseWithTools (response)
TOOL_CALL: 1,
RESPONSE: 2,
// ClientSideToolV2Call
TOOL_ID: 3,
TOOL_NAME: 9,
TOOL_RAW_ARGS: 10,
TOOL_IS_LAST: 11,
TOOL_MCP_PARAMS: 27,
// ClientSideToolV2Call
TOOL_ID: 3,
TOOL_NAME: 9,
TOOL_RAW_ARGS: 10,
TOOL_IS_LAST: 11,
TOOL_MCP_PARAMS: 27,
// MCPParams
MCP_TOOLS_LIST: 1,
// MCPParams
MCP_TOOLS_LIST: 1,
// MCPParams.Tool (nested)
MCP_NESTED_NAME: 1,
MCP_NESTED_PARAMS: 3,
// MCPParams.Tool (nested)
MCP_NESTED_NAME: 1,
MCP_NESTED_PARAMS: 3,
// StreamUnifiedChatResponse
RESPONSE_TEXT: 1,
THINKING: 25,
// StreamUnifiedChatResponse
RESPONSE_TEXT: 1,
THINKING: 25,
// Thinking
THINKING_TEXT: 1,
// Thinking
THINKING_TEXT: 1,
} as const;
/** Type definitions */
export type WireType = (typeof WIRE_TYPE)[keyof typeof WIRE_TYPE];
export type RoleType = (typeof ROLE)[keyof typeof ROLE];
export type UnifiedModeType = (typeof UNIFIED_MODE)[keyof typeof UNIFIED_MODE];
export type ThinkingLevelType =
(typeof THINKING_LEVEL)[keyof typeof THINKING_LEVEL];
export type ThinkingLevelType = (typeof THINKING_LEVEL)[keyof typeof THINKING_LEVEL];
export type FieldNumber = (typeof FIELD)[keyof typeof FIELD];
/** Cursor tool definition */
export interface CursorTool {
function?: {
name?: string;
description?: string;
parameters?: Record<string, unknown>;
};
name?: string;
description?: string;
input_schema?: Record<string, unknown>;
function?: {
name?: string;
description?: string;
parameters?: Record<string, unknown>;
};
name?: string;
description?: string;
input_schema?: Record<string, unknown>;
}
/** Cursor tool result */
export interface CursorToolResult {
tool_call_id?: string;
name?: string;
index?: number;
raw_args?: string;
tool_call_id?: string;
name?: string;
index?: number;
raw_args?: string;
}
/** Cursor message format */
export interface CursorMessage {
role: string;
content: string;
tool_results?: CursorToolResult[];
tool_calls?: Array<{
id: string;
type: string;
function: {
name: string;
arguments: string;
};
}>;
role: string;
content: string;
tool_results?: CursorToolResult[];
tool_calls?: Array<{
id: string;
type: string;
function: {
name: string;
arguments: string;
};
}>;
}
/** Formatted message for encoding */
export interface FormattedMessage {
content: string;
role: RoleType;
messageId: string;
isLast: boolean;
hasTools: boolean;
toolResults: CursorToolResult[];
content: string;
role: RoleType;
messageId: string;
isLast: boolean;
hasTools: boolean;
toolResults: CursorToolResult[];
}
/** Message ID structure */
export interface MessageId {
messageId: string;
role: RoleType;
messageId: string;
role: RoleType;
}
/** Compression flags for ConnectRPC frames */
export const COMPRESS_FLAG = {
NONE: 0x00,
GZIP: 0x01,
NONE: 0x00,
GZIP: 0x01,
} as const;
+146 -163
View File
@@ -3,210 +3,193 @@
* Exports encoder/decoder functions and builds complete requests
*/
import { randomUUID } from "crypto";
import { randomUUID } from 'crypto';
import {
ROLE,
UNIFIED_MODE,
THINKING_LEVEL,
FIELD,
type CursorMessage,
type CursorTool,
type FormattedMessage,
type MessageId,
type ThinkingLevelType,
} from "./cursor-protobuf-schema.js";
ROLE,
UNIFIED_MODE,
THINKING_LEVEL,
FIELD,
type CursorMessage,
type CursorTool,
type FormattedMessage,
type MessageId,
type ThinkingLevelType,
} from './cursor-protobuf-schema.js';
import {
encodeField,
encodeVarint,
encodeMessage,
encodeInstruction,
encodeModel,
encodeCursorSetting,
encodeMetadata,
encodeMessageId,
encodeMcpTool,
wrapConnectRPCFrame,
} from "./cursor-protobuf-encoder.js";
encodeField,
encodeVarint,
encodeMessage,
encodeInstruction,
encodeModel,
encodeCursorSetting,
encodeMetadata,
encodeMessageId,
encodeMcpTool,
wrapConnectRPCFrame,
} from './cursor-protobuf-encoder.js';
import {
decodeVarint,
decodeField,
decodeMessage,
parseConnectRPCFrame,
extractTextFromResponse,
} from "./cursor-protobuf-decoder.js";
import { WIRE_TYPE } from "./cursor-protobuf-schema.js";
decodeVarint,
decodeField,
decodeMessage,
parseConnectRPCFrame,
extractTextFromResponse,
} from './cursor-protobuf-decoder.js';
import { WIRE_TYPE } from './cursor-protobuf-schema.js';
/**
* Build complete chat request protobuf
*/
export function encodeRequest(
messages: CursorMessage[],
modelName: string,
tools: CursorTool[] = [],
reasoningEffort: string | null = null
messages: CursorMessage[],
modelName: string,
tools: CursorTool[] = [],
reasoningEffort: string | null = null
): Uint8Array {
const hasTools = tools?.length > 0;
const isAgentic = hasTools;
const formattedMessages: FormattedMessage[] = [];
const messageIds: MessageId[] = [];
const hasTools = tools?.length > 0;
const isAgentic = hasTools;
const formattedMessages: FormattedMessage[] = [];
const messageIds: MessageId[] = [];
// Prepare messages
for (let i = 0; i < messages.length; i++) {
const msg = messages[i];
const role = msg.role === "user" ? ROLE.USER : ROLE.ASSISTANT;
const msgId = randomUUID();
const isLast = i === messages.length - 1;
// Prepare messages
for (let i = 0; i < messages.length; i++) {
const msg = messages[i];
const role = msg.role === 'user' ? ROLE.USER : ROLE.ASSISTANT;
const msgId = randomUUID();
const isLast = i === messages.length - 1;
formattedMessages.push({
content: msg.content,
role,
messageId: msgId,
isLast,
hasTools,
toolResults: msg.tool_results || [],
});
formattedMessages.push({
content: msg.content,
role,
messageId: msgId,
isLast,
hasTools,
toolResults: msg.tool_results || [],
});
messageIds.push({ messageId: msgId, role });
}
messageIds.push({ messageId: msgId, role });
}
// Map reasoning effort to thinking level
let thinkingLevel: ThinkingLevelType = THINKING_LEVEL.UNSPECIFIED;
if (reasoningEffort === "medium") thinkingLevel = THINKING_LEVEL.MEDIUM;
else if (reasoningEffort === "high") thinkingLevel = THINKING_LEVEL.HIGH;
// Map reasoning effort to thinking level
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
const messageFields = formattedMessages.map((fm) =>
encodeField(
FIELD.MESSAGES,
WIRE_TYPE.LEN,
encodeMessage(
fm.content,
fm.role,
fm.messageId,
fm.isLast,
fm.hasTools,
fm.toolResults
)
)
);
// Build arrays for messages and tools
const messageFields = formattedMessages.map((fm) =>
encodeField(
FIELD.MESSAGES,
WIRE_TYPE.LEN,
encodeMessage(fm.content, fm.role, fm.messageId, fm.isLast, fm.hasTools, fm.toolResults)
)
);
const messageIdFields = messageIds.map((mid) =>
encodeField(
FIELD.MESSAGE_IDS,
WIRE_TYPE.LEN,
encodeMessageId(mid.messageId, mid.role)
)
);
const messageIdFields = messageIds.map((mid) =>
encodeField(FIELD.MESSAGE_IDS, WIRE_TYPE.LEN, encodeMessageId(mid.messageId, mid.role))
);
const toolFields =
tools?.length > 0
? tools.map((tool) =>
encodeField(FIELD.MCP_TOOLS, WIRE_TYPE.LEN, encodeMcpTool(tool))
)
: [];
const toolFields =
tools?.length > 0
? tools.map((tool) => encodeField(FIELD.MCP_TOOLS, WIRE_TYPE.LEN, encodeMcpTool(tool)))
: [];
const supportedToolsField = isAgentic
? [encodeField(FIELD.SUPPORTED_TOOLS, WIRE_TYPE.LEN, encodeVarint(1))]
: [];
const supportedToolsField = isAgentic
? [encodeField(FIELD.SUPPORTED_TOOLS, WIRE_TYPE.LEN, encodeVarint(1))]
: [];
// 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"
),
];
// 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);
return concatArrays(...parts);
}
/**
* Build chat request wrapped in top-level message
*/
export function buildChatRequest(
messages: CursorMessage[],
modelName: string,
tools: CursorTool[] = [],
reasoningEffort: string | null = null
messages: CursorMessage[],
modelName: string,
tools: CursorTool[] = [],
reasoningEffort: string | null = null
): Uint8Array {
return encodeField(
FIELD.REQUEST,
WIRE_TYPE.LEN,
encodeRequest(messages, modelName, tools, reasoningEffort)
);
return encodeField(
FIELD.REQUEST,
WIRE_TYPE.LEN,
encodeRequest(messages, modelName, tools, reasoningEffort)
);
}
/**
* Generate complete Cursor request body with ConnectRPC framing
*/
export function generateCursorBody(
messages: CursorMessage[],
modelName: string,
tools: CursorTool[] = [],
reasoningEffort: string | null = null
messages: CursorMessage[],
modelName: string,
tools: CursorTool[] = [],
reasoningEffort: string | null = null
): Uint8Array {
const protobuf = buildChatRequest(messages, modelName, tools, reasoningEffort);
const framed = wrapConnectRPCFrame(protobuf, false); // Cursor doesn't support compressed requests
return framed;
const protobuf = buildChatRequest(messages, modelName, tools, reasoningEffort);
const framed = wrapConnectRPCFrame(protobuf, false); // Cursor doesn't support compressed requests
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;
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
export {
encodeVarint,
encodeField,
encodeMessage,
encodeInstruction,
encodeModel,
encodeCursorSetting,
encodeMetadata,
encodeMessageId,
encodeMcpTool,
wrapConnectRPCFrame,
decodeVarint,
decodeField,
decodeMessage,
parseConnectRPCFrame,
extractTextFromResponse,
encodeVarint,
encodeField,
encodeMessage,
encodeInstruction,
encodeModel,
encodeCursorSetting,
encodeMetadata,
encodeMessageId,
encodeMcpTool,
wrapConnectRPCFrame,
decodeVarint,
decodeField,
decodeMessage,
parseConnectRPCFrame,
extractTextFromResponse,
};
+96 -100
View File
@@ -3,30 +3,26 @@
* Converts OpenAI messages to Cursor format
*/
import type {
CursorMessage,
CursorToolResult,
CursorTool,
} from "./cursor-protobuf-schema.js";
import type { CursorMessage, CursorToolResult, CursorTool } from './cursor-protobuf-schema.js';
/** OpenAI message format */
interface OpenAIMessage {
role: string;
content: string | Array<{ type: string; text?: string }>;
name?: string;
tool_call_id?: string;
tool_calls?: Array<{
id: string;
type: string;
function: { name: string; arguments: string };
}>;
role: string;
content: string | Array<{ type: string; text?: string }>;
name?: string;
tool_call_id?: string;
tool_calls?: Array<{
id: string;
type: string;
function: { name: string; arguments: string };
}>;
}
/** OpenAI request body */
interface OpenAIRequestBody {
messages: OpenAIMessage[];
tools?: CursorTool[];
reasoning_effort?: string;
messages: OpenAIMessage[];
tools?: CursorTool[];
reasoning_effort?: string;
}
/**
@@ -36,91 +32,91 @@ interface OpenAIRequestBody {
* - assistant with tool_calls → keep tool_calls structure (Cursor supports it natively)
*/
function convertMessages(messages: OpenAIMessage[]): CursorMessage[] {
const result: CursorMessage[] = [];
let pendingToolResults: CursorToolResult[] = [];
const result: CursorMessage[] = [];
let pendingToolResults: CursorToolResult[] = [];
for (let i = 0; i < messages.length; i++) {
const msg = messages[i];
for (let i = 0; i < messages.length; i++) {
const msg = messages[i];
if (msg.role === "system") {
result.push({
role: "user",
content: `[System Instructions]\n${msg.content}`,
});
continue;
}
if (msg.role === 'system') {
result.push({
role: 'user',
content: `[System Instructions]\n${msg.content}`,
});
continue;
}
if (msg.role === "tool") {
let toolContent = "";
if (typeof msg.content === "string") {
toolContent = msg.content;
} else if (Array.isArray(msg.content)) {
for (const part of msg.content) {
if (part.type === "text" && part.text) {
toolContent += part.text;
}
}
}
if (msg.role === 'tool') {
let toolContent = '';
if (typeof msg.content === 'string') {
toolContent = msg.content;
} else if (Array.isArray(msg.content)) {
for (const part of msg.content) {
if (part.type === 'text' && part.text) {
toolContent += part.text;
}
}
}
const toolName = msg.name || "tool";
const toolCallId = msg.tool_call_id || "";
const toolName = msg.name || 'tool';
const toolCallId = msg.tool_call_id || '';
// Accumulate tool result
pendingToolResults.push({
tool_call_id: toolCallId,
name: toolName,
index: pendingToolResults.length,
raw_args: toolContent,
});
continue;
}
// Accumulate tool result
pendingToolResults.push({
tool_call_id: toolCallId,
name: toolName,
index: pendingToolResults.length,
raw_args: toolContent,
});
continue;
}
if (msg.role === "user" || msg.role === "assistant") {
let content = "";
if (msg.role === 'user' || msg.role === 'assistant') {
let content = '';
if (typeof msg.content === "string") {
content = msg.content;
} else if (Array.isArray(msg.content)) {
for (const part of msg.content) {
if (part.type === "text" && part.text) {
content += part.text;
}
}
}
if (typeof msg.content === 'string') {
content = msg.content;
} else if (Array.isArray(msg.content)) {
for (const part of msg.content) {
if (part.type === 'text' && part.text) {
content += part.text;
}
}
}
// Keep tool_calls structure for assistant messages
if (msg.role === "assistant" && msg.tool_calls && msg.tool_calls.length > 0) {
const assistantMsg: CursorMessage = { role: "assistant", content: "" };
if (content) {
assistantMsg.content = content;
}
assistantMsg.tool_calls = msg.tool_calls;
// Keep tool_calls structure for assistant messages
if (msg.role === 'assistant' && msg.tool_calls && msg.tool_calls.length > 0) {
const assistantMsg: CursorMessage = { role: 'assistant', content: '' };
if (content) {
assistantMsg.content = content;
}
assistantMsg.tool_calls = msg.tool_calls;
// Attach pending tool results to assistant message with tool_calls
if (pendingToolResults.length > 0) {
assistantMsg.tool_results = pendingToolResults;
pendingToolResults = [];
}
// Attach pending tool results to assistant message with tool_calls
if (pendingToolResults.length > 0) {
assistantMsg.tool_results = pendingToolResults;
pendingToolResults = [];
}
result.push(assistantMsg);
} else if (content || pendingToolResults.length > 0) {
const msgObj: CursorMessage = {
role: msg.role,
content: content || "",
};
result.push(assistantMsg);
} else if (content || pendingToolResults.length > 0) {
const msgObj: CursorMessage = {
role: msg.role,
content: content || '',
};
// Attach pending tool results to this message
if (pendingToolResults.length > 0) {
msgObj.tool_results = pendingToolResults;
pendingToolResults = [];
}
// Attach pending tool results to this message
if (pendingToolResults.length > 0) {
msgObj.tool_results = 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
*/
export function buildCursorRequest(
model: string,
body: OpenAIRequestBody,
stream: boolean,
credentials: unknown
model: string,
body: OpenAIRequestBody,
stream: boolean,
credentials: unknown
): {
messages: CursorMessage[];
tools?: CursorTool[];
messages: CursorMessage[];
tools?: CursorTool[];
} {
const messages = convertMessages(body.messages || []);
const messages = convertMessages(body.messages || []);
return {
...body,
messages,
};
return {
...body,
messages,
};
}
+1 -6
View File
@@ -8,12 +8,7 @@
export * from './types';
// Auth
export {
autoDetectTokens,
saveCredentials,
loadCredentials,
checkAuthStatus,
} from './cursor-auth';
export { autoDetectTokens, saveCredentials, loadCredentials, checkAuthStatus } from './cursor-auth';
// Daemon
export { isDaemonRunning, getDaemonStatus, startDaemon, stopDaemon } from './cursor-daemon';