Files
ccs/src/web-server/routes/cursor-routes.ts
T

250 lines
6.7 KiB
TypeScript

/**
* Cursor Routes - Cursor IDE integration via cursor proxy daemon
*/
import type { Request, Response } from 'express';
import { Router } from 'express';
import {
getDaemonStatus,
getAvailableModels,
startDaemon,
stopDaemon,
checkAuthStatus,
autoDetectTokens,
probeCursorRuntime,
saveCredentials,
validateToken,
} from '../../cursor';
import { getCursorConfig } from '../../config/unified-config-loader';
import cursorSettingsRoutes from './cursor-settings-routes';
const router = Router();
interface DaemonStartPreconditionInput {
enabled: boolean;
authenticated: boolean;
tokenExpired?: boolean;
}
interface DaemonStartPreconditionError {
status: number;
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 {
if (!input.enabled) {
return {
status: 400,
error: 'Cursor integration is disabled. Enable it before starting daemon.',
};
}
if (!input.authenticated) {
return {
status: 401,
error: 'Cursor authentication required. Import credentials before starting daemon.',
};
}
if (input.tokenExpired) {
return {
status: 401,
error: 'Cursor credentials expired. Re-authenticate before starting daemon.',
};
}
return null;
}
// Mount settings sub-routes
router.use('/settings', cursorSettingsRoutes);
/**
* GET /api/cursor/status - Get Cursor status (auth + daemon)
*/
router.get('/status', async (_req: Request, res: Response): Promise<void> => {
try {
const cursorConfig = getCursorConfig();
const authStatus = checkAuthStatus();
const daemonStatus = await getDaemonStatus(cursorConfig.port);
res.json({
enabled: cursorConfig.enabled,
authenticated: authStatus.authenticated,
auth_method: authStatus.credentials?.authMethod ?? null,
token_age: authStatus.tokenAge ?? null,
token_expired: authStatus.expired ?? false,
daemon_running: daemonStatus.running,
port: cursorConfig.port,
auto_start: cursorConfig.auto_start,
ghost_mode: cursorConfig.ghost_mode,
});
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});
/**
* POST /api/cursor/auth/import - Import Cursor token manually
*/
router.post('/auth/import', async (req: Request, res: Response): Promise<void> => {
try {
const { accessToken, machineId } = req.body;
if (!accessToken || !machineId) {
res.status(400).json({ error: 'Missing accessToken or machineId' });
return;
}
// Validate token format
if (!validateToken(accessToken, machineId)) {
res.status(400).json({ error: 'Invalid token or machine ID format' });
return;
}
// Save credentials
saveCredentials({
accessToken,
machineId,
authMethod: 'manual',
importedAt: new Date().toISOString(),
});
res.json({ success: true, message: 'Token imported successfully' });
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});
/**
* POST /api/cursor/auth/auto-detect - Auto-detect token from SQLite
*/
router.post('/auth/auto-detect', async (_req: Request, res: Response): Promise<void> => {
try {
const result = autoDetectTokens();
if (!result.found || !result.accessToken || !result.machineId) {
const status = getAutoDetectFailureStatus(result.reason);
res.status(status).json({
error: getPublicAutoDetectError(result),
reason: result.reason ?? null,
});
return;
}
// Save credentials
saveCredentials({
accessToken: result.accessToken,
machineId: result.machineId,
authMethod: 'auto-detect',
importedAt: new Date().toISOString(),
});
res.json({ success: true, message: 'Token auto-detected and imported' });
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});
/**
* GET /api/cursor/models - List available models
*/
router.get('/models', async (_req: Request, res: Response): Promise<void> => {
try {
const cursorConfig = getCursorConfig();
const models = await getAvailableModels(cursorConfig.port);
res.json({ models, current: cursorConfig.model });
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});
/**
* 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}
*/
router.post('/daemon/start', async (_req: Request, res: Response): Promise<void> => {
try {
const cursorConfig = getCursorConfig();
const authStatus = checkAuthStatus();
const preconditionError = getDaemonStartPreconditionError({
enabled: cursorConfig.enabled,
authenticated: authStatus.authenticated,
tokenExpired: authStatus.expired ?? false,
});
if (preconditionError) {
res.status(preconditionError.status).json({
success: false,
error: preconditionError.error,
});
return;
}
const result = await startDaemon({
port: cursorConfig.port,
ghost_mode: cursorConfig.ghost_mode,
});
res.json(result);
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});
/**
* POST /api/cursor/daemon/stop - Stop cursor proxy daemon
* Path matches copilot convention: /api/{provider}/daemon/{action}
*/
router.post('/daemon/stop', async (_req: Request, res: Response): Promise<void> => {
try {
const result = await stopDaemon();
res.json(result);
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});
export default router;