mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-02 08:19:59 +00:00
feat(cursor): add daemon lifecycle, models catalog, and CLI commands
Implements #520 - Cursor IDE daemon, models catalog, module index, and CLI. Files created: - src/cursor/cursor-daemon.ts (~230 LOC) - Lifecycle management (start/stop/status) - src/cursor/cursor-models.ts (~145 LOC) - Model catalog with Anthropic/OpenAI/Google/Cursor models - src/cursor/index.ts (~30 LOC) - Module barrel exports - src/commands/cursor-command.ts (~230 LOC) - CLI commands (auth/status/models/start/stop) Key features: - Daemon health checks via HTTP /health endpoint - PID file management in ~/.ccs/cursor/daemon.pid - Model catalog with fetchModelsFromDaemon() and fallback defaults - CLI subcommand routing following copilot-command.ts pattern - ASCII-only output ([OK], [X], [i] markers) Temporary defaults for config (port: 4242, model: gpt-4.1) until #521 adds cursor to unified config schema. Note: Actual daemon server implementation (CursorExecutor integration) deferred to #522 - this creates the lifecycle structure only.
This commit is contained in:
@@ -0,0 +1,238 @@
|
|||||||
|
/**
|
||||||
|
* Cursor CLI Command
|
||||||
|
*
|
||||||
|
* Handles `ccs cursor <subcommand>` commands.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
autoDetectTokens,
|
||||||
|
checkAuthStatus,
|
||||||
|
startDaemon,
|
||||||
|
stopDaemon,
|
||||||
|
getDaemonStatus,
|
||||||
|
getAvailableModels,
|
||||||
|
} 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: 4242,
|
||||||
|
model: 'gpt-4.1',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle cursor subcommand.
|
||||||
|
*/
|
||||||
|
export async function handleCursorCommand(args: string[]): Promise<number> {
|
||||||
|
const subcommand = args[0];
|
||||||
|
|
||||||
|
switch (subcommand) {
|
||||||
|
case 'auth':
|
||||||
|
return handleAuth();
|
||||||
|
case 'status':
|
||||||
|
return handleStatus();
|
||||||
|
case 'models':
|
||||||
|
return handleModels();
|
||||||
|
case 'start':
|
||||||
|
return handleStart();
|
||||||
|
case 'stop':
|
||||||
|
return handleStop();
|
||||||
|
case undefined:
|
||||||
|
case 'help':
|
||||||
|
case '--help':
|
||||||
|
case '-h':
|
||||||
|
return handleHelp();
|
||||||
|
default:
|
||||||
|
console.error(fail(`Unknown subcommand: ${subcommand}`));
|
||||||
|
console.error('');
|
||||||
|
return handleHelp();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle auth subcommand.
|
||||||
|
*/
|
||||||
|
async function handleAuth(): Promise<number> {
|
||||||
|
console.log(info('Importing Cursor IDE authentication...'));
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// Try auto-detection first
|
||||||
|
console.log(info('Attempting auto-detection...'));
|
||||||
|
const autoResult = await autoDetectTokens();
|
||||||
|
|
||||||
|
if (autoResult.found && autoResult.accessToken && autoResult.machineId) {
|
||||||
|
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');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to manual import
|
||||||
|
console.log('');
|
||||||
|
console.log('Auto-detection failed. Please provide credentials manually.');
|
||||||
|
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('');
|
||||||
|
|
||||||
|
// 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 authStatus = await 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.toFixed(1)} 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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)');
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle start subcommand.
|
||||||
|
*/
|
||||||
|
async function handleStart(): Promise<number> {
|
||||||
|
// TODO: Load from unified config when #521 is complete
|
||||||
|
const cursorConfig = DEFAULT_CURSOR_CONFIG;
|
||||||
|
|
||||||
|
// Check auth first
|
||||||
|
const authStatus = await checkAuthStatus();
|
||||||
|
if (!authStatus.authenticated) {
|
||||||
|
console.error(fail('Not authenticated. Run: ccs cursor auth'));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(info(`Starting cursor daemon on port ${cursorConfig.port}...`));
|
||||||
|
|
||||||
|
const result = await startDaemon(cursorConfig);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
console.log(ok(`Daemon started (PID: ${result.pid})`));
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
console.error(fail(result.error || 'Failed to start daemon'));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle stop subcommand.
|
||||||
|
*/
|
||||||
|
async function handleStop(): Promise<number> {
|
||||||
|
console.log(info('Stopping cursor daemon...'));
|
||||||
|
|
||||||
|
const result = await stopDaemon();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
console.log(ok('Daemon stopped'));
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
console.error(fail(result.error || 'Failed to stop daemon'));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
/**
|
||||||
|
* Cursor Daemon Manager
|
||||||
|
*
|
||||||
|
* Manages the cursor daemon lifecycle (start/stop/status).
|
||||||
|
* Uses CursorExecutor for OpenAI-compatible API proxy to Cursor backend.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { spawn, ChildProcess } from 'child_process';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as http from 'http';
|
||||||
|
import type { CursorDaemonStatus } from './types';
|
||||||
|
import { getCcsDir } from '../utils/config-manager';
|
||||||
|
|
||||||
|
// Temporary interface until #521 adds cursor to unified config
|
||||||
|
interface CursorConfig {
|
||||||
|
port: number;
|
||||||
|
model: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Cursor directory path.
|
||||||
|
*/
|
||||||
|
function getCursorDir(): string {
|
||||||
|
return path.join(getCcsDir(), 'cursor');
|
||||||
|
}
|
||||||
|
|
||||||
|
const PID_FILE = path.join(getCursorDir(), 'daemon.pid');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if cursor daemon is running on the specified port.
|
||||||
|
* Uses 127.0.0.1 instead of localhost for more reliable local connections.
|
||||||
|
*/
|
||||||
|
export async function isDaemonRunning(port: number): Promise<boolean> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const req = http.request(
|
||||||
|
{
|
||||||
|
hostname: '127.0.0.1',
|
||||||
|
port,
|
||||||
|
path: '/health',
|
||||||
|
method: 'GET',
|
||||||
|
timeout: 3000,
|
||||||
|
},
|
||||||
|
(res) => {
|
||||||
|
resolve(res.statusCode === 200);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
req.on('error', () => {
|
||||||
|
resolve(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('timeout', () => {
|
||||||
|
req.destroy();
|
||||||
|
resolve(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get daemon status.
|
||||||
|
*/
|
||||||
|
export async function getDaemonStatus(port: number): Promise<CursorDaemonStatus> {
|
||||||
|
const running = await isDaemonRunning(port);
|
||||||
|
const pid = getPidFromFile();
|
||||||
|
|
||||||
|
return {
|
||||||
|
running,
|
||||||
|
port,
|
||||||
|
pid: running ? (pid ?? undefined) : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read PID from file.
|
||||||
|
*/
|
||||||
|
function getPidFromFile(): number | null {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(PID_FILE)) {
|
||||||
|
const content = fs.readFileSync(PID_FILE, 'utf8').trim();
|
||||||
|
const pid = parseInt(content, 10);
|
||||||
|
return isNaN(pid) ? null : pid;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore errors
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write PID to file.
|
||||||
|
*/
|
||||||
|
function writePidToFile(pid: number): void {
|
||||||
|
try {
|
||||||
|
const dir = path.dirname(PID_FILE);
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
||||||
|
}
|
||||||
|
fs.writeFileSync(PID_FILE, pid.toString(), { mode: 0o600 });
|
||||||
|
} catch {
|
||||||
|
// Ignore errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove PID file.
|
||||||
|
*/
|
||||||
|
function removePidFile(): void {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(PID_FILE)) {
|
||||||
|
fs.unlinkSync(PID_FILE);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the cursor daemon.
|
||||||
|
*
|
||||||
|
* @param config Cursor configuration
|
||||||
|
* @returns Promise that resolves when daemon is ready
|
||||||
|
*/
|
||||||
|
export async function startDaemon(
|
||||||
|
config: CursorConfig
|
||||||
|
): Promise<{ success: boolean; pid?: number; error?: string }> {
|
||||||
|
// Check if already running
|
||||||
|
if (await isDaemonRunning(config.port)) {
|
||||||
|
return { success: true, pid: getPidFromFile() ?? undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
// For now, create a simple structure that will be filled in later
|
||||||
|
// The actual server implementation will be added in a separate task
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let proc: ChildProcess;
|
||||||
|
|
||||||
|
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');
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
proc = spawn('node', args, {
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
detached: true,
|
||||||
|
shell: process.platform === 'win32',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Unref so parent can exit
|
||||||
|
proc.unref();
|
||||||
|
|
||||||
|
if (proc.pid) {
|
||||||
|
writePidToFile(proc.pid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for daemon to be ready (poll for up to 30 seconds)
|
||||||
|
let attempts = 0;
|
||||||
|
const maxAttempts = 30;
|
||||||
|
const checkInterval = setInterval(async () => {
|
||||||
|
attempts++;
|
||||||
|
|
||||||
|
if (await isDaemonRunning(config.port)) {
|
||||||
|
clearInterval(checkInterval);
|
||||||
|
resolve({ success: true, pid: proc.pid });
|
||||||
|
} else if (attempts >= maxAttempts) {
|
||||||
|
clearInterval(checkInterval);
|
||||||
|
resolve({
|
||||||
|
success: false,
|
||||||
|
error: 'Daemon did not start within 30 seconds',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
proc.on('error', (err) => {
|
||||||
|
clearInterval(checkInterval);
|
||||||
|
resolve({
|
||||||
|
success: false,
|
||||||
|
error: `Failed to start daemon: ${err.message}`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
resolve({
|
||||||
|
success: false,
|
||||||
|
error: `Failed to spawn daemon: ${(err as Error).message}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the cursor daemon.
|
||||||
|
*/
|
||||||
|
export async function stopDaemon(): Promise<{ success: boolean; error?: string }> {
|
||||||
|
const pid = getPidFromFile();
|
||||||
|
|
||||||
|
if (!pid) {
|
||||||
|
// No PID file, try to find by port
|
||||||
|
removePidFile();
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Send SIGTERM to the process
|
||||||
|
process.kill(pid, 'SIGTERM');
|
||||||
|
|
||||||
|
// Wait for process to exit (up to 5 seconds)
|
||||||
|
let attempts = 0;
|
||||||
|
while (attempts < 10) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
|
try {
|
||||||
|
// Check if process still exists (kill(pid, 0) throws if not)
|
||||||
|
process.kill(pid, 0);
|
||||||
|
attempts++;
|
||||||
|
} catch {
|
||||||
|
// Process no longer exists
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removePidFile();
|
||||||
|
return { success: true };
|
||||||
|
} catch (err) {
|
||||||
|
const error = err as NodeJS.ErrnoException;
|
||||||
|
if (error.code === 'ESRCH') {
|
||||||
|
// Process doesn't exist
|
||||||
|
removePidFile();
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `Failed to stop daemon: ${error.message}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
/**
|
||||||
|
* Cursor Model Catalog
|
||||||
|
*
|
||||||
|
* Manages available models from Cursor IDE.
|
||||||
|
* Based on Cursor's supported models as of Feb 2025.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as http from 'http';
|
||||||
|
import type { CursorModel } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default models available through Cursor IDE.
|
||||||
|
* Used as fallback when daemon is not reachable.
|
||||||
|
* Source: Cursor IDE supported models (Feb 2025)
|
||||||
|
*/
|
||||||
|
export const DEFAULT_CURSOR_MODELS: CursorModel[] = [
|
||||||
|
// Anthropic Models
|
||||||
|
{
|
||||||
|
id: 'claude-sonnet-4',
|
||||||
|
name: 'Claude Sonnet 4',
|
||||||
|
provider: 'anthropic',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'claude-sonnet-4.5',
|
||||||
|
name: 'Claude Sonnet 4.5',
|
||||||
|
provider: 'anthropic',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'claude-opus-4',
|
||||||
|
name: 'Claude Opus 4',
|
||||||
|
provider: 'anthropic',
|
||||||
|
},
|
||||||
|
|
||||||
|
// OpenAI Models
|
||||||
|
{
|
||||||
|
id: 'gpt-4.1',
|
||||||
|
name: 'GPT-4.1',
|
||||||
|
provider: 'openai',
|
||||||
|
isDefault: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gpt-5-mini',
|
||||||
|
name: 'GPT-5 Mini',
|
||||||
|
provider: 'openai',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'o3-mini',
|
||||||
|
name: 'O3 Mini',
|
||||||
|
provider: 'openai',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Google Models
|
||||||
|
{
|
||||||
|
id: 'gemini-2.5-pro',
|
||||||
|
name: 'Gemini 2.5 Pro',
|
||||||
|
provider: 'google',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Cursor Custom Models
|
||||||
|
{
|
||||||
|
id: 'cursor-small',
|
||||||
|
name: 'Cursor Small',
|
||||||
|
provider: 'cursor',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch available models from running cursor daemon.
|
||||||
|
*
|
||||||
|
* @param port The port cursor daemon is running on
|
||||||
|
* @returns List of available models
|
||||||
|
*/
|
||||||
|
export async function fetchModelsFromDaemon(port: number): Promise<CursorModel[]> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const req = http.request(
|
||||||
|
{
|
||||||
|
// Use 127.0.0.1 instead of localhost for more reliable local connections
|
||||||
|
hostname: '127.0.0.1',
|
||||||
|
port,
|
||||||
|
path: '/v1/models',
|
||||||
|
method: 'GET',
|
||||||
|
timeout: 5000,
|
||||||
|
},
|
||||||
|
(res) => {
|
||||||
|
let data = '';
|
||||||
|
|
||||||
|
res.on('data', (chunk) => {
|
||||||
|
data += chunk;
|
||||||
|
});
|
||||||
|
|
||||||
|
res.on('end', () => {
|
||||||
|
try {
|
||||||
|
const response = JSON.parse(data) as { data?: Array<{ id: string }> };
|
||||||
|
if (response.data && Array.isArray(response.data)) {
|
||||||
|
const models: CursorModel[] = response.data.map((m) => ({
|
||||||
|
id: m.id,
|
||||||
|
name: formatModelName(m.id),
|
||||||
|
provider: detectProvider(m.id),
|
||||||
|
isDefault: m.id === 'gpt-4.1',
|
||||||
|
}));
|
||||||
|
resolve(models.length > 0 ? models : DEFAULT_CURSOR_MODELS);
|
||||||
|
} else {
|
||||||
|
resolve(DEFAULT_CURSOR_MODELS);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
resolve(DEFAULT_CURSOR_MODELS);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
req.on('error', () => {
|
||||||
|
resolve(DEFAULT_CURSOR_MODELS);
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('timeout', () => {
|
||||||
|
req.destroy();
|
||||||
|
resolve(DEFAULT_CURSOR_MODELS);
|
||||||
|
});
|
||||||
|
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get available models (from daemon or defaults).
|
||||||
|
*/
|
||||||
|
export async function getAvailableModels(port: number): Promise<CursorModel[]> {
|
||||||
|
return fetchModelsFromDaemon(port);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the default model.
|
||||||
|
* Uses gpt-4.1 as it's commonly available.
|
||||||
|
*/
|
||||||
|
export function getDefaultModel(): string {
|
||||||
|
return 'gpt-4.1';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect provider from model ID.
|
||||||
|
*/
|
||||||
|
function detectProvider(modelId: string): string {
|
||||||
|
if (modelId.includes('claude')) return 'anthropic';
|
||||||
|
if (modelId.includes('gpt') || modelId.includes('o3')) return 'openai';
|
||||||
|
if (modelId.includes('gemini')) return 'google';
|
||||||
|
if (modelId.includes('cursor')) return 'cursor';
|
||||||
|
return 'openai';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format model ID to human-readable name.
|
||||||
|
*/
|
||||||
|
function formatModelName(modelId: string): string {
|
||||||
|
// Find model in catalog for metadata
|
||||||
|
const model = DEFAULT_CURSOR_MODELS.find((m) => m.id === modelId);
|
||||||
|
if (model) {
|
||||||
|
return model.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: convert kebab-case to title case
|
||||||
|
return modelId
|
||||||
|
.split('-')
|
||||||
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||||
|
.join(' ');
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* Cursor Module Index
|
||||||
|
*
|
||||||
|
* Central exports for Cursor IDE integration.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Types
|
||||||
|
export * from './types';
|
||||||
|
|
||||||
|
// Auth
|
||||||
|
export {
|
||||||
|
autoDetectTokens,
|
||||||
|
saveCredentials,
|
||||||
|
loadCredentials,
|
||||||
|
checkAuthStatus,
|
||||||
|
} from './cursor-auth';
|
||||||
|
|
||||||
|
// Daemon
|
||||||
|
export { isDaemonRunning, getDaemonStatus, startDaemon, stopDaemon } from './cursor-daemon';
|
||||||
|
|
||||||
|
// Models
|
||||||
|
export {
|
||||||
|
DEFAULT_CURSOR_MODELS,
|
||||||
|
fetchModelsFromDaemon,
|
||||||
|
getAvailableModels,
|
||||||
|
getDefaultModel,
|
||||||
|
} from './cursor-models';
|
||||||
|
|
||||||
|
// Executor
|
||||||
|
export { CursorExecutor } from './cursor-executor';
|
||||||
Reference in New Issue
Block a user