@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,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';