From afb5e746b3c11951147c381b298bc562a078685c Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Thu, 12 Feb 2026 04:14:28 +0700 Subject: [PATCH] fix(cursor): address remaining PR #527 review feedback - Add SIGKILL escalation in stopDaemon after SIGTERM timeout - Document router difference between cursor/copilot help behavior - Fix detectProvider to handle o1/o4 models via regex pattern - Add fetchModelsFromDaemon fallback test for unreachable daemon - Update CLAUDE.md help table with cursor command entry --- CLAUDE.md | 1 + src/ccs.ts | 2 ++ src/cursor/cursor-daemon.ts | 8 ++++++++ src/cursor/cursor-models.ts | 2 +- tests/unit/cursor/cursor-models.test.ts | 17 +++++++++++++++++ 5 files changed, 29 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index a8aaa564..715f18d0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -129,6 +129,7 @@ bun run validate # Step 3: Final check (must pass) | `ccs cliproxy --help` | `src/commands/cliproxy-command.ts` → `showHelp()` | | `ccs config --help` | `src/commands/config-command.ts` → `showHelp()` | | `ccs copilot --help` | `src/commands/copilot-command.ts` → `handleHelp()` | +| `ccs cursor --help` | `src/commands/cursor-command.ts` → `handleHelp()` | | `ccs doctor --help` | `src/commands/doctor-command.ts` → `showHelp()` | | `ccs migrate --help` | `src/commands/migrate-command.ts` → `printMigrateHelp()` | | `ccs env --help` | `src/commands/env-command.ts` → `showHelp()` | diff --git a/src/ccs.ts b/src/ccs.ts index 720a37dd..0161870c 100644 --- a/src/ccs.ts +++ b/src/ccs.ts @@ -534,6 +534,8 @@ async function main(): Promise { // Special case: cursor command (Cursor IDE integration) // Only route to command handler for known subcommands, otherwise treat as profile + // Note: Bare `ccs cursor` shows help (unlike copilot which falls through to profile) + // This is intentional — cursor has no profile-switching mode const CURSOR_SUBCOMMANDS = ['auth', 'status', 'models', 'start', 'stop', 'help', '--help', '-h']; if (firstArg === 'cursor' && (args.length === 1 || CURSOR_SUBCOMMANDS.includes(args[1]))) { // `ccs cursor ` - route to cursor command handler diff --git a/src/cursor/cursor-daemon.ts b/src/cursor/cursor-daemon.ts index 0b240747..0efe8f9a 100644 --- a/src/cursor/cursor-daemon.ts +++ b/src/cursor/cursor-daemon.ts @@ -265,6 +265,14 @@ export async function stopDaemon(): Promise<{ success: boolean; error?: string } } } + // Escalate to SIGKILL if process still alive after SIGTERM attempts + try { + process.kill(pid, 0); // Check if still alive + process.kill(pid, 'SIGKILL'); // Escalate to force kill + } catch { + // Already dead — good + } + removePidFile(); return { success: true }; } catch (err) { diff --git a/src/cursor/cursor-models.ts b/src/cursor/cursor-models.ts index 2a35baa7..f11c9ed1 100644 --- a/src/cursor/cursor-models.ts +++ b/src/cursor/cursor-models.ts @@ -148,7 +148,7 @@ export function getDefaultModel(): string { */ export function detectProvider(modelId: string): string { if (modelId.includes('claude')) return 'anthropic'; - if (modelId.includes('gpt') || modelId.includes('o3')) return 'openai'; + if (modelId.includes('gpt') || /^o\d/.test(modelId)) return 'openai'; if (modelId.includes('gemini')) return 'google'; if (modelId.includes('cursor')) return 'cursor'; return 'openai'; diff --git a/tests/unit/cursor/cursor-models.test.ts b/tests/unit/cursor/cursor-models.test.ts index 9cdd183b..d8872dfb 100644 --- a/tests/unit/cursor/cursor-models.test.ts +++ b/tests/unit/cursor/cursor-models.test.ts @@ -10,6 +10,7 @@ import { getDefaultModel, detectProvider, formatModelName, + fetchModelsFromDaemon, } from '../../../src/cursor/cursor-models'; describe('DEFAULT_CURSOR_MODELS', () => { @@ -57,6 +58,12 @@ describe('detectProvider', () => { expect(detectProvider('o3-mini')).toBe('openai'); }); + it('detects o1 and o4 models as openai', () => { + expect(detectProvider('o1')).toBe('openai'); + expect(detectProvider('o1-preview')).toBe('openai'); + expect(detectProvider('o4-mini')).toBe('openai'); + }); + it('detects google models', () => { expect(detectProvider('gemini-2.5-pro')).toBe('google'); }); @@ -80,3 +87,13 @@ describe('formatModelName', () => { expect(formatModelName('my-custom-model')).toBe('My Custom Model'); }); }); + +describe('fetchModelsFromDaemon', () => { + it('falls back to DEFAULT_CURSOR_MODELS when daemon is unreachable', async () => { + // Use a port that nothing is listening on + const unreachablePort = 9999; + const models = await fetchModelsFromDaemon(unreachablePort); + + expect(models).toEqual(DEFAULT_CURSOR_MODELS); + }); +});