@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,370 @@
1
+ import { spawn, ChildProcess } from 'child_process';
2
+ import * as readline from 'readline';
3
+ import * as fs from 'fs';
4
+ import * as os from 'os';
5
+ import * as path from 'path';
6
+ import { StreamEvent, extractProgressFromAction } from './events';
7
+
8
+ const DEFAULT_TIMEOUT = 10 * 60 * 1000; // 10 minutes
9
+
10
+ // Find cursor-agent CLI in common locations
11
+ function findCursorCli(): string {
12
+ if (process.env.CURSOR_CLI_PATH) {
13
+ return process.env.CURSOR_CLI_PATH;
14
+ }
15
+
16
+ const home = os.homedir();
17
+ const commonPaths = [
18
+ path.join(home, '.cursor', 'bin', 'cursor-agent'),
19
+ path.join(home, '.local', 'bin', 'cursor-agent'),
20
+ '/usr/local/bin/cursor-agent',
21
+ '/opt/homebrew/bin/cursor-agent',
22
+ 'cursor-agent' // Fall back to PATH
23
+ ];
24
+
25
+ for (const p of commonPaths) {
26
+ if (p === 'cursor-agent') return p; // PATH fallback
27
+ try {
28
+ if (fs.existsSync(p)) {
29
+ return p;
30
+ }
31
+ } catch {
32
+ continue;
33
+ }
34
+ }
35
+
36
+ return 'cursor-agent'; // Fall back to PATH
37
+ }
38
+
39
+ const CLI_PATH = findCursorCli();
40
+ console.log(`[CursorAdapter] Using CLI path: ${CLI_PATH}`);
41
+
42
+ interface RunningTask {
43
+ taskId: string;
44
+ sessionId: string;
45
+ question: string;
46
+ context: string;
47
+ process: ChildProcess | null;
48
+ timeoutHandle: NodeJS.Timeout | null;
49
+ }
50
+
51
+ type EventCallback = (
52
+ taskId: string,
53
+ eventType: string,
54
+ data: Record<string, unknown>
55
+ ) => void;
56
+
57
+ export class CursorAdapter {
58
+ private running: Map<string, RunningTask> = new Map();
59
+ private onEvent: EventCallback;
60
+
61
+ constructor(onEvent: EventCallback) {
62
+ this.onEvent = onEvent;
63
+ }
64
+
65
+ /**
66
+ * Start a new task
67
+ */
68
+ async startTask(
69
+ taskId: string,
70
+ instruction: string,
71
+ projectPath?: string
72
+ ): Promise<void> {
73
+ console.log(`[${taskId}] Starting task: ${instruction.substring(0, 50)}...`);
74
+
75
+ const rt: RunningTask = {
76
+ taskId,
77
+ sessionId: '',
78
+ question: '',
79
+ context: '',
80
+ process: null,
81
+ timeoutHandle: null
82
+ };
83
+
84
+ this.running.set(taskId, rt);
85
+
86
+ // Validate cwd exists
87
+ let cwd: string | undefined = undefined;
88
+ if (projectPath && fs.existsSync(projectPath)) {
89
+ cwd = projectPath;
90
+ } else if (projectPath) {
91
+ console.log(`[${taskId}] Warning: project path does not exist: ${projectPath}, using home dir`);
92
+ cwd = os.homedir();
93
+ this.onEvent(taskId, 'WARNING', {
94
+ warning: `Project path "${projectPath}" does not exist. Running in home directory instead.`
95
+ });
96
+ }
97
+
98
+ // Build command arguments for Cursor CLI
99
+ // cursor-agent -p "instruction" --output-format stream-json
100
+ const args = [
101
+ '-p', instruction,
102
+ '--output-format', 'stream-json'
103
+ ];
104
+
105
+ console.log(`[${taskId}] Spawning: ${CLI_PATH} with cwd: ${cwd || 'default'}`);
106
+
107
+ // Spawn Cursor CLI
108
+ const proc = spawn(CLI_PATH, args, {
109
+ cwd,
110
+ stdio: ['ignore', 'pipe', 'pipe'],
111
+ env: {
112
+ ...process.env,
113
+ // Pass through Cursor API key if set
114
+ CURSOR_API_KEY: process.env.CURSOR_API_KEY
115
+ }
116
+ });
117
+
118
+ rt.process = proc;
119
+
120
+ // Set timeout
121
+ rt.timeoutHandle = setTimeout(() => {
122
+ console.log(`[${taskId}] Task timed out`);
123
+ proc.kill('SIGKILL');
124
+ this.onEvent(taskId, 'ERROR', { error: 'execution timeout' });
125
+ }, DEFAULT_TIMEOUT);
126
+
127
+ // Handle process events
128
+ this.handleProcessOutput(taskId, proc, rt);
129
+ }
130
+
131
+ /**
132
+ * Resume a task with user's reply
133
+ */
134
+ async resumeTask(
135
+ taskId: string,
136
+ sessionId: string,
137
+ message: string,
138
+ projectPath?: string
139
+ ): Promise<void> {
140
+ console.log(`[${taskId}] Resuming task with session ${sessionId}`);
141
+
142
+ const rt: RunningTask = {
143
+ taskId,
144
+ sessionId,
145
+ question: '',
146
+ context: '',
147
+ process: null,
148
+ timeoutHandle: null
149
+ };
150
+
151
+ this.running.set(taskId, rt);
152
+
153
+ // Validate cwd exists
154
+ let cwd: string | undefined = undefined;
155
+ if (projectPath && fs.existsSync(projectPath)) {
156
+ cwd = projectPath;
157
+ } else if (projectPath) {
158
+ console.log(`[${taskId}] Warning: project path does not exist: ${projectPath}, using home dir`);
159
+ cwd = os.homedir();
160
+ }
161
+
162
+ // Build command arguments with --resume
163
+ // cursor-agent --resume="session-id" -p "user response" --output-format stream-json
164
+ const args = [
165
+ `--resume=${sessionId}`,
166
+ '-p', message,
167
+ '--output-format', 'stream-json'
168
+ ];
169
+
170
+ console.log(`[${taskId}] Spawning resume: ${CLI_PATH} --resume=${sessionId} with cwd: ${cwd || 'default'}`);
171
+
172
+ // Spawn Cursor CLI
173
+ const proc = spawn(CLI_PATH, args, {
174
+ cwd,
175
+ stdio: ['ignore', 'pipe', 'pipe'],
176
+ env: {
177
+ ...process.env,
178
+ CURSOR_API_KEY: process.env.CURSOR_API_KEY
179
+ }
180
+ });
181
+
182
+ rt.process = proc;
183
+
184
+ // Set timeout
185
+ rt.timeoutHandle = setTimeout(() => {
186
+ console.log(`[${taskId}] Task timed out`);
187
+ proc.kill('SIGKILL');
188
+ this.onEvent(taskId, 'ERROR', { error: 'execution timeout' });
189
+ }, DEFAULT_TIMEOUT);
190
+
191
+ // Handle process events
192
+ this.handleProcessOutput(taskId, proc, rt);
193
+ }
194
+
195
+ /**
196
+ * Cancel a running task
197
+ */
198
+ async cancelTask(taskId: string): Promise<void> {
199
+ const rt = this.running.get(taskId);
200
+ if (!rt) {
201
+ console.log(`[${taskId}] Task not found for cancellation`);
202
+ return;
203
+ }
204
+
205
+ if (rt.process) {
206
+ rt.process.kill('SIGTERM');
207
+ }
208
+ if (rt.timeoutHandle) {
209
+ clearTimeout(rt.timeoutHandle);
210
+ }
211
+
212
+ this.running.delete(taskId);
213
+ console.log(`[${taskId}] Task cancelled`);
214
+ }
215
+
216
+ /**
217
+ * Stop all running tasks
218
+ */
219
+ async stopAll(): Promise<void> {
220
+ for (const [taskId, rt] of this.running) {
221
+ console.log(`[${taskId}] Stopping task`);
222
+ if (rt.process) {
223
+ rt.process.kill('SIGTERM');
224
+ }
225
+ if (rt.timeoutHandle) {
226
+ clearTimeout(rt.timeoutHandle);
227
+ }
228
+ }
229
+ this.running.clear();
230
+ }
231
+
232
+ /**
233
+ * Get list of running task IDs
234
+ */
235
+ getRunningTasks(): string[] {
236
+ return Array.from(this.running.keys());
237
+ }
238
+
239
+ /**
240
+ * Handle process stdout/stderr and emit events
241
+ */
242
+ private handleProcessOutput(
243
+ taskId: string,
244
+ proc: ChildProcess,
245
+ rt: RunningTask
246
+ ): void {
247
+ // Create readline interface for NDJSON parsing
248
+ const rl = readline.createInterface({
249
+ input: proc.stdout!,
250
+ crlfDelay: Infinity
251
+ });
252
+
253
+ // Parse each line as JSON
254
+ rl.on('line', (line) => {
255
+ // Emit raw output for verbose mode
256
+ this.onEvent(taskId, 'OUTPUT', { output: line });
257
+
258
+ try {
259
+ const event = JSON.parse(line) as StreamEvent;
260
+ this.handleStreamEvent(taskId, event, rt);
261
+ } catch {
262
+ // Not valid JSON, skip
263
+ }
264
+ });
265
+
266
+ // Log stderr
267
+ proc.stderr?.on('data', (data) => {
268
+ console.log(`[${taskId}] stderr: ${data.toString()}`);
269
+ });
270
+
271
+ // Handle process exit
272
+ proc.on('close', (code) => {
273
+ console.log(`[${taskId}] Process exited with code ${code}`);
274
+
275
+ if (rt.timeoutHandle) {
276
+ clearTimeout(rt.timeoutHandle);
277
+ }
278
+ this.running.delete(taskId);
279
+ });
280
+
281
+ proc.on('error', (err) => {
282
+ console.error(`[${taskId}] Process error:`, err);
283
+ this.onEvent(taskId, 'ERROR', { error: err.message });
284
+
285
+ if (rt.timeoutHandle) {
286
+ clearTimeout(rt.timeoutHandle);
287
+ }
288
+ this.running.delete(taskId);
289
+ });
290
+ }
291
+
292
+ /**
293
+ * Handle a parsed stream event from Cursor CLI
294
+ */
295
+ private handleStreamEvent(
296
+ taskId: string,
297
+ event: StreamEvent,
298
+ rt: RunningTask
299
+ ): void {
300
+ switch (event.type) {
301
+ case 'start':
302
+ if (event.session_id) {
303
+ rt.sessionId = event.session_id;
304
+ console.log(`[${taskId}] Session initialized: ${event.session_id}`);
305
+ }
306
+ break;
307
+
308
+ case 'thinking':
309
+ // Accumulate thinking content for context
310
+ if (event.content) {
311
+ if (rt.context) {
312
+ rt.context += '\n\n';
313
+ }
314
+ rt.context += event.content;
315
+ }
316
+ break;
317
+
318
+ case 'action':
319
+ // Track action for progress
320
+ const progress = extractProgressFromAction(event);
321
+ if (progress) {
322
+ this.onEvent(taskId, 'PROGRESS', {
323
+ action: progress.action,
324
+ target: progress.target
325
+ });
326
+ }
327
+ break;
328
+
329
+ case 'approval_request':
330
+ // User input needed - pause and wait
331
+ console.log(`[${taskId}] Approval requested: ${event.action}`);
332
+ rt.question = event.action || 'Approve this action?';
333
+
334
+ this.onEvent(taskId, 'WAIT_FOR_USER', {
335
+ session_id: rt.sessionId,
336
+ prompt: rt.question,
337
+ options: [],
338
+ context: rt.context,
339
+ approval_details: {
340
+ action: event.action,
341
+ tool: event.tool,
342
+ file: event.file
343
+ }
344
+ });
345
+ break;
346
+
347
+ case 'action_complete':
348
+ // Action was approved and completed
349
+ console.log(`[${taskId}] Action complete: ${event.status}`);
350
+ break;
351
+
352
+ case 'result':
353
+ // Task completed
354
+ console.log(`[${taskId}] Task completed`);
355
+ this.onEvent(taskId, 'TASK_COMPLETE', {
356
+ session_id: rt.sessionId,
357
+ result: event.content || ''
358
+ });
359
+ break;
360
+
361
+ case 'error':
362
+ // Error occurred
363
+ console.error(`[${taskId}] Error: ${event.error}`);
364
+ this.onEvent(taskId, 'ERROR', {
365
+ error: event.error || 'Unknown error'
366
+ });
367
+ break;
368
+ }
369
+ }
370
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Types for Cursor CLI stream-json output
3
+ * Note: These are based on research and may need adjustment
4
+ * once we verify actual Cursor CLI output format
5
+ */
6
+
7
+ export interface StreamEvent {
8
+ type: 'start' | 'thinking' | 'action' | 'approval_request' | 'action_complete' | 'result' | 'error';
9
+ session_id?: string;
10
+ timestamp?: string;
11
+ content?: string;
12
+ tool?: string;
13
+ file?: string;
14
+ diff?: string;
15
+ action?: string;
16
+ status?: string;
17
+ error?: string;
18
+ }
19
+
20
+ export interface ApprovalRequest {
21
+ action: string;
22
+ tool: string;
23
+ file?: string;
24
+ command?: string;
25
+ description?: string;
26
+ }
27
+
28
+ export interface ProgressInfo {
29
+ action: string;
30
+ target: string;
31
+ }
32
+
33
+ /**
34
+ * Extract progress info from action event
35
+ */
36
+ export function extractProgressFromAction(event: StreamEvent): ProgressInfo | null {
37
+ if (event.type !== 'action' || !event.tool) {
38
+ return null;
39
+ }
40
+
41
+ switch (event.tool) {
42
+ case 'file_read':
43
+ return {
44
+ action: 'Reading',
45
+ target: event.file || 'file'
46
+ };
47
+ case 'file_write':
48
+ case 'file_edit':
49
+ return {
50
+ action: 'Editing',
51
+ target: event.file || 'file'
52
+ };
53
+ case 'shell':
54
+ case 'terminal':
55
+ const cmd = event.content || '';
56
+ return {
57
+ action: 'Running',
58
+ target: cmd.length > 30 ? cmd.substring(0, 30) + '...' : cmd
59
+ };
60
+ case 'search':
61
+ case 'grep':
62
+ return {
63
+ action: 'Searching',
64
+ target: event.content || 'files'
65
+ };
66
+ case 'web_search':
67
+ return {
68
+ action: 'Searching web',
69
+ target: event.content || ''
70
+ };
71
+ default:
72
+ return {
73
+ action: event.tool,
74
+ target: event.file || event.content || ''
75
+ };
76
+ }
77
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Message types for daemon <-> server communication
3
+ */
4
+
5
+ // Server -> Daemon messages
6
+
7
+ export interface PingMessage {
8
+ type: 'ping';
9
+ }
10
+
11
+ export interface TaskStartMessage {
12
+ type: 'task_start';
13
+ task_id: string;
14
+ instruction: string;
15
+ project_path?: string;
16
+ }
17
+
18
+ export interface TaskResumeMessage {
19
+ type: 'task_resume';
20
+ task_id: string;
21
+ session_id: string;
22
+ message: string;
23
+ project_path?: string;
24
+ }
25
+
26
+ export interface TaskCancelMessage {
27
+ type: 'task_cancel';
28
+ task_id: string;
29
+ }
30
+
31
+ export interface VersionStatusMessage {
32
+ type: 'version_status';
33
+ status: 'current' | 'update_available' | 'update_required';
34
+ your_version: string;
35
+ min_version?: string;
36
+ recommended_version?: string;
37
+ latest_version?: string;
38
+ changelog_url?: string;
39
+ message?: string;
40
+ }
41
+
42
+ export type ServerMessage =
43
+ | PingMessage
44
+ | TaskStartMessage
45
+ | TaskResumeMessage
46
+ | TaskCancelMessage
47
+ | VersionStatusMessage;
48
+
49
+ // Daemon -> Server messages
50
+
51
+ export interface PongMessage {
52
+ type: 'pong';
53
+ }
54
+
55
+ export interface StatusMessage {
56
+ type: 'status';
57
+ running_tasks: string[];
58
+ }
59
+
60
+ export interface EventMessage {
61
+ type: 'event';
62
+ task_id: string;
63
+ event_type: string;
64
+ [key: string]: unknown;
65
+ }
66
+
67
+ export type DaemonMessage = PongMessage | StatusMessage | EventMessage;
68
+
69
+ // Event types sent from daemon to server
70
+ export type EventType =
71
+ | 'WAIT_FOR_USER'
72
+ | 'TASK_COMPLETE'
73
+ | 'OUTPUT'
74
+ | 'PROGRESS'
75
+ | 'ERROR';