Merge pull request #971 from kaitranntt/kai/feat/966-cursor-integration-usable

feat: make Cursor integration usable and supportable
This commit is contained in:
Kai (Tam Nhu) Tran authored and GitHub committed 2026-04-12 16:42:32 -04:00
commit a18146b83e
25 files changed
+2455 -319

No files matched your search

+24 -2
View File
@@ -1,4 +1,5 @@
import type { CursorAuthStatus, CursorDaemonStatus, CursorModel } from '../cursor/types';
import type { CursorProbeResult } from '../cursor/cursor-runtime-probe';
import type { CursorConfig } from '../config/unified-config-types';
import { getCcsDirDisplay } from '../utils/config-manager';
import { color } from '../utils/ui';
@@ -18,6 +19,7 @@ export function renderCursorHelp(): number {
'Subcommands:',
' auth Import Cursor IDE authentication token',
' status Show integration, authentication, and daemon status',
' probe Run a live authenticated runtime probe',
' models List available models',
' start Start cursor daemon',
' stop Stop cursor daemon',
@@ -36,8 +38,9 @@ export function renderCursorHelp(): number {
' 1. ccs cursor enable # Enable integration',
' 2. ccs cursor auth # Import Cursor IDE token',
' 3. ccs cursor start # Start daemon',
' 4. ccs cursor "task" # Run Claude through Cursor',
' 5. ccs cursor status # Inspect auth/daemon wiring',
' 4. ccs cursor probe # Verify live runtime health',
' 5. ccs cursor "task" # Run Claude through Cursor',
' 6. ccs cursor status # Inspect auth/daemon wiring',
'',
'Or use the web UI: ccs config -> Cursor page',
'',
@@ -98,6 +101,7 @@ export function renderCursorStatus(
console.log('Client setup:');
console.log(` Raw settings: ${dirDisplay}/cursor.settings.json`);
console.log(' Runtime entry: ccs cursor [claude args]');
console.log(' Live probe: ccs cursor probe');
console.log(' Status command: ccs cursor status');
console.log(' Help command: ccs cursor help');
@@ -135,3 +139,21 @@ export function renderCursorModels(models: CursorModel[], defaultModel: string):
console.log('Model selection is request-driven by the calling client.');
console.log('Dashboard: ccs config -> Cursor page');
}
export function renderCursorProbe(result: CursorProbeResult): void {
const statusIcon = result.ok ? color('[OK]', 'success') : color('[X]', 'error');
console.log('Cursor Live Probe');
console.log('─────────────────');
console.log('');
console.log(`Result: ${statusIcon} ${result.ok ? 'Success' : 'Failure'}`);
console.log(`Stage: ${result.stage}`);
console.log(`HTTP status: ${result.status}`);
console.log(`Duration: ${result.duration_ms} ms`);
if (result.model) {
console.log(`Model: ${result.model}`);
}
if (result.error_type) {
console.log(`Error type: ${result.error_type}`);
}
console.log(`Message: ${result.message}`);
}
+45 -2
View File
@@ -14,10 +14,16 @@ import {
getDaemonStatus,
getAvailableModels,
getDefaultModel,
probeCursorRuntime,
} from '../cursor';
import { getCursorConfig, mutateUnifiedConfig } from '../config/unified-config-loader';
import { DEFAULT_CURSOR_CONFIG } from '../config/unified-config-types';
import { renderCursorHelp, renderCursorModels, renderCursorStatus } from './cursor-command-display';
import {
renderCursorHelp,
renderCursorModels,
renderCursorProbe,
renderCursorStatus,
} from './cursor-command-display';
import { ok, fail, info } from '../utils/ui';
/**
@@ -31,6 +37,8 @@ export async function handleCursorCommand(args: string[]): Promise<number> {
return handleAuth(args.slice(1));
case 'status':
return handleStatus();
case 'probe':
return handleProbe();
case 'models':
return handleModels();
case 'start':
@@ -74,6 +82,34 @@ function parseOptionValue(args: string[], key: string): string | undefined {
return undefined;
}
function printAutoDetectFailure(result: {
error?: string;
checkedPaths?: string[];
reason?: string;
}): void {
console.error(fail(`Auto-detection failed: ${result.error ?? 'Unknown error'}`));
if (result.checkedPaths?.length && result.reason === 'db_not_found') {
console.log('');
console.log('Checked paths:');
for (const candidate of result.checkedPaths) {
console.log(` - ${candidate}`);
}
}
if (result.reason === 'sqlite_unavailable') {
console.log('');
console.log('Recommended next steps:');
console.log(' 1. Install sqlite3 so CCS can read Cursor state automatically');
console.log(' 2. Or use manual import immediately');
} else if (result.reason === 'db_query_failed') {
console.log('');
console.log('Recommended next steps:');
console.log(' 1. Close Cursor IDE and retry auto-detect');
console.log(' 2. If the database remains unreadable, use manual import');
}
}
/**
* Handle auth subcommand.
*/
@@ -139,7 +175,7 @@ async function handleAuth(args: string[]): Promise<number> {
}
console.log('');
console.error(fail(`Auto-detection failed: ${autoResult.error ?? 'Unknown error'}`));
printAutoDetectFailure(autoResult);
console.log('');
console.log('Manual fallback:');
console.log(' ccs cursor auth --manual --token <token> --machine-id <machine-id>');
@@ -156,6 +192,13 @@ async function handleStatus(): Promise<number> {
return 0;
}
async function handleProbe(): Promise<number> {
const cursorConfig = getCursorConfig();
const result = await probeCursorRuntime(cursorConfig);
renderCursorProbe(result);
return result.ok ? 0 : 1;
}
async function handleModels(): Promise<number> {
const cursorConfig = getCursorConfig();
const models = await getAvailableModels(cursorConfig.port);
+1
View File
@@ -1,6 +1,7 @@
export const CURSOR_SUBCOMMANDS = [
'auth',
'status',
'probe',
'models',
'start',
'stop',
+197 -41
View File
@@ -21,6 +21,13 @@ import * as os from 'os';
import type { CursorCredentials, CursorAuthStatus, AutoDetectResult } from './types';
import { getCcsDir } from '../utils/config-manager';
const ACCESS_TOKEN_KEYS = ['cursorAuth/accessToken', 'cursorAuth/token'] as const;
const MACHINE_ID_KEYS = [
'storage.serviceMachineId',
'storage.machineId',
'telemetry.machineId',
] as const;
/**
* Resolve home directory from environment first for deterministic testability,
* then fall back to os.homedir() when env vars are unavailable.
@@ -36,98 +43,247 @@ function resolveHomeDir(): string {
/**
* Get platform-specific path to Cursor's state.vscdb
*/
export function getTokenStoragePath(): string {
export function getTokenStorageCandidates(): string[] {
const platform = process.platform;
const home = resolveHomeDir();
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');
const localAppData = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
return [
path.join(appData, 'Cursor', 'User', 'globalStorage', 'state.vscdb'),
path.join(appData, 'Cursor - Insiders', 'User', 'globalStorage', 'state.vscdb'),
path.join(localAppData, 'Cursor', 'User', 'globalStorage', 'state.vscdb'),
path.join(localAppData, 'Programs', 'Cursor', 'User', 'globalStorage', 'state.vscdb'),
];
}
if (platform === 'darwin') {
return [
path.join(
home,
'Library',
'Application Support',
'Cursor',
'User',
'globalStorage',
'state.vscdb'
),
path.join(
home,
'Library',
'Application Support',
'Cursor - Insiders',
'User',
'globalStorage',
'state.vscdb'
),
];
}
return [
path.join(home, '.config', 'Cursor', 'User', 'globalStorage', 'state.vscdb'),
path.join(home, '.config', 'cursor', 'User', 'globalStorage', 'state.vscdb'),
];
}
export function getTokenStoragePath(): string {
return getTokenStorageCandidates()[0];
}
function normalizeStateValue(value: string): string {
const trimmed = value.trim();
if (!trimmed) return trimmed;
try {
const parsed = JSON.parse(trimmed);
return typeof parsed === 'string' ? parsed : trimmed;
} catch {
return trimmed;
}
}
function getSqliteBinary(): string {
return process.env.CCS_CURSOR_SQLITE_BIN || 'sqlite3';
}
/**
* Query Cursor's SQLite database using sqlite3 CLI
*/
function queryStateDb(dbPath: string, key: string): string | null {
function queryStateDb(
dbPath: string,
key: string
): { value: string | null; sqliteAvailable: boolean; queryFailed: boolean } {
try {
// Escape single quotes to prevent SQL injection
const sanitizedKey = key.replace(/'/g, "''");
const result = execFileSync(
'sqlite3',
getSqliteBinary(),
[dbPath, `SELECT value FROM itemTable WHERE key='${sanitizedKey}'`],
{ encoding: 'utf8', timeout: 5000, stdio: ['pipe', 'pipe', 'ignore'] }
).trim();
return result || null;
);
return {
value: normalizeStateValue(result) || null,
sqliteAvailable: true,
queryFailed: false,
};
} catch (err) {
// Check if sqlite3 is not installed
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
// sqlite3 not found - could log this if needed
return null;
return { value: null, sqliteAvailable: false, queryFailed: false };
}
return null;
return { value: null, sqliteAvailable: true, queryFailed: true };
}
}
function queryStateDbKeys(
dbPath: string,
keys: readonly string[]
): { value: string | null; sqliteAvailable: boolean; queryFailed: boolean } {
for (const key of keys) {
const result = queryStateDb(dbPath, key);
if (!result.sqliteAvailable || result.queryFailed) return result;
if (result.value) return result;
}
return { value: null, sqliteAvailable: true, queryFailed: false };
}
/**
* Auto-detect tokens from Cursor's SQLite database
*/
export function autoDetectTokens(): AutoDetectResult {
// sqlite3 CLI is not bundled with Windows
if (process.platform === 'win32') {
const checkedPaths = getTokenStorageCandidates();
const existingPaths = checkedPaths.filter((candidate) => fs.existsSync(candidate));
if (existingPaths.length === 0) {
return {
found: false,
checkedPaths,
reason: 'db_not_found',
error: `Cursor state database not found. Checked:\n${checkedPaths.join('\n')}`,
};
}
let sawSqliteUnavailable = false;
let sawQueryFailure = false;
let sawTokenMissing = false;
let sawMachineIdMissing = false;
let sawInvalidCredentials = false;
let firstQueryFailurePath: string | undefined;
let firstInvalidCredentialsPath: string | undefined;
for (const dbPath of existingPaths) {
const accessTokenResult = queryStateDbKeys(dbPath, ACCESS_TOKEN_KEYS);
if (!accessTokenResult.sqliteAvailable) {
sawSqliteUnavailable = true;
continue;
}
if (accessTokenResult.queryFailed) {
sawQueryFailure = true;
firstQueryFailurePath ??= dbPath;
continue;
}
if (!accessTokenResult.value) {
sawTokenMissing = true;
continue;
}
const machineIdResult = queryStateDbKeys(dbPath, MACHINE_ID_KEYS);
if (!machineIdResult.sqliteAvailable) {
sawSqliteUnavailable = true;
continue;
}
if (machineIdResult.queryFailed) {
sawQueryFailure = true;
firstQueryFailurePath ??= dbPath;
continue;
}
if (!machineIdResult.value) {
sawMachineIdMissing = true;
continue;
}
if (!validateToken(accessTokenResult.value, machineIdResult.value)) {
sawInvalidCredentials = true;
firstInvalidCredentialsPath ??= dbPath;
continue;
}
return {
found: true,
accessToken: accessTokenResult.value,
machineId: machineIdResult.value,
dbPath,
checkedPaths,
};
}
if (sawSqliteUnavailable) {
return {
found: false,
checkedPaths,
dbPath: existingPaths[0],
reason: 'sqlite_unavailable',
error:
'Auto-detection is not supported on Windows. Please import tokens manually using ccs cursor auth --manual.',
'Cursor state database was found, but sqlite3 is not available in PATH. Install sqlite3 or use manual import.',
};
}
const dbPath = getTokenStoragePath();
// Check if database exists
if (!fs.existsSync(dbPath)) {
if (sawQueryFailure) {
return {
found: false,
checkedPaths,
dbPath: firstQueryFailurePath ?? existingPaths[0],
reason: 'db_query_failed',
error:
'Cursor state database not found. Make sure Cursor IDE is installed and you are logged in.',
'Cursor state database was found, but CCS could not query it. The database may be locked, corrupted, or use an unexpected schema.',
};
}
// Try to query access token
const accessToken = queryStateDb(dbPath, 'cursorAuth/accessToken');
if (!accessToken) {
if (sawInvalidCredentials) {
return {
found: false,
error: 'Access token not found in database. Please log in to Cursor IDE first.',
checkedPaths,
dbPath: firstInvalidCredentialsPath ?? existingPaths[0],
reason: 'invalid_token_format',
error:
'Cursor credentials were found, but the access token or machine ID format was invalid. Re-authenticate in Cursor IDE or use manual import.',
};
}
// Try to query machine ID
const machineId = queryStateDb(dbPath, 'storage.serviceMachineId');
if (!machineId) {
if (sawMachineIdMissing) {
return {
found: false,
error: 'Machine ID not found in database.',
checkedPaths,
dbPath: existingPaths[0],
reason: 'machine_id_not_found',
error:
'Cursor access token was found, but the machine ID was not present in the database. Re-open Cursor IDE or use manual import.',
};
}
if (sawTokenMissing) {
return {
found: false,
checkedPaths,
dbPath: existingPaths[0],
reason: 'access_token_not_found',
error:
'Access token not found in Cursor state database. Make sure you are logged in to Cursor IDE first.',
};
}
return {
found: true,
accessToken,
machineId,
found: false,
checkedPaths,
dbPath: existingPaths[0],
reason: 'db_not_found',
error: 'Cursor credentials could not be detected from the discovered database paths.',
};
}
+368 -201
View File
@@ -6,10 +6,20 @@
import type { IncomingHttpHeaders } from 'http';
import { generateCursorBody, extractTextFromResponse } from './cursor-protobuf.js';
import { buildCursorRequest } from './cursor-translator.js';
import type { CursorTool, CursorApiCredentials } from './cursor-protobuf-schema.js';
import {
isEndStreamConnectFrame,
type CursorTool,
type CursorApiCredentials,
} from './cursor-protobuf-schema.js';
import { buildCursorConnectHeaders, generateCursorChecksum } from './cursor-client-policy.js';
import { StreamingFrameParser, decompressPayload } from './cursor-stream-parser.js';
import {
CursorConnectFrameError,
type FrameResult,
StreamingFrameParser,
decompressPayload,
mapCursorConnectError,
} from './cursor-stream-parser.js';
/** Executor parameters */
interface ExecutorParams {
@@ -41,6 +51,13 @@ interface Http2Response {
body: Buffer;
}
interface CursorExecutorErrorPayload {
message: string;
status: number;
errorType: string;
code: string;
}
/** Lazy import http2 */
let http2Module: typeof import('http2') | null = null;
async function getHttp2() {
@@ -59,34 +76,63 @@ async function getHttp2() {
/**
* Create error response from JSON error
*/
function createErrorResponse(jsonError: {
function toCursorErrorPayloadFromJson(jsonError: {
error?: {
code?: string;
message?: string;
details?: Array<{ debug?: { details?: { title?: string; detail?: string }; error?: string } }>;
};
}): Response {
}): CursorExecutorErrorPayload {
const errorMsg =
jsonError?.error?.details?.[0]?.debug?.details?.title ||
jsonError?.error?.details?.[0]?.debug?.details?.detail ||
jsonError?.error?.message ||
'API Error';
const isRateLimit = jsonError?.error?.code === 'resource_exhausted';
const mappedError = mapCursorConnectError(jsonError?.error?.code);
return new Response(
JSON.stringify({
error: {
message: errorMsg,
type: isRateLimit ? 'rate_limit_error' : 'api_error',
code: jsonError?.error?.details?.[0]?.debug?.error || 'unknown',
},
}),
{
status: isRateLimit ? 429 : 400,
headers: { 'Content-Type': 'application/json' },
}
);
return {
message: errorMsg,
status: mappedError.status,
errorType: mappedError.errorType,
code: jsonError?.error?.details?.[0]?.debug?.error || 'unknown',
};
}
function buildCursorErrorEnvelope(error: CursorExecutorErrorPayload): string {
return JSON.stringify({
error: {
message: error.message,
type: error.errorType,
code: error.code,
status: error.status,
},
});
}
function createCursorErrorResponse(error: CursorExecutorErrorPayload): Response {
return new Response(buildCursorErrorEnvelope(error), {
status: error.status,
headers: { 'Content-Type': 'application/json' },
});
}
function toCursorExecutorErrorPayload(error: unknown): CursorExecutorErrorPayload {
if (error instanceof CursorConnectFrameError) {
return {
message: error.message,
status: error.status,
errorType: error.errorType,
code: 'cursor_protocol_error',
};
}
return {
message: error instanceof Error ? error.message : 'Cursor streaming failed.',
status: 502,
errorType: 'server_error',
code: 'cursor_error',
};
}
export class CursorExecutor {
@@ -413,180 +459,243 @@ export class CursorExecutor {
index: number;
}
>();
const pendingPackets: string[] = [];
let chunkCount = 0;
let toolCallCount = 0;
let streamResponseResolved = false;
const flushPendingPackets = () => {
if (!streamController || pendingPackets.length === 0 || streamClosed) return;
for (const packet of pendingPackets.splice(0)) {
streamController.enqueue(enc.encode(packet));
}
};
const queuePacket = (packet: string) => {
if (streamClosed) return;
if (streamController) {
streamController.enqueue(enc.encode(packet));
return;
}
pendingPackets.push(packet);
};
const emitSSE = (data: string) => {
queuePacket(`data: ${data}\n\n`);
};
const emitSSEEvent = (event: string, data: string) => {
queuePacket(`event: ${event}\ndata: ${data}\n\n`);
};
const readable = new ReadableStream<Uint8Array>({
start(controller) {
streamController = controller;
const emitSSE = (data: string) => {
if (streamClosed) return;
controller.enqueue(enc.encode(`data: ${data}\n\n`));
};
const closeStream = () => {
if (streamClosed) return;
streamClosed = true;
try {
controller.close();
} catch {
/* already closed */
}
client.close();
};
const buildChunk = (delta: Record<string, unknown>, finishReason: string | null) =>
JSON.stringify({
id: responseId,
object: 'chat.completion.chunk',
created,
model,
choices: [{ index: 0, delta, finish_reason: finishReason }],
});
req.on('data', (chunk: Buffer) => {
if (streamClosed) return;
for (const frame of parser.push(chunk)) {
if (frame.type === 'error') {
emitSSE(
JSON.stringify({
error: { message: frame.message, type: frame.errorType, code: '' },
})
);
emitSSE('[DONE]');
closeStream();
return;
}
if (frame.type === 'toolCall') {
const tc = frame.toolCall;
// Emit role chunk on first content
if (chunkCount === 0) {
emitSSE(buildChunk({ role: 'assistant', content: '' }, null));
chunkCount++;
}
if (toolCallsMap.has(tc.id)) {
const existing = toolCallsMap.get(tc.id);
if (!existing) continue;
existing.function.arguments += tc.function.arguments;
existing.isLast = tc.isLast;
if (tc.function.arguments) {
emitSSE(
buildChunk(
{
tool_calls: [
{
index: existing.index,
id: tc.id,
type: 'function',
function: {
name: tc.function.name,
arguments: tc.function.arguments,
},
},
],
},
null
)
);
chunkCount++;
}
} else {
const idx = toolCallCount++;
toolCallsMap.set(tc.id, { ...tc, index: idx });
emitSSE(
buildChunk(
{
tool_calls: [
{
index: idx,
id: tc.id,
type: 'function',
function: {
name: tc.function.name,
arguments: tc.function.arguments,
},
},
],
},
null
)
);
chunkCount++;
}
}
if (frame.type === 'text') {
const delta =
chunkCount === 0 && toolCallCount === 0
? { role: 'assistant', content: frame.text }
: { content: frame.text };
emitSSE(buildChunk(delta, null));
chunkCount++;
}
if (frame.type === 'thinking') {
const delta =
chunkCount === 0 && toolCallCount === 0
? { role: 'assistant', reasoning_content: frame.text }
: { reasoning_content: frame.text };
emitSSE(buildChunk(delta, null));
chunkCount++;
}
}
});
req.on('end', () => {
if (streamClosed) return;
if (chunkCount === 0 && toolCallCount === 0) {
emitSSE(buildChunk({ role: 'assistant', content: '' }, null));
}
emitSSE(
JSON.stringify({
id: responseId,
object: 'chat.completion.chunk',
created,
model,
choices: [
{
index: 0,
delta: {},
finish_reason: toolCallCount > 0 ? 'tool_calls' : 'stop',
},
],
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
})
);
emitSSE('[DONE]');
closeStream();
});
req.on('error', (err) => {
if (!streamClosed) {
try {
controller.error(err);
} catch {
/* already closed */
}
}
client.close();
});
flushPendingPackets();
},
});
resolveOnce(
new Response(readable, {
status: 200,
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
},
})
);
const resolveStreamingResponse = () => {
if (streamResponseResolved || settled) return;
streamResponseResolved = true;
resolveOnce(
new Response(readable, {
status: 200,
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
},
})
);
flushPendingPackets();
};
const closeStream = () => {
if (streamClosed) return;
streamClosed = true;
if (streamController) {
try {
streamController.close();
} catch {
/* already closed */
}
}
client.close();
};
const buildChunk = (delta: Record<string, unknown>, finishReason: string | null) =>
JSON.stringify({
id: responseId,
object: 'chat.completion.chunk',
created,
model,
choices: [{ index: 0, delta, finish_reason: finishReason }],
});
const handleFrameError = (frame: Extract<FrameResult, { type: 'error' }>) => {
const errorPayload = buildCursorErrorEnvelope({
message: frame.message,
status: frame.status,
errorType: frame.errorType,
code: frame.errorType === 'rate_limit_error' ? 'rate_limited' : 'cursor_error',
});
if (!streamResponseResolved && chunkCount === 0 && toolCallCount === 0) {
streamClosed = true;
req.close();
client.close();
resolveOnce(
new Response(errorPayload, {
status: frame.status,
headers: { 'Content-Type': 'application/json' },
})
);
return;
}
resolveStreamingResponse();
emitSSEEvent('error', errorPayload);
closeStream();
};
req.on('data', (chunk: Buffer) => {
if (streamClosed) return;
for (const frame of parser.push(chunk)) {
if (frame.type === 'error') {
handleFrameError(frame);
return;
}
resolveStreamingResponse();
if (frame.type === 'toolCall') {
const tc = frame.toolCall;
// Emit role chunk on first content
if (chunkCount === 0) {
emitSSE(buildChunk({ role: 'assistant', content: '' }, null));
chunkCount++;
}
if (toolCallsMap.has(tc.id)) {
const existing = toolCallsMap.get(tc.id);
if (!existing) continue;
existing.function.arguments += tc.function.arguments;
existing.isLast = tc.isLast;
if (tc.function.arguments) {
emitSSE(
buildChunk(
{
tool_calls: [
{
index: existing.index,
id: tc.id,
type: 'function',
function: {
name: tc.function.name,
arguments: tc.function.arguments,
},
},
],
},
null
)
);
chunkCount++;
}
} else {
const idx = toolCallCount++;
toolCallsMap.set(tc.id, { ...tc, index: idx });
emitSSE(
buildChunk(
{
tool_calls: [
{
index: idx,
id: tc.id,
type: 'function',
function: {
name: tc.function.name,
arguments: tc.function.arguments,
},
},
],
},
null
)
);
chunkCount++;
}
}
if (frame.type === 'text') {
const delta =
chunkCount === 0 && toolCallCount === 0
? { role: 'assistant', content: frame.text }
: { content: frame.text };
emitSSE(buildChunk(delta, null));
chunkCount++;
}
if (frame.type === 'thinking') {
const delta =
chunkCount === 0 && toolCallCount === 0
? { role: 'assistant', reasoning_content: frame.text }
: { reasoning_content: frame.text };
emitSSE(buildChunk(delta, null));
chunkCount++;
}
}
});
req.on('end', () => {
if (streamClosed) return;
for (const frame of parser.finish()) {
if (frame.type === 'error') {
handleFrameError(frame);
return;
}
}
resolveStreamingResponse();
if (chunkCount === 0 && toolCallCount === 0) {
emitSSE(buildChunk({ role: 'assistant', content: '' }, null));
}
emitSSE(
JSON.stringify({
id: responseId,
object: 'chat.completion.chunk',
created,
model,
choices: [
{
index: 0,
delta: {},
finish_reason: toolCallCount > 0 ? 'tool_calls' : 'stop',
},
],
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
})
);
emitSSE('[DONE]');
closeStream();
});
req.on('error', (err) => {
client.close();
if (!streamResponseResolved) {
rejectOnce(err instanceof Error ? err : new Error(String(err)));
return;
}
if (!streamClosed) {
try {
streamController?.error(err);
} catch {
/* already closed */
}
}
});
});
req.on('error', (err) => {
@@ -604,7 +713,7 @@ export class CursorExecutor {
* Shared logic between JSON and SSE transformers.
*/
private *parseProtobufFrames(buffer: Buffer): Generator<
| { type: 'error'; response: Response }
| { type: 'error'; error: CursorExecutorErrorPayload }
| { type: 'text'; text: string }
| { type: 'thinking'; text: string }
| {
@@ -630,13 +739,54 @@ export class CursorExecutor {
let payload = buffer.slice(offset + 5, offset + 5 + length);
offset += 5 + length;
payload = decompressPayload(payload, flags);
try {
payload = decompressPayload(payload, flags);
} catch (error) {
yield { type: 'error', error: toCursorExecutorErrorPayload(error) };
return;
}
if (isEndStreamConnectFrame(flags)) {
try {
const json = JSON.parse(payload.toString('utf-8')) as {
error?: {
code?: string;
message?: string;
details?: Array<{
debug?: { details?: { title?: string; detail?: string }; error?: string };
}>;
};
};
const msg =
json?.error?.details?.[0]?.debug?.details?.title ||
json?.error?.details?.[0]?.debug?.details?.detail ||
json?.error?.message;
if (msg) {
const mappedError = mapCursorConnectError(json?.error?.code);
yield {
type: 'error',
error: {
message: msg,
status: mappedError.status,
errorType: mappedError.errorType,
code: json?.error?.code || 'cursor_error',
},
};
return;
}
} catch {
// Ignore successful end-stream metadata trailers.
}
continue;
}
// Check for JSON error format
try {
const text = payload.toString('utf-8');
if (text.startsWith('{') && text.includes('"error"')) {
yield { type: 'error', response: createErrorResponse(JSON.parse(text)) };
yield { type: 'error', error: toCursorErrorPayloadFromJson(JSON.parse(text)) };
return;
}
} catch (err) {
@@ -656,19 +806,12 @@ export class CursorExecutor {
errorLower.includes('too many requests');
yield {
type: 'error',
response: new Response(
JSON.stringify({
error: {
message: result.error,
type: isRateLimit ? 'rate_limit_error' : 'server_error',
code: isRateLimit ? 'rate_limited' : 'cursor_error',
},
}),
{
status: isRateLimit ? 429 : 400,
headers: { 'Content-Type': 'application/json' },
}
),
error: {
message: result.error,
status: isRateLimit ? 429 : 502,
errorType: isRateLimit ? 'rate_limit_error' : 'server_error',
code: isRateLimit ? 'rate_limited' : 'cursor_error',
},
};
return;
}
@@ -685,6 +828,18 @@ export class CursorExecutor {
yield { type: 'thinking', text: result.thinking };
}
}
if (offset !== buffer.length) {
yield {
type: 'error',
error: {
message: 'Truncated Cursor ConnectRPC frame.',
status: 502,
errorType: 'server_error',
code: 'cursor_protocol_error',
},
};
}
}
transformProtobufToJSON(buffer: Buffer, model: string, _body: ExecutorParams['body']): Response {
@@ -711,7 +866,7 @@ export class CursorExecutor {
for (const frame of this.parseProtobufFrames(buffer)) {
if (frame.type === 'error') {
return frame.response;
return createCursorErrorResponse(frame.error);
}
if (frame.type === 'toolCall') {
@@ -840,7 +995,19 @@ export class CursorExecutor {
for (const frame of this.parseProtobufFrames(buffer)) {
if (frame.type === 'error') {
return frame.response;
if (chunks.length === 0 && toolCalls.length === 0) {
return createCursorErrorResponse(frame.error);
}
chunks.push(`event: error\ndata: ${buildCursorErrorEnvelope(frame.error)}\n\n`);
return new Response(chunks.join(''), {
status: 200,
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
},
});
}
if (frame.type === 'toolCall') {
+7 -6
View File
@@ -4,7 +4,12 @@
*/
import * as zlib from 'zlib';
import { WIRE_TYPE, FIELD, COMPRESS_FLAG, type WireType } from './cursor-protobuf-schema.js';
import {
WIRE_TYPE,
FIELD,
isCompressedConnectFrame,
type WireType,
} from './cursor-protobuf-schema.js';
/**
* Decode a varint from buffer
@@ -121,11 +126,7 @@ export function parseConnectRPCFrame(buffer: Buffer): {
let payload = buffer.slice(5, 5 + length);
// Decompress if gzip
if (
flags === COMPRESS_FLAG.GZIP ||
flags === COMPRESS_FLAG.GZIP_ALT ||
flags === COMPRESS_FLAG.GZIP_BOTH
) {
if (isCompressedConnectFrame(flags)) {
try {
payload = Buffer.from(zlib.gunzipSync(payload));
} catch (err) {
+25 -4
View File
@@ -192,10 +192,31 @@ export interface MessageId {
role: RoleType;
}
/** Compression flags for ConnectRPC frames */
/** Bit flags for ConnectRPC envelopes. */
export const CONNECT_FRAME_FLAG = {
COMPRESSED: 0x01,
END_STREAM: 0x02,
} as const;
/** ConnectRPC frame flag presets. */
export const COMPRESS_FLAG = {
NONE: 0x00,
GZIP: 0x01,
GZIP_ALT: 0x02,
GZIP_BOTH: 0x03,
GZIP: CONNECT_FRAME_FLAG.COMPRESSED,
END_STREAM: CONNECT_FRAME_FLAG.END_STREAM,
GZIP_END_STREAM: CONNECT_FRAME_FLAG.COMPRESSED | CONNECT_FRAME_FLAG.END_STREAM,
} as const;
export const CONNECT_FRAME_FLAG_MASK =
CONNECT_FRAME_FLAG.COMPRESSED | CONNECT_FRAME_FLAG.END_STREAM;
export function isCompressedConnectFrame(flags: number): boolean {
return (flags & CONNECT_FRAME_FLAG.COMPRESSED) === CONNECT_FRAME_FLAG.COMPRESSED;
}
export function isEndStreamConnectFrame(flags: number): boolean {
return (flags & CONNECT_FRAME_FLAG.END_STREAM) === CONNECT_FRAME_FLAG.END_STREAM;
}
export function hasUnknownConnectFrameFlags(flags: number): boolean {
return (flags & ~CONNECT_FRAME_FLAG_MASK) !== 0;
}
+2 -4
View File
@@ -153,7 +153,7 @@ export function buildChatRequest(
}
/**
* Generate complete Cursor request body with ConnectRPC framing
* Generate the raw top-level protobuf request body expected by Cursor upstream.
*/
export function generateCursorBody(
messages: CursorMessage[],
@@ -161,9 +161,7 @@ export function generateCursorBody(
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;
return buildChatRequest(messages, modelName, tools, reasoningEffort);
}
// Re-export all functions
+262
View File
@@ -0,0 +1,262 @@
import type { CursorConfig } from '../config/unified-config-types';
import { checkAuthStatus } from './cursor-auth';
import { isDaemonRunning, startDaemon } from './cursor-daemon';
import { getModelsForDaemon, resolveCursorRequestModel } from './cursor-models';
export interface CursorProbeResult {
ok: boolean;
stage: 'config' | 'auth' | 'daemon' | 'runtime';
status: number;
duration_ms: number;
model?: string;
error_type?: string | null;
message: string;
}
const PROBE_PROMPT = 'Reply with OK only.';
const PROBE_TIMEOUT_MS = 15_000;
const PROBE_SUCCESS_PATTERN = /^ok[.!]?$/i;
function isDaemonReachabilityError(error: unknown): boolean {
if (!(error instanceof Error)) return false;
const message = error.message.toLowerCase();
if (
message.includes('fetch failed') ||
message.includes('econnrefused') ||
message.includes('econnreset') ||
message.includes('socket hang up') ||
message.includes('connection refused')
) {
return true;
}
const cause = (error as Error & { cause?: unknown }).cause;
if (!cause || typeof cause !== 'object' || !('code' in cause)) {
return false;
}
const code = String((cause as { code?: unknown }).code ?? '').toUpperCase();
return ['ECONNREFUSED', 'ECONNRESET', 'ECONNABORTED', 'EPIPE', 'UND_ERR_SOCKET'].includes(code);
}
function parseProbeError(text: string): { errorType: string | null; message: string } {
try {
const parsed = JSON.parse(text) as {
error?: { type?: string; message?: string };
type?: string;
};
if (parsed.error?.message) {
return {
errorType: parsed.error.type ?? null,
message: parsed.error.message,
};
}
if (parsed.type === 'error') {
return {
errorType: null,
message: text,
};
}
} catch {
// fall through to raw text
}
return {
errorType: null,
message: text || 'Unknown probe failure',
};
}
function parseProbeSuccess(text: string): { ok: boolean; message: string } {
try {
const parsed = JSON.parse(text) as {
choices?: Array<{
message?: { content?: string | null };
}>;
error?: { message?: string };
};
if (parsed.error?.message) {
return { ok: false, message: parsed.error.message };
}
const content = parsed.choices?.[0]?.message?.content;
if (typeof content !== 'string' || !content.trim()) {
return { ok: false, message: 'Probe response was missing assistant content.' };
}
return {
ok: PROBE_SUCCESS_PATTERN.test(content.trim()),
message: PROBE_SUCCESS_PATTERN.test(content.trim())
? 'Live probe succeeded.'
: `Probe returned unexpected assistant content: ${content.trim()}`,
};
} catch {
return { ok: false, message: 'Probe response was not valid JSON.' };
}
}
export async function probeCursorRuntime(config: CursorConfig): Promise<CursorProbeResult> {
const startedAt = Date.now();
if (!config.enabled) {
return {
ok: false,
stage: 'config',
status: 400,
duration_ms: Date.now() - startedAt,
message: 'Cursor integration is disabled.',
error_type: 'configuration_error',
};
}
const authStatus = checkAuthStatus();
if (!authStatus.authenticated || !authStatus.credentials) {
return {
ok: false,
stage: 'auth',
status: 401,
duration_ms: Date.now() - startedAt,
message: 'Cursor credentials not found. Run `ccs cursor auth` first.',
error_type: 'authentication_error',
};
}
if (authStatus.expired) {
return {
ok: false,
stage: 'auth',
status: 401,
duration_ms: Date.now() - startedAt,
message: 'Cursor credentials expired. Run `ccs cursor auth` again.',
error_type: 'authentication_error',
};
}
let daemonRunning = await isDaemonRunning(config.port);
if (!daemonRunning && config.auto_start) {
const startResult = await startDaemon({
port: config.port,
ghost_mode: config.ghost_mode,
});
if (!startResult.success) {
daemonRunning = await isDaemonRunning(config.port);
} else {
daemonRunning = true;
}
if (!daemonRunning) {
return {
ok: false,
stage: 'daemon',
status: 503,
duration_ms: Date.now() - startedAt,
message: startResult.error || 'Failed to start Cursor daemon for live probe.',
error_type: 'daemon_start_failed',
};
}
}
if (!daemonRunning) {
return {
ok: false,
stage: 'daemon',
status: 503,
duration_ms: Date.now() - startedAt,
message:
'Cursor daemon is not running. Start it with `ccs cursor start` or enable auto_start.',
error_type: 'daemon_not_running',
};
}
try {
const credentials = {
accessToken: authStatus.credentials.accessToken,
machineId: authStatus.credentials.machineId,
ghostMode: config.ghost_mode,
};
const availableModels = await getModelsForDaemon({ credentials });
const model = resolveCursorRequestModel(config.model, availableModels);
const abortController = new AbortController();
const timeout = setTimeout(() => abortController.abort(), PROBE_TIMEOUT_MS);
try {
const response = await fetch(`http://127.0.0.1:${config.port}/v1/chat/completions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model,
max_tokens: 8,
messages: [{ role: 'user', content: PROBE_PROMPT }],
}),
signal: abortController.signal,
});
const text = await response.text();
const duration = Date.now() - startedAt;
if (!response.ok) {
const error = parseProbeError(text);
return {
ok: false,
stage: 'runtime',
status: response.status,
duration_ms: duration,
model,
error_type: error.errorType,
message: error.message,
};
}
const success = parseProbeSuccess(text);
return {
ok: success.ok,
stage: 'runtime',
status: success.ok ? response.status : 502,
duration_ms: duration,
model,
error_type: success.ok ? null : 'probe_validation_failed',
message: success.message,
};
} finally {
clearTimeout(timeout);
}
} catch (error) {
const daemonReachabilityError = isDaemonReachabilityError(error);
return {
ok: false,
stage:
error instanceof Error && error.name === 'AbortError'
? 'runtime'
: daemonReachabilityError
? 'daemon'
: 'runtime',
status:
error instanceof Error && error.name === 'AbortError'
? 504
: daemonReachabilityError
? 503
: 500,
duration_ms: Date.now() - startedAt,
error_type:
error instanceof Error && error.name === 'AbortError'
? 'probe_timeout'
: daemonReachabilityError
? 'daemon_unreachable'
: 'runtime_error',
message:
error instanceof Error && error.name === 'AbortError'
? `Live probe timed out after ${PROBE_TIMEOUT_MS}ms.`
: daemonReachabilityError
? 'Cursor daemon became unreachable during the live probe. Start it again and retry.'
: error instanceof Error
? error.message
: 'Unknown runtime probe failure.',
};
}
}
+137 -12
View File
@@ -4,7 +4,11 @@
*/
import * as zlib from 'zlib';
import { COMPRESS_FLAG } from './cursor-protobuf-schema.js';
import {
hasUnknownConnectFrameFlags,
isCompressedConnectFrame,
isEndStreamConnectFrame,
} from './cursor-protobuf-schema.js';
import { extractTextFromResponse } from './cursor-protobuf-decoder.js';
/** Frame parsing result types */
@@ -22,12 +26,85 @@ export type FrameResult =
};
};
export class CursorConnectFrameError extends Error {
constructor(
message: string,
readonly status = 502,
readonly errorType = 'server_error'
) {
super(message);
this.name = 'CursorConnectFrameError';
}
}
export function mapCursorConnectError(code?: string): { status: number; errorType: string } {
switch (code?.toLowerCase()) {
case 'resource_exhausted':
return { status: 429, errorType: 'rate_limit_error' };
case 'unauthenticated':
return { status: 401, errorType: 'authentication_error' };
case 'permission_denied':
return { status: 403, errorType: 'permission_error' };
case 'not_found':
return { status: 404, errorType: 'api_error' };
case 'already_exists':
case 'aborted':
return { status: 409, errorType: 'api_error' };
case 'deadline_exceeded':
return { status: 504, errorType: 'api_error' };
case 'unimplemented':
return { status: 501, errorType: 'api_error' };
case 'unavailable':
return { status: 503, errorType: 'api_error' };
case 'invalid_argument':
case 'failed_precondition':
case 'out_of_range':
return { status: 400, errorType: 'api_error' };
default:
return { status: 500, errorType: 'api_error' };
}
}
function formatConnectFrameFlags(flags: number): string {
return `0x${flags.toString(16).padStart(2, '0')}`;
}
function toFrameErrorResult(error: unknown): Extract<FrameResult, { type: 'error' }> {
if (error instanceof CursorConnectFrameError) {
return {
type: 'error',
message: error.message,
status: error.status,
errorType: error.errorType,
};
}
return {
type: 'error',
message: error instanceof Error ? error.message : 'Cursor stream parsing failed.',
status: 502,
errorType: 'server_error',
};
}
function createTruncatedFrameError(): CursorConnectFrameError {
return new CursorConnectFrameError('Truncated Cursor ConnectRPC frame.', 502, 'server_error');
}
/**
* Decompress payload if gzip-compressed.
* Skips decompression for JSON error payloads.
* NOTE: Uses synchronous gzip for single-request CLI tool. Async not warranted for small payloads.
*/
export function decompressPayload(payload: Buffer, flags: number): Buffer {
if (hasUnknownConnectFrameFlags(flags)) {
throw new CursorConnectFrameError(
`Unsupported ConnectRPC frame flags: ${formatConnectFrameFlags(flags)}`,
502,
'server_error'
);
}
if (payload.length > 10 && payload[0] === 0x7b && payload[1] === 0x22) {
try {
const text = payload.toString('utf-8');
@@ -37,18 +114,18 @@ export function decompressPayload(payload: Buffer, flags: number): Buffer {
}
}
if (
flags === COMPRESS_FLAG.GZIP ||
flags === COMPRESS_FLAG.GZIP_ALT ||
flags === COMPRESS_FLAG.GZIP_BOTH
) {
if (isCompressedConnectFrame(flags)) {
try {
return zlib.gunzipSync(payload);
} catch (err) {
if (process.env.CCS_DEBUG) {
console.error('[cursor] gzip decompression failed:', err);
}
return Buffer.alloc(0);
throw new CursorConnectFrameError(
'Failed to decompress Cursor ConnectRPC frame.',
502,
'server_error'
);
}
}
return payload;
@@ -80,7 +157,46 @@ export class StreamingFrameParser {
let payload = this.buffer.slice(5, frameSize);
this.buffer = this.buffer.slice(frameSize);
payload = decompressPayload(payload, flags);
try {
payload = decompressPayload(payload, flags);
} catch (error) {
results.push(toFrameErrorResult(error));
return results;
}
if (isEndStreamConnectFrame(flags)) {
try {
const text = payload.toString('utf-8');
const json = JSON.parse(text) as {
error?: {
code?: string;
message?: string;
details?: Array<{
debug?: { details?: { title?: string; detail?: string }; error?: string };
}>;
};
};
const msg =
json?.error?.details?.[0]?.debug?.details?.title ||
json?.error?.details?.[0]?.debug?.details?.detail ||
json?.error?.message;
if (msg) {
const mappedError = mapCursorConnectError(json?.error?.code);
results.push({
type: 'error',
message: msg,
status: mappedError.status,
errorType: mappedError.errorType,
});
return results;
}
} catch {
// Ignore successful end-stream metadata trailers.
}
continue;
}
// Check for JSON error
try {
@@ -92,12 +208,12 @@ export class StreamingFrameParser {
json?.error?.details?.[0]?.debug?.details?.detail ||
json?.error?.message ||
'API Error';
const isRateLimit = json?.error?.code === 'resource_exhausted';
const mappedError = mapCursorConnectError(json?.error?.code);
results.push({
type: 'error',
message: msg,
status: isRateLimit ? 429 : 400,
errorType: isRateLimit ? 'rate_limit_error' : 'api_error',
status: mappedError.status,
errorType: mappedError.errorType,
});
return results;
}
@@ -116,7 +232,7 @@ export class StreamingFrameParser {
results.push({
type: 'error',
message: result.error,
status: isRateLimit ? 429 : 400,
status: isRateLimit ? 429 : 502,
errorType: isRateLimit ? 'rate_limit_error' : 'server_error',
});
return results;
@@ -133,4 +249,13 @@ export class StreamingFrameParser {
hasPartial(): boolean {
return this.buffer.length > 0;
}
finish(): FrameResult[] {
if (this.buffer.length === 0) {
return [];
}
this.buffer = Buffer.alloc(0);
return [toFrameErrorResult(createTruncatedFrameError())];
}
}
+1
View File
@@ -46,3 +46,4 @@ export {
// Executor
export { CursorExecutor } from './cursor-executor';
export { executeCursorProfile, generateCursorEnv } from './cursor-profile-executor';
export { probeCursorRuntime } from './cursor-runtime-probe';
+12
View File
@@ -54,6 +54,18 @@ export interface AutoDetectResult {
accessToken?: string;
/** Machine ID (if found) */
machineId?: string;
/** SQLite database path used during detection */
dbPath?: string;
/** Paths checked while looking for Cursor state */
checkedPaths?: string[];
/** Structured failure reason when detection fails */
reason?:
| 'db_not_found'
| 'sqlite_unavailable'
| 'db_query_failed'
| 'access_token_not_found'
| 'machine_id_not_found'
| 'invalid_token_format';
/** Error message (if detection failed) */
error?: string;
}
+47 -1
View File
@@ -11,6 +11,7 @@ import {
stopDaemon,
checkAuthStatus,
autoDetectTokens,
probeCursorRuntime,
saveCredentials,
validateToken,
} from '../../cursor';
@@ -30,6 +31,34 @@ interface DaemonStartPreconditionError {
error: string;
}
export function getAutoDetectFailureStatus(
reason?: ReturnType<typeof autoDetectTokens>['reason']
): number {
switch (reason) {
case 'sqlite_unavailable':
return 503;
case 'db_query_failed':
return 500;
case 'invalid_token_format':
return 400;
default:
return 404;
}
}
function getPublicAutoDetectError(result: ReturnType<typeof autoDetectTokens>): string {
switch (result.reason) {
case 'db_not_found':
return 'Cursor state database not found on this host.';
case 'sqlite_unavailable':
return 'Cursor state database was found, but sqlite3 is not available in PATH.';
case 'db_query_failed':
return 'Cursor state database was found, but CCS could not query it.';
default:
return result.error ?? 'Token not found';
}
}
export function getDaemonStartPreconditionError(
input: DaemonStartPreconditionInput
): DaemonStartPreconditionError | null {
@@ -124,7 +153,11 @@ router.post('/auth/auto-detect', async (_req: Request, res: Response): Promise<v
const result = autoDetectTokens();
if (!result.found || !result.accessToken || !result.machineId) {
res.status(404).json({ error: result.error ?? 'Token not found' });
const status = getAutoDetectFailureStatus(result.reason);
res.status(status).json({
error: getPublicAutoDetectError(result),
reason: result.reason ?? null,
});
return;
}
@@ -155,6 +188,19 @@ router.get('/models', async (_req: Request, res: Response): Promise<void> => {
}
});
/**
* POST /api/cursor/probe - Run a live authenticated runtime probe
*/
router.post('/probe', async (_req: Request, res: Response): Promise<void> => {
try {
const cursorConfig = getCursorConfig();
const result = await probeCursorRuntime(cursorConfig);
res.status(result.status).json(result);
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});
/**
* POST /api/cursor/daemon/start - Start cursor proxy daemon
* Path matches copilot convention: /api/{provider}/daemon/{action}