diff --git a/src/cursor/cursor-daemon.ts b/src/cursor/cursor-daemon.ts index 0166be41..9fe6681c 100644 --- a/src/cursor/cursor-daemon.ts +++ b/src/cursor/cursor-daemon.ts @@ -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 * 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, diff --git a/src/cursor/cursor-executor.ts b/src/cursor/cursor-executor.ts index 28ce9f55..2d011b3a 100644 --- a/src/cursor/cursor-executor.ts +++ b/src/cursor/cursor-executor.ts @@ -3,784 +3,769 @@ * 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 = { - NONE: 0x00, - GZIP: 0x01, - GZIP_ALT: 0x02, - GZIP_BOTH: 0x03, + NONE: 0x00, + GZIP: 0x01, + GZIP_ALT: 0x02, + GZIP_BOTH: 0x03, } as const; /** Cursor credentials structure */ interface CursorCredentials { - accessToken: string; - providerSpecificData?: { - machineId?: string; - ghostMode?: boolean; - }; + accessToken: string; + providerSpecificData?: { + machineId?: string; + ghostMode?: boolean; + }; } /** Executor parameters */ interface ExecutorParams { - model: string; - body: { - messages: Array<{ - 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 }; - }>; - }>; - tools?: CursorTool[]; - reasoning_effort?: string; - }; - stream: boolean; - credentials: CursorCredentials; - signal?: AbortSignal; + model: string; + body: { + messages: Array<{ + 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 }; + }>; + }>; + tools?: CursorTool[]; + reasoning_effort?: string; + }; + stream: boolean; + credentials: CursorCredentials; + signal?: AbortSignal; } /** HTTP/2 response structure */ interface Http2Response { - status: number; - headers: IncomingHttpHeaders; - body: Buffer; + status: number; + headers: IncomingHttpHeaders; + body: Buffer; } /** Detect cloud environment */ function isCloudEnv(): boolean { - 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; - } catch { - // Continue - } - return false; + 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; + } catch { + // Continue + } + return false; } /** 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"); - return http2Module; - } catch { - return null; - } - } - return null; + if (http2Module) return http2Module; + if (!isCloudEnv()) { + try { + http2Module = await import('http2'); + return http2Module; + } catch { + return null; + } + } + return null; } /** * Decompress payload if needed */ 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"); - if (text.startsWith('{"error"')) { - return payload; - } - } catch { - // Continue - } - } + // Check if payload is JSON error + if (payload.length > 10 && payload[0] === 0x7b && payload[1] === 0x22) { + try { + const text = payload.toString('utf-8'); + if (text.startsWith('{"error"')) { + return payload; + } + } catch { + // Continue + } + } - if ( - flags === COMPRESS_FLAG.GZIP || - flags === COMPRESS_FLAG.GZIP_ALT || - flags === COMPRESS_FLAG.GZIP_BOTH - ) { - try { - return zlib.gunzipSync(payload); - } catch { - return payload; - } - } - return payload; + if ( + flags === COMPRESS_FLAG.GZIP || + flags === COMPRESS_FLAG.GZIP_ALT || + flags === COMPRESS_FLAG.GZIP_BOTH + ) { + try { + return zlib.gunzipSync(payload); + } catch { + return payload; + } + } + return payload; } /** * Create error response from JSON error */ function createErrorResponse(jsonError: { - error?: { - code?: string; - message?: string; - details?: Array<{ debug?: { details?: { title?: string; detail?: string }; error?: string } }>; - }; + error?: { + code?: string; + message?: string; + details?: Array<{ debug?: { details?: { title?: string; detail?: string }; error?: string } }>; + }; }): Response { - const errorMsg = - jsonError?.error?.details?.[0]?.debug?.details?.title || - jsonError?.error?.details?.[0]?.debug?.details?.detail || - jsonError?.error?.message || - "API Error"; + const errorMsg = + jsonError?.error?.details?.[0]?.debug?.details?.title || + jsonError?.error?.details?.[0]?.debug?.details?.detail || + jsonError?.error?.message || + '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", - }, - }), - { - status: isRateLimit ? 429 : 400, - headers: { "Content-Type": "application/json" }, - } - ); + return new Response( + JSON.stringify({ + error: { + message: errorMsg, + type: isRateLimit ? 'rate_limit_error' : 'api_error', + code: jsonError?.error?.details?.[0]?.debug?.error || 'unknown', + }, + }), + { + status: isRateLimit ? 429 : 400, + headers: { 'Content-Type': 'application/json' }, + } + ); } export class CursorExecutor { - private readonly baseUrl = "https://api2.cursor.sh"; - private readonly chatPath = "/aiserver.v1.AiService/StreamChat"; - - buildUrl(): string { - return `${this.baseUrl}${this.chatPath}`; - } - - /** - * Generate checksum using Jyh cipher (time-based XOR with rolling key seed=165) - */ - generateChecksum(machineId: string): string { - const timestamp = Math.floor(Date.now() / 1000000); - const byteArray = new Uint8Array([ - (timestamp >> 40) & 0xff, - (timestamp >> 32) & 0xff, - (timestamp >> 24) & 0xff, - (timestamp >> 16) & 0xff, - (timestamp >> 8) & 0xff, - timestamp & 0xff, - ]); - - let t = 165; - for (let i = 0; i < byteArray.length; i++) { - byteArray[i] = ((byteArray[i] ^ t) + (i % 256)) & 0xff; - t = byteArray[i]; - } - - const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; - let encoded = ""; - - for (let i = 0; i < byteArray.length; i += 3) { - const a = byteArray[i]; - const b = i + 1 < byteArray.length ? byteArray[i + 1] : 0; - const c = i + 2 < byteArray.length ? byteArray[i + 2] : 0; - - encoded += alphabet[a >> 2]; - encoded += alphabet[((a & 3) << 4) | (b >> 4)]; - - if (i + 1 < byteArray.length) { - encoded += alphabet[((b & 15) << 2) | (c >> 6)]; - } - if (i + 2 < byteArray.length) { - encoded += alphabet[c & 63]; - } - } - - return `${encoded}${machineId}`; - } - - buildHeaders(credentials: CursorCredentials): Record { - const accessToken = credentials.accessToken; - const machineId = credentials.providerSpecificData?.machineId; - const ghostMode = credentials.providerSpecificData?.ghostMode !== false; - - if (!machineId) { - throw new Error("Machine ID is required for Cursor API"); - } - - 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), - }; - } - - transformRequest( - model: string, - body: ExecutorParams["body"], - stream: boolean, - credentials: CursorCredentials - ): Uint8Array { - const translatedBody = buildCursorRequest(model, body, stream, credentials); - const messages = translatedBody.messages || []; - const tools = (translatedBody.tools || body.tools || []) as CursorTool[]; - const reasoningEffort = body.reasoning_effort || null; - return generateCursorBody(messages, model, tools, reasoningEffort); - } - - async makeFetchRequest( - url: string, - headers: Record, - body: Uint8Array, - signal?: AbortSignal - ): Promise { - const response = await fetch(url, { - method: "POST", - headers, - body, - signal, - }); - - const responseHeaders: Record = {}; - response.headers.forEach((value, key) => { - responseHeaders[key] = value; - }); - - return { - status: response.status, - headers: responseHeaders, - body: Buffer.from(await response.arrayBuffer()), - }; - } - - async makeHttp2Request( - url: string, - headers: Record, - body: Uint8Array, - signal?: AbortSignal - ): Promise { - const http2 = await getHttp2(); - if (!http2) { - throw new Error("http2 module not available"); - } - - return new Promise((resolve, reject) => { - const urlObj = new URL(url); - const client = http2.connect(`https://${urlObj.host}`); - const chunks: Buffer[] = []; - let responseHeaders: IncomingHttpHeaders = {}; - - client.on("error", reject); - - const req = client.request({ - ":method": "POST", - ":path": urlObj.pathname, - ":authority": urlObj.host, - ":scheme": "https", - ...headers, - }); - - req.on("response", (hdrs) => { - responseHeaders = hdrs; - }); - req.on("data", (chunk: Buffer) => { - chunks.push(chunk); - }); - req.on("end", () => { - client.close(); - resolve({ - status: Number(responseHeaders[":status"]), - headers: responseHeaders, - body: Buffer.concat(chunks), - }); - }); - req.on("error", (err) => { - client.close(); - reject(err); - }); - - if (signal) { - signal.addEventListener("abort", () => { - req.close(); - client.close(); - reject(new Error("Request aborted")); - }); - } - - req.write(body); - req.end(); - }); - } - - async execute(params: ExecutorParams): Promise<{ - response: Response; - url: string; - headers: Record; - transformedBody: ExecutorParams["body"]; - }> { - const { model, body, stream, credentials, signal } = params; - const url = this.buildUrl(); - const headers = this.buildHeaders(credentials); - const transformedBody = this.transformRequest(model, body, stream, credentials); - - try { - const http2 = await getHttp2(); - const response = http2 - ? await this.makeHttp2Request(url, headers, transformedBody, signal) - : await this.makeFetchRequest(url, headers, transformedBody, signal); - - if (response.status !== 200) { - const errorText = response.body?.toString() || "Unknown error"; - const errorResponse = new Response( - JSON.stringify({ - error: { - message: `[${response.status}]: ${errorText}`, - type: "invalid_request_error", - code: "", - }, - }), - { - status: response.status, - headers: { "Content-Type": "application/json" }, - } - ); - return { response: errorResponse, url, headers, transformedBody: body }; - } - - const transformedResponse = - stream !== false - ? this.transformProtobufToSSE(response.body, model, body) - : this.transformProtobufToJSON(response.body, model, body); - - return { response: transformedResponse, url, headers, transformedBody: body }; - } catch (error) { - const errorResponse = new Response( - JSON.stringify({ - error: { - message: (error as Error).message, - type: "connection_error", - code: "", - }, - }), - { - status: 500, - headers: { "Content-Type": "application/json" }, - } - ); - return { response: errorResponse, url, headers, transformedBody: body }; - } - } - - 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 = ""; - const toolCalls: Array<{ - id: string; - type: string; - function: { name: string; arguments: string }; - }> = []; - const toolCallsMap = new Map< - string, - { - id: string; - type: string; - function: { name: string; arguments: string }; - isLast: boolean; - index: number; - } - >(); - - while (offset < buffer.length) { - if (offset + 5 > buffer.length) break; - - const flags = buffer[offset]; - const length = buffer.readUInt32BE(offset + 1); - - if (offset + 5 + length > buffer.length) break; - - let payload = buffer.slice(offset + 5, offset + 5 + length); - offset += 5 + length; - - payload = decompressPayload(payload, flags); - - try { - const text = payload.toString("utf-8"); - if (text.startsWith("{") && text.includes('"error"')) { - return createErrorResponse(JSON.parse(text)); - } - } catch { - // Continue - } - - const result = extractTextFromResponse(new Uint8Array(payload)); - - if (result.error) { - return new Response( - JSON.stringify({ - error: { - message: result.error, - type: "rate_limit_error", - code: "rate_limited", - }, - }), - { - status: 429, - headers: { "Content-Type": "application/json" }, - } - ); - } - - if (result.toolCall) { - const tc = result.toolCall; - - if (toolCallsMap.has(tc.id)) { - const existing = toolCallsMap.get(tc.id)!; - existing.function.arguments += tc.function.arguments; - existing.isLast = tc.isLast; - } else { - toolCallsMap.set(tc.id, { - ...tc, - index: toolCallsMap.size, - }); - } - - if (tc.isLast) { - const finalToolCall = toolCallsMap.get(tc.id)!; - toolCalls.push({ - id: finalToolCall.id, - type: finalToolCall.type, - function: { - name: finalToolCall.function.name, - arguments: finalToolCall.function.arguments, - }, - }); - } - } - - if (result.text) totalContent += result.text; - } - - // Finalize remaining tool calls - for (const id of Array.from(toolCallsMap.keys())) { - const tc = toolCallsMap.get(id)!; - if (!toolCalls.find((t) => t.id === id)) { - toolCalls.push({ - id: tc.id, - type: tc.type, - function: { - name: tc.function.name, - arguments: tc.function.arguments, - }, - }); - } - } - - const message: { - role: string; - content: string | null; - tool_calls?: Array<{ - id: string; - type: string; - function: { name: string; arguments: string }; - }>; - } = { - role: "assistant", - content: totalContent || null, - }; - - if (toolCalls.length > 0) { - message.tool_calls = toolCalls; - } - - const completion = { - id: responseId, - object: "chat.completion", - created, - model, - choices: [ - { - index: 0, - message, - finish_reason: toolCalls.length > 0 ? "tool_calls" : "stop", - }, - ], - usage: { - prompt_tokens: 0, - completion_tokens: 0, - total_tokens: 0, - }, - }; - - return new Response(JSON.stringify(completion), { - status: 200, - headers: { "Content-Type": "application/json" }, - }); - } - - 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 = ""; - const toolCalls: Array<{ - id: string; - type: string; - function: { name: string; arguments: string }; - index: number; - }> = []; - const toolCallsMap = new Map< - string, - { - id: string; - type: string; - function: { name: string; arguments: string }; - isLast: boolean; - index: number; - } - >(); - - while (offset < buffer.length) { - if (offset + 5 > buffer.length) break; - - const flags = buffer[offset]; - const length = buffer.readUInt32BE(offset + 1); - - if (offset + 5 + length > buffer.length) break; - - let payload = buffer.slice(offset + 5, offset + 5 + length); - offset += 5 + length; - - payload = decompressPayload(payload, flags); - - try { - const text = payload.toString("utf-8"); - if (text.startsWith("{") && text.includes('"error"')) { - return createErrorResponse(JSON.parse(text)); - } - } catch { - // Continue - } - - const result = extractTextFromResponse(new Uint8Array(payload)); - - if (result.error) { - return new Response( - JSON.stringify({ - error: { - message: result.error, - type: "rate_limit_error", - code: "rate_limited", - }, - }), - { - status: 429, - headers: { "Content-Type": "application/json" }, - } - ); - } - - if (result.toolCall) { - const tc = result.toolCall; - - if (chunks.length === 0) { - chunks.push( - `data: ${JSON.stringify({ - id: responseId, - object: "chat.completion.chunk", - created, - model, - choices: [ - { - index: 0, - delta: { role: "assistant", content: "" }, - finish_reason: null, - }, - ], - })}\n\n` - ); - } - - if (toolCallsMap.has(tc.id)) { - const existing = toolCallsMap.get(tc.id)!; - existing.function.arguments += tc.function.arguments; - existing.isLast = tc.isLast; - - if (tc.function.arguments) { - chunks.push( - `data: ${JSON.stringify({ - id: responseId, - object: "chat.completion.chunk", - created, - model, - choices: [ - { - index: 0, - delta: { - tool_calls: [ - { - index: existing.index, - id: tc.id, - type: "function", - function: { - name: tc.function.name, - arguments: tc.function.arguments, - }, - }, - ], - }, - finish_reason: null, - }, - ], - })}\n\n` - ); - } - } else { - const toolCallIndex = toolCalls.length; - toolCalls.push({ ...tc, index: toolCallIndex }); - toolCallsMap.set(tc.id, { ...tc, index: toolCallIndex }); - - chunks.push( - `data: ${JSON.stringify({ - id: responseId, - object: "chat.completion.chunk", - created, - model, - choices: [ - { - index: 0, - delta: { - tool_calls: [ - { - index: toolCallIndex, - id: tc.id, - type: "function", - function: { - name: tc.function.name, - arguments: tc.function.arguments, - }, - }, - ], - }, - finish_reason: null, - }, - ], - })}\n\n` - ); - } - } - - if (result.text) { - totalContent += result.text; - chunks.push( - `data: ${JSON.stringify({ - id: responseId, - object: "chat.completion.chunk", - created, - model, - choices: [ - { - index: 0, - delta: - chunks.length === 0 && toolCalls.length === 0 - ? { role: "assistant", content: result.text } - : { content: result.text }, - finish_reason: null, - }, - ], - })}\n\n` - ); - } - } - - if (chunks.length === 0 && toolCalls.length === 0) { - chunks.push( - `data: ${JSON.stringify({ - id: responseId, - object: "chat.completion.chunk", - created, - model, - choices: [ - { - index: 0, - delta: { role: "assistant", content: "" }, - finish_reason: null, - }, - ], - })}\n\n` - ); - } - - chunks.push( - `data: ${JSON.stringify({ - id: responseId, - object: "chat.completion.chunk", - created, - model, - choices: [ - { - index: 0, - delta: {}, - finish_reason: toolCalls.length > 0 ? "tool_calls" : "stop", - }, - ], - usage: { - prompt_tokens: 0, - completion_tokens: 0, - total_tokens: 0, - }, - })}\n\n` - ); - chunks.push("data: [DONE]\n\n"); - - return new Response(chunks.join(""), { - status: 200, - headers: { - "Content-Type": "text/event-stream", - "Cache-Control": "no-cache", - Connection: "keep-alive", - }, - }); - } + private readonly baseUrl = 'https://api2.cursor.sh'; + private readonly chatPath = '/aiserver.v1.AiService/StreamChat'; + + buildUrl(): string { + return `${this.baseUrl}${this.chatPath}`; + } + + /** + * Generate checksum using Jyh cipher (time-based XOR with rolling key seed=165) + */ + generateChecksum(machineId: string): string { + const timestamp = Math.floor(Date.now() / 1000000); + const byteArray = new Uint8Array([ + (timestamp >> 40) & 0xff, + (timestamp >> 32) & 0xff, + (timestamp >> 24) & 0xff, + (timestamp >> 16) & 0xff, + (timestamp >> 8) & 0xff, + timestamp & 0xff, + ]); + + let t = 165; + for (let i = 0; i < byteArray.length; i++) { + byteArray[i] = ((byteArray[i] ^ t) + (i % 256)) & 0xff; + t = byteArray[i]; + } + + const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'; + let encoded = ''; + + for (let i = 0; i < byteArray.length; i += 3) { + const a = byteArray[i]; + const b = i + 1 < byteArray.length ? byteArray[i + 1] : 0; + const c = i + 2 < byteArray.length ? byteArray[i + 2] : 0; + + encoded += alphabet[a >> 2]; + encoded += alphabet[((a & 3) << 4) | (b >> 4)]; + + if (i + 1 < byteArray.length) { + encoded += alphabet[((b & 15) << 2) | (c >> 6)]; + } + if (i + 2 < byteArray.length) { + encoded += alphabet[c & 63]; + } + } + + return `${encoded}${machineId}`; + } + + buildHeaders(credentials: CursorCredentials): Record { + const accessToken = credentials.accessToken; + const machineId = credentials.providerSpecificData?.machineId; + const ghostMode = credentials.providerSpecificData?.ghostMode !== false; + + if (!machineId) { + throw new Error('Machine ID is required for Cursor API'); + } + + 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), + }; + } + + transformRequest( + model: string, + body: ExecutorParams['body'], + stream: boolean, + credentials: CursorCredentials + ): Uint8Array { + const translatedBody = buildCursorRequest(model, body, stream, credentials); + const messages = translatedBody.messages || []; + const tools = (translatedBody.tools || body.tools || []) as CursorTool[]; + const reasoningEffort = body.reasoning_effort || null; + return generateCursorBody(messages, model, tools, reasoningEffort); + } + + async makeFetchRequest( + url: string, + headers: Record, + body: Uint8Array, + signal?: AbortSignal + ): Promise { + const response = await fetch(url, { + method: 'POST', + headers, + body, + signal, + }); + + const responseHeaders: Record = {}; + response.headers.forEach((value, key) => { + responseHeaders[key] = value; + }); + + return { + status: response.status, + headers: responseHeaders, + body: Buffer.from(await response.arrayBuffer()), + }; + } + + async makeHttp2Request( + url: string, + headers: Record, + body: Uint8Array, + signal?: AbortSignal + ): Promise { + const http2 = await getHttp2(); + if (!http2) { + throw new Error('http2 module not available'); + } + + return new Promise((resolve, reject) => { + const urlObj = new URL(url); + const client = http2.connect(`https://${urlObj.host}`); + const chunks: Buffer[] = []; + let responseHeaders: IncomingHttpHeaders = {}; + + client.on('error', reject); + + const req = client.request({ + ':method': 'POST', + ':path': urlObj.pathname, + ':authority': urlObj.host, + ':scheme': 'https', + ...headers, + }); + + req.on('response', (hdrs) => { + responseHeaders = hdrs; + }); + req.on('data', (chunk: Buffer) => { + chunks.push(chunk); + }); + req.on('end', () => { + client.close(); + resolve({ + status: Number(responseHeaders[':status']), + headers: responseHeaders, + body: Buffer.concat(chunks), + }); + }); + req.on('error', (err) => { + client.close(); + reject(err); + }); + + if (signal) { + signal.addEventListener('abort', () => { + req.close(); + client.close(); + reject(new Error('Request aborted')); + }); + } + + req.write(body); + req.end(); + }); + } + + async execute(params: ExecutorParams): Promise<{ + response: Response; + url: string; + headers: Record; + transformedBody: ExecutorParams['body']; + }> { + const { model, body, stream, credentials, signal } = params; + const url = this.buildUrl(); + const headers = this.buildHeaders(credentials); + const transformedBody = this.transformRequest(model, body, stream, credentials); + + try { + const http2 = await getHttp2(); + const response = http2 + ? await this.makeHttp2Request(url, headers, transformedBody, signal) + : await this.makeFetchRequest(url, headers, transformedBody, signal); + + if (response.status !== 200) { + const errorText = response.body?.toString() || 'Unknown error'; + const errorResponse = new Response( + JSON.stringify({ + error: { + message: `[${response.status}]: ${errorText}`, + type: 'invalid_request_error', + code: '', + }, + }), + { + status: response.status, + headers: { 'Content-Type': 'application/json' }, + } + ); + return { response: errorResponse, url, headers, transformedBody: body }; + } + + const transformedResponse = + stream !== false + ? this.transformProtobufToSSE(response.body, model, body) + : this.transformProtobufToJSON(response.body, model, body); + + return { response: transformedResponse, url, headers, transformedBody: body }; + } catch (error) { + const errorResponse = new Response( + JSON.stringify({ + error: { + message: (error as Error).message, + type: 'connection_error', + code: '', + }, + }), + { + status: 500, + headers: { 'Content-Type': 'application/json' }, + } + ); + return { response: errorResponse, url, headers, transformedBody: body }; + } + } + + 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 = ''; + const toolCalls: Array<{ + id: string; + type: string; + function: { name: string; arguments: string }; + }> = []; + const toolCallsMap = new Map< + string, + { + id: string; + type: string; + function: { name: string; arguments: string }; + isLast: boolean; + index: number; + } + >(); + + while (offset < buffer.length) { + if (offset + 5 > buffer.length) break; + + const flags = buffer[offset]; + const length = buffer.readUInt32BE(offset + 1); + + if (offset + 5 + length > buffer.length) break; + + let payload = buffer.slice(offset + 5, offset + 5 + length); + offset += 5 + length; + + payload = decompressPayload(payload, flags); + + try { + const text = payload.toString('utf-8'); + if (text.startsWith('{') && text.includes('"error"')) { + return createErrorResponse(JSON.parse(text)); + } + } catch { + // Continue + } + + const result = extractTextFromResponse(new Uint8Array(payload)); + + if (result.error) { + return new Response( + JSON.stringify({ + error: { + message: result.error, + type: 'rate_limit_error', + code: 'rate_limited', + }, + }), + { + status: 429, + headers: { 'Content-Type': 'application/json' }, + } + ); + } + + if (result.toolCall) { + const tc = result.toolCall; + + if (toolCallsMap.has(tc.id)) { + const existing = toolCallsMap.get(tc.id)!; + existing.function.arguments += tc.function.arguments; + existing.isLast = tc.isLast; + } else { + toolCallsMap.set(tc.id, { + ...tc, + index: toolCallsMap.size, + }); + } + + if (tc.isLast) { + const finalToolCall = toolCallsMap.get(tc.id)!; + toolCalls.push({ + id: finalToolCall.id, + type: finalToolCall.type, + function: { + name: finalToolCall.function.name, + arguments: finalToolCall.function.arguments, + }, + }); + } + } + + if (result.text) totalContent += result.text; + } + + // Finalize remaining tool calls + for (const id of Array.from(toolCallsMap.keys())) { + const tc = toolCallsMap.get(id)!; + if (!toolCalls.find((t) => t.id === id)) { + toolCalls.push({ + id: tc.id, + type: tc.type, + function: { + name: tc.function.name, + arguments: tc.function.arguments, + }, + }); + } + } + + const message: { + role: string; + content: string | null; + tool_calls?: Array<{ + id: string; + type: string; + function: { name: string; arguments: string }; + }>; + } = { + role: 'assistant', + content: totalContent || null, + }; + + if (toolCalls.length > 0) { + message.tool_calls = toolCalls; + } + + const completion = { + id: responseId, + object: 'chat.completion', + created, + model, + choices: [ + { + index: 0, + message, + finish_reason: toolCalls.length > 0 ? 'tool_calls' : 'stop', + }, + ], + usage: { + prompt_tokens: 0, + completion_tokens: 0, + total_tokens: 0, + }, + }; + + return new Response(JSON.stringify(completion), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + } + + 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 = ''; + const toolCalls: Array<{ + id: string; + type: string; + function: { name: string; arguments: string }; + index: number; + }> = []; + const toolCallsMap = new Map< + string, + { + id: string; + type: string; + function: { name: string; arguments: string }; + isLast: boolean; + index: number; + } + >(); + + while (offset < buffer.length) { + if (offset + 5 > buffer.length) break; + + const flags = buffer[offset]; + const length = buffer.readUInt32BE(offset + 1); + + if (offset + 5 + length > buffer.length) break; + + let payload = buffer.slice(offset + 5, offset + 5 + length); + offset += 5 + length; + + payload = decompressPayload(payload, flags); + + try { + const text = payload.toString('utf-8'); + if (text.startsWith('{') && text.includes('"error"')) { + return createErrorResponse(JSON.parse(text)); + } + } catch { + // Continue + } + + const result = extractTextFromResponse(new Uint8Array(payload)); + + if (result.error) { + return new Response( + JSON.stringify({ + error: { + message: result.error, + type: 'rate_limit_error', + code: 'rate_limited', + }, + }), + { + status: 429, + headers: { 'Content-Type': 'application/json' }, + } + ); + } + + if (result.toolCall) { + const tc = result.toolCall; + + if (chunks.length === 0) { + chunks.push( + `data: ${JSON.stringify({ + id: responseId, + object: 'chat.completion.chunk', + created, + model, + choices: [ + { + index: 0, + delta: { role: 'assistant', content: '' }, + finish_reason: null, + }, + ], + })}\n\n` + ); + } + + if (toolCallsMap.has(tc.id)) { + const existing = toolCallsMap.get(tc.id)!; + existing.function.arguments += tc.function.arguments; + existing.isLast = tc.isLast; + + if (tc.function.arguments) { + chunks.push( + `data: ${JSON.stringify({ + id: responseId, + object: 'chat.completion.chunk', + created, + model, + choices: [ + { + index: 0, + delta: { + tool_calls: [ + { + index: existing.index, + id: tc.id, + type: 'function', + function: { + name: tc.function.name, + arguments: tc.function.arguments, + }, + }, + ], + }, + finish_reason: null, + }, + ], + })}\n\n` + ); + } + } else { + const toolCallIndex = toolCalls.length; + toolCalls.push({ ...tc, index: toolCallIndex }); + toolCallsMap.set(tc.id, { ...tc, index: toolCallIndex }); + + chunks.push( + `data: ${JSON.stringify({ + id: responseId, + object: 'chat.completion.chunk', + created, + model, + choices: [ + { + index: 0, + delta: { + tool_calls: [ + { + index: toolCallIndex, + id: tc.id, + type: 'function', + function: { + name: tc.function.name, + arguments: tc.function.arguments, + }, + }, + ], + }, + finish_reason: null, + }, + ], + })}\n\n` + ); + } + } + + if (result.text) { + totalContent += result.text; + chunks.push( + `data: ${JSON.stringify({ + id: responseId, + object: 'chat.completion.chunk', + created, + model, + choices: [ + { + index: 0, + delta: + chunks.length === 0 && toolCalls.length === 0 + ? { role: 'assistant', content: result.text } + : { content: result.text }, + finish_reason: null, + }, + ], + })}\n\n` + ); + } + } + + if (chunks.length === 0 && toolCalls.length === 0) { + chunks.push( + `data: ${JSON.stringify({ + id: responseId, + object: 'chat.completion.chunk', + created, + model, + choices: [ + { + index: 0, + delta: { role: 'assistant', content: '' }, + finish_reason: null, + }, + ], + })}\n\n` + ); + } + + chunks.push( + `data: ${JSON.stringify({ + id: responseId, + object: 'chat.completion.chunk', + created, + model, + choices: [ + { + index: 0, + delta: {}, + finish_reason: toolCalls.length > 0 ? 'tool_calls' : 'stop', + }, + ], + usage: { + prompt_tokens: 0, + completion_tokens: 0, + total_tokens: 0, + }, + })}\n\n` + ); + chunks.push('data: [DONE]\n\n'); + + return new Response(chunks.join(''), { + status: 200, + headers: { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + Connection: 'keep-alive', + }, + }); + } } export default CursorExecutor; diff --git a/src/cursor/cursor-protobuf-decoder.ts b/src/cursor/cursor-protobuf-decoder.ts index 7b3e0d6a..7a5c12b6 100644 --- a/src/cursor/cursor-protobuf-decoder.ts +++ b/src/cursor/cursor-protobuf-decoder.ts @@ -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> { - const fields = new Map< - number, - Array<{ wireType: WireType; value: Uint8Array | number }> - >(); - let pos = 0; + const fields = new Map>(); + 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 }; + } } diff --git a/src/cursor/cursor-protobuf-encoder.ts b/src/cursor/cursor-protobuf-encoder.ts index 6958d219..857be448 100644 --- a/src/cursor/cursor-protobuf-encoder.ts +++ b/src/cursor/cursor-protobuf-encoder.ts @@ -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; } diff --git a/src/cursor/cursor-protobuf-schema.ts b/src/cursor/cursor-protobuf-schema.ts index 64034e61..8cee42cf 100644 --- a/src/cursor/cursor-protobuf-schema.ts +++ b/src/cursor/cursor-protobuf-schema.ts @@ -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; - }; - name?: string; - description?: string; - input_schema?: Record; + function?: { + name?: string; + description?: string; + parameters?: Record; + }; + name?: string; + description?: string; + input_schema?: Record; } /** 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; diff --git a/src/cursor/cursor-protobuf.ts b/src/cursor/cursor-protobuf.ts index 60e4d588..b26d8281 100644 --- a/src/cursor/cursor-protobuf.ts +++ b/src/cursor/cursor-protobuf.ts @@ -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, }; diff --git a/src/cursor/cursor-translator.ts b/src/cursor/cursor-translator.ts index e40d4d5d..452c7d39 100644 --- a/src/cursor/cursor-translator.ts +++ b/src/cursor/cursor-translator.ts @@ -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, + }; } diff --git a/src/cursor/index.ts b/src/cursor/index.ts index 014cf84d..46832336 100644 --- a/src/cursor/index.ts +++ b/src/cursor/index.ts @@ -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';