@cmdctrl/cursor-cli 0.1.1

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.
Files changed (57) hide show
  1. package/dist/adapter/cursor-cli.d.ts +36 -0
  2. package/dist/adapter/cursor-cli.d.ts.map +1 -0
  3. package/dist/adapter/cursor-cli.js +332 -0
  4. package/dist/adapter/cursor-cli.js.map +1 -0
  5. package/dist/adapter/events.d.ts +33 -0
  6. package/dist/adapter/events.d.ts.map +1 -0
  7. package/dist/adapter/events.js +53 -0
  8. package/dist/adapter/events.js.map +1 -0
  9. package/dist/client/messages.d.ts +50 -0
  10. package/dist/client/messages.d.ts.map +1 -0
  11. package/dist/client/messages.js +6 -0
  12. package/dist/client/messages.js.map +1 -0
  13. package/dist/client/websocket.d.ts +69 -0
  14. package/dist/client/websocket.d.ts.map +1 -0
  15. package/dist/client/websocket.js +272 -0
  16. package/dist/client/websocket.js.map +1 -0
  17. package/dist/commands/register.d.ts +10 -0
  18. package/dist/commands/register.d.ts.map +1 -0
  19. package/dist/commands/register.js +173 -0
  20. package/dist/commands/register.js.map +1 -0
  21. package/dist/commands/start.d.ts +9 -0
  22. package/dist/commands/start.d.ts.map +1 -0
  23. package/dist/commands/start.js +49 -0
  24. package/dist/commands/start.js.map +1 -0
  25. package/dist/commands/status.d.ts +5 -0
  26. package/dist/commands/status.d.ts.map +1 -0
  27. package/dist/commands/status.js +39 -0
  28. package/dist/commands/status.js.map +1 -0
  29. package/dist/commands/stop.d.ts +5 -0
  30. package/dist/commands/stop.d.ts.map +1 -0
  31. package/dist/commands/stop.js +42 -0
  32. package/dist/commands/stop.js.map +1 -0
  33. package/dist/commands/update.d.ts +2 -0
  34. package/dist/commands/update.d.ts.map +1 -0
  35. package/dist/commands/update.js +72 -0
  36. package/dist/commands/update.js.map +1 -0
  37. package/dist/config/config.d.ts +60 -0
  38. package/dist/config/config.d.ts.map +1 -0
  39. package/dist/config/config.js +176 -0
  40. package/dist/config/config.js.map +1 -0
  41. package/dist/index.d.ts +3 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +50 -0
  44. package/dist/index.js.map +1 -0
  45. package/package.json +38 -0
  46. package/src/adapter/cursor-cli.ts +370 -0
  47. package/src/adapter/events.ts +77 -0
  48. package/src/client/messages.ts +75 -0
  49. package/src/client/websocket.ts +308 -0
  50. package/src/commands/register.ts +199 -0
  51. package/src/commands/start.ts +64 -0
  52. package/src/commands/status.ts +46 -0
  53. package/src/commands/stop.ts +47 -0
  54. package/src/commands/update.ts +73 -0
  55. package/src/config/config.ts +146 -0
  56. package/src/index.ts +56 -0
  57. package/tsconfig.json +19 -0
