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 committed 2026-02-11 19:06:59 +07:00
1 parent aaa31c6427
commit fe97d720d4
8 files changed
+208 -288

No files matched your search

+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,
+102 -117
View File
@@ -3,12 +3,12 @@
* Handles HTTP/2 requests to Cursor API with protobuf encoding/decoding
*/
import * as crypto from "crypto";
import * as zlib from "zlib";
import type { IncomingHttpHeaders } from "http";
import { generateCursorBody, extractTextFromResponse } from "./cursor-protobuf.js";
import { buildCursorRequest } from "./cursor-translator.js";
import type { CursorMessage, CursorTool } from "./cursor-protobuf-schema.js";
import * as crypto from 'crypto';
import * as zlib from 'zlib';
import type { IncomingHttpHeaders } from 'http';
import { generateCursorBody, extractTextFromResponse } from './cursor-protobuf.js';
import { buildCursorRequest } from './cursor-translator.js';
import type { CursorMessage, CursorTool } from './cursor-protobuf-schema.js';
/** Compression flags for response parsing */
const COMPRESS_FLAG = {
@@ -59,10 +59,10 @@ interface Http2Response {
/** Detect cloud environment */
function isCloudEnv(): boolean {
if (typeof caches !== "undefined" && typeof caches === "object") return true;
if (typeof caches !== 'undefined' && typeof caches === 'object') return true;
try {
// Check for EdgeRuntime without causing compilation error
if (typeof (globalThis as { EdgeRuntime?: string }).EdgeRuntime !== "undefined") return true;
if (typeof (globalThis as { EdgeRuntime?: string }).EdgeRuntime !== 'undefined') return true;
} catch {
// Continue
}
@@ -70,12 +70,12 @@ function isCloudEnv(): boolean {
}
/** Lazy import http2 */
let http2Module: typeof import("http2") | null = null;
let http2Module: typeof import('http2') | null = null;
async function getHttp2() {
if (http2Module) return http2Module;
if (!isCloudEnv()) {
try {
http2Module = await import("http2");
http2Module = await import('http2');
return http2Module;
} catch {
return null;
@@ -91,7 +91,7 @@ function decompressPayload(payload: Buffer, flags: number): Buffer {
// Check if payload is JSON error
if (payload.length > 10 && payload[0] === 0x7b && payload[1] === 0x22) {
try {
const text = payload.toString("utf-8");
const text = payload.toString('utf-8');
if (text.startsWith('{"error"')) {
return payload;
}
@@ -128,28 +128,28 @@ function createErrorResponse(jsonError: {
jsonError?.error?.details?.[0]?.debug?.details?.title ||
jsonError?.error?.details?.[0]?.debug?.details?.detail ||
jsonError?.error?.message ||
"API Error";
'API Error';
const isRateLimit = jsonError?.error?.code === "resource_exhausted";
const isRateLimit = jsonError?.error?.code === 'resource_exhausted';
return new Response(
JSON.stringify({
error: {
message: errorMsg,
type: isRateLimit ? "rate_limit_error" : "api_error",
code: jsonError?.error?.details?.[0]?.debug?.error || "unknown",
type: isRateLimit ? 'rate_limit_error' : 'api_error',
code: jsonError?.error?.details?.[0]?.debug?.error || 'unknown',
},
}),
{
status: isRateLimit ? 429 : 400,
headers: { "Content-Type": "application/json" },
headers: { 'Content-Type': 'application/json' },
}
);
}
export class CursorExecutor {
private readonly baseUrl = "https://api2.cursor.sh";
private readonly chatPath = "/aiserver.v1.AiService/StreamChat";
private readonly baseUrl = 'https://api2.cursor.sh';
private readonly chatPath = '/aiserver.v1.AiService/StreamChat';
buildUrl(): string {
return `${this.baseUrl}${this.chatPath}`;
@@ -175,8 +175,8 @@ export class CursorExecutor {
t = byteArray[i];
}
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
let encoded = "";
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
let encoded = '';
for (let i = 0; i < byteArray.length; i += 3) {
const a = byteArray[i];
@@ -203,48 +203,41 @@ export class CursorExecutor {
const ghostMode = credentials.providerSpecificData?.ghostMode !== false;
if (!machineId) {
throw new Error("Machine ID is required for Cursor API");
throw new Error('Machine ID is required for Cursor API');
}
const cleanToken = accessToken.includes("::")
? accessToken.split("::")[1]
: accessToken;
const cleanToken = accessToken.includes('::') ? accessToken.split('::')[1] : accessToken;
return {
authorization: `Bearer ${cleanToken}`,
"connect-accept-encoding": "gzip",
"connect-protocol-version": "1",
"content-type": "application/connect+proto",
"user-agent": "connect-es/1.6.1",
"x-amzn-trace-id": `Root=${crypto.randomUUID()}`,
"x-client-key": crypto.createHash("sha256").update(cleanToken).digest("hex"),
"x-cursor-checksum": this.generateChecksum(machineId),
"x-cursor-client-version": "2.3.41",
"x-cursor-client-type": "ide",
"x-cursor-client-os":
process.platform === "win32"
? "windows"
: process.platform === "darwin"
? "macos"
: "linux",
"x-cursor-client-arch": process.arch === "arm64" ? "aarch64" : "x64",
"x-cursor-client-device-type": "desktop",
"x-cursor-config-version": crypto.randomUUID(),
"x-cursor-timezone":
Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC",
"x-ghost-mode": ghostMode ? "true" : "false",
"x-request-id": crypto.randomUUID(),
"x-session-id": crypto
.createHash("sha256")
.update(cleanToken)
.digest("hex")
.substring(0, 36),
'connect-accept-encoding': 'gzip',
'connect-protocol-version': '1',
'content-type': 'application/connect+proto',
'user-agent': 'connect-es/1.6.1',
'x-amzn-trace-id': `Root=${crypto.randomUUID()}`,
'x-client-key': crypto.createHash('sha256').update(cleanToken).digest('hex'),
'x-cursor-checksum': this.generateChecksum(machineId),
'x-cursor-client-version': '2.3.41',
'x-cursor-client-type': 'ide',
'x-cursor-client-os':
process.platform === 'win32'
? 'windows'
: process.platform === 'darwin'
? 'macos'
: 'linux',
'x-cursor-client-arch': process.arch === 'arm64' ? 'aarch64' : 'x64',
'x-cursor-client-device-type': 'desktop',
'x-cursor-config-version': crypto.randomUUID(),
'x-cursor-timezone': Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC',
'x-ghost-mode': ghostMode ? 'true' : 'false',
'x-request-id': crypto.randomUUID(),
'x-session-id': crypto.createHash('sha256').update(cleanToken).digest('hex').substring(0, 36),
};
}
transformRequest(
model: string,
body: ExecutorParams["body"],
body: ExecutorParams['body'],
stream: boolean,
credentials: CursorCredentials
): Uint8Array {
@@ -262,7 +255,7 @@ export class CursorExecutor {
signal?: AbortSignal
): Promise<Http2Response> {
const response = await fetch(url, {
method: "POST",
method: 'POST',
headers,
body,
signal,
@@ -288,7 +281,7 @@ export class CursorExecutor {
): Promise<Http2Response> {
const http2 = await getHttp2();
if (!http2) {
throw new Error("http2 module not available");
throw new Error('http2 module not available');
}
return new Promise((resolve, reject) => {
@@ -297,40 +290,40 @@ export class CursorExecutor {
const chunks: Buffer[] = [];
let responseHeaders: IncomingHttpHeaders = {};
client.on("error", reject);
client.on('error', reject);
const req = client.request({
":method": "POST",
":path": urlObj.pathname,
":authority": urlObj.host,
":scheme": "https",
':method': 'POST',
':path': urlObj.pathname,
':authority': urlObj.host,
':scheme': 'https',
...headers,
});
req.on("response", (hdrs) => {
req.on('response', (hdrs) => {
responseHeaders = hdrs;
});
req.on("data", (chunk: Buffer) => {
req.on('data', (chunk: Buffer) => {
chunks.push(chunk);
});
req.on("end", () => {
req.on('end', () => {
client.close();
resolve({
status: Number(responseHeaders[":status"]),
status: Number(responseHeaders[':status']),
headers: responseHeaders,
body: Buffer.concat(chunks),
});
});
req.on("error", (err) => {
req.on('error', (err) => {
client.close();
reject(err);
});
if (signal) {
signal.addEventListener("abort", () => {
signal.addEventListener('abort', () => {
req.close();
client.close();
reject(new Error("Request aborted"));
reject(new Error('Request aborted'));
});
}
@@ -343,7 +336,7 @@ export class CursorExecutor {
response: Response;
url: string;
headers: Record<string, string>;
transformedBody: ExecutorParams["body"];
transformedBody: ExecutorParams['body'];
}> {
const { model, body, stream, credentials, signal } = params;
const url = this.buildUrl();
@@ -357,18 +350,18 @@ export class CursorExecutor {
: await this.makeFetchRequest(url, headers, transformedBody, signal);
if (response.status !== 200) {
const errorText = response.body?.toString() || "Unknown error";
const errorText = response.body?.toString() || 'Unknown error';
const errorResponse = new Response(
JSON.stringify({
error: {
message: `[${response.status}]: ${errorText}`,
type: "invalid_request_error",
code: "",
type: 'invalid_request_error',
code: '',
},
}),
{
status: response.status,
headers: { "Content-Type": "application/json" },
headers: { 'Content-Type': 'application/json' },
}
);
return { response: errorResponse, url, headers, transformedBody: body };
@@ -385,29 +378,25 @@ export class CursorExecutor {
JSON.stringify({
error: {
message: (error as Error).message,
type: "connection_error",
code: "",
type: 'connection_error',
code: '',
},
}),
{
status: 500,
headers: { "Content-Type": "application/json" },
headers: { 'Content-Type': 'application/json' },
}
);
return { response: errorResponse, url, headers, transformedBody: body };
}
}
transformProtobufToJSON(
buffer: Buffer,
model: string,
body: ExecutorParams["body"]
): Response {
transformProtobufToJSON(buffer: Buffer, model: string, body: ExecutorParams['body']): Response {
const responseId = `chatcmpl-cursor-${Date.now()}`;
const created = Math.floor(Date.now() / 1000);
let offset = 0;
let totalContent = "";
let totalContent = '';
const toolCalls: Array<{
id: string;
type: string;
@@ -438,8 +427,8 @@ export class CursorExecutor {
payload = decompressPayload(payload, flags);
try {
const text = payload.toString("utf-8");
if (text.startsWith("{") && text.includes('"error"')) {
const text = payload.toString('utf-8');
if (text.startsWith('{') && text.includes('"error"')) {
return createErrorResponse(JSON.parse(text));
}
} catch {
@@ -453,13 +442,13 @@ export class CursorExecutor {
JSON.stringify({
error: {
message: result.error,
type: "rate_limit_error",
code: "rate_limited",
type: 'rate_limit_error',
code: 'rate_limited',
},
}),
{
status: 429,
headers: { "Content-Type": "application/json" },
headers: { 'Content-Type': 'application/json' },
}
);
}
@@ -518,7 +507,7 @@ export class CursorExecutor {
function: { name: string; arguments: string };
}>;
} = {
role: "assistant",
role: 'assistant',
content: totalContent || null,
};
@@ -528,14 +517,14 @@ export class CursorExecutor {
const completion = {
id: responseId,
object: "chat.completion",
object: 'chat.completion',
created,
model,
choices: [
{
index: 0,
message,
finish_reason: toolCalls.length > 0 ? "tool_calls" : "stop",
finish_reason: toolCalls.length > 0 ? 'tool_calls' : 'stop',
},
],
usage: {
@@ -547,21 +536,17 @@ export class CursorExecutor {
return new Response(JSON.stringify(completion), {
status: 200,
headers: { "Content-Type": "application/json" },
headers: { 'Content-Type': 'application/json' },
});
}
transformProtobufToSSE(
buffer: Buffer,
model: string,
body: ExecutorParams["body"]
): Response {
transformProtobufToSSE(buffer: Buffer, model: string, body: ExecutorParams['body']): Response {
const responseId = `chatcmpl-cursor-${Date.now()}`;
const created = Math.floor(Date.now() / 1000);
const chunks: string[] = [];
let offset = 0;
let totalContent = "";
let totalContent = '';
const toolCalls: Array<{
id: string;
type: string;
@@ -593,8 +578,8 @@ export class CursorExecutor {
payload = decompressPayload(payload, flags);
try {
const text = payload.toString("utf-8");
if (text.startsWith("{") && text.includes('"error"')) {
const text = payload.toString('utf-8');
if (text.startsWith('{') && text.includes('"error"')) {
return createErrorResponse(JSON.parse(text));
}
} catch {
@@ -608,13 +593,13 @@ export class CursorExecutor {
JSON.stringify({
error: {
message: result.error,
type: "rate_limit_error",
code: "rate_limited",
type: 'rate_limit_error',
code: 'rate_limited',
},
}),
{
status: 429,
headers: { "Content-Type": "application/json" },
headers: { 'Content-Type': 'application/json' },
}
);
}
@@ -626,13 +611,13 @@ export class CursorExecutor {
chunks.push(
`data: ${JSON.stringify({
id: responseId,
object: "chat.completion.chunk",
object: 'chat.completion.chunk',
created,
model,
choices: [
{
index: 0,
delta: { role: "assistant", content: "" },
delta: { role: 'assistant', content: '' },
finish_reason: null,
},
],
@@ -649,7 +634,7 @@ export class CursorExecutor {
chunks.push(
`data: ${JSON.stringify({
id: responseId,
object: "chat.completion.chunk",
object: 'chat.completion.chunk',
created,
model,
choices: [
@@ -660,7 +645,7 @@ export class CursorExecutor {
{
index: existing.index,
id: tc.id,
type: "function",
type: 'function',
function: {
name: tc.function.name,
arguments: tc.function.arguments,
@@ -682,7 +667,7 @@ export class CursorExecutor {
chunks.push(
`data: ${JSON.stringify({
id: responseId,
object: "chat.completion.chunk",
object: 'chat.completion.chunk',
created,
model,
choices: [
@@ -693,7 +678,7 @@ export class CursorExecutor {
{
index: toolCallIndex,
id: tc.id,
type: "function",
type: 'function',
function: {
name: tc.function.name,
arguments: tc.function.arguments,
@@ -714,7 +699,7 @@ export class CursorExecutor {
chunks.push(
`data: ${JSON.stringify({
id: responseId,
object: "chat.completion.chunk",
object: 'chat.completion.chunk',
created,
model,
choices: [
@@ -722,7 +707,7 @@ export class CursorExecutor {
index: 0,
delta:
chunks.length === 0 && toolCalls.length === 0
? { role: "assistant", content: result.text }
? { role: 'assistant', content: result.text }
: { content: result.text },
finish_reason: null,
},
@@ -736,13 +721,13 @@ export class CursorExecutor {
chunks.push(
`data: ${JSON.stringify({
id: responseId,
object: "chat.completion.chunk",
object: 'chat.completion.chunk',
created,
model,
choices: [
{
index: 0,
delta: { role: "assistant", content: "" },
delta: { role: 'assistant', content: '' },
finish_reason: null,
},
],
@@ -753,14 +738,14 @@ export class CursorExecutor {
chunks.push(
`data: ${JSON.stringify({
id: responseId,
object: "chat.completion.chunk",
object: 'chat.completion.chunk',
created,
model,
choices: [
{
index: 0,
delta: {},
finish_reason: toolCalls.length > 0 ? "tool_calls" : "stop",
finish_reason: toolCalls.length > 0 ? 'tool_calls' : 'stop',
},
],
usage: {
@@ -770,14 +755,14 @@ export class CursorExecutor {
},
})}\n\n`
);
chunks.push("data: [DONE]\n\n");
chunks.push('data: [DONE]\n\n');
return new Response(chunks.join(""), {
return new Response(chunks.join(''), {
status: 200,
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
},
});
}
+23 -49
View File
@@ -3,21 +3,14 @@
* 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] {
export function decodeVarint(buffer: Uint8Array, offset: number): [number, number] {
let result = 0;
let shift = 0;
let pos = offset;
@@ -77,10 +70,7 @@ export function decodeField(
export function decodeMessage(
data: Uint8Array
): Map<number, Array<{ wireType: WireType; value: Uint8Array | number }>> {
const fields = new Map<
number,
Array<{ wireType: WireType; value: Uint8Array | number }>
>();
const fields = new Map<number, Array<{ wireType: WireType; value: Uint8Array | number }>>();
let pos = 0;
while (pos < data.length) {
@@ -110,8 +100,7 @@ export function parseConnectRPCFrame(buffer: Buffer): {
if (buffer.length < 5) return null;
const flags = buffer[0];
const length =
(buffer[1] << 24) | (buffer[2] << 16) | (buffer[3] << 8) | buffer[4];
const length = (buffer[1] << 24) | (buffer[2] << 16) | (buffer[3] << 8) | buffer[4];
if (buffer.length < 5 + length) return null;
@@ -144,24 +133,20 @@ function extractToolCall(toolCallData: Uint8Array): {
isLast: boolean;
} | null {
const toolCall = decodeMessage(toolCallData);
let toolCallId = "";
let toolName = "";
let rawArgs = "";
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
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
);
toolName = new TextDecoder().decode(toolCall.get(FIELD.TOOL_NAME)![0].value as Uint8Array);
}
// Extract is_last flag
@@ -172,14 +157,10 @@ function extractToolCall(toolCallData: 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
);
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
);
const tool = decodeMessage(mcpParams.get(FIELD.MCP_TOOLS_LIST)![0].value as Uint8Array);
if (tool.has(FIELD.MCP_NESTED_NAME)) {
toolName = new TextDecoder().decode(
@@ -200,18 +181,16 @@ function extractToolCall(toolCallData: 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
);
rawArgs = new TextDecoder().decode(toolCall.get(FIELD.TOOL_RAW_ARGS)![0].value as Uint8Array);
}
if (toolCallId && toolName) {
return {
id: toolCallId,
type: "function",
type: 'function',
function: {
name: toolName,
arguments: rawArgs || "{}",
arguments: rawArgs || '{}',
},
isLast,
};
@@ -223,26 +202,23 @@ function extractToolCall(toolCallData: Uint8Array): {
/**
* Extract text and thinking from response data
*/
function extractTextAndThinking(
responseData: Uint8Array
): { text: string | null; thinking: string | 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
);
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
);
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
@@ -275,9 +251,7 @@ export function extractTextFromResponse(payload: Uint8Array): {
// Field 1: ClientSideToolV2Call
if (fields.has(FIELD.TOOL_CALL)) {
const toolCall = extractToolCall(
fields.get(FIELD.TOOL_CALL)![0].value as Uint8Array
);
const toolCall = extractToolCall(fields.get(FIELD.TOOL_CALL)![0].value as Uint8Array);
if (toolCall) {
return { text: null, error: null, toolCall, thinking: null };
}
+24 -55
View File
@@ -3,8 +3,8 @@
* 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,
@@ -20,7 +20,7 @@ import {
type CursorMessage,
type FormattedMessage,
type MessageId,
} from "./cursor-protobuf-schema.js";
} from './cursor-protobuf-schema.js';
/**
* Encode a varint (variable-length integer)
@@ -54,7 +54,7 @@ export function encodeField(
if (wireType === WIRE_TYPE.LEN) {
const dataBytes =
typeof value === "string"
typeof value === 'string'
? new TextEncoder().encode(value)
: value instanceof Uint8Array
? value
@@ -85,10 +85,10 @@ function concatArrays(...arrays: Uint8Array[]): Uint8Array {
* Encode a tool result
*/
export function encodeToolResult(toolResult: CursorToolResult): Uint8Array {
const toolCallId = toolResult.tool_call_id || "";
const toolName = toolResult.name || "";
const toolCallId = toolResult.tool_call_id || '';
const toolName = toolResult.name || '';
const toolIndex = toolResult.index || 0;
const rawArgs = toolResult.raw_args || "{}";
const rawArgs = toolResult.raw_args || '{}';
return concatArrays(
encodeField(FIELD.TOOL_RESULT_CALL_ID, WIRE_TYPE.LEN, toolCallId),
@@ -115,11 +115,7 @@ export function encodeMessage(
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_TOOL_RESULTS, WIRE_TYPE.LEN, encodeToolResult(tr))
)
: []),
encodeField(FIELD.MSG_IS_AGENTIC, WIRE_TYPE.VARINT, hasTools ? 1 : 0),
@@ -129,13 +125,7 @@ export function encodeMessage(
hasTools ? UNIFIED_MODE.AGENT : UNIFIED_MODE.CHAT
),
...(isLast && hasTools
? [
encodeField(
FIELD.MSG_SUPPORTED_TOOLS,
WIRE_TYPE.LEN,
encodeVarint(1)
),
]
? [encodeField(FIELD.MSG_SUPPORTED_TOOLS, WIRE_TYPE.LEN, encodeVarint(1))]
: [])
);
}
@@ -144,9 +134,7 @@ export function encodeMessage(
* 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);
}
/**
@@ -169,7 +157,7 @@ export function encodeCursorSetting(): Uint8Array {
);
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_6, WIRE_TYPE.LEN, unknown6),
encodeField(FIELD.SETTING_UNKNOWN_8, WIRE_TYPE.VARINT, 1),
@@ -182,10 +170,10 @@ export function encodeCursorSetting(): Uint8Array {
*/
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_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())
);
}
@@ -193,16 +181,10 @@ export function encodeMetadata(): Uint8Array {
/**
* Encode message ID
*/
export function encodeMessageId(
messageId: string,
role: RoleType,
summaryId?: string
): Uint8Array {
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)]
: []),
...(summaryId ? [encodeField(FIELD.MSGID_SUMMARY, WIRE_TYPE.LEN, summaryId)] : []),
encodeField(FIELD.MSGID_ROLE, WIRE_TYPE.VARINT, role)
);
}
@@ -211,37 +193,24 @@ export function encodeMessageId(
* Encode MCP tool
*/
export function encodeMcpTool(tool: CursorTool): Uint8Array {
const toolName = tool.function?.name || tool.name || "";
const toolDesc = tool.function?.description || tool.description || "";
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)]
: []),
...(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_PARAMS, WIRE_TYPE.LEN, JSON.stringify(inputSchema))]
: []),
encodeField(FIELD.MCP_TOOL_SERVER, WIRE_TYPE.LEN, "custom")
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 {
export function wrapConnectRPCFrame(payload: Uint8Array, compress = false): Uint8Array {
let finalPayload = payload;
let flags: number = COMPRESS_FLAG.NONE;
+1 -2
View File
@@ -143,8 +143,7 @@ export const FIELD = {
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 */
+15 -32
View File
@@ -3,7 +3,7 @@
* Exports encoder/decoder functions and builds complete requests
*/
import { randomUUID } from "crypto";
import { randomUUID } from 'crypto';
import {
ROLE,
UNIFIED_MODE,
@@ -14,7 +14,7 @@ import {
type FormattedMessage,
type MessageId,
type ThinkingLevelType,
} from "./cursor-protobuf-schema.js";
} from './cursor-protobuf-schema.js';
import {
encodeField,
encodeVarint,
@@ -26,15 +26,15 @@ import {
encodeMessageId,
encodeMcpTool,
wrapConnectRPCFrame,
} from "./cursor-protobuf-encoder.js";
} from './cursor-protobuf-encoder.js';
import {
decodeVarint,
decodeField,
decodeMessage,
parseConnectRPCFrame,
extractTextFromResponse,
} from "./cursor-protobuf-decoder.js";
import { WIRE_TYPE } from "./cursor-protobuf-schema.js";
} from './cursor-protobuf-decoder.js';
import { WIRE_TYPE } from './cursor-protobuf-schema.js';
/**
* Build complete chat request protobuf
@@ -53,7 +53,7 @@ export function encodeRequest(
// Prepare messages
for (let i = 0; i < messages.length; 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 isLast = i === messages.length - 1;
@@ -71,38 +71,25 @@ export function encodeRequest(
// 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;
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
)
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)
)
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))
)
? tools.map((tool) => encodeField(FIELD.MCP_TOOLS, WIRE_TYPE.LEN, encodeMcpTool(tool)))
: [];
const supportedToolsField = isAgentic
@@ -113,10 +100,10 @@ export function encodeRequest(
const parts: Uint8Array[] = [
...messageFields,
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.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.CURSOR_SETTING, WIRE_TYPE.LEN, encodeCursorSetting()),
encodeField(FIELD.UNKNOWN_19, WIRE_TYPE.VARINT, 1),
@@ -133,16 +120,12 @@ export function encodeRequest(
WIRE_TYPE.VARINT,
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.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"
),
encodeField(FIELD.UNIFIED_MODE_NAME, WIRE_TYPE.LEN, isAgentic ? 'Agent' : 'Ask'),
];
return concatArrays(...parts);
+16 -20
View File
@@ -3,11 +3,7 @@
* 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 {
@@ -42,28 +38,28 @@ function convertMessages(messages: OpenAIMessage[]): CursorMessage[] {
for (let i = 0; i < messages.length; i++) {
const msg = messages[i];
if (msg.role === "system") {
if (msg.role === 'system') {
result.push({
role: "user",
role: 'user',
content: `[System Instructions]\n${msg.content}`,
});
continue;
}
if (msg.role === "tool") {
let toolContent = "";
if (typeof msg.content === "string") {
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) {
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({
@@ -75,22 +71,22 @@ function convertMessages(messages: OpenAIMessage[]): CursorMessage[] {
continue;
}
if (msg.role === "user" || msg.role === "assistant") {
let content = "";
if (msg.role === 'user' || msg.role === 'assistant') {
let content = '';
if (typeof msg.content === "string") {
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) {
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 (msg.role === 'assistant' && msg.tool_calls && msg.tool_calls.length > 0) {
const assistantMsg: CursorMessage = { role: 'assistant', content: '' };
if (content) {
assistantMsg.content = content;
}
@@ -106,7 +102,7 @@ function convertMessages(messages: OpenAIMessage[]): CursorMessage[] {
} else if (content || pendingToolResults.length > 0) {
const msgObj: CursorMessage = {
role: msg.role,
content: content || "",
content: content || '',
};
// Attach pending tool results to this message
+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';