mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-17 16:16:37 +00:00
This commit is contained in:
@@ -0,0 +1,246 @@
|
|||||||
|
/**
|
||||||
|
* Cursor IDE Authentication Handler
|
||||||
|
*
|
||||||
|
* Handles token import and authentication for Cursor IDE integration.
|
||||||
|
* Supports auto-detection from Cursor's SQLite database.
|
||||||
|
*
|
||||||
|
* Token Location:
|
||||||
|
* - Linux: ~/.config/Cursor/User/globalStorage/state.vscdb
|
||||||
|
* - macOS: ~/Library/Application Support/Cursor/User/globalStorage/state.vscdb
|
||||||
|
* - Windows: %APPDATA%\Cursor\User\globalStorage\state.vscdb
|
||||||
|
*
|
||||||
|
* Database Keys:
|
||||||
|
* - cursorAuth/accessToken: Access token
|
||||||
|
* - storage.serviceMachineId: Machine ID for checksum
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as os from 'os';
|
||||||
|
import type { CursorCredentials, CursorAuthStatus, AutoDetectResult } from './types';
|
||||||
|
import { getCcsDir } from '../utils/config-manager';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get platform-specific path to Cursor's state.vscdb
|
||||||
|
*/
|
||||||
|
export function getTokenStoragePath(): string {
|
||||||
|
const platform = process.platform;
|
||||||
|
const home = os.homedir();
|
||||||
|
|
||||||
|
if (platform === 'win32') {
|
||||||
|
const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
|
||||||
|
return path.join(appData, 'Cursor', 'User', 'globalStorage', 'state.vscdb');
|
||||||
|
} else if (platform === 'darwin') {
|
||||||
|
return path.join(
|
||||||
|
home,
|
||||||
|
'Library',
|
||||||
|
'Application Support',
|
||||||
|
'Cursor',
|
||||||
|
'User',
|
||||||
|
'globalStorage',
|
||||||
|
'state.vscdb'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Linux
|
||||||
|
return path.join(home, '.config', 'Cursor', 'User', 'globalStorage', 'state.vscdb');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query Cursor's SQLite database using sqlite3 CLI
|
||||||
|
*/
|
||||||
|
function queryStateDb(dbPath: string, key: string): string | null {
|
||||||
|
try {
|
||||||
|
const result = execSync(
|
||||||
|
`sqlite3 "${dbPath}" "SELECT value FROM itemTable WHERE key='${key}'" 2>/dev/null`,
|
||||||
|
{ encoding: 'utf8', timeout: 5000 }
|
||||||
|
).trim();
|
||||||
|
return result || null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-detect tokens from Cursor's SQLite database
|
||||||
|
*/
|
||||||
|
export function autoDetectTokens(): AutoDetectResult {
|
||||||
|
const dbPath = getTokenStoragePath();
|
||||||
|
|
||||||
|
// Check if database exists
|
||||||
|
if (!fs.existsSync(dbPath)) {
|
||||||
|
return {
|
||||||
|
found: false,
|
||||||
|
error:
|
||||||
|
'Cursor state database not found. Make sure Cursor IDE is installed and you are logged in.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to query access token
|
||||||
|
const accessToken = queryStateDb(dbPath, 'cursorAuth/accessToken');
|
||||||
|
if (!accessToken) {
|
||||||
|
return {
|
||||||
|
found: false,
|
||||||
|
error: 'Access token not found in database. Please log in to Cursor IDE first.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to query machine ID
|
||||||
|
const machineId = queryStateDb(dbPath, 'storage.serviceMachineId');
|
||||||
|
if (!machineId) {
|
||||||
|
return {
|
||||||
|
found: false,
|
||||||
|
error: 'Machine ID not found in database.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
found: true,
|
||||||
|
accessToken,
|
||||||
|
machineId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate token and machine ID format
|
||||||
|
*/
|
||||||
|
export function validateToken(accessToken: string, machineId: string): boolean {
|
||||||
|
// Basic validation
|
||||||
|
if (!accessToken || typeof accessToken !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!machineId || typeof machineId !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token format validation (Cursor tokens are typically long strings)
|
||||||
|
if (accessToken.length < 50) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Machine ID format validation (should be UUID-like)
|
||||||
|
const uuidRegex = /^[a-f0-9-]{32,}$/i;
|
||||||
|
if (!uuidRegex.test(machineId.replace(/-/g, ''))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract user info from token if possible
|
||||||
|
* Cursor tokens may contain encoded user info as JWT
|
||||||
|
*/
|
||||||
|
export function extractUserInfo(accessToken: string): { email?: string; userId?: string } | null {
|
||||||
|
try {
|
||||||
|
// Try to decode as JWT
|
||||||
|
const parts = accessToken.split('.');
|
||||||
|
if (parts.length === 3) {
|
||||||
|
let payload = parts[1];
|
||||||
|
// Add padding if needed
|
||||||
|
while (payload.length % 4) {
|
||||||
|
payload += '=';
|
||||||
|
}
|
||||||
|
const decoded = JSON.parse(
|
||||||
|
Buffer.from(payload.replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString()
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
email: decoded.email || decoded.sub,
|
||||||
|
userId: decoded.sub || decoded.user_id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Token is not a JWT, that's okay
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get path to credentials file
|
||||||
|
*/
|
||||||
|
export function getCredentialsPath(): string {
|
||||||
|
return path.join(getCcsDir(), 'cursor', 'credentials.json');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save credentials to CCS config directory
|
||||||
|
*/
|
||||||
|
export function saveCredentials(credentials: CursorCredentials): void {
|
||||||
|
const credPath = getCredentialsPath();
|
||||||
|
const dir = path.dirname(credPath);
|
||||||
|
|
||||||
|
// Ensure directory exists
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write credentials
|
||||||
|
fs.writeFileSync(credPath, JSON.stringify(credentials, null, 2), 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load credentials from CCS config directory
|
||||||
|
*/
|
||||||
|
export function loadCredentials(): CursorCredentials | null {
|
||||||
|
const credPath = getCredentialsPath();
|
||||||
|
|
||||||
|
if (!fs.existsSync(credPath)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const raw = fs.readFileSync(credPath, 'utf8');
|
||||||
|
const parsed: unknown = JSON.parse(raw);
|
||||||
|
|
||||||
|
// Basic validation
|
||||||
|
if (
|
||||||
|
typeof parsed === 'object' &&
|
||||||
|
parsed !== null &&
|
||||||
|
'accessToken' in parsed &&
|
||||||
|
'machineId' in parsed &&
|
||||||
|
'authMethod' in parsed &&
|
||||||
|
'importedAt' in parsed
|
||||||
|
) {
|
||||||
|
return parsed as CursorCredentials;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check authentication status
|
||||||
|
*/
|
||||||
|
export function checkAuthStatus(): CursorAuthStatus {
|
||||||
|
const credentials = loadCredentials();
|
||||||
|
|
||||||
|
if (!credentials) {
|
||||||
|
return { authenticated: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate credentials are still valid format
|
||||||
|
if (!validateToken(credentials.accessToken, credentials.machineId)) {
|
||||||
|
return { authenticated: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate token age in hours
|
||||||
|
let tokenAge: number | undefined;
|
||||||
|
try {
|
||||||
|
const importedDate = new Date(credentials.importedAt);
|
||||||
|
const now = new Date();
|
||||||
|
tokenAge = Math.floor((now.getTime() - importedDate.getTime()) / (1000 * 60 * 60));
|
||||||
|
} catch {
|
||||||
|
// Invalid date format
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
authenticated: true,
|
||||||
|
credentials,
|
||||||
|
tokenAge,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,786 @@
|
|||||||
|
/**
|
||||||
|
* Cursor Executor
|
||||||
|
* 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";
|
||||||
|
|
||||||
|
/** Compression flags for response parsing */
|
||||||
|
const COMPRESS_FLAG = {
|
||||||
|
NONE: 0x00,
|
||||||
|
GZIP: 0x01,
|
||||||
|
GZIP_ALT: 0x02,
|
||||||
|
GZIP_BOTH: 0x03,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** Cursor credentials structure */
|
||||||
|
interface CursorCredentials {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** HTTP/2 response structure */
|
||||||
|
interface Http2Response {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Lazy import http2 */
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 } }>;
|
||||||
|
};
|
||||||
|
}): Response {
|
||||||
|
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";
|
||||||
|
|
||||||
|
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<string, string> {
|
||||||
|
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<string, string>,
|
||||||
|
body: Uint8Array,
|
||||||
|
signal?: AbortSignal
|
||||||
|
): Promise<Http2Response> {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers,
|
||||||
|
body,
|
||||||
|
signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
const responseHeaders: Record<string, string> = {};
|
||||||
|
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<string, string>,
|
||||||
|
body: Uint8Array,
|
||||||
|
signal?: AbortSignal
|
||||||
|
): Promise<Http2Response> {
|
||||||
|
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<string, string>;
|
||||||
|
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;
|
||||||
@@ -0,0 +1,301 @@
|
|||||||
|
/**
|
||||||
|
* Cursor Protobuf Decoder
|
||||||
|
* Implements ConnectRPC protobuf wire format decoding
|
||||||
|
*/
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
while (pos < buffer.length) {
|
||||||
|
const b = buffer[pos];
|
||||||
|
result |= (b & 0x7f) << shift;
|
||||||
|
pos++;
|
||||||
|
if (!(b & 0x80)) break;
|
||||||
|
shift += 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [result, pos];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode a single protobuf field
|
||||||
|
* Returns [fieldNum, wireType, value, newOffset]
|
||||||
|
*/
|
||||||
|
export function decodeField(
|
||||||
|
buffer: Uint8Array,
|
||||||
|
offset: number
|
||||||
|
): [number | null, WireType | null, Uint8Array | number | null, number] {
|
||||||
|
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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [fieldNum, wireType, value, pos];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode a protobuf message into a map of fields
|
||||||
|
*/
|
||||||
|
export function decodeMessage(
|
||||||
|
data: Uint8Array
|
||||||
|
): 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) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse ConnectRPC frame from buffer
|
||||||
|
* Returns frame data or null if incomplete
|
||||||
|
*/
|
||||||
|
export function parseConnectRPCFrame(buffer: Buffer): {
|
||||||
|
flags: number;
|
||||||
|
length: number;
|
||||||
|
payload: Uint8Array;
|
||||||
|
consumed: number;
|
||||||
|
} | null {
|
||||||
|
if (buffer.length < 5) return null;
|
||||||
|
|
||||||
|
const flags = buffer[0];
|
||||||
|
const length =
|
||||||
|
(buffer[1] << 24) | (buffer[2] << 16) | (buffer[3] << 8) | buffer[4];
|
||||||
|
|
||||||
|
if (buffer.length < 5 + length) return null;
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
} | null {
|
||||||
|
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 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 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 (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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
} {
|
||||||
|
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 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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { text: null, error: null, toolCall: null, thinking: null };
|
||||||
|
} catch {
|
||||||
|
return { text: null, error: null, toolCall: null, thinking: null };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
/**
|
||||||
|
* Cursor Protobuf Encoder
|
||||||
|
* Implements ConnectRPC protobuf wire format encoding
|
||||||
|
*/
|
||||||
|
|
||||||
|
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";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode a varint (variable-length integer)
|
||||||
|
*/
|
||||||
|
export function encodeVarint(value: number): Uint8Array {
|
||||||
|
const bytes: number[] = [];
|
||||||
|
let val = value >>> 0; // Ensure unsigned
|
||||||
|
while (val >= 0x80) {
|
||||||
|
bytes.push((val & 0x7f) | 0x80);
|
||||||
|
val >>>= 7;
|
||||||
|
}
|
||||||
|
bytes.push(val & 0x7f);
|
||||||
|
return new Uint8Array(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode a protobuf field (tag + value)
|
||||||
|
*/
|
||||||
|
export function encodeField(
|
||||||
|
fieldNum: number,
|
||||||
|
wireType: WireType,
|
||||||
|
value: number | string | Uint8Array
|
||||||
|
): Uint8Array {
|
||||||
|
const tag = (fieldNum << 3) | wireType;
|
||||||
|
const tagBytes = encodeVarint(tag);
|
||||||
|
|
||||||
|
if (wireType === WIRE_TYPE.VARINT) {
|
||||||
|
const valueBytes = encodeVarint(value as number);
|
||||||
|
return concatArrays(tagBytes, valueBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wireType === WIRE_TYPE.LEN) {
|
||||||
|
const dataBytes =
|
||||||
|
typeof value === "string"
|
||||||
|
? new TextEncoder().encode(value)
|
||||||
|
: value instanceof Uint8Array
|
||||||
|
? value
|
||||||
|
: new Uint8Array(0);
|
||||||
|
|
||||||
|
const lengthBytes = encodeVarint(dataBytes.length);
|
||||||
|
return concatArrays(tagBytes, lengthBytes, dataBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Uint8Array(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Concatenate multiple Uint8Arrays
|
||||||
|
*/
|
||||||
|
function concatArrays(...arrays: Uint8Array[]): Uint8Array {
|
||||||
|
const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);
|
||||||
|
const result = new Uint8Array(totalLength);
|
||||||
|
let offset = 0;
|
||||||
|
for (const arr of arrays) {
|
||||||
|
result.set(arr, offset);
|
||||||
|
offset += arr.length;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode a tool result
|
||||||
|
*/
|
||||||
|
export function encodeToolResult(toolResult: CursorToolResult): Uint8Array {
|
||||||
|
const toolCallId = toolResult.tool_call_id || "";
|
||||||
|
const toolName = toolResult.name || "";
|
||||||
|
const toolIndex = toolResult.index || 0;
|
||||||
|
const rawArgs = toolResult.raw_args || "{}";
|
||||||
|
|
||||||
|
return concatArrays(
|
||||||
|
encodeField(FIELD.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[]
|
||||||
|
): 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)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: [])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode instruction text
|
||||||
|
*/
|
||||||
|
export function encodeInstruction(text: string): Uint8Array {
|
||||||
|
return text
|
||||||
|
? encodeField(FIELD.INSTRUCTION_TEXT, WIRE_TYPE.LEN, text)
|
||||||
|
: new Uint8Array(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode model information
|
||||||
|
*/
|
||||||
|
export function encodeModel(modelName: string): Uint8Array {
|
||||||
|
return concatArrays(
|
||||||
|
encodeField(FIELD.MODEL_NAME, WIRE_TYPE.LEN, modelName),
|
||||||
|
encodeField(FIELD.MODEL_EMPTY, WIRE_TYPE.LEN, new Uint8Array(0))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode cursor settings
|
||||||
|
*/
|
||||||
|
export function encodeCursorSetting(): Uint8Array {
|
||||||
|
const unknown6 = concatArrays(
|
||||||
|
encodeField(FIELD.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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode MCP tool
|
||||||
|
*/
|
||||||
|
export function encodeMcpTool(tool: CursorTool): Uint8Array {
|
||||||
|
const toolName = tool.function?.name || tool.name || "";
|
||||||
|
const toolDesc = tool.function?.description || tool.description || "";
|
||||||
|
const inputSchema = tool.function?.parameters || tool.input_schema || {};
|
||||||
|
|
||||||
|
return concatArrays(
|
||||||
|
...(toolName
|
||||||
|
? [encodeField(FIELD.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;
|
||||||
|
|
||||||
|
if (compress) {
|
||||||
|
finalPayload = new Uint8Array(zlib.gzipSync(Buffer.from(payload)));
|
||||||
|
flags = COMPRESS_FLAG.GZIP;
|
||||||
|
}
|
||||||
|
|
||||||
|
const frame = new Uint8Array(5 + finalPayload.length);
|
||||||
|
frame[0] = flags;
|
||||||
|
frame[1] = (finalPayload.length >> 24) & 0xff;
|
||||||
|
frame[2] = (finalPayload.length >> 16) & 0xff;
|
||||||
|
frame[3] = (finalPayload.length >> 8) & 0xff;
|
||||||
|
frame[4] = finalPayload.length & 0xff;
|
||||||
|
frame.set(finalPayload, 5);
|
||||||
|
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
/**
|
||||||
|
* Cursor Protobuf Schema Constants
|
||||||
|
* Field definitions and wire types for ConnectRPC protocol
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Wire types for protobuf encoding */
|
||||||
|
export const WIRE_TYPE = {
|
||||||
|
VARINT: 0,
|
||||||
|
FIXED64: 1,
|
||||||
|
LEN: 2,
|
||||||
|
FIXED32: 5,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** Message role constants */
|
||||||
|
export const ROLE = {
|
||||||
|
USER: 1,
|
||||||
|
ASSISTANT: 2,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** Unified mode constants */
|
||||||
|
export const UNIFIED_MODE = {
|
||||||
|
CHAT: 1,
|
||||||
|
AGENT: 2,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** Thinking level constants */
|
||||||
|
export const THINKING_LEVEL = {
|
||||||
|
UNSPECIFIED: 0,
|
||||||
|
MEDIUM: 1,
|
||||||
|
HIGH: 2,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** Field numbers for all protobuf messages */
|
||||||
|
export const FIELD = {
|
||||||
|
// 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,
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
|
||||||
|
// Model
|
||||||
|
MODEL_NAME: 1,
|
||||||
|
MODEL_EMPTY: 4,
|
||||||
|
|
||||||
|
// 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.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,
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
|
||||||
|
// MCPParams
|
||||||
|
MCP_TOOLS_LIST: 1,
|
||||||
|
|
||||||
|
// MCPParams.Tool (nested)
|
||||||
|
MCP_NESTED_NAME: 1,
|
||||||
|
MCP_NESTED_PARAMS: 3,
|
||||||
|
|
||||||
|
// StreamUnifiedChatResponse
|
||||||
|
RESPONSE_TEXT: 1,
|
||||||
|
THINKING: 25,
|
||||||
|
|
||||||
|
// 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 FieldNumber = (typeof FIELD)[keyof typeof FIELD];
|
||||||
|
|
||||||
|
/** Cursor tool definition */
|
||||||
|
export interface CursorTool {
|
||||||
|
function?: {
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
parameters?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
input_schema?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Cursor tool result */
|
||||||
|
export interface CursorToolResult {
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Formatted message for encoding */
|
||||||
|
export interface FormattedMessage {
|
||||||
|
content: string;
|
||||||
|
role: RoleType;
|
||||||
|
messageId: string;
|
||||||
|
isLast: boolean;
|
||||||
|
hasTools: boolean;
|
||||||
|
toolResults: CursorToolResult[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Message ID structure */
|
||||||
|
export interface MessageId {
|
||||||
|
messageId: string;
|
||||||
|
role: RoleType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Compression flags for ConnectRPC frames */
|
||||||
|
export const COMPRESS_FLAG = {
|
||||||
|
NONE: 0x00,
|
||||||
|
GZIP: 0x01,
|
||||||
|
} as const;
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
/**
|
||||||
|
* Cursor Protobuf Main Module
|
||||||
|
* Exports encoder/decoder functions and builds complete requests
|
||||||
|
*/
|
||||||
|
|
||||||
|
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";
|
||||||
|
import {
|
||||||
|
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";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build complete chat request protobuf
|
||||||
|
*/
|
||||||
|
export function encodeRequest(
|
||||||
|
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[] = [];
|
||||||
|
|
||||||
|
// 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 || [],
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// 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 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))]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build chat request wrapped in top-level message
|
||||||
|
*/
|
||||||
|
export function buildChatRequest(
|
||||||
|
messages: CursorMessage[],
|
||||||
|
modelName: string,
|
||||||
|
tools: CursorTool[] = [],
|
||||||
|
reasoningEffort: string | null = null
|
||||||
|
): Uint8Array {
|
||||||
|
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
|
||||||
|
): Uint8Array {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-export all functions
|
||||||
|
export {
|
||||||
|
encodeVarint,
|
||||||
|
encodeField,
|
||||||
|
encodeMessage,
|
||||||
|
encodeInstruction,
|
||||||
|
encodeModel,
|
||||||
|
encodeCursorSetting,
|
||||||
|
encodeMetadata,
|
||||||
|
encodeMessageId,
|
||||||
|
encodeMcpTool,
|
||||||
|
wrapConnectRPCFrame,
|
||||||
|
decodeVarint,
|
||||||
|
decodeField,
|
||||||
|
decodeMessage,
|
||||||
|
parseConnectRPCFrame,
|
||||||
|
extractTextFromResponse,
|
||||||
|
};
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
/**
|
||||||
|
* OpenAI to Cursor Request Translator
|
||||||
|
* Converts OpenAI messages to Cursor format
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 };
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** OpenAI request body */
|
||||||
|
interface OpenAIRequestBody {
|
||||||
|
messages: OpenAIMessage[];
|
||||||
|
tools?: CursorTool[];
|
||||||
|
reasoning_effort?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert OpenAI messages to Cursor format with native tool_results support
|
||||||
|
* - system → user with [System Instructions] prefix
|
||||||
|
* - tool → accumulate into tool_results array for next user/assistant message
|
||||||
|
* - assistant with tool_calls → keep tool_calls structure (Cursor supports it natively)
|
||||||
|
*/
|
||||||
|
function convertMessages(messages: OpenAIMessage[]): CursorMessage[] {
|
||||||
|
const result: CursorMessage[] = [];
|
||||||
|
let pendingToolResults: CursorToolResult[] = [];
|
||||||
|
|
||||||
|
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 === "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 || "";
|
||||||
|
|
||||||
|
// 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 (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;
|
||||||
|
|
||||||
|
// 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 || "",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Attach pending tool results to this message
|
||||||
|
if (pendingToolResults.length > 0) {
|
||||||
|
msgObj.tool_results = pendingToolResults;
|
||||||
|
pendingToolResults = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push(msgObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform OpenAI request to Cursor format
|
||||||
|
* Returns modified body with converted messages
|
||||||
|
*/
|
||||||
|
export function buildCursorRequest(
|
||||||
|
model: string,
|
||||||
|
body: OpenAIRequestBody,
|
||||||
|
stream: boolean,
|
||||||
|
credentials: unknown
|
||||||
|
): {
|
||||||
|
messages: CursorMessage[];
|
||||||
|
tools?: CursorTool[];
|
||||||
|
} {
|
||||||
|
const messages = convertMessages(body.messages || []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...body,
|
||||||
|
messages,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
/**
|
||||||
|
* Cursor IDE Type Definitions
|
||||||
|
*
|
||||||
|
* TypeScript interfaces for the Cursor module.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cursor authentication credentials
|
||||||
|
*/
|
||||||
|
export interface CursorCredentials {
|
||||||
|
/** Access token from Cursor IDE */
|
||||||
|
accessToken: string;
|
||||||
|
/** Machine ID for checksum generation */
|
||||||
|
machineId: string;
|
||||||
|
/** User email (if available from token) */
|
||||||
|
email?: string;
|
||||||
|
/** User ID (if available from token) */
|
||||||
|
userId?: string;
|
||||||
|
/** How credentials were obtained */
|
||||||
|
authMethod: 'auto-detect' | 'manual';
|
||||||
|
/** ISO datetime when credentials were imported */
|
||||||
|
importedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cursor authentication status
|
||||||
|
*/
|
||||||
|
export interface CursorAuthStatus {
|
||||||
|
/** Whether user is authenticated */
|
||||||
|
authenticated: boolean;
|
||||||
|
/** Current credentials (if authenticated) */
|
||||||
|
credentials?: CursorCredentials;
|
||||||
|
/** Hours since credentials were imported (if available) */
|
||||||
|
tokenAge?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cursor daemon/process status
|
||||||
|
*/
|
||||||
|
export interface CursorDaemonStatus {
|
||||||
|
/** Whether daemon is running */
|
||||||
|
running: boolean;
|
||||||
|
/** Port number daemon is listening on */
|
||||||
|
port: number;
|
||||||
|
/** Process ID (if available) */
|
||||||
|
pid?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cursor AI model
|
||||||
|
*/
|
||||||
|
export interface CursorModel {
|
||||||
|
/** Model ID */
|
||||||
|
id: string;
|
||||||
|
/** Display name */
|
||||||
|
name: string;
|
||||||
|
/** Provider (e.g., 'openai', 'anthropic') */
|
||||||
|
provider: string;
|
||||||
|
/** Whether this is the default model */
|
||||||
|
isDefault?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Message role
|
||||||
|
*/
|
||||||
|
export type MessageRole = 'user' | 'assistant';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cursor message for protobuf
|
||||||
|
*/
|
||||||
|
export interface CursorMessage {
|
||||||
|
/** Message role */
|
||||||
|
role: MessageRole;
|
||||||
|
/** Message content */
|
||||||
|
content: string;
|
||||||
|
/** Tool calls (if any) */
|
||||||
|
tool_calls?: CursorToolCall[];
|
||||||
|
/** Tool results (if any) */
|
||||||
|
tool_results?: CursorToolResult[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cursor tool call
|
||||||
|
*/
|
||||||
|
export interface CursorToolCall {
|
||||||
|
/** Unique ID for this tool call */
|
||||||
|
id: string;
|
||||||
|
/** Type of tool call */
|
||||||
|
type: 'function';
|
||||||
|
/** Function details */
|
||||||
|
function: {
|
||||||
|
/** Function name */
|
||||||
|
name: string;
|
||||||
|
/** JSON-encoded arguments */
|
||||||
|
arguments: string;
|
||||||
|
};
|
||||||
|
/** Whether this is the last tool call in sequence */
|
||||||
|
isLast?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cursor tool result
|
||||||
|
*/
|
||||||
|
export interface CursorToolResult {
|
||||||
|
/** ID of the tool call this result is for */
|
||||||
|
tool_call_id: string;
|
||||||
|
/** Tool name */
|
||||||
|
name: string;
|
||||||
|
/** Result index */
|
||||||
|
index: number;
|
||||||
|
/** Raw arguments */
|
||||||
|
raw_args: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result from protobuf extraction
|
||||||
|
*/
|
||||||
|
export interface ProtobufExtractResult {
|
||||||
|
/** Extracted text content */
|
||||||
|
text: string | null;
|
||||||
|
/** Error message (if extraction failed) */
|
||||||
|
error: string | null;
|
||||||
|
/** Extracted tool call (if any) */
|
||||||
|
toolCall: CursorToolCall | null;
|
||||||
|
/** Thinking/reasoning content (if any) */
|
||||||
|
thinking: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-detection result
|
||||||
|
*/
|
||||||
|
export interface AutoDetectResult {
|
||||||
|
/** Whether tokens were found */
|
||||||
|
found: boolean;
|
||||||
|
/** Access token (if found) */
|
||||||
|
accessToken?: string;
|
||||||
|
/** Machine ID (if found) */
|
||||||
|
machineId?: string;
|
||||||
|
/** Error message (if detection failed) */
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user