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