@dynamicu/chromedebug-mcp 2.2.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 (95) hide show
  1. package/CLAUDE.md +344 -0
  2. package/LICENSE +21 -0
  3. package/README.md +250 -0
  4. package/chrome-extension/README.md +41 -0
  5. package/chrome-extension/background.js +3917 -0
  6. package/chrome-extension/chrome-session-manager.js +706 -0
  7. package/chrome-extension/content.css +181 -0
  8. package/chrome-extension/content.js +3022 -0
  9. package/chrome-extension/data-buffer.js +435 -0
  10. package/chrome-extension/dom-tracker.js +411 -0
  11. package/chrome-extension/extension-config.js +78 -0
  12. package/chrome-extension/firebase-client.js +278 -0
  13. package/chrome-extension/firebase-config.js +32 -0
  14. package/chrome-extension/firebase-config.module.js +22 -0
  15. package/chrome-extension/firebase-config.module.template.js +27 -0
  16. package/chrome-extension/firebase-config.template.js +36 -0
  17. package/chrome-extension/frame-capture.js +407 -0
  18. package/chrome-extension/icon128.png +1 -0
  19. package/chrome-extension/icon16.png +1 -0
  20. package/chrome-extension/icon48.png +1 -0
  21. package/chrome-extension/license-helper.js +181 -0
  22. package/chrome-extension/logger.js +23 -0
  23. package/chrome-extension/manifest.json +73 -0
  24. package/chrome-extension/network-tracker.js +510 -0
  25. package/chrome-extension/offscreen.html +10 -0
  26. package/chrome-extension/options.html +203 -0
  27. package/chrome-extension/options.js +282 -0
  28. package/chrome-extension/pako.min.js +2 -0
  29. package/chrome-extension/performance-monitor.js +533 -0
  30. package/chrome-extension/pii-redactor.js +405 -0
  31. package/chrome-extension/popup.html +532 -0
  32. package/chrome-extension/popup.js +2446 -0
  33. package/chrome-extension/upload-manager.js +323 -0
  34. package/chrome-extension/web-vitals.iife.js +1 -0
  35. package/config/api-keys.json +11 -0
  36. package/config/chrome-pilot-config.json +45 -0
  37. package/package.json +126 -0
  38. package/scripts/cleanup-processes.js +109 -0
  39. package/scripts/config-manager.js +280 -0
  40. package/scripts/generate-extension-config.js +53 -0
  41. package/scripts/setup-security.js +64 -0
  42. package/src/capture/architecture.js +426 -0
  43. package/src/capture/error-handling-tests.md +38 -0
  44. package/src/capture/error-handling-types.ts +360 -0
  45. package/src/capture/index.js +508 -0
  46. package/src/capture/interfaces.js +625 -0
  47. package/src/capture/memory-manager.js +713 -0
  48. package/src/capture/types.js +342 -0
  49. package/src/chrome-controller.js +2658 -0
  50. package/src/cli.js +19 -0
  51. package/src/config-loader.js +303 -0
  52. package/src/database.js +2178 -0
  53. package/src/firebase-license-manager.js +462 -0
  54. package/src/firebase-privacy-guard.js +397 -0
  55. package/src/http-server.js +1516 -0
  56. package/src/index-direct.js +157 -0
  57. package/src/index-modular.js +219 -0
  58. package/src/index-monolithic-backup.js +2230 -0
  59. package/src/index.js +305 -0
  60. package/src/legacy/chrome-controller-old.js +1406 -0
  61. package/src/legacy/index-express.js +625 -0
  62. package/src/legacy/index-old.js +977 -0
  63. package/src/legacy/routes.js +260 -0
  64. package/src/legacy/shared-storage.js +101 -0
  65. package/src/logger.js +10 -0
  66. package/src/mcp/handlers/chrome-tool-handler.js +306 -0
  67. package/src/mcp/handlers/element-tool-handler.js +51 -0
  68. package/src/mcp/handlers/frame-tool-handler.js +957 -0
  69. package/src/mcp/handlers/request-handler.js +104 -0
  70. package/src/mcp/handlers/workflow-tool-handler.js +636 -0
  71. package/src/mcp/server.js +68 -0
  72. package/src/mcp/tools/index.js +701 -0
  73. package/src/middleware/auth.js +371 -0
  74. package/src/middleware/security.js +267 -0
  75. package/src/port-discovery.js +258 -0
  76. package/src/routes/admin.js +182 -0
  77. package/src/services/browser-daemon.js +494 -0
  78. package/src/services/chrome-service.js +375 -0
  79. package/src/services/failover-manager.js +412 -0
  80. package/src/services/git-safety-service.js +675 -0
  81. package/src/services/heartbeat-manager.js +200 -0
  82. package/src/services/http-client.js +195 -0
  83. package/src/services/process-manager.js +318 -0
  84. package/src/services/process-tracker.js +574 -0
  85. package/src/services/profile-manager.js +449 -0
  86. package/src/services/project-manager.js +415 -0
  87. package/src/services/session-manager.js +497 -0
  88. package/src/services/session-registry.js +491 -0
  89. package/src/services/unified-session-manager.js +678 -0
  90. package/src/shared-storage-old.js +267 -0
  91. package/src/standalone-server.js +53 -0
  92. package/src/utils/extension-path.js +145 -0
  93. package/src/utils.js +187 -0
  94. package/src/validation/log-transformer.js +125 -0
  95. package/src/validation/schemas.js +391 -0
