@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,957 @@
1
+ /**
2
+ * Frame Tool Handler - Handles frame recording and interaction tools
3
+ * Extracted from original index.js with preserved functionality and security measures
4
+ * Now uses HTTP client for authenticated access to recordings
5
+ */
6
+
7
+ import { httpClient } from '../../services/http-client.js';
8
+
9
+ export class FrameToolHandler {
10
+ constructor(chromeController) {
11
+ this.chromeController = chromeController;
12
+ this.useHttpClient = true; // Flag to use HTTP client instead of direct database access
13
+ }
14
+
15
+ /**
16
+ * Handles frame-related tool calls
17
+ * @param {string} name - Tool name
18
+ * @param {Object} args - Tool arguments
19
+ * @returns {Object} Tool execution result
20
+ */
21
+ async handle(name, args) {
22
+ switch (name) {
23
+ case 'chromedebug_show_frames':
24
+ return await this.handleShowFrames(args);
25
+
26
+ case 'get_frame_session_info':
27
+ return await this.handleGetFrameSessionInfo(args);
28
+
29
+ case 'get_frame':
30
+ return await this.handleGetFrame(args);
31
+
32
+ case 'search_frame_logs':
33
+ return await this.handleSearchFrameLogs(args);
34
+
35
+ case 'get_frame_logs_paginated':
36
+ return await this.handleGetFrameLogsPaginated(args);
37
+
38
+ case 'get_frame_screenshot':
39
+ return await this.handleGetFrameScreenshot(args);
40
+
41
+ case 'get_screen_interactions':
42
+ return await this.handleGetScreenInteractions(args);
43
+
44
+ /*
45
+ * SNAPSHOT FEATURE DISABLED (2025-10-01)
46
+ * Snapshot case handlers removed - see SNAPSHOT_FEATURE_DISABLED.md
47
+ */
48
+ /*
49
+ case 'take_snapshot':
50
+ return await this.handleTakeSnapshot(args);
51
+
52
+ case 'list_snapshots':
53
+ return await this.handleListSnapshots(args);
54
+
55
+ case 'get_snapshot':
56
+ return await this.handleGetSnapshot(args);
57
+
58
+ case 'search_snapshot_logs':
59
+ return await this.handleSearchSnapshotLogs(args);
60
+ */
61
+
62
+ default:
63
+ throw new Error(`Unknown frame tool: ${name}`);
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Handle chromedebug_show_frames tool
69
+ * @param {Object} args - Arguments with sessionId and display options
70
+ * @returns {Object} Formatted frame display
71
+ */
72
+ async handleShowFrames(args) {
73
+ if (!args.sessionId) {
74
+ throw new Error('Session ID is required');
75
+ }
76
+
77
+ const maxFrames = args.maxFrames || 10;
78
+ const showLogs = args.showLogs !== false;
79
+ const showInteractions = args.showInteractions !== false;
80
+ const logLevel = args.logLevel || 'all';
81
+ const maxLogsPerFrame = args.maxLogsPerFrame || 10;
82
+
83
+ let sessionInfo, availableSessions;
84
+
85
+ try {
86
+ if (this.useHttpClient) {
87
+ sessionInfo = await httpClient.getFrameSessionInfo(args.sessionId);
88
+ if (!sessionInfo) {
89
+ availableSessions = await httpClient.listFrameSessions();
90
+ }
91
+ } else {
92
+ sessionInfo = await this.chromeController.getFrameSessionInfo(args.sessionId);
93
+ if (!sessionInfo) {
94
+ availableSessions = await this.chromeController.listFrameSessions();
95
+ }
96
+ }
97
+ } catch (error) {
98
+ // Fall back to direct database access if HTTP client fails
99
+ console.warn('[FrameToolHandler] HTTP client failed, falling back to direct access:', error.message);
100
+ sessionInfo = await this.chromeController.getFrameSessionInfo(args.sessionId);
101
+ if (!sessionInfo) {
102
+ availableSessions = await this.chromeController.listFrameSessions();
103
+ }
104
+ }
105
+
106
+ if (!sessionInfo) {
107
+ const recentSessions = availableSessions ? availableSessions.slice(0, 5) : [];
108
+
109
+ let errorMsg = `Frame session not found: ${args.sessionId}\n\n`;
110
+ if (recentSessions.length > 0) {
111
+ errorMsg += 'Available frame sessions:\n';
112
+ recentSessions.forEach(s => {
113
+ errorMsg += ` - ${s.sessionId} (${s.totalFrames} frames, ${new Date(s.timestamp).toLocaleString()})\n`;
114
+ });
115
+ errorMsg += '\nTip: Use list_workflow_recordings to see all available recordings.';
116
+ } else {
117
+ errorMsg += 'No frame sessions found in the database.\n';
118
+ errorMsg += 'Frame recordings may have been created in a different Chrome Debug instance.';
119
+ }
120
+
121
+ throw new Error(errorMsg);
122
+ }
123
+
124
+ // Get only frame metadata without image data to avoid token limits
125
+ const { database } = await import('../../database.js');
126
+ const recording = database.getRecording(args.sessionId);
127
+ if (!recording) {
128
+ throw new Error('Frame session data not found');
129
+ }
130
+
131
+ // Get frames without image data
132
+ const framesMetadataStmt = database.db.prepare(`
133
+ SELECT frame_index, timestamp, absolute_timestamp
134
+ FROM frames
135
+ WHERE recording_id = ?
136
+ ORDER BY frame_index ASC
137
+ LIMIT ?
138
+ `);
139
+ const framesToShow = framesMetadataStmt.all(recording.id, maxFrames);
140
+
141
+ const result = {
142
+ sessionId: args.sessionId,
143
+ type: recording.type,
144
+ created: new Date(sessionInfo.timestamp).toLocaleString(),
145
+ totalFrames: sessionInfo.totalFrames,
146
+ showingFrames: framesToShow.length,
147
+ logFilter: logLevel !== 'all' ? logLevel.toUpperCase() : null,
148
+ frames: []
149
+ };
150
+
151
+ // Handle case where recording exists but has no frames
152
+ if (sessionInfo.totalFrames === 0) {
153
+ result.status = 'Recording exists but contains no frames';
154
+
155
+ // Check for screen interactions
156
+ try {
157
+ const interactionsResult = this.useHttpClient ?
158
+ await httpClient.getScreenInteractions(args.sessionId) :
159
+ await this.chromeController.getScreenInteractions(args.sessionId);
160
+
161
+ const interactions = this.useHttpClient ? interactionsResult.interactions : interactionsResult;
162
+
163
+ if (interactions && interactions.length > 0) {
164
+ result.interactionsFound = interactions.length;
165
+ result.note = 'This recording has captured user interactions but no frame data. Use get_screen_interactions to view the captured interactions.';
166
+ } else {
167
+ result.note = 'No frame data or interactions found. This may indicate the recording was started but stopped immediately.';
168
+ }
169
+ } catch (error) {
170
+ console.warn('[FrameToolHandler] Failed to check interactions:', error.message);
171
+ result.note = 'No frame data found. Unable to check for interactions.';
172
+ }
173
+
174
+ return result;
175
+ }
176
+
177
+ // Get all interactions for this recording if requested
178
+ let allInteractions = [];
179
+ if (showInteractions && !this.useHttpClient) {
180
+ // Only get all interactions if using direct access (for filtering)
181
+ allInteractions = await this.chromeController.getScreenInteractions(args.sessionId);
182
+ }
183
+
184
+ // Process each frame
185
+ for (const frame of framesToShow) {
186
+ const frameData = {
187
+ frameIndex: frame.frame_index,
188
+ timestamp: `${frame.timestamp}ms`,
189
+ logs: [],
190
+ interactions: []
191
+ };
192
+
193
+ // Get logs for this specific frame if requested
194
+ if (showLogs) {
195
+ const frameRowStmt = database.db.prepare(`
196
+ SELECT id FROM frames
197
+ WHERE recording_id = ? AND frame_index = ?
198
+ `);
199
+ const frameRow = frameRowStmt.get(recording.id, frame.frame_index);
200
+
201
+ if (frameRow) {
202
+ let logsQuery = `
203
+ SELECT level, message, relative_time
204
+ FROM console_logs
205
+ WHERE frame_id = ?`;
206
+ let queryParams = [frameRow.id];
207
+
208
+ if (logLevel !== 'all') {
209
+ logsQuery += ` AND level = ?`;
210
+ queryParams.push(logLevel);
211
+ }
212
+
213
+ logsQuery += ` ORDER BY relative_time ASC`;
214
+
215
+ if (maxLogsPerFrame > 0) {
216
+ logsQuery += ` LIMIT ?`;
217
+ queryParams.push(maxLogsPerFrame);
218
+ }
219
+
220
+ const logsStmt = database.db.prepare(logsQuery);
221
+ const logs = logsStmt.all(...queryParams);
222
+
223
+ frameData.logs = logs.map(log => ({
224
+ level: log.level.toUpperCase(),
225
+ message: log.message
226
+ }));
227
+
228
+ if (maxLogsPerFrame > 0 && logs.length === maxLogsPerFrame) {
229
+ frameData.logsTruncated = true;
230
+ }
231
+ }
232
+ } else {
233
+ // Count all logs for this frame to show total
234
+ const frameRowStmt = database.db.prepare(`
235
+ SELECT id FROM frames
236
+ WHERE recording_id = ? AND frame_index = ?
237
+ `);
238
+ const frameRow = frameRowStmt.get(recording.id, frame.frame_index);
239
+ let totalLogCount = 0;
240
+
241
+ if (frameRow) {
242
+ let countQuery = `SELECT COUNT(*) as count FROM console_logs WHERE frame_id = ?`;
243
+ let countParams = [frameRow.id];
244
+
245
+ if (logLevel !== 'all') {
246
+ countQuery += ` AND level = ?`;
247
+ countParams.push(logLevel);
248
+ }
249
+
250
+ const countStmt = database.db.prepare(countQuery);
251
+ totalLogCount = countStmt.get(...countParams).count;
252
+ }
253
+
254
+ frameData.totalLogs = totalLogCount;
255
+ }
256
+
257
+ // Show interactions for this frame
258
+ if (showInteractions) {
259
+ try {
260
+ // Get frame details with enhanced interaction data
261
+ const frameDetails = this.useHttpClient ?
262
+ await httpClient.getFrame(args.sessionId, frame.frame_index) :
263
+ await this.chromeController.getFrame(args.sessionId, frame.frame_index);
264
+
265
+ if (frameDetails) {
266
+ // Legacy embedded interactions
267
+ const embeddedInteractions = frameDetails.interactions || [];
268
+ // New timestamp-associated interactions
269
+ const associatedInteractions = frameDetails.associatedInteractions || [];
270
+
271
+ frameData.interactions = embeddedInteractions.map(interaction =>
272
+ this.formatInteraction(interaction)
273
+ );
274
+ frameData.associatedInteractions = associatedInteractions.map(interaction =>
275
+ this.formatInteraction(interaction)
276
+ );
277
+ frameData.interactionMetadata = frameDetails.interactionMetadata || {
278
+ totalFound: 0,
279
+ displayed: 0,
280
+ timeWindow: 500
281
+ };
282
+ } else {
283
+ frameData.interactions = [];
284
+ frameData.associatedInteractions = [];
285
+ frameData.interactionMetadata = { totalFound: 0, displayed: 0, timeWindow: 500 };
286
+ }
287
+ } catch (error) {
288
+ console.warn('[FrameToolHandler] Failed to get frame interactions:', error.message);
289
+ frameData.interactions = [];
290
+ frameData.associatedInteractions = [];
291
+ frameData.interactionMetadata = { totalFound: 0, displayed: 0, timeWindow: 500 };
292
+ }
293
+ }
294
+
295
+ result.frames.push(frameData);
296
+ }
297
+
298
+ if (sessionInfo.totalFrames > maxFrames) {
299
+ result.hasMoreFrames = true;
300
+ result.remainingFrames = sessionInfo.totalFrames - maxFrames;
301
+ result.note = 'Use get_frame with frameIndex to view specific frames';
302
+ }
303
+
304
+ return result;
305
+ }
306
+
307
+ /**
308
+ * Handle get_frame_session_info tool
309
+ * @param {Object} args - Arguments with sessionId
310
+ * @returns {Object} Session info
311
+ */
312
+ async handleGetFrameSessionInfo(args) {
313
+ if (!args.sessionId) {
314
+ throw new Error('Session ID is required');
315
+ }
316
+
317
+ let info, availableSessions;
318
+
319
+ try {
320
+ if (this.useHttpClient) {
321
+ info = await httpClient.getFrameSessionInfo(args.sessionId);
322
+ if (!info) {
323
+ availableSessions = await httpClient.listFrameSessions();
324
+ }
325
+ } else {
326
+ info = await this.chromeController.getFrameSessionInfo(args.sessionId);
327
+ if (!info) {
328
+ availableSessions = await this.chromeController.listFrameSessions();
329
+ }
330
+ }
331
+ } catch (error) {
332
+ console.warn('[FrameToolHandler] HTTP client failed, falling back to direct access:', error.message);
333
+ info = await this.chromeController.getFrameSessionInfo(args.sessionId);
334
+ if (!info) {
335
+ availableSessions = await this.chromeController.listFrameSessions();
336
+ }
337
+ }
338
+
339
+ if (!info) {
340
+ const recentSessions = availableSessions ? availableSessions.slice(0, 5) : [];
341
+
342
+ let errorMsg = `Frame session not found: ${args.sessionId}\n\n`;
343
+ if (recentSessions.length > 0) {
344
+ errorMsg += 'Available frame sessions:\n';
345
+ recentSessions.forEach(s => {
346
+ errorMsg += ` - ${s.sessionId} (${s.totalFrames} frames, ${new Date(s.timestamp).toLocaleString()})\n`;
347
+ });
348
+ errorMsg += '\nTip: Frame recordings are stored in the local database and may not persist across different Chrome Debug instances.';
349
+ } else {
350
+ errorMsg += 'No frame sessions found in the database.\n';
351
+ errorMsg += 'Frame recordings may have been created in a different Chrome Debug instance or the database may have been reset.';
352
+ }
353
+
354
+ throw new Error(errorMsg);
355
+ }
356
+
357
+ // Get interaction summary for this session
358
+ let interactionSummary = {
359
+ totalInteractions: 0,
360
+ interactionTypes: [],
361
+ embeddedInteractions: 0,
362
+ timestampAssociated: 0
363
+ };
364
+
365
+ try {
366
+ if (this.useHttpClient) {
367
+ const interactionsResult = await httpClient.getScreenInteractions(args.sessionId);
368
+ const interactions = interactionsResult.interactions || [];
369
+ interactionSummary.totalInteractions = interactions.length;
370
+
371
+ // Count interaction types
372
+ const typeCount = {};
373
+ interactions.forEach(interaction => {
374
+ typeCount[interaction.type] = (typeCount[interaction.type] || 0) + 1;
375
+ });
376
+ interactionSummary.interactionTypes = Object.entries(typeCount).map(([type, count]) => `${type}: ${count}`);
377
+ } else {
378
+ const interactions = await this.chromeController.getScreenInteractions(args.sessionId);
379
+ interactionSummary.totalInteractions = interactions.length;
380
+
381
+ // Count interaction types
382
+ const typeCount = {};
383
+ interactions.forEach(interaction => {
384
+ typeCount[interaction.type] = (typeCount[interaction.type] || 0) + 1;
385
+ });
386
+ interactionSummary.interactionTypes = Object.entries(typeCount).map(([type, count]) => `${type}: ${count}`);
387
+ }
388
+ } catch (error) {
389
+ console.warn('[FrameToolHandler] Failed to get interaction summary:', error.message);
390
+ // Keep default empty summary
391
+ }
392
+
393
+ return {
394
+ sessionId: args.sessionId,
395
+ totalFrames: info.totalFrames,
396
+ created: new Date(info.timestamp).toISOString(),
397
+ frameTimestamps: info.frameTimestamps ?
398
+ info.frameTimestamps.slice(0, 5).join(', ') + (info.frameTimestamps.length > 5 ? '...' : '') :
399
+ 'N/A',
400
+ interactionSummary: interactionSummary
401
+ };
402
+ }
403
+
404
+ /**
405
+ * Handle get_frame tool
406
+ * @param {Object} args - Arguments with sessionId, frameIndex, and filter options
407
+ * @returns {Object} Frame data
408
+ */
409
+ async handleGetFrame(args) {
410
+ if (!args.sessionId || args.frameIndex === undefined) {
411
+ throw new Error('Session ID and frame index are required');
412
+ }
413
+
414
+ const logLevel = args.logLevel || 'all';
415
+ const maxLogs = args.maxLogs || 20;
416
+ const searchLogs = args.searchLogs;
417
+
418
+ let frame;
419
+
420
+ try {
421
+ if (this.useHttpClient) {
422
+ frame = await httpClient.getFrame(args.sessionId, args.frameIndex);
423
+ } else {
424
+ frame = await this.chromeController.getFrame(args.sessionId, args.frameIndex);
425
+ }
426
+ } catch (error) {
427
+ console.warn('[FrameToolHandler] HTTP client failed, falling back to direct access:', error.message);
428
+ frame = await this.chromeController.getFrame(args.sessionId, args.frameIndex);
429
+ }
430
+
431
+ if (!frame) {
432
+ throw new Error('Frame not found');
433
+ }
434
+
435
+ // Apply filtering to logs
436
+ let filteredLogs = frame.logs || [];
437
+
438
+ // Filter by log level
439
+ if (logLevel !== 'all') {
440
+ filteredLogs = filteredLogs.filter(log => log.level === logLevel);
441
+ }
442
+
443
+ // Filter by search text
444
+ if (searchLogs) {
445
+ const searchText = searchLogs.toLowerCase();
446
+ filteredLogs = filteredLogs.filter(log =>
447
+ log.message.toLowerCase().includes(searchText)
448
+ );
449
+ }
450
+
451
+ // Limit the number of logs
452
+ const totalMatchingLogs = filteredLogs.length;
453
+ if (maxLogs > 0 && filteredLogs.length > maxLogs) {
454
+ filteredLogs = filteredLogs.slice(0, maxLogs);
455
+ }
456
+
457
+ const result = {
458
+ frameIndex: args.frameIndex,
459
+ sessionId: args.sessionId,
460
+ timestamp: `${frame.timestamp}ms`,
461
+ imageSize: frame.imageData ? `${Math.round(frame.imageData.length / 1024)}KB` : 'N/A',
462
+ totalLogs: frame.logs ? frame.logs.length : 0,
463
+ filteredLogs: totalMatchingLogs,
464
+ showingLogs: filteredLogs.length,
465
+ logs: filteredLogs.map(log => ({
466
+ level: log.level.toUpperCase(),
467
+ message: log.message
468
+ })),
469
+ hasImageData: !!frame.imageData
470
+ };
471
+
472
+ if (logLevel !== 'all' || searchLogs) {
473
+ result.filters = {};
474
+ if (searchLogs) result.filters.searchText = searchLogs;
475
+ if (logLevel !== 'all') result.filters.logLevel = logLevel.toUpperCase();
476
+ }
477
+
478
+ if (totalMatchingLogs > 500) {
479
+ result.warning = `This frame has ${totalMatchingLogs} matching logs! Showing only first ${filteredLogs.length} to prevent output overflow.`;
480
+ result.recommendation = 'For better navigation, use: get_frame_logs_paginated';
481
+ }
482
+
483
+ if (filteredLogs.length < totalMatchingLogs) {
484
+ result.hasMoreLogs = true;
485
+ result.remainingLogs = totalMatchingLogs - filteredLogs.length;
486
+ if (totalMatchingLogs > 100) {
487
+ result.tip = 'Use get_frame_logs_paginated for better navigation with large log volumes';
488
+ } else {
489
+ result.tip = 'Use maxLogs=0 to see all, or search_frame_logs for cross-frame search';
490
+ }
491
+ }
492
+
493
+ // Show legacy embedded interactions for backward compatibility
494
+ if (frame.interactions && frame.interactions.length > 0) {
495
+ result.interactions = frame.interactions.map(interaction =>
496
+ this.formatInteraction(interaction)
497
+ );
498
+ }
499
+
500
+ // Show new timestamp-associated interactions
501
+ if (frame.associatedInteractions && frame.associatedInteractions.length > 0) {
502
+ result.associatedInteractions = frame.associatedInteractions.map(interaction =>
503
+ this.formatInteraction(interaction)
504
+ );
505
+ }
506
+
507
+ // Include interaction metadata
508
+ if (frame.interactionMetadata) {
509
+ result.interactionMetadata = frame.interactionMetadata;
510
+ }
511
+
512
+ return result;
513
+ }
514
+
515
+ /**
516
+ * Handle search_frame_logs tool
517
+ * @param {Object} args - Arguments with sessionId, searchText, and filters
518
+ * @returns {Object} Search results
519
+ */
520
+ async handleSearchFrameLogs(args) {
521
+ if (!args.sessionId || !args.searchText) {
522
+ throw new Error('Session ID and search text are required');
523
+ }
524
+
525
+ const logLevel = args.logLevel || 'all';
526
+ const maxResults = args.maxResults || 50;
527
+
528
+ let results;
529
+
530
+ try {
531
+ if (this.useHttpClient) {
532
+ results = await httpClient.searchFrameLogs(args.sessionId, args.searchText, logLevel, maxResults);
533
+ } else {
534
+ results = await this.chromeController.searchFrameLogs(args.sessionId, args.searchText, logLevel, maxResults);
535
+ }
536
+ } catch (error) {
537
+ console.warn('[FrameToolHandler] HTTP client failed, falling back to direct access:', error.message);
538
+ results = await this.chromeController.searchFrameLogs(args.sessionId, args.searchText, logLevel, maxResults);
539
+ }
540
+
541
+ return {
542
+ sessionId: args.sessionId,
543
+ searchText: args.searchText,
544
+ logLevel: logLevel !== 'all' ? logLevel.toUpperCase() : 'ALL',
545
+ maxResults: maxResults,
546
+ foundResults: results.length,
547
+ results: results.map(result => ({
548
+ frameIndex: result.frameIndex,
549
+ level: result.level.toUpperCase(),
550
+ message: result.message,
551
+ timestamp: result.timestamp
552
+ }))
553
+ };
554
+ }
555
+
556
+ /**
557
+ * Handle get_frame_logs_paginated tool
558
+ * @param {Object} args - Arguments with sessionId, frameIndex, pagination, and filters
559
+ * @returns {Object} Paginated logs
560
+ */
561
+ async handleGetFrameLogsPaginated(args) {
562
+ if (!args.sessionId || args.frameIndex === undefined) {
563
+ throw new Error('Session ID and frame index are required');
564
+ }
565
+
566
+ const offset = args.offset || 0;
567
+ const limit = args.limit || 100;
568
+ const logLevel = args.logLevel || 'all';
569
+ const searchText = args.searchText;
570
+
571
+ let results;
572
+
573
+ try {
574
+ if (this.useHttpClient) {
575
+ results = await httpClient.getFrameLogsPaginated(args.sessionId, args.frameIndex, offset, limit, logLevel, searchText);
576
+ } else {
577
+ results = await this.chromeController.getFrameLogsPaginated(args.sessionId, args.frameIndex, offset, limit, logLevel, searchText);
578
+ }
579
+ } catch (error) {
580
+ console.warn('[FrameToolHandler] HTTP client failed, falling back to direct access:', error.message);
581
+ results = await this.chromeController.getFrameLogsPaginated(args.sessionId, args.frameIndex, offset, limit, logLevel, searchText);
582
+ }
583
+
584
+ return {
585
+ sessionId: args.sessionId,
586
+ frameIndex: args.frameIndex,
587
+ offset: offset,
588
+ limit: limit,
589
+ logLevel: logLevel !== 'all' ? logLevel.toUpperCase() : 'ALL',
590
+ searchText: searchText || null,
591
+ totalLogs: results.total,
592
+ returnedLogs: results.logs.length,
593
+ hasMore: offset + results.logs.length < results.total,
594
+ logs: results.logs.map(log => ({
595
+ level: log.level.toUpperCase(),
596
+ message: log.message,
597
+ relativeTime: log.relative_time
598
+ }))
599
+ };
600
+ }
601
+
602
+ /**
603
+ * Handle get_frame_screenshot tool
604
+ * @param {Object} args - Arguments with sessionId, frameIndex, and includeMetadata
605
+ * @returns {Object} Frame screenshot data
606
+ */
607
+ async handleGetFrameScreenshot(args) {
608
+ if (!args.sessionId || args.frameIndex === undefined) {
609
+ throw new Error('Session ID and frame index are required');
610
+ }
611
+
612
+ const includeMetadata = args.includeMetadata || false;
613
+
614
+ try {
615
+ // Get frame data directly from database to access image_data
616
+ const { database } = await import('../../database.js');
617
+ const recording = database.getRecording(args.sessionId);
618
+
619
+ if (!recording) {
620
+ throw new Error(`Frame session not found: ${args.sessionId}`);
621
+ }
622
+
623
+ // Get the specific frame with image data
624
+ const frameStmt = database.db.prepare(`
625
+ SELECT frame_index, timestamp, absolute_timestamp, image_data
626
+ FROM frames
627
+ WHERE recording_id = ? AND frame_index = ?
628
+ `);
629
+ const frame = frameStmt.get(recording.id, args.frameIndex);
630
+
631
+ if (!frame) {
632
+ // Get total frames to provide helpful error
633
+ const countStmt = database.db.prepare(`SELECT COUNT(*) as count FROM frames WHERE recording_id = ?`);
634
+ const totalFrames = countStmt.get(recording.id).count;
635
+ throw new Error(`Frame ${args.frameIndex} not found in session ${args.sessionId}. Available frames: 0-${totalFrames - 1}`);
636
+ }
637
+
638
+ if (!frame.image_data) {
639
+ throw new Error(`No screenshot data available for frame ${args.frameIndex} in session ${args.sessionId}`);
640
+ }
641
+
642
+ const result = {
643
+ sessionId: args.sessionId,
644
+ frameIndex: args.frameIndex,
645
+ timestamp: `${frame.timestamp}ms`,
646
+ absoluteTimestamp: frame.absolute_timestamp,
647
+ imageData: frame.image_data, // Base64 encoded image data
648
+ imageFormat: 'jpeg', // Chrome Debug stores as JPEG by default
649
+ hasImageData: true
650
+ };
651
+
652
+ if (includeMetadata) {
653
+ // Calculate approximate image size
654
+ const imageSize = Math.round((frame.image_data.length * 3) / 4); // Base64 to bytes conversion
655
+ result.metadata = {
656
+ imageSizeBytes: imageSize,
657
+ imageSizeFormatted: imageSize > 1024 ? `${Math.round(imageSize / 1024)}KB` : `${imageSize}B`,
658
+ encoding: 'base64',
659
+ captureTime: new Date(frame.absolute_timestamp).toISOString()
660
+ };
661
+ }
662
+
663
+ return result;
664
+
665
+ } catch (error) {
666
+ if (error.message.includes('not found') || error.message.includes('No screenshot data')) {
667
+ throw error;
668
+ }
669
+
670
+ // For other errors, provide more context
671
+ throw new Error(`Failed to retrieve screenshot for frame ${args.frameIndex}: ${error.message}`);
672
+ }
673
+ }
674
+
675
+ /**
676
+ * Handle get_screen_interactions tool
677
+ * @param {Object} args - Arguments with sessionId and filters
678
+ * @returns {Object} Screen interactions
679
+ */
680
+ async handleGetScreenInteractions(args) {
681
+ if (!args.sessionId) {
682
+ throw new Error('Session ID is required');
683
+ }
684
+
685
+ let interactions;
686
+
687
+ try {
688
+ if (this.useHttpClient) {
689
+ const result = await httpClient.getScreenInteractions(args.sessionId, args.frameIndex, args.type);
690
+ interactions = result.interactions || [];
691
+ } else {
692
+ if (args.frameIndex !== undefined) {
693
+ // Get interactions for specific frame
694
+ interactions = await this.chromeController.getFrameInteractions(args.sessionId, args.frameIndex);
695
+ } else {
696
+ // Get all interactions for the recording
697
+ interactions = await this.chromeController.getScreenInteractions(args.sessionId);
698
+ }
699
+ }
700
+ } catch (error) {
701
+ console.warn('[FrameToolHandler] HTTP client failed, falling back to direct access:', error.message);
702
+ if (args.frameIndex !== undefined) {
703
+ interactions = await this.chromeController.getFrameInteractions(args.sessionId, args.frameIndex);
704
+ } else {
705
+ interactions = await this.chromeController.getScreenInteractions(args.sessionId);
706
+ }
707
+ }
708
+
709
+ // Filter by type if requested
710
+ if (args.type) {
711
+ interactions = interactions.filter(i => i.type === args.type);
712
+ }
713
+
714
+ if (interactions.length === 0) {
715
+ return {
716
+ sessionId: args.sessionId,
717
+ frameIndex: args.frameIndex,
718
+ type: args.type || null,
719
+ message: `No interactions found for session ${args.sessionId}${args.frameIndex !== undefined ? ` frame ${args.frameIndex}` : ''}${args.type ? ` of type '${args.type}'` : ''}.`,
720
+ interactions: []
721
+ };
722
+ }
723
+
724
+ return {
725
+ sessionId: args.sessionId,
726
+ frameIndex: args.frameIndex,
727
+ type: args.type || null,
728
+ totalInteractions: interactions.length,
729
+ interactions: interactions.map((interaction, index) => ({
730
+ index: index + 1,
731
+ ...this.formatInteraction(interaction),
732
+ timestamp: new Date(interaction.timestamp).toISOString()
733
+ }))
734
+ };
735
+ }
736
+
737
+ /**
738
+ * Format interaction data for consistent output
739
+ * @param {Object} interaction - Raw interaction data
740
+ * @returns {Object} Formatted interaction
741
+ */
742
+ formatInteraction(interaction) {
743
+ const formatted = {
744
+ type: interaction.type.toUpperCase(),
745
+ frameIndex: interaction.frame_index !== null ? interaction.frame_index : null
746
+ };
747
+
748
+ switch (interaction.type) {
749
+ case 'click':
750
+ formatted.target = interaction.selector || `at (${interaction.x}, ${interaction.y})`;
751
+ if (interaction.text) formatted.text = interaction.text;
752
+ if (interaction.xpath) formatted.xpath = interaction.xpath;
753
+ break;
754
+ case 'input':
755
+ formatted.selector = interaction.selector;
756
+ formatted.value = interaction.value;
757
+ if (interaction.placeholder) formatted.placeholder = interaction.placeholder;
758
+ break;
759
+ case 'keypress':
760
+ formatted.key = interaction.key;
761
+ break;
762
+ case 'scroll':
763
+ formatted.position = {
764
+ x: interaction.scrollX || interaction.x,
765
+ y: interaction.scrollY || interaction.y
766
+ };
767
+ break;
768
+ default:
769
+ // Include any additional properties for unknown types
770
+ Object.keys(interaction).forEach(key => {
771
+ if (!['type', 'frame_index', 'timestamp'].includes(key)) {
772
+ formatted[key] = interaction[key];
773
+ }
774
+ });
775
+ }
776
+
777
+ return formatted;
778
+ }
779
+
780
+ /*
781
+ * SNAPSHOT FEATURE DISABLED (2025-10-01)
782
+ *
783
+ * Snapshot handler methods disabled - see SNAPSHOT_FEATURE_DISABLED.md
784
+ *
785
+ * WHY DISABLED:
786
+ * - Snapshots without console logs are just screenshots (users can do this natively)
787
+ * - Console log capture requires always-on monitoring (privacy concern)
788
+ * - Core value proposition (screenshot + searchable logs) cannot be achieved cleanly
789
+ *
790
+ * TO RE-ENABLE:
791
+ * 1. Implement privacy-conscious always-on log monitoring
792
+ * 2. Uncomment these methods
793
+ * 3. Re-enable MCP tool definitions in tools/index.js
794
+ * 4. Re-enable case handlers above
795
+ *
796
+ * Handle take_snapshot tool - creates a new snapshot
797
+ * @param {Object} args - Tool arguments
798
+ * @returns {Object} Snapshot creation result
799
+ */
800
+ /*
801
+ async handleTakeSnapshot(args) {
802
+ try {
803
+ // Take screenshot and get current logs
804
+ const screenshot = await this.chromeController.takeScreenshot(true, true, 30);
805
+ const logs = await this.chromeController.getLogs();
806
+
807
+ // Generate unique session ID for the snapshot
808
+ const sessionId = `snapshot_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
809
+
810
+ // Create frame data structure
811
+ const frameData = {
812
+ timestamp: Date.now(),
813
+ absoluteTimestamp: Date.now(),
814
+ imageData: screenshot.imageData,
815
+ logs: logs.logs || [],
816
+ frameIndex: 0,
817
+ index: 0
818
+ };
819
+
820
+ // Store snapshot via database
821
+ const result = await this.chromeController.database.storeSnapshot(sessionId, frameData, args.userNote);
822
+
823
+ return {
824
+ success: true,
825
+ sessionId: result,
826
+ timestamp: frameData.timestamp,
827
+ userNote: args.userNote || null,
828
+ logCount: frameData.logs.length,
829
+ message: `Snapshot created successfully with ID: ${result}`
830
+ };
831
+ } catch (error) {
832
+ console.error('Error taking snapshot:', error);
833
+ throw new Error(`Failed to take snapshot: ${error.message}`);
834
+ }
835
+ }
836
+
837
+ // Handle list_snapshots tool - lists all snapshots
838
+ // @param {Object} args - Tool arguments
839
+ // @returns {Object} List of snapshots
840
+ async handleListSnapshots(args) {
841
+ try {
842
+ const limit = args.limit || 50;
843
+ const snapshots = await this.chromeController.database.getSnapshots(limit);
844
+
845
+ return {
846
+ success: true,
847
+ snapshots: snapshots.map(snapshot => ({
848
+ sessionId: snapshot.id,
849
+ timestamp: snapshot.timestamp,
850
+ userNote: snapshot.user_note,
851
+ frameCount: snapshot.frame_count,
852
+ createdAt: snapshot.created_at,
853
+ recordingType: snapshot.recording_type
854
+ })),
855
+ total: snapshots.length,
856
+ limit: limit
857
+ };
858
+ } catch (error) {
859
+ console.error('Error listing snapshots:', error);
860
+ throw new Error(`Failed to list snapshots: ${error.message}`);
861
+ }
862
+ }
863
+
864
+ // Handle get_snapshot tool - gets a specific snapshot
865
+ // @param {Object} args - Tool arguments
866
+ // @returns {Object} Snapshot data
867
+ async handleGetSnapshot(args) {
868
+ try {
869
+ const { sessionId, includeMetadata = true } = args;
870
+
871
+ // Get the snapshot session (reuse existing frame session logic)
872
+ const snapshot = await this.chromeController.getFrameSession(sessionId);
873
+
874
+ if (!snapshot) {
875
+ throw new Error(`Snapshot not found: ${sessionId}`);
876
+ }
877
+
878
+ const result = {
879
+ sessionId: sessionId,
880
+ snapshot: {
881
+ timestamp: snapshot.timestamp,
882
+ userNote: snapshot.userNote,
883
+ frame: snapshot.frames?.[0] || null,
884
+ logCount: snapshot.frames?.[0]?.logs?.length || 0
885
+ }
886
+ };
887
+
888
+ if (includeMetadata) {
889
+ result.metadata = {
890
+ recordingType: 'snapshot',
891
+ frameCount: snapshot.frames?.length || 0,
892
+ createdAt: snapshot.created_at,
893
+ updatedAt: snapshot.updated_at
894
+ };
895
+ }
896
+
897
+ return result;
898
+ } catch (error) {
899
+ console.error('Error getting snapshot:', error);
900
+ throw new Error(`Failed to get snapshot: ${error.message}`);
901
+ }
902
+ }
903
+
904
+ // Handle search_snapshot_logs tool - searches logs within snapshots
905
+ // @param {Object} args - Tool arguments
906
+ // @returns {Object} Search results
907
+ async handleSearchSnapshotLogs(args) {
908
+ try {
909
+ const { query, level, limit = 50 } = args;
910
+
911
+ // Get all snapshots first
912
+ const snapshots = await this.chromeController.database.getSnapshots(100);
913
+
914
+ const results = [];
915
+
916
+ for (const snapshot of snapshots) {
917
+ try {
918
+ const sessionData = await this.chromeController.getFrameSession(snapshot.id);
919
+ if (sessionData?.frames?.[0]?.logs) {
920
+ const logs = sessionData.frames[0].logs;
921
+
922
+ // Filter logs by query and level
923
+ const matchingLogs = logs.filter(log => {
924
+ const queryMatch = log.message.toLowerCase().includes(query.toLowerCase());
925
+ const levelMatch = !level || log.level === level;
926
+ return queryMatch && levelMatch;
927
+ });
928
+
929
+ if (matchingLogs.length > 0) {
930
+ results.push({
931
+ sessionId: snapshot.id,
932
+ userNote: snapshot.user_note,
933
+ timestamp: snapshot.timestamp,
934
+ matchingLogs: matchingLogs.slice(0, limit)
935
+ });
936
+ }
937
+ }
938
+ } catch (error) {
939
+ console.warn(`Error processing snapshot ${snapshot.id}:`, error.message);
940
+ }
941
+ }
942
+
943
+ return {
944
+ success: true,
945
+ query: query,
946
+ level: level || 'all',
947
+ totalSnapshots: snapshots.length,
948
+ snapshotsWithMatches: results.length,
949
+ results: results.slice(0, limit)
950
+ };
951
+ } catch (error) {
952
+ console.error('Error searching snapshot logs:', error);
953
+ throw new Error(`Failed to search snapshot logs: ${error.message}`);
954
+ }
955
+ }
956
+ */
957
+ }