import { access, readdir } from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; import { spawn } from 'node:child_process'; import crossSpawn from 'cross-spawn'; import type { IProviderModels } from '@/shared/interfaces.js'; import type { ProviderChangeActiveModelInput, ProviderCurrentActiveModel, ProviderModelOption, ProviderModelsDefinition, ProviderSessionActiveModelChange, } from '@/shared/types.js'; import { buildDefaultProviderCurrentActiveModel, sanitizeLeafDirectoryName, writeProviderSessionActiveModelChange, } from '@/shared/utils.js'; export const CURSOR_FALLBACK_MODELS: ProviderModelsDefinition = { OPTIONS: [ { value: "auto", label: "auto", description: "Auto", }, { value: "composer-2-fast", label: "composer-2-fast", description: "Composer 2 Fast", }, { value: "composer-2", label: "composer-2", description: "Composer 2", }, { value: "gpt-5.3-codex-low", label: "gpt-5.3-codex-low", description: "Codex 5.3 Low", }, { value: "gpt-5.3-codex-low-fast", label: "gpt-5.3-codex-low-fast", description: "Codex 5.3 Low Fast", }, { value: "gpt-5.3-codex", label: "gpt-5.3-codex", description: "Codex 5.3", }, { value: "gpt-5.3-codex-fast", label: "gpt-5.3-codex-fast", description: "Codex 5.3 Fast", }, { value: "gpt-5.3-codex-high", label: "gpt-5.3-codex-high", description: "Codex 5.3 High", }, { value: "gpt-5.3-codex-high-fast", label: "gpt-5.3-codex-high-fast", description: "Codex 5.3 High Fast", }, { value: "gpt-5.3-codex-xhigh", label: "gpt-5.3-codex-xhigh", description: "Codex 5.3 Extra High", }, { value: "gpt-5.3-codex-xhigh-fast", label: "gpt-5.3-codex-xhigh-fast", description: "Codex 5.3 Extra High Fast", }, { value: "gpt-5.2", label: "gpt-5.2", description: "GPT-5.2", }, { value: "gpt-5.2-codex-low", label: "gpt-5.2-codex-low", description: "Codex 5.2 Low", }, { value: "gpt-5.2-codex-low-fast", label: "gpt-5.2-codex-low-fast", description: "Codex 5.2 Low Fast", }, { value: "gpt-5.2-codex", label: "gpt-5.2-codex", description: "Codex 5.2", }, { value: "gpt-5.2-codex-fast", label: "gpt-5.2-codex-fast", description: "Codex 5.2 Fast", }, { value: "gpt-5.2-codex-high", label: "gpt-5.2-codex-high", description: "Codex 5.2 High", }, { value: "gpt-5.2-codex-high-fast", label: "gpt-5.2-codex-high-fast", description: "Codex 5.2 High Fast", }, { value: "gpt-5.2-codex-xhigh", label: "gpt-5.2-codex-xhigh", description: "Codex 5.2 Extra High", }, { value: "gpt-5.2-codex-xhigh-fast", label: "gpt-5.2-codex-xhigh-fast", description: "Codex 5.2 Extra High Fast", }, { value: "gpt-5.1-codex-max-low", label: "gpt-5.1-codex-max-low", description: "Codex 5.1 Max Low", }, { value: "gpt-5.1-codex-max-low-fast", label: "gpt-5.1-codex-max-low-fast", description: "Codex 5.1 Max Low Fast", }, { value: "gpt-5.1-codex-max-medium", label: "gpt-5.1-codex-max-medium", description: "Codex 5.1 Max", }, { value: "gpt-5.1-codex-max-medium-fast", label: "gpt-5.1-codex-max-medium-fast", description: "Codex 5.1 Max Medium Fast", }, { value: "gpt-5.1-codex-max-high", label: "gpt-5.1-codex-max-high", description: "Codex 5.1 Max High", }, { value: "gpt-5.1-codex-max-high-fast", label: "gpt-5.1-codex-max-high-fast", description: "Codex 5.1 Max High Fast", }, { value: "gpt-5.1-codex-max-xhigh", label: "gpt-5.1-codex-max-xhigh", description: "Codex 5.1 Max Extra High", }, { value: "gpt-5.1-codex-max-xhigh-fast", label: "gpt-5.1-codex-max-xhigh-fast", description: "Codex 5.1 Max Extra High Fast", }, { value: "composer-2.5", label: "composer-2.5", description: "Composer 2.5", }, { value: "gpt-5.5-high", label: "gpt-5.5-high", description: "GPT-5.5 1M High", }, { value: "gpt-5.5-high-fast", label: "gpt-5.5-high-fast", description: "GPT-5.5 High Fast", }, { value: "claude-opus-4-7-thinking-high", label: "claude-opus-4-7-thinking-high", description: "Opus 4.7 1M High Thinking", }, { value: "gpt-5.4-high", label: "gpt-5.4-high", description: "GPT-5.4 1M High", }, { value: "gpt-5.4-high-fast", label: "gpt-5.4-high-fast", description: "GPT-5.4 High Fast", }, { value: "claude-4.6-opus-high-thinking", label: "claude-4.6-opus-high-thinking", description: "Opus 4.6 1M Thinking", }, { value: "claude-4.6-opus-high-thinking-fast", label: "claude-4.6-opus-high-thinking-fast", description: "Opus 4.6 1M Thinking Fast", }, { value: "composer-2.5-fast", label: "composer-2.5-fast", description: "Composer 2.5 Fast", }, { value: "gpt-5.5-none", label: "gpt-5.5-none", description: "GPT-5.5 1M None", }, { value: "gpt-5.5-none-fast", label: "gpt-5.5-none-fast", description: "GPT-5.5 None Fast", }, { value: "gpt-5.5-low", label: "gpt-5.5-low", description: "GPT-5.5 1M Low", }, { value: "gpt-5.5-low-fast", label: "gpt-5.5-low-fast", description: "GPT-5.5 Low Fast", }, { value: "gpt-5.5-medium", label: "gpt-5.5-medium", description: "GPT-5.5 1M", }, { value: "gpt-5.5-medium-fast", label: "gpt-5.5-medium-fast", description: "GPT-5.5 Fast", }, { value: "gpt-5.5-extra-high", label: "gpt-5.5-extra-high", description: "GPT-5.5 1M Extra High", }, { value: "gpt-5.5-extra-high-fast", label: "gpt-5.5-extra-high-fast", description: "GPT-5.5 Extra High Fast", }, { value: "claude-4.6-sonnet-medium", label: "claude-4.6-sonnet-medium", description: "Sonnet 4.6 1M", }, { value: "claude-4.6-sonnet-medium-thinking", label: "claude-4.6-sonnet-medium-thinking", description: "Sonnet 4.6 1M Thinking", }, { value: "claude-opus-4-7-low", label: "claude-opus-4-7-low", description: "Opus 4.7 1M Low", }, { value: "claude-opus-4-7-low-fast", label: "claude-opus-4-7-low-fast", description: "Opus 4.7 1M Low Fast", }, { value: "claude-opus-4-7-medium", label: "claude-opus-4-7-medium", description: "Opus 4.7 1M Medium", }, { value: "claude-opus-4-7-medium-fast", label: "claude-opus-4-7-medium-fast", description: "Opus 4.7 1M Medium Fast", }, { value: "claude-opus-4-7-high", label: "claude-opus-4-7-high", description: "Opus 4.7 1M High", }, { value: "claude-opus-4-7-high-fast", label: "claude-opus-4-7-high-fast", description: "Opus 4.7 1M High Fast", }, { value: "claude-opus-4-7-xhigh", label: "claude-opus-4-7-xhigh", description: "Opus 4.7 1M", }, { value: "claude-opus-4-7-xhigh-fast", label: "claude-opus-4-7-xhigh-fast", description: "Opus 4.7 1M Fast", }, { value: "claude-opus-4-7-max", label: "claude-opus-4-7-max", description: "Opus 4.7 1M Max", }, { value: "claude-opus-4-7-max-fast", label: "claude-opus-4-7-max-fast", description: "Opus 4.7 1M Max Fast", }, { value: "claude-opus-4-7-thinking-low", label: "claude-opus-4-7-thinking-low", description: "Opus 4.7 1M Low Thinking", }, { value: "claude-opus-4-7-thinking-low-fast", label: "claude-opus-4-7-thinking-low-fast", description: "Opus 4.7 1M Low Thinking Fast", }, { value: "claude-opus-4-7-thinking-medium", label: "claude-opus-4-7-thinking-medium", description: "Opus 4.7 1M Medium Thinking", }, { value: "claude-opus-4-7-thinking-medium-fast", label: "claude-opus-4-7-thinking-medium-fast", description: "Opus 4.7 1M Medium Thinking Fast", }, { value: "claude-opus-4-7-thinking-high-fast", label: "claude-opus-4-7-thinking-high-fast", description: "Opus 4.7 1M High Thinking Fast", }, { value: "claude-opus-4-7-thinking-xhigh", label: "claude-opus-4-7-thinking-xhigh", description: "Opus 4.7 1M Thinking", }, { value: "claude-opus-4-7-thinking-xhigh-fast", label: "claude-opus-4-7-thinking-xhigh-fast", description: "Opus 4.7 1M Thinking Fast", }, { value: "claude-opus-4-7-thinking-max", label: "claude-opus-4-7-thinking-max", description: "Opus 4.7 1M Max Thinking", }, { value: "claude-opus-4-7-thinking-max-fast", label: "claude-opus-4-7-thinking-max-fast", description: "Opus 4.7 1M Max Thinking Fast", }, { value: "grok-build-0.1", label: "grok-build-0.1", description: "Grok Build 0.1 1M", }, { value: "gpt-5.4-low", label: "gpt-5.4-low", description: "GPT-5.4 1M Low", }, { value: "gpt-5.4-medium", label: "gpt-5.4-medium", description: "GPT-5.4 1M", }, { value: "gpt-5.4-medium-fast", label: "gpt-5.4-medium-fast", description: "GPT-5.4 Fast", }, { value: "gpt-5.4-xhigh", label: "gpt-5.4-xhigh", description: "GPT-5.4 1M Extra High", }, { value: "gpt-5.4-xhigh-fast", label: "gpt-5.4-xhigh-fast", description: "GPT-5.4 Extra High Fast", }, { value: "claude-4.6-opus-high", label: "claude-4.6-opus-high", description: "Opus 4.6 1M", }, { value: "claude-4.6-opus-max", label: "claude-4.6-opus-max", description: "Opus 4.6 1M Max", }, { value: "claude-4.6-opus-max-thinking", label: "claude-4.6-opus-max-thinking", description: "Opus 4.6 1M Max Thinking", }, { value: "claude-4.6-opus-max-thinking-fast", label: "claude-4.6-opus-max-thinking-fast", description: "Opus 4.6 1M Max Thinking Fast", }, { value: "claude-4.5-opus-high", label: "claude-4.5-opus-high", description: "Opus 4.5", }, { value: "claude-4.5-opus-high-thinking", label: "claude-4.5-opus-high-thinking", description: "Opus 4.5 Thinking", }, { value: "gpt-5.2-low", label: "gpt-5.2-low", description: "GPT-5.2 Low", }, { value: "gpt-5.2-low-fast", label: "gpt-5.2-low-fast", description: "GPT-5.2 Low Fast", }, { value: "gpt-5.2-fast", label: "gpt-5.2-fast", description: "GPT-5.2 Fast", }, { value: "gpt-5.2-high", label: "gpt-5.2-high", description: "GPT-5.2 High", }, { value: "gpt-5.2-high-fast", label: "gpt-5.2-high-fast", description: "GPT-5.2 High Fast", }, { value: "gpt-5.2-xhigh", label: "gpt-5.2-xhigh", description: "GPT-5.2 Extra High", }, { value: "gpt-5.2-xhigh-fast", label: "gpt-5.2-xhigh-fast", description: "GPT-5.2 Extra High Fast", }, { value: "gemini-3.1-pro", label: "gemini-3.1-pro", description: "Gemini 3.1 Pro", }, { value: "gpt-5.4-mini-none", label: "gpt-5.4-mini-none", description: "GPT-5.4 Mini None", }, { value: "gpt-5.4-mini-low", label: "gpt-5.4-mini-low", description: "GPT-5.4 Mini Low", }, { value: "gpt-5.4-mini-medium", label: "gpt-5.4-mini-medium", description: "GPT-5.4 Mini", }, { value: "gpt-5.4-mini-high", label: "gpt-5.4-mini-high", description: "GPT-5.4 Mini High", }, { value: "gpt-5.4-mini-xhigh", label: "gpt-5.4-mini-xhigh", description: "GPT-5.4 Mini Extra High", }, { value: "gpt-5.4-nano-none", label: "gpt-5.4-nano-none", description: "GPT-5.4 Nano None", }, { value: "gpt-5.4-nano-low", label: "gpt-5.4-nano-low", description: "GPT-5.4 Nano Low", }, { value: "gpt-5.4-nano-medium", label: "gpt-5.4-nano-medium", description: "GPT-5.4 Nano", }, { value: "gpt-5.4-nano-high", label: "gpt-5.4-nano-high", description: "GPT-5.4 Nano High", }, { value: "gpt-5.4-nano-xhigh", label: "gpt-5.4-nano-xhigh", description: "GPT-5.4 Nano Extra High", }, { value: "grok-4.3", label: "grok-4.3", description: "Grok 4.3 1M", }, { value: "claude-4.5-sonnet", label: "claude-4.5-sonnet", description: "Sonnet 4.5", }, { value: "claude-4.5-sonnet-thinking", label: "claude-4.5-sonnet-thinking", description: "Sonnet 4.5 Thinking", }, { value: "gpt-5.1-low", label: "gpt-5.1-low", description: "GPT-5.1 Low", }, { value: "gpt-5.1", label: "gpt-5.1", description: "GPT-5.1", }, { value: "gpt-5.1-high", label: "gpt-5.1-high", description: "GPT-5.1 High", }, { value: "gemini-3-flash", label: "gemini-3-flash", description: "Gemini 3 Flash", }, { value: "gemini-3.5-flash", label: "gemini-3.5-flash", description: "Gemini 3.5 Flash", }, { value: "gpt-5.1-codex-mini-low", label: "gpt-5.1-codex-mini-low", description: "Codex 5.1 Mini Low", }, { value: "gpt-5.1-codex-mini", label: "gpt-5.1-codex-mini", description: "Codex 5.1 Mini", }, { value: "gpt-5.1-codex-mini-high", label: "gpt-5.1-codex-mini-high", description: "Codex 5.1 Mini High", }, { value: "claude-4-sonnet", label: "claude-4-sonnet", description: "Sonnet 4", }, { value: "claude-4-sonnet-thinking", label: "claude-4-sonnet-thinking", description: "Sonnet 4 Thinking", }, { value: "gpt-5-mini", label: "gpt-5-mini", description: "GPT-5 Mini", }, { value: "kimi-k2.5", label: "kimi-k2.5", description: "Kimi K2.5", }, ], DEFAULT: "composer-2.5-fast", }; type CursorModelRow = { name: string; description: string; current: boolean; default: boolean; }; const CURSOR_MODELS_TIMEOUT_MS = 10_000; const CURSOR_CHATS_ROOT = path.join(os.homedir(), '.cursor', 'chats'); const spawnFunction = process.platform === 'win32' ? crossSpawn : spawn; const ANSI_PATTERN = new RegExp( // eslint-disable-next-line no-control-regex '[\\u001B\\u009B][[\\]()#;?]*(?:' + '(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]' + '|(?:[\\dA-PR-TZcf-ntqry=><~]))', 'g', ); const stripAnsi = (value: string): string => value.replace(ANSI_PATTERN, ''); const parseModelLine = (line: string): CursorModelRow | null => { const trimmed = line.trim(); if ( !trimmed || trimmed === 'Available models' || trimmed.startsWith('Loading models') || trimmed.startsWith('Tip:') ) { return null; } const match = trimmed.match(/^(.+?)\s+-\s+(.+)$/); if (!match) { return null; } const name = match[1].trim(); let description = match[2].trim(); const current = /\(current\)/i.test(description); const defaultModel = /\(default\)/i.test(description); description = description.replace(/\s*\((current|default)\)/gi, '').replace(/\s{2,}/g, ' ').trim(); return { name, description, current, default: defaultModel, }; }; const parseModelsOutput = (text: string): CursorModelRow[] => { const models: CursorModelRow[] = []; for (const line of stripAnsi(text).split(/\r?\n/)) { const parsed = parseModelLine(line); if (parsed) { models.push(parsed); } } return models; }; const runCursorListModels = (): Promise => new Promise((resolve, reject) => { const cursorProcess = spawnFunction('cursor-agent', ['--list-models'], { env: { ...process.env }, }); let stdout = ''; let stderr = ''; let settled = false; const timer = setTimeout(() => { cursorProcess.kill('SIGTERM'); if (!settled) { settled = true; reject(new Error('cursor-agent --list-models timed out')); } }, CURSOR_MODELS_TIMEOUT_MS); const finish = (error: Error | null, output: string) => { if (settled) { return; } settled = true; clearTimeout(timer); if (error) { reject(error); return; } resolve(output); }; cursorProcess.stdout?.on('data', (chunk: Buffer) => { stdout += chunk.toString(); }); cursorProcess.stderr?.on('data', (chunk: Buffer) => { stderr += chunk.toString(); }); cursorProcess.on('error', (error) => { finish(error instanceof Error ? error : new Error(String(error)), ''); }); cursorProcess.on('close', (code) => { if (code !== 0) { finish(new Error(stderr.trim() || `cursor-agent --list-models exited with code ${code}`), ''); return; } finish(null, stdout); }); }); const buildCursorModelsDefinition = (models: CursorModelRow[]): ProviderModelsDefinition => { const options: ProviderModelOption[] = []; const seenValues = new Set(); for (const model of models) { if (seenValues.has(model.name)) { continue; } seenValues.add(model.name); options.push({ value: model.name, label: model.name, description: model.description || undefined, }); } if (options.length === 0) { return CURSOR_FALLBACK_MODELS; } const defaultValue = models.find((model) => model.default)?.name ?? models.find((model) => model.current)?.name ?? options[0]?.value ?? CURSOR_FALLBACK_MODELS.DEFAULT; return { OPTIONS: options, DEFAULT: defaultValue, }; }; const resolveCursorSessionStorePath = async (sessionId: string): Promise => { const safeSessionId = sanitizeLeafDirectoryName(sessionId, 'cursor session id'); try { const workspaceEntries = await readdir(CURSOR_CHATS_ROOT, { withFileTypes: true }); for (const workspaceEntry of workspaceEntries) { if (!workspaceEntry.isDirectory()) { continue; } const storeDbPath = path.join(CURSOR_CHATS_ROOT, workspaceEntry.name, safeSessionId, 'store.db'); try { await access(storeDbPath); return storeDbPath; } catch { // Keep scanning sibling workspaces until the matching session directory is found. } } } catch { return null; } return null; }; export class CursorProviderModels implements IProviderModels { async getSupportedModels(): Promise { try { const stdout = await runCursorListModels(); const models = parseModelsOutput(stdout); return buildCursorModelsDefinition(models); } catch { return CURSOR_FALLBACK_MODELS; } } async getCurrentActiveModel(sessionId?: string): Promise { if (!sessionId?.trim()) { return buildDefaultProviderCurrentActiveModel(await this.getSupportedModels()); } try { const storeDbPath = await resolveCursorSessionStorePath(sessionId); if (!storeDbPath) { return buildDefaultProviderCurrentActiveModel(await this.getSupportedModels()); } const { default: Database } = await import('better-sqlite3'); const db = new Database(storeDbPath, { readonly: true, fileMustExist: true }); try { const row = db.prepare(`SELECT value FROM meta WHERE key='0' LIMIT 1;`).get() as { value?: Buffer | string; } | undefined; const metadataText = Buffer.isBuffer(row?.value) ? row.value.toString('utf8') : typeof row?.value === 'string' && row.value.trim() ? Buffer.from(row.value.trim(), 'hex').toString('utf8') : ''; if (!metadataText) { return buildDefaultProviderCurrentActiveModel(await this.getSupportedModels()); } const metadata = JSON.parse(metadataText) as { lastUsedModel?: string }; if (typeof metadata.lastUsedModel === 'string' && metadata.lastUsedModel.trim()) { return { model: metadata.lastUsedModel.trim(), }; } } finally { db.close(); } } catch { // Fall through to the provider default when Cursor metadata cannot be read. } return buildDefaultProviderCurrentActiveModel(await this.getSupportedModels()); } async changeActiveModel( input: ProviderChangeActiveModelInput, ): Promise { return writeProviderSessionActiveModelChange('cursor', input); } }