feat(cursor): complete daemon wiring and add dedicated dashboard page

This commit is contained in:
Tam Nhu Tran committed 2026-02-14 06:22:28 +07:00
1 parent 6c2083949f
commit be9d7cf73e
18 files changed
+1581 -212

No files matched your search

+115
View File
@@ -0,0 +1,115 @@
import type { CursorAuthStatus, CursorDaemonStatus, CursorModel } from '../cursor/types';
import type { CursorConfig } from '../config/unified-config-types';
import { color } from '../utils/ui';
function printLines(lines: string[]): void {
for (const line of lines) {
console.log(line);
}
}
export function renderCursorHelp(): number {
printLines([
'Cursor IDE Integration',
'',
'Usage: ccs cursor <subcommand> [options]',
'',
'Subcommands:',
' auth Import Cursor IDE authentication token',
' status Show integration, authentication, and daemon status',
' models List available models',
' start Start cursor daemon',
' stop Stop cursor daemon',
' enable Enable cursor integration in unified config',
' disable Disable cursor integration in unified config',
' help Show this help message',
'',
'Auth options:',
' ccs cursor auth # Auto-detect from Cursor SQLite',
' ccs cursor auth --manual --token <t> --machine-id <id>',
'',
'Quick start:',
' 1. ccs cursor enable # Enable integration',
' 2. ccs cursor auth # Import Cursor IDE token',
' 3. ccs cursor start # Start daemon',
'',
'Or use the web UI: ccs config -> Cursor page',
'',
]);
return 0;
}
export function renderCursorStatus(
cursorConfig: CursorConfig,
authStatus: CursorAuthStatus,
daemonStatus: CursorDaemonStatus
): void {
console.log('Cursor IDE Status');
console.log('─────────────────');
console.log('');
const enabledIcon = cursorConfig.enabled ? color('[OK]', 'success') : color('[X]', 'error');
console.log(`Integration: ${enabledIcon} ${cursorConfig.enabled ? 'Enabled' : 'Disabled'}`);
const authIcon = authStatus.authenticated ? color('[OK]', 'success') : color('[X]', 'error');
const expiredSuffix = authStatus.authenticated && authStatus.expired ? ' (expired)' : '';
const authText = authStatus.authenticated ? `Authenticated${expiredSuffix}` : 'Not authenticated';
console.log(`Authentication: ${authIcon} ${authText}`);
if (authStatus.authenticated && authStatus.tokenAge !== undefined) {
console.log(` Token age: ${authStatus.tokenAge} hours`);
if (authStatus.credentials?.authMethod) {
console.log(` Method: ${authStatus.credentials.authMethod}`);
}
}
const daemonIcon = daemonStatus.running ? color('[OK]', 'success') : color('[X]', 'error');
console.log(`Daemon: ${daemonIcon} ${daemonStatus.running ? 'Running' : 'Not running'}`);
if (daemonStatus.pid) {
console.log(` PID: ${daemonStatus.pid}`);
}
console.log('');
console.log('Configuration:');
console.log(` Port: ${cursorConfig.port}`);
console.log(` Auto-start: ${cursorConfig.auto_start ? 'Yes' : 'No'}`);
console.log(` Ghost mode: ${cursorConfig.ghost_mode ? 'On' : 'Off'}`);
console.log('');
if (
cursorConfig.enabled &&
authStatus.authenticated &&
!authStatus.expired &&
daemonStatus.running
) {
return;
}
console.log('Next steps:');
if (!cursorConfig.enabled) {
console.log(' - Enable: ccs cursor enable');
}
if (!authStatus.authenticated || authStatus.expired) {
console.log(' - Auth: ccs cursor auth');
}
if (!daemonStatus.running) {
console.log(' - Start: ccs cursor start');
}
}
export function renderCursorModels(models: CursorModel[], defaultModel: string): void {
console.log('Available Cursor Models');
console.log('───────────────────────');
console.log('');
for (const model of models) {
const defaultMark = model.id === defaultModel ? ' [DEFAULT]' : '';
console.log(` ${model.id}${defaultMark}`);
console.log(` Provider: ${model.provider}`);
}
console.log('');
console.log('Model selection is request-driven by the calling client.');
console.log('Dashboard: ccs config -> Cursor page');
}
+136 -126
View File
@@ -6,22 +6,23 @@
import {
autoDetectTokens,
validateToken,
saveCredentials,
checkAuthStatus,
startDaemon,
stopDaemon,
getDaemonStatus,
getAvailableModels,
DEFAULT_CURSOR_PORT,
DEFAULT_CURSOR_MODEL,
getDefaultModel,
} from '../cursor';
import { ok, fail, info, color } from '../utils/ui';
// Temporary default config until #521 adds cursor to unified config
const DEFAULT_CURSOR_CONFIG = {
port: DEFAULT_CURSOR_PORT,
model: DEFAULT_CURSOR_MODEL,
};
import {
getCursorConfig,
loadOrCreateUnifiedConfig,
saveUnifiedConfig,
} from '../config/unified-config-loader';
import { DEFAULT_CURSOR_CONFIG } from '../config/unified-config-types';
import { renderCursorHelp, renderCursorModels, renderCursorStatus } from './cursor-command-display';
import { ok, fail, info } from '../utils/ui';
/** Valid cursor subcommands — imported by ccs.ts for routing */
export const CURSOR_SUBCOMMANDS = [
@@ -30,6 +31,8 @@ export const CURSOR_SUBCOMMANDS = [
'models',
'start',
'stop',
'enable',
'disable',
'help',
'--help',
'-h',
@@ -43,7 +46,7 @@ export async function handleCursorCommand(args: string[]): Promise<number> {
switch (subcommand) {
case 'auth':
return handleAuth();
return handleAuth(args.slice(1));
case 'status':
return handleStatus();
case 'models':
@@ -52,6 +55,10 @@ export async function handleCursorCommand(args: string[]): Promise<number> {
return handleStart();
case 'stop':
return handleStop();
case 'enable':
return handleEnable();
case 'disable':
return handleDisable();
case undefined:
case 'help':
case '--help':
@@ -65,41 +72,70 @@ export async function handleCursorCommand(args: string[]): Promise<number> {
}
}
/**
* Show help for cursor commands.
*/
function handleHelp(): number {
console.log('Cursor IDE Integration');
console.log('');
console.log('Usage: ccs cursor <subcommand>');
console.log('');
console.log('Subcommands:');
console.log(' auth Import Cursor IDE authentication token');
console.log(' status Show authentication and daemon status');
console.log(' models List available models');
console.log(' start Start cursor daemon');
console.log(' stop Stop cursor daemon');
console.log(' help Show this help message');
console.log('');
console.log('Quick start:');
console.log(' 1. ccs cursor auth # Import Cursor IDE token');
console.log(' 2. ccs cursor start # Start daemon');
console.log(' 3. Use cursor models # Via daemon on configured port');
console.log('');
console.log('Or use the web UI: ccs config → Cursor tab');
console.log('');
return 0;
return renderCursorHelp();
}
function parseOptionValue(args: string[], key: string): string | undefined {
const exactIndex = args.findIndex((arg) => arg === key);
if (exactIndex !== -1 && args[exactIndex + 1]) {
return args[exactIndex + 1];
}
const prefix = `${key}=`;
const withEquals = args.find((arg) => arg.startsWith(prefix));
if (withEquals) {
return withEquals.slice(prefix.length);
}
return undefined;
}
/**
* Handle auth subcommand.
*/
async function handleAuth(): Promise<number> {
async function handleAuth(args: string[]): Promise<number> {
const manual = args.includes('--manual');
if (manual) {
const accessToken =
parseOptionValue(args, '--token') ?? parseOptionValue(args, '--access-token') ?? '';
const machineId =
parseOptionValue(args, '--machine-id') ?? parseOptionValue(args, '--machineId') ?? '';
if (!accessToken || !machineId) {
console.error(
fail(
'Manual auth requires both token and machine ID.\n\nExample:\n ccs cursor auth --manual --token <token> --machine-id <machine-id>'
)
);
return 1;
}
if (!validateToken(accessToken, machineId)) {
console.error(fail('Invalid token or machine ID format'));
return 1;
}
saveCredentials({
accessToken,
machineId,
authMethod: 'manual',
importedAt: new Date().toISOString(),
});
console.log(ok('Cursor credentials imported (manual mode)'));
console.log('');
console.log('Next steps:');
console.log(' 1. Enable integration: ccs cursor enable');
console.log(' 2. Start daemon: ccs cursor start');
return 0;
}
console.log(info('Importing Cursor IDE authentication...'));
console.log('');
// Try auto-detection first
console.log(info('Attempting auto-detection...'));
const autoResult = autoDetectTokens();
if (autoResult.found && autoResult.accessToken && autoResult.machineId) {
@@ -109,113 +145,39 @@ async function handleAuth(): Promise<number> {
authMethod: 'auto-detect',
importedAt: new Date().toISOString(),
});
console.log(ok('Auto-detected Cursor credentials'));
console.log('');
console.log('Next steps:');
console.log(' 1. Start daemon: ccs cursor start');
console.log(' 2. Check status: ccs cursor status');
console.log(' 1. Enable integration: ccs cursor enable');
console.log(' 2. Start daemon: ccs cursor start');
console.log(' 3. Check status: ccs cursor status');
return 0;
}
// Fall back to manual import
console.log('');
if (autoResult.error) {
console.log(`Auto-detection failed: ${autoResult.error}`);
} else {
console.log('Auto-detection failed. Please provide credentials manually.');
}
console.error(fail(`Auto-detection failed: ${autoResult.error ?? 'Unknown error'}`));
console.log('');
console.log('To find your Cursor credentials:');
console.log(' 1. Open Cursor IDE');
console.log(' 2. Check application data directory');
console.log(' 3. Look for access token and machine ID');
console.log('Manual fallback:');
console.log(' ccs cursor auth --manual --token <token> --machine-id <machine-id>');
console.log('');
// For now, just show instructions
// Manual import flow will be implemented when needed
console.error(fail('Manual import not yet implemented'));
console.error('');
console.error('Use auto-detection for now or wait for manual import feature.');
return 1;
}
/**
* Handle status subcommand.
*/
async function handleStatus(): Promise<number> {
// TODO: Load from unified config when #521 is complete
const cursorConfig = DEFAULT_CURSOR_CONFIG;
const cursorConfig = getCursorConfig();
const authStatus = checkAuthStatus();
const daemonStatus = await getDaemonStatus(cursorConfig.port);
console.log('Cursor IDE Status');
console.log('─────────────────');
console.log('');
// Auth status
const authIcon = authStatus.authenticated ? color('[OK]', 'success') : color('[X]', 'error');
const authText = authStatus.authenticated ? 'Authenticated' : 'Not authenticated';
console.log(`Authentication: ${authIcon} ${authText}`);
if (authStatus.authenticated && authStatus.tokenAge !== undefined) {
console.log(` Token age: ${authStatus.tokenAge} hours`);
}
// Daemon status
const daemonIcon = daemonStatus.running ? color('[OK]', 'success') : color('[X]', 'error');
const daemonText = daemonStatus.running ? 'Running' : 'Not running';
console.log(`Daemon: ${daemonIcon} ${daemonText}`);
if (daemonStatus.pid) {
console.log(` PID: ${daemonStatus.pid}`);
}
console.log('');
console.log('Configuration:');
console.log(` Port: ${cursorConfig.port}`);
console.log(` Model: ${cursorConfig.model}`);
console.log('');
// Show next steps if not fully configured
if (!authStatus.authenticated || !daemonStatus.running) {
console.log('Next steps:');
if (!authStatus.authenticated) {
console.log(' - Auth: ccs cursor auth');
}
if (!daemonStatus.running) {
console.log(' - Start: ccs cursor start');
}
}
renderCursorStatus(cursorConfig, authStatus, daemonStatus);
return 0;
}
/**
* Handle models subcommand.
*/
async function handleModels(): Promise<number> {
// TODO: Load from unified config when #521 is complete
const cursorConfig = DEFAULT_CURSOR_CONFIG;
console.log('Available Cursor Models');
console.log('───────────────────────');
console.log('');
const cursorConfig = getCursorConfig();
const models = await getAvailableModels(cursorConfig.port);
for (const model of models) {
const current = model.id === cursorConfig.model ? ' [CURRENT]' : '';
const defaultMark = model.isDefault ? ' (default)' : '';
console.log(` ${model.id}${current}${defaultMark}`);
console.log(` Provider: ${model.provider}`);
}
console.log('');
console.log('To change model: ccs config (Cursor section)');
const defaultModel = getDefaultModel();
renderCursorModels(models, defaultModel);
return 0;
}
@@ -223,19 +185,29 @@ async function handleModels(): Promise<number> {
* Handle start subcommand.
*/
async function handleStart(): Promise<number> {
// TODO: Load from unified config when #521 is complete
const cursorConfig = DEFAULT_CURSOR_CONFIG;
const cursorConfig = getCursorConfig();
if (!cursorConfig.enabled) {
console.error(fail('Cursor integration is disabled. Run: ccs cursor enable'));
return 1;
}
// Check auth first
const authStatus = checkAuthStatus();
if (!authStatus.authenticated) {
console.error(fail('Not authenticated. Run: ccs cursor auth'));
return 1;
}
if (authStatus.expired) {
console.error(fail('Credentials expired. Run: ccs cursor auth'));
return 1;
}
console.log(info(`Starting cursor daemon on port ${cursorConfig.port}...`));
const result = await startDaemon(cursorConfig);
const result = await startDaemon({
port: cursorConfig.port,
ghost_mode: cursorConfig.ghost_mode,
});
if (result.success) {
console.log(ok(`Daemon started (PID: ${result.pid})`));
@@ -262,3 +234,41 @@ async function handleStop(): Promise<number> {
return 1;
}
}
/**
* Handle enable subcommand.
*/
async function handleEnable(): Promise<number> {
const config = loadOrCreateUnifiedConfig();
if (!config.cursor) {
config.cursor = { ...DEFAULT_CURSOR_CONFIG };
}
config.cursor.enabled = true;
saveUnifiedConfig(config);
console.log(ok('Cursor integration enabled'));
console.log('');
console.log('Next steps:');
console.log(' 1. Authenticate: ccs cursor auth');
console.log(' 2. Start daemon: ccs cursor start');
console.log(' 3. Check status: ccs cursor status');
return 0;
}
/**
* Handle disable subcommand.
*/
async function handleDisable(): Promise<number> {
const config = loadOrCreateUnifiedConfig();
if (config.cursor) {
config.cursor.enabled = false;
saveUnifiedConfig(config);
}
console.log(ok('Cursor integration disabled'));
return 0;
}
+3
View File
@@ -232,10 +232,13 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
[
['ccs cursor <cmd>', 'Use Cursor IDE integration'],
['ccs cursor auth', 'Import Cursor token'],
['ccs cursor auth --manual --token <t> --machine-id <id>', 'Manual token import'],
['ccs cursor status', 'Show connection status'],
['ccs cursor models', 'List available models'],
['ccs cursor start', 'Start proxy daemon'],
['ccs cursor stop', 'Stop proxy daemon'],
['ccs cursor enable', 'Enable cursor integration'],
['ccs cursor disable', 'Disable cursor integration'],
]
);
+283
View File
@@ -0,0 +1,283 @@
/**
* Cursor Daemon Entry
*
* Dedicated child-process entrypoint for local OpenAI-compatible Cursor proxy.
*/
import * as http from 'http';
import { Readable } from 'stream';
import { CursorExecutor } from './cursor-executor';
import { checkAuthStatus } from './cursor-auth';
import { DEFAULT_CURSOR_MODEL, DEFAULT_CURSOR_MODELS } from './cursor-models';
import type { CursorTool } from './cursor-protobuf-schema';
interface DaemonRuntimeOptions {
port: number;
ghostMode: boolean;
}
interface OpenAIMessage {
role: string;
content?: string | Array<{ type: string; text?: string }> | null;
name?: string;
tool_call_id?: string;
tool_calls?: Array<{
id: string;
type: string;
function: { name: string; arguments: string };
}>;
}
interface NormalizedOpenAIMessage {
role: string;
content: string | Array<{ type: string; text?: string }>;
name?: string;
tool_call_id?: string;
tool_calls?: Array<{
id: string;
type: string;
function: { name: string; arguments: string };
}>;
}
interface OpenAIChatRequest {
model?: string;
stream?: boolean;
reasoning_effort?: string;
tools?: CursorTool[];
messages?: OpenAIMessage[];
}
const MAX_BODY_SIZE = 10 * 1024 * 1024; // 10MB
function writeJson(res: http.ServerResponse, statusCode: number, payload: unknown): void {
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(payload));
}
function readJsonBody(req: http.IncomingMessage): Promise<unknown> {
return new Promise((resolve, reject) => {
const chunks: Buffer[] = [];
let total = 0;
req.on('data', (chunk: Buffer) => {
total += chunk.length;
if (total > MAX_BODY_SIZE) {
req.destroy();
reject(new Error('Request body too large (max 10MB)'));
return;
}
chunks.push(chunk);
});
req.on('end', () => {
const raw = Buffer.concat(chunks).toString('utf8').trim();
if (!raw) {
resolve({});
return;
}
try {
resolve(JSON.parse(raw));
} catch {
reject(new Error('Invalid JSON in request body'));
}
});
req.on('error', reject);
});
}
function normalizeMessages(raw: unknown): NormalizedOpenAIMessage[] {
if (!Array.isArray(raw)) {
throw new Error('messages must be an array');
}
return raw.map((message, index) => {
if (typeof message !== 'object' || message === null) {
throw new Error(`messages[${index}] must be an object`);
}
const m = message as Record<string, unknown>;
if (typeof m.role !== 'string' || !m.role) {
throw new Error(`messages[${index}].role must be a non-empty string`);
}
const content = m.content;
if (
content !== undefined &&
content !== null &&
typeof content !== 'string' &&
!Array.isArray(content)
) {
throw new Error(`messages[${index}].content must be string, array, or null`);
}
return {
role: m.role,
content: (content ?? '') as NormalizedOpenAIMessage['content'],
name: typeof m.name === 'string' ? m.name : undefined,
tool_call_id: typeof m.tool_call_id === 'string' ? m.tool_call_id : undefined,
tool_calls: Array.isArray(m.tool_calls)
? (m.tool_calls as NormalizedOpenAIMessage['tool_calls'])
: undefined,
};
});
}
async function pipeWebResponseToNode(response: Response, res: http.ServerResponse): Promise<void> {
res.statusCode = response.status;
response.headers.forEach((value, key) => {
res.setHeader(key, value);
});
if (!response.body) {
res.end();
return;
}
const nodeStream = Readable.fromWeb(response.body as unknown as ReadableStream<Uint8Array>);
await new Promise<void>((resolve, reject) => {
nodeStream.on('error', reject);
nodeStream.on('end', resolve);
nodeStream.pipe(res);
});
}
function parseArgs(argv: string[]): DaemonRuntimeOptions {
let port = 20129;
let ghostMode = true;
for (let i = 0; i < argv.length; i++) {
const arg = argv[i];
if (arg === '--port' && argv[i + 1]) {
const parsed = parseInt(argv[i + 1], 10);
if (Number.isInteger(parsed) && parsed > 0 && parsed <= 65535) {
port = parsed;
}
i++;
continue;
}
if (arg === '--ghost-mode' && argv[i + 1]) {
const value = argv[i + 1];
ghostMode = value !== 'false' && value !== '0';
i++;
continue;
}
}
return { port, ghostMode };
}
export function startCursorDaemonServer(options: DaemonRuntimeOptions): http.Server {
const executor = new CursorExecutor();
const server = http.createServer(async (req, res) => {
try {
const method = req.method || 'GET';
const requestUrl = req.url || '/';
if (method === 'GET' && requestUrl === '/health') {
writeJson(res, 200, { ok: true, service: 'cursor-daemon' });
return;
}
if (method === 'GET' && requestUrl === '/v1/models') {
const data = DEFAULT_CURSOR_MODELS.map((model) => ({
id: model.id,
object: 'model',
created: 0,
owned_by: model.provider,
}));
writeJson(res, 200, { object: 'list', data });
return;
}
if (method !== 'POST' || requestUrl !== '/v1/chat/completions') {
writeJson(res, 404, { error: 'Not found' });
return;
}
const parsedBody = (await readJsonBody(req)) as OpenAIChatRequest;
const messages = normalizeMessages(parsedBody.messages);
const model =
typeof parsedBody.model === 'string' && parsedBody.model
? parsedBody.model
: DEFAULT_CURSOR_MODEL;
const stream = parsedBody.stream === true;
const authStatus = checkAuthStatus();
if (!authStatus.authenticated || !authStatus.credentials) {
writeJson(res, 401, {
error: {
type: 'authentication_error',
message: 'Cursor credentials not found. Run `ccs cursor auth` first.',
},
});
return;
}
if (authStatus.expired) {
writeJson(res, 401, {
error: {
type: 'authentication_error',
message: 'Cursor credentials expired. Run `ccs cursor auth` again.',
},
});
return;
}
const abortController = new AbortController();
req.on('close', () => {
if (!res.writableEnded) {
abortController.abort();
}
});
const result = await executor.execute({
model,
stream,
signal: abortController.signal,
credentials: {
accessToken: authStatus.credentials.accessToken,
machineId: authStatus.credentials.machineId,
ghostMode: options.ghostMode,
},
body: {
messages,
tools: Array.isArray(parsedBody.tools) ? parsedBody.tools : undefined,
reasoning_effort:
typeof parsedBody.reasoning_effort === 'string'
? parsedBody.reasoning_effort
: undefined,
},
});
await pipeWebResponseToNode(result.response, res);
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
writeJson(res, 400, {
error: {
type: 'invalid_request_error',
message,
},
});
}
});
server.listen(options.port, '127.0.0.1');
return server;
}
if (require.main === module) {
const options = parseArgs(process.argv.slice(2));
const server = startCursorDaemonServer(options);
const shutdown = () => {
server.close(() => process.exit(0));
};
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);
}
+29 -25
View File
@@ -9,7 +9,7 @@ import { spawn, ChildProcess } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import * as http from 'http';
import type { CursorConfig, CursorDaemonStatus } from './types';
import type { CursorDaemonConfig, CursorDaemonStatus } from './types';
import { getCcsDir } from '../utils/config-manager';
/**
@@ -27,6 +27,16 @@ function getPidFilePath(): string {
return path.join(getCursorDir(), 'daemon.pid');
}
/**
* Resolve daemon entrypoint script path for the current runtime.
* - Dist runtime: cursor-daemon-entry.js
* - Bun source runtime (tests/dev): cursor-daemon-entry.ts
*/
function resolveDaemonEntrypoint(): string {
const isBunRuntime = process.execPath.toLowerCase().includes('bun');
return path.join(__dirname, isBunRuntime ? 'cursor-daemon-entry.ts' : 'cursor-daemon-entry.js');
}
/**
* Check if cursor daemon is running on the specified port.
* Uses 127.0.0.1 instead of localhost for more reliable local connections.
@@ -128,7 +138,7 @@ export function removePidFile(): void {
* @returns Promise that resolves when daemon is ready
*/
export async function startDaemon(
config: CursorConfig
config: CursorDaemonConfig
): Promise<{ success: boolean; pid?: number; error?: string }> {
// Check if already running
if (await isDaemonRunning(config.port)) {
@@ -140,8 +150,16 @@ export async function startDaemon(
return { success: false, error: `Invalid port: ${config.port}` };
}
// For now, create a simple structure that will be filled in later
// The actual server implementation will be added in a separate task
const daemonEntry = resolveDaemonEntrypoint();
try {
await fs.promises.access(daemonEntry, fs.constants.R_OK);
} catch {
return {
success: false,
error: 'Cursor daemon entrypoint not found. Run `bun run build` and retry.',
};
}
return new Promise((resolve) => {
let proc: ChildProcess;
let resolved = false;
@@ -157,30 +175,16 @@ export async function startDaemon(
let checkTimeout: NodeJS.Timeout | null = null;
try {
// Spawn a placeholder Node.js process
// TODO: Replace with actual CursorExecutor-based server
const args = [
'-e',
`
const http = require('http');
const server = http.createServer((req, res) => {
if (req.url === '/health') {
res.writeHead(200);
res.end('OK');
} else if (req.url === '/v1/models') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ data: [] }));
} else {
res.writeHead(404);
res.end('Not found');
}
});
server.listen(${config.port}, '127.0.0.1');
`,
daemonEntry,
'--port',
String(config.port),
'--ghost-mode',
String(config.ghost_mode !== false),
'--ccs-daemon',
];
// Append --ccs-daemon marker for PID validation in stopDaemon
proc = spawn(process.execPath, [...args, '--ccs-daemon'], {
proc = spawn(process.execPath, args, {
stdio: 'ignore',
detached: true,
});
+1 -1
View File
@@ -10,7 +10,7 @@ import type { CursorModel } from './types';
import { isDaemonRunning } from './cursor-daemon';
/** Default daemon port */
export const DEFAULT_CURSOR_PORT = 4242;
export const DEFAULT_CURSOR_PORT = 20129;
/** Default model ID */
export const DEFAULT_CURSOR_MODEL = 'gpt-4.1';
+8 -1
View File
@@ -8,7 +8,14 @@
export * from './types';
// Auth
export { autoDetectTokens, saveCredentials, loadCredentials, checkAuthStatus } from './cursor-auth';
export {
autoDetectTokens,
validateToken,
saveCredentials,
loadCredentials,
deleteCredentials,
checkAuthStatus,
} from './cursor-auth';
// Daemon
export {
+3 -4
View File
@@ -5,12 +5,11 @@
*/
/**
* Cursor daemon configuration.
* Temporary interface until #521 adds cursor to unified config.
* Cursor daemon runtime configuration.
*/
export interface CursorConfig {
export interface CursorDaemonConfig {
port: number;
model: string;
ghost_mode?: boolean;
}
/**
+16 -46
View File
@@ -5,11 +5,16 @@
import type { Request, Response } from 'express';
import { Router } from 'express';
import {
getDaemonStatus,
getAvailableModels,
getDefaultModel,
startDaemon,
stopDaemon,
checkAuthStatus,
autoDetectTokens,
saveCredentials,
validateToken,
} from '../../cursor/cursor-auth';
} from '../../cursor';
import { getCursorConfig } from '../../config/unified-config-loader';
import cursorSettingsRoutes from './cursor-settings-routes';
@@ -18,48 +23,6 @@ const router = Router();
// Mount settings sub-routes
router.use('/settings', cursorSettingsRoutes);
/**
* Get daemon status
* TODO: Implement in cursor-executor.ts (#520)
*/
async function getDaemonStatus(port: number): Promise<{ running: boolean; port?: number }> {
// Stub - will be implemented in #520
return { running: false, port };
}
/**
* Get available models
* TODO: Implement in cursor-executor.ts (#520)
*/
async function getAvailableModels(): Promise<string[]> {
// Stub - will be implemented in #520
return []; // TODO: populated by cursor-models.ts (#520)
}
/**
* Start daemon
* TODO: Implement in cursor-executor.ts (#520)
*/
async function startDaemon(
port: number,
ghostMode: boolean
): Promise<{ success: boolean; message: string }> {
// Stub - will be implemented in #520
return {
success: false,
message: `Daemon start not implemented (port: ${port}, ghost: ${ghostMode})`,
};
}
/**
* Stop daemon
* TODO: Implement in cursor-executor.ts (#520)
*/
async function stopDaemon(): Promise<{ success: boolean; message: string }> {
// Stub - will be implemented in #520
return { success: false, message: 'Daemon stop not implemented' };
}
/**
* GET /api/cursor/status - Get Cursor status (auth + daemon)
*/
@@ -72,6 +35,9 @@ router.get('/status', async (_req: Request, res: Response): Promise<void> => {
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,
@@ -144,8 +110,9 @@ router.post('/auth/auto-detect', async (_req: Request, res: Response): Promise<v
*/
router.get('/models', async (_req: Request, res: Response): Promise<void> => {
try {
const models = await getAvailableModels();
res.json({ models });
const cursorConfig = getCursorConfig();
const models = await getAvailableModels(cursorConfig.port);
res.json({ models, current: getDefaultModel() });
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
@@ -158,7 +125,10 @@ router.get('/models', async (_req: Request, res: Response): Promise<void> => {
router.post('/daemon/start', async (_req: Request, res: Response): Promise<void> => {
try {
const cursorConfig = getCursorConfig();
const result = await startDaemon(cursorConfig.port, cursorConfig.ghost_mode);
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 });
@@ -16,6 +16,24 @@ import {
const router = Router();
function parseLocalCursorPort(settings: unknown): number | null {
if (typeof settings !== 'object' || settings === null) return null;
const env = (settings as { env?: unknown }).env;
if (typeof env !== 'object' || env === null) return null;
const baseUrl = (env as { ANTHROPIC_BASE_URL?: unknown }).ANTHROPIC_BASE_URL;
if (typeof baseUrl !== 'string' || !baseUrl) return null;
try {
const parsed = new URL(baseUrl);
if (parsed.hostname !== '127.0.0.1' && parsed.hostname !== 'localhost') return null;
const port = Number(parsed.port);
if (!Number.isInteger(port) || port < 1 || port > 65535) return null;
return port;
} catch {
return null;
}
}
/**
* GET /api/cursor/settings - Get cursor config (port, auto_start, ghost_mode)
*/
@@ -108,7 +126,7 @@ router.get('/raw', (_req: Request, res: Response): void => {
res.json({
settings: defaultSettings,
mtime: Date.now(),
path: settingsPath,
path: '~/.ccs/cursor.settings.json',
exists: false,
});
return;
@@ -121,7 +139,7 @@ router.get('/raw', (_req: Request, res: Response): void => {
res.json({
settings,
mtime: stat.mtimeMs,
path: settingsPath,
path: '~/.ccs/cursor.settings.json',
exists: true,
});
} catch (error) {
@@ -158,7 +176,16 @@ router.put('/raw', (req: Request, res: Response): void => {
fs.writeFileSync(tempPath, JSON.stringify(settings, null, 2) + '\n');
fs.renameSync(tempPath, settingsPath);
// TODO: Sync raw settings back to unified config when cursor-daemon is integrated (#520)
// Keep unified config aligned with raw settings edits (parity with Copilot raw editor).
const parsedPort = parseLocalCursorPort(settings);
if (parsedPort) {
const config = loadOrCreateUnifiedConfig();
config.cursor = {
...(config.cursor ?? DEFAULT_CURSOR_CONFIG),
port: parsedPort,
};
saveUnifiedConfig(config);
}
const stat = fs.statSync(settingsPath);
res.json({ success: true, mtime: stat.mtimeMs });