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)
```
### 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
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) |
+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 shutdown = () => {
server.close(() => process.exit(0));
server.close();
};
process.on('SIGTERM', shutdown);
+32 -9
View File
@@ -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<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}` };
}
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.',
+42 -25
View File
@@ -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<CursorConfigDraft>(() => buildConfigDraft());
const [configDirty, setConfigDirty] = useState(false);
const [rawConfigText, setRawConfigText] = useState<string>('{}');
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 ? <Badge>Enabled</Badge> : <Badge variant="secondary">Disabled</Badge>),
@@ -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 }));
}}
/>
</div>
@@ -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 }));
}}
/>
</div>
@@ -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 }));
}}
/>
</div>