@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.
- package/CLAUDE.md +344 -0
- package/LICENSE +21 -0
- package/README.md +250 -0
- package/chrome-extension/README.md +41 -0
- package/chrome-extension/background.js +3917 -0
- package/chrome-extension/chrome-session-manager.js +706 -0
- package/chrome-extension/content.css +181 -0
- package/chrome-extension/content.js +3022 -0
- package/chrome-extension/data-buffer.js +435 -0
- package/chrome-extension/dom-tracker.js +411 -0
- package/chrome-extension/extension-config.js +78 -0
- package/chrome-extension/firebase-client.js +278 -0
- package/chrome-extension/firebase-config.js +32 -0
- package/chrome-extension/firebase-config.module.js +22 -0
- package/chrome-extension/firebase-config.module.template.js +27 -0
- package/chrome-extension/firebase-config.template.js +36 -0
- package/chrome-extension/frame-capture.js +407 -0
- package/chrome-extension/icon128.png +1 -0
- package/chrome-extension/icon16.png +1 -0
- package/chrome-extension/icon48.png +1 -0
- package/chrome-extension/license-helper.js +181 -0
- package/chrome-extension/logger.js +23 -0
- package/chrome-extension/manifest.json +73 -0
- package/chrome-extension/network-tracker.js +510 -0
- package/chrome-extension/offscreen.html +10 -0
- package/chrome-extension/options.html +203 -0
- package/chrome-extension/options.js +282 -0
- package/chrome-extension/pako.min.js +2 -0
- package/chrome-extension/performance-monitor.js +533 -0
- package/chrome-extension/pii-redactor.js +405 -0
- package/chrome-extension/popup.html +532 -0
- package/chrome-extension/popup.js +2446 -0
- package/chrome-extension/upload-manager.js +323 -0
- package/chrome-extension/web-vitals.iife.js +1 -0
- package/config/api-keys.json +11 -0
- package/config/chrome-pilot-config.json +45 -0
- package/package.json +126 -0
- package/scripts/cleanup-processes.js +109 -0
- package/scripts/config-manager.js +280 -0
- package/scripts/generate-extension-config.js +53 -0
- package/scripts/setup-security.js +64 -0
- package/src/capture/architecture.js +426 -0
- package/src/capture/error-handling-tests.md +38 -0
- package/src/capture/error-handling-types.ts +360 -0
- package/src/capture/index.js +508 -0
- package/src/capture/interfaces.js +625 -0
- package/src/capture/memory-manager.js +713 -0
- package/src/capture/types.js +342 -0
- package/src/chrome-controller.js +2658 -0
- package/src/cli.js +19 -0
- package/src/config-loader.js +303 -0
- package/src/database.js +2178 -0
- package/src/firebase-license-manager.js +462 -0
- package/src/firebase-privacy-guard.js +397 -0
- package/src/http-server.js +1516 -0
- package/src/index-direct.js +157 -0
- package/src/index-modular.js +219 -0
- package/src/index-monolithic-backup.js +2230 -0
- package/src/index.js +305 -0
- package/src/legacy/chrome-controller-old.js +1406 -0
- package/src/legacy/index-express.js +625 -0
- package/src/legacy/index-old.js +977 -0
- package/src/legacy/routes.js +260 -0
- package/src/legacy/shared-storage.js +101 -0
- package/src/logger.js +10 -0
- package/src/mcp/handlers/chrome-tool-handler.js +306 -0
- package/src/mcp/handlers/element-tool-handler.js +51 -0
- package/src/mcp/handlers/frame-tool-handler.js +957 -0
- package/src/mcp/handlers/request-handler.js +104 -0
- package/src/mcp/handlers/workflow-tool-handler.js +636 -0
- package/src/mcp/server.js +68 -0
- package/src/mcp/tools/index.js +701 -0
- package/src/middleware/auth.js +371 -0
- package/src/middleware/security.js +267 -0
- package/src/port-discovery.js +258 -0
- package/src/routes/admin.js +182 -0
- package/src/services/browser-daemon.js +494 -0
- package/src/services/chrome-service.js +375 -0
- package/src/services/failover-manager.js +412 -0
- package/src/services/git-safety-service.js +675 -0
- package/src/services/heartbeat-manager.js +200 -0
- package/src/services/http-client.js +195 -0
- package/src/services/process-manager.js +318 -0
- package/src/services/process-tracker.js +574 -0
- package/src/services/profile-manager.js +449 -0
- package/src/services/project-manager.js +415 -0
- package/src/services/session-manager.js +497 -0
- package/src/services/session-registry.js +491 -0
- package/src/services/unified-session-manager.js +678 -0
- package/src/shared-storage-old.js +267 -0
- package/src/standalone-server.js +53 -0
- package/src/utils/extension-path.js +145 -0
- package/src/utils.js +187 -0
- package/src/validation/log-transformer.js +125 -0
- 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
|
+
}
|