@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,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
|
+
}
|