@cmdctrl/cursor-ide 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter/cdp-client.d.ts +66 -0
- package/dist/adapter/cdp-client.d.ts.map +1 -0
- package/dist/adapter/cdp-client.js +304 -0
- package/dist/adapter/cdp-client.js.map +1 -0
- package/dist/adapter/cursor-db.d.ts +114 -0
- package/dist/adapter/cursor-db.d.ts.map +1 -0
- package/dist/adapter/cursor-db.js +438 -0
- package/dist/adapter/cursor-db.js.map +1 -0
- package/dist/client/messages.d.ts +98 -0
- package/dist/client/messages.d.ts.map +1 -0
- package/dist/client/messages.js +6 -0
- package/dist/client/messages.js.map +1 -0
- package/dist/client/websocket.d.ts +103 -0
- package/dist/client/websocket.d.ts.map +1 -0
- package/dist/client/websocket.js +428 -0
- package/dist/client/websocket.js.map +1 -0
- package/dist/commands/register.d.ts +10 -0
- package/dist/commands/register.d.ts.map +1 -0
- package/dist/commands/register.js +175 -0
- package/dist/commands/register.js.map +1 -0
- package/dist/commands/start.d.ts +9 -0
- package/dist/commands/start.d.ts.map +1 -0
- package/dist/commands/start.js +86 -0
- package/dist/commands/start.js.map +1 -0
- package/dist/commands/status.d.ts +5 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +75 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/stop.d.ts +5 -0
- package/dist/commands/stop.d.ts.map +1 -0
- package/dist/commands/stop.js +59 -0
- package/dist/commands/stop.js.map +1 -0
- package/dist/config/config.d.ts +68 -0
- package/dist/config/config.d.ts.map +1 -0
- package/dist/config/config.js +189 -0
- package/dist/config/config.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -0
- package/dist/session-discovery.d.ts +22 -0
- package/dist/session-discovery.d.ts.map +1 -0
- package/dist/session-discovery.js +90 -0
- package/dist/session-discovery.js.map +1 -0
- package/dist/session-watcher.d.ts +62 -0
- package/dist/session-watcher.d.ts.map +1 -0
- package/dist/session-watcher.js +210 -0
- package/dist/session-watcher.js.map +1 -0
- package/package.json +40 -0
- package/src/adapter/cdp-client.ts +296 -0
- package/src/adapter/cursor-db.ts +486 -0
- package/src/client/messages.ts +138 -0
- package/src/client/websocket.ts +486 -0
- package/src/commands/register.ts +201 -0
- package/src/commands/start.ts +106 -0
- package/src/commands/status.ts +83 -0
- package/src/commands/stop.ts +58 -0
- package/src/config/config.ts +167 -0
- package/src/index.ts +39 -0
- package/src/session-discovery.ts +115 -0
- package/src/session-watcher.ts +253 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readConfig,
|
|
3
|
+
readCredentials,
|
|
4
|
+
isRegistered,
|
|
5
|
+
writePidFile,
|
|
6
|
+
isDaemonRunning,
|
|
7
|
+
cursorDbExists,
|
|
8
|
+
} from '../config/config';
|
|
9
|
+
import { DaemonClient } from '../client/websocket';
|
|
10
|
+
import { getCDPClient } from '../adapter/cdp-client';
|
|
11
|
+
|
|
12
|
+
interface StartOptions {
|
|
13
|
+
foreground?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Start command - launch the daemon and connect to server
|
|
18
|
+
*/
|
|
19
|
+
export async function start(options: StartOptions): Promise<void> {
|
|
20
|
+
// Set up global error handlers to catch silent crashes
|
|
21
|
+
process.on('uncaughtException', (err) => {
|
|
22
|
+
console.error('[FATAL] Uncaught exception:', err);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
27
|
+
console.error('[FATAL] Unhandled promise rejection:', reason);
|
|
28
|
+
console.error('Promise:', promise);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Check registration
|
|
33
|
+
if (!isRegistered()) {
|
|
34
|
+
console.error('Device not registered. Run "cmdctrl-cursor-ide register" first.');
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Check if already running
|
|
39
|
+
if (isDaemonRunning()) {
|
|
40
|
+
console.error('Daemon is already running. Run "cmdctrl-cursor-ide stop" first.');
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const config = readConfig()!;
|
|
45
|
+
const credentials = readCredentials()!;
|
|
46
|
+
|
|
47
|
+
console.log(`Starting CmdCtrl Cursor IDE daemon...`);
|
|
48
|
+
console.log(`Server: ${config.serverUrl}`);
|
|
49
|
+
console.log(`Device: ${config.deviceName} (${config.deviceId})`);
|
|
50
|
+
|
|
51
|
+
// Check if Cursor database exists
|
|
52
|
+
if (!cursorDbExists()) {
|
|
53
|
+
console.warn('\nWarning: Cursor database not found.');
|
|
54
|
+
console.warn('Make sure Cursor has been run at least once.');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Check CDP availability
|
|
58
|
+
const cdp = getCDPClient();
|
|
59
|
+
const cdpAvailable = await cdp.isAvailable();
|
|
60
|
+
if (!cdpAvailable) {
|
|
61
|
+
console.warn('\nWarning: Cursor CDP not available.');
|
|
62
|
+
console.warn('To enable message injection, start Cursor with:');
|
|
63
|
+
console.warn(' /Applications/Cursor.app/Contents/MacOS/Cursor --remote-debugging-port=9222');
|
|
64
|
+
console.warn('\nContinuing with observation-only mode...');
|
|
65
|
+
} else {
|
|
66
|
+
console.log('\nCDP: Connected to Cursor');
|
|
67
|
+
const title = await cdp.getWindowTitle();
|
|
68
|
+
if (title) {
|
|
69
|
+
console.log(` Active project: ${title}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Write PID file
|
|
74
|
+
writePidFile(process.pid);
|
|
75
|
+
|
|
76
|
+
// Create and start client
|
|
77
|
+
const client = new DaemonClient(config, credentials);
|
|
78
|
+
|
|
79
|
+
// Handle shutdown signals
|
|
80
|
+
const shutdown = async () => {
|
|
81
|
+
console.log('\nShutting down...');
|
|
82
|
+
await client.disconnect();
|
|
83
|
+
process.exit(0);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
process.on('SIGINT', shutdown);
|
|
87
|
+
process.on('SIGTERM', shutdown);
|
|
88
|
+
|
|
89
|
+
// Connect and run
|
|
90
|
+
try {
|
|
91
|
+
await client.connect();
|
|
92
|
+
console.log('Connected to CmdCtrl server.');
|
|
93
|
+
|
|
94
|
+
if (options.foreground) {
|
|
95
|
+
console.log('Running in foreground. Press Ctrl+C to stop.\n');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Keep process alive
|
|
99
|
+
await new Promise(() => {
|
|
100
|
+
// Never resolves - daemon runs until killed
|
|
101
|
+
});
|
|
102
|
+
} catch (err) {
|
|
103
|
+
console.error('Failed to start daemon:', err);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readConfig,
|
|
3
|
+
readCredentials,
|
|
4
|
+
isRegistered,
|
|
5
|
+
isDaemonRunning,
|
|
6
|
+
readPidFile,
|
|
7
|
+
cursorDbExists,
|
|
8
|
+
} from '../config/config';
|
|
9
|
+
import { getCDPClient } from '../adapter/cdp-client';
|
|
10
|
+
import { getCursorDB } from '../adapter/cursor-db';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Status command - check daemon and CDP connection status
|
|
14
|
+
*/
|
|
15
|
+
export async function status(): Promise<void> {
|
|
16
|
+
console.log('CmdCtrl Cursor IDE Daemon Status\n');
|
|
17
|
+
|
|
18
|
+
// Registration status
|
|
19
|
+
if (!isRegistered()) {
|
|
20
|
+
console.log('Registration: Not registered');
|
|
21
|
+
console.log('\nRun "cmdctrl-cursor-ide register" to register this device.');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const config = readConfig()!;
|
|
26
|
+
const credentials = readCredentials()!;
|
|
27
|
+
|
|
28
|
+
console.log('Registration: Registered');
|
|
29
|
+
console.log(` Server: ${config.serverUrl}`);
|
|
30
|
+
console.log(` Device: ${config.deviceName}`);
|
|
31
|
+
console.log(` Device ID: ${config.deviceId}`);
|
|
32
|
+
|
|
33
|
+
// Token status
|
|
34
|
+
const tokenExpired = credentials.expiresAt < Date.now();
|
|
35
|
+
console.log(`\nToken: ${tokenExpired ? 'Expired' : 'Valid'}`);
|
|
36
|
+
if (!tokenExpired) {
|
|
37
|
+
const expiresIn = Math.round((credentials.expiresAt - Date.now()) / 1000 / 60);
|
|
38
|
+
console.log(` Expires in: ${expiresIn} minutes`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Daemon status
|
|
42
|
+
const running = isDaemonRunning();
|
|
43
|
+
console.log(`\nDaemon: ${running ? 'Running' : 'Stopped'}`);
|
|
44
|
+
if (running) {
|
|
45
|
+
const pid = readPidFile();
|
|
46
|
+
console.log(` PID: ${pid}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Cursor database status
|
|
50
|
+
const dbExists = cursorDbExists();
|
|
51
|
+
console.log(`\nCursor Database: ${dbExists ? 'Found' : 'Not found'}`);
|
|
52
|
+
if (dbExists) {
|
|
53
|
+
try {
|
|
54
|
+
const db = getCursorDB();
|
|
55
|
+
const composers = db.getComposers();
|
|
56
|
+
console.log(` Sessions: ${composers.length}`);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
console.log(` Error reading database: ${(err as Error).message}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// CDP status
|
|
63
|
+
console.log('\nCDP (Chrome DevTools Protocol):');
|
|
64
|
+
try {
|
|
65
|
+
const cdp = getCDPClient();
|
|
66
|
+
const available = await cdp.isAvailable();
|
|
67
|
+
if (available) {
|
|
68
|
+
console.log(' Status: Available');
|
|
69
|
+
const title = await cdp.getWindowTitle();
|
|
70
|
+
if (title) {
|
|
71
|
+
console.log(` Active project: ${title}`);
|
|
72
|
+
}
|
|
73
|
+
const composerOpen = await cdp.isComposerOpen();
|
|
74
|
+
console.log(` Composer panel: ${composerOpen ? 'Open' : 'Closed'}`);
|
|
75
|
+
} else {
|
|
76
|
+
console.log(' Status: Not available');
|
|
77
|
+
console.log(' To enable, start Cursor with:');
|
|
78
|
+
console.log(' /Applications/Cursor.app/Contents/MacOS/Cursor --remote-debugging-port=9222');
|
|
79
|
+
}
|
|
80
|
+
} catch (err) {
|
|
81
|
+
console.log(` Status: Error - ${(err as Error).message}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { isDaemonRunning, readPidFile, deletePidFile } from '../config/config';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Stop command - stop the running daemon
|
|
5
|
+
*/
|
|
6
|
+
export async function stop(): Promise<void> {
|
|
7
|
+
if (!isDaemonRunning()) {
|
|
8
|
+
console.log('Daemon is not running.');
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const pid = readPidFile();
|
|
13
|
+
if (pid === null) {
|
|
14
|
+
console.log('No PID file found.');
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
console.log(`Stopping daemon (PID ${pid})...`);
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
// Send SIGTERM for graceful shutdown
|
|
22
|
+
process.kill(pid, 'SIGTERM');
|
|
23
|
+
|
|
24
|
+
// Wait for process to exit (up to 5 seconds)
|
|
25
|
+
let attempts = 0;
|
|
26
|
+
while (attempts < 50) {
|
|
27
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
28
|
+
try {
|
|
29
|
+
process.kill(pid, 0); // Check if still running
|
|
30
|
+
attempts++;
|
|
31
|
+
} catch {
|
|
32
|
+
// Process has exited
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// If still running after 5 seconds, force kill
|
|
38
|
+
try {
|
|
39
|
+
process.kill(pid, 0);
|
|
40
|
+
console.log('Daemon did not stop gracefully, sending SIGKILL...');
|
|
41
|
+
process.kill(pid, 'SIGKILL');
|
|
42
|
+
} catch {
|
|
43
|
+
// Already dead, good
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
deletePidFile();
|
|
47
|
+
console.log('Daemon stopped.');
|
|
48
|
+
} catch (err) {
|
|
49
|
+
if ((err as NodeJS.ErrnoException).code === 'ESRCH') {
|
|
50
|
+
// Process doesn't exist
|
|
51
|
+
deletePidFile();
|
|
52
|
+
console.log('Daemon was not running (stale PID file removed).');
|
|
53
|
+
} else {
|
|
54
|
+
console.error('Failed to stop daemon:', err);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
|
|
5
|
+
export interface CmdCtrlConfig {
|
|
6
|
+
serverUrl: string;
|
|
7
|
+
deviceId: string;
|
|
8
|
+
deviceName: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface Credentials {
|
|
12
|
+
accessToken: string;
|
|
13
|
+
refreshToken: string;
|
|
14
|
+
expiresAt: number; // Unix timestamp
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const CONFIG_DIR = path.join(os.homedir(), '.cmdctrl-cursor-ide');
|
|
18
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
19
|
+
const CREDENTIALS_FILE = path.join(CONFIG_DIR, 'credentials');
|
|
20
|
+
const PID_FILE = path.join(CONFIG_DIR, 'daemon.pid');
|
|
21
|
+
|
|
22
|
+
// Cursor data paths (macOS)
|
|
23
|
+
export const CURSOR_GLOBAL_STORAGE = path.join(
|
|
24
|
+
os.homedir(),
|
|
25
|
+
'Library/Application Support/Cursor/User/globalStorage/state.vscdb'
|
|
26
|
+
);
|
|
27
|
+
export const CURSOR_WORKSPACE_STORAGE = path.join(
|
|
28
|
+
os.homedir(),
|
|
29
|
+
'Library/Application Support/Cursor/User/workspaceStorage'
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
// CDP default port
|
|
33
|
+
export const CDP_PORT = 9222;
|
|
34
|
+
export const CDP_URL = `http://localhost:${CDP_PORT}`;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Ensure the config directory exists with proper permissions
|
|
38
|
+
*/
|
|
39
|
+
export function ensureConfigDir(): void {
|
|
40
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
41
|
+
fs.mkdirSync(CONFIG_DIR, { mode: 0o700 });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Read the config file
|
|
47
|
+
*/
|
|
48
|
+
export function readConfig(): CmdCtrlConfig | null {
|
|
49
|
+
try {
|
|
50
|
+
if (!fs.existsSync(CONFIG_FILE)) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
|
54
|
+
return JSON.parse(content) as CmdCtrlConfig;
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Write the config file
|
|
62
|
+
*/
|
|
63
|
+
export function writeConfig(config: CmdCtrlConfig): void {
|
|
64
|
+
ensureConfigDir();
|
|
65
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Read credentials (access/refresh tokens)
|
|
70
|
+
*/
|
|
71
|
+
export function readCredentials(): Credentials | null {
|
|
72
|
+
try {
|
|
73
|
+
if (!fs.existsSync(CREDENTIALS_FILE)) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
const content = fs.readFileSync(CREDENTIALS_FILE, 'utf-8');
|
|
77
|
+
return JSON.parse(content) as Credentials;
|
|
78
|
+
} catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Write credentials with restrictive permissions (600)
|
|
85
|
+
*/
|
|
86
|
+
export function writeCredentials(creds: Credentials): void {
|
|
87
|
+
ensureConfigDir();
|
|
88
|
+
fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(creds, null, 2), { mode: 0o600 });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Delete credentials (for logout/revoke)
|
|
93
|
+
*/
|
|
94
|
+
export function deleteCredentials(): void {
|
|
95
|
+
if (fs.existsSync(CREDENTIALS_FILE)) {
|
|
96
|
+
fs.unlinkSync(CREDENTIALS_FILE);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Check if device is registered
|
|
102
|
+
*/
|
|
103
|
+
export function isRegistered(): boolean {
|
|
104
|
+
const config = readConfig();
|
|
105
|
+
const creds = readCredentials();
|
|
106
|
+
return config !== null && creds !== null && config.deviceId !== '';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Write daemon PID file
|
|
111
|
+
*/
|
|
112
|
+
export function writePidFile(pid: number): void {
|
|
113
|
+
ensureConfigDir();
|
|
114
|
+
fs.writeFileSync(PID_FILE, pid.toString(), { mode: 0o600 });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Read daemon PID
|
|
119
|
+
*/
|
|
120
|
+
export function readPidFile(): number | null {
|
|
121
|
+
try {
|
|
122
|
+
if (!fs.existsSync(PID_FILE)) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
const content = fs.readFileSync(PID_FILE, 'utf-8');
|
|
126
|
+
return parseInt(content, 10);
|
|
127
|
+
} catch {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Delete PID file
|
|
134
|
+
*/
|
|
135
|
+
export function deletePidFile(): void {
|
|
136
|
+
if (fs.existsSync(PID_FILE)) {
|
|
137
|
+
fs.unlinkSync(PID_FILE);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Check if daemon is running
|
|
143
|
+
*/
|
|
144
|
+
export function isDaemonRunning(): boolean {
|
|
145
|
+
const pid = readPidFile();
|
|
146
|
+
if (pid === null) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
// Signal 0 doesn't kill, just checks if process exists
|
|
151
|
+
process.kill(pid, 0);
|
|
152
|
+
return true;
|
|
153
|
+
} catch {
|
|
154
|
+
// Process doesn't exist, clean up stale PID file
|
|
155
|
+
deletePidFile();
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Check if Cursor global storage database exists
|
|
162
|
+
*/
|
|
163
|
+
export function cursorDbExists(): boolean {
|
|
164
|
+
return fs.existsSync(CURSOR_GLOBAL_STORAGE);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export { CONFIG_DIR, CONFIG_FILE, CREDENTIALS_FILE, PID_FILE };
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { register } from './commands/register';
|
|
5
|
+
import { start } from './commands/start';
|
|
6
|
+
import { status } from './commands/status';
|
|
7
|
+
import { stop } from './commands/stop';
|
|
8
|
+
|
|
9
|
+
const program = new Command();
|
|
10
|
+
|
|
11
|
+
program
|
|
12
|
+
.name('cmdctrl-cursor-ide')
|
|
13
|
+
.description('Cursor IDE daemon - connects Cursor sessions to the CmdCtrl orchestration server')
|
|
14
|
+
.version('0.1.0');
|
|
15
|
+
|
|
16
|
+
program
|
|
17
|
+
.command('register')
|
|
18
|
+
.description('Register this device with a CmdCtrl server')
|
|
19
|
+
.option('-s, --server <url>', 'CmdCtrl server URL', 'http://localhost:4000')
|
|
20
|
+
.option('-n, --name <name>', 'Device name (defaults to hostname-cursor)')
|
|
21
|
+
.action(register);
|
|
22
|
+
|
|
23
|
+
program
|
|
24
|
+
.command('start')
|
|
25
|
+
.description('Start the daemon and connect to the CmdCtrl server')
|
|
26
|
+
.option('-f, --foreground', 'Run in foreground (don\'t daemonize)')
|
|
27
|
+
.action(start);
|
|
28
|
+
|
|
29
|
+
program
|
|
30
|
+
.command('status')
|
|
31
|
+
.description('Check daemon and CDP connection status')
|
|
32
|
+
.action(status);
|
|
33
|
+
|
|
34
|
+
program
|
|
35
|
+
.command('stop')
|
|
36
|
+
.description('Stop the running daemon')
|
|
37
|
+
.action(stop);
|
|
38
|
+
|
|
39
|
+
program.parse();
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { getCursorDB, ComposerInfo } from './adapter/cursor-db';
|
|
2
|
+
|
|
3
|
+
export interface ExternalSession {
|
|
4
|
+
session_id: string;
|
|
5
|
+
slug: string;
|
|
6
|
+
title: string;
|
|
7
|
+
project: string;
|
|
8
|
+
project_name: string;
|
|
9
|
+
file_path: string; // For Cursor, this is the DB path
|
|
10
|
+
last_message: string;
|
|
11
|
+
last_activity: string;
|
|
12
|
+
is_active: boolean;
|
|
13
|
+
message_count: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Discover Cursor IDE sessions from the SQLite database
|
|
18
|
+
* Returns sessions in a format compatible with CmdCtrl's external session API
|
|
19
|
+
*/
|
|
20
|
+
export function discoverSessions(excludeSessionIds: Set<string> = new Set()): ExternalSession[] {
|
|
21
|
+
const cursorDb = getCursorDB();
|
|
22
|
+
const composers = cursorDb.getComposers();
|
|
23
|
+
const sessions: ExternalSession[] = [];
|
|
24
|
+
|
|
25
|
+
const now = Date.now();
|
|
26
|
+
const ACTIVE_THRESHOLD = 30 * 1000; // 30 seconds
|
|
27
|
+
|
|
28
|
+
for (const composer of composers) {
|
|
29
|
+
// Skip excluded sessions (managed by CmdCtrl)
|
|
30
|
+
if (excludeSessionIds.has(composer.composerId)) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Use composer metadata directly for discovery (avoid expensive per-session queries)
|
|
35
|
+
// The detailed info (message count, last message preview) can be fetched on-demand
|
|
36
|
+
const lastActivity = new Date(composer.lastUpdatedAt).toISOString();
|
|
37
|
+
const isActive = (now - composer.lastUpdatedAt) < ACTIVE_THRESHOLD;
|
|
38
|
+
|
|
39
|
+
// Generate a slug from the composer name
|
|
40
|
+
const slug = generateSlug(composer.name);
|
|
41
|
+
|
|
42
|
+
// Use project path if available, otherwise empty (frontend can show device name)
|
|
43
|
+
const project = composer.projectPath || '';
|
|
44
|
+
|
|
45
|
+
sessions.push({
|
|
46
|
+
session_id: composer.composerId,
|
|
47
|
+
slug,
|
|
48
|
+
title: composer.name || 'Untitled',
|
|
49
|
+
project,
|
|
50
|
+
project_name: project ? project.split('/').pop() || 'cursor' : 'cursor',
|
|
51
|
+
file_path: 'state.vscdb', // Reference to SQLite
|
|
52
|
+
last_message: '', // Fetched on-demand when viewing session
|
|
53
|
+
last_activity: lastActivity,
|
|
54
|
+
is_active: isActive,
|
|
55
|
+
message_count: 0, // Fetched on-demand when viewing session
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Sort by last activity (newest first)
|
|
60
|
+
return sessions.sort((a, b) =>
|
|
61
|
+
new Date(b.last_activity).getTime() - new Date(a.last_activity).getTime()
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Generate a URL-friendly slug from a title
|
|
67
|
+
*/
|
|
68
|
+
function generateSlug(title: string): string {
|
|
69
|
+
return title
|
|
70
|
+
.toLowerCase()
|
|
71
|
+
.replace(/[^a-z0-9\s-]/g, '')
|
|
72
|
+
.replace(/\s+/g, '-')
|
|
73
|
+
.replace(/-+/g, '-')
|
|
74
|
+
.substring(0, 50);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get details for a specific session
|
|
79
|
+
*/
|
|
80
|
+
export function getSessionDetails(sessionId: string): ExternalSession | null {
|
|
81
|
+
const cursorDb = getCursorDB();
|
|
82
|
+
const composers = cursorDb.getComposers();
|
|
83
|
+
const composer = composers.find(c => c.composerId === sessionId);
|
|
84
|
+
|
|
85
|
+
if (!composer) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const messageCount = cursorDb.getBubbleCount(sessionId);
|
|
90
|
+
const latestBubble = cursorDb.getLatestBubble(sessionId);
|
|
91
|
+
const lastMessage = latestBubble?.text?.substring(0, 100) || '';
|
|
92
|
+
const lastActivity = latestBubble?.createdAt || new Date(composer.lastUpdatedAt).toISOString();
|
|
93
|
+
|
|
94
|
+
const now = Date.now();
|
|
95
|
+
const ACTIVE_THRESHOLD = 30 * 1000;
|
|
96
|
+
const lastUpdateTime = latestBubble
|
|
97
|
+
? new Date(latestBubble.createdAt).getTime()
|
|
98
|
+
: composer.lastUpdatedAt;
|
|
99
|
+
const isActive = (now - lastUpdateTime) < ACTIVE_THRESHOLD;
|
|
100
|
+
|
|
101
|
+
const project = composer.projectPath || '';
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
session_id: composer.composerId,
|
|
105
|
+
slug: generateSlug(composer.name),
|
|
106
|
+
title: composer.name || 'Untitled',
|
|
107
|
+
project,
|
|
108
|
+
project_name: project ? project.split('/').pop() || 'cursor' : 'cursor',
|
|
109
|
+
file_path: 'state.vscdb',
|
|
110
|
+
last_message: lastMessage,
|
|
111
|
+
last_activity: lastActivity,
|
|
112
|
+
is_active: isActive,
|
|
113
|
+
message_count: messageCount,
|
|
114
|
+
};
|
|
115
|
+
}
|