mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-17 04:17:11 +00:00
fix(cursor): address CI gate and review regressions
This commit is contained in:
@@ -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) |
|
||||
|
||||
@@ -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`.
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user