@@ -0,0 +1,2178 @@
1
+ // SQLite database module for ChromeDebug MCP recordings
2
+ import Database from 'better-sqlite3';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import fs from 'fs';
6
+ import { ProjectManager } from './services/project-manager.js';
7
+
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
+
10
+ // Initialize project manager for path resolution
11
+ const projectManager = new ProjectManager();
12
+
13
+ /**
14
+ * Gets the appropriate database path (always use global chromedebug.db)
15
+ * @returns {string} Database file path
16
+ */
17
+ function getDatabasePath() {
18
+ // Always use global database for cross-project accessibility
19
+ const globalDbPath = path.join(__dirname, '../data/chromedebug.db');
20
+
21
+ // Ensure data directory exists
22
+ const dbDir = path.dirname(globalDbPath);
23
+ if (!fs.existsSync(dbDir)) {
24
+ fs.mkdirSync(dbDir, { recursive: true });
25
+ }
26
+
27
+ console.log(`[Database] Using global database: ${globalDbPath}`);
28
+ return globalDbPath;
29
+ }
30
+
31
+ // Get the database path (project-local or global)
32
+ const DB_PATH = getDatabasePath();
33
+
34
+ class ChromePilotDatabase {
35
+ constructor() {
36
+ this.db = null;
37
+ this.initialized = false;
38
+ }
39
+
40
+ init() {
41
+ if (this.initialized) return;
42
+
43
+ // Ensure data directory exists
44
+ const dataDir = path.dirname(DB_PATH);
45
+ if (!fs.existsSync(dataDir)) {
46
+ fs.mkdirSync(dataDir, { recursive: true });
47
+ }
48
+
49
+ this.db = new Database(DB_PATH);
50
+ this.db.pragma('journal_mode = WAL'); // Enable WAL mode for better concurrent access
51
+
52
+ // Create tables
53
+ this.createTables();
54
+ this.initialized = true;
55
+ console.log(`ChromeDebug MCP database initialized at: ${DB_PATH}`);
56
+ }
57
+
58
+ createTables() {
59
+ // Recordings table for frame capture sessions
60
+ this.db.exec(`
61
+ CREATE TABLE IF NOT EXISTS recordings (
62
+ id TEXT PRIMARY KEY,
63
+ session_id TEXT NOT NULL,
64
+ type TEXT NOT NULL DEFAULT 'frame_capture',
65
+ timestamp INTEGER NOT NULL,
66
+ total_frames INTEGER DEFAULT 0,
67
+ name TEXT,
68
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
69
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
70
+ )
71
+ `);
72
+
73
+ // Frames table to store individual frames
74
+ this.db.exec(`
75
+ CREATE TABLE IF NOT EXISTS frames (
76
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
77
+ recording_id TEXT NOT NULL,
78
+ frame_index INTEGER NOT NULL,
79
+ timestamp INTEGER NOT NULL,
80
+ absolute_timestamp INTEGER NOT NULL,
81
+ image_data TEXT NOT NULL,
82
+ interactions TEXT DEFAULT '[]',
83
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
84
+ FOREIGN KEY (recording_id) REFERENCES recordings (id) ON DELETE CASCADE,
85
+ UNIQUE(recording_id, frame_index)
86
+ )
87
+ `);
88
+
89
+ // Add interactions column to existing frames table if it doesn't exist
90
+ try {
91
+ this.db.exec(`ALTER TABLE frames ADD COLUMN interactions TEXT DEFAULT '[]'`);
92
+ console.log('[Database] Added interactions column to frames table');
93
+ } catch (error) {
94
+ // Column already exists, ignore error
95
+ if (!error.message.includes('duplicate column name')) {
96
+ console.warn('[Database] Unexpected error adding interactions column:', error.message);
97
+ }
98
+ }
99
+
100
+ // Console logs table
101
+ this.db.exec(`
102
+ CREATE TABLE IF NOT EXISTS console_logs (
103
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
104
+ frame_id INTEGER NOT NULL,
105
+ level TEXT NOT NULL,
106
+ message TEXT NOT NULL,
107
+ relative_time INTEGER DEFAULT 0,
108
+ sequence_number INTEGER DEFAULT 0,
109
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
110
+ FOREIGN KEY (frame_id) REFERENCES frames (id) ON DELETE CASCADE
111
+ )
112
+ `);
113
+
114
+ // Deferred logs table for logs that arrive before frames are created
115
+ this.db.exec(`
116
+ CREATE TABLE IF NOT EXISTS deferred_logs (
117
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
118
+ session_id TEXT NOT NULL,
119
+ level TEXT NOT NULL,
120
+ message TEXT NOT NULL,
121
+ timestamp INTEGER NOT NULL,
122
+ sequence_number INTEGER NOT NULL,
123
+ args TEXT,
124
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
125
+ )
126
+ `);
127
+
128
+ // Screen interactions table for recording user interactions during screen recordings
129
+ this.db.exec(`
130
+ CREATE TABLE IF NOT EXISTS screen_interactions (
131
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
132
+ recording_id TEXT NOT NULL,
133
+ interaction_index INTEGER NOT NULL,
134
+ type TEXT NOT NULL,
135
+ selector TEXT,
136
+ xpath TEXT,
137
+ x INTEGER,
138
+ y INTEGER,
139
+ value TEXT,
140
+ text TEXT,
141
+ key TEXT,
142
+ timestamp INTEGER NOT NULL,
143
+ frame_index INTEGER,
144
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
145
+ FOREIGN KEY (recording_id) REFERENCES recordings (id) ON DELETE CASCADE,
146
+ UNIQUE(recording_id, interaction_index)
147
+ )
148
+ `);
149
+
150
+ // Workflow recordings table
151
+ this.db.exec(`
152
+ CREATE TABLE IF NOT EXISTS workflow_recordings (
153
+ id TEXT PRIMARY KEY,
154
+ session_id TEXT NOT NULL,
155
+ name TEXT,
156
+ url TEXT NOT NULL,
157
+ title TEXT,
158
+ timestamp INTEGER NOT NULL,
159
+ total_actions INTEGER DEFAULT 0,
160
+ include_logs BOOLEAN DEFAULT FALSE,
161
+ screenshot_settings TEXT,
162
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
163
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
164
+ )
165
+ `);
166
+
167
+ // Workflow actions table
168
+ this.db.exec(`
169
+ CREATE TABLE IF NOT EXISTS workflow_actions (
170
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
171
+ workflow_id TEXT NOT NULL,
172
+ action_index INTEGER NOT NULL,
173
+ type TEXT NOT NULL,
174
+ selector TEXT NOT NULL,
175
+ x INTEGER,
176
+ y INTEGER,
177
+ value TEXT,
178
+ text TEXT,
179
+ placeholder TEXT,
180
+ timestamp INTEGER NOT NULL,
181
+ screenshot_data TEXT,
182
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
183
+ FOREIGN KEY (workflow_id) REFERENCES workflow_recordings (id) ON DELETE CASCADE,
184
+ UNIQUE(workflow_id, action_index)
185
+ )
186
+ `);
187
+
188
+ // Workflow logs table
189
+ this.db.exec(`
190
+ CREATE TABLE IF NOT EXISTS workflow_logs (
191
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
192
+ workflow_id TEXT NOT NULL,
193
+ level TEXT NOT NULL,
194
+ message TEXT NOT NULL,
195
+ timestamp INTEGER NOT NULL,
196
+ relative_time INTEGER DEFAULT 0,
197
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
198
+ FOREIGN KEY (workflow_id) REFERENCES workflow_recordings (id) ON DELETE CASCADE
199
+ )
200
+ `);
201
+
202
+ // Add columns for function trace data if they don't exist
203
+ try {
204
+ // Check if component column exists
205
+ const columns = this.db.pragma('table_info(workflow_actions)');
206
+ const hasComponent = columns.some(col => col.name === 'component');
207
+ const hasArgs = columns.some(col => col.name === 'args');
208
+ const hasStack = columns.some(col => col.name === 'stack');
209
+ const hasElementHtml = columns.some(col => col.name === 'element_html');
210
+ const hasComponentData = columns.some(col => col.name === 'component_data');
211
+ const hasEventHandlers = columns.some(col => col.name === 'event_handlers');
212
+ const hasElementState = columns.some(col => col.name === 'element_state');
213
+ const hasPerformanceMetrics = columns.some(col => col.name === 'performance_metrics');
214
+
215
+ if (!hasComponent) {
216
+ this.db.exec('ALTER TABLE workflow_actions ADD COLUMN component TEXT');
217
+ console.log('[Database] Added component column to workflow_actions');
218
+ }
219
+ if (!hasArgs) {
220
+ this.db.exec('ALTER TABLE workflow_actions ADD COLUMN args TEXT');
221
+ console.log('[Database] Added args column to workflow_actions');
222
+ }
223
+ if (!hasStack) {
224
+ this.db.exec('ALTER TABLE workflow_actions ADD COLUMN stack TEXT');
225
+ console.log('[Database] Added stack column to workflow_actions');
226
+ }
227
+
228
+ // Add enhanced click tracking columns
229
+ if (!hasElementHtml) {
230
+ this.db.exec('ALTER TABLE workflow_actions ADD COLUMN element_html TEXT');
231
+ console.log('[Database] Added element_html column to workflow_actions');
232
+ }
233
+ if (!hasComponentData) {
234
+ this.db.exec('ALTER TABLE workflow_actions ADD COLUMN component_data TEXT');
235
+ console.log('[Database] Added component_data column to workflow_actions');
236
+ }
237
+ if (!hasEventHandlers) {
238
+ this.db.exec('ALTER TABLE workflow_actions ADD COLUMN event_handlers TEXT');
239
+ console.log('[Database] Added event_handlers column to workflow_actions');
240
+ }
241
+ if (!hasElementState) {
242
+ this.db.exec('ALTER TABLE workflow_actions ADD COLUMN element_state TEXT');
243
+ console.log('[Database] Added element_state column to workflow_actions');
244
+ }
245
+ if (!hasPerformanceMetrics) {
246
+ this.db.exec('ALTER TABLE workflow_actions ADD COLUMN performance_metrics TEXT');
247
+ console.log('[Database] Added performance_metrics column to workflow_actions');
248
+ }
249
+ } catch (error) {
250
+ // Columns might already exist, that's ok
251
+ console.log('[Database] Function trace columns already exist or migration failed:', error.message);
252
+ }
253
+
254
+ // Add enhanced columns to screen_interactions table
255
+ try {
256
+ const screenInteractionsColumns = this.db.pragma('table_info(screen_interactions)');
257
+ const hasScreenElementHtml = screenInteractionsColumns.some(col => col.name === 'element_html');
258
+ const hasScreenComponentData = screenInteractionsColumns.some(col => col.name === 'component_data');
259
+ const hasScreenEventHandlers = screenInteractionsColumns.some(col => col.name === 'event_handlers');
260
+ const hasScreenElementState = screenInteractionsColumns.some(col => col.name === 'element_state');
261
+ const hasScreenPerformanceMetrics = screenInteractionsColumns.some(col => col.name === 'performance_metrics');
262
+
263
+ // Add enhanced click tracking columns to screen_interactions
264
+ if (!hasScreenElementHtml) {
265
+ this.db.exec('ALTER TABLE screen_interactions ADD COLUMN element_html TEXT');
266
+ console.log('[Database] Added element_html column to screen_interactions');
267
+ }
268
+ if (!hasScreenComponentData) {
269
+ this.db.exec('ALTER TABLE screen_interactions ADD COLUMN component_data TEXT');
270
+ console.log('[Database] Added component_data column to screen_interactions');
271
+ }
272
+ if (!hasScreenEventHandlers) {
273
+ this.db.exec('ALTER TABLE screen_interactions ADD COLUMN event_handlers TEXT');
274
+ console.log('[Database] Added event_handlers column to screen_interactions');
275
+ }
276
+ if (!hasScreenElementState) {
277
+ this.db.exec('ALTER TABLE screen_interactions ADD COLUMN element_state TEXT');
278
+ console.log('[Database] Added element_state column to screen_interactions');
279
+ }
280
+ if (!hasScreenPerformanceMetrics) {
281
+ this.db.exec('ALTER TABLE screen_interactions ADD COLUMN performance_metrics TEXT');
282
+ console.log('[Database] Added performance_metrics column to screen_interactions');
283
+ }
284
+ } catch (error) {
285
+ // Columns might already exist, that's ok
286
+ console.log('[Database] Screen interactions enhanced columns already exist or migration failed:', error.message);
287
+ }
288
+
289
+ // Restore points table
290
+ this.db.exec(`
291
+ CREATE TABLE IF NOT EXISTS restore_points (
292
+ id TEXT PRIMARY KEY,
293
+ workflow_id TEXT NOT NULL,
294
+ action_index INTEGER,
295
+ timestamp INTEGER NOT NULL,
296
+ url TEXT NOT NULL,
297
+ title TEXT,
298
+ dom_snapshot TEXT NOT NULL,
299
+ scroll_x INTEGER DEFAULT 0,
300
+ scroll_y INTEGER DEFAULT 0,
301
+ local_storage TEXT,
302
+ session_storage TEXT,
303
+ cookies TEXT,
304
+ console_logs TEXT,
305
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
306
+ FOREIGN KEY (workflow_id) REFERENCES workflow_recordings (id) ON DELETE CASCADE
307
+ )
308
+ `);
309
+
310
+ // Server instances table to track single-server mode
311
+ this.db.exec(`
312
+ CREATE TABLE IF NOT EXISTS server_instances (
313
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
314
+ pid INTEGER NOT NULL,
315
+ mode TEXT NOT NULL,
316
+ started_at DATETIME DEFAULT CURRENT_TIMESTAMP,
317
+ UNIQUE(pid)
318
+ )
319
+ `);
320
+
321
+ // Full data recording configuration table
322
+ this.db.exec(`
323
+ CREATE TABLE IF NOT EXISTS full_recording_configs (
324
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
325
+ recording_id TEXT NOT NULL,
326
+ instrumentation_level INTEGER DEFAULT 0,
327
+ capture_console BOOLEAN DEFAULT TRUE,
328
+ capture_dom BOOLEAN DEFAULT FALSE,
329
+ capture_functions BOOLEAN DEFAULT FALSE,
330
+ capture_variables BOOLEAN DEFAULT FALSE,
331
+ capture_network BOOLEAN DEFAULT FALSE,
332
+ performance_threshold_ms INTEGER DEFAULT 10,
333
+ max_data_size_mb INTEGER DEFAULT 100,
334
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
335
+ FOREIGN KEY (recording_id) REFERENCES recordings (id) ON DELETE CASCADE
336
+ )
337
+ `);
338
+
339
+ // Execution traces table for function call tracking
340
+ this.db.exec(`
341
+ CREATE TABLE IF NOT EXISTS execution_traces (
342
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
343
+ recording_id TEXT NOT NULL,
344
+ trace_index INTEGER NOT NULL,
345
+ timestamp INTEGER NOT NULL,
346
+ function_name TEXT,
347
+ function_source TEXT,
348
+ arguments TEXT,
349
+ return_value TEXT,
350
+ error TEXT,
351
+ call_stack TEXT,
352
+ execution_time_ms INTEGER,
353
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
354
+ FOREIGN KEY (recording_id) REFERENCES recordings (id) ON DELETE CASCADE,
355
+ UNIQUE(recording_id, trace_index)
356
+ )
357
+ `);
358
+
359
+ // Variable states table for runtime variable tracking
360
+ this.db.exec(`
361
+ CREATE TABLE IF NOT EXISTS variable_states (
362
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
363
+ recording_id TEXT NOT NULL,
364
+ timestamp INTEGER NOT NULL,
365
+ scope_chain TEXT,
366
+ variables TEXT,
367
+ heap_snapshot BLOB,
368
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
369
+ FOREIGN KEY (recording_id) REFERENCES recordings (id) ON DELETE CASCADE
370
+ )
371
+ `);
372
+
373
+ // DOM mutations table for DOM change tracking
374
+ this.db.exec(`
375
+ CREATE TABLE IF NOT EXISTS dom_mutations (
376
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
377
+ recording_id TEXT NOT NULL,
378
+ timestamp INTEGER NOT NULL,
379
+ mutation_type TEXT,
380
+ target_selector TEXT,
381
+ target_xpath TEXT,
382
+ old_value TEXT,
383
+ new_value TEXT,
384
+ attributes_changed TEXT,
385
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
386
+ FOREIGN KEY (recording_id) REFERENCES recordings (id) ON DELETE CASCADE
387
+ )
388
+ `);
389
+
390
+ // Network requests table for API tracking
391
+ this.db.exec(`
392
+ CREATE TABLE IF NOT EXISTS network_requests (
393
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
394
+ recording_id TEXT NOT NULL,
395
+ timestamp INTEGER NOT NULL,
396
+ request_id TEXT,
397
+ method TEXT,
398
+ url TEXT,
399
+ headers TEXT,
400
+ body TEXT,
401
+ response_status INTEGER,
402
+ response_headers TEXT,
403
+ response_body TEXT,
404
+ duration_ms INTEGER,
405
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
406
+ FOREIGN KEY (recording_id) REFERENCES recordings (id) ON DELETE CASCADE
407
+ )
408
+ `);
409
+
410
+ // Performance metrics table
411
+ this.db.exec(`
412
+ CREATE TABLE IF NOT EXISTS performance_metrics (
413
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
414
+ recording_id TEXT NOT NULL,
415
+ timestamp INTEGER NOT NULL,
416
+ metric_type TEXT,
417
+ metric_name TEXT,
418
+ metric_value REAL,
419
+ metadata TEXT,
420
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
421
+ FOREIGN KEY (recording_id) REFERENCES recordings (id) ON DELETE CASCADE
422
+ )
423
+ `);
424
+
425
+ // Create indexes for better performance
426
+ this.db.exec(`
427
+ CREATE INDEX IF NOT EXISTS idx_recordings_session_id ON recordings (session_id);
428
+ CREATE INDEX IF NOT EXISTS idx_recordings_timestamp ON recordings (timestamp);
429
+ CREATE INDEX IF NOT EXISTS idx_frames_recording_id ON frames (recording_id);
430
+ CREATE INDEX IF NOT EXISTS idx_frames_frame_index ON frames (recording_id, frame_index);
431
+ CREATE INDEX IF NOT EXISTS idx_console_logs_frame_id ON console_logs (frame_id);
432
+ CREATE INDEX IF NOT EXISTS idx_screen_interactions_recording_id ON screen_interactions (recording_id);
433
+ CREATE INDEX IF NOT EXISTS idx_screen_interactions_timestamp ON screen_interactions (timestamp);
434
+ CREATE INDEX IF NOT EXISTS idx_screen_interactions_frame_index ON screen_interactions (recording_id, frame_index);
435
+ CREATE INDEX IF NOT EXISTS idx_workflow_recordings_session_id ON workflow_recordings (session_id);
436
+ CREATE INDEX IF NOT EXISTS idx_workflow_recordings_timestamp ON workflow_recordings (timestamp);
437
+ CREATE INDEX IF NOT EXISTS idx_workflow_actions_workflow_id ON workflow_actions (workflow_id);
438
+ CREATE INDEX IF NOT EXISTS idx_workflow_actions_action_index ON workflow_actions (workflow_id, action_index);
439
+ CREATE INDEX IF NOT EXISTS idx_workflow_logs_workflow_id ON workflow_logs (workflow_id);
440
+ CREATE INDEX IF NOT EXISTS idx_restore_points_workflow_id ON restore_points (workflow_id);
441
+ CREATE INDEX IF NOT EXISTS idx_restore_points_timestamp ON restore_points (timestamp);
442
+
443
+ -- Indexes for new full recording tables
444
+ CREATE INDEX IF NOT EXISTS idx_full_recording_configs_recording_id ON full_recording_configs (recording_id);
445
+ CREATE INDEX IF NOT EXISTS idx_execution_traces_recording_id ON execution_traces (recording_id, timestamp);
446
+ CREATE INDEX IF NOT EXISTS idx_execution_traces_function ON execution_traces (function_name);
447
+ CREATE INDEX IF NOT EXISTS idx_variable_states_recording_id ON variable_states (recording_id, timestamp);
448
+ CREATE INDEX IF NOT EXISTS idx_dom_mutations_recording_id ON dom_mutations (recording_id, timestamp);
449
+ CREATE INDEX IF NOT EXISTS idx_network_requests_recording_id ON network_requests (recording_id, timestamp);
450
+ CREATE INDEX IF NOT EXISTS idx_performance_metrics_recording_id ON performance_metrics (recording_id, timestamp);
451
+ `);
452
+
453
+ // Run migrations to update existing databases
454
+ this.runMigrations();
455
+ }
456
+
457
+ runMigrations() {
458
+ // Check if workflow_recordings table has name column
459
+ const columns = this.db.pragma(`table_info(workflow_recordings)`);
460
+ const hasNameColumn = columns.some(col => col.name === 'name');
461
+ const hasScreenshotSettings = columns.some(col => col.name === 'screenshot_settings');
462
+
463
+ if (!hasNameColumn) {
464
+ console.log('[Database] Migrating workflow_recordings table to add name column');
465
+ this.db.exec(`ALTER TABLE workflow_recordings ADD COLUMN name TEXT`);
466
+ }
467
+
468
+ if (!hasScreenshotSettings) {
469
+ console.log('[Database] Migrating workflow_recordings table to add screenshot_settings column');
470
+ this.db.exec(`ALTER TABLE workflow_recordings ADD COLUMN screenshot_settings TEXT`);
471
+ }
472
+
473
+ // Check if workflow_actions table has screenshot_data column
474
+ const actionColumns = this.db.pragma(`table_info(workflow_actions)`);
475
+ const hasScreenshotData = actionColumns.some(col => col.name === 'screenshot_data');
476
+
477
+ if (!hasScreenshotData) {
478
+ console.log('[Database] Migrating workflow_actions table to add screenshot_data column');
479
+ this.db.exec(`ALTER TABLE workflow_actions ADD COLUMN screenshot_data TEXT`);
480
+ }
481
+
482
+ // Check if console_logs table has sequence_number column
483
+ const logColumns = this.db.pragma(`table_info(console_logs)`);
484
+ const hasSequenceNumber = logColumns.some(col => col.name === 'sequence_number');
485
+
486
+ if (!hasSequenceNumber) {
487
+ console.log('[Database] Migrating console_logs table to add sequence_number column');
488
+ this.db.exec(`ALTER TABLE console_logs ADD COLUMN sequence_number INTEGER DEFAULT 0`);
489
+ }
490
+
491
+ // Migration for snapshot feature support
492
+ const recordingsColumns = this.db.pragma(`table_info(recordings)`);
493
+ const hasRecordingType = recordingsColumns.some(col => col.name === 'recording_type');
494
+ const hasUserNote = recordingsColumns.some(col => col.name === 'user_note');
495
+ const hasName = recordingsColumns.some(col => col.name === 'name');
496
+
497
+ if (!hasRecordingType) {
498
+ console.log('[Database] Migrating recordings table to add recording_type column');
499
+ this.db.exec(`ALTER TABLE recordings ADD COLUMN recording_type TEXT DEFAULT 'continuous'`);
500
+ }
501
+
502
+ if (!hasUserNote) {
503
+ console.log('[Database] Migrating recordings table to add user_note column');
504
+ this.db.exec(`ALTER TABLE recordings ADD COLUMN user_note TEXT`);
505
+ }
506
+
507
+ if (!hasName) {
508
+ console.log('[Database] Migrating recordings table to add name column');
509
+ this.db.exec(`ALTER TABLE recordings ADD COLUMN name TEXT`);
510
+ }
511
+ }
512
+
513
+ // Store a recording session
514
+ storeRecording(sessionId, type = 'frame_capture', recordingType = 'continuous', userNote = null, name = null) {
515
+ this.init();
516
+
517
+ const stmt = this.db.prepare(`
518
+ INSERT OR REPLACE INTO recordings (id, session_id, type, timestamp, updated_at, recording_type, user_note, name)
519
+ VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP, ?, ?, ?)
520
+ `);
521
+
522
+ // Use base ID instead of prefixed ID
523
+ const recordingId = sessionId;
524
+ const timestamp = Date.now();
525
+
526
+ console.log(`[Database] Storing recording: ${recordingId} for session: ${sessionId}, type: ${recordingType}, name: ${name || 'none'}`);
527
+ const result = stmt.run(recordingId, sessionId, type, timestamp, recordingType, userNote, name);
528
+ console.log(`[Database] Recording stored with changes: ${result.changes}, lastInsertRowid: ${result.lastInsertRowid}`);
529
+
530
+ return recordingId;
531
+ }
532
+
533
+ /*
534
+ * SNAPSHOT FEATURE DISABLED (2025-10-01)
535
+ *
536
+ * Snapshot database methods disabled - see SNAPSHOT_FEATURE_DISABLED.md
537
+ *
538
+ * WHY DISABLED:
539
+ * - Snapshots without console logs are just screenshots (users can do this natively)
540
+ * - Console log capture requires always-on monitoring (privacy concern)
541
+ * - Core value proposition (screenshot + searchable logs) cannot be achieved cleanly
542
+ *
543
+ * NOTE: Database schema (recording_type column) is preserved to maintain data integrity.
544
+ * Existing snapshot data in the database is not deleted.
545
+ *
546
+ * TO RE-ENABLE:
547
+ * 1. Implement privacy-conscious always-on log monitoring
548
+ * 2. Uncomment these methods
549
+ * 3. Re-enable chrome-controller.js methods
550
+ * 4. Re-enable HTTP endpoint in http-server.js
551
+ * 5. Re-enable extension UI and handlers
552
+ */
553
+ /*
554
+ storeSnapshot(sessionId, frameData, userNote = null) {
555
+ this.init();
556
+
557
+ // Create the recording as a snapshot type
558
+ const recordingId = this.storeRecording(sessionId, 'frame_capture', 'snapshot', userNote);
559
+
560
+ // Store the single frame
561
+ if (frameData) {
562
+ this.storeFrameBatch(sessionId, [frameData]);
563
+ }
564
+
565
+ console.log(`[Database] Snapshot stored: ${recordingId} with note: ${userNote || 'none'}`);
566
+ return recordingId;
567
+ }
568
+
569
+ getSnapshots(limit = 50) {
570
+ this.init();
571
+
572
+ const stmt = this.db.prepare(`
573
+ SELECT r.*, COUNT(f.id) as frame_count
574
+ FROM recordings r
575
+ LEFT JOIN frames f ON f.recording_id = r.id
576
+ WHERE r.recording_type = 'snapshot'
577
+ GROUP BY r.id
578
+ ORDER BY r.timestamp DESC
579
+ LIMIT ?
580
+ `);
581
+
582
+ return stmt.all(limit);
583
+ }
584
+ */
585
+
586
+ // Store frame batch
587
+ storeFrameBatch(sessionId, frames, name = null) {
588
+ this.init();
589
+
590
+ // Use base ID instead of prefixed ID
591
+ const recordingId = sessionId;
592
+
593
+ console.log(`[storeFrameBatch] DEATH SPIRAL FIX: Processing ${frames.length} frames for session ${sessionId}`);
594
+
595
+ // Ensure recording exists (but don't overwrite existing)
596
+ const existingRecording = this.getRecording(sessionId);
597
+ if (!existingRecording) {
598
+ this.storeRecording(sessionId, 'frame_capture', 'continuous', null, name);
599
+ } else {
600
+ console.log(`[storeFrameBatch] Using existing recording: ${existingRecording.id}`);
601
+ // Update name if provided and different
602
+ if (name && name !== existingRecording.name) {
603
+ const updateStmt = this.db.prepare(`UPDATE recordings SET name = ? WHERE id = ?`);
604
+ updateStmt.run(name, sessionId);
605
+ console.log(`[storeFrameBatch] Updated recording name to: ${name}`);
606
+ }
607
+ }
608
+
609
+ // Check existing frame count BEFORE insertion
610
+ const preCountStmt = this.db.prepare(`SELECT COUNT(*) as count FROM frames WHERE recording_id = ?`);
611
+ const preCount = preCountStmt.get(recordingId).count;
612
+ console.log(`[storeFrameBatch] PRE-INSERT: ${preCount} existing frames in database`);
613
+
614
+ // Use INSERT OR REPLACE instead of INSERT OR IGNORE to handle updates
615
+ const frameStmt = this.db.prepare(`
616
+ INSERT OR REPLACE INTO frames (recording_id, frame_index, timestamp, absolute_timestamp, image_data, interactions)
617
+ VALUES (?, ?, ?, ?, ?, ?)
618
+ `);
619
+
620
+ const transaction = this.db.transaction((frames) => {
621
+ let insertedCount = 0;
622
+ let skippedCount = 0;
623
+
624
+ for (let i = 0; i < frames.length; i++) {
625
+ const frame = frames[i];
626
+ // Use frame.index from extension (properly tracked per session), fallback to i for backward compatibility
627
+ const frameIndex = frame.index !== undefined ? frame.index : i;
628
+
629
+ console.log(`[storeFrameBatch] Inserting frame ${frameIndex}: timestamp=${frame.timestamp}, absoluteTimestamp=${frame.absoluteTimestamp}`);
630
+
631
+ try {
632
+ // Serialize interactions as JSON
633
+ const interactionsJson = JSON.stringify(frame.interactions || []);
634
+
635
+ const result = frameStmt.run(
636
+ recordingId,
637
+ frameIndex,
638
+ frame.timestamp || Date.now(),
639
+ frame.absoluteTimestamp || Date.now(),
640
+ frame.imageData,
641
+ interactionsJson
642
+ );
643
+
644
+ if (result.changes > 0) {
645
+ insertedCount++;
646
+ console.log(`[storeFrameBatch] SUCCESS: Frame ${frameIndex} inserted/updated`);
647
+ } else {
648
+ skippedCount++;
649
+ console.log(`[storeFrameBatch] WARNING: Frame ${frameIndex} not changed`);
650
+ }
651
+ } catch (error) {
652
+ console.error(`[storeFrameBatch] ERROR inserting frame ${frameIndex}:`, error.message);
653
+ throw error; // Fail fast on SQL errors
654
+ }
655
+ }
656
+
657
+ console.log(`[storeFrameBatch] Transaction complete: ${insertedCount} inserted, ${skippedCount} skipped`);
658
+
659
+ // Update total frames count
660
+ const updateStmt = this.db.prepare(`
661
+ UPDATE recordings
662
+ SET total_frames = (SELECT COUNT(*) FROM frames WHERE recording_id = ?),
663
+ updated_at = CURRENT_TIMESTAMP
664
+ WHERE id = ?
665
+ `);
666
+ updateStmt.run(recordingId, recordingId);
667
+
668
+ // Debug: Check the actual count after update
669
+ const finalCountStmt = this.db.prepare(`SELECT COUNT(*) as count FROM frames WHERE recording_id = ?`);
670
+ const finalCount = finalCountStmt.get(recordingId).count;
671
+ console.log(`[storeFrameBatch] FINAL: ${finalCount} frames in database (was ${preCount})`);
672
+
673
+ return { insertedCount, skippedCount, finalCount };
674
+ });
675
+
676
+ const result = transaction(frames);
677
+ console.log(`[storeFrameBatch] DEATH SPIRAL RESOLVED: ${result.insertedCount}/${frames.length} frames stored successfully`);
678
+
679
+ // Verify what was actually stored
680
+ const verifyStmt = this.db.prepare(`
681
+ SELECT frame_index, timestamp FROM frames
682
+ WHERE recording_id = ?
683
+ ORDER BY frame_index ASC
684
+ `);
685
+ const storedFrames = verifyStmt.all(recordingId);
686
+ console.log(`[storeFrameBatch] VERIFICATION: ${storedFrames.length} frames with indices: [${storedFrames.map(f => f.frame_index).join(', ')}]`);
687
+
688
+ if (storedFrames.length === 0) {
689
+ console.error(`[storeFrameBatch] DEATH SPIRAL DETECTED: NO FRAMES STORED! Check SQL constraints and frame data.`);
690
+ throw new Error(`Frame storage failed: 0 frames stored for session ${sessionId}`);
691
+ }
692
+
693
+ return this.getRecording(recordingId);
694
+ }
695
+
696
+ // Associate logs with frames
697
+ associateLogsWithFrames(sessionId, logs) {
698
+ this.init();
699
+
700
+ // Use base ID instead of prefixed ID
701
+ const recordingId = sessionId;
702
+
703
+ // Clear existing logs for this recording
704
+ const clearLogsStmt = this.db.prepare(`
705
+ DELETE FROM console_logs
706
+ WHERE frame_id IN (SELECT id FROM frames WHERE recording_id = ?)
707
+ `);
708
+ clearLogsStmt.run(recordingId);
709
+
710
+ // Get all frames sorted by timestamp
711
+ const framesStmt = this.db.prepare(`
712
+ SELECT id, frame_index, absolute_timestamp
713
+ FROM frames
714
+ WHERE recording_id = ?
715
+ ORDER BY absolute_timestamp ASC
716
+ `);
717
+ const frames = framesStmt.all(recordingId);
718
+
719
+ console.log(`[associateLogsWithFrames] Found ${frames.length} frames for log association`);
720
+ console.log(`[associateLogsWithFrames] Frame indices found: [${frames.map(f => f.frame_index).join(', ')}]`);
721
+
722
+ // Debug: Check total frames before processing
723
+ const countStmt = this.db.prepare(`SELECT COUNT(*) as count FROM frames WHERE recording_id = ?`);
724
+ const frameCount = countStmt.get(recordingId).count;
725
+ console.log(`[associateLogsWithFrames] Total frames in database before log association: ${frameCount}`);
726
+
727
+ if (frames.length === 0) {
728
+ return { success: false, message: 'No frames found for session' };
729
+ }
730
+
731
+ // Sort logs by timestamp
732
+ logs.sort((a, b) => a.timestamp - b.timestamp);
733
+
734
+ const logStmt = this.db.prepare(`
735
+ INSERT INTO console_logs (frame_id, level, message, relative_time)
736
+ VALUES (?, ?, ?, ?)
737
+ `);
738
+
739
+ const transaction = this.db.transaction((logs, frames) => {
740
+ let associatedCount = 0;
741
+ let frameBucketIndex = 0;
742
+
743
+ for (const log of logs) {
744
+ // Find the correct frame bucket for this log
745
+ while (frameBucketIndex < frames.length &&
746
+ frames[frameBucketIndex].absolute_timestamp < log.timestamp) {
747
+ frameBucketIndex++;
748
+ }
749
+
750
+ if (frameBucketIndex < frames.length) {
751
+ const frame = frames[frameBucketIndex];
752
+ const relativeTime = log.timestamp - frame.absolute_timestamp;
753
+
754
+ // Truncate large log messages before storing
755
+ let message = log.message;
756
+ const maxMessageLength = 10000; // 10KB limit per log entry
757
+
758
+ if (message && message.length > maxMessageLength) {
759
+ // Check if it's a base64 image that was already truncated by the extension
760
+ if (message.includes('[Base64 Image:') && message.includes('truncated...]')) {
761
+ // Already truncated by extension, keep as is
762
+ } else {
763
+ // Additional truncation for any other large content
764
+ message = message.substring(0, maxMessageLength) + `... [DB TRUNCATED: ${message.length} total bytes]`;
765
+ }
766
+ }
767
+
768
+ logStmt.run(frame.id, log.level, message, relativeTime);
769
+ associatedCount++;
770
+ }
771
+ }
772
+
773
+ return associatedCount;
774
+ });
775
+
776
+ const associatedCount = transaction(logs, frames);
777
+
778
+ console.log(`Associated ${associatedCount} of ${logs.length} logs with frames`);
779
+
780
+ // Debug: Check total frames after processing
781
+ const finalFrameCount = countStmt.get(recordingId).count;
782
+ console.log(`[associateLogsWithFrames] Total frames in database after log association: ${finalFrameCount}`);
783
+
784
+ return {
785
+ success: true,
786
+ sessionId: sessionId,
787
+ logsReceived: logs.length,
788
+ logsAssociated: associatedCount
789
+ };
790
+ }
791
+
792
+ // Stream logs to frames - optimized for real-time batched streaming
793
+ streamLogsToFrames(sessionId, logs) {
794
+ this.init();
795
+
796
+ // Use base ID instead of prefixed ID
797
+ const recordingId = sessionId;
798
+
799
+ console.log(`[streamLogsToFrames] Processing ${logs.length} logs for session: ${sessionId}`);
800
+
801
+ // Get all frames sorted by timestamp for efficient association
802
+ const framesStmt = this.db.prepare(`
803
+ SELECT id, frame_index, absolute_timestamp
804
+ FROM frames
805
+ WHERE recording_id = ?
806
+ ORDER BY absolute_timestamp ASC
807
+ `);
808
+ const frames = framesStmt.all(recordingId);
809
+
810
+ if (frames.length === 0) {
811
+ console.log(`[streamLogsToFrames] No frames found for session: ${sessionId}, storing ${logs.length} logs as deferred`);
812
+ return this.storeDeferredLogs(sessionId, logs);
813
+ }
814
+
815
+ // Sort logs by sequence number for proper ordering
816
+ logs.sort((a, b) => a.sequence - b.sequence);
817
+
818
+ const logStmt = this.db.prepare(`
819
+ INSERT INTO console_logs (frame_id, level, message, relative_time, sequence_number)
820
+ VALUES (?, ?, ?, ?, ?)
821
+ `);
822
+
823
+ const transaction = this.db.transaction((logs, frames) => {
824
+ let associatedCount = 0;
825
+
826
+ for (const log of logs) {
827
+ // Find the best frame for this log using binary search approach
828
+ let targetFrame = null;
829
+ let minTimeDiff = Infinity;
830
+
831
+ // For each log, find the frame with the closest timestamp
832
+ for (let i = 0; i < frames.length; i++) {
833
+ const frame = frames[i];
834
+ const timeDiff = Math.abs(log.timestamp - frame.absolute_timestamp);
835
+
836
+ // If this frame is closer than previous best, use it
837
+ if (timeDiff < minTimeDiff) {
838
+ minTimeDiff = timeDiff;
839
+ targetFrame = frame;
840
+ }
841
+
842
+ // If we found a frame that's very close (within 50ms), use it
843
+ if (timeDiff <= 50) {
844
+ break;
845
+ }
846
+ }
847
+
848
+ // Fallback: if no close frame found, use temporal ordering
849
+ if (!targetFrame || minTimeDiff > 1000) {
850
+ // Find frame that occurs before or at the log timestamp
851
+ for (let i = frames.length - 1; i >= 0; i--) {
852
+ if (frames[i].absolute_timestamp <= log.timestamp) {
853
+ targetFrame = frames[i];
854
+ break;
855
+ }
856
+ }
857
+
858
+ // If still no frame, use the first available frame
859
+ if (!targetFrame && frames.length > 0) {
860
+ targetFrame = frames[0];
861
+ }
862
+ }
863
+
864
+ if (targetFrame) {
865
+ const relativeTime = log.timestamp - targetFrame.absolute_timestamp;
866
+
867
+ // Truncate message if too long to prevent DB issues
868
+ let message = log.message;
869
+ const maxMessageLength = 50000; // 50KB limit
870
+ if (message && message.length > maxMessageLength) {
871
+ message = message.substring(0, maxMessageLength) + `... [STREAM TRUNCATED: ${message.length} total bytes]`;
872
+ }
873
+
874
+ // Insert with sequence number for ordering
875
+ logStmt.run(targetFrame.id, log.level, message, relativeTime, log.sequence);
876
+ associatedCount++;
877
+ }
878
+ }
879
+
880
+ return associatedCount;
881
+ });
882
+
883
+ const associatedCount = transaction(logs, frames);
884
+
885
+ console.log(`[streamLogsToFrames] Streamed ${associatedCount} of ${logs.length} logs to frames`);
886
+
887
+ return {
888
+ success: true,
889
+ sessionId: sessionId,
890
+ logsReceived: logs.length,
891
+ logsStreamed: associatedCount,
892
+ method: 'stream'
893
+ };
894
+ }
895
+
896
+ // Store logs temporarily when frames don't exist yet
897
+ storeDeferredLogs(sessionId, logs) {
898
+ this.init();
899
+
900
+ console.log(`[storeDeferredLogs] Storing ${logs.length} deferred logs for session: ${sessionId}`);
901
+
902
+ const deferredLogStmt = this.db.prepare(`
903
+ INSERT INTO deferred_logs (session_id, level, message, timestamp, sequence_number, args)
904
+ VALUES (?, ?, ?, ?, ?, ?)
905
+ `);
906
+
907
+ const transaction = this.db.transaction((logs) => {
908
+ let stored = 0;
909
+ for (const log of logs) {
910
+ try {
911
+ deferredLogStmt.run(
912
+ sessionId,
913
+ log.level,
914
+ log.message,
915
+ log.timestamp,
916
+ log.sequence || 0,
917
+ log.args ? JSON.stringify(log.args) : null
918
+ );
919
+ stored++;
920
+ } catch (error) {
921
+ console.error(`[storeDeferredLogs] Failed to store deferred log:`, error, log);
922
+ }
923
+ }
924
+ return stored;
925
+ });
926
+
927
+ const storedCount = transaction(logs);
928
+
929
+ console.log(`[storeDeferredLogs] Stored ${storedCount} of ${logs.length} deferred logs`);
930
+
931
+ return {
932
+ success: true,
933
+ sessionId: sessionId,
934
+ logsReceived: logs.length,
935
+ logsDeferred: storedCount,
936
+ method: 'deferred'
937
+ };
938
+ }
939
+
940
+ // Associate deferred logs with frames when frames become available
941
+ associateDeferredLogs(sessionId) {
942
+ this.init();
943
+
944
+ console.log(`[associateDeferredLogs] Checking for deferred logs for session: ${sessionId}`);
945
+
946
+ // Get deferred logs for this session
947
+ const deferredLogsStmt = this.db.prepare(`
948
+ SELECT level, message, timestamp, sequence_number, args
949
+ FROM deferred_logs
950
+ WHERE session_id = ?
951
+ ORDER BY sequence_number ASC
952
+ `);
953
+ const deferredLogs = deferredLogsStmt.all(sessionId);
954
+
955
+ if (deferredLogs.length === 0) {
956
+ console.log(`[associateDeferredLogs] No deferred logs found for session: ${sessionId}`);
957
+ return { success: true, logsAssociated: 0 };
958
+ }
959
+
960
+ console.log(`[associateDeferredLogs] Found ${deferredLogs.length} deferred logs, attempting to associate with frames`);
961
+
962
+ // Convert deferred logs back to the format expected by streamLogsToFrames
963
+ const logs = deferredLogs.map(log => ({
964
+ level: log.level,
965
+ message: log.message,
966
+ timestamp: log.timestamp,
967
+ sequence: log.sequence_number,
968
+ args: log.args ? JSON.parse(log.args) : []
969
+ }));
970
+
971
+ // Try to associate with frames (this will work if frames now exist)
972
+ const result = this.streamLogsToFrames(sessionId, logs);
973
+
974
+ if (result.success && result.method !== 'deferred') {
975
+ // Successfully associated - clean up deferred logs
976
+ const cleanupStmt = this.db.prepare(`DELETE FROM deferred_logs WHERE session_id = ?`);
977
+ cleanupStmt.run(sessionId);
978
+ console.log(`[associateDeferredLogs] Successfully associated ${result.logsStreamed} logs and cleaned up deferred storage`);
979
+
980
+ return {
981
+ success: true,
982
+ logsAssociated: result.logsStreamed,
983
+ logsDeferred: deferredLogs.length
984
+ };
985
+ }
986
+
987
+ console.log(`[associateDeferredLogs] Frames still not available for session: ${sessionId}`);
988
+ return { success: false, message: 'Frames still not available' };
989
+ }
990
+
991
+ // Get recording by ID
992
+ getRecording(recordingId) {
993
+ this.init();
994
+
995
+ console.log(`[getRecording] Looking for recording with ID: ${recordingId}`);
996
+
997
+ // Handle both old format (frame_<id>) and new format (base id)
998
+ let baseId = recordingId;
999
+ if (recordingId.startsWith('frame_')) {
1000
+ baseId = recordingId.substring(6); // Remove 'frame_' prefix
1001
+ console.log(`[getRecording] Converting old format ID ${recordingId} to base ID: ${baseId}`);
1002
+ }
1003
+
1004
+ // Priority order: exact id match, then session_id match with frames, then any session_id match
1005
+ // Check both the original recordingId and the baseId to handle dual compatibility
1006
+ const stmt = this.db.prepare(`
1007
+ SELECT * FROM recordings
1008
+ WHERE id = ? OR id = ? OR session_id = ? OR session_id = ?
1009
+ ORDER BY
1010
+ CASE
1011
+ WHEN id = ? THEN 1
1012
+ WHEN id = ? THEN 2
1013
+ WHEN session_id = ? THEN 3
1014
+ ELSE 4
1015
+ END,
1016
+ total_frames DESC,
1017
+ created_at DESC
1018
+ LIMIT 1
1019
+ `);
1020
+ const recording = stmt.get(recordingId, baseId, recordingId, baseId, recordingId, baseId, recordingId);
1021
+
1022
+ if (recording) {
1023
+ console.log(`[getRecording] Found recording with ID: ${recording.id}, total_frames: ${recording.total_frames}`);
1024
+ return recording;
1025
+ }
1026
+
1027
+ console.log(`[getRecording] No recording found for: ${recordingId} or base ID: ${baseId}`);
1028
+ return null;
1029
+ }
1030
+
1031
+ // Get frame session info
1032
+ getFrameSessionInfo(sessionId) {
1033
+ this.init();
1034
+
1035
+ const recording = this.getRecording(sessionId);
1036
+ if (!recording) return null;
1037
+
1038
+ const frameTimestampsStmt = this.db.prepare(`
1039
+ SELECT timestamp FROM frames
1040
+ WHERE recording_id = ?
1041
+ ORDER BY frame_index ASC
1042
+ `);
1043
+ const frameTimestamps = frameTimestampsStmt.all(recording.id).map(f => f.timestamp);
1044
+
1045
+ return {
1046
+ sessionId: recording.session_id,
1047
+ totalFrames: recording.total_frames,
1048
+ timestamp: recording.timestamp,
1049
+ frameTimestamps: frameTimestamps
1050
+ };
1051
+ }
1052
+
1053
+ // Get specific frame
1054
+ getFrame(sessionId, frameIndex) {
1055
+ this.init();
1056
+
1057
+ const recording = this.getRecording(sessionId);
1058
+ if (!recording) return null;
1059
+
1060
+ const frameStmt = this.db.prepare(`
1061
+ SELECT * FROM frames
1062
+ WHERE recording_id = ? AND frame_index = ?
1063
+ `);
1064
+ const frame = frameStmt.get(recording.id, frameIndex);
1065
+
1066
+ if (!frame) return null;
1067
+
1068
+ // Get console logs for this frame
1069
+ const logsStmt = this.db.prepare(`
1070
+ SELECT level, message, relative_time
1071
+ FROM console_logs
1072
+ WHERE frame_id = ?
1073
+ ORDER BY relative_time ASC
1074
+ `);
1075
+ const rawLogs = logsStmt.all(frame.id);
1076
+
1077
+ // Convert snake_case field names to camelCase for frontend compatibility
1078
+ const logs = rawLogs.map(log => ({
1079
+ level: log.level,
1080
+ message: log.message,
1081
+ relativeTime: log.relative_time
1082
+ }));
1083
+
1084
+ // Deserialize interactions from the frame record (legacy embedded interactions)
1085
+ let embeddedInteractions = [];
1086
+ try {
1087
+ embeddedInteractions = JSON.parse(frame.interactions || '[]');
1088
+ } catch (error) {
1089
+ console.warn(`[getFrame] Failed to parse embedded interactions for frame ${frameIndex}:`, error);
1090
+ embeddedInteractions = [];
1091
+ }
1092
+
1093
+ // Get timestamp-associated interactions using the new method
1094
+ const associatedInteractionsResult = this.getInteractionsForFrame(
1095
+ recording.id,
1096
+ frame.timestamp || frame.absolute_timestamp, // Use relative timestamp for proper association
1097
+ 500 // ±500ms window
1098
+ );
1099
+
1100
+ return {
1101
+ timestamp: frame.timestamp,
1102
+ absoluteTimestamp: frame.absolute_timestamp,
1103
+ imageData: frame.image_data,
1104
+ logs: logs,
1105
+ interactions: embeddedInteractions, // Keep legacy field for backward compatibility
1106
+ associatedInteractions: associatedInteractionsResult.interactions,
1107
+ interactionMetadata: associatedInteractionsResult.metadata
1108
+ };
1109
+ }
1110
+
1111
+ // Get entire frame session
1112
+ getFrameSession(sessionId) {
1113
+ this.init();
1114
+
1115
+ const recording = this.getRecording(sessionId);
1116
+ if (!recording) return null;
1117
+
1118
+ // Debug: Check what's actually in the database
1119
+ const allFramesStmt = this.db.prepare(`
1120
+ SELECT COUNT(*) as count, MIN(frame_index) as min_index, MAX(frame_index) as max_index
1121
+ FROM frames
1122
+ WHERE recording_id = ?
1123
+ `);
1124
+ const frameStats = allFramesStmt.get(recording.id);
1125
+ console.log(`[getFrameSession] Database stats for ${recording.id}: ${frameStats.count} frames, indices ${frameStats.min_index} to ${frameStats.max_index}`);
1126
+
1127
+ const framesStmt = this.db.prepare(`
1128
+ SELECT frame_index, timestamp, absolute_timestamp, image_data, interactions
1129
+ FROM frames
1130
+ WHERE recording_id = ?
1131
+ ORDER BY frame_index ASC
1132
+ `);
1133
+ const frames = framesStmt.all(recording.id);
1134
+
1135
+ console.log(`[getFrameSession] Found ${frames.length} frames in database for session ${sessionId}`);
1136
+ console.log(`[getFrameSession] Frame indices: [${frames.map(f => f.frame_index).join(', ')}]`);
1137
+
1138
+ // Get logs and interactions for all frames
1139
+ for (const frame of frames) {
1140
+ const frameRowStmt = this.db.prepare(`
1141
+ SELECT id FROM frames
1142
+ WHERE recording_id = ? AND frame_index = ?
1143
+ `);
1144
+ const frameRow = frameRowStmt.get(recording.id, frame.frame_index);
1145
+
1146
+ if (frameRow) {
1147
+ const logsStmt = this.db.prepare(`
1148
+ SELECT level, message, relative_time
1149
+ FROM console_logs
1150
+ WHERE frame_id = ?
1151
+ ORDER BY relative_time ASC
1152
+ `);
1153
+ const rawLogs = logsStmt.all(frameRow.id);
1154
+
1155
+ // Convert snake_case field names to camelCase for frontend compatibility
1156
+ frame.logs = rawLogs.map(log => ({
1157
+ level: log.level,
1158
+ message: log.message,
1159
+ relativeTime: log.relative_time
1160
+ }));
1161
+ } else {
1162
+ frame.logs = [];
1163
+ }
1164
+
1165
+ // Rename fields to match expected format and deserialize embedded interactions
1166
+ frame.index = frame.frame_index;
1167
+ frame.imageData = frame.image_data;
1168
+ frame.absoluteTimestamp = frame.absolute_timestamp;
1169
+
1170
+ // Deserialize embedded interactions from JSON (legacy)
1171
+ let embeddedInteractions = [];
1172
+ try {
1173
+ embeddedInteractions = JSON.parse(frame.interactions || '[]');
1174
+ } catch (error) {
1175
+ console.warn(`[getFrameSession] Failed to parse embedded interactions for frame ${frame.frame_index}:`, error);
1176
+ embeddedInteractions = [];
1177
+ }
1178
+
1179
+ // Get timestamp-associated interactions for this frame
1180
+ const associatedInteractionsResult = this.getInteractionsForFrame(
1181
+ recording.id,
1182
+ frame.timestamp || frame.absolute_timestamp, // Use relative timestamp for proper association
1183
+ 500 // ±500ms window
1184
+ );
1185
+
1186
+ // Set interactions fields
1187
+ frame.interactions = embeddedInteractions; // Keep legacy field for backward compatibility
1188
+ frame.associatedInteractions = associatedInteractionsResult.interactions;
1189
+ frame.interactionMetadata = associatedInteractionsResult.metadata;
1190
+
1191
+ delete frame.frame_index;
1192
+ delete frame.image_data;
1193
+ delete frame.absolute_timestamp;
1194
+ }
1195
+
1196
+ // Get all interactions for this recording
1197
+ const interactionsStmt = this.db.prepare(`
1198
+ SELECT * FROM screen_interactions
1199
+ WHERE recording_id = ?
1200
+ ORDER BY timestamp ASC
1201
+ `);
1202
+ const interactions = interactionsStmt.all(recording.id);
1203
+
1204
+ return {
1205
+ type: 'frame_capture',
1206
+ sessionId: recording.session_id,
1207
+ timestamp: recording.timestamp,
1208
+ frames: frames,
1209
+ interactions: interactions
1210
+ };
1211
+ }
1212
+
1213
+ // List all recordings
1214
+ listRecordings() {
1215
+ this.init();
1216
+
1217
+ const stmt = this.db.prepare(`
1218
+ SELECT session_id, total_frames, timestamp, name
1219
+ FROM recordings
1220
+ WHERE type = 'frame_capture'
1221
+ ORDER BY timestamp DESC
1222
+ `);
1223
+
1224
+ return stmt.all().map(r => ({
1225
+ id: r.session_id,
1226
+ sessionId: r.session_id,
1227
+ totalFrames: r.total_frames,
1228
+ timestamp: r.timestamp,
1229
+ name: r.name
1230
+ }));
1231
+ }
1232
+
1233
+ // Get frame screenshot data only
1234
+ getFrameScreenshot(sessionId, frameIndex, includeMetadata = false) {
1235
+ this.init();
1236
+
1237
+ const recording = this.getRecording(sessionId);
1238
+ if (!recording) return null;
1239
+
1240
+ const frameStmt = this.db.prepare(`
1241
+ SELECT image_data, timestamp, absolute_timestamp FROM frames
1242
+ WHERE recording_id = ? AND frame_index = ?
1243
+ `);
1244
+ const frame = frameStmt.get(recording.id, frameIndex);
1245
+
1246
+ if (!frame || !frame.image_data) return null;
1247
+
1248
+ const result = {
1249
+ imageData: frame.image_data.toString('base64'),
1250
+ imageFormat: 'jpeg'
1251
+ };
1252
+
1253
+ if (includeMetadata) {
1254
+ result.metadata = {
1255
+ sessionId: sessionId,
1256
+ frameIndex: frameIndex,
1257
+ timestamp: frame.timestamp,
1258
+ absoluteTimestamp: frame.absolute_timestamp,
1259
+ size: frame.image_data.length,
1260
+ encoding: 'base64'
1261
+ };
1262
+ }
1263
+
1264
+ return result;
1265
+ }
1266
+
1267
+ // Delete recording
1268
+ deleteRecording(recordingId) {
1269
+ this.init();
1270
+
1271
+ const recording = this.getRecording(recordingId);
1272
+ if (!recording) return false;
1273
+
1274
+ const stmt = this.db.prepare(`DELETE FROM recordings WHERE id = ?`);
1275
+ const result = stmt.run(recording.id);
1276
+
1277
+ return result.changes > 0;
1278
+ }
1279
+
1280
+ // Delete workflow recording
1281
+ deleteWorkflowRecording(recordingId) {
1282
+ this.init();
1283
+
1284
+ const transaction = this.db.transaction(() => {
1285
+ // Delete associated workflow actions first
1286
+ const deleteActionsStmt = this.db.prepare(`DELETE FROM workflow_actions WHERE workflow_id = ?`);
1287
+ deleteActionsStmt.run(recordingId);
1288
+
1289
+ // Delete workflow recording
1290
+ const deleteRecordingStmt = this.db.prepare(`DELETE FROM workflow_recordings WHERE id = ?`);
1291
+ const result = deleteRecordingStmt.run(recordingId);
1292
+
1293
+ return result.changes > 0;
1294
+ });
1295
+
1296
+ return transaction();
1297
+ }
1298
+
1299
+ // Get database stats
1300
+ getStats() {
1301
+ this.init();
1302
+
1303
+ const recordingsCount = this.db.prepare(`SELECT COUNT(*) as count FROM recordings`).get().count;
1304
+ const framesCount = this.db.prepare(`SELECT COUNT(*) as count FROM frames`).get().count;
1305
+ const logsCount = this.db.prepare(`SELECT COUNT(*) as count FROM console_logs`).get().count;
1306
+
1307
+ return {
1308
+ recordings: recordingsCount,
1309
+ frames: framesCount,
1310
+ logs: logsCount,
1311
+ dbPath: DB_PATH
1312
+ };
1313
+ }
1314
+
1315
+ // Store workflow recording
1316
+ storeWorkflowRecording(sessionId, url, title, includeLogs = false, name = null, screenshotSettings = null) {
1317
+ this.init();
1318
+
1319
+ console.log('[Database] Storing workflow recording with name:', name, 'sessionId:', sessionId);
1320
+
1321
+ const stmt = this.db.prepare(`
1322
+ INSERT OR REPLACE INTO workflow_recordings (id, session_id, name, url, title, timestamp, include_logs, screenshot_settings, updated_at)
1323
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
1324
+ `);
1325
+
1326
+ const recordingId = `workflow_${sessionId}`;
1327
+ const timestamp = Date.now();
1328
+
1329
+ stmt.run(
1330
+ recordingId,
1331
+ sessionId,
1332
+ name,
1333
+ url,
1334
+ title,
1335
+ timestamp,
1336
+ includeLogs ? 1 : 0,
1337
+ screenshotSettings ? JSON.stringify(screenshotSettings) : null
1338
+ );
1339
+ return recordingId;
1340
+ }
1341
+
1342
+ // Store workflow actions
1343
+ storeWorkflowActions(workflowId, actions) {
1344
+ this.init();
1345
+
1346
+ const stmt = this.db.prepare(`
1347
+ INSERT OR REPLACE INTO workflow_actions
1348
+ (workflow_id, action_index, type, selector, x, y, value, text, placeholder, timestamp,
1349
+ screenshot_data, component, args, stack, element_html, component_data,
1350
+ event_handlers, element_state, performance_metrics)
1351
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1352
+ `);
1353
+
1354
+ const transaction = this.db.transaction((actions) => {
1355
+ let storedCount = 0;
1356
+
1357
+ for (const action of actions) {
1358
+ // Helper function to safely stringify and truncate JSON data
1359
+ const safeJsonStringify = (data, maxSize) => {
1360
+ if (!data) return null;
1361
+ let jsonStr = typeof data === 'string' ? data : JSON.stringify(data);
1362
+ if (jsonStr.length > maxSize) {
1363
+ console.warn(`[Database] Truncating data field from ${jsonStr.length} to ${maxSize} bytes`);
1364
+ jsonStr = jsonStr.substring(0, maxSize - 50) + '...[TRUNCATED]';
1365
+ }
1366
+ return jsonStr;
1367
+ };
1368
+
1369
+ // Helper function to safely get enhanced field data with dual naming convention support
1370
+ const getEnhancedFieldData = (action, camelCaseName, snakeCaseName) => {
1371
+ return action[camelCaseName] || action[snakeCaseName] || null;
1372
+ };
1373
+
1374
+ stmt.run(
1375
+ workflowId,
1376
+ action.index,
1377
+ action.type,
1378
+ action.selector || '', // Provide empty string as default for selector
1379
+ action.x || null,
1380
+ action.y || null,
1381
+ action.value || null,
1382
+ action.text || null,
1383
+ action.placeholder || null,
1384
+ action.timestamp,
1385
+ action.screenshot_data || null,
1386
+ action.component || null, // For function traces
1387
+ action.args ? JSON.stringify(action.args) : null, // Store args as JSON string
1388
+ action.stack || null, // For function traces
1389
+ // Enhanced click tracking data with size limits - supports both camelCase and snake_case
1390
+ safeJsonStringify(getEnhancedFieldData(action, 'elementHTML', 'element_html'), 10240), // 10KB limit
1391
+ safeJsonStringify(getEnhancedFieldData(action, 'componentData', 'component_data'), 3072), // 3KB limit
1392
+ safeJsonStringify(getEnhancedFieldData(action, 'eventHandlers', 'event_handlers'), 2048), // 2KB limit
1393
+ safeJsonStringify(getEnhancedFieldData(action, 'elementState', 'element_state'), 2048), // 2KB limit
1394
+ safeJsonStringify(getEnhancedFieldData(action, 'performanceMetrics', 'performance_metrics'), 1024) // 1KB limit
1395
+ );
1396
+ storedCount++;
1397
+ }
1398
+
1399
+ return storedCount;
1400
+ });
1401
+
1402
+ const storedCount = transaction(actions);
1403
+
1404
+ // Update total actions count
1405
+ const updateStmt = this.db.prepare(`
1406
+ UPDATE workflow_recordings
1407
+ SET total_actions = ?, updated_at = CURRENT_TIMESTAMP
1408
+ WHERE id = ?
1409
+ `);
1410
+ updateStmt.run(storedCount, workflowId);
1411
+
1412
+ return { success: true, storedCount };
1413
+ }
1414
+
1415
+ // Store workflow logs
1416
+ storeWorkflowLogs(workflowId, logs) {
1417
+ this.init();
1418
+
1419
+ const stmt = this.db.prepare(`
1420
+ INSERT INTO workflow_logs (workflow_id, level, message, timestamp, relative_time)
1421
+ VALUES (?, ?, ?, ?, ?)
1422
+ `);
1423
+
1424
+ const transaction = this.db.transaction((logs) => {
1425
+ let storedCount = 0;
1426
+
1427
+ for (const log of logs) {
1428
+ // Truncate large log messages before storing
1429
+ let message = log.message;
1430
+ const maxMessageLength = 10000; // 10KB limit per log entry
1431
+
1432
+ if (message && message.length > maxMessageLength) {
1433
+ // Check if it's a base64 image that was already truncated by the extension
1434
+ if (message.includes('[Base64 Image:') && message.includes('truncated...]')) {
1435
+ // Already truncated by extension, keep as is
1436
+ } else {
1437
+ // Additional truncation for any other large content
1438
+ message = message.substring(0, maxMessageLength) + `... [DB TRUNCATED: ${message.length} total bytes]`;
1439
+ }
1440
+ }
1441
+
1442
+ stmt.run(
1443
+ workflowId,
1444
+ log.level,
1445
+ message,
1446
+ log.timestamp,
1447
+ log.relativeTime || 0
1448
+ );
1449
+ storedCount++;
1450
+ }
1451
+
1452
+ return storedCount;
1453
+ });
1454
+
1455
+ const storedCount = transaction(logs);
1456
+
1457
+ return { success: true, storedCount };
1458
+ }
1459
+
1460
+ // Get workflow recording info
1461
+ getWorkflowRecordingInfo(sessionId) {
1462
+ this.init();
1463
+
1464
+ const stmt = this.db.prepare(`
1465
+ SELECT * FROM workflow_recordings
1466
+ WHERE session_id = ?
1467
+ `);
1468
+
1469
+ const recording = stmt.get(sessionId);
1470
+
1471
+ if (!recording) {
1472
+ return { error: 'Workflow recording not found' };
1473
+ }
1474
+
1475
+ return {
1476
+ sessionId: recording.session_id,
1477
+ name: recording.name,
1478
+ url: recording.url,
1479
+ title: recording.title,
1480
+ timestamp: recording.timestamp,
1481
+ totalActions: recording.total_actions,
1482
+ includeLogs: recording.include_logs,
1483
+ screenshotSettings: recording.screenshot_settings ? JSON.parse(recording.screenshot_settings) : null
1484
+ };
1485
+ }
1486
+
1487
+ // Get workflow actions
1488
+ getWorkflowActions(sessionId) {
1489
+ this.init();
1490
+
1491
+ const workflowStmt = this.db.prepare(`
1492
+ SELECT id FROM workflow_recordings
1493
+ WHERE session_id = ?
1494
+ `);
1495
+
1496
+ const recording = workflowStmt.get(sessionId);
1497
+
1498
+ if (!recording) {
1499
+ return { error: 'Workflow recording not found' };
1500
+ }
1501
+
1502
+ const actionsStmt = this.db.prepare(`
1503
+ SELECT * FROM workflow_actions
1504
+ WHERE workflow_id = ?
1505
+ ORDER BY action_index ASC
1506
+ `);
1507
+
1508
+ const actions = actionsStmt.all(recording.id);
1509
+
1510
+ return actions.map(action => {
1511
+ const result = {
1512
+ type: action.type,
1513
+ selector: action.selector,
1514
+ x: action.x,
1515
+ y: action.y,
1516
+ value: action.value,
1517
+ text: action.text,
1518
+ placeholder: action.placeholder,
1519
+ timestamp: action.timestamp,
1520
+ screenshot_data: action.screenshot_data
1521
+ };
1522
+
1523
+ // Add function trace fields if they exist
1524
+ if (action.component) {
1525
+ result.component = action.component;
1526
+ }
1527
+ if (action.args) {
1528
+ try {
1529
+ result.args = JSON.parse(action.args);
1530
+ } catch (e) {
1531
+ result.args = action.args;
1532
+ }
1533
+ }
1534
+ if (action.stack) {
1535
+ result.stack = action.stack;
1536
+ }
1537
+
1538
+ // Add enhanced click tracking fields if they exist
1539
+ if (action.element_html) {
1540
+ try {
1541
+ result.elementHTML = JSON.parse(action.element_html);
1542
+ } catch (e) {
1543
+ result.elementHTML = action.element_html;
1544
+ }
1545
+ }
1546
+ if (action.component_data) {
1547
+ try {
1548
+ result.componentData = JSON.parse(action.component_data);
1549
+ } catch (e) {
1550
+ result.componentData = action.component_data;
1551
+ }
1552
+ }
1553
+ if (action.event_handlers) {
1554
+ try {
1555
+ result.eventHandlers = JSON.parse(action.event_handlers);
1556
+ } catch (e) {
1557
+ result.eventHandlers = action.event_handlers;
1558
+ }
1559
+ }
1560
+ if (action.element_state) {
1561
+ try {
1562
+ result.elementState = JSON.parse(action.element_state);
1563
+ } catch (e) {
1564
+ result.elementState = action.element_state;
1565
+ }
1566
+ }
1567
+ if (action.performance_metrics) {
1568
+ try {
1569
+ result.performanceMetrics = JSON.parse(action.performance_metrics);
1570
+ } catch (e) {
1571
+ result.performanceMetrics = action.performance_metrics;
1572
+ }
1573
+ }
1574
+
1575
+ return result;
1576
+ });
1577
+ }
1578
+
1579
+ // Get workflow logs
1580
+ getWorkflowLogs(sessionId) {
1581
+ this.init();
1582
+
1583
+ const workflowStmt = this.db.prepare(`
1584
+ SELECT id FROM workflow_recordings
1585
+ WHERE session_id = ?
1586
+ `);
1587
+
1588
+ const recording = workflowStmt.get(sessionId);
1589
+
1590
+ if (!recording) {
1591
+ return { error: 'Workflow recording not found' };
1592
+ }
1593
+
1594
+ const logsStmt = this.db.prepare(`
1595
+ SELECT * FROM workflow_logs
1596
+ WHERE workflow_id = ?
1597
+ ORDER BY timestamp ASC
1598
+ `);
1599
+
1600
+ const logs = logsStmt.all(recording.id);
1601
+
1602
+ return logs.map(log => ({
1603
+ level: log.level,
1604
+ message: log.message,
1605
+ timestamp: log.timestamp,
1606
+ relativeTime: log.relative_time
1607
+ }));
1608
+ }
1609
+
1610
+ // Get complete workflow recording
1611
+ getWorkflowRecording(sessionId) {
1612
+ this.init();
1613
+
1614
+ const info = this.getWorkflowRecordingInfo(sessionId);
1615
+ if (info.error) {
1616
+ return info;
1617
+ }
1618
+
1619
+ const actions = this.getWorkflowActions(sessionId);
1620
+ const logs = info.includeLogs ? this.getWorkflowLogs(sessionId) : [];
1621
+
1622
+ return {
1623
+ type: 'workflow_recording',
1624
+ sessionId: info.sessionId,
1625
+ name: info.name,
1626
+ url: info.url,
1627
+ title: info.title,
1628
+ timestamp: info.timestamp,
1629
+ totalActions: info.totalActions,
1630
+ screenshotSettings: info.screenshotSettings,
1631
+ actions: actions,
1632
+ logs: logs
1633
+ };
1634
+ }
1635
+
1636
+ // List all workflow recordings
1637
+ listWorkflowRecordings() {
1638
+ this.init();
1639
+
1640
+ const stmt = this.db.prepare(`
1641
+ SELECT id, session_id, name, url, title, total_actions, timestamp, screenshot_settings
1642
+ FROM workflow_recordings
1643
+ ORDER BY timestamp DESC
1644
+ `);
1645
+
1646
+ return stmt.all().map(r => ({
1647
+ id: r.id,
1648
+ session_id: r.session_id,
1649
+ name: r.name,
1650
+ url: r.url,
1651
+ title: r.title,
1652
+ total_actions: r.total_actions,
1653
+ timestamp: r.timestamp,
1654
+ screenshot_settings: r.screenshot_settings ? JSON.parse(r.screenshot_settings) : null
1655
+ }));
1656
+ }
1657
+
1658
+ // Clear all workflow recordings
1659
+ clearWorkflowRecordings() {
1660
+ this.init();
1661
+
1662
+ const transaction = this.db.transaction(() => {
1663
+ // Delete all workflow logs
1664
+ const deleteLogsStmt = this.db.prepare(`DELETE FROM workflow_logs`);
1665
+ deleteLogsStmt.run();
1666
+
1667
+ // Delete all workflow actions
1668
+ const deleteActionsStmt = this.db.prepare(`DELETE FROM workflow_actions`);
1669
+ deleteActionsStmt.run();
1670
+
1671
+ // Delete all restore points
1672
+ const deleteRestorePointsStmt = this.db.prepare(`DELETE FROM restore_points`);
1673
+ deleteRestorePointsStmt.run();
1674
+
1675
+ // Delete all workflow recordings
1676
+ const deleteRecordingsStmt = this.db.prepare(`DELETE FROM workflow_recordings`);
1677
+ const result = deleteRecordingsStmt.run();
1678
+
1679
+ console.log(`[Database] Cleared workflow recordings: ${result.changes} recordings deleted`);
1680
+ return result.changes;
1681
+ });
1682
+
1683
+ return transaction();
1684
+ }
1685
+
1686
+ // Store restore point
1687
+ storeRestorePoint(workflowId, actionIndex, restoreData) {
1688
+ this.init();
1689
+
1690
+ const stmt = this.db.prepare(`
1691
+ INSERT OR REPLACE INTO restore_points
1692
+ (id, workflow_id, action_index, timestamp, url, title, dom_snapshot,
1693
+ scroll_x, scroll_y, local_storage, session_storage, cookies, console_logs)
1694
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1695
+ `);
1696
+
1697
+ const restorePointId = `restore_${workflowId}_${actionIndex}_${Date.now()}`;
1698
+
1699
+ try {
1700
+ stmt.run(
1701
+ restorePointId,
1702
+ workflowId,
1703
+ actionIndex,
1704
+ Date.now(),
1705
+ restoreData.url,
1706
+ restoreData.title,
1707
+ JSON.stringify(restoreData.domSnapshot),
1708
+ restoreData.scrollX || 0,
1709
+ restoreData.scrollY || 0,
1710
+ JSON.stringify(restoreData.localStorage || {}),
1711
+ JSON.stringify(restoreData.sessionStorage || {}),
1712
+ JSON.stringify(restoreData.cookies || []),
1713
+ JSON.stringify(restoreData.consoleLogs || [])
1714
+ );
1715
+
1716
+ return { success: true, restorePointId };
1717
+ } catch (error) {
1718
+ console.error('Error storing restore point:', error);
1719
+ return { success: false, error: error.message };
1720
+ }
1721
+ }
1722
+
1723
+ // Get restore point
1724
+ getRestorePoint(restorePointId) {
1725
+ this.init();
1726
+
1727
+ const stmt = this.db.prepare(`
1728
+ SELECT * FROM restore_points WHERE id = ?
1729
+ `);
1730
+
1731
+ const restorePoint = stmt.get(restorePointId);
1732
+
1733
+ if (!restorePoint) {
1734
+ return { error: 'Restore point not found' };
1735
+ }
1736
+
1737
+ return {
1738
+ id: restorePoint.id,
1739
+ workflowId: restorePoint.workflow_id,
1740
+ actionIndex: restorePoint.action_index,
1741
+ timestamp: restorePoint.timestamp,
1742
+ url: restorePoint.url,
1743
+ title: restorePoint.title,
1744
+ domSnapshot: JSON.parse(restorePoint.dom_snapshot),
1745
+ scrollX: restorePoint.scroll_x,
1746
+ scrollY: restorePoint.scroll_y,
1747
+ localStorage: JSON.parse(restorePoint.local_storage),
1748
+ sessionStorage: JSON.parse(restorePoint.session_storage),
1749
+ cookies: JSON.parse(restorePoint.cookies),
1750
+ consoleLogs: JSON.parse(restorePoint.console_logs)
1751
+ };
1752
+ }
1753
+
1754
+ // List restore points for a workflow
1755
+ listRestorePoints(workflowId) {
1756
+ this.init();
1757
+
1758
+ const stmt = this.db.prepare(`
1759
+ SELECT id, action_index, timestamp, url, title
1760
+ FROM restore_points
1761
+ WHERE workflow_id = ?
1762
+ ORDER BY action_index ASC, timestamp DESC
1763
+ `);
1764
+
1765
+ return stmt.all(workflowId).map(rp => ({
1766
+ id: rp.id,
1767
+ actionIndex: rp.action_index,
1768
+ timestamp: rp.timestamp,
1769
+ url: rp.url,
1770
+ title: rp.title
1771
+ }));
1772
+ }
1773
+
1774
+ // Close database
1775
+ // Server instance tracking methods
1776
+ async registerServerInstance(pid, mode) {
1777
+ this.init();
1778
+
1779
+ // Clean up old entries first
1780
+ await this.cleanupDeadServerInstances();
1781
+
1782
+ const stmt = this.db.prepare(`
1783
+ INSERT OR REPLACE INTO server_instances (pid, mode)
1784
+ VALUES (?, ?)
1785
+ `);
1786
+
1787
+ stmt.run(pid, mode);
1788
+ console.log(`Registered server instance: PID ${pid}, mode: ${mode}`);
1789
+ }
1790
+
1791
+ async getSingleServerInstance() {
1792
+ this.init();
1793
+
1794
+ // Clean up dead processes first
1795
+ await this.cleanupDeadServerInstances();
1796
+
1797
+ const stmt = this.db.prepare(`
1798
+ SELECT pid FROM server_instances
1799
+ WHERE mode = 'single-server'
1800
+ ORDER BY started_at DESC
1801
+ LIMIT 1
1802
+ `);
1803
+
1804
+ const result = stmt.get();
1805
+ return result ? result.pid : null;
1806
+ }
1807
+
1808
+ async cleanupDeadServerInstances() {
1809
+ this.init();
1810
+
1811
+ const stmt = this.db.prepare(`SELECT pid FROM server_instances`);
1812
+ const instances = stmt.all();
1813
+
1814
+ const { exec } = await import('child_process');
1815
+ const { promisify } = await import('util');
1816
+ const execAsync = promisify(exec);
1817
+
1818
+ for (const instance of instances) {
1819
+ try {
1820
+ // Check if process is still running
1821
+ await execAsync(`ps -p ${instance.pid}`);
1822
+ // Process is still running, keep it
1823
+ } catch (error) {
1824
+ // Process is dead, remove it
1825
+ const deleteStmt = this.db.prepare(`DELETE FROM server_instances WHERE pid = ?`);
1826
+ deleteStmt.run(instance.pid);
1827
+ console.log(`Cleaned up dead server instance: PID ${instance.pid}`);
1828
+ }
1829
+ }
1830
+ }
1831
+
1832
+ unregisterServerInstance(pid) {
1833
+ this.init();
1834
+
1835
+ const stmt = this.db.prepare(`DELETE FROM server_instances WHERE pid = ?`);
1836
+ const result = stmt.run(pid);
1837
+
1838
+ if (result.changes > 0) {
1839
+ console.log(`Unregistered server instance: PID ${pid}`);
1840
+ }
1841
+ }
1842
+
1843
+ // Store screen interactions for a recording
1844
+ storeScreenInteractions(recordingId, interactions) {
1845
+ this.init();
1846
+
1847
+ // Ensure a recording exists for this ID
1848
+ const checkStmt = this.db.prepare(`SELECT id FROM recordings WHERE id = ?`);
1849
+ const existingRecord = checkStmt.get(recordingId);
1850
+
1851
+ if (!existingRecord) {
1852
+ // Create a recording record if it doesn't exist
1853
+ const insertRecordingStmt = this.db.prepare(`
1854
+ INSERT INTO recordings (id, session_id, type, timestamp, updated_at)
1855
+ VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
1856
+ `);
1857
+ insertRecordingStmt.run(recordingId, recordingId, 'screen_recording', Date.now());
1858
+ }
1859
+
1860
+ const stmt = this.db.prepare(`
1861
+ INSERT OR REPLACE INTO screen_interactions
1862
+ (recording_id, interaction_index, type, selector, xpath, x, y, value, text, key, timestamp, frame_index,
1863
+ element_html, component_data, event_handlers, element_state, performance_metrics)
1864
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1865
+ `);
1866
+
1867
+ const transaction = this.db.transaction(() => {
1868
+ interactions.forEach((interaction, index) => {
1869
+ stmt.run(
1870
+ recordingId,
1871
+ index,
1872
+ interaction.type,
1873
+ interaction.selector || null,
1874
+ interaction.xpath || null,
1875
+ interaction.x || null,
1876
+ interaction.y || null,
1877
+ interaction.value || null,
1878
+ interaction.text || null,
1879
+ interaction.key || null,
1880
+ interaction.timestamp,
1881
+ interaction.frameIndex || null,
1882
+ // Enhanced fields - handle mixed string/object types correctly
1883
+ interaction.element_html || null, // This is a string, store directly
1884
+ interaction.component_data ? JSON.stringify(interaction.component_data) : null,
1885
+ interaction.event_handlers ? JSON.stringify(interaction.event_handlers) : null,
1886
+ interaction.element_state ? JSON.stringify(interaction.element_state) : null,
1887
+ interaction.performance_metrics ? JSON.stringify(interaction.performance_metrics) : null
1888
+ );
1889
+ });
1890
+ });
1891
+
1892
+ try {
1893
+ transaction();
1894
+ return { success: true, count: interactions.length };
1895
+ } catch (error) {
1896
+ console.error('Error storing screen interactions:', error);
1897
+ return { success: false, error: error.message };
1898
+ }
1899
+ }
1900
+
1901
+ // Get screen interactions for a recording
1902
+ getScreenInteractions(recordingId) {
1903
+ this.init();
1904
+
1905
+ // Apply same ID resolution logic as getRecording() method
1906
+ // Handle both old format (frame_<id>) and new format (base id)
1907
+ let baseId = recordingId;
1908
+ if (recordingId.startsWith('frame_')) {
1909
+ baseId = recordingId.substring(6); // Remove 'frame_' prefix
1910
+ console.log(`[getScreenInteractions] Converting old format ID ${recordingId} to base ID: ${baseId}`);
1911
+ }
1912
+
1913
+ // Priority order: exact recording_id match, then baseId match
1914
+ // Check both the original recordingId and the baseId to handle dual compatibility
1915
+ const stmt = this.db.prepare(`
1916
+ SELECT * FROM screen_interactions
1917
+ WHERE recording_id = ? OR recording_id = ?
1918
+ ORDER BY
1919
+ CASE
1920
+ WHEN recording_id = ? THEN 1
1921
+ WHEN recording_id = ? THEN 2
1922
+ ELSE 3
1923
+ END,
1924
+ interaction_index ASC
1925
+ `);
1926
+
1927
+ return stmt.all(recordingId, baseId, recordingId, baseId);
1928
+ }
1929
+
1930
+ // Get interactions for a specific frame
1931
+ getFrameInteractions(recordingId, frameIndex) {
1932
+ this.init();
1933
+
1934
+ const stmt = this.db.prepare(`
1935
+ SELECT * FROM screen_interactions
1936
+ WHERE recording_id = ? AND frame_index = ?
1937
+ ORDER BY timestamp ASC
1938
+ `);
1939
+
1940
+ return stmt.all(recordingId, frameIndex);
1941
+ }
1942
+
1943
+ // Get interactions associated with a frame by timestamp window
1944
+ getInteractionsForFrame(recordingId, frameTimestamp, windowMs = 500) {
1945
+ this.init();
1946
+
1947
+ const lowerBound = frameTimestamp - windowMs;
1948
+ const upperBound = frameTimestamp + windowMs;
1949
+
1950
+ const stmt = this.db.prepare(`
1951
+ SELECT
1952
+ id,
1953
+ interaction_index,
1954
+ type,
1955
+ selector,
1956
+ xpath,
1957
+ x,
1958
+ y,
1959
+ value,
1960
+ text,
1961
+ key,
1962
+ timestamp,
1963
+ frame_index,
1964
+ ABS(timestamp - ?) as time_diff
1965
+ FROM screen_interactions
1966
+ WHERE recording_id = ?
1967
+ AND timestamp BETWEEN ? AND ?
1968
+ ORDER BY time_diff ASC, timestamp ASC
1969
+ LIMIT 10
1970
+ `);
1971
+
1972
+ const interactions = stmt.all(frameTimestamp, recordingId, lowerBound, upperBound);
1973
+
1974
+ // Diagnostic logging for interaction association debugging
1975
+ console.log(`[getInteractionsForFrame] DEBUGGING - recordingId: ${recordingId}, frameTimestamp: ${frameTimestamp}, window: ±${windowMs}ms`);
1976
+ console.log(`[getInteractionsForFrame] Search range: ${lowerBound} to ${upperBound}`);
1977
+ console.log(`[getInteractionsForFrame] Found ${interactions.length} interactions`);
1978
+ if (interactions.length > 0) {
1979
+ console.log(`[getInteractionsForFrame] First interaction:`, interactions[0]);
1980
+ }
1981
+
1982
+ // Convert to InteractionData format with metadata
1983
+ const result = interactions.map(interaction => ({
1984
+ id: interaction.id,
1985
+ index: interaction.interaction_index,
1986
+ type: interaction.type,
1987
+ selector: interaction.selector,
1988
+ xpath: interaction.xpath,
1989
+ x: interaction.x,
1990
+ y: interaction.y,
1991
+ value: interaction.value,
1992
+ text: interaction.text,
1993
+ key: interaction.key,
1994
+ timestamp: interaction.timestamp,
1995
+ frameIndex: interaction.frame_index,
1996
+ timeDifference: interaction.time_diff
1997
+ }));
1998
+
1999
+ // Add metadata
2000
+ const totalFoundStmt = this.db.prepare(`
2001
+ SELECT COUNT(*) as count
2002
+ FROM screen_interactions
2003
+ WHERE recording_id = ?
2004
+ AND timestamp BETWEEN ? AND ?
2005
+ `);
2006
+ const totalFound = totalFoundStmt.get(recordingId, lowerBound, upperBound).count;
2007
+
2008
+ return {
2009
+ interactions: result,
2010
+ metadata: {
2011
+ totalFound: totalFound,
2012
+ displayed: result.length,
2013
+ timeWindow: windowMs,
2014
+ frameTimestamp: frameTimestamp,
2015
+ searchRange: {
2016
+ start: lowerBound,
2017
+ end: upperBound
2018
+ }
2019
+ }
2020
+ };
2021
+ }
2022
+
2023
+ // Store execution trace data
2024
+ storeExecutionTrace(recordingId, event) {
2025
+ this.init();
2026
+
2027
+ // Generate a trace_index if not provided (use timestamp + random for uniqueness)
2028
+ const traceIndex = event.trace_index || Date.now() + Math.floor(Math.random() * 1000);
2029
+
2030
+ const stmt = this.db.prepare(`
2031
+ INSERT OR REPLACE INTO execution_traces (
2032
+ recording_id, trace_index, timestamp, function_name, function_source,
2033
+ arguments, return_value, error, call_stack, execution_time_ms
2034
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2035
+ `);
2036
+
2037
+ // Build call stack info from the event data
2038
+ const callStackInfo = {
2039
+ call_id: event.call_id,
2040
+ call_depth: event.call_depth || 0,
2041
+ parent_call_id: event.parent_call_id,
2042
+ category: event.category || 'unknown',
2043
+ metadata: event.metadata || {}
2044
+ };
2045
+
2046
+ return stmt.run(
2047
+ recordingId,
2048
+ traceIndex,
2049
+ event.timestamp || Date.now(),
2050
+ event.function_name,
2051
+ event.function_source || null, // Source location if available
2052
+ JSON.stringify(event.arguments || []),
2053
+ JSON.stringify(event.return_value),
2054
+ event.error,
2055
+ JSON.stringify(callStackInfo),
2056
+ event.execution_time_ms || 0
2057
+ );
2058
+ }
2059
+
2060
+ // Store variable state data
2061
+ storeVariableState(recordingId, event) {
2062
+ this.init();
2063
+
2064
+ const stmt = this.db.prepare(`
2065
+ INSERT INTO variable_states (
2066
+ recording_id, timestamp, scope_chain, variables, heap_snapshot
2067
+ ) VALUES (?, ?, ?, ?, ?)
2068
+ `);
2069
+
2070
+ // Build variables object from event data
2071
+ const variables = {
2072
+ [event.variable_name || 'unknown']: event.variable_value,
2073
+ call_id: event.call_id,
2074
+ capture_point: event.capture_point || 'unknown'
2075
+ };
2076
+
2077
+ const scopeChain = {
2078
+ scope: event.scope || 'unknown',
2079
+ call_id: event.call_id
2080
+ };
2081
+
2082
+ return stmt.run(
2083
+ recordingId,
2084
+ event.timestamp || Date.now(),
2085
+ JSON.stringify(scopeChain),
2086
+ JSON.stringify(variables),
2087
+ null // heap_snapshot - not captured yet
2088
+ );
2089
+ }
2090
+
2091
+ // Store DOM mutation data
2092
+ storeDomMutation(recordingId, event) {
2093
+ this.init();
2094
+
2095
+ const stmt = this.db.prepare(`
2096
+ INSERT INTO dom_mutations (
2097
+ recording_id, timestamp, mutation_type, target_selector,
2098
+ target_xpath, old_value, new_value, attributes_changed
2099
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
2100
+ `);
2101
+
2102
+ // Build attributes changed object
2103
+ const attributesChanged = {
2104
+ attribute_name: event.attribute_name,
2105
+ added_nodes: event.added_nodes || [],
2106
+ removed_nodes: event.removed_nodes || []
2107
+ };
2108
+
2109
+ return stmt.run(
2110
+ recordingId,
2111
+ event.timestamp || Date.now(),
2112
+ event.mutation_type,
2113
+ event.target_selector,
2114
+ event.target_xpath,
2115
+ event.old_value,
2116
+ event.new_value,
2117
+ JSON.stringify(attributesChanged)
2118
+ );
2119
+ }
2120
+
2121
+ // Store network request data
2122
+ storeNetworkRequest(recordingId, event) {
2123
+ this.init();
2124
+
2125
+ const stmt = this.db.prepare(`
2126
+ INSERT INTO network_requests (
2127
+ recording_id, timestamp, request_id, method, url,
2128
+ headers, body, response_status, response_headers,
2129
+ response_body, duration_ms
2130
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2131
+ `);
2132
+
2133
+ return stmt.run(
2134
+ recordingId,
2135
+ event.timestamp || Date.now(),
2136
+ event.request_id || 'req_' + Date.now(),
2137
+ event.method,
2138
+ event.url,
2139
+ JSON.stringify(event.request_headers || {}),
2140
+ event.request_body,
2141
+ event.status_code || event.response_status,
2142
+ JSON.stringify(event.response_headers || {}),
2143
+ event.response_body,
2144
+ event.duration_ms || 0
2145
+ );
2146
+ }
2147
+
2148
+ // Store performance metric data
2149
+ storePerformanceMetric(recordingId, event) {
2150
+ this.init();
2151
+
2152
+ const stmt = this.db.prepare(`
2153
+ INSERT INTO performance_metrics (
2154
+ recording_id, timestamp, metric_type, metric_name,
2155
+ metric_value, metadata
2156
+ ) VALUES (?, ?, ?, ?, ?, ?)
2157
+ `);
2158
+
2159
+ return stmt.run(
2160
+ recordingId,
2161
+ event.timestamp || Date.now(),
2162
+ event.metric_type || 'unknown',
2163
+ event.metric_name,
2164
+ event.metric_value,
2165
+ JSON.stringify(event.metadata || {})
2166
+ );
2167
+ }
2168
+
2169
+ close() {
2170
+ if (this.db) {
2171
+ this.db.close();
2172
+ this.initialized = false;
2173
+ }
2174
+ }
2175
+ }
2176
+
2177
+ // Export singleton instance
2178
+ export const database = new ChromePilotDatabase();