@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.
Files changed (62) hide show
  1. package/dist/adapter/cdp-client.d.ts +66 -0
  2. package/dist/adapter/cdp-client.d.ts.map +1 -0
  3. package/dist/adapter/cdp-client.js +304 -0
  4. package/dist/adapter/cdp-client.js.map +1 -0
  5. package/dist/adapter/cursor-db.d.ts +114 -0
  6. package/dist/adapter/cursor-db.d.ts.map +1 -0
  7. package/dist/adapter/cursor-db.js +438 -0
  8. package/dist/adapter/cursor-db.js.map +1 -0
  9. package/dist/client/messages.d.ts +98 -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 +103 -0
  14. package/dist/client/websocket.d.ts.map +1 -0
  15. package/dist/client/websocket.js +428 -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 +175 -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 +86 -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 +75 -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 +59 -0
  32. package/dist/commands/stop.js.map +1 -0
  33. package/dist/config/config.d.ts +68 -0
  34. package/dist/config/config.d.ts.map +1 -0
  35. package/dist/config/config.js +189 -0
  36. package/dist/config/config.js.map +1 -0
  37. package/dist/index.d.ts +3 -0
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +34 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/session-discovery.d.ts +22 -0
  42. package/dist/session-discovery.d.ts.map +1 -0
  43. package/dist/session-discovery.js +90 -0
  44. package/dist/session-discovery.js.map +1 -0
  45. package/dist/session-watcher.d.ts +62 -0
  46. package/dist/session-watcher.d.ts.map +1 -0
  47. package/dist/session-watcher.js +210 -0
  48. package/dist/session-watcher.js.map +1 -0
  49. package/package.json +40 -0
  50. package/src/adapter/cdp-client.ts +296 -0
  51. package/src/adapter/cursor-db.ts +486 -0
  52. package/src/client/messages.ts +138 -0
  53. package/src/client/websocket.ts +486 -0
  54. package/src/commands/register.ts +201 -0
  55. package/src/commands/start.ts +106 -0
  56. package/src/commands/status.ts +83 -0
  57. package/src/commands/stop.ts +58 -0
  58. package/src/config/config.ts +167 -0
  59. package/src/index.ts +39 -0
  60. package/src/session-discovery.ts +115 -0
  61. package/src/session-watcher.ts +253 -0
  62. package/tsconfig.json +19 -0
