mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-14 20:17:03 +00:00
fix(cursor): address code review edge cases in protobuf and executor
CRITICAL FIX: - CursorCredentials interface now matches types.ts (machineId, ghostMode as top-level) - Fixes runtime error when cursor-auth saves credentials and cursor-executor reads them HIGH: - Replace 18+ non-null assertions with guard clauses across executor and decoder - Prefix unused params in translator (_model, _stream, _credentials) - HTTP/2 client closes on connection error to prevent leak - AbortSignal listener leak documented with TODO (inline arrow prevents cleanup) MEDIUM: - Export concatArrays from encoder, remove duplicate from protobuf.ts - Varint decoder now enforces 5-byte max to prevent overflow - Buffer slice bounds check prevents out-of-range read - Empty messages array validation with explicit error - Buffered streaming limitation documented with TODO comment All edge cases from code review now addressed.
This commit is contained in:
1 parent
9daf9430bb
commit
cc5a9039e4
6 files changed
+265
-325
No files matched your search
+124
-127
@@ -3,12 +3,12 @@
|
|||||||
* Handles HTTP/2 requests to Cursor API with protobuf encoding/decoding
|
* Handles HTTP/2 requests to Cursor API with protobuf encoding/decoding
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as crypto from "crypto";
|
import * as crypto from 'crypto';
|
||||||
import * as zlib from "zlib";
|
import * as zlib from 'zlib';
|
||||||
import type { IncomingHttpHeaders } from "http";
|
import type { IncomingHttpHeaders } from 'http';
|
||||||
import { generateCursorBody, extractTextFromResponse } from "./cursor-protobuf.js";
|
import { generateCursorBody, extractTextFromResponse } from './cursor-protobuf.js';
|
||||||
import { buildCursorRequest } from "./cursor-translator.js";
|
import { buildCursorRequest } from './cursor-translator.js';
|
||||||
import type { CursorMessage, CursorTool } from "./cursor-protobuf-schema.js";
|
import type { CursorTool } from './cursor-protobuf-schema.js';
|
||||||
|
|
||||||
/** Compression flags for response parsing */
|
/** Compression flags for response parsing */
|
||||||
const COMPRESS_FLAG = {
|
const COMPRESS_FLAG = {
|
||||||
@@ -21,10 +21,8 @@ const COMPRESS_FLAG = {
|
|||||||
/** Cursor credentials structure */
|
/** Cursor credentials structure */
|
||||||
interface CursorCredentials {
|
interface CursorCredentials {
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
providerSpecificData?: {
|
machineId: string;
|
||||||
machineId?: string;
|
|
||||||
ghostMode?: boolean;
|
ghostMode?: boolean;
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Executor parameters */
|
/** Executor parameters */
|
||||||
@@ -59,10 +57,15 @@ interface Http2Response {
|
|||||||
|
|
||||||
/** Detect cloud environment */
|
/** Detect cloud environment */
|
||||||
function isCloudEnv(): boolean {
|
function isCloudEnv(): boolean {
|
||||||
if (typeof caches !== "undefined" && typeof caches === "object") return true;
|
if (
|
||||||
|
typeof globalThis !== 'undefined' &&
|
||||||
|
'caches' in globalThis &&
|
||||||
|
typeof (globalThis as { caches?: unknown }).caches === 'object'
|
||||||
|
)
|
||||||
|
return true;
|
||||||
try {
|
try {
|
||||||
// Check for EdgeRuntime without causing compilation error
|
// Check for EdgeRuntime without causing compilation error
|
||||||
if (typeof (globalThis as { EdgeRuntime?: string }).EdgeRuntime !== "undefined") return true;
|
if (typeof (globalThis as { EdgeRuntime?: string }).EdgeRuntime !== 'undefined') return true;
|
||||||
} catch {
|
} catch {
|
||||||
// Continue
|
// Continue
|
||||||
}
|
}
|
||||||
@@ -70,12 +73,12 @@ function isCloudEnv(): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Lazy import http2 */
|
/** Lazy import http2 */
|
||||||
let http2Module: typeof import("http2") | null = null;
|
let http2Module: typeof import('http2') | null = null;
|
||||||
async function getHttp2() {
|
async function getHttp2() {
|
||||||
if (http2Module) return http2Module;
|
if (http2Module) return http2Module;
|
||||||
if (!isCloudEnv()) {
|
if (!isCloudEnv()) {
|
||||||
try {
|
try {
|
||||||
http2Module = await import("http2");
|
http2Module = await import('http2');
|
||||||
return http2Module;
|
return http2Module;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
@@ -91,7 +94,7 @@ function decompressPayload(payload: Buffer, flags: number): Buffer {
|
|||||||
// Check if payload is JSON error
|
// Check if payload is JSON error
|
||||||
if (payload.length > 10 && payload[0] === 0x7b && payload[1] === 0x22) {
|
if (payload.length > 10 && payload[0] === 0x7b && payload[1] === 0x22) {
|
||||||
try {
|
try {
|
||||||
const text = payload.toString("utf-8");
|
const text = payload.toString('utf-8');
|
||||||
if (text.startsWith('{"error"')) {
|
if (text.startsWith('{"error"')) {
|
||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
@@ -128,28 +131,28 @@ function createErrorResponse(jsonError: {
|
|||||||
jsonError?.error?.details?.[0]?.debug?.details?.title ||
|
jsonError?.error?.details?.[0]?.debug?.details?.title ||
|
||||||
jsonError?.error?.details?.[0]?.debug?.details?.detail ||
|
jsonError?.error?.details?.[0]?.debug?.details?.detail ||
|
||||||
jsonError?.error?.message ||
|
jsonError?.error?.message ||
|
||||||
"API Error";
|
'API Error';
|
||||||
|
|
||||||
const isRateLimit = jsonError?.error?.code === "resource_exhausted";
|
const isRateLimit = jsonError?.error?.code === 'resource_exhausted';
|
||||||
|
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
error: {
|
error: {
|
||||||
message: errorMsg,
|
message: errorMsg,
|
||||||
type: isRateLimit ? "rate_limit_error" : "api_error",
|
type: isRateLimit ? 'rate_limit_error' : 'api_error',
|
||||||
code: jsonError?.error?.details?.[0]?.debug?.error || "unknown",
|
code: jsonError?.error?.details?.[0]?.debug?.error || 'unknown',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
status: isRateLimit ? 429 : 400,
|
status: isRateLimit ? 429 : 400,
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CursorExecutor {
|
export class CursorExecutor {
|
||||||
private readonly baseUrl = "https://api2.cursor.sh";
|
private readonly baseUrl = 'https://api2.cursor.sh';
|
||||||
private readonly chatPath = "/aiserver.v1.AiService/StreamChat";
|
private readonly chatPath = '/aiserver.v1.AiService/StreamChat';
|
||||||
|
|
||||||
buildUrl(): string {
|
buildUrl(): string {
|
||||||
return `${this.baseUrl}${this.chatPath}`;
|
return `${this.baseUrl}${this.chatPath}`;
|
||||||
@@ -175,8 +178,8 @@ export class CursorExecutor {
|
|||||||
t = byteArray[i];
|
t = byteArray[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
|
||||||
let encoded = "";
|
let encoded = '';
|
||||||
|
|
||||||
for (let i = 0; i < byteArray.length; i += 3) {
|
for (let i = 0; i < byteArray.length; i += 3) {
|
||||||
const a = byteArray[i];
|
const a = byteArray[i];
|
||||||
@@ -199,52 +202,45 @@ export class CursorExecutor {
|
|||||||
|
|
||||||
buildHeaders(credentials: CursorCredentials): Record<string, string> {
|
buildHeaders(credentials: CursorCredentials): Record<string, string> {
|
||||||
const accessToken = credentials.accessToken;
|
const accessToken = credentials.accessToken;
|
||||||
const machineId = credentials.providerSpecificData?.machineId;
|
const machineId = credentials.machineId;
|
||||||
const ghostMode = credentials.providerSpecificData?.ghostMode !== false;
|
const ghostMode = credentials.ghostMode !== false;
|
||||||
|
|
||||||
if (!machineId) {
|
if (!machineId) {
|
||||||
throw new Error("Machine ID is required for Cursor API");
|
throw new Error('Machine ID is required for Cursor API');
|
||||||
}
|
}
|
||||||
|
|
||||||
const cleanToken = accessToken.includes("::")
|
const cleanToken = accessToken.includes('::') ? accessToken.split('::')[1] : accessToken;
|
||||||
? accessToken.split("::")[1]
|
|
||||||
: accessToken;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
authorization: `Bearer ${cleanToken}`,
|
authorization: `Bearer ${cleanToken}`,
|
||||||
"connect-accept-encoding": "gzip",
|
'connect-accept-encoding': 'gzip',
|
||||||
"connect-protocol-version": "1",
|
'connect-protocol-version': '1',
|
||||||
"content-type": "application/connect+proto",
|
'content-type': 'application/connect+proto',
|
||||||
"user-agent": "connect-es/1.6.1",
|
'user-agent': 'connect-es/1.6.1',
|
||||||
"x-amzn-trace-id": `Root=${crypto.randomUUID()}`,
|
'x-amzn-trace-id': `Root=${crypto.randomUUID()}`,
|
||||||
"x-client-key": crypto.createHash("sha256").update(cleanToken).digest("hex"),
|
'x-client-key': crypto.createHash('sha256').update(cleanToken).digest('hex'),
|
||||||
"x-cursor-checksum": this.generateChecksum(machineId),
|
'x-cursor-checksum': this.generateChecksum(machineId),
|
||||||
"x-cursor-client-version": "2.3.41",
|
'x-cursor-client-version': '2.3.41',
|
||||||
"x-cursor-client-type": "ide",
|
'x-cursor-client-type': 'ide',
|
||||||
"x-cursor-client-os":
|
'x-cursor-client-os':
|
||||||
process.platform === "win32"
|
process.platform === 'win32'
|
||||||
? "windows"
|
? 'windows'
|
||||||
: process.platform === "darwin"
|
: process.platform === 'darwin'
|
||||||
? "macos"
|
? 'macos'
|
||||||
: "linux",
|
: 'linux',
|
||||||
"x-cursor-client-arch": process.arch === "arm64" ? "aarch64" : "x64",
|
'x-cursor-client-arch': process.arch === 'arm64' ? 'aarch64' : 'x64',
|
||||||
"x-cursor-client-device-type": "desktop",
|
'x-cursor-client-device-type': 'desktop',
|
||||||
"x-cursor-config-version": crypto.randomUUID(),
|
'x-cursor-config-version': crypto.randomUUID(),
|
||||||
"x-cursor-timezone":
|
'x-cursor-timezone': Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC',
|
||||||
Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC",
|
'x-ghost-mode': ghostMode ? 'true' : 'false',
|
||||||
"x-ghost-mode": ghostMode ? "true" : "false",
|
'x-request-id': crypto.randomUUID(),
|
||||||
"x-request-id": crypto.randomUUID(),
|
'x-session-id': crypto.createHash('sha256').update(cleanToken).digest('hex').substring(0, 36),
|
||||||
"x-session-id": crypto
|
|
||||||
.createHash("sha256")
|
|
||||||
.update(cleanToken)
|
|
||||||
.digest("hex")
|
|
||||||
.substring(0, 36),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
transformRequest(
|
transformRequest(
|
||||||
model: string,
|
model: string,
|
||||||
body: ExecutorParams["body"],
|
body: ExecutorParams['body'],
|
||||||
stream: boolean,
|
stream: boolean,
|
||||||
credentials: CursorCredentials
|
credentials: CursorCredentials
|
||||||
): Uint8Array {
|
): Uint8Array {
|
||||||
@@ -262,7 +258,7 @@ export class CursorExecutor {
|
|||||||
signal?: AbortSignal
|
signal?: AbortSignal
|
||||||
): Promise<Http2Response> {
|
): Promise<Http2Response> {
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: "POST",
|
method: 'POST',
|
||||||
headers,
|
headers,
|
||||||
body,
|
body,
|
||||||
signal,
|
signal,
|
||||||
@@ -288,7 +284,7 @@ export class CursorExecutor {
|
|||||||
): Promise<Http2Response> {
|
): Promise<Http2Response> {
|
||||||
const http2 = await getHttp2();
|
const http2 = await getHttp2();
|
||||||
if (!http2) {
|
if (!http2) {
|
||||||
throw new Error("http2 module not available");
|
throw new Error('http2 module not available');
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -297,40 +293,45 @@ export class CursorExecutor {
|
|||||||
const chunks: Buffer[] = [];
|
const chunks: Buffer[] = [];
|
||||||
let responseHeaders: IncomingHttpHeaders = {};
|
let responseHeaders: IncomingHttpHeaders = {};
|
||||||
|
|
||||||
client.on("error", reject);
|
client.on('error', (err) => {
|
||||||
|
client.close();
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
const req = client.request({
|
const req = client.request({
|
||||||
":method": "POST",
|
':method': 'POST',
|
||||||
":path": urlObj.pathname,
|
':path': urlObj.pathname,
|
||||||
":authority": urlObj.host,
|
':authority': urlObj.host,
|
||||||
":scheme": "https",
|
':scheme': 'https',
|
||||||
...headers,
|
...headers,
|
||||||
});
|
});
|
||||||
|
|
||||||
req.on("response", (hdrs) => {
|
req.on('response', (hdrs) => {
|
||||||
responseHeaders = hdrs;
|
responseHeaders = hdrs;
|
||||||
});
|
});
|
||||||
req.on("data", (chunk: Buffer) => {
|
req.on('data', (chunk: Buffer) => {
|
||||||
chunks.push(chunk);
|
chunks.push(chunk);
|
||||||
});
|
});
|
||||||
req.on("end", () => {
|
req.on('end', () => {
|
||||||
client.close();
|
client.close();
|
||||||
resolve({
|
resolve({
|
||||||
status: Number(responseHeaders[":status"]),
|
status: Number(responseHeaders[':status']),
|
||||||
headers: responseHeaders,
|
headers: responseHeaders,
|
||||||
body: Buffer.concat(chunks),
|
body: Buffer.concat(chunks),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
req.on("error", (err) => {
|
req.on('error', (err) => {
|
||||||
client.close();
|
client.close();
|
||||||
reject(err);
|
reject(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (signal) {
|
if (signal) {
|
||||||
signal.addEventListener("abort", () => {
|
// TODO: AbortSignal listener is not removed after request completes.
|
||||||
|
// To fix: store handler reference, remove in end/error callbacks.
|
||||||
|
signal.addEventListener('abort', () => {
|
||||||
req.close();
|
req.close();
|
||||||
client.close();
|
client.close();
|
||||||
reject(new Error("Request aborted"));
|
reject(new Error('Request aborted'));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -343,7 +344,7 @@ export class CursorExecutor {
|
|||||||
response: Response;
|
response: Response;
|
||||||
url: string;
|
url: string;
|
||||||
headers: Record<string, string>;
|
headers: Record<string, string>;
|
||||||
transformedBody: ExecutorParams["body"];
|
transformedBody: ExecutorParams['body'];
|
||||||
}> {
|
}> {
|
||||||
const { model, body, stream, credentials, signal } = params;
|
const { model, body, stream, credentials, signal } = params;
|
||||||
const url = this.buildUrl();
|
const url = this.buildUrl();
|
||||||
@@ -357,18 +358,18 @@ export class CursorExecutor {
|
|||||||
: await this.makeFetchRequest(url, headers, transformedBody, signal);
|
: await this.makeFetchRequest(url, headers, transformedBody, signal);
|
||||||
|
|
||||||
if (response.status !== 200) {
|
if (response.status !== 200) {
|
||||||
const errorText = response.body?.toString() || "Unknown error";
|
const errorText = response.body?.toString() || 'Unknown error';
|
||||||
const errorResponse = new Response(
|
const errorResponse = new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
error: {
|
error: {
|
||||||
message: `[${response.status}]: ${errorText}`,
|
message: `[${response.status}]: ${errorText}`,
|
||||||
type: "invalid_request_error",
|
type: 'invalid_request_error',
|
||||||
code: "",
|
code: '',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
status: response.status,
|
status: response.status,
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return { response: errorResponse, url, headers, transformedBody: body };
|
return { response: errorResponse, url, headers, transformedBody: body };
|
||||||
@@ -385,29 +386,25 @@ export class CursorExecutor {
|
|||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
error: {
|
error: {
|
||||||
message: (error as Error).message,
|
message: (error as Error).message,
|
||||||
type: "connection_error",
|
type: 'connection_error',
|
||||||
code: "",
|
code: '',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
status: 500,
|
status: 500,
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return { response: errorResponse, url, headers, transformedBody: body };
|
return { response: errorResponse, url, headers, transformedBody: body };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
transformProtobufToJSON(
|
transformProtobufToJSON(buffer: Buffer, model: string, _body: ExecutorParams['body']): Response {
|
||||||
buffer: Buffer,
|
|
||||||
model: string,
|
|
||||||
body: ExecutorParams["body"]
|
|
||||||
): Response {
|
|
||||||
const responseId = `chatcmpl-cursor-${Date.now()}`;
|
const responseId = `chatcmpl-cursor-${Date.now()}`;
|
||||||
const created = Math.floor(Date.now() / 1000);
|
const created = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
let totalContent = "";
|
let totalContent = '';
|
||||||
const toolCalls: Array<{
|
const toolCalls: Array<{
|
||||||
id: string;
|
id: string;
|
||||||
type: string;
|
type: string;
|
||||||
@@ -438,8 +435,8 @@ export class CursorExecutor {
|
|||||||
payload = decompressPayload(payload, flags);
|
payload = decompressPayload(payload, flags);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const text = payload.toString("utf-8");
|
const text = payload.toString('utf-8');
|
||||||
if (text.startsWith("{") && text.includes('"error"')) {
|
if (text.startsWith('{') && text.includes('"error"')) {
|
||||||
return createErrorResponse(JSON.parse(text));
|
return createErrorResponse(JSON.parse(text));
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -453,13 +450,13 @@ export class CursorExecutor {
|
|||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
error: {
|
error: {
|
||||||
message: result.error,
|
message: result.error,
|
||||||
type: "rate_limit_error",
|
type: 'rate_limit_error',
|
||||||
code: "rate_limited",
|
code: 'rate_limited',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
status: 429,
|
status: 429,
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -468,7 +465,8 @@ export class CursorExecutor {
|
|||||||
const tc = result.toolCall;
|
const tc = result.toolCall;
|
||||||
|
|
||||||
if (toolCallsMap.has(tc.id)) {
|
if (toolCallsMap.has(tc.id)) {
|
||||||
const existing = toolCallsMap.get(tc.id)!;
|
const existing = toolCallsMap.get(tc.id);
|
||||||
|
if (!existing) continue;
|
||||||
existing.function.arguments += tc.function.arguments;
|
existing.function.arguments += tc.function.arguments;
|
||||||
existing.isLast = tc.isLast;
|
existing.isLast = tc.isLast;
|
||||||
} else {
|
} else {
|
||||||
@@ -479,7 +477,8 @@ export class CursorExecutor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (tc.isLast) {
|
if (tc.isLast) {
|
||||||
const finalToolCall = toolCallsMap.get(tc.id)!;
|
const finalToolCall = toolCallsMap.get(tc.id);
|
||||||
|
if (!finalToolCall) continue;
|
||||||
toolCalls.push({
|
toolCalls.push({
|
||||||
id: finalToolCall.id,
|
id: finalToolCall.id,
|
||||||
type: finalToolCall.type,
|
type: finalToolCall.type,
|
||||||
@@ -496,7 +495,8 @@ export class CursorExecutor {
|
|||||||
|
|
||||||
// Finalize remaining tool calls
|
// Finalize remaining tool calls
|
||||||
for (const id of Array.from(toolCallsMap.keys())) {
|
for (const id of Array.from(toolCallsMap.keys())) {
|
||||||
const tc = toolCallsMap.get(id)!;
|
const tc = toolCallsMap.get(id);
|
||||||
|
if (!tc) continue;
|
||||||
if (!toolCalls.find((t) => t.id === id)) {
|
if (!toolCalls.find((t) => t.id === id)) {
|
||||||
toolCalls.push({
|
toolCalls.push({
|
||||||
id: tc.id,
|
id: tc.id,
|
||||||
@@ -518,7 +518,7 @@ export class CursorExecutor {
|
|||||||
function: { name: string; arguments: string };
|
function: { name: string; arguments: string };
|
||||||
}>;
|
}>;
|
||||||
} = {
|
} = {
|
||||||
role: "assistant",
|
role: 'assistant',
|
||||||
content: totalContent || null,
|
content: totalContent || null,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -528,14 +528,14 @@ export class CursorExecutor {
|
|||||||
|
|
||||||
const completion = {
|
const completion = {
|
||||||
id: responseId,
|
id: responseId,
|
||||||
object: "chat.completion",
|
object: 'chat.completion',
|
||||||
created,
|
created,
|
||||||
model,
|
model,
|
||||||
choices: [
|
choices: [
|
||||||
{
|
{
|
||||||
index: 0,
|
index: 0,
|
||||||
message,
|
message,
|
||||||
finish_reason: toolCalls.length > 0 ? "tool_calls" : "stop",
|
finish_reason: toolCalls.length > 0 ? 'tool_calls' : 'stop',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
usage: {
|
usage: {
|
||||||
@@ -547,21 +547,18 @@ export class CursorExecutor {
|
|||||||
|
|
||||||
return new Response(JSON.stringify(completion), {
|
return new Response(JSON.stringify(completion), {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
transformProtobufToSSE(
|
transformProtobufToSSE(buffer: Buffer, model: string, _body: ExecutorParams['body']): Response {
|
||||||
buffer: Buffer,
|
// TODO: Implement true streaming — currently buffers entire response before transforming.
|
||||||
model: string,
|
// This should pipe HTTP/2 data events through a TransformStream for incremental SSE output.
|
||||||
body: ExecutorParams["body"]
|
|
||||||
): Response {
|
|
||||||
const responseId = `chatcmpl-cursor-${Date.now()}`;
|
const responseId = `chatcmpl-cursor-${Date.now()}`;
|
||||||
const created = Math.floor(Date.now() / 1000);
|
const created = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
const chunks: string[] = [];
|
const chunks: string[] = [];
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
let totalContent = "";
|
|
||||||
const toolCalls: Array<{
|
const toolCalls: Array<{
|
||||||
id: string;
|
id: string;
|
||||||
type: string;
|
type: string;
|
||||||
@@ -593,8 +590,8 @@ export class CursorExecutor {
|
|||||||
payload = decompressPayload(payload, flags);
|
payload = decompressPayload(payload, flags);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const text = payload.toString("utf-8");
|
const text = payload.toString('utf-8');
|
||||||
if (text.startsWith("{") && text.includes('"error"')) {
|
if (text.startsWith('{') && text.includes('"error"')) {
|
||||||
return createErrorResponse(JSON.parse(text));
|
return createErrorResponse(JSON.parse(text));
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -608,13 +605,13 @@ export class CursorExecutor {
|
|||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
error: {
|
error: {
|
||||||
message: result.error,
|
message: result.error,
|
||||||
type: "rate_limit_error",
|
type: 'rate_limit_error',
|
||||||
code: "rate_limited",
|
code: 'rate_limited',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
status: 429,
|
status: 429,
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -626,13 +623,13 @@ export class CursorExecutor {
|
|||||||
chunks.push(
|
chunks.push(
|
||||||
`data: ${JSON.stringify({
|
`data: ${JSON.stringify({
|
||||||
id: responseId,
|
id: responseId,
|
||||||
object: "chat.completion.chunk",
|
object: 'chat.completion.chunk',
|
||||||
created,
|
created,
|
||||||
model,
|
model,
|
||||||
choices: [
|
choices: [
|
||||||
{
|
{
|
||||||
index: 0,
|
index: 0,
|
||||||
delta: { role: "assistant", content: "" },
|
delta: { role: 'assistant', content: '' },
|
||||||
finish_reason: null,
|
finish_reason: null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -641,7 +638,8 @@ export class CursorExecutor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (toolCallsMap.has(tc.id)) {
|
if (toolCallsMap.has(tc.id)) {
|
||||||
const existing = toolCallsMap.get(tc.id)!;
|
const existing = toolCallsMap.get(tc.id);
|
||||||
|
if (!existing) continue;
|
||||||
existing.function.arguments += tc.function.arguments;
|
existing.function.arguments += tc.function.arguments;
|
||||||
existing.isLast = tc.isLast;
|
existing.isLast = tc.isLast;
|
||||||
|
|
||||||
@@ -649,7 +647,7 @@ export class CursorExecutor {
|
|||||||
chunks.push(
|
chunks.push(
|
||||||
`data: ${JSON.stringify({
|
`data: ${JSON.stringify({
|
||||||
id: responseId,
|
id: responseId,
|
||||||
object: "chat.completion.chunk",
|
object: 'chat.completion.chunk',
|
||||||
created,
|
created,
|
||||||
model,
|
model,
|
||||||
choices: [
|
choices: [
|
||||||
@@ -660,7 +658,7 @@ export class CursorExecutor {
|
|||||||
{
|
{
|
||||||
index: existing.index,
|
index: existing.index,
|
||||||
id: tc.id,
|
id: tc.id,
|
||||||
type: "function",
|
type: 'function',
|
||||||
function: {
|
function: {
|
||||||
name: tc.function.name,
|
name: tc.function.name,
|
||||||
arguments: tc.function.arguments,
|
arguments: tc.function.arguments,
|
||||||
@@ -682,7 +680,7 @@ export class CursorExecutor {
|
|||||||
chunks.push(
|
chunks.push(
|
||||||
`data: ${JSON.stringify({
|
`data: ${JSON.stringify({
|
||||||
id: responseId,
|
id: responseId,
|
||||||
object: "chat.completion.chunk",
|
object: 'chat.completion.chunk',
|
||||||
created,
|
created,
|
||||||
model,
|
model,
|
||||||
choices: [
|
choices: [
|
||||||
@@ -693,7 +691,7 @@ export class CursorExecutor {
|
|||||||
{
|
{
|
||||||
index: toolCallIndex,
|
index: toolCallIndex,
|
||||||
id: tc.id,
|
id: tc.id,
|
||||||
type: "function",
|
type: 'function',
|
||||||
function: {
|
function: {
|
||||||
name: tc.function.name,
|
name: tc.function.name,
|
||||||
arguments: tc.function.arguments,
|
arguments: tc.function.arguments,
|
||||||
@@ -710,11 +708,10 @@ export class CursorExecutor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (result.text) {
|
if (result.text) {
|
||||||
totalContent += result.text;
|
|
||||||
chunks.push(
|
chunks.push(
|
||||||
`data: ${JSON.stringify({
|
`data: ${JSON.stringify({
|
||||||
id: responseId,
|
id: responseId,
|
||||||
object: "chat.completion.chunk",
|
object: 'chat.completion.chunk',
|
||||||
created,
|
created,
|
||||||
model,
|
model,
|
||||||
choices: [
|
choices: [
|
||||||
@@ -722,7 +719,7 @@ export class CursorExecutor {
|
|||||||
index: 0,
|
index: 0,
|
||||||
delta:
|
delta:
|
||||||
chunks.length === 0 && toolCalls.length === 0
|
chunks.length === 0 && toolCalls.length === 0
|
||||||
? { role: "assistant", content: result.text }
|
? { role: 'assistant', content: result.text }
|
||||||
: { content: result.text },
|
: { content: result.text },
|
||||||
finish_reason: null,
|
finish_reason: null,
|
||||||
},
|
},
|
||||||
@@ -736,13 +733,13 @@ export class CursorExecutor {
|
|||||||
chunks.push(
|
chunks.push(
|
||||||
`data: ${JSON.stringify({
|
`data: ${JSON.stringify({
|
||||||
id: responseId,
|
id: responseId,
|
||||||
object: "chat.completion.chunk",
|
object: 'chat.completion.chunk',
|
||||||
created,
|
created,
|
||||||
model,
|
model,
|
||||||
choices: [
|
choices: [
|
||||||
{
|
{
|
||||||
index: 0,
|
index: 0,
|
||||||
delta: { role: "assistant", content: "" },
|
delta: { role: 'assistant', content: '' },
|
||||||
finish_reason: null,
|
finish_reason: null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -753,14 +750,14 @@ export class CursorExecutor {
|
|||||||
chunks.push(
|
chunks.push(
|
||||||
`data: ${JSON.stringify({
|
`data: ${JSON.stringify({
|
||||||
id: responseId,
|
id: responseId,
|
||||||
object: "chat.completion.chunk",
|
object: 'chat.completion.chunk',
|
||||||
created,
|
created,
|
||||||
model,
|
model,
|
||||||
choices: [
|
choices: [
|
||||||
{
|
{
|
||||||
index: 0,
|
index: 0,
|
||||||
delta: {},
|
delta: {},
|
||||||
finish_reason: toolCalls.length > 0 ? "tool_calls" : "stop",
|
finish_reason: toolCalls.length > 0 ? 'tool_calls' : 'stop',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
usage: {
|
usage: {
|
||||||
@@ -770,14 +767,14 @@ export class CursorExecutor {
|
|||||||
},
|
},
|
||||||
})}\n\n`
|
})}\n\n`
|
||||||
);
|
);
|
||||||
chunks.push("data: [DONE]\n\n");
|
chunks.push('data: [DONE]\n\n');
|
||||||
|
|
||||||
return new Response(chunks.join(""), {
|
return new Response(chunks.join(''), {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "text/event-stream",
|
'Content-Type': 'text/event-stream',
|
||||||
"Cache-Control": "no-cache",
|
'Cache-Control': 'no-cache',
|
||||||
Connection: "keep-alive",
|
Connection: 'keep-alive',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,26 +3,20 @@
|
|||||||
* Implements ConnectRPC protobuf wire format decoding
|
* Implements ConnectRPC protobuf wire format decoding
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as zlib from "zlib";
|
import * as zlib from 'zlib';
|
||||||
import {
|
import { WIRE_TYPE, FIELD, type WireType } from './cursor-protobuf-schema.js';
|
||||||
WIRE_TYPE,
|
|
||||||
FIELD,
|
|
||||||
type WireType,
|
|
||||||
} from "./cursor-protobuf-schema.js";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decode a varint from buffer
|
* Decode a varint from buffer
|
||||||
* Returns [value, newOffset]
|
* Returns [value, newOffset]
|
||||||
*/
|
*/
|
||||||
export function decodeVarint(
|
export function decodeVarint(buffer: Uint8Array, offset: number): [number, number] {
|
||||||
buffer: Uint8Array,
|
|
||||||
offset: number
|
|
||||||
): [number, number] {
|
|
||||||
let result = 0;
|
let result = 0;
|
||||||
let shift = 0;
|
let shift = 0;
|
||||||
let pos = offset;
|
let pos = offset;
|
||||||
|
const maxBytes = 5;
|
||||||
|
|
||||||
while (pos < buffer.length) {
|
while (pos < buffer.length && pos - offset < maxBytes) {
|
||||||
const b = buffer[pos];
|
const b = buffer[pos];
|
||||||
result |= (b & 0x7f) << shift;
|
result |= (b & 0x7f) << shift;
|
||||||
pos++;
|
pos++;
|
||||||
@@ -56,6 +50,9 @@ export function decodeField(
|
|||||||
[value, pos] = decodeVarint(buffer, pos);
|
[value, pos] = decodeVarint(buffer, pos);
|
||||||
} else if (wireType === WIRE_TYPE.LEN) {
|
} else if (wireType === WIRE_TYPE.LEN) {
|
||||||
const [length, pos2] = decodeVarint(buffer, pos);
|
const [length, pos2] = decodeVarint(buffer, pos);
|
||||||
|
if (pos2 + length > buffer.length) {
|
||||||
|
return [null, null, null, buffer.length];
|
||||||
|
}
|
||||||
value = buffer.slice(pos2, pos2 + length);
|
value = buffer.slice(pos2, pos2 + length);
|
||||||
pos = pos2 + length;
|
pos = pos2 + length;
|
||||||
} else if (wireType === WIRE_TYPE.FIXED64) {
|
} else if (wireType === WIRE_TYPE.FIXED64) {
|
||||||
@@ -77,10 +74,7 @@ export function decodeField(
|
|||||||
export function decodeMessage(
|
export function decodeMessage(
|
||||||
data: Uint8Array
|
data: Uint8Array
|
||||||
): Map<number, Array<{ wireType: WireType; value: Uint8Array | number }>> {
|
): Map<number, Array<{ wireType: WireType; value: Uint8Array | number }>> {
|
||||||
const fields = new Map<
|
const fields = new Map<number, Array<{ wireType: WireType; value: Uint8Array | number }>>();
|
||||||
number,
|
|
||||||
Array<{ wireType: WireType; value: Uint8Array | number }>
|
|
||||||
>();
|
|
||||||
let pos = 0;
|
let pos = 0;
|
||||||
|
|
||||||
while (pos < data.length) {
|
while (pos < data.length) {
|
||||||
@@ -90,7 +84,10 @@ export function decodeMessage(
|
|||||||
if (!fields.has(fieldNum)) {
|
if (!fields.has(fieldNum)) {
|
||||||
fields.set(fieldNum, []);
|
fields.set(fieldNum, []);
|
||||||
}
|
}
|
||||||
fields.get(fieldNum)!.push({ wireType, value: value as Uint8Array | number });
|
const fieldArray = fields.get(fieldNum);
|
||||||
|
if (fieldArray) {
|
||||||
|
fieldArray.push({ wireType, value: value as Uint8Array | number });
|
||||||
|
}
|
||||||
pos = newPos;
|
pos = newPos;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,8 +107,7 @@ export function parseConnectRPCFrame(buffer: Buffer): {
|
|||||||
if (buffer.length < 5) return null;
|
if (buffer.length < 5) return null;
|
||||||
|
|
||||||
const flags = buffer[0];
|
const flags = buffer[0];
|
||||||
const length =
|
const length = (buffer[1] << 24) | (buffer[2] << 16) | (buffer[3] << 8) | buffer[4];
|
||||||
(buffer[1] << 24) | (buffer[2] << 16) | (buffer[3] << 8) | buffer[4];
|
|
||||||
|
|
||||||
if (buffer.length < 5 + length) return null;
|
if (buffer.length < 5 + length) return null;
|
||||||
|
|
||||||
@@ -144,53 +140,62 @@ function extractToolCall(toolCallData: Uint8Array): {
|
|||||||
isLast: boolean;
|
isLast: boolean;
|
||||||
} | null {
|
} | null {
|
||||||
const toolCall = decodeMessage(toolCallData);
|
const toolCall = decodeMessage(toolCallData);
|
||||||
let toolCallId = "";
|
let toolCallId = '';
|
||||||
let toolName = "";
|
let toolName = '';
|
||||||
let rawArgs = "";
|
let rawArgs = '';
|
||||||
let isLast = false;
|
let isLast = false;
|
||||||
|
|
||||||
// Extract tool call ID
|
// Extract tool call ID
|
||||||
if (toolCall.has(FIELD.TOOL_ID)) {
|
if (toolCall.has(FIELD.TOOL_ID)) {
|
||||||
const fullId = new TextDecoder().decode(
|
const idField = toolCall.get(FIELD.TOOL_ID);
|
||||||
toolCall.get(FIELD.TOOL_ID)![0].value as Uint8Array
|
if (idField && idField[0]) {
|
||||||
);
|
const fullId = new TextDecoder().decode(idField[0].value as Uint8Array);
|
||||||
toolCallId = fullId.split("\n")[0]; // Take first line
|
toolCallId = fullId.split('\n')[0]; // Take first line
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract tool name
|
// Extract tool name
|
||||||
if (toolCall.has(FIELD.TOOL_NAME)) {
|
if (toolCall.has(FIELD.TOOL_NAME)) {
|
||||||
toolName = new TextDecoder().decode(
|
const nameField = toolCall.get(FIELD.TOOL_NAME);
|
||||||
toolCall.get(FIELD.TOOL_NAME)![0].value as Uint8Array
|
if (nameField && nameField[0]) {
|
||||||
);
|
toolName = new TextDecoder().decode(nameField[0].value as Uint8Array);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract is_last flag
|
// Extract is_last flag
|
||||||
if (toolCall.has(FIELD.TOOL_IS_LAST)) {
|
if (toolCall.has(FIELD.TOOL_IS_LAST)) {
|
||||||
isLast = (toolCall.get(FIELD.TOOL_IS_LAST)![0].value as number) !== 0;
|
const lastField = toolCall.get(FIELD.TOOL_IS_LAST);
|
||||||
|
if (lastField && lastField[0]) {
|
||||||
|
isLast = (lastField[0].value as number) !== 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract MCP params - nested real tool info
|
// Extract MCP params - nested real tool info
|
||||||
if (toolCall.has(FIELD.TOOL_MCP_PARAMS)) {
|
if (toolCall.has(FIELD.TOOL_MCP_PARAMS)) {
|
||||||
try {
|
try {
|
||||||
const mcpParams = decodeMessage(
|
const mcpField = toolCall.get(FIELD.TOOL_MCP_PARAMS);
|
||||||
toolCall.get(FIELD.TOOL_MCP_PARAMS)![0].value as Uint8Array
|
if (!mcpField || !mcpField[0]) return null;
|
||||||
);
|
|
||||||
|
const mcpParams = decodeMessage(mcpField[0].value as Uint8Array);
|
||||||
|
|
||||||
if (mcpParams.has(FIELD.MCP_TOOLS_LIST)) {
|
if (mcpParams.has(FIELD.MCP_TOOLS_LIST)) {
|
||||||
const tool = decodeMessage(
|
const toolsList = mcpParams.get(FIELD.MCP_TOOLS_LIST);
|
||||||
mcpParams.get(FIELD.MCP_TOOLS_LIST)![0].value as Uint8Array
|
if (!toolsList || !toolsList[0]) return null;
|
||||||
);
|
|
||||||
|
const tool = decodeMessage(toolsList[0].value as Uint8Array);
|
||||||
|
|
||||||
if (tool.has(FIELD.MCP_NESTED_NAME)) {
|
if (tool.has(FIELD.MCP_NESTED_NAME)) {
|
||||||
toolName = new TextDecoder().decode(
|
const nestedName = tool.get(FIELD.MCP_NESTED_NAME);
|
||||||
tool.get(FIELD.MCP_NESTED_NAME)![0].value as Uint8Array
|
if (nestedName && nestedName[0]) {
|
||||||
);
|
toolName = new TextDecoder().decode(nestedName[0].value as Uint8Array);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tool.has(FIELD.MCP_NESTED_PARAMS)) {
|
if (tool.has(FIELD.MCP_NESTED_PARAMS)) {
|
||||||
rawArgs = new TextDecoder().decode(
|
const nestedParams = tool.get(FIELD.MCP_NESTED_PARAMS);
|
||||||
tool.get(FIELD.MCP_NESTED_PARAMS)![0].value as Uint8Array
|
if (nestedParams && nestedParams[0]) {
|
||||||
);
|
rawArgs = new TextDecoder().decode(nestedParams[0].value as Uint8Array);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -200,18 +205,19 @@ function extractToolCall(toolCallData: Uint8Array): {
|
|||||||
|
|
||||||
// Fallback to raw_args
|
// Fallback to raw_args
|
||||||
if (!rawArgs && toolCall.has(FIELD.TOOL_RAW_ARGS)) {
|
if (!rawArgs && toolCall.has(FIELD.TOOL_RAW_ARGS)) {
|
||||||
rawArgs = new TextDecoder().decode(
|
const rawArgsField = toolCall.get(FIELD.TOOL_RAW_ARGS);
|
||||||
toolCall.get(FIELD.TOOL_RAW_ARGS)![0].value as Uint8Array
|
if (rawArgsField && rawArgsField[0]) {
|
||||||
);
|
rawArgs = new TextDecoder().decode(rawArgsField[0].value as Uint8Array);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toolCallId && toolName) {
|
if (toolCallId && toolName) {
|
||||||
return {
|
return {
|
||||||
id: toolCallId,
|
id: toolCallId,
|
||||||
type: "function",
|
type: 'function',
|
||||||
function: {
|
function: {
|
||||||
name: toolName,
|
name: toolName,
|
||||||
arguments: rawArgs || "{}",
|
arguments: rawArgs || '{}',
|
||||||
},
|
},
|
||||||
isLast,
|
isLast,
|
||||||
};
|
};
|
||||||
@@ -223,30 +229,34 @@ function extractToolCall(toolCallData: Uint8Array): {
|
|||||||
/**
|
/**
|
||||||
* Extract text and thinking from response data
|
* Extract text and thinking from response data
|
||||||
*/
|
*/
|
||||||
function extractTextAndThinking(
|
function extractTextAndThinking(responseData: Uint8Array): {
|
||||||
responseData: Uint8Array
|
text: string | null;
|
||||||
): { text: string | null; thinking: string | null } {
|
thinking: string | null;
|
||||||
|
} {
|
||||||
const nested = decodeMessage(responseData);
|
const nested = decodeMessage(responseData);
|
||||||
let text: string | null = null;
|
let text: string | null = null;
|
||||||
let thinking: string | null = null;
|
let thinking: string | null = null;
|
||||||
|
|
||||||
// Extract text
|
// Extract text
|
||||||
if (nested.has(FIELD.RESPONSE_TEXT)) {
|
if (nested.has(FIELD.RESPONSE_TEXT)) {
|
||||||
text = new TextDecoder().decode(
|
const textField = nested.get(FIELD.RESPONSE_TEXT);
|
||||||
nested.get(FIELD.RESPONSE_TEXT)![0].value as Uint8Array
|
if (textField && textField[0]) {
|
||||||
);
|
text = new TextDecoder().decode(textField[0].value as Uint8Array);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract thinking
|
// Extract thinking
|
||||||
if (nested.has(FIELD.THINKING)) {
|
if (nested.has(FIELD.THINKING)) {
|
||||||
try {
|
try {
|
||||||
const thinkingMsg = decodeMessage(
|
const thinkingField = nested.get(FIELD.THINKING);
|
||||||
nested.get(FIELD.THINKING)![0].value as Uint8Array
|
if (thinkingField && thinkingField[0]) {
|
||||||
);
|
const thinkingMsg = decodeMessage(thinkingField[0].value as Uint8Array);
|
||||||
if (thinkingMsg.has(FIELD.THINKING_TEXT)) {
|
if (thinkingMsg.has(FIELD.THINKING_TEXT)) {
|
||||||
thinking = new TextDecoder().decode(
|
const thinkingTextField = thinkingMsg.get(FIELD.THINKING_TEXT);
|
||||||
thinkingMsg.get(FIELD.THINKING_TEXT)![0].value as Uint8Array
|
if (thinkingTextField && thinkingTextField[0]) {
|
||||||
);
|
thinking = new TextDecoder().decode(thinkingTextField[0].value as Uint8Array);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Thinking parse error, continue
|
// Thinking parse error, continue
|
||||||
@@ -275,24 +285,26 @@ export function extractTextFromResponse(payload: Uint8Array): {
|
|||||||
|
|
||||||
// Field 1: ClientSideToolV2Call
|
// Field 1: ClientSideToolV2Call
|
||||||
if (fields.has(FIELD.TOOL_CALL)) {
|
if (fields.has(FIELD.TOOL_CALL)) {
|
||||||
const toolCall = extractToolCall(
|
const toolCallField = fields.get(FIELD.TOOL_CALL);
|
||||||
fields.get(FIELD.TOOL_CALL)![0].value as Uint8Array
|
if (toolCallField && toolCallField[0]) {
|
||||||
);
|
const toolCall = extractToolCall(toolCallField[0].value as Uint8Array);
|
||||||
if (toolCall) {
|
if (toolCall) {
|
||||||
return { text: null, error: null, toolCall, thinking: null };
|
return { text: null, error: null, toolCall, thinking: null };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Field 2: StreamUnifiedChatResponse
|
// Field 2: StreamUnifiedChatResponse
|
||||||
if (fields.has(FIELD.RESPONSE)) {
|
if (fields.has(FIELD.RESPONSE)) {
|
||||||
const { text, thinking } = extractTextAndThinking(
|
const responseField = fields.get(FIELD.RESPONSE);
|
||||||
fields.get(FIELD.RESPONSE)![0].value as Uint8Array
|
if (responseField && responseField[0]) {
|
||||||
);
|
const { text, thinking } = extractTextAndThinking(responseField[0].value as Uint8Array);
|
||||||
|
|
||||||
if (text || thinking) {
|
if (text || thinking) {
|
||||||
return { text, error: null, toolCall: null, thinking };
|
return { text, error: null, toolCall: null, thinking };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return { text: null, error: null, toolCall: null, thinking: null };
|
return { text: null, error: null, toolCall: null, thinking: null };
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -3,24 +3,17 @@
|
|||||||
* Implements ConnectRPC protobuf wire format encoding
|
* Implements ConnectRPC protobuf wire format encoding
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { randomUUID } from "crypto";
|
import * as zlib from 'zlib';
|
||||||
import * as zlib from "zlib";
|
|
||||||
import {
|
import {
|
||||||
WIRE_TYPE,
|
WIRE_TYPE,
|
||||||
ROLE,
|
|
||||||
UNIFIED_MODE,
|
|
||||||
THINKING_LEVEL,
|
|
||||||
FIELD,
|
FIELD,
|
||||||
COMPRESS_FLAG,
|
COMPRESS_FLAG,
|
||||||
|
UNIFIED_MODE,
|
||||||
type WireType,
|
type WireType,
|
||||||
type RoleType,
|
type RoleType,
|
||||||
type ThinkingLevelType,
|
|
||||||
type CursorTool,
|
type CursorTool,
|
||||||
type CursorToolResult,
|
type CursorToolResult,
|
||||||
type CursorMessage,
|
} from './cursor-protobuf-schema.js';
|
||||||
type FormattedMessage,
|
|
||||||
type MessageId,
|
|
||||||
} from "./cursor-protobuf-schema.js";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encode a varint (variable-length integer)
|
* Encode a varint (variable-length integer)
|
||||||
@@ -54,7 +47,7 @@ export function encodeField(
|
|||||||
|
|
||||||
if (wireType === WIRE_TYPE.LEN) {
|
if (wireType === WIRE_TYPE.LEN) {
|
||||||
const dataBytes =
|
const dataBytes =
|
||||||
typeof value === "string"
|
typeof value === 'string'
|
||||||
? new TextEncoder().encode(value)
|
? new TextEncoder().encode(value)
|
||||||
: value instanceof Uint8Array
|
: value instanceof Uint8Array
|
||||||
? value
|
? value
|
||||||
@@ -70,7 +63,7 @@ export function encodeField(
|
|||||||
/**
|
/**
|
||||||
* Concatenate multiple Uint8Arrays
|
* Concatenate multiple Uint8Arrays
|
||||||
*/
|
*/
|
||||||
function concatArrays(...arrays: Uint8Array[]): Uint8Array {
|
export function concatArrays(...arrays: Uint8Array[]): Uint8Array {
|
||||||
const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);
|
const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);
|
||||||
const result = new Uint8Array(totalLength);
|
const result = new Uint8Array(totalLength);
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
@@ -85,10 +78,10 @@ function concatArrays(...arrays: Uint8Array[]): Uint8Array {
|
|||||||
* Encode a tool result
|
* Encode a tool result
|
||||||
*/
|
*/
|
||||||
export function encodeToolResult(toolResult: CursorToolResult): Uint8Array {
|
export function encodeToolResult(toolResult: CursorToolResult): Uint8Array {
|
||||||
const toolCallId = toolResult.tool_call_id || "";
|
const toolCallId = toolResult.tool_call_id || '';
|
||||||
const toolName = toolResult.name || "";
|
const toolName = toolResult.name || '';
|
||||||
const toolIndex = toolResult.index || 0;
|
const toolIndex = toolResult.index || 0;
|
||||||
const rawArgs = toolResult.raw_args || "{}";
|
const rawArgs = toolResult.raw_args || '{}';
|
||||||
|
|
||||||
return concatArrays(
|
return concatArrays(
|
||||||
encodeField(FIELD.TOOL_RESULT_CALL_ID, WIRE_TYPE.LEN, toolCallId),
|
encodeField(FIELD.TOOL_RESULT_CALL_ID, WIRE_TYPE.LEN, toolCallId),
|
||||||
@@ -115,11 +108,7 @@ export function encodeMessage(
|
|||||||
encodeField(FIELD.MSG_ID, WIRE_TYPE.LEN, messageId),
|
encodeField(FIELD.MSG_ID, WIRE_TYPE.LEN, messageId),
|
||||||
...(toolResults.length > 0
|
...(toolResults.length > 0
|
||||||
? toolResults.map((tr) =>
|
? toolResults.map((tr) =>
|
||||||
encodeField(
|
encodeField(FIELD.MSG_TOOL_RESULTS, WIRE_TYPE.LEN, encodeToolResult(tr))
|
||||||
FIELD.MSG_TOOL_RESULTS,
|
|
||||||
WIRE_TYPE.LEN,
|
|
||||||
encodeToolResult(tr)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
: []),
|
: []),
|
||||||
encodeField(FIELD.MSG_IS_AGENTIC, WIRE_TYPE.VARINT, hasTools ? 1 : 0),
|
encodeField(FIELD.MSG_IS_AGENTIC, WIRE_TYPE.VARINT, hasTools ? 1 : 0),
|
||||||
@@ -129,13 +118,7 @@ export function encodeMessage(
|
|||||||
hasTools ? UNIFIED_MODE.AGENT : UNIFIED_MODE.CHAT
|
hasTools ? UNIFIED_MODE.AGENT : UNIFIED_MODE.CHAT
|
||||||
),
|
),
|
||||||
...(isLast && hasTools
|
...(isLast && hasTools
|
||||||
? [
|
? [encodeField(FIELD.MSG_SUPPORTED_TOOLS, WIRE_TYPE.LEN, encodeVarint(1))]
|
||||||
encodeField(
|
|
||||||
FIELD.MSG_SUPPORTED_TOOLS,
|
|
||||||
WIRE_TYPE.LEN,
|
|
||||||
encodeVarint(1)
|
|
||||||
),
|
|
||||||
]
|
|
||||||
: [])
|
: [])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -144,9 +127,7 @@ export function encodeMessage(
|
|||||||
* Encode instruction text
|
* Encode instruction text
|
||||||
*/
|
*/
|
||||||
export function encodeInstruction(text: string): Uint8Array {
|
export function encodeInstruction(text: string): Uint8Array {
|
||||||
return text
|
return text ? encodeField(FIELD.INSTRUCTION_TEXT, WIRE_TYPE.LEN, text) : new Uint8Array(0);
|
||||||
? encodeField(FIELD.INSTRUCTION_TEXT, WIRE_TYPE.LEN, text)
|
|
||||||
: new Uint8Array(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -169,7 +150,7 @@ export function encodeCursorSetting(): Uint8Array {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return concatArrays(
|
return concatArrays(
|
||||||
encodeField(FIELD.SETTING_PATH, WIRE_TYPE.LEN, "cursor\\aisettings"),
|
encodeField(FIELD.SETTING_PATH, WIRE_TYPE.LEN, 'cursor\\aisettings'),
|
||||||
encodeField(FIELD.SETTING_UNKNOWN_3, WIRE_TYPE.LEN, new Uint8Array(0)),
|
encodeField(FIELD.SETTING_UNKNOWN_3, WIRE_TYPE.LEN, new Uint8Array(0)),
|
||||||
encodeField(FIELD.SETTING_UNKNOWN_6, WIRE_TYPE.LEN, unknown6),
|
encodeField(FIELD.SETTING_UNKNOWN_6, WIRE_TYPE.LEN, unknown6),
|
||||||
encodeField(FIELD.SETTING_UNKNOWN_8, WIRE_TYPE.VARINT, 1),
|
encodeField(FIELD.SETTING_UNKNOWN_8, WIRE_TYPE.VARINT, 1),
|
||||||
@@ -182,10 +163,10 @@ export function encodeCursorSetting(): Uint8Array {
|
|||||||
*/
|
*/
|
||||||
export function encodeMetadata(): Uint8Array {
|
export function encodeMetadata(): Uint8Array {
|
||||||
return concatArrays(
|
return concatArrays(
|
||||||
encodeField(FIELD.META_PLATFORM, WIRE_TYPE.LEN, process.platform || "linux"),
|
encodeField(FIELD.META_PLATFORM, WIRE_TYPE.LEN, process.platform || 'linux'),
|
||||||
encodeField(FIELD.META_ARCH, WIRE_TYPE.LEN, process.arch || "x64"),
|
encodeField(FIELD.META_ARCH, WIRE_TYPE.LEN, process.arch || 'x64'),
|
||||||
encodeField(FIELD.META_VERSION, WIRE_TYPE.LEN, process.version || "v20.0.0"),
|
encodeField(FIELD.META_VERSION, WIRE_TYPE.LEN, process.version || 'v20.0.0'),
|
||||||
encodeField(FIELD.META_CWD, WIRE_TYPE.LEN, process.cwd() || "/"),
|
encodeField(FIELD.META_CWD, WIRE_TYPE.LEN, process.cwd() || '/'),
|
||||||
encodeField(FIELD.META_TIMESTAMP, WIRE_TYPE.LEN, new Date().toISOString())
|
encodeField(FIELD.META_TIMESTAMP, WIRE_TYPE.LEN, new Date().toISOString())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -193,16 +174,10 @@ export function encodeMetadata(): Uint8Array {
|
|||||||
/**
|
/**
|
||||||
* Encode message ID
|
* Encode message ID
|
||||||
*/
|
*/
|
||||||
export function encodeMessageId(
|
export function encodeMessageId(messageId: string, role: RoleType, summaryId?: string): Uint8Array {
|
||||||
messageId: string,
|
|
||||||
role: RoleType,
|
|
||||||
summaryId?: string
|
|
||||||
): Uint8Array {
|
|
||||||
return concatArrays(
|
return concatArrays(
|
||||||
encodeField(FIELD.MSGID_ID, WIRE_TYPE.LEN, messageId),
|
encodeField(FIELD.MSGID_ID, WIRE_TYPE.LEN, messageId),
|
||||||
...(summaryId
|
...(summaryId ? [encodeField(FIELD.MSGID_SUMMARY, WIRE_TYPE.LEN, summaryId)] : []),
|
||||||
? [encodeField(FIELD.MSGID_SUMMARY, WIRE_TYPE.LEN, summaryId)]
|
|
||||||
: []),
|
|
||||||
encodeField(FIELD.MSGID_ROLE, WIRE_TYPE.VARINT, role)
|
encodeField(FIELD.MSGID_ROLE, WIRE_TYPE.VARINT, role)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -211,37 +186,24 @@ export function encodeMessageId(
|
|||||||
* Encode MCP tool
|
* Encode MCP tool
|
||||||
*/
|
*/
|
||||||
export function encodeMcpTool(tool: CursorTool): Uint8Array {
|
export function encodeMcpTool(tool: CursorTool): Uint8Array {
|
||||||
const toolName = tool.function?.name || tool.name || "";
|
const toolName = tool.function?.name || tool.name || '';
|
||||||
const toolDesc = tool.function?.description || tool.description || "";
|
const toolDesc = tool.function?.description || tool.description || '';
|
||||||
const inputSchema = tool.function?.parameters || tool.input_schema || {};
|
const inputSchema = tool.function?.parameters || tool.input_schema || {};
|
||||||
|
|
||||||
return concatArrays(
|
return concatArrays(
|
||||||
...(toolName
|
...(toolName ? [encodeField(FIELD.MCP_TOOL_NAME, WIRE_TYPE.LEN, toolName)] : []),
|
||||||
? [encodeField(FIELD.MCP_TOOL_NAME, WIRE_TYPE.LEN, toolName)]
|
...(toolDesc ? [encodeField(FIELD.MCP_TOOL_DESC, WIRE_TYPE.LEN, toolDesc)] : []),
|
||||||
: []),
|
|
||||||
...(toolDesc
|
|
||||||
? [encodeField(FIELD.MCP_TOOL_DESC, WIRE_TYPE.LEN, toolDesc)]
|
|
||||||
: []),
|
|
||||||
...(Object.keys(inputSchema).length > 0
|
...(Object.keys(inputSchema).length > 0
|
||||||
? [
|
? [encodeField(FIELD.MCP_TOOL_PARAMS, WIRE_TYPE.LEN, JSON.stringify(inputSchema))]
|
||||||
encodeField(
|
|
||||||
FIELD.MCP_TOOL_PARAMS,
|
|
||||||
WIRE_TYPE.LEN,
|
|
||||||
JSON.stringify(inputSchema)
|
|
||||||
),
|
|
||||||
]
|
|
||||||
: []),
|
: []),
|
||||||
encodeField(FIELD.MCP_TOOL_SERVER, WIRE_TYPE.LEN, "custom")
|
encodeField(FIELD.MCP_TOOL_SERVER, WIRE_TYPE.LEN, 'custom')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wrap payload in ConnectRPC frame (5-byte header + payload)
|
* Wrap payload in ConnectRPC frame (5-byte header + payload)
|
||||||
*/
|
*/
|
||||||
export function wrapConnectRPCFrame(
|
export function wrapConnectRPCFrame(payload: Uint8Array, compress = false): Uint8Array {
|
||||||
payload: Uint8Array,
|
|
||||||
compress = false
|
|
||||||
): Uint8Array {
|
|
||||||
let finalPayload = payload;
|
let finalPayload = payload;
|
||||||
let flags: number = COMPRESS_FLAG.NONE;
|
let flags: number = COMPRESS_FLAG.NONE;
|
||||||
|
|
||||||
|
|||||||
@@ -143,8 +143,7 @@ export const FIELD = {
|
|||||||
export type WireType = (typeof WIRE_TYPE)[keyof typeof WIRE_TYPE];
|
export type WireType = (typeof WIRE_TYPE)[keyof typeof WIRE_TYPE];
|
||||||
export type RoleType = (typeof ROLE)[keyof typeof ROLE];
|
export type RoleType = (typeof ROLE)[keyof typeof ROLE];
|
||||||
export type UnifiedModeType = (typeof UNIFIED_MODE)[keyof typeof UNIFIED_MODE];
|
export type UnifiedModeType = (typeof UNIFIED_MODE)[keyof typeof UNIFIED_MODE];
|
||||||
export type ThinkingLevelType =
|
export type ThinkingLevelType = (typeof THINKING_LEVEL)[keyof typeof THINKING_LEVEL];
|
||||||
(typeof THINKING_LEVEL)[keyof typeof THINKING_LEVEL];
|
|
||||||
export type FieldNumber = (typeof FIELD)[keyof typeof FIELD];
|
export type FieldNumber = (typeof FIELD)[keyof typeof FIELD];
|
||||||
|
|
||||||
/** Cursor tool definition */
|
/** Cursor tool definition */
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Exports encoder/decoder functions and builds complete requests
|
* Exports encoder/decoder functions and builds complete requests
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { randomUUID } from "crypto";
|
import { randomUUID } from 'crypto';
|
||||||
import {
|
import {
|
||||||
ROLE,
|
ROLE,
|
||||||
UNIFIED_MODE,
|
UNIFIED_MODE,
|
||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
type FormattedMessage,
|
type FormattedMessage,
|
||||||
type MessageId,
|
type MessageId,
|
||||||
type ThinkingLevelType,
|
type ThinkingLevelType,
|
||||||
} from "./cursor-protobuf-schema.js";
|
} from './cursor-protobuf-schema.js';
|
||||||
import {
|
import {
|
||||||
encodeField,
|
encodeField,
|
||||||
encodeVarint,
|
encodeVarint,
|
||||||
@@ -26,15 +26,16 @@ import {
|
|||||||
encodeMessageId,
|
encodeMessageId,
|
||||||
encodeMcpTool,
|
encodeMcpTool,
|
||||||
wrapConnectRPCFrame,
|
wrapConnectRPCFrame,
|
||||||
} from "./cursor-protobuf-encoder.js";
|
concatArrays,
|
||||||
|
} from './cursor-protobuf-encoder.js';
|
||||||
import {
|
import {
|
||||||
decodeVarint,
|
decodeVarint,
|
||||||
decodeField,
|
decodeField,
|
||||||
decodeMessage,
|
decodeMessage,
|
||||||
parseConnectRPCFrame,
|
parseConnectRPCFrame,
|
||||||
extractTextFromResponse,
|
extractTextFromResponse,
|
||||||
} from "./cursor-protobuf-decoder.js";
|
} from './cursor-protobuf-decoder.js';
|
||||||
import { WIRE_TYPE } from "./cursor-protobuf-schema.js";
|
import { WIRE_TYPE } from './cursor-protobuf-schema.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build complete chat request protobuf
|
* Build complete chat request protobuf
|
||||||
@@ -45,6 +46,10 @@ export function encodeRequest(
|
|||||||
tools: CursorTool[] = [],
|
tools: CursorTool[] = [],
|
||||||
reasoningEffort: string | null = null
|
reasoningEffort: string | null = null
|
||||||
): Uint8Array {
|
): Uint8Array {
|
||||||
|
if (messages.length === 0) {
|
||||||
|
throw new Error('Messages array must not be empty');
|
||||||
|
}
|
||||||
|
|
||||||
const hasTools = tools?.length > 0;
|
const hasTools = tools?.length > 0;
|
||||||
const isAgentic = hasTools;
|
const isAgentic = hasTools;
|
||||||
const formattedMessages: FormattedMessage[] = [];
|
const formattedMessages: FormattedMessage[] = [];
|
||||||
@@ -53,7 +58,7 @@ export function encodeRequest(
|
|||||||
// Prepare messages
|
// Prepare messages
|
||||||
for (let i = 0; i < messages.length; i++) {
|
for (let i = 0; i < messages.length; i++) {
|
||||||
const msg = messages[i];
|
const msg = messages[i];
|
||||||
const role = msg.role === "user" ? ROLE.USER : ROLE.ASSISTANT;
|
const role = msg.role === 'user' ? ROLE.USER : ROLE.ASSISTANT;
|
||||||
const msgId = randomUUID();
|
const msgId = randomUUID();
|
||||||
const isLast = i === messages.length - 1;
|
const isLast = i === messages.length - 1;
|
||||||
|
|
||||||
@@ -71,38 +76,25 @@ export function encodeRequest(
|
|||||||
|
|
||||||
// Map reasoning effort to thinking level
|
// Map reasoning effort to thinking level
|
||||||
let thinkingLevel: ThinkingLevelType = THINKING_LEVEL.UNSPECIFIED;
|
let thinkingLevel: ThinkingLevelType = THINKING_LEVEL.UNSPECIFIED;
|
||||||
if (reasoningEffort === "medium") thinkingLevel = THINKING_LEVEL.MEDIUM;
|
if (reasoningEffort === 'medium') thinkingLevel = THINKING_LEVEL.MEDIUM;
|
||||||
else if (reasoningEffort === "high") thinkingLevel = THINKING_LEVEL.HIGH;
|
else if (reasoningEffort === 'high') thinkingLevel = THINKING_LEVEL.HIGH;
|
||||||
|
|
||||||
// Build arrays for messages and tools
|
// Build arrays for messages and tools
|
||||||
const messageFields = formattedMessages.map((fm) =>
|
const messageFields = formattedMessages.map((fm) =>
|
||||||
encodeField(
|
encodeField(
|
||||||
FIELD.MESSAGES,
|
FIELD.MESSAGES,
|
||||||
WIRE_TYPE.LEN,
|
WIRE_TYPE.LEN,
|
||||||
encodeMessage(
|
encodeMessage(fm.content, fm.role, fm.messageId, fm.isLast, fm.hasTools, fm.toolResults)
|
||||||
fm.content,
|
|
||||||
fm.role,
|
|
||||||
fm.messageId,
|
|
||||||
fm.isLast,
|
|
||||||
fm.hasTools,
|
|
||||||
fm.toolResults
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const messageIdFields = messageIds.map((mid) =>
|
const messageIdFields = messageIds.map((mid) =>
|
||||||
encodeField(
|
encodeField(FIELD.MESSAGE_IDS, WIRE_TYPE.LEN, encodeMessageId(mid.messageId, mid.role))
|
||||||
FIELD.MESSAGE_IDS,
|
|
||||||
WIRE_TYPE.LEN,
|
|
||||||
encodeMessageId(mid.messageId, mid.role)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const toolFields =
|
const toolFields =
|
||||||
tools?.length > 0
|
tools?.length > 0
|
||||||
? tools.map((tool) =>
|
? tools.map((tool) => encodeField(FIELD.MCP_TOOLS, WIRE_TYPE.LEN, encodeMcpTool(tool)))
|
||||||
encodeField(FIELD.MCP_TOOLS, WIRE_TYPE.LEN, encodeMcpTool(tool))
|
|
||||||
)
|
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
const supportedToolsField = isAgentic
|
const supportedToolsField = isAgentic
|
||||||
@@ -113,10 +105,10 @@ export function encodeRequest(
|
|||||||
const parts: Uint8Array[] = [
|
const parts: Uint8Array[] = [
|
||||||
...messageFields,
|
...messageFields,
|
||||||
encodeField(FIELD.UNKNOWN_2, WIRE_TYPE.VARINT, 1),
|
encodeField(FIELD.UNKNOWN_2, WIRE_TYPE.VARINT, 1),
|
||||||
encodeField(FIELD.INSTRUCTION, WIRE_TYPE.LEN, encodeInstruction("")),
|
encodeField(FIELD.INSTRUCTION, WIRE_TYPE.LEN, encodeInstruction('')),
|
||||||
encodeField(FIELD.UNKNOWN_4, WIRE_TYPE.VARINT, 1),
|
encodeField(FIELD.UNKNOWN_4, WIRE_TYPE.VARINT, 1),
|
||||||
encodeField(FIELD.MODEL, WIRE_TYPE.LEN, encodeModel(modelName)),
|
encodeField(FIELD.MODEL, WIRE_TYPE.LEN, encodeModel(modelName)),
|
||||||
encodeField(FIELD.WEB_TOOL, WIRE_TYPE.LEN, ""),
|
encodeField(FIELD.WEB_TOOL, WIRE_TYPE.LEN, ''),
|
||||||
encodeField(FIELD.UNKNOWN_13, WIRE_TYPE.VARINT, 1),
|
encodeField(FIELD.UNKNOWN_13, WIRE_TYPE.VARINT, 1),
|
||||||
encodeField(FIELD.CURSOR_SETTING, WIRE_TYPE.LEN, encodeCursorSetting()),
|
encodeField(FIELD.CURSOR_SETTING, WIRE_TYPE.LEN, encodeCursorSetting()),
|
||||||
encodeField(FIELD.UNKNOWN_19, WIRE_TYPE.VARINT, 1),
|
encodeField(FIELD.UNKNOWN_19, WIRE_TYPE.VARINT, 1),
|
||||||
@@ -133,16 +125,12 @@ export function encodeRequest(
|
|||||||
WIRE_TYPE.VARINT,
|
WIRE_TYPE.VARINT,
|
||||||
isAgentic ? UNIFIED_MODE.AGENT : UNIFIED_MODE.CHAT
|
isAgentic ? UNIFIED_MODE.AGENT : UNIFIED_MODE.CHAT
|
||||||
),
|
),
|
||||||
encodeField(FIELD.UNKNOWN_47, WIRE_TYPE.LEN, ""),
|
encodeField(FIELD.UNKNOWN_47, WIRE_TYPE.LEN, ''),
|
||||||
encodeField(FIELD.SHOULD_DISABLE_TOOLS, WIRE_TYPE.VARINT, isAgentic ? 0 : 1),
|
encodeField(FIELD.SHOULD_DISABLE_TOOLS, WIRE_TYPE.VARINT, isAgentic ? 0 : 1),
|
||||||
encodeField(FIELD.THINKING_LEVEL, WIRE_TYPE.VARINT, thinkingLevel),
|
encodeField(FIELD.THINKING_LEVEL, WIRE_TYPE.VARINT, thinkingLevel),
|
||||||
encodeField(FIELD.UNKNOWN_51, WIRE_TYPE.VARINT, 0),
|
encodeField(FIELD.UNKNOWN_51, WIRE_TYPE.VARINT, 0),
|
||||||
encodeField(FIELD.UNKNOWN_53, WIRE_TYPE.VARINT, 1),
|
encodeField(FIELD.UNKNOWN_53, WIRE_TYPE.VARINT, 1),
|
||||||
encodeField(
|
encodeField(FIELD.UNIFIED_MODE_NAME, WIRE_TYPE.LEN, isAgentic ? 'Agent' : 'Ask'),
|
||||||
FIELD.UNIFIED_MODE_NAME,
|
|
||||||
WIRE_TYPE.LEN,
|
|
||||||
isAgentic ? "Agent" : "Ask"
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
return concatArrays(...parts);
|
return concatArrays(...parts);
|
||||||
@@ -178,20 +166,6 @@ export function generateCursorBody(
|
|||||||
return framed;
|
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
|
// Re-export all functions
|
||||||
export {
|
export {
|
||||||
encodeVarint,
|
encodeVarint,
|
||||||
|
|||||||
@@ -3,11 +3,7 @@
|
|||||||
* Converts OpenAI messages to Cursor format
|
* Converts OpenAI messages to Cursor format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {
|
import type { CursorMessage, CursorToolResult, CursorTool } from './cursor-protobuf-schema.js';
|
||||||
CursorMessage,
|
|
||||||
CursorToolResult,
|
|
||||||
CursorTool,
|
|
||||||
} from "./cursor-protobuf-schema.js";
|
|
||||||
|
|
||||||
/** OpenAI message format */
|
/** OpenAI message format */
|
||||||
interface OpenAIMessage {
|
interface OpenAIMessage {
|
||||||
@@ -42,28 +38,28 @@ function convertMessages(messages: OpenAIMessage[]): CursorMessage[] {
|
|||||||
for (let i = 0; i < messages.length; i++) {
|
for (let i = 0; i < messages.length; i++) {
|
||||||
const msg = messages[i];
|
const msg = messages[i];
|
||||||
|
|
||||||
if (msg.role === "system") {
|
if (msg.role === 'system') {
|
||||||
result.push({
|
result.push({
|
||||||
role: "user",
|
role: 'user',
|
||||||
content: `[System Instructions]\n${msg.content}`,
|
content: `[System Instructions]\n${msg.content}`,
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg.role === "tool") {
|
if (msg.role === 'tool') {
|
||||||
let toolContent = "";
|
let toolContent = '';
|
||||||
if (typeof msg.content === "string") {
|
if (typeof msg.content === 'string') {
|
||||||
toolContent = msg.content;
|
toolContent = msg.content;
|
||||||
} else if (Array.isArray(msg.content)) {
|
} else if (Array.isArray(msg.content)) {
|
||||||
for (const part of msg.content) {
|
for (const part of msg.content) {
|
||||||
if (part.type === "text" && part.text) {
|
if (part.type === 'text' && part.text) {
|
||||||
toolContent += part.text;
|
toolContent += part.text;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const toolName = msg.name || "tool";
|
const toolName = msg.name || 'tool';
|
||||||
const toolCallId = msg.tool_call_id || "";
|
const toolCallId = msg.tool_call_id || '';
|
||||||
|
|
||||||
// Accumulate tool result
|
// Accumulate tool result
|
||||||
pendingToolResults.push({
|
pendingToolResults.push({
|
||||||
@@ -75,22 +71,22 @@ function convertMessages(messages: OpenAIMessage[]): CursorMessage[] {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg.role === "user" || msg.role === "assistant") {
|
if (msg.role === 'user' || msg.role === 'assistant') {
|
||||||
let content = "";
|
let content = '';
|
||||||
|
|
||||||
if (typeof msg.content === "string") {
|
if (typeof msg.content === 'string') {
|
||||||
content = msg.content;
|
content = msg.content;
|
||||||
} else if (Array.isArray(msg.content)) {
|
} else if (Array.isArray(msg.content)) {
|
||||||
for (const part of msg.content) {
|
for (const part of msg.content) {
|
||||||
if (part.type === "text" && part.text) {
|
if (part.type === 'text' && part.text) {
|
||||||
content += part.text;
|
content += part.text;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep tool_calls structure for assistant messages
|
// Keep tool_calls structure for assistant messages
|
||||||
if (msg.role === "assistant" && msg.tool_calls && msg.tool_calls.length > 0) {
|
if (msg.role === 'assistant' && msg.tool_calls && msg.tool_calls.length > 0) {
|
||||||
const assistantMsg: CursorMessage = { role: "assistant", content: "" };
|
const assistantMsg: CursorMessage = { role: 'assistant', content: '' };
|
||||||
if (content) {
|
if (content) {
|
||||||
assistantMsg.content = content;
|
assistantMsg.content = content;
|
||||||
}
|
}
|
||||||
@@ -106,7 +102,7 @@ function convertMessages(messages: OpenAIMessage[]): CursorMessage[] {
|
|||||||
} else if (content || pendingToolResults.length > 0) {
|
} else if (content || pendingToolResults.length > 0) {
|
||||||
const msgObj: CursorMessage = {
|
const msgObj: CursorMessage = {
|
||||||
role: msg.role,
|
role: msg.role,
|
||||||
content: content || "",
|
content: content || '',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Attach pending tool results to this message
|
// Attach pending tool results to this message
|
||||||
@@ -128,10 +124,10 @@ function convertMessages(messages: OpenAIMessage[]): CursorMessage[] {
|
|||||||
* Returns modified body with converted messages
|
* Returns modified body with converted messages
|
||||||
*/
|
*/
|
||||||
export function buildCursorRequest(
|
export function buildCursorRequest(
|
||||||
model: string,
|
_model: string,
|
||||||
body: OpenAIRequestBody,
|
body: OpenAIRequestBody,
|
||||||
stream: boolean,
|
_stream: boolean,
|
||||||
credentials: unknown
|
_credentials: unknown
|
||||||
): {
|
): {
|
||||||
messages: CursorMessage[];
|
messages: CursorMessage[];
|
||||||
tools?: CursorTool[];
|
tools?: CursorTool[];
|
||||||
|
|||||||
Reference in new issue
Block a user