mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-11 03:13:12 +00:00
250 lines
6.7 KiB
TypeScript
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;
|