From 0e55db88a5b92ecde8528ee8bf396ce411c93296 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Sat, 14 Feb 2026 06:41:57 +0700 Subject: [PATCH] fix(cursor): address CI gate and review regressions --- README.md | 23 +++++++ docs/cursor-integration.md | 107 ++++++++++++++++++++++++++++++ src/cursor/cursor-daemon-entry.ts | 2 +- src/cursor/cursor-daemon.ts | 41 +++++++++--- ui/src/pages/cursor.tsx | 67 ++++++++++++------- 5 files changed, 205 insertions(+), 35 deletions(-) create mode 100644 docs/cursor-integration.md diff --git a/README.md b/README.md index 22f06a44..b3125fb0 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,28 @@ ccs ollama # Local Ollama (no API key needed) ccs glm # GLM (API key) ``` +### Cursor IDE Quick Start + +```bash +ccs cursor enable +ccs cursor auth +ccs cursor start +ccs cursor status +``` + +If auto-detect is unavailable: + +```bash +ccs cursor auth --manual --token --machine-id +``` + +Defaults: +- Port: `20129` +- Ghost mode: enabled +- Dashboard page: `ccs config` -> `Cursor IDE` + +Detailed guide: [`docs/cursor-integration.md`](./docs/cursor-integration.md) + ### Parallel Workflows Run multiple terminals with different providers: @@ -327,6 +349,7 @@ See [Remote Proxy documentation](https://docs.ccs.kaitran.ca/features/remote-pro | Multi-Account Claude | [docs.ccs.kaitran.ca/providers/claude-accounts](https://docs.ccs.kaitran.ca/providers/claude-accounts) | | API Profiles | [docs.ccs.kaitran.ca/providers/api-profiles](https://docs.ccs.kaitran.ca/providers/api-profiles) | | Remote Proxy | [docs.ccs.kaitran.ca/features/remote-proxy](https://docs.ccs.kaitran.ca/features/remote-proxy) | +| Cursor IDE (local guide) | [./docs/cursor-integration.md](./docs/cursor-integration.md) | | CLI Reference | [docs.ccs.kaitran.ca/reference/cli-commands](https://docs.ccs.kaitran.ca/reference/cli-commands) | | Architecture | [docs.ccs.kaitran.ca/reference/architecture](https://docs.ccs.kaitran.ca/reference/architecture) | | Troubleshooting | [docs.ccs.kaitran.ca/reference/troubleshooting](https://docs.ccs.kaitran.ca/reference/troubleshooting) | diff --git a/docs/cursor-integration.md b/docs/cursor-integration.md new file mode 100644 index 00000000..6997dfd3 --- /dev/null +++ b/docs/cursor-integration.md @@ -0,0 +1,107 @@ +# Cursor IDE Integration + +This guide covers the local Cursor integration in CCS, including CLI setup, daemon lifecycle, and dashboard controls. + +## What It Provides + +- OpenAI-compatible local endpoint powered by Cursor credentials. +- Cursor model list and chat completions via local daemon. +- Dedicated dashboard page: `ccs config` -> `Cursor IDE`. + +## Prerequisites + +- Cursor IDE installed and logged in. +- CCS installed and configured (`ccs config` works). +- For auto-detect auth on macOS/Linux: `sqlite3` available in PATH. + +## CLI Workflow + +### 1) Enable integration + +```bash +ccs cursor enable +``` + +### 2) Import credentials + +Auto-detect from Cursor local SQLite state: + +```bash +ccs cursor auth +``` + +Manual fallback: + +```bash +ccs cursor auth --manual --token --machine-id +``` + +### 3) Start daemon + +```bash +ccs cursor start +``` + +### 4) Verify status + +```bash +ccs cursor status +``` + +### 5) Stop daemon + +```bash +ccs cursor stop +``` + +## Runtime Defaults + +- Default port: `20129` +- `ghost_mode`: enabled +- `auto_start`: disabled + +These values are managed in unified config and can be updated from CLI or dashboard. + +## Dashboard Usage + +Open dashboard: + +```bash +ccs config +``` + +Then navigate to `Cursor IDE` in the sidebar. + +Available controls: + +- Integration toggle (`enabled`) +- Auth actions (auto-detect, manual import) +- Daemon actions (start/stop) +- Runtime config (port, auto-start, ghost mode) +- Models list +- Raw editor for `~/.ccs/cursor.settings.json` + +## Raw Settings and Unified Config Sync + +Raw settings are stored in: + +`~/.ccs/cursor.settings.json` + +When raw settings include a local `ANTHROPIC_BASE_URL` port override, CCS synchronizes that port back into unified config so CLI and dashboard remain consistent. + +## Troubleshooting + +### `Not authenticated` or `expired` in `ccs cursor status` + +- Re-run `ccs cursor auth` (or manual auth command). + +### Auto-detect fails + +- Ensure Cursor is logged in. +- Confirm `sqlite3` is installed (macOS/Linux). +- Use manual auth import if needed. + +### Daemon fails to start + +- Check if port `20129` is in use. +- Change port in dashboard config tab, then retry `ccs cursor start`. diff --git a/src/cursor/cursor-daemon-entry.ts b/src/cursor/cursor-daemon-entry.ts index 4d7fd284..2fcf0afd 100644 --- a/src/cursor/cursor-daemon-entry.ts +++ b/src/cursor/cursor-daemon-entry.ts @@ -275,7 +275,7 @@ if (require.main === module) { const server = startCursorDaemonServer(options); const shutdown = () => { - server.close(() => process.exit(0)); + server.close(); }; process.on('SIGTERM', shutdown); diff --git a/src/cursor/cursor-daemon.ts b/src/cursor/cursor-daemon.ts index d7a7fa58..5985b81d 100644 --- a/src/cursor/cursor-daemon.ts +++ b/src/cursor/cursor-daemon.ts @@ -28,13 +28,38 @@ function getPidFilePath(): string { } /** - * Resolve daemon entrypoint script path for the current runtime. - * - Dist runtime: cursor-daemon-entry.js - * - Bun source runtime (tests/dev): cursor-daemon-entry.ts + * Resolve daemon entrypoint candidates for current runtime. + * - Dist runtime always uses JS artifact. + * - Bun source runtime prefers TS, then falls back to JS. */ -function resolveDaemonEntrypoint(): string { +function getDaemonEntrypointCandidates(): string[] { + const jsEntry = path.join(__dirname, 'cursor-daemon-entry.js'); + const tsEntry = path.join(__dirname, 'cursor-daemon-entry.ts'); const isBunRuntime = process.execPath.toLowerCase().includes('bun'); - return path.join(__dirname, isBunRuntime ? 'cursor-daemon-entry.ts' : 'cursor-daemon-entry.js'); + const runningFromDist = __filename.endsWith('.js'); + + if (runningFromDist) { + return [jsEntry]; + } + + if (isBunRuntime) { + return [tsEntry, jsEntry]; + } + + return [jsEntry]; +} + +async function resolveDaemonEntrypoint(): Promise { + for (const candidate of getDaemonEntrypointCandidates()) { + try { + await fs.promises.access(candidate, fs.constants.R_OK); + return candidate; + } catch { + // Try next candidate + } + } + + return null; } /** @@ -150,10 +175,8 @@ export async function startDaemon( return { success: false, error: `Invalid port: ${config.port}` }; } - const daemonEntry = resolveDaemonEntrypoint(); - try { - await fs.promises.access(daemonEntry, fs.constants.R_OK); - } catch { + const daemonEntry = await resolveDaemonEntrypoint(); + if (!daemonEntry) { return { success: false, error: 'Cursor daemon entrypoint not found. Run `bun run build` and retry.', diff --git a/ui/src/pages/cursor.tsx b/ui/src/pages/cursor.tsx index 880e2685..f7dc3e0b 100644 --- a/ui/src/pages/cursor.tsx +++ b/ui/src/pages/cursor.tsx @@ -40,6 +40,24 @@ import { DialogTitle, } from '@/components/ui/dialog'; +interface CursorConfigDraft { + port: string; + auto_start: boolean; + ghost_mode: boolean; +} + +function buildConfigDraft(config?: { + port?: number; + auto_start?: boolean; + ghost_mode?: boolean; +}): CursorConfigDraft { + return { + port: String(config?.port ?? 20129), + auto_start: config?.auto_start ?? false, + ghost_mode: config?.ghost_mode ?? true, + }; +} + function StatusItem({ icon: Icon, label, @@ -100,15 +118,7 @@ export function CursorPage() { isStoppingDaemon, } = useCursor(); - const [configDraft, setConfigDraft] = useState<{ - port: string; - auto_start: boolean; - ghost_mode: boolean; - }>({ - port: '20129', - auto_start: false, - ghost_mode: true, - }); + const [configDraft, setConfigDraft] = useState(() => buildConfigDraft()); const [configDirty, setConfigDirty] = useState(false); const [rawConfigText, setRawConfigText] = useState('{}'); const [rawConfigDirty, setRawConfigDirty] = useState(false); @@ -116,15 +126,23 @@ export function CursorPage() { const [manualToken, setManualToken] = useState(''); const [manualMachineId, setManualMachineId] = useState(''); - const effectivePort = configDirty ? configDraft.port : String(config?.port ?? 20129); - const effectiveAutoStart = configDirty ? configDraft.auto_start : Boolean(config?.auto_start); - const effectiveGhostMode = configDirty - ? configDraft.ghost_mode - : Boolean(config?.ghost_mode ?? true); + const pristineConfigDraft = buildConfigDraft(config); + + const effectivePort = configDirty ? configDraft.port : pristineConfigDraft.port; + const effectiveAutoStart = configDirty ? configDraft.auto_start : pristineConfigDraft.auto_start; + const effectiveGhostMode = configDirty ? configDraft.ghost_mode : pristineConfigDraft.ghost_mode; const effectiveRawConfigText = rawConfigDirty ? rawConfigText : JSON.stringify(rawSettings?.settings ?? {}, null, 2); + const updateConfigDraft = (updater: (draft: CursorConfigDraft) => CursorConfigDraft) => { + setConfigDraft((previousDraft) => { + const baseDraft = configDirty ? previousDraft : pristineConfigDraft; + return updater(baseDraft); + }); + setConfigDirty(true); + }; + const canStart = Boolean(status?.enabled && status?.authenticated && !status?.token_expired); const integrationBadge = useMemo( () => (status?.enabled ? Enabled : Disabled), @@ -145,11 +163,13 @@ export function CursorPage() { ghost_mode: effectiveGhostMode, }); setConfigDirty(false); - setConfigDraft({ - port: String(parsedPort), - auto_start: effectiveAutoStart, - ghost_mode: effectiveGhostMode, - }); + setConfigDraft( + buildConfigDraft({ + port: parsedPort, + auto_start: effectiveAutoStart, + ghost_mode: effectiveGhostMode, + }) + ); toast.success('Cursor config saved'); } catch (error) { toast.error((error as Error).message || 'Failed to save config'); @@ -426,8 +446,7 @@ export function CursorPage() { max={65535} value={effectivePort} onChange={(e) => { - setConfigDirty(true); - setConfigDraft((prev) => ({ ...prev, port: e.target.value })); + updateConfigDraft((draft) => ({ ...draft, port: e.target.value })); }} /> @@ -443,8 +462,7 @@ export function CursorPage() { id="cursor-auto-start" checked={effectiveAutoStart} onCheckedChange={(value) => { - setConfigDirty(true); - setConfigDraft((prev) => ({ ...prev, auto_start: value })); + updateConfigDraft((draft) => ({ ...draft, auto_start: value })); }} /> @@ -460,8 +478,7 @@ export function CursorPage() { id="cursor-ghost-mode" checked={effectiveGhostMode} onCheckedChange={(value) => { - setConfigDirty(true); - setConfigDraft((prev) => ({ ...prev, ghost_mode: value })); + updateConfigDraft((draft) => ({ ...draft, ghost_mode: value })); }} />