@@ -0,0 +1,308 @@
1
+ import WebSocket from 'ws';
2
+ import { URL } from 'url';
3
+ import { CmdCtrlConfig, Credentials } from '../config/config';
4
+ import { CursorAdapter } from '../adapter/cursor-cli';
5
+ import {
6
+ ServerMessage,
7
+ DaemonMessage,
8
+ TaskStartMessage,
9
+ TaskResumeMessage,
10
+ TaskCancelMessage,
11
+ VersionStatusMessage,
12
+ } from './messages';
13
+ import { readFileSync } from 'fs';
14
+ import { join } from 'path';
15
+
16
+ const MAX_RECONNECT_DELAY = 30000; // 30 seconds
17
+ const INITIAL_RECONNECT_DELAY = 1000; // 1 second
18
+ const PING_INTERVAL = 30000; // 30 seconds
19
+
20
+ export class DaemonClient {
21
+ private ws: WebSocket | null = null;
22
+ private config: CmdCtrlConfig;
23
+ private credentials: Credentials;
24
+ private reconnectDelay = INITIAL_RECONNECT_DELAY;
25
+ private reconnectTimer: NodeJS.Timeout | null = null;
26
+ private pingTimer: NodeJS.Timeout | null = null;
27
+ private shouldReconnect = true;
28
+ private adapter: CursorAdapter;
29
+
30
+ constructor(config: CmdCtrlConfig, credentials: Credentials) {
31
+ this.config = config;
32
+ this.credentials = credentials;
33
+ this.adapter = new CursorAdapter(this.sendEvent.bind(this));
34
+ }
35
+
36
+ /**
37
+ * Connect to the CmdCtrl server via WebSocket
38
+ */
39
+ async connect(): Promise<void> {
40
+ return new Promise((resolve, reject) => {
41
+ const serverUrl = new URL(this.config.serverUrl);
42
+ const wsProtocol = serverUrl.protocol === 'https:' ? 'wss:' : 'ws:';
43
+ const wsUrl = `${wsProtocol}//${serverUrl.host}/ws/daemon`;
44
+
45
+ console.log(`Connecting to ${wsUrl}...`);
46
+
47
+ // Read version from package.json
48
+ let daemonVersion = 'unknown';
49
+ try {
50
+ const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
51
+ daemonVersion = pkg.version;
52
+ } catch {
53
+ try {
54
+ const pkg = JSON.parse(readFileSync(join(__dirname, '..', '..', 'package.json'), 'utf-8'));
55
+ daemonVersion = pkg.version;
56
+ } catch { /* use default */ }
57
+ }
58
+
59
+ this.ws = new WebSocket(wsUrl, {
60
+ headers: {
61
+ Authorization: `Bearer ${this.credentials.accessToken}`,
62
+ 'X-Device-ID': this.config.deviceId,
63
+ 'X-Agent-Type': 'cursor_cli',
64
+ 'X-Daemon-Version': daemonVersion,
65
+ }
66
+ });
67
+
68
+ this.ws.on('open', () => {
69
+ console.log('WebSocket connected');
70
+ this.reconnectDelay = INITIAL_RECONNECT_DELAY;
71
+ this.startPingInterval();
72
+ this.sendStatus();
73
+ resolve();
74
+ });
75
+
76
+ this.ws.on('message', (data) => {
77
+ this.handleMessage(data.toString());
78
+ });
79
+
80
+ this.ws.on('close', (code, reason) => {
81
+ console.log(`WebSocket closed: ${code} ${reason}`);
82
+ this.stopPingInterval();
83
+ this.scheduleReconnect();
84
+ });
85
+
86
+ this.ws.on('error', (err) => {
87
+ console.error('WebSocket error:', err.message);
88
+ if (this.ws?.readyState === WebSocket.CONNECTING) {
89
+ reject(err);
90
+ }
91
+ // For established connections, the 'close' event usually follows 'error',
92
+ // but if the connection was killed abruptly, we may not get a clean close.
93
+ // Force close the socket to ensure 'close' event fires and triggers reconnect.
94
+ if (this.ws?.readyState === WebSocket.OPEN) {
95
+ this.ws.terminate();
96
+ }
97
+ });
98
+ });
99
+ }
100
+
101
+ /**
102
+ * Disconnect from server
103
+ */
104
+ async disconnect(): Promise<void> {
105
+ this.shouldReconnect = false;
106
+
107
+ if (this.reconnectTimer) {
108
+ clearTimeout(this.reconnectTimer);
109
+ this.reconnectTimer = null;
110
+ }
111
+
112
+ this.stopPingInterval();
113
+
114
+ // Stop all running tasks
115
+ await this.adapter.stopAll();
116
+
117
+ if (this.ws) {
118
+ this.ws.close(1000, 'Daemon shutting down');
119
+ this.ws = null;
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Send a message to the server
125
+ */
126
+ private send(message: DaemonMessage): void {
127
+ if (this.ws?.readyState === WebSocket.OPEN) {
128
+ this.ws.send(JSON.stringify(message));
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Send an event for a task
134
+ */
135
+ private sendEvent(
136
+ taskId: string,
137
+ eventType: string,
138
+ data: Record<string, unknown>
139
+ ): void {
140
+ this.send({
141
+ type: 'event',
142
+ task_id: taskId,
143
+ event_type: eventType,
144
+ ...data
145
+ });
146
+ }
147
+
148
+ /**
149
+ * Send current status to server
150
+ */
151
+ private sendStatus(): void {
152
+ this.send({
153
+ type: 'status',
154
+ running_tasks: this.adapter.getRunningTasks()
155
+ });
156
+ }
157
+
158
+ /**
159
+ * Handle incoming message from server
160
+ */
161
+ private handleMessage(raw: string): void {
162
+ let msg: ServerMessage;
163
+ try {
164
+ msg = JSON.parse(raw) as ServerMessage;
165
+ } catch {
166
+ console.error('Failed to parse message:', raw);
167
+ return;
168
+ }
169
+
170
+ switch (msg.type) {
171
+ case 'ping':
172
+ this.send({ type: 'pong' });
173
+ break;
174
+
175
+ case 'task_start':
176
+ this.handleTaskStart(msg as TaskStartMessage);
177
+ break;
178
+
179
+ case 'task_resume':
180
+ this.handleTaskResume(msg as TaskResumeMessage);
181
+ break;
182
+
183
+ case 'task_cancel':
184
+ this.handleTaskCancel(msg as TaskCancelMessage);
185
+ break;
186
+
187
+ case 'version_status':
188
+ this.handleVersionStatus(msg as VersionStatusMessage);
189
+ break;
190
+
191
+ default:
192
+ console.warn('Unknown message type:', (msg as { type: string }).type);
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Handle task_start message
198
+ */
199
+ private async handleTaskStart(msg: TaskStartMessage): Promise<void> {
200
+ console.log(`Starting task ${msg.task_id}: ${msg.instruction.substring(0, 50)}...`);
201
+
202
+ try {
203
+ await this.adapter.startTask(msg.task_id, msg.instruction, msg.project_path);
204
+ } catch (err) {
205
+ console.error(`Failed to start task ${msg.task_id}:`, err);
206
+ this.sendEvent(msg.task_id, 'ERROR', {
207
+ error: (err as Error).message
208
+ });
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Handle task_resume message
214
+ */
215
+ private async handleTaskResume(msg: TaskResumeMessage): Promise<void> {
216
+ console.log(`Resuming task ${msg.task_id} with session ${msg.session_id}`);
217
+
218
+ try {
219
+ await this.adapter.resumeTask(msg.task_id, msg.session_id, msg.message, msg.project_path);
220
+ } catch (err) {
221
+ console.error(`Failed to resume task ${msg.task_id}:`, err);
222
+ this.sendEvent(msg.task_id, 'ERROR', {
223
+ error: (err as Error).message
224
+ });
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Handle task_cancel message
230
+ */
231
+ private async handleTaskCancel(msg: TaskCancelMessage): Promise<void> {
232
+ console.log(`Cancelling task ${msg.task_id}`);
233
+ await this.adapter.cancelTask(msg.task_id);
234
+ }
235
+
236
+ /**
237
+ * Handle version_status message from server
238
+ */
239
+ private handleVersionStatus(msg: VersionStatusMessage): void {
240
+ if (msg.status === 'update_required') {
241
+ console.error(`\n✖ Daemon version ${msg.your_version} is no longer supported (minimum: ${msg.min_version})`);
242
+ console.error(` Run: cmdctrl-cursor-cli update`);
243
+ if (msg.changelog_url) console.error(` Changelog: ${msg.changelog_url}`);
244
+ if (msg.message) console.error(` ${msg.message}`);
245
+ console.error('');
246
+ this.shouldReconnect = false;
247
+ process.exit(1);
248
+ } else if (msg.status === 'update_available') {
249
+ console.warn(`\n⚠ Update available: v${msg.latest_version} (you have v${msg.your_version})`);
250
+ console.warn(` Run: cmdctrl-cursor-cli update`);
251
+ if (msg.changelog_url) console.warn(` Changelog: ${msg.changelog_url}`);
252
+ console.warn('');
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Schedule reconnection with exponential backoff
258
+ */
259
+ private scheduleReconnect(): void {
260
+ if (!this.shouldReconnect) {
261
+ return;
262
+ }
263
+
264
+ console.log(`Reconnecting in ${this.reconnectDelay / 1000}s...`);
265
+
266
+ this.reconnectTimer = setTimeout(async () => {
267
+ try {
268
+ await this.connect();
269
+ } catch {
270
+ // Increase delay with exponential backoff
271
+ this.reconnectDelay = Math.min(
272
+ this.reconnectDelay * 2,
273
+ MAX_RECONNECT_DELAY
274
+ );
275
+ this.scheduleReconnect();
276
+ }
277
+ }, this.reconnectDelay);
278
+ }
279
+
280
+ /**
281
+ * Start ping interval to keep connection alive
282
+ */
283
+ private startPingInterval(): void {
284
+ this.pingTimer = setInterval(() => {
285
+ if (this.ws?.readyState === WebSocket.OPEN) {
286
+ this.ws.ping();
287
+ }
288
+ }, PING_INTERVAL);
289
+ }
290
+
291
+ /**
292
+ * Stop ping interval
293
+ */
294
+ private stopPingInterval(): void {
295
+ if (this.pingTimer) {
296
+ clearInterval(this.pingTimer);
297
+ this.pingTimer = null;
298
+ }
299
+ }
300
+
301
+ /**
302
+ * Refresh access token using refresh token
303
+ */
304
+ async refreshToken(): Promise<boolean> {
305
+ console.log('Token refresh not yet implemented');
306
+ return false;
307
+ }
308
+ }
@@ -0,0 +1,199 @@
1
+ import * as os from 'os';
2
+ import * as http from 'http';
3
+ import * as https from 'https';
4
+ import { URL } from 'url';
5
+ import {
6
+ writeConfig,
7
+ writeCredentials,
8
+ readConfig,
9
+ isRegistered,
10
+ CmdCtrlConfig,
11
+ Credentials
12
+ } from '../config/config';
13
+
14
+ interface RegisterOptions {
15
+ server: string;
16
+ name?: string;
17
+ }
18
+
19
+ interface DeviceCodeResponse {
20
+ deviceCode: string;
21
+ userCode: string;
22
+ verificationUrl: string;
23
+ expiresIn: number;
24
+ interval: number;
25
+ }
26
+
27
+ interface TokenResponse {
28
+ accessToken: string;
29
+ refreshToken: string;
30
+ expiresIn: number;
31
+ deviceId: string;
32
+ }
33
+
34
+ /**
35
+ * Make an HTTP(S) request
36
+ */
37
+ function request(
38
+ url: string,
39
+ method: string,
40
+ body?: object
41
+ ): Promise<{ status: number; data: unknown }> {
42
+ return new Promise((resolve, reject) => {
43
+ const parsed = new URL(url);
44
+ const client = parsed.protocol === 'https:' ? https : http;
45
+
46
+ const options = {
47
+ hostname: parsed.hostname,
48
+ port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
49
+ path: parsed.pathname + parsed.search,
50
+ method,
51
+ headers: {
52
+ 'Content-Type': 'application/json',
53
+ Accept: 'application/json'
54
+ }
55
+ };
56
+
57
+ const req = client.request(options, (res) => {
58
+ let data = '';
59
+ res.on('data', (chunk) => (data += chunk));
60
+ res.on('end', () => {
61
+ try {
62
+ const parsed = data ? JSON.parse(data) : {};
63
+ resolve({ status: res.statusCode || 0, data: parsed });
64
+ } catch {
65
+ resolve({ status: res.statusCode || 0, data: {} });
66
+ }
67
+ });
68
+ });
69
+
70
+ req.on('error', reject);
71
+
72
+ if (body) {
73
+ req.write(JSON.stringify(body));
74
+ }
75
+ req.end();
76
+ });
77
+ }
78
+
79
+ /**
80
+ * Poll for token completion (after user verifies in browser)
81
+ */
82
+ async function pollForToken(
83
+ serverUrl: string,
84
+ deviceCode: string,
85
+ interval: number,
86
+ expiresIn: number
87
+ ): Promise<TokenResponse | null> {
88
+ const startTime = Date.now();
89
+ const expiresAt = startTime + expiresIn * 1000;
90
+
91
+ while (Date.now() < expiresAt) {
92
+ await new Promise((resolve) => setTimeout(resolve, interval * 1000));
93
+
94
+ try {
95
+ const response = await request(`${serverUrl}/api/devices/token`, 'POST', {
96
+ deviceCode
97
+ });
98
+
99
+ if (response.status === 200) {
100
+ return response.data as TokenResponse;
101
+ }
102
+
103
+ // 400 with "authorization_pending" means keep polling
104
+ const data = response.data as { error?: string };
105
+ if (response.status === 400 && data.error === 'authorization_pending') {
106
+ process.stdout.write('.');
107
+ continue;
108
+ }
109
+
110
+ // Other errors should stop polling
111
+ if (response.status >= 400) {
112
+ console.error('\nError polling for token:', data);
113
+ return null;
114
+ }
115
+ } catch (err) {
116
+ console.error('\nError polling for token:', err);
117
+ return null;
118
+ }
119
+ }
120
+
121
+ console.error('\nDevice code expired. Please try again.');
122
+ return null;
123
+ }
124
+
125
+ /**
126
+ * Register command - implements GitHub CLI style device auth flow
127
+ */
128
+ export async function register(options: RegisterOptions): Promise<void> {
129
+ const serverUrl = options.server.replace(/\/$/, ''); // Remove trailing slash
130
+ const deviceName = options.name || `${os.hostname()}-cursor`;
131
+
132
+ // Check if already registered
133
+ if (isRegistered()) {
134
+ const config = readConfig();
135
+ console.log(`Already registered as "${config?.deviceName}" (${config?.deviceId})`);
136
+ console.log(`Server: ${config?.serverUrl}`);
137
+ console.log(`\nTo re-register, run: ${process.argv[1]} unregister`);
138
+ return;
139
+ }
140
+
141
+ console.log(`Registering Cursor CLI device "${deviceName}" with ${serverUrl}...\n`);
142
+
143
+ // Step 1: Request device code
144
+ let codeResponse: DeviceCodeResponse;
145
+ try {
146
+ const response = await request(`${serverUrl}/api/devices/code`, 'POST', {
147
+ deviceName,
148
+ hostname: os.hostname(),
149
+ agentType: 'cursor_cli'
150
+ });
151
+
152
+ if (response.status !== 200) {
153
+ console.error('Failed to get device code:', response.data);
154
+ process.exit(1);
155
+ }
156
+
157
+ codeResponse = response.data as DeviceCodeResponse;
158
+ } catch (err) {
159
+ console.error('Failed to connect to server:', err);
160
+ process.exit(1);
161
+ }
162
+
163
+ // Step 2: Display instructions to user
164
+ console.log('To complete registration, open this URL in your browser:\n');
165
+ console.log(` ${codeResponse.verificationUrl}\n`);
166
+ console.log('Waiting for verification...');
167
+
168
+ // Step 3: Poll for completion
169
+ const tokenResponse = await pollForToken(
170
+ serverUrl,
171
+ codeResponse.deviceCode,
172
+ codeResponse.interval,
173
+ codeResponse.expiresIn
174
+ );
175
+
176
+ if (!tokenResponse) {
177
+ process.exit(1);
178
+ }
179
+
180
+ // Step 4: Save config and credentials
181
+ const config: CmdCtrlConfig = {
182
+ serverUrl,
183
+ deviceId: tokenResponse.deviceId,
184
+ deviceName
185
+ };
186
+
187
+ const credentials: Credentials = {
188
+ accessToken: tokenResponse.accessToken,
189
+ refreshToken: tokenResponse.refreshToken,
190
+ expiresAt: Date.now() + tokenResponse.expiresIn * 1000
191
+ };
192
+
193
+ writeConfig(config);
194
+ writeCredentials(credentials);
195
+
196
+ console.log('\n\nRegistration complete!');
197
+ console.log(`Device ID: ${tokenResponse.deviceId}`);
198
+ console.log(`\nRun 'cmdctrl-cursor-cli start' to connect to the server.`);
199
+ }
@@ -0,0 +1,64 @@
1
+ import {
2
+ readConfig,
3
+ readCredentials,
4
+ isRegistered,
5
+ writePidFile,
6
+ isDaemonRunning
7
+ } from '../config/config';
8
+ import { DaemonClient } from '../client/websocket';
9
+
10
+ interface StartOptions {
11
+ foreground?: boolean;
12
+ }
13
+
14
+ /**
15
+ * Start the daemon
16
+ */
17
+ export async function start(options: StartOptions): Promise<void> {
18
+ // Check if registered
19
+ if (!isRegistered()) {
20
+ console.error('Device not registered. Run "cmdctrl-cursor-cli-daemon register" first.');
21
+ process.exit(1);
22
+ }
23
+
24
+ // Check if already running
25
+ if (isDaemonRunning()) {
26
+ console.error('Daemon is already running. Use "cmdctrl-cursor-cli-daemon stop" to stop it.');
27
+ process.exit(1);
28
+ }
29
+
30
+ const config = readConfig()!;
31
+ const credentials = readCredentials()!;
32
+
33
+ console.log(`Starting Cursor CLI daemon for device "${config.deviceName}"...`);
34
+ console.log(`Server: ${config.serverUrl}`);
35
+
36
+ // Write PID file
37
+ writePidFile(process.pid);
38
+
39
+ // Create and connect client
40
+ const client = new DaemonClient(config, credentials);
41
+
42
+ // Handle shutdown signals
43
+ const shutdown = async (signal: string) => {
44
+ console.log(`\nReceived ${signal}, shutting down...`);
45
+ await client.disconnect();
46
+ process.exit(0);
47
+ };
48
+
49
+ process.on('SIGINT', () => shutdown('SIGINT'));
50
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
51
+
52
+ try {
53
+ await client.connect();
54
+ console.log('Cursor CLI daemon connected and ready for tasks.');
55
+
56
+ // In foreground mode, just keep running
57
+ if (options.foreground) {
58
+ console.log('Running in foreground. Press Ctrl+C to stop.');
59
+ }
60
+ } catch (err) {
61
+ console.error('Failed to connect:', err);
62
+ process.exit(1);
63
+ }
64
+ }
@@ -0,0 +1,46 @@
1
+ import {
2
+ readConfig,
3
+ readCredentials,
4
+ isRegistered,
5
+ isDaemonRunning,
6
+ readPidFile
7
+ } from '../config/config';
8
+
9
+ /**
10
+ * Show daemon status
11
+ */
12
+ export async function status(): Promise<void> {
13
+ if (!isRegistered()) {
14
+ console.log('Status: Not registered');
15
+ console.log('\nRun "cmdctrl-cursor-cli-daemon register" to register this device.');
16
+ return;
17
+ }
18
+
19
+ const config = readConfig()!;
20
+ const credentials = readCredentials();
21
+ const running = isDaemonRunning();
22
+ const pid = readPidFile();
23
+
24
+ console.log('Cursor CLI Daemon Status');
25
+ console.log('========================');
26
+ console.log(`Device Name: ${config.deviceName}`);
27
+ console.log(`Device ID: ${config.deviceId}`);
28
+ console.log(`Server: ${config.serverUrl}`);
29
+ console.log();
30
+ console.log(`Daemon: ${running ? `Running (PID ${pid})` : 'Stopped'}`);
31
+
32
+ if (credentials) {
33
+ const expiresIn = credentials.expiresAt - Date.now();
34
+ const expired = expiresIn <= 0;
35
+ console.log(`Token: ${expired ? 'Expired' : 'Valid'}`);
36
+ if (!expired) {
37
+ const hours = Math.floor(expiresIn / 1000 / 60 / 60);
38
+ const minutes = Math.floor((expiresIn / 1000 / 60) % 60);
39
+ console.log(`Expires in: ${hours}h ${minutes}m`);
40
+ }
41
+ }
42
+
43
+ if (!running) {
44
+ console.log('\nRun "cmdctrl-cursor-cli-daemon start" to start the daemon.');
45
+ }
46
+ }
@@ -0,0 +1,47 @@
1
+ import {
2
+ isDaemonRunning,
3
+ readPidFile,
4
+ deletePidFile
5
+ } from '../config/config';
6
+
7
+ /**
8
+ * Stop the daemon
9
+ */
10
+ export async function stop(): Promise<void> {
11
+ if (!isDaemonRunning()) {
12
+ console.log('Daemon is not running.');
13
+ return;
14
+ }
15
+
16
+ const pid = readPidFile();
17
+ if (!pid) {
18
+ console.log('Could not find daemon PID.');
19
+ return;
20
+ }
21
+
22
+ console.log(`Stopping Cursor CLI daemon (PID ${pid})...`);
23
+
24
+ try {
25
+ // Send SIGTERM to gracefully shutdown
26
+ process.kill(pid, 'SIGTERM');
27
+
28
+ // Wait a bit for graceful shutdown
29
+ await new Promise(resolve => setTimeout(resolve, 1000));
30
+
31
+ // Check if still running
32
+ try {
33
+ process.kill(pid, 0);
34
+ // Still running, send SIGKILL
35
+ console.log('Daemon not responding, force killing...');
36
+ process.kill(pid, 'SIGKILL');
37
+ } catch {
38
+ // Process is gone
39
+ }
40
+
41
+ deletePidFile();
42
+ console.log('Daemon stopped.');
43
+ } catch (err) {
44
+ console.error('Failed to stop daemon:', err);
45
+ deletePidFile();
46
+ }
47
+ }