fix(cursor): address CI gate and review regressions

This commit is contained in:
Tam Nhu Tran
2026-02-14 06:41:57 +07:00
parent be9d7cf73e
commit 0e55db88a5
5 changed files with 205 additions and 35 deletions
+23
View File
@@ -141,6 +141,28 @@ ccs ollama # Local Ollama (no API key needed)
ccs glm # GLM (API key) 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 <token> --machine-id <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 ### Parallel Workflows
Run multiple terminals with different providers: 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) | | 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) | | 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) | | 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) | | 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) | | 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) | | Troubleshooting | [docs.ccs.kaitran.ca/reference/troubleshooting](https://docs.ccs.kaitran.ca/reference/troubleshooting) |
+107
View File
@@ -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 <token> --machine-id <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`.
+1 -1
View File
@@ -275,7 +275,7 @@ if (require.main === module) {
const server = startCursorDaemonServer(options); const server = startCursorDaemonServer(options);
const shutdown = () => { const shutdown = () => {
server.close(() => process.exit(0)); server.close();
}; };
process.on('SIGTERM', shutdown); process.on('SIGTERM', shutdown);
+32 -9
View File
@@ -28,13 +28,38 @@ function getPidFilePath(): string {
} }
/** /**
* Resolve daemon entrypoint script path for the current runtime. * Resolve daemon entrypoint candidates for current runtime.
* - Dist runtime: cursor-daemon-entry.js * - Dist runtime always uses JS artifact.
* - Bun source runtime (tests/dev): cursor-daemon-entry.ts * - 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'); 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<string | null> {
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}` }; return { success: false, error: `Invalid port: ${config.port}` };
} }
const daemonEntry = resolveDaemonEntrypoint(); const daemonEntry = await resolveDaemonEntrypoint();
try { if (!daemonEntry) {
await fs.promises.access(daemonEntry, fs.constants.R_OK);
} catch {
return { return {
success: false, success: false,
error: 'Cursor daemon entrypoint not found. Run `bun run build` and retry.', error: 'Cursor daemon entrypoint not found. Run `bun run build` and retry.',
+42 -25
View File
@@ -40,6 +40,24 @@ import {
DialogTitle, DialogTitle,
} from '@/components/ui/dialog'; } 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({ function StatusItem({
icon: Icon, icon: Icon,
label, label,
@@ -100,15 +118,7 @@ export function CursorPage() {
isStoppingDaemon, isStoppingDaemon,
} = useCursor(); } = useCursor();
const [configDraft, setConfigDraft] = useState<{ const [configDraft, setConfigDraft] = useState<CursorConfigDraft>(() => buildConfigDraft());
port: string;
auto_start: boolean;
ghost_mode: boolean;
}>({
port: '20129',
auto_start: false,
ghost_mode: true,
});
const [configDirty, setConfigDirty] = useState(false); const [configDirty, setConfigDirty] = useState(false);
const [rawConfigText, setRawConfigText] = useState<string>('{}'); const [rawConfigText, setRawConfigText] = useState<string>('{}');
const [rawConfigDirty, setRawConfigDirty] = useState(false); const [rawConfigDirty, setRawConfigDirty] = useState(false);
@@ -116,15 +126,23 @@ export function CursorPage() {
const [manualToken, setManualToken] = useState(''); const [manualToken, setManualToken] = useState('');
const [manualMachineId, setManualMachineId] = useState(''); const [manualMachineId, setManualMachineId] = useState('');
const effectivePort = configDirty ? configDraft.port : String(config?.port ?? 20129); const pristineConfigDraft = buildConfigDraft(config);
const effectiveAutoStart = configDirty ? configDraft.auto_start : Boolean(config?.auto_start);
const effectiveGhostMode = configDirty const effectivePort = configDirty ? configDraft.port : pristineConfigDraft.port;
? configDraft.ghost_mode const effectiveAutoStart = configDirty ? configDraft.auto_start : pristineConfigDraft.auto_start;
: Boolean(config?.ghost_mode ?? true); const effectiveGhostMode = configDirty ? configDraft.ghost_mode : pristineConfigDraft.ghost_mode;
const effectiveRawConfigText = rawConfigDirty const effectiveRawConfigText = rawConfigDirty
? rawConfigText ? rawConfigText
: JSON.stringify(rawSettings?.settings ?? {}, null, 2); : 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 canStart = Boolean(status?.enabled && status?.authenticated && !status?.token_expired);
const integrationBadge = useMemo( const integrationBadge = useMemo(
() => (status?.enabled ? <Badge>Enabled</Badge> : <Badge variant="secondary">Disabled</Badge>), () => (status?.enabled ? <Badge>Enabled</Badge> : <Badge variant="secondary">Disabled</Badge>),
@@ -145,11 +163,13 @@ export function CursorPage() {
ghost_mode: effectiveGhostMode, ghost_mode: effectiveGhostMode,
}); });
setConfigDirty(false); setConfigDirty(false);
setConfigDraft({ setConfigDraft(
port: String(parsedPort), buildConfigDraft({
auto_start: effectiveAutoStart, port: parsedPort,
ghost_mode: effectiveGhostMode, auto_start: effectiveAutoStart,
}); ghost_mode: effectiveGhostMode,
})
);
toast.success('Cursor config saved'); toast.success('Cursor config saved');
} catch (error) { } catch (error) {
toast.error((error as Error).message || 'Failed to save config'); toast.error((error as Error).message || 'Failed to save config');
@@ -426,8 +446,7 @@ export function CursorPage() {
max={65535} max={65535}
value={effectivePort} value={effectivePort}
onChange={(e) => { onChange={(e) => {
setConfigDirty(true); updateConfigDraft((draft) => ({ ...draft, port: e.target.value }));
setConfigDraft((prev) => ({ ...prev, port: e.target.value }));
}} }}
/> />
</div> </div>
@@ -443,8 +462,7 @@ export function CursorPage() {
id="cursor-auto-start" id="cursor-auto-start"
checked={effectiveAutoStart} checked={effectiveAutoStart}
onCheckedChange={(value) => { onCheckedChange={(value) => {
setConfigDirty(true); updateConfigDraft((draft) => ({ ...draft, auto_start: value }));
setConfigDraft((prev) => ({ ...prev, auto_start: value }));
}} }}
/> />
</div> </div>
@@ -460,8 +478,7 @@ export function CursorPage() {
id="cursor-ghost-mode" id="cursor-ghost-mode"
checked={effectiveGhostMode} checked={effectiveGhostMode}
onCheckedChange={(value) => { onCheckedChange={(value) => {
setConfigDirty(true); updateConfigDraft((draft) => ({ ...draft, ghost_mode: value }));
setConfigDraft((prev) => ({ ...prev, ghost_mode: value }));
}} }}
/> />
</div> </div>