@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,486 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { CURSOR_GLOBAL_STORAGE, CURSOR_WORKSPACE_STORAGE } from '../config/config';
|
|
5
|
+
|
|
6
|
+
export interface ComposerInfo {
|
|
7
|
+
composerId: string;
|
|
8
|
+
name: string;
|
|
9
|
+
createdAt: number;
|
|
10
|
+
lastUpdatedAt: number;
|
|
11
|
+
unifiedMode: string;
|
|
12
|
+
contextUsagePercent: number;
|
|
13
|
+
projectPath?: string; // Extracted from file context
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ComposerData {
|
|
17
|
+
allComposers: ComposerInfo[];
|
|
18
|
+
fullConversationHeadersOnly?: Array<{
|
|
19
|
+
bubbleId: string;
|
|
20
|
+
type: number; // 1 = user, 2 = assistant
|
|
21
|
+
}>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface BubbleData {
|
|
25
|
+
_v: number;
|
|
26
|
+
type: number; // 1 = user, 2 = assistant
|
|
27
|
+
bubbleId: string;
|
|
28
|
+
text: string;
|
|
29
|
+
createdAt: string;
|
|
30
|
+
tokenCount?: {
|
|
31
|
+
inputTokens: number;
|
|
32
|
+
outputTokens: number;
|
|
33
|
+
};
|
|
34
|
+
toolResults?: unknown[];
|
|
35
|
+
codebaseContextChunks?: unknown[];
|
|
36
|
+
allThinkingBlocks?: unknown[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface MessageEntry {
|
|
40
|
+
uuid: string;
|
|
41
|
+
role: 'USER' | 'AGENT';
|
|
42
|
+
content: string;
|
|
43
|
+
timestamp: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Cursor SQLite Database Reader
|
|
48
|
+
* Reads conversation data from Cursor's local storage
|
|
49
|
+
*/
|
|
50
|
+
export class CursorDB {
|
|
51
|
+
private globalDb: Database.Database | null = null;
|
|
52
|
+
private lastOpenAttempt = 0;
|
|
53
|
+
private lastRefresh = 0;
|
|
54
|
+
private readonly RETRY_INTERVAL = 5000; // 5 seconds between retries
|
|
55
|
+
private readonly REFRESH_INTERVAL = 2000; // 2s - refresh connection to see WAL updates
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Check if the Cursor database exists
|
|
59
|
+
*/
|
|
60
|
+
static exists(): boolean {
|
|
61
|
+
return fs.existsSync(CURSOR_GLOBAL_STORAGE);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Open the global storage database (read-only)
|
|
66
|
+
* Periodically refreshes the connection to see WAL updates from Cursor
|
|
67
|
+
*/
|
|
68
|
+
private openGlobalDb(): Database.Database | null {
|
|
69
|
+
const now = Date.now();
|
|
70
|
+
|
|
71
|
+
// If we have a connection but it's stale, refresh it to see WAL updates
|
|
72
|
+
if (this.globalDb && now - this.lastRefresh > this.REFRESH_INTERVAL) {
|
|
73
|
+
this.globalDb.close();
|
|
74
|
+
this.globalDb = null;
|
|
75
|
+
// Reset lastOpenAttempt so we don't hit the retry cooldown after refresh
|
|
76
|
+
this.lastOpenAttempt = 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (this.globalDb) {
|
|
80
|
+
return this.globalDb;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Don't retry too frequently after FAILED open attempts (not refreshes)
|
|
84
|
+
if (this.lastOpenAttempt > 0 && now - this.lastOpenAttempt < this.RETRY_INTERVAL) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!CursorDB.exists()) {
|
|
89
|
+
console.warn('[CursorDB] Database not found:', CURSOR_GLOBAL_STORAGE);
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
this.globalDb = new Database(CURSOR_GLOBAL_STORAGE, {
|
|
95
|
+
readonly: true,
|
|
96
|
+
fileMustExist: true,
|
|
97
|
+
});
|
|
98
|
+
// Force WAL checkpoint to see latest changes
|
|
99
|
+
try {
|
|
100
|
+
this.globalDb.pragma('wal_checkpoint(PASSIVE)');
|
|
101
|
+
} catch {
|
|
102
|
+
// Checkpoint may fail on readonly, that's ok
|
|
103
|
+
}
|
|
104
|
+
this.lastRefresh = now;
|
|
105
|
+
this.lastOpenAttempt = now;
|
|
106
|
+
return this.globalDb;
|
|
107
|
+
} catch (err) {
|
|
108
|
+
console.error('[CursorDB] Failed to open database:', err);
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Close database connections
|
|
115
|
+
*/
|
|
116
|
+
close(): void {
|
|
117
|
+
if (this.globalDb) {
|
|
118
|
+
this.globalDb.close();
|
|
119
|
+
this.globalDb = null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Extract project path from composer context
|
|
125
|
+
* Looks at fileSelections and folderSelections for path info
|
|
126
|
+
*/
|
|
127
|
+
private extractProjectPath(context: unknown): string | undefined {
|
|
128
|
+
if (!context || typeof context !== 'object') {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const ctx = context as Record<string, unknown>;
|
|
133
|
+
|
|
134
|
+
// Try folderSelections first
|
|
135
|
+
const folderSelections = ctx.folderSelections as Array<{ uri?: { fsPath?: string } }> | undefined;
|
|
136
|
+
if (folderSelections?.length) {
|
|
137
|
+
const firstFolder = folderSelections[0]?.uri?.fsPath;
|
|
138
|
+
if (firstFolder) {
|
|
139
|
+
return firstFolder;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Try fileSelections - extract directory from first file
|
|
144
|
+
const fileSelections = ctx.fileSelections as Array<{ uri?: { fsPath?: string } }> | undefined;
|
|
145
|
+
if (fileSelections?.length) {
|
|
146
|
+
const firstFile = fileSelections[0]?.uri?.fsPath;
|
|
147
|
+
if (firstFile) {
|
|
148
|
+
// Find a reasonable project root (go up to find common patterns)
|
|
149
|
+
return this.findProjectRoot(firstFile);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Find project root from a file path
|
|
158
|
+
* Looks for common project markers (package.json, .git, go.mod, etc.)
|
|
159
|
+
*/
|
|
160
|
+
private findProjectRoot(filePath: string): string {
|
|
161
|
+
let dir = path.dirname(filePath);
|
|
162
|
+
const markers = ['package.json', '.git', 'go.mod', 'Cargo.toml', 'pyproject.toml', 'pom.xml'];
|
|
163
|
+
|
|
164
|
+
// Walk up max 10 levels
|
|
165
|
+
for (let i = 0; i < 10 && dir !== '/'; i++) {
|
|
166
|
+
for (const marker of markers) {
|
|
167
|
+
if (fs.existsSync(path.join(dir, marker))) {
|
|
168
|
+
return dir;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
dir = path.dirname(dir);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// If no marker found, return the parent of the file
|
|
175
|
+
return path.dirname(filePath);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Get all composers (conversation sessions)
|
|
180
|
+
*/
|
|
181
|
+
getComposers(): ComposerInfo[] {
|
|
182
|
+
const db = this.openGlobalDb();
|
|
183
|
+
if (!db) return [];
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
const stmt = db.prepare(`
|
|
187
|
+
SELECT key, value FROM cursorDiskKV
|
|
188
|
+
WHERE key LIKE 'composerData:%'
|
|
189
|
+
`);
|
|
190
|
+
const rows = stmt.all() as Array<{ key: string; value: string | Buffer }>;
|
|
191
|
+
|
|
192
|
+
const allComposers: ComposerInfo[] = [];
|
|
193
|
+
for (const row of rows) {
|
|
194
|
+
try {
|
|
195
|
+
const valueStr = typeof row.value === 'string'
|
|
196
|
+
? row.value
|
|
197
|
+
: row.value.toString('utf-8');
|
|
198
|
+
const data = JSON.parse(valueStr);
|
|
199
|
+
|
|
200
|
+
// Each composerData:* key contains ONE composer directly
|
|
201
|
+
// Format: { _v, composerId, name, createdAt, lastUpdatedAt, context, ... }
|
|
202
|
+
if (data.composerId) {
|
|
203
|
+
const composerId = data.composerId;
|
|
204
|
+
const createdAt = data.createdAt || Date.now();
|
|
205
|
+
|
|
206
|
+
// Use lastUpdatedAt if present, otherwise fall back to createdAt
|
|
207
|
+
const lastUpdatedAt = data.lastUpdatedAt || createdAt;
|
|
208
|
+
|
|
209
|
+
// Try to extract project path from file context
|
|
210
|
+
const projectPath = this.extractProjectPath(data.context);
|
|
211
|
+
|
|
212
|
+
allComposers.push({
|
|
213
|
+
composerId,
|
|
214
|
+
name: data.name || 'Untitled Session',
|
|
215
|
+
createdAt,
|
|
216
|
+
lastUpdatedAt,
|
|
217
|
+
unifiedMode: data.unifiedMode || 'unknown',
|
|
218
|
+
contextUsagePercent: data.contextUsagePercent || 0,
|
|
219
|
+
projectPath,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
} catch {
|
|
223
|
+
// Skip malformed entries
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return allComposers.sort((a, b) => b.lastUpdatedAt - a.lastUpdatedAt);
|
|
228
|
+
} catch (err) {
|
|
229
|
+
console.error('[CursorDB] Error getting composers:', err);
|
|
230
|
+
return [];
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Get composer data including conversation headers
|
|
236
|
+
*/
|
|
237
|
+
getComposerData(composerId: string): ComposerData | null {
|
|
238
|
+
const db = this.openGlobalDb();
|
|
239
|
+
if (!db) return null;
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
const stmt = db.prepare(`
|
|
243
|
+
SELECT value FROM cursorDiskKV
|
|
244
|
+
WHERE key = ?
|
|
245
|
+
`);
|
|
246
|
+
const row = stmt.get(`composerData:${composerId}`) as { value: string | Buffer } | undefined;
|
|
247
|
+
|
|
248
|
+
if (!row) return null;
|
|
249
|
+
|
|
250
|
+
const valueStr = typeof row.value === 'string'
|
|
251
|
+
? row.value
|
|
252
|
+
: row.value.toString('utf-8');
|
|
253
|
+
return JSON.parse(valueStr) as ComposerData;
|
|
254
|
+
} catch (err) {
|
|
255
|
+
console.error('[CursorDB] Error getting composer data:', err);
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Get all bubbles (messages) for a composer
|
|
262
|
+
*/
|
|
263
|
+
getBubbles(composerId: string): BubbleData[] {
|
|
264
|
+
const db = this.openGlobalDb();
|
|
265
|
+
if (!db) return [];
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
const stmt = db.prepare(`
|
|
269
|
+
SELECT key, value FROM cursorDiskKV
|
|
270
|
+
WHERE key LIKE ?
|
|
271
|
+
`);
|
|
272
|
+
const pattern = `bubbleId:${composerId}:%`;
|
|
273
|
+
const rows = stmt.all(pattern) as Array<{ key: string; value: string | Buffer }>;
|
|
274
|
+
|
|
275
|
+
const bubbles: BubbleData[] = [];
|
|
276
|
+
for (const row of rows) {
|
|
277
|
+
try {
|
|
278
|
+
const valueStr = typeof row.value === 'string'
|
|
279
|
+
? row.value
|
|
280
|
+
: row.value.toString('utf-8');
|
|
281
|
+
const bubble = JSON.parse(valueStr) as BubbleData;
|
|
282
|
+
bubbles.push(bubble);
|
|
283
|
+
} catch {
|
|
284
|
+
// Skip malformed entries
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Sort by creation time
|
|
289
|
+
return bubbles.sort((a, b) =>
|
|
290
|
+
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
|
|
291
|
+
);
|
|
292
|
+
} catch (err) {
|
|
293
|
+
console.error('[CursorDB] Error getting bubbles:', err);
|
|
294
|
+
return [];
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Get the latest bubble for a composer (optimized - uses SQL sorting)
|
|
300
|
+
*/
|
|
301
|
+
getLatestBubble(composerId: string): BubbleData | null {
|
|
302
|
+
const db = this.openGlobalDb();
|
|
303
|
+
if (!db) return null;
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
// Use json_extract to sort by createdAt directly in SQL
|
|
307
|
+
const stmt = db.prepare(`
|
|
308
|
+
SELECT key, value FROM cursorDiskKV
|
|
309
|
+
WHERE key LIKE ?
|
|
310
|
+
ORDER BY json_extract(value, '$.createdAt') DESC
|
|
311
|
+
LIMIT 1
|
|
312
|
+
`);
|
|
313
|
+
const pattern = `bubbleId:${composerId}:%`;
|
|
314
|
+
const row = stmt.get(pattern) as { key: string; value: string | Buffer | null } | undefined;
|
|
315
|
+
|
|
316
|
+
if (!row || !row.value) return null;
|
|
317
|
+
|
|
318
|
+
const valueStr = typeof row.value === 'string'
|
|
319
|
+
? row.value
|
|
320
|
+
: row.value.toString('utf-8');
|
|
321
|
+
return JSON.parse(valueStr) as BubbleData;
|
|
322
|
+
} catch (err) {
|
|
323
|
+
console.error('[CursorDB] Error getting latest bubble:', err);
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Get messages for a session in a format compatible with CmdCtrl API
|
|
330
|
+
* @param composerId The composer/session ID
|
|
331
|
+
* @param limit Maximum number of messages to return
|
|
332
|
+
* @param beforeUuid Return messages before this UUID (for backward pagination)
|
|
333
|
+
* @param afterUuid Return messages after this UUID (for incremental/forward fetches)
|
|
334
|
+
*/
|
|
335
|
+
getMessages(composerId: string, limit = 30, beforeUuid?: string, afterUuid?: string): {
|
|
336
|
+
messages: MessageEntry[];
|
|
337
|
+
hasMore: boolean;
|
|
338
|
+
oldestUuid?: string;
|
|
339
|
+
newestUuid?: string;
|
|
340
|
+
} {
|
|
341
|
+
const bubbles = this.getBubbles(composerId);
|
|
342
|
+
|
|
343
|
+
// Handle afterUuid for incremental fetches (messages AFTER the given UUID)
|
|
344
|
+
if (afterUuid) {
|
|
345
|
+
const afterIdx = bubbles.findIndex(b => b.bubbleId === afterUuid);
|
|
346
|
+
if (afterIdx !== -1) {
|
|
347
|
+
// Get all bubbles after the cursor
|
|
348
|
+
const rawSlice = bubbles.slice(afterIdx + 1, afterIdx + 1 + limit);
|
|
349
|
+
|
|
350
|
+
// Filter out empty bubbles - Cursor creates entries BEFORE populating text
|
|
351
|
+
const slice = rawSlice.filter(b => b.text && b.text.trim().length > 0);
|
|
352
|
+
|
|
353
|
+
const messages: MessageEntry[] = slice.map(b => ({
|
|
354
|
+
uuid: b.bubbleId,
|
|
355
|
+
role: b.type === 1 ? 'USER' : 'AGENT',
|
|
356
|
+
content: b.text || '',
|
|
357
|
+
timestamp: b.createdAt,
|
|
358
|
+
}));
|
|
359
|
+
|
|
360
|
+
return {
|
|
361
|
+
messages,
|
|
362
|
+
hasMore: afterIdx + 1 + limit < bubbles.length,
|
|
363
|
+
oldestUuid: slice.length > 0 ? slice[0].bubbleId : undefined,
|
|
364
|
+
newestUuid: slice.length > 0 ? slice[slice.length - 1].bubbleId : undefined,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
// If afterUuid not found, fall through to return all messages
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Handle beforeUuid for backward pagination
|
|
371
|
+
let startIndex = bubbles.length;
|
|
372
|
+
if (beforeUuid) {
|
|
373
|
+
const idx = bubbles.findIndex(b => b.bubbleId === beforeUuid);
|
|
374
|
+
if (idx !== -1) {
|
|
375
|
+
startIndex = idx;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Get messages before the cursor, limited to `limit`
|
|
380
|
+
// Filter out empty bubbles (Cursor creates entries before text is populated)
|
|
381
|
+
const startFrom = Math.max(0, startIndex - limit);
|
|
382
|
+
const slice = bubbles
|
|
383
|
+
.slice(startFrom, startIndex)
|
|
384
|
+
.filter(b => b.text && b.text.trim().length > 0);
|
|
385
|
+
|
|
386
|
+
const messages: MessageEntry[] = slice.map(b => ({
|
|
387
|
+
uuid: b.bubbleId,
|
|
388
|
+
role: b.type === 1 ? 'USER' : 'AGENT',
|
|
389
|
+
content: b.text || '',
|
|
390
|
+
timestamp: b.createdAt,
|
|
391
|
+
}));
|
|
392
|
+
|
|
393
|
+
// Return oldest-first (chronological order) to match Claude Code daemon
|
|
394
|
+
return {
|
|
395
|
+
messages,
|
|
396
|
+
hasMore: startFrom > 0,
|
|
397
|
+
oldestUuid: slice.length > 0 ? slice[0].bubbleId : undefined,
|
|
398
|
+
newestUuid: slice.length > 0 ? slice[slice.length - 1].bubbleId : undefined,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Get the count of bubbles for a composer
|
|
404
|
+
*/
|
|
405
|
+
getBubbleCount(composerId: string): number {
|
|
406
|
+
const db = this.openGlobalDb();
|
|
407
|
+
if (!db) return 0;
|
|
408
|
+
|
|
409
|
+
try {
|
|
410
|
+
const stmt = db.prepare(`
|
|
411
|
+
SELECT COUNT(*) as count FROM cursorDiskKV
|
|
412
|
+
WHERE key LIKE ?
|
|
413
|
+
`);
|
|
414
|
+
const pattern = `bubbleId:${composerId}:%`;
|
|
415
|
+
const row = stmt.get(pattern) as { count: number };
|
|
416
|
+
return row.count;
|
|
417
|
+
} catch (err) {
|
|
418
|
+
console.error('[CursorDB] Error counting bubbles:', err);
|
|
419
|
+
return 0;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Get workspace storage paths that contain state.vscdb files
|
|
425
|
+
*/
|
|
426
|
+
getWorkspaceStoragePaths(): string[] {
|
|
427
|
+
const paths: string[] = [];
|
|
428
|
+
if (!fs.existsSync(CURSOR_WORKSPACE_STORAGE)) {
|
|
429
|
+
return paths;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
try {
|
|
433
|
+
const entries = fs.readdirSync(CURSOR_WORKSPACE_STORAGE);
|
|
434
|
+
for (const entry of entries) {
|
|
435
|
+
const dbPath = path.join(CURSOR_WORKSPACE_STORAGE, entry, 'state.vscdb');
|
|
436
|
+
if (fs.existsSync(dbPath)) {
|
|
437
|
+
paths.push(dbPath);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
} catch (err) {
|
|
441
|
+
console.error('[CursorDB] Error reading workspace storage:', err);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return paths;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Try to determine the project path for a workspace storage hash
|
|
449
|
+
* This is a best-effort attempt based on workspace.json if it exists
|
|
450
|
+
*/
|
|
451
|
+
getWorkspaceProjectPath(workspaceHash: string): string | null {
|
|
452
|
+
const workspaceJsonPath = path.join(
|
|
453
|
+
CURSOR_WORKSPACE_STORAGE,
|
|
454
|
+
workspaceHash,
|
|
455
|
+
'workspace.json'
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
if (!fs.existsSync(workspaceJsonPath)) {
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
try {
|
|
463
|
+
const content = fs.readFileSync(workspaceJsonPath, 'utf-8');
|
|
464
|
+
const data = JSON.parse(content);
|
|
465
|
+
// workspace.json typically contains a "folder" property with the URI
|
|
466
|
+
if (data.folder) {
|
|
467
|
+
// Convert file:// URI to path
|
|
468
|
+
return data.folder.replace('file://', '');
|
|
469
|
+
}
|
|
470
|
+
} catch {
|
|
471
|
+
// Ignore errors
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Singleton instance
|
|
479
|
+
let cursorDbInstance: CursorDB | null = null;
|
|
480
|
+
|
|
481
|
+
export function getCursorDB(): CursorDB {
|
|
482
|
+
if (!cursorDbInstance) {
|
|
483
|
+
cursorDbInstance = new CursorDB();
|
|
484
|
+
}
|
|
485
|
+
return cursorDbInstance;
|
|
486
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
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 GetMessagesMessage {
|
|
32
|
+
type: 'get_messages';
|
|
33
|
+
request_id: string;
|
|
34
|
+
session_id: string;
|
|
35
|
+
limit: number;
|
|
36
|
+
before_uuid?: string; // Cursor for backward pagination
|
|
37
|
+
after_uuid?: string; // Cursor for incremental/forward fetches
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface WatchSessionMessage {
|
|
41
|
+
type: 'watch_session';
|
|
42
|
+
session_id: string;
|
|
43
|
+
file_path: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface UnwatchSessionMessage {
|
|
47
|
+
type: 'unwatch_session';
|
|
48
|
+
session_id: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type ServerMessage =
|
|
52
|
+
| PingMessage
|
|
53
|
+
| TaskStartMessage
|
|
54
|
+
| TaskResumeMessage
|
|
55
|
+
| TaskCancelMessage
|
|
56
|
+
| GetMessagesMessage
|
|
57
|
+
| WatchSessionMessage
|
|
58
|
+
| UnwatchSessionMessage;
|
|
59
|
+
|
|
60
|
+
// Daemon -> Server messages
|
|
61
|
+
|
|
62
|
+
export interface PongMessage {
|
|
63
|
+
type: 'pong';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface StatusMessage {
|
|
67
|
+
type: 'status';
|
|
68
|
+
running_tasks: string[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface EventMessage {
|
|
72
|
+
type: 'event';
|
|
73
|
+
task_id: string;
|
|
74
|
+
event_type: string;
|
|
75
|
+
[key: string]: unknown;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface SessionInfo {
|
|
79
|
+
session_id: string;
|
|
80
|
+
slug: string;
|
|
81
|
+
title: string;
|
|
82
|
+
project: string;
|
|
83
|
+
project_name: string;
|
|
84
|
+
file_path: string;
|
|
85
|
+
last_message: string;
|
|
86
|
+
last_activity: string;
|
|
87
|
+
is_active: boolean;
|
|
88
|
+
message_count: number;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface ReportSessionsMessage {
|
|
92
|
+
type: 'report_sessions';
|
|
93
|
+
sessions: SessionInfo[];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface MessageEntry {
|
|
97
|
+
uuid: string;
|
|
98
|
+
role: 'USER' | 'AGENT';
|
|
99
|
+
content: string;
|
|
100
|
+
timestamp: string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface MessagesResponseMessage {
|
|
104
|
+
type: 'messages';
|
|
105
|
+
request_id: string;
|
|
106
|
+
session_id: string;
|
|
107
|
+
messages: MessageEntry[];
|
|
108
|
+
has_more: boolean;
|
|
109
|
+
oldest_uuid?: string;
|
|
110
|
+
newest_uuid?: string;
|
|
111
|
+
error?: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface SessionActivityMessage {
|
|
115
|
+
type: 'session_activity';
|
|
116
|
+
session_id: string;
|
|
117
|
+
file_path: string;
|
|
118
|
+
last_message: string;
|
|
119
|
+
message_count: number;
|
|
120
|
+
is_completion: boolean; // True when last message is from assistant
|
|
121
|
+
user_message_uuid?: string; // UUID of the triggering user message (for positioning verbose output)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export type DaemonMessage =
|
|
125
|
+
| PongMessage
|
|
126
|
+
| StatusMessage
|
|
127
|
+
| EventMessage
|
|
128
|
+
| ReportSessionsMessage
|
|
129
|
+
| MessagesResponseMessage
|
|
130
|
+
| SessionActivityMessage;
|
|
131
|
+
|
|
132
|
+
// Event types sent from daemon to server
|
|
133
|
+
export type EventType =
|
|
134
|
+
| 'WAIT_FOR_USER'
|
|
135
|
+
| 'TASK_COMPLETE'
|
|
136
|
+
| 'OUTPUT'
|
|
137
|
+
| 'PROGRESS'
|
|
138
|
+
| 'ERROR';
|