@@ -0,0 +1,296 @@
1
+ import WebSocket from 'ws';
2
+ import * as http from 'http';
3
+ import { CDP_URL } from '../config/config';
4
+
5
+ export interface CDPTarget {
6
+ id: string;
7
+ title: string;
8
+ url: string;
9
+ webSocketDebuggerUrl: string;
10
+ }
11
+
12
+ /**
13
+ * Chrome DevTools Protocol client for Cursor IDE
14
+ * Connects to Cursor's remote debugging port and allows sending messages
15
+ */
16
+ export class CDPClient {
17
+ private ws: WebSocket | null = null;
18
+ private msgId = 1;
19
+ private pendingRequests = new Map<number, {
20
+ resolve: (value: unknown) => void;
21
+ reject: (reason: unknown) => void;
22
+ }>();
23
+ private reconnecting = false;
24
+
25
+ /**
26
+ * Check if CDP is available (Cursor running with --remote-debugging-port)
27
+ */
28
+ async isAvailable(): Promise<boolean> {
29
+ try {
30
+ const targets = await this.getTargets();
31
+ return targets.length > 0;
32
+ } catch {
33
+ return false;
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Get available CDP targets (Cursor windows/pages)
39
+ */
40
+ async getTargets(): Promise<CDPTarget[]> {
41
+ return new Promise((resolve, reject) => {
42
+ const req = http.get(`${CDP_URL}/json`, (res) => {
43
+ let data = '';
44
+ res.on('data', (chunk) => { data += chunk; });
45
+ res.on('end', () => {
46
+ try {
47
+ const targets = JSON.parse(data) as CDPTarget[];
48
+ resolve(targets);
49
+ } catch (err) {
50
+ reject(new Error(`Failed to parse CDP targets: ${err}`));
51
+ }
52
+ });
53
+ });
54
+ req.on('error', (err) => {
55
+ reject(new Error(`CDP not available: ${err.message}`));
56
+ });
57
+ req.setTimeout(3000, () => {
58
+ req.destroy();
59
+ reject(new Error('CDP connection timeout'));
60
+ });
61
+ });
62
+ }
63
+
64
+ /**
65
+ * Connect to Cursor via CDP WebSocket
66
+ */
67
+ async connect(): Promise<void> {
68
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
69
+ return; // Already connected
70
+ }
71
+
72
+ const targets = await this.getTargets();
73
+ if (targets.length === 0) {
74
+ throw new Error('No CDP targets available. Is Cursor running with --remote-debugging-port=9222?');
75
+ }
76
+
77
+ // Find the main Cursor window (usually the workbench)
78
+ const target = targets.find(t => t.url.includes('workbench.html')) || targets[0];
79
+
80
+ return new Promise((resolve, reject) => {
81
+ this.ws = new WebSocket(target.webSocketDebuggerUrl);
82
+
83
+ this.ws.on('open', async () => {
84
+ console.log('[CDP] Connected to Cursor');
85
+ // Enable required domains
86
+ await this.sendCommand('Runtime.enable');
87
+ resolve();
88
+ });
89
+
90
+ this.ws.on('message', (data) => {
91
+ try {
92
+ const msg = JSON.parse(data.toString());
93
+ if (msg.id !== undefined && this.pendingRequests.has(msg.id)) {
94
+ const pending = this.pendingRequests.get(msg.id)!;
95
+ this.pendingRequests.delete(msg.id);
96
+ if (msg.error) {
97
+ pending.reject(new Error(msg.error.message));
98
+ } else {
99
+ pending.resolve(msg.result);
100
+ }
101
+ }
102
+ // Events (no id) are ignored for now
103
+ } catch {
104
+ // Ignore parse errors
105
+ }
106
+ });
107
+
108
+ this.ws.on('error', (err) => {
109
+ console.error('[CDP] WebSocket error:', err.message);
110
+ reject(err);
111
+ });
112
+
113
+ this.ws.on('close', () => {
114
+ console.log('[CDP] Disconnected from Cursor');
115
+ this.ws = null;
116
+ this.pendingRequests.clear();
117
+ });
118
+ });
119
+ }
120
+
121
+ /**
122
+ * Disconnect from CDP
123
+ */
124
+ disconnect(): void {
125
+ if (this.ws) {
126
+ this.ws.close();
127
+ this.ws = null;
128
+ }
129
+ this.pendingRequests.clear();
130
+ }
131
+
132
+ /**
133
+ * Check if connected to CDP
134
+ */
135
+ isConnected(): boolean {
136
+ return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
137
+ }
138
+
139
+ /**
140
+ * Send a CDP command and wait for response
141
+ */
142
+ private async sendCommand(method: string, params?: Record<string, unknown>): Promise<unknown> {
143
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
144
+ throw new Error('CDP not connected');
145
+ }
146
+
147
+ const id = this.msgId++;
148
+ const msg = { id, method, params };
149
+
150
+ return new Promise((resolve, reject) => {
151
+ this.pendingRequests.set(id, { resolve, reject });
152
+ this.ws!.send(JSON.stringify(msg));
153
+
154
+ // Timeout after 10 seconds
155
+ setTimeout(() => {
156
+ if (this.pendingRequests.has(id)) {
157
+ this.pendingRequests.delete(id);
158
+ reject(new Error(`CDP command timeout: ${method}`));
159
+ }
160
+ }, 10000);
161
+ });
162
+ }
163
+
164
+ /**
165
+ * Evaluate JavaScript in the Cursor page context
166
+ */
167
+ async evaluate(expression: string): Promise<unknown> {
168
+ const result = await this.sendCommand('Runtime.evaluate', {
169
+ expression,
170
+ returnByValue: true,
171
+ }) as { result?: { value?: unknown } };
172
+ return result?.result?.value;
173
+ }
174
+
175
+ /**
176
+ * Send a message to Cursor's chat composer
177
+ * 1. Focus the composer input
178
+ * 2. Clear any existing text
179
+ * 3. Insert the new message
180
+ * 4. Press Enter to submit
181
+ */
182
+ async sendMessage(text: string): Promise<boolean> {
183
+ if (!this.isConnected()) {
184
+ await this.connect();
185
+ }
186
+
187
+ try {
188
+ // Focus the composer input
189
+ const focusResult = await this.evaluate(`
190
+ (function() {
191
+ const input = document.querySelector('.aislash-editor-input');
192
+ if (!input) return { success: false, error: 'Composer input not found' };
193
+ input.focus();
194
+ // Select all to clear existing text
195
+ const sel = window.getSelection();
196
+ if (sel) sel.selectAllChildren(input);
197
+ return { success: true };
198
+ })()
199
+ `) as { success: boolean; error?: string };
200
+
201
+ if (!focusResult?.success) {
202
+ console.error('[CDP] Failed to focus composer:', focusResult?.error);
203
+ return false;
204
+ }
205
+
206
+ // Insert the text
207
+ await this.sendCommand('Input.insertText', { text });
208
+
209
+ // Press Enter to submit
210
+ await this.sendCommand('Input.dispatchKeyEvent', {
211
+ type: 'keyDown',
212
+ key: 'Enter',
213
+ code: 'Enter',
214
+ windowsVirtualKeyCode: 13,
215
+ nativeVirtualKeyCode: 13,
216
+ });
217
+ await this.sendCommand('Input.dispatchKeyEvent', {
218
+ type: 'keyUp',
219
+ key: 'Enter',
220
+ code: 'Enter',
221
+ });
222
+
223
+ console.log(`[CDP] Message sent: ${text.substring(0, 50)}...`);
224
+ return true;
225
+ } catch (err) {
226
+ console.error('[CDP] Failed to send message:', err);
227
+ return false;
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Check if the composer panel is open
233
+ */
234
+ async isComposerOpen(): Promise<boolean> {
235
+ if (!this.isConnected()) {
236
+ return false;
237
+ }
238
+
239
+ try {
240
+ const result = await this.evaluate(`
241
+ (function() {
242
+ const input = document.querySelector('.aislash-editor-input');
243
+ return input !== null;
244
+ })()
245
+ `);
246
+ return result === true;
247
+ } catch {
248
+ return false;
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Toggle the composer panel open/closed (Cmd+I)
254
+ */
255
+ async toggleComposer(): Promise<void> {
256
+ if (!this.isConnected()) {
257
+ await this.connect();
258
+ }
259
+
260
+ // Simulate Cmd+I
261
+ await this.sendCommand('Input.dispatchKeyEvent', {
262
+ type: 'keyDown',
263
+ key: 'i',
264
+ code: 'KeyI',
265
+ modifiers: 4, // Meta/Cmd
266
+ });
267
+ await this.sendCommand('Input.dispatchKeyEvent', {
268
+ type: 'keyUp',
269
+ key: 'i',
270
+ code: 'KeyI',
271
+ });
272
+ }
273
+
274
+ /**
275
+ * Get the current Cursor window title (useful for detecting active project)
276
+ */
277
+ async getWindowTitle(): Promise<string | null> {
278
+ try {
279
+ const targets = await this.getTargets();
280
+ const target = targets.find(t => t.url.includes('workbench.html'));
281
+ return target?.title || null;
282
+ } catch {
283
+ return null;
284
+ }
285
+ }
286
+ }
287
+
288
+ // Singleton instance
289
+ let cdpClientInstance: CDPClient | null = null;
290
+
291
+ export function getCDPClient(): CDPClient {
292
+ if (!cdpClientInstance) {
293
+ cdpClientInstance = new CDPClient();
294
+ }
295
+ return cdpClientInstance;
296
+ }