@dynamicu/chromedebug-mcp 2.6.7 → 2.7.1
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 +17 -1
- package/README.md +1 -1
- package/chrome-extension/activation-manager.js +10 -10
- package/chrome-extension/background.js +1045 -736
- package/chrome-extension/browser-recording-manager.js +1 -1
- package/chrome-extension/chrome-debug-logger.js +168 -0
- package/chrome-extension/chrome-session-manager.js +5 -5
- package/chrome-extension/console-interception-library.js +430 -0
- package/chrome-extension/content.css +16 -16
- package/chrome-extension/content.js +739 -221
- package/chrome-extension/data-buffer.js +5 -5
- package/chrome-extension/dom-tracker.js +9 -9
- package/chrome-extension/extension-config.js +1 -1
- package/chrome-extension/firebase-client.js +13 -13
- package/chrome-extension/frame-capture.js +20 -38
- package/chrome-extension/license-helper.js +33 -7
- package/chrome-extension/manifest.free.json +3 -6
- package/chrome-extension/network-tracker.js +9 -9
- package/chrome-extension/options.html +10 -0
- package/chrome-extension/options.js +21 -8
- package/chrome-extension/performance-monitor.js +17 -17
- package/chrome-extension/popup.html +230 -193
- package/chrome-extension/popup.js +146 -458
- package/chrome-extension/pro/enhanced-capture.js +406 -0
- package/chrome-extension/pro/frame-editor.html +433 -0
- package/chrome-extension/pro/frame-editor.js +1567 -0
- package/chrome-extension/pro/function-tracker.js +843 -0
- package/chrome-extension/pro/jszip.min.js +13 -0
- package/chrome-extension/upload-manager.js +7 -7
- package/dist/chromedebug-extension-free.zip +0 -0
- package/package.json +3 -1
- package/scripts/webpack.config.free.cjs +8 -8
- package/scripts/webpack.config.pro.cjs +2 -0
- package/src/cli.js +2 -2
- package/src/database.js +55 -7
- package/src/index.js +9 -6
- package/src/mcp/server.js +2 -2
- package/src/services/process-manager.js +10 -6
- package/src/services/process-tracker.js +10 -5
- package/src/services/profile-manager.js +17 -2
- package/src/validation/schemas.js +12 -11
- package/src/index-direct.js +0 -157
- package/src/index-modular.js +0 -219
- package/src/index-monolithic-backup.js +0 -2230
- package/src/legacy/chrome-controller-old.js +0 -1406
- package/src/legacy/index-express.js +0 -625
- package/src/legacy/index-old.js +0 -977
- package/src/legacy/routes.js +0 -260
- package/src/legacy/shared-storage.js +0 -101
|
@@ -1,2230 +0,0 @@
|
|
|
1
|
-
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
-
import {
|
|
4
|
-
ListToolsRequestSchema,
|
|
5
|
-
CallToolRequestSchema
|
|
6
|
-
} from '@modelcontextprotocol/sdk/types.js';
|
|
7
|
-
import { ChromeController } from './chrome-controller.js';
|
|
8
|
-
import { discoverServer } from './port-discovery.js';
|
|
9
|
-
import { startHttpServer, startWebSocketServer } from './http-server.js';
|
|
10
|
-
|
|
11
|
-
// Create a single Chrome controller instance for this MCP server
|
|
12
|
-
const chromeController = new ChromeController();
|
|
13
|
-
|
|
14
|
-
// Track the HTTP server port for the Chrome extension
|
|
15
|
-
let httpServerPort = null;
|
|
16
|
-
|
|
17
|
-
const server = new Server(
|
|
18
|
-
{
|
|
19
|
-
name: 'chrome-pilot',
|
|
20
|
-
version: '1.0.0',
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
capabilities: {
|
|
24
|
-
tools: {},
|
|
25
|
-
},
|
|
26
|
-
}
|
|
27
|
-
);
|
|
28
|
-
|
|
29
|
-
const tools = [
|
|
30
|
-
{
|
|
31
|
-
name: 'launch_chrome',
|
|
32
|
-
description: 'Launch a Chrome browser instance for this MCP server',
|
|
33
|
-
inputSchema: {
|
|
34
|
-
type: 'object',
|
|
35
|
-
properties: {},
|
|
36
|
-
},
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
name: 'connect_to_existing_chrome',
|
|
40
|
-
description: 'Connect to an existing Chrome instance with debugging enabled',
|
|
41
|
-
inputSchema: {
|
|
42
|
-
type: 'object',
|
|
43
|
-
properties: {
|
|
44
|
-
port: {
|
|
45
|
-
type: 'number',
|
|
46
|
-
description: 'Debugging port Chrome is running on (default: 9222)',
|
|
47
|
-
default: 9222,
|
|
48
|
-
},
|
|
49
|
-
},
|
|
50
|
-
},
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
name: 'navigate_to',
|
|
54
|
-
description: 'Navigate to a URL',
|
|
55
|
-
inputSchema: {
|
|
56
|
-
type: 'object',
|
|
57
|
-
properties: {
|
|
58
|
-
url: {
|
|
59
|
-
type: 'string',
|
|
60
|
-
description: 'URL to navigate to',
|
|
61
|
-
},
|
|
62
|
-
},
|
|
63
|
-
required: ['url'],
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
name: 'pause_execution',
|
|
68
|
-
description: 'Pause Chrome execution',
|
|
69
|
-
inputSchema: {
|
|
70
|
-
type: 'object',
|
|
71
|
-
properties: {},
|
|
72
|
-
},
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
name: 'resume_execution',
|
|
76
|
-
description: 'Resume Chrome execution',
|
|
77
|
-
inputSchema: {
|
|
78
|
-
type: 'object',
|
|
79
|
-
properties: {},
|
|
80
|
-
},
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
name: 'step_over',
|
|
84
|
-
description: 'Step over in Chrome debugger',
|
|
85
|
-
inputSchema: {
|
|
86
|
-
type: 'object',
|
|
87
|
-
properties: {},
|
|
88
|
-
},
|
|
89
|
-
},
|
|
90
|
-
{
|
|
91
|
-
name: 'evaluate_expression',
|
|
92
|
-
description: 'Evaluate a JavaScript expression in Chrome',
|
|
93
|
-
inputSchema: {
|
|
94
|
-
type: 'object',
|
|
95
|
-
properties: {
|
|
96
|
-
expression: {
|
|
97
|
-
type: 'string',
|
|
98
|
-
description: 'JavaScript expression to evaluate',
|
|
99
|
-
},
|
|
100
|
-
},
|
|
101
|
-
required: ['expression'],
|
|
102
|
-
},
|
|
103
|
-
},
|
|
104
|
-
{
|
|
105
|
-
name: 'get_scopes',
|
|
106
|
-
description: 'Get scope variables from the top call frame',
|
|
107
|
-
inputSchema: {
|
|
108
|
-
type: 'object',
|
|
109
|
-
properties: {},
|
|
110
|
-
},
|
|
111
|
-
},
|
|
112
|
-
{
|
|
113
|
-
name: 'set_breakpoint',
|
|
114
|
-
description: 'Set a breakpoint at a specific URL and line',
|
|
115
|
-
inputSchema: {
|
|
116
|
-
type: 'object',
|
|
117
|
-
properties: {
|
|
118
|
-
url: {
|
|
119
|
-
type: 'string',
|
|
120
|
-
description: 'URL of the script',
|
|
121
|
-
},
|
|
122
|
-
lineNumber: {
|
|
123
|
-
type: 'number',
|
|
124
|
-
description: 'Line number for the breakpoint',
|
|
125
|
-
},
|
|
126
|
-
},
|
|
127
|
-
required: ['url', 'lineNumber'],
|
|
128
|
-
},
|
|
129
|
-
},
|
|
130
|
-
{
|
|
131
|
-
name: 'get_logs',
|
|
132
|
-
description: 'Get recent console logs',
|
|
133
|
-
inputSchema: {
|
|
134
|
-
type: 'object',
|
|
135
|
-
properties: {},
|
|
136
|
-
},
|
|
137
|
-
},
|
|
138
|
-
{
|
|
139
|
-
name: 'check_connection',
|
|
140
|
-
description: 'Check if Chrome is connected',
|
|
141
|
-
inputSchema: {
|
|
142
|
-
type: 'object',
|
|
143
|
-
properties: {},
|
|
144
|
-
},
|
|
145
|
-
},
|
|
146
|
-
{
|
|
147
|
-
name: 'force_reset',
|
|
148
|
-
description: 'Force reset Chrome by killing all processes and clearing state',
|
|
149
|
-
inputSchema: {
|
|
150
|
-
type: 'object',
|
|
151
|
-
properties: {},
|
|
152
|
-
},
|
|
153
|
-
},
|
|
154
|
-
{
|
|
155
|
-
name: 'take_screenshot',
|
|
156
|
-
description: 'Take a screenshot optimized for AI processing',
|
|
157
|
-
inputSchema: {
|
|
158
|
-
type: 'object',
|
|
159
|
-
properties: {
|
|
160
|
-
type: {
|
|
161
|
-
type: 'string',
|
|
162
|
-
description: 'Image type (default: jpeg)',
|
|
163
|
-
enum: ['png', 'jpeg'],
|
|
164
|
-
},
|
|
165
|
-
fullPage: {
|
|
166
|
-
type: 'boolean',
|
|
167
|
-
description: 'Capture full page (default: true, but limited to 600px height for AI)',
|
|
168
|
-
},
|
|
169
|
-
lowRes: {
|
|
170
|
-
type: 'boolean',
|
|
171
|
-
description: 'Low-resolution mode for AI (default: true)',
|
|
172
|
-
},
|
|
173
|
-
quality: {
|
|
174
|
-
type: 'number',
|
|
175
|
-
description: 'JPEG quality (1-100, default: 30)',
|
|
176
|
-
minimum: 1,
|
|
177
|
-
maximum: 100,
|
|
178
|
-
},
|
|
179
|
-
path: {
|
|
180
|
-
type: 'string',
|
|
181
|
-
description: 'Save screenshot to file path',
|
|
182
|
-
},
|
|
183
|
-
},
|
|
184
|
-
},
|
|
185
|
-
},
|
|
186
|
-
{
|
|
187
|
-
name: 'get_page_content',
|
|
188
|
-
description: 'Get the content of the current page including text, HTML, and DOM structure. Useful for checking if specific content exists without taking a screenshot.',
|
|
189
|
-
inputSchema: {
|
|
190
|
-
type: 'object',
|
|
191
|
-
properties: {
|
|
192
|
-
includeText: {
|
|
193
|
-
type: 'boolean',
|
|
194
|
-
description: 'Include extracted text content (default: true)',
|
|
195
|
-
},
|
|
196
|
-
includeHtml: {
|
|
197
|
-
type: 'boolean',
|
|
198
|
-
description: 'Include full HTML source (default: false, can be large)',
|
|
199
|
-
},
|
|
200
|
-
includeStructure: {
|
|
201
|
-
type: 'boolean',
|
|
202
|
-
description: 'Include DOM structure with important elements and attributes (default: true)',
|
|
203
|
-
},
|
|
204
|
-
},
|
|
205
|
-
},
|
|
206
|
-
},
|
|
207
|
-
{
|
|
208
|
-
name: 'get_selected_element',
|
|
209
|
-
description: 'Get information about the currently selected element from Chrome extension',
|
|
210
|
-
inputSchema: {
|
|
211
|
-
type: 'object',
|
|
212
|
-
properties: {},
|
|
213
|
-
},
|
|
214
|
-
},
|
|
215
|
-
{
|
|
216
|
-
name: 'apply_css_to_selected',
|
|
217
|
-
description: 'Apply CSS styles to the currently selected element',
|
|
218
|
-
inputSchema: {
|
|
219
|
-
type: 'object',
|
|
220
|
-
properties: {
|
|
221
|
-
css: {
|
|
222
|
-
type: 'string',
|
|
223
|
-
description: 'CSS rules to apply',
|
|
224
|
-
},
|
|
225
|
-
},
|
|
226
|
-
required: ['css'],
|
|
227
|
-
},
|
|
228
|
-
},
|
|
229
|
-
{
|
|
230
|
-
name: 'execute_js_on_selected',
|
|
231
|
-
description: 'Execute JavaScript code on the currently selected element',
|
|
232
|
-
inputSchema: {
|
|
233
|
-
type: 'object',
|
|
234
|
-
properties: {
|
|
235
|
-
code: {
|
|
236
|
-
type: 'string',
|
|
237
|
-
description: 'JavaScript code to execute. The element is available as the "element" variable.',
|
|
238
|
-
},
|
|
239
|
-
},
|
|
240
|
-
required: ['code'],
|
|
241
|
-
},
|
|
242
|
-
},
|
|
243
|
-
{
|
|
244
|
-
name: 'clear_selected_element',
|
|
245
|
-
description: 'Clear the currently selected element',
|
|
246
|
-
inputSchema: {
|
|
247
|
-
type: 'object',
|
|
248
|
-
properties: {},
|
|
249
|
-
},
|
|
250
|
-
},
|
|
251
|
-
{
|
|
252
|
-
name: 'get_websocket_info',
|
|
253
|
-
description: 'Get WebSocket server information for Chrome extension',
|
|
254
|
-
inputSchema: {
|
|
255
|
-
type: 'object',
|
|
256
|
-
properties: {},
|
|
257
|
-
},
|
|
258
|
-
},
|
|
259
|
-
{
|
|
260
|
-
name: 'get_workflow_recording',
|
|
261
|
-
description: 'Get a complete workflow recording including actions and optional logs',
|
|
262
|
-
inputSchema: {
|
|
263
|
-
type: 'object',
|
|
264
|
-
properties: {
|
|
265
|
-
sessionId: {
|
|
266
|
-
type: 'string',
|
|
267
|
-
description: 'The workflow session ID',
|
|
268
|
-
},
|
|
269
|
-
},
|
|
270
|
-
required: ['sessionId'],
|
|
271
|
-
},
|
|
272
|
-
},
|
|
273
|
-
{
|
|
274
|
-
name: 'list_workflow_recordings',
|
|
275
|
-
description: 'List all workflow recordings stored in the database',
|
|
276
|
-
inputSchema: {
|
|
277
|
-
type: 'object',
|
|
278
|
-
properties: {},
|
|
279
|
-
},
|
|
280
|
-
},
|
|
281
|
-
{
|
|
282
|
-
name: 'save_restore_point',
|
|
283
|
-
description: 'Save a restore point for the current browser state including DOM, form values, storage, and cookies',
|
|
284
|
-
inputSchema: {
|
|
285
|
-
type: 'object',
|
|
286
|
-
properties: {
|
|
287
|
-
workflowId: {
|
|
288
|
-
type: 'string',
|
|
289
|
-
description: 'The workflow ID this restore point belongs to',
|
|
290
|
-
},
|
|
291
|
-
actionIndex: {
|
|
292
|
-
type: 'number',
|
|
293
|
-
description: 'The action index in the workflow where this restore point was created',
|
|
294
|
-
},
|
|
295
|
-
description: {
|
|
296
|
-
type: 'string',
|
|
297
|
-
description: 'Optional description of this restore point',
|
|
298
|
-
},
|
|
299
|
-
},
|
|
300
|
-
required: ['workflowId'],
|
|
301
|
-
},
|
|
302
|
-
},
|
|
303
|
-
{
|
|
304
|
-
name: 'restore_from_point',
|
|
305
|
-
description: 'Restore the browser to a previously saved restore point',
|
|
306
|
-
inputSchema: {
|
|
307
|
-
type: 'object',
|
|
308
|
-
properties: {
|
|
309
|
-
restorePointId: {
|
|
310
|
-
type: 'string',
|
|
311
|
-
description: 'The ID of the restore point to restore from',
|
|
312
|
-
},
|
|
313
|
-
},
|
|
314
|
-
required: ['restorePointId'],
|
|
315
|
-
},
|
|
316
|
-
},
|
|
317
|
-
{
|
|
318
|
-
name: 'list_restore_points',
|
|
319
|
-
description: 'List all restore points for a workflow',
|
|
320
|
-
inputSchema: {
|
|
321
|
-
type: 'object',
|
|
322
|
-
properties: {
|
|
323
|
-
workflowId: {
|
|
324
|
-
type: 'string',
|
|
325
|
-
description: 'The workflow ID to get restore points for',
|
|
326
|
-
},
|
|
327
|
-
},
|
|
328
|
-
required: ['workflowId'],
|
|
329
|
-
},
|
|
330
|
-
},
|
|
331
|
-
{
|
|
332
|
-
name: 'chrome_pilot_show_frames',
|
|
333
|
-
description: 'Display frame recording information in a compact format optimized for MCP tools. Shows frame metadata, console logs, and timestamps without including large image data.',
|
|
334
|
-
inputSchema: {
|
|
335
|
-
type: 'object',
|
|
336
|
-
properties: {
|
|
337
|
-
sessionId: {
|
|
338
|
-
type: 'string',
|
|
339
|
-
description: 'The frame session ID to display',
|
|
340
|
-
},
|
|
341
|
-
maxFrames: {
|
|
342
|
-
type: 'number',
|
|
343
|
-
description: 'Maximum number of frames to display (default: 10)',
|
|
344
|
-
default: 10,
|
|
345
|
-
},
|
|
346
|
-
showLogs: {
|
|
347
|
-
type: 'boolean',
|
|
348
|
-
description: 'Whether to show console logs for each frame (default: true)',
|
|
349
|
-
default: true,
|
|
350
|
-
},
|
|
351
|
-
showInteractions: {
|
|
352
|
-
type: 'boolean',
|
|
353
|
-
description: 'Whether to show user interactions for each frame (default: true)',
|
|
354
|
-
default: true,
|
|
355
|
-
},
|
|
356
|
-
logLevel: {
|
|
357
|
-
type: 'string',
|
|
358
|
-
description: 'Filter logs by level: error, warn, info, debug, log, or all (default: all)',
|
|
359
|
-
enum: ['error', 'warn', 'info', 'debug', 'log', 'all'],
|
|
360
|
-
default: 'all',
|
|
361
|
-
},
|
|
362
|
-
maxLogsPerFrame: {
|
|
363
|
-
type: 'number',
|
|
364
|
-
description: 'Maximum number of logs to show per frame (default: 10). Use 0 for no limit.',
|
|
365
|
-
default: 10,
|
|
366
|
-
},
|
|
367
|
-
},
|
|
368
|
-
required: ['sessionId'],
|
|
369
|
-
},
|
|
370
|
-
},
|
|
371
|
-
{
|
|
372
|
-
name: 'get_frame_session_info',
|
|
373
|
-
description: 'Get information about a frame capture session including total frames and timestamps',
|
|
374
|
-
inputSchema: {
|
|
375
|
-
type: 'object',
|
|
376
|
-
properties: {
|
|
377
|
-
sessionId: {
|
|
378
|
-
type: 'string',
|
|
379
|
-
description: 'The frame session ID returned when recording was saved',
|
|
380
|
-
},
|
|
381
|
-
},
|
|
382
|
-
required: ['sessionId'],
|
|
383
|
-
},
|
|
384
|
-
},
|
|
385
|
-
{
|
|
386
|
-
name: 'get_screen_interactions',
|
|
387
|
-
description: 'Get all user interactions (clicks, inputs, keypresses, scrolls) recorded during a screen recording session',
|
|
388
|
-
inputSchema: {
|
|
389
|
-
type: 'object',
|
|
390
|
-
properties: {
|
|
391
|
-
sessionId: {
|
|
392
|
-
type: 'string',
|
|
393
|
-
description: 'The screen recording session ID',
|
|
394
|
-
},
|
|
395
|
-
},
|
|
396
|
-
required: ['sessionId'],
|
|
397
|
-
},
|
|
398
|
-
},
|
|
399
|
-
{
|
|
400
|
-
name: 'get_frame',
|
|
401
|
-
description: 'Get a specific frame from a frame capture session',
|
|
402
|
-
inputSchema: {
|
|
403
|
-
type: 'object',
|
|
404
|
-
properties: {
|
|
405
|
-
sessionId: {
|
|
406
|
-
type: 'string',
|
|
407
|
-
description: 'The frame session ID',
|
|
408
|
-
},
|
|
409
|
-
frameIndex: {
|
|
410
|
-
type: 'number',
|
|
411
|
-
description: 'The frame index to retrieve (0-based)',
|
|
412
|
-
},
|
|
413
|
-
logLevel: {
|
|
414
|
-
type: 'string',
|
|
415
|
-
description: 'Filter logs by level: error, warn, info, debug, log, or all (default: all)',
|
|
416
|
-
enum: ['error', 'warn', 'info', 'debug', 'log', 'all'],
|
|
417
|
-
default: 'all',
|
|
418
|
-
},
|
|
419
|
-
maxLogs: {
|
|
420
|
-
type: 'number',
|
|
421
|
-
description: 'Maximum number of logs to show (default: 20). Use 0 for no limit.',
|
|
422
|
-
default: 20,
|
|
423
|
-
},
|
|
424
|
-
searchLogs: {
|
|
425
|
-
type: 'string',
|
|
426
|
-
description: 'Search for specific text in log messages (case insensitive)',
|
|
427
|
-
},
|
|
428
|
-
},
|
|
429
|
-
required: ['sessionId', 'frameIndex'],
|
|
430
|
-
},
|
|
431
|
-
},
|
|
432
|
-
{
|
|
433
|
-
name: 'search_frame_logs',
|
|
434
|
-
description: 'Search for specific logs across all frames in a recording session',
|
|
435
|
-
inputSchema: {
|
|
436
|
-
type: 'object',
|
|
437
|
-
properties: {
|
|
438
|
-
sessionId: {
|
|
439
|
-
type: 'string',
|
|
440
|
-
description: 'The frame session ID to search',
|
|
441
|
-
},
|
|
442
|
-
searchText: {
|
|
443
|
-
type: 'string',
|
|
444
|
-
description: 'Text to search for in log messages (case insensitive)',
|
|
445
|
-
},
|
|
446
|
-
logLevel: {
|
|
447
|
-
type: 'string',
|
|
448
|
-
description: 'Filter by log level: error, warn, info, debug, log, or all (default: all)',
|
|
449
|
-
enum: ['error', 'warn', 'info', 'debug', 'log', 'all'],
|
|
450
|
-
default: 'all',
|
|
451
|
-
},
|
|
452
|
-
maxResults: {
|
|
453
|
-
type: 'number',
|
|
454
|
-
description: 'Maximum number of matching logs to return (default: 50)',
|
|
455
|
-
default: 50,
|
|
456
|
-
},
|
|
457
|
-
},
|
|
458
|
-
required: ['sessionId', 'searchText'],
|
|
459
|
-
},
|
|
460
|
-
},
|
|
461
|
-
{
|
|
462
|
-
name: 'get_frame_logs_paginated',
|
|
463
|
-
description: 'Get logs for a specific frame with pagination to handle large log volumes',
|
|
464
|
-
inputSchema: {
|
|
465
|
-
type: 'object',
|
|
466
|
-
properties: {
|
|
467
|
-
sessionId: {
|
|
468
|
-
type: 'string',
|
|
469
|
-
description: 'The frame session ID',
|
|
470
|
-
},
|
|
471
|
-
frameIndex: {
|
|
472
|
-
type: 'number',
|
|
473
|
-
description: 'The frame index to retrieve logs from (0-based)',
|
|
474
|
-
},
|
|
475
|
-
logLevel: {
|
|
476
|
-
type: 'string',
|
|
477
|
-
description: 'Filter by log level: error, warn, info, debug, log, or all (default: all)',
|
|
478
|
-
enum: ['error', 'warn', 'info', 'debug', 'log', 'all'],
|
|
479
|
-
default: 'all',
|
|
480
|
-
},
|
|
481
|
-
offset: {
|
|
482
|
-
type: 'number',
|
|
483
|
-
description: 'Number of logs to skip (for pagination, default: 0)',
|
|
484
|
-
default: 0,
|
|
485
|
-
},
|
|
486
|
-
limit: {
|
|
487
|
-
type: 'number',
|
|
488
|
-
description: 'Maximum number of logs to return (default: 100)',
|
|
489
|
-
default: 100,
|
|
490
|
-
},
|
|
491
|
-
searchText: {
|
|
492
|
-
type: 'string',
|
|
493
|
-
description: 'Search for specific text in log messages (case insensitive)',
|
|
494
|
-
},
|
|
495
|
-
},
|
|
496
|
-
required: ['sessionId', 'frameIndex'],
|
|
497
|
-
},
|
|
498
|
-
},
|
|
499
|
-
{
|
|
500
|
-
name: 'play_workflow_recording',
|
|
501
|
-
description: 'Play a workflow recording by executing all recorded actions',
|
|
502
|
-
inputSchema: {
|
|
503
|
-
type: 'object',
|
|
504
|
-
properties: {
|
|
505
|
-
sessionId: {
|
|
506
|
-
type: 'string',
|
|
507
|
-
description: 'The workflow session ID to play',
|
|
508
|
-
},
|
|
509
|
-
speed: {
|
|
510
|
-
type: 'number',
|
|
511
|
-
description: 'Playback speed multiplier (1 = normal, 2 = 2x faster, 0.5 = half speed)',
|
|
512
|
-
default: 1,
|
|
513
|
-
},
|
|
514
|
-
},
|
|
515
|
-
required: ['sessionId'],
|
|
516
|
-
},
|
|
517
|
-
},
|
|
518
|
-
{
|
|
519
|
-
name: 'play_workflow_by_name',
|
|
520
|
-
description: 'Play a workflow recording by its name',
|
|
521
|
-
inputSchema: {
|
|
522
|
-
type: 'object',
|
|
523
|
-
properties: {
|
|
524
|
-
name: {
|
|
525
|
-
type: 'string',
|
|
526
|
-
description: 'The name of the workflow recording to play',
|
|
527
|
-
},
|
|
528
|
-
speed: {
|
|
529
|
-
type: 'number',
|
|
530
|
-
description: 'Playback speed multiplier (1 = normal, 2 = 2x faster, 0.5 = half speed)',
|
|
531
|
-
default: 1,
|
|
532
|
-
},
|
|
533
|
-
},
|
|
534
|
-
required: ['name'],
|
|
535
|
-
},
|
|
536
|
-
},
|
|
537
|
-
{
|
|
538
|
-
name: 'get_screen_interactions',
|
|
539
|
-
description: 'Get all user interactions from a screen recording session or for a specific frame',
|
|
540
|
-
inputSchema: {
|
|
541
|
-
type: 'object',
|
|
542
|
-
properties: {
|
|
543
|
-
sessionId: {
|
|
544
|
-
type: 'string',
|
|
545
|
-
description: 'The screen recording session ID',
|
|
546
|
-
},
|
|
547
|
-
frameIndex: {
|
|
548
|
-
type: 'number',
|
|
549
|
-
description: 'Optional: specific frame index to get interactions for',
|
|
550
|
-
},
|
|
551
|
-
type: {
|
|
552
|
-
type: 'string',
|
|
553
|
-
description: 'Optional: filter by interaction type (click, input, keypress, scroll)',
|
|
554
|
-
enum: ['click', 'input', 'keypress', 'scroll'],
|
|
555
|
-
},
|
|
556
|
-
},
|
|
557
|
-
required: ['sessionId'],
|
|
558
|
-
},
|
|
559
|
-
},
|
|
560
|
-
];
|
|
561
|
-
|
|
562
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
563
|
-
return { tools };
|
|
564
|
-
});
|
|
565
|
-
|
|
566
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
567
|
-
const { name, arguments: args } = request.params;
|
|
568
|
-
|
|
569
|
-
try {
|
|
570
|
-
switch (name) {
|
|
571
|
-
case 'launch_chrome': {
|
|
572
|
-
const result = await chromeController.launch();
|
|
573
|
-
|
|
574
|
-
return {
|
|
575
|
-
content: [
|
|
576
|
-
{
|
|
577
|
-
type: 'text',
|
|
578
|
-
text: JSON.stringify(result, null, 2),
|
|
579
|
-
},
|
|
580
|
-
],
|
|
581
|
-
};
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
case 'connect_to_existing_chrome': {
|
|
585
|
-
const port = args.port || 9222;
|
|
586
|
-
const result = await chromeController.connectToExisting(port);
|
|
587
|
-
|
|
588
|
-
return {
|
|
589
|
-
content: [
|
|
590
|
-
{
|
|
591
|
-
type: 'text',
|
|
592
|
-
text: JSON.stringify(result, null, 2),
|
|
593
|
-
},
|
|
594
|
-
],
|
|
595
|
-
};
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
case 'navigate_to': {
|
|
599
|
-
const result = await chromeController.navigateTo(args.url);
|
|
600
|
-
return {
|
|
601
|
-
content: [
|
|
602
|
-
{
|
|
603
|
-
type: 'text',
|
|
604
|
-
text: JSON.stringify(result, null, 2),
|
|
605
|
-
},
|
|
606
|
-
],
|
|
607
|
-
};
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
case 'pause_execution': {
|
|
611
|
-
const result = await chromeController.pause();
|
|
612
|
-
return {
|
|
613
|
-
content: [
|
|
614
|
-
{
|
|
615
|
-
type: 'text',
|
|
616
|
-
text: JSON.stringify(result, null, 2),
|
|
617
|
-
},
|
|
618
|
-
],
|
|
619
|
-
};
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
case 'resume_execution': {
|
|
623
|
-
const result = await chromeController.resume();
|
|
624
|
-
return {
|
|
625
|
-
content: [
|
|
626
|
-
{
|
|
627
|
-
type: 'text',
|
|
628
|
-
text: JSON.stringify(result, null, 2),
|
|
629
|
-
},
|
|
630
|
-
],
|
|
631
|
-
};
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
case 'step_over': {
|
|
635
|
-
const result = await chromeController.stepOver();
|
|
636
|
-
return {
|
|
637
|
-
content: [
|
|
638
|
-
{
|
|
639
|
-
type: 'text',
|
|
640
|
-
text: JSON.stringify(result, null, 2),
|
|
641
|
-
},
|
|
642
|
-
],
|
|
643
|
-
};
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
case 'evaluate_expression': {
|
|
647
|
-
const result = await chromeController.evaluate(args.expression);
|
|
648
|
-
return {
|
|
649
|
-
content: [
|
|
650
|
-
{
|
|
651
|
-
type: 'text',
|
|
652
|
-
text: JSON.stringify(result, null, 2),
|
|
653
|
-
},
|
|
654
|
-
],
|
|
655
|
-
};
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
case 'get_scopes': {
|
|
659
|
-
const result = await chromeController.getScopes();
|
|
660
|
-
return {
|
|
661
|
-
content: [
|
|
662
|
-
{
|
|
663
|
-
type: 'text',
|
|
664
|
-
text: JSON.stringify(result, null, 2),
|
|
665
|
-
},
|
|
666
|
-
],
|
|
667
|
-
};
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
case 'set_breakpoint': {
|
|
671
|
-
const result = await chromeController.setBreakpoint(args.url, args.lineNumber);
|
|
672
|
-
return {
|
|
673
|
-
content: [
|
|
674
|
-
{
|
|
675
|
-
type: 'text',
|
|
676
|
-
text: JSON.stringify(result, null, 2),
|
|
677
|
-
},
|
|
678
|
-
],
|
|
679
|
-
};
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
case 'get_logs': {
|
|
683
|
-
const result = chromeController.getLogs();
|
|
684
|
-
return {
|
|
685
|
-
content: [
|
|
686
|
-
{
|
|
687
|
-
type: 'text',
|
|
688
|
-
text: JSON.stringify(result, null, 2),
|
|
689
|
-
},
|
|
690
|
-
],
|
|
691
|
-
};
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
case 'check_connection': {
|
|
695
|
-
const status = await chromeController.getConnectionStatus();
|
|
696
|
-
return {
|
|
697
|
-
content: [
|
|
698
|
-
{
|
|
699
|
-
type: 'text',
|
|
700
|
-
text: JSON.stringify({
|
|
701
|
-
connected: status.connected,
|
|
702
|
-
debugPort: status.debugPort,
|
|
703
|
-
wsEndpoint: status.browserWSEndpoint
|
|
704
|
-
}, null, 2),
|
|
705
|
-
},
|
|
706
|
-
],
|
|
707
|
-
};
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
case 'force_reset': {
|
|
711
|
-
const result = await chromeController.forceReset();
|
|
712
|
-
|
|
713
|
-
return {
|
|
714
|
-
content: [
|
|
715
|
-
{
|
|
716
|
-
type: 'text',
|
|
717
|
-
text: JSON.stringify(result, null, 2),
|
|
718
|
-
},
|
|
719
|
-
],
|
|
720
|
-
};
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
case 'take_screenshot': {
|
|
724
|
-
const result = await chromeController.takeScreenshot(args);
|
|
725
|
-
|
|
726
|
-
if (result.saved) {
|
|
727
|
-
return {
|
|
728
|
-
content: [
|
|
729
|
-
{
|
|
730
|
-
type: 'text',
|
|
731
|
-
text: `Screenshot saved successfully!\n\nPath: ${result.path}\nType: ${result.type}\nFull page: ${result.fullPage}`,
|
|
732
|
-
},
|
|
733
|
-
],
|
|
734
|
-
};
|
|
735
|
-
} else if (result.truncated) {
|
|
736
|
-
return {
|
|
737
|
-
content: [
|
|
738
|
-
{
|
|
739
|
-
type: 'text',
|
|
740
|
-
text: `Screenshot preview (truncated):\n\nSize: ${result.size}\nType: ${result.type}\nFull page: ${result.fullPage}\n\n${result.message}`,
|
|
741
|
-
},
|
|
742
|
-
],
|
|
743
|
-
};
|
|
744
|
-
} else if (result.error) {
|
|
745
|
-
return {
|
|
746
|
-
content: [
|
|
747
|
-
{
|
|
748
|
-
type: 'text',
|
|
749
|
-
text: `Error taking screenshot: ${result.message}`,
|
|
750
|
-
},
|
|
751
|
-
],
|
|
752
|
-
isError: true,
|
|
753
|
-
};
|
|
754
|
-
} else if (result.lowRes) {
|
|
755
|
-
return {
|
|
756
|
-
content: [
|
|
757
|
-
{
|
|
758
|
-
type: 'text',
|
|
759
|
-
text: `Low-resolution screenshot captured for AI parsing\n\nSize: ${result.size}\nType: ${result.type}\nFull page: ${result.fullPage}\nQuality: ${result.quality || 'N/A'}\n\ndata:image/${result.type};base64,${result.screenshot}`,
|
|
760
|
-
},
|
|
761
|
-
],
|
|
762
|
-
};
|
|
763
|
-
} else {
|
|
764
|
-
return {
|
|
765
|
-
content: [
|
|
766
|
-
{
|
|
767
|
-
type: 'text',
|
|
768
|
-
text: `Screenshot captured!\n\nSize: ${result.size}\nType: ${result.type}\nFull page: ${result.fullPage}\n\nBase64 data: ${result.screenshot.substring(0, 100)}...\n\nNote: Full screenshot is too large to display. Use 'lowRes: true' for AI-parseable screenshots or 'path' to save to file.`,
|
|
769
|
-
},
|
|
770
|
-
],
|
|
771
|
-
};
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
case 'get_page_content': {
|
|
776
|
-
const result = await chromeController.getPageContent(args);
|
|
777
|
-
|
|
778
|
-
if (result.error) {
|
|
779
|
-
return {
|
|
780
|
-
content: [
|
|
781
|
-
{
|
|
782
|
-
type: 'text',
|
|
783
|
-
text: `Error getting page content: ${result.message}`,
|
|
784
|
-
},
|
|
785
|
-
],
|
|
786
|
-
isError: true,
|
|
787
|
-
};
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
let output = `Page Content Analysis:\n\n`;
|
|
791
|
-
output += `URL: ${result.url}\n`;
|
|
792
|
-
output += `Title: ${result.title}\n`;
|
|
793
|
-
|
|
794
|
-
if (Object.keys(result.meta).length > 0) {
|
|
795
|
-
output += `\nMeta Tags:\n`;
|
|
796
|
-
for (const [key, value] of Object.entries(result.meta)) {
|
|
797
|
-
output += ` ${key}: ${value}\n`;
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
output += `\nPage Statistics:\n`;
|
|
802
|
-
output += ` Forms: ${result.statistics.forms}\n`;
|
|
803
|
-
output += ` Images: ${result.statistics.images}\n`;
|
|
804
|
-
output += ` Links: ${result.statistics.links}\n`;
|
|
805
|
-
output += ` Scripts: ${result.statistics.scripts}\n`;
|
|
806
|
-
|
|
807
|
-
if (result.text !== undefined) {
|
|
808
|
-
output += `\nText Content (${result.textLength} characters):\n`;
|
|
809
|
-
if (result.textLength > 1000) {
|
|
810
|
-
output += result.text.substring(0, 1000) + '...\n';
|
|
811
|
-
output += `(Showing first 1000 of ${result.textLength} characters)\n`;
|
|
812
|
-
} else {
|
|
813
|
-
output += result.text + '\n';
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
if (result.structure !== undefined) {
|
|
818
|
-
output += `\nDOM Structure:\n`;
|
|
819
|
-
const formatStructure = (node, indent = '') => {
|
|
820
|
-
let str = `${indent}<${node.tag}`;
|
|
821
|
-
|
|
822
|
-
if (Object.keys(node.attributes).length > 0) {
|
|
823
|
-
for (const [attr, value] of Object.entries(node.attributes)) {
|
|
824
|
-
str += ` ${attr}="${value}"`;
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
str += '>';
|
|
828
|
-
|
|
829
|
-
if (node.text) {
|
|
830
|
-
str += ` ${node.text}`;
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
output += str + '\n';
|
|
834
|
-
|
|
835
|
-
if (node.children) {
|
|
836
|
-
for (const child of node.children) {
|
|
837
|
-
formatStructure(child, indent + ' ');
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
};
|
|
841
|
-
|
|
842
|
-
formatStructure(result.structure);
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
if (result.html !== undefined) {
|
|
846
|
-
output += `\nHTML Content (${result.htmlLength} characters):\n`;
|
|
847
|
-
if (result.htmlLength > 500) {
|
|
848
|
-
output += result.html.substring(0, 500) + '...\n';
|
|
849
|
-
output += `(Showing first 500 of ${result.htmlLength} characters)\n`;
|
|
850
|
-
} else {
|
|
851
|
-
output += result.html + '\n';
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
return {
|
|
856
|
-
content: [
|
|
857
|
-
{
|
|
858
|
-
type: 'text',
|
|
859
|
-
text: output,
|
|
860
|
-
},
|
|
861
|
-
],
|
|
862
|
-
};
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
case 'get_selected_element': {
|
|
866
|
-
// Check the controller's selected element
|
|
867
|
-
const result = await chromeController.getSelectedElement();
|
|
868
|
-
return {
|
|
869
|
-
content: [
|
|
870
|
-
{
|
|
871
|
-
type: 'text',
|
|
872
|
-
text: JSON.stringify(result, null, 2),
|
|
873
|
-
},
|
|
874
|
-
],
|
|
875
|
-
};
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
case 'apply_css_to_selected': {
|
|
879
|
-
const result = await chromeController.applyToSelectedElement(args.css);
|
|
880
|
-
return {
|
|
881
|
-
content: [
|
|
882
|
-
{
|
|
883
|
-
type: 'text',
|
|
884
|
-
text: JSON.stringify(result, null, 2),
|
|
885
|
-
},
|
|
886
|
-
],
|
|
887
|
-
};
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
case 'execute_js_on_selected': {
|
|
891
|
-
const result = await chromeController.executeOnSelectedElement(args.code);
|
|
892
|
-
return {
|
|
893
|
-
content: [
|
|
894
|
-
{
|
|
895
|
-
type: 'text',
|
|
896
|
-
text: JSON.stringify(result, null, 2),
|
|
897
|
-
},
|
|
898
|
-
],
|
|
899
|
-
};
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
case 'clear_selected_element': {
|
|
903
|
-
const result = await chromeController.clearSelectedElement();
|
|
904
|
-
return {
|
|
905
|
-
content: [
|
|
906
|
-
{
|
|
907
|
-
type: 'text',
|
|
908
|
-
text: JSON.stringify(result, null, 2),
|
|
909
|
-
},
|
|
910
|
-
],
|
|
911
|
-
};
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
case 'get_websocket_info': {
|
|
915
|
-
return {
|
|
916
|
-
content: [
|
|
917
|
-
{
|
|
918
|
-
type: 'text',
|
|
919
|
-
text: `WebSocket Server Information:\n\n` +
|
|
920
|
-
`Note: WebSocket server is running on a separate HTTP server process.\n` +
|
|
921
|
-
`Default URL: ws://localhost:3001\n\n` +
|
|
922
|
-
`Chrome extension should connect to this WebSocket.`,
|
|
923
|
-
},
|
|
924
|
-
],
|
|
925
|
-
};
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
case 'get_workflow_recording': {
|
|
929
|
-
if (!args.sessionId) {
|
|
930
|
-
throw new Error('Session ID is required');
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
const recording = await chromeController.getWorkflowRecording(args.sessionId);
|
|
934
|
-
|
|
935
|
-
if (recording.error) {
|
|
936
|
-
return {
|
|
937
|
-
content: [
|
|
938
|
-
{
|
|
939
|
-
type: 'text',
|
|
940
|
-
text: `Error: ${recording.error}`,
|
|
941
|
-
},
|
|
942
|
-
],
|
|
943
|
-
isError: true,
|
|
944
|
-
};
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
let output = `Workflow Recording: ${recording.sessionId}\n\n`;
|
|
948
|
-
output += `URL: ${recording.url}\n`;
|
|
949
|
-
output += `Title: ${recording.title}\n`;
|
|
950
|
-
output += `Timestamp: ${new Date(recording.timestamp).toISOString()}\n`;
|
|
951
|
-
output += `Total Actions: ${recording.totalActions}\n\n`;
|
|
952
|
-
|
|
953
|
-
if (recording.actions && recording.actions.length > 0) {
|
|
954
|
-
output += `Actions:\n`;
|
|
955
|
-
recording.actions.forEach((action, index) => {
|
|
956
|
-
output += ` ${index + 1}. ${action.type} - ${action.selector}\n`;
|
|
957
|
-
if (action.value) output += ` Value: ${action.value}\n`;
|
|
958
|
-
if (action.text) output += ` Text: ${action.text}\n`;
|
|
959
|
-
});
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
if (recording.logs && recording.logs.length > 0) {
|
|
963
|
-
output += `\nConsole Logs (${recording.logs.length}):\n`;
|
|
964
|
-
recording.logs.forEach((log, index) => {
|
|
965
|
-
output += ` ${index + 1}. [${log.level}] ${log.message}\n`;
|
|
966
|
-
});
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
return {
|
|
970
|
-
content: [
|
|
971
|
-
{
|
|
972
|
-
type: 'text',
|
|
973
|
-
text: output,
|
|
974
|
-
},
|
|
975
|
-
],
|
|
976
|
-
};
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
case 'list_workflow_recordings': {
|
|
980
|
-
const result = await chromeController.listWorkflowRecordings();
|
|
981
|
-
const recordings = result.recordings || [];
|
|
982
|
-
|
|
983
|
-
if (recordings.length === 0) {
|
|
984
|
-
return {
|
|
985
|
-
content: [
|
|
986
|
-
{
|
|
987
|
-
type: 'text',
|
|
988
|
-
text: 'No workflow recordings found.',
|
|
989
|
-
},
|
|
990
|
-
],
|
|
991
|
-
};
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
let output = `Workflow Recordings (${recordings.length}):\n\n`;
|
|
995
|
-
recordings.forEach((recording, index) => {
|
|
996
|
-
const name = recording.name || recording.session_id;
|
|
997
|
-
output += `${index + 1}. ${name}\n`;
|
|
998
|
-
output += ` Session ID: ${recording.session_id}\n`;
|
|
999
|
-
output += ` URL: ${recording.url}\n`;
|
|
1000
|
-
output += ` Title: ${recording.title}\n`;
|
|
1001
|
-
output += ` Actions: ${recording.total_actions}\n`;
|
|
1002
|
-
output += ` Date: ${new Date(recording.timestamp).toISOString()}\n`;
|
|
1003
|
-
if (recording.screenshot_settings) {
|
|
1004
|
-
output += ` Screenshots: Enabled\n`;
|
|
1005
|
-
}
|
|
1006
|
-
output += `\n`;
|
|
1007
|
-
});
|
|
1008
|
-
|
|
1009
|
-
return {
|
|
1010
|
-
content: [
|
|
1011
|
-
{
|
|
1012
|
-
type: 'text',
|
|
1013
|
-
text: output,
|
|
1014
|
-
},
|
|
1015
|
-
],
|
|
1016
|
-
};
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
case 'save_restore_point': {
|
|
1020
|
-
if (!args.workflowId) {
|
|
1021
|
-
throw new Error('Workflow ID is required');
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
// Capture current browser state
|
|
1025
|
-
const page = await chromeController.getPage();
|
|
1026
|
-
if (!page) {
|
|
1027
|
-
throw new Error('No page available. Please launch Chrome and navigate to a URL first.');
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
// Execute content script to capture state
|
|
1031
|
-
const captureResult = await page.evaluate(async () => {
|
|
1032
|
-
// This runs in the browser context
|
|
1033
|
-
const captureState = () => {
|
|
1034
|
-
const state = {
|
|
1035
|
-
url: window.location.href,
|
|
1036
|
-
title: document.title,
|
|
1037
|
-
domSnapshot: {
|
|
1038
|
-
html: document.documentElement.outerHTML,
|
|
1039
|
-
formData: {},
|
|
1040
|
-
checkboxStates: {},
|
|
1041
|
-
radioStates: {},
|
|
1042
|
-
selectValues: {},
|
|
1043
|
-
textareaValues: {},
|
|
1044
|
-
},
|
|
1045
|
-
scrollX: window.scrollX,
|
|
1046
|
-
scrollY: window.scrollY,
|
|
1047
|
-
localStorage: {},
|
|
1048
|
-
sessionStorage: {},
|
|
1049
|
-
};
|
|
1050
|
-
|
|
1051
|
-
// Capture form values
|
|
1052
|
-
document.querySelectorAll('input').forEach(input => {
|
|
1053
|
-
const id = input.id || input.name || Math.random().toString(36);
|
|
1054
|
-
if (input.type === 'checkbox') {
|
|
1055
|
-
state.domSnapshot.checkboxStates[id] = input.checked;
|
|
1056
|
-
} else if (input.type === 'radio') {
|
|
1057
|
-
state.domSnapshot.radioStates[id] = {
|
|
1058
|
-
checked: input.checked,
|
|
1059
|
-
value: input.value,
|
|
1060
|
-
};
|
|
1061
|
-
} else if (input.type !== 'file' && input.type !== 'password') {
|
|
1062
|
-
state.domSnapshot.formData[id] = input.value;
|
|
1063
|
-
}
|
|
1064
|
-
});
|
|
1065
|
-
|
|
1066
|
-
// Capture select values
|
|
1067
|
-
document.querySelectorAll('select').forEach(select => {
|
|
1068
|
-
const id = select.id || select.name || Math.random().toString(36);
|
|
1069
|
-
state.domSnapshot.selectValues[id] = select.value;
|
|
1070
|
-
});
|
|
1071
|
-
|
|
1072
|
-
// Capture textarea values
|
|
1073
|
-
document.querySelectorAll('textarea').forEach(textarea => {
|
|
1074
|
-
const id = textarea.id || textarea.name || Math.random().toString(36);
|
|
1075
|
-
state.domSnapshot.textareaValues[id] = textarea.value;
|
|
1076
|
-
});
|
|
1077
|
-
|
|
1078
|
-
// Capture storage
|
|
1079
|
-
try {
|
|
1080
|
-
for (let i = 0; i < localStorage.length; i++) {
|
|
1081
|
-
const key = localStorage.key(i);
|
|
1082
|
-
state.localStorage[key] = localStorage.getItem(key);
|
|
1083
|
-
}
|
|
1084
|
-
} catch (e) {}
|
|
1085
|
-
|
|
1086
|
-
try {
|
|
1087
|
-
for (let i = 0; i < sessionStorage.length; i++) {
|
|
1088
|
-
const key = sessionStorage.key(i);
|
|
1089
|
-
state.sessionStorage[key] = sessionStorage.getItem(key);
|
|
1090
|
-
}
|
|
1091
|
-
} catch (e) {}
|
|
1092
|
-
|
|
1093
|
-
return state;
|
|
1094
|
-
};
|
|
1095
|
-
|
|
1096
|
-
return captureState();
|
|
1097
|
-
});
|
|
1098
|
-
|
|
1099
|
-
// Get cookies
|
|
1100
|
-
const cookies = await page.cookies();
|
|
1101
|
-
captureResult.cookies = cookies;
|
|
1102
|
-
|
|
1103
|
-
// Save restore point
|
|
1104
|
-
const restoreData = {
|
|
1105
|
-
workflowId: args.workflowId,
|
|
1106
|
-
actionIndex: args.actionIndex || 0,
|
|
1107
|
-
...captureResult,
|
|
1108
|
-
description: args.description,
|
|
1109
|
-
timestamp: Date.now(),
|
|
1110
|
-
};
|
|
1111
|
-
|
|
1112
|
-
const result = await chromeController.saveRestorePoint(restoreData);
|
|
1113
|
-
|
|
1114
|
-
return {
|
|
1115
|
-
content: [
|
|
1116
|
-
{
|
|
1117
|
-
type: 'text',
|
|
1118
|
-
text: `Restore point saved successfully!\nID: ${result.restorePointId}\nWorkflow: ${args.workflowId}\nDescription: ${args.description || 'N/A'}`,
|
|
1119
|
-
},
|
|
1120
|
-
],
|
|
1121
|
-
};
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
case 'restore_from_point': {
|
|
1125
|
-
if (!args.restorePointId) {
|
|
1126
|
-
throw new Error('Restore point ID is required');
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
// Get restore point data
|
|
1130
|
-
const restoreData = await chromeController.getRestorePoint(args.restorePointId);
|
|
1131
|
-
if (restoreData.error) {
|
|
1132
|
-
throw new Error(restoreData.error);
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
|
-
const page = await chromeController.getPage();
|
|
1136
|
-
if (!page) {
|
|
1137
|
-
throw new Error('No page available. Please launch Chrome first.');
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
// Navigate if needed
|
|
1141
|
-
const currentUrl = await page.url();
|
|
1142
|
-
if (currentUrl !== restoreData.url) {
|
|
1143
|
-
await page.goto(restoreData.url, { waitUntil: 'networkidle0' });
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
// Restore state
|
|
1147
|
-
await page.evaluate((data) => {
|
|
1148
|
-
// Restore storage
|
|
1149
|
-
try {
|
|
1150
|
-
localStorage.clear();
|
|
1151
|
-
Object.entries(data.localStorage).forEach(([key, value]) => {
|
|
1152
|
-
localStorage.setItem(key, value);
|
|
1153
|
-
});
|
|
1154
|
-
} catch (e) {}
|
|
1155
|
-
|
|
1156
|
-
try {
|
|
1157
|
-
sessionStorage.clear();
|
|
1158
|
-
Object.entries(data.sessionStorage).forEach(([key, value]) => {
|
|
1159
|
-
sessionStorage.setItem(key, value);
|
|
1160
|
-
});
|
|
1161
|
-
} catch (e) {}
|
|
1162
|
-
|
|
1163
|
-
// Restore form values
|
|
1164
|
-
const snapshot = data.domSnapshot;
|
|
1165
|
-
Object.entries(snapshot.formData).forEach(([id, value]) => {
|
|
1166
|
-
const element = document.getElementById(id) || document.querySelector(`[name="${id}"]`);
|
|
1167
|
-
if (element) element.value = value;
|
|
1168
|
-
});
|
|
1169
|
-
|
|
1170
|
-
Object.entries(snapshot.checkboxStates).forEach(([id, checked]) => {
|
|
1171
|
-
const element = document.getElementById(id) || document.querySelector(`[name="${id}"]`);
|
|
1172
|
-
if (element) element.checked = checked;
|
|
1173
|
-
});
|
|
1174
|
-
|
|
1175
|
-
Object.entries(snapshot.radioStates).forEach(([id, state]) => {
|
|
1176
|
-
const element = document.getElementById(id) || document.querySelector(`[name="${id}"]`);
|
|
1177
|
-
if (element && state.checked) element.checked = true;
|
|
1178
|
-
});
|
|
1179
|
-
|
|
1180
|
-
Object.entries(snapshot.selectValues).forEach(([id, value]) => {
|
|
1181
|
-
const element = document.getElementById(id) || document.querySelector(`[name="${id}"]`);
|
|
1182
|
-
if (element) element.value = value;
|
|
1183
|
-
});
|
|
1184
|
-
|
|
1185
|
-
Object.entries(snapshot.textareaValues).forEach(([id, value]) => {
|
|
1186
|
-
const element = document.getElementById(id) || document.querySelector(`[name="${id}"]`);
|
|
1187
|
-
if (element) element.value = value;
|
|
1188
|
-
});
|
|
1189
|
-
|
|
1190
|
-
// Restore scroll
|
|
1191
|
-
window.scrollTo(data.scrollX, data.scrollY);
|
|
1192
|
-
}, restoreData);
|
|
1193
|
-
|
|
1194
|
-
// Restore cookies
|
|
1195
|
-
if (restoreData.cookies && restoreData.cookies.length > 0) {
|
|
1196
|
-
for (const cookie of restoreData.cookies) {
|
|
1197
|
-
await page.setCookie(cookie);
|
|
1198
|
-
}
|
|
1199
|
-
}
|
|
1200
|
-
|
|
1201
|
-
return {
|
|
1202
|
-
content: [
|
|
1203
|
-
{
|
|
1204
|
-
type: 'text',
|
|
1205
|
-
text: `Successfully restored from restore point!\nURL: ${restoreData.url}\nTitle: ${restoreData.title}\nTimestamp: ${new Date(restoreData.timestamp).toISOString()}`,
|
|
1206
|
-
},
|
|
1207
|
-
],
|
|
1208
|
-
};
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
case 'list_restore_points': {
|
|
1212
|
-
if (!args.workflowId) {
|
|
1213
|
-
throw new Error('Workflow ID is required');
|
|
1214
|
-
}
|
|
1215
|
-
|
|
1216
|
-
const restorePoints = await chromeController.listRestorePoints(args.workflowId);
|
|
1217
|
-
|
|
1218
|
-
if (restorePoints.length === 0) {
|
|
1219
|
-
return {
|
|
1220
|
-
content: [
|
|
1221
|
-
{
|
|
1222
|
-
type: 'text',
|
|
1223
|
-
text: `No restore points found for workflow: ${args.workflowId}`,
|
|
1224
|
-
},
|
|
1225
|
-
],
|
|
1226
|
-
};
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
let output = `Restore Points for workflow ${args.workflowId} (${restorePoints.length}):\n\n`;
|
|
1230
|
-
restorePoints.forEach((rp, index) => {
|
|
1231
|
-
output += `${index + 1}. ${rp.id}\n`;
|
|
1232
|
-
output += ` Action Index: ${rp.actionIndex}\n`;
|
|
1233
|
-
output += ` URL: ${rp.url}\n`;
|
|
1234
|
-
output += ` Title: ${rp.title || 'N/A'}\n`;
|
|
1235
|
-
output += ` Date: ${new Date(rp.timestamp).toISOString()}\n\n`;
|
|
1236
|
-
});
|
|
1237
|
-
|
|
1238
|
-
return {
|
|
1239
|
-
content: [
|
|
1240
|
-
{
|
|
1241
|
-
type: 'text',
|
|
1242
|
-
text: output,
|
|
1243
|
-
},
|
|
1244
|
-
],
|
|
1245
|
-
};
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
|
-
case 'chrome_pilot_show_frames': {
|
|
1249
|
-
if (!args.sessionId) {
|
|
1250
|
-
throw new Error('Session ID is required');
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
const maxFrames = args.maxFrames || 10;
|
|
1254
|
-
const showLogs = args.showLogs !== false;
|
|
1255
|
-
const showInteractions = args.showInteractions !== false;
|
|
1256
|
-
const logLevel = args.logLevel || 'all';
|
|
1257
|
-
const maxLogsPerFrame = args.maxLogsPerFrame || 10;
|
|
1258
|
-
|
|
1259
|
-
const sessionInfo = await chromeController.getFrameSessionInfo(args.sessionId);
|
|
1260
|
-
if (!sessionInfo) {
|
|
1261
|
-
// Get list of available sessions for better error message
|
|
1262
|
-
const availableSessions = await chromeController.listFrameSessions();
|
|
1263
|
-
const recentSessions = availableSessions.slice(0, 5);
|
|
1264
|
-
|
|
1265
|
-
let errorMsg = `Frame session not found: ${args.sessionId}\n\n`;
|
|
1266
|
-
if (recentSessions.length > 0) {
|
|
1267
|
-
errorMsg += 'Available frame sessions:\n';
|
|
1268
|
-
recentSessions.forEach(s => {
|
|
1269
|
-
errorMsg += ` - ${s.sessionId} (${s.totalFrames} frames, ${new Date(s.timestamp).toLocaleString()})\n`;
|
|
1270
|
-
});
|
|
1271
|
-
errorMsg += '\nTip: Use list_workflow_recordings to see all available recordings.';
|
|
1272
|
-
} else {
|
|
1273
|
-
errorMsg += 'No frame sessions found in the database.\n';
|
|
1274
|
-
errorMsg += 'Frame recordings may have been created in a different Chrome Debug instance.';
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
|
-
throw new Error(errorMsg);
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
// Get only frame metadata without image data to avoid token limits
|
|
1281
|
-
const { database } = await import('./database.js');
|
|
1282
|
-
const recording = database.getRecording(args.sessionId);
|
|
1283
|
-
if (!recording) {
|
|
1284
|
-
throw new Error('Frame session data not found');
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
// Get frames without image data
|
|
1288
|
-
const framesMetadataStmt = database.db.prepare(`
|
|
1289
|
-
SELECT frame_index, timestamp, absolute_timestamp
|
|
1290
|
-
FROM frames
|
|
1291
|
-
WHERE recording_id = ?
|
|
1292
|
-
ORDER BY frame_index ASC
|
|
1293
|
-
LIMIT ?
|
|
1294
|
-
`);
|
|
1295
|
-
const framesToShow = framesMetadataStmt.all(recording.id, maxFrames);
|
|
1296
|
-
|
|
1297
|
-
let output = `Frame Recording: ${args.sessionId}\n`;
|
|
1298
|
-
output += `Type: ${recording.type}\n`;
|
|
1299
|
-
output += `Created: ${new Date(sessionInfo.timestamp).toLocaleString()}\n`;
|
|
1300
|
-
output += `Total Frames: ${sessionInfo.totalFrames}\n`;
|
|
1301
|
-
|
|
1302
|
-
// Handle case where recording exists but has no frames
|
|
1303
|
-
if (sessionInfo.totalFrames === 0) {
|
|
1304
|
-
output += `Status: Recording exists but contains no frames\n`;
|
|
1305
|
-
|
|
1306
|
-
// Check for screen interactions
|
|
1307
|
-
const interactions = await chromeController.getScreenInteractions(args.sessionId);
|
|
1308
|
-
if (interactions && interactions.length > 0) {
|
|
1309
|
-
output += `\nScreen Interactions Found: ${interactions.length}\n`;
|
|
1310
|
-
output += `Note: This recording has captured user interactions but no frame data.\n`;
|
|
1311
|
-
output += `Use get_screen_interactions to view the captured interactions.\n`;
|
|
1312
|
-
} else {
|
|
1313
|
-
output += `\nNo frame data or interactions found.\n`;
|
|
1314
|
-
output += `This may indicate the recording was started but stopped immediately.\n`;
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
|
-
return {
|
|
1318
|
-
content: [{ type: 'text', text: output }]
|
|
1319
|
-
};
|
|
1320
|
-
}
|
|
1321
|
-
|
|
1322
|
-
output += `Showing: ${framesToShow.length} of ${sessionInfo.totalFrames} frames\n`;
|
|
1323
|
-
if (logLevel !== 'all') {
|
|
1324
|
-
output += `Log Filter: ${logLevel.toUpperCase()} level only\n`;
|
|
1325
|
-
}
|
|
1326
|
-
output += '\n';
|
|
1327
|
-
|
|
1328
|
-
// Get all interactions for this recording if requested
|
|
1329
|
-
let allInteractions = [];
|
|
1330
|
-
if (showInteractions) {
|
|
1331
|
-
allInteractions = await chromeController.getScreenInteractions(args.sessionId);
|
|
1332
|
-
}
|
|
1333
|
-
|
|
1334
|
-
framesToShow.forEach((frame, index) => {
|
|
1335
|
-
output += `Frame ${frame.frame_index}:\n`;
|
|
1336
|
-
output += ` Timestamp: ${frame.timestamp}ms\n`;
|
|
1337
|
-
|
|
1338
|
-
// Get logs for this specific frame if requested
|
|
1339
|
-
let logs = [];
|
|
1340
|
-
if (showLogs) {
|
|
1341
|
-
const frameRowStmt = database.db.prepare(`
|
|
1342
|
-
SELECT id FROM frames
|
|
1343
|
-
WHERE recording_id = ? AND frame_index = ?
|
|
1344
|
-
`);
|
|
1345
|
-
const frameRow = frameRowStmt.get(recording.id, frame.frame_index);
|
|
1346
|
-
|
|
1347
|
-
if (frameRow) {
|
|
1348
|
-
let logsQuery = `
|
|
1349
|
-
SELECT level, message, relative_time
|
|
1350
|
-
FROM console_logs
|
|
1351
|
-
WHERE frame_id = ?`;
|
|
1352
|
-
let queryParams = [frameRow.id];
|
|
1353
|
-
|
|
1354
|
-
if (logLevel !== 'all') {
|
|
1355
|
-
logsQuery += ` AND level = ?`;
|
|
1356
|
-
queryParams.push(logLevel);
|
|
1357
|
-
}
|
|
1358
|
-
|
|
1359
|
-
logsQuery += ` ORDER BY relative_time ASC`;
|
|
1360
|
-
|
|
1361
|
-
if (maxLogsPerFrame > 0) {
|
|
1362
|
-
logsQuery += ` LIMIT ?`;
|
|
1363
|
-
queryParams.push(maxLogsPerFrame);
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
|
-
const logsStmt = database.db.prepare(logsQuery);
|
|
1367
|
-
logs = logsStmt.all(...queryParams);
|
|
1368
|
-
}
|
|
1369
|
-
}
|
|
1370
|
-
|
|
1371
|
-
if (showLogs && logs.length > 0) {
|
|
1372
|
-
output += ` Console Logs (${logs.length}${maxLogsPerFrame > 0 && logs.length === maxLogsPerFrame ? '+' : ''}):\n`;
|
|
1373
|
-
logs.forEach(log => {
|
|
1374
|
-
output += ` [${log.level.toUpperCase()}] ${log.message}\n`;
|
|
1375
|
-
});
|
|
1376
|
-
if (maxLogsPerFrame > 0 && logs.length === maxLogsPerFrame) {
|
|
1377
|
-
output += ` ... (truncated, use get_frame or search_frame_logs for more)\n`;
|
|
1378
|
-
}
|
|
1379
|
-
} else {
|
|
1380
|
-
// Count all logs for this frame to show total
|
|
1381
|
-
const frameRowStmt = database.db.prepare(`
|
|
1382
|
-
SELECT id FROM frames
|
|
1383
|
-
WHERE recording_id = ? AND frame_index = ?
|
|
1384
|
-
`);
|
|
1385
|
-
const frameRow = frameRowStmt.get(recording.id, frame.frame_index);
|
|
1386
|
-
let totalLogCount = 0;
|
|
1387
|
-
|
|
1388
|
-
if (frameRow) {
|
|
1389
|
-
let countQuery = `SELECT COUNT(*) as count FROM console_logs WHERE frame_id = ?`;
|
|
1390
|
-
let countParams = [frameRow.id];
|
|
1391
|
-
|
|
1392
|
-
if (logLevel !== 'all') {
|
|
1393
|
-
countQuery += ` AND level = ?`;
|
|
1394
|
-
countParams.push(logLevel);
|
|
1395
|
-
}
|
|
1396
|
-
|
|
1397
|
-
const countStmt = database.db.prepare(countQuery);
|
|
1398
|
-
totalLogCount = countStmt.get(...countParams).count;
|
|
1399
|
-
}
|
|
1400
|
-
|
|
1401
|
-
output += ` Console Logs: ${totalLogCount}\n`;
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
|
-
// Show interactions for this frame
|
|
1405
|
-
if (showInteractions && allInteractions.length > 0) {
|
|
1406
|
-
const frameInteractions = allInteractions.filter(i => i.frame_index === frame.frame_index);
|
|
1407
|
-
if (frameInteractions.length > 0) {
|
|
1408
|
-
output += ` User Interactions (${frameInteractions.length}):\n`;
|
|
1409
|
-
frameInteractions.forEach(interaction => {
|
|
1410
|
-
switch (interaction.type) {
|
|
1411
|
-
case 'click':
|
|
1412
|
-
output += ` [CLICK] ${interaction.selector || `at (${interaction.x}, ${interaction.y})`}`;
|
|
1413
|
-
if (interaction.text) output += ` - "${interaction.text}"`;
|
|
1414
|
-
output += '\n';
|
|
1415
|
-
break;
|
|
1416
|
-
case 'input':
|
|
1417
|
-
output += ` [INPUT] ${interaction.selector} = "${interaction.value}"`;
|
|
1418
|
-
if (interaction.placeholder) output += ` (placeholder: ${interaction.placeholder})`;
|
|
1419
|
-
output += '\n';
|
|
1420
|
-
break;
|
|
1421
|
-
case 'keypress':
|
|
1422
|
-
output += ` [KEY] ${interaction.key}\n`;
|
|
1423
|
-
break;
|
|
1424
|
-
case 'scroll':
|
|
1425
|
-
output += ` [SCROLL] to (${interaction.scrollX}, ${interaction.scrollY})\n`;
|
|
1426
|
-
break;
|
|
1427
|
-
default:
|
|
1428
|
-
output += ` [${interaction.type.toUpperCase()}]\n`;
|
|
1429
|
-
}
|
|
1430
|
-
});
|
|
1431
|
-
}
|
|
1432
|
-
}
|
|
1433
|
-
|
|
1434
|
-
output += '\n';
|
|
1435
|
-
});
|
|
1436
|
-
|
|
1437
|
-
if (sessionInfo.totalFrames > maxFrames) {
|
|
1438
|
-
output += `... and ${sessionInfo.totalFrames - maxFrames} more frames\n`;
|
|
1439
|
-
output += `Use get_frame with frameIndex to view specific frames\n`;
|
|
1440
|
-
}
|
|
1441
|
-
|
|
1442
|
-
return {
|
|
1443
|
-
content: [
|
|
1444
|
-
{
|
|
1445
|
-
type: 'text',
|
|
1446
|
-
text: output,
|
|
1447
|
-
},
|
|
1448
|
-
],
|
|
1449
|
-
};
|
|
1450
|
-
}
|
|
1451
|
-
|
|
1452
|
-
case 'get_frame_session_info': {
|
|
1453
|
-
if (!args.sessionId) {
|
|
1454
|
-
throw new Error('Session ID is required');
|
|
1455
|
-
}
|
|
1456
|
-
|
|
1457
|
-
const info = await chromeController.getFrameSessionInfo(args.sessionId);
|
|
1458
|
-
if (!info) {
|
|
1459
|
-
// Get list of available sessions for better error message
|
|
1460
|
-
const availableSessions = await chromeController.listFrameSessions();
|
|
1461
|
-
const recentSessions = availableSessions.slice(0, 5);
|
|
1462
|
-
|
|
1463
|
-
let errorMsg = `Frame session not found: ${args.sessionId}\n\n`;
|
|
1464
|
-
if (recentSessions.length > 0) {
|
|
1465
|
-
errorMsg += 'Available frame sessions:\n';
|
|
1466
|
-
recentSessions.forEach(s => {
|
|
1467
|
-
errorMsg += ` - ${s.sessionId} (${s.totalFrames} frames, ${new Date(s.timestamp).toLocaleString()})\n`;
|
|
1468
|
-
});
|
|
1469
|
-
errorMsg += '\nTip: Frame recordings are stored in the local database and may not persist across different Chrome Debug instances.';
|
|
1470
|
-
} else {
|
|
1471
|
-
errorMsg += 'No frame sessions found in the database.\n';
|
|
1472
|
-
errorMsg += 'Frame recordings may have been created in a different Chrome Debug instance or the database may have been reset.';
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1475
|
-
throw new Error(errorMsg);
|
|
1476
|
-
}
|
|
1477
|
-
|
|
1478
|
-
return {
|
|
1479
|
-
content: [
|
|
1480
|
-
{
|
|
1481
|
-
type: 'text',
|
|
1482
|
-
text: `Frame Session ${args.sessionId}:\n` +
|
|
1483
|
-
`Total Frames: ${info.totalFrames}\n` +
|
|
1484
|
-
`Created: ${new Date(info.timestamp).toISOString()}\n` +
|
|
1485
|
-
`Frame Timestamps: ${info.frameTimestamps ? info.frameTimestamps.slice(0, 5).join(', ') + (info.frameTimestamps.length > 5 ? '...' : '') : 'N/A'}`,
|
|
1486
|
-
},
|
|
1487
|
-
],
|
|
1488
|
-
};
|
|
1489
|
-
}
|
|
1490
|
-
|
|
1491
|
-
case 'get_frame': {
|
|
1492
|
-
if (!args.sessionId || args.frameIndex === undefined) {
|
|
1493
|
-
throw new Error('Session ID and frame index are required');
|
|
1494
|
-
}
|
|
1495
|
-
|
|
1496
|
-
const logLevel = args.logLevel || 'all';
|
|
1497
|
-
const maxLogs = args.maxLogs || 20;
|
|
1498
|
-
const searchLogs = args.searchLogs;
|
|
1499
|
-
|
|
1500
|
-
const frame = await chromeController.getFrame(args.sessionId, args.frameIndex);
|
|
1501
|
-
if (!frame) {
|
|
1502
|
-
throw new Error('Frame not found');
|
|
1503
|
-
}
|
|
1504
|
-
|
|
1505
|
-
// Apply filtering to logs
|
|
1506
|
-
let filteredLogs = frame.logs || [];
|
|
1507
|
-
|
|
1508
|
-
// Filter by log level
|
|
1509
|
-
if (logLevel !== 'all') {
|
|
1510
|
-
filteredLogs = filteredLogs.filter(log => log.level === logLevel);
|
|
1511
|
-
}
|
|
1512
|
-
|
|
1513
|
-
// Filter by search text
|
|
1514
|
-
if (searchLogs) {
|
|
1515
|
-
const searchText = searchLogs.toLowerCase();
|
|
1516
|
-
filteredLogs = filteredLogs.filter(log =>
|
|
1517
|
-
log.message.toLowerCase().includes(searchText)
|
|
1518
|
-
);
|
|
1519
|
-
}
|
|
1520
|
-
|
|
1521
|
-
// Limit the number of logs
|
|
1522
|
-
const totalMatchingLogs = filteredLogs.length;
|
|
1523
|
-
if (maxLogs > 0 && filteredLogs.length > maxLogs) {
|
|
1524
|
-
filteredLogs = filteredLogs.slice(0, maxLogs);
|
|
1525
|
-
}
|
|
1526
|
-
|
|
1527
|
-
let output = `Frame ${args.frameIndex} from Session ${args.sessionId}:\n`;
|
|
1528
|
-
output += `Timestamp: ${frame.timestamp}ms\n`;
|
|
1529
|
-
output += `Image Size: ${frame.imageData ? Math.round(frame.imageData.length / 1024) + 'KB' : 'N/A'}\n`;
|
|
1530
|
-
output += `Total Console Logs: ${frame.logs ? frame.logs.length : 0}\n`;
|
|
1531
|
-
|
|
1532
|
-
if (logLevel !== 'all' || searchLogs) {
|
|
1533
|
-
output += `Filtered Logs: ${totalMatchingLogs}`;
|
|
1534
|
-
if (searchLogs) {
|
|
1535
|
-
output += ` (containing "${searchLogs}")`;
|
|
1536
|
-
}
|
|
1537
|
-
if (logLevel !== 'all') {
|
|
1538
|
-
output += ` (${logLevel.toUpperCase()} level)`;
|
|
1539
|
-
}
|
|
1540
|
-
output += '\n';
|
|
1541
|
-
}
|
|
1542
|
-
|
|
1543
|
-
if (filteredLogs.length > 0) {
|
|
1544
|
-
// If there are a lot of logs, recommend paginated approach
|
|
1545
|
-
if (totalMatchingLogs > 500) {
|
|
1546
|
-
output += `\n⚠️ WARNING: This frame has ${totalMatchingLogs} matching logs!\n`;
|
|
1547
|
-
output += `Showing only first ${filteredLogs.length} logs to prevent output overflow.\n`;
|
|
1548
|
-
output += `For better navigation, use: get_frame_logs_paginated\n\n`;
|
|
1549
|
-
} else {
|
|
1550
|
-
output += `\nShowing: ${filteredLogs.length}${filteredLogs.length < totalMatchingLogs ? ` of ${totalMatchingLogs}` : ''} logs\n\n`;
|
|
1551
|
-
}
|
|
1552
|
-
|
|
1553
|
-
output += 'Logs:\n';
|
|
1554
|
-
filteredLogs.forEach(log => {
|
|
1555
|
-
output += ` [${log.level.toUpperCase()}] ${log.message}\n`;
|
|
1556
|
-
});
|
|
1557
|
-
|
|
1558
|
-
if (filteredLogs.length < totalMatchingLogs) {
|
|
1559
|
-
if (totalMatchingLogs > 100) {
|
|
1560
|
-
output += `\n... ${totalMatchingLogs - filteredLogs.length} more logs\n`;
|
|
1561
|
-
output += `💡 TIP: Use get_frame_logs_paginated for better navigation with large log volumes\n`;
|
|
1562
|
-
} else {
|
|
1563
|
-
output += `\n... ${totalMatchingLogs - filteredLogs.length} more logs (use maxLogs=0 to see all, or search_frame_logs for cross-frame search)\n`;
|
|
1564
|
-
}
|
|
1565
|
-
}
|
|
1566
|
-
}
|
|
1567
|
-
|
|
1568
|
-
// Show interactions for this frame
|
|
1569
|
-
if (frame.interactions && frame.interactions.length > 0) {
|
|
1570
|
-
output += `\n\nUser Interactions (${frame.interactions.length}):\n`;
|
|
1571
|
-
frame.interactions.forEach(interaction => {
|
|
1572
|
-
switch (interaction.type) {
|
|
1573
|
-
case 'click':
|
|
1574
|
-
output += ` [CLICK] ${interaction.selector || `at (${interaction.x}, ${interaction.y})`}`;
|
|
1575
|
-
if (interaction.text) output += ` - "${interaction.text}"`;
|
|
1576
|
-
output += '\n';
|
|
1577
|
-
break;
|
|
1578
|
-
case 'input':
|
|
1579
|
-
output += ` [INPUT] ${interaction.selector} = "${interaction.value}"`;
|
|
1580
|
-
if (interaction.placeholder) output += ` (placeholder: ${interaction.placeholder})`;
|
|
1581
|
-
output += '\n';
|
|
1582
|
-
break;
|
|
1583
|
-
case 'keypress':
|
|
1584
|
-
output += ` [KEY] ${interaction.key}\n`;
|
|
1585
|
-
break;
|
|
1586
|
-
case 'scroll':
|
|
1587
|
-
output += ` [SCROLL] to (${interaction.scrollX}, ${interaction.scrollY})\n`;
|
|
1588
|
-
break;
|
|
1589
|
-
default:
|
|
1590
|
-
output += ` [${interaction.type.toUpperCase()}]\n`;
|
|
1591
|
-
}
|
|
1592
|
-
});
|
|
1593
|
-
}
|
|
1594
|
-
|
|
1595
|
-
if (frame.imageData) {
|
|
1596
|
-
output += '\n[Frame image data available]';
|
|
1597
|
-
}
|
|
1598
|
-
|
|
1599
|
-
return {
|
|
1600
|
-
content: [
|
|
1601
|
-
{
|
|
1602
|
-
type: 'text',
|
|
1603
|
-
text: output,
|
|
1604
|
-
},
|
|
1605
|
-
],
|
|
1606
|
-
};
|
|
1607
|
-
}
|
|
1608
|
-
|
|
1609
|
-
case 'search_frame_logs': {
|
|
1610
|
-
if (!args.sessionId || !args.searchText) {
|
|
1611
|
-
throw new Error('Session ID and search text are required');
|
|
1612
|
-
}
|
|
1613
|
-
|
|
1614
|
-
const searchText = args.searchText.toLowerCase();
|
|
1615
|
-
const logLevel = args.logLevel || 'all';
|
|
1616
|
-
const maxResults = args.maxResults || 50;
|
|
1617
|
-
|
|
1618
|
-
const { database } = await import('./database.js');
|
|
1619
|
-
const recording = database.getRecording(args.sessionId);
|
|
1620
|
-
if (!recording) {
|
|
1621
|
-
throw new Error('Frame session not found');
|
|
1622
|
-
}
|
|
1623
|
-
|
|
1624
|
-
// Search across all frames in the session
|
|
1625
|
-
let searchQuery = `
|
|
1626
|
-
SELECT f.frame_index, cl.level, cl.message, cl.relative_time, f.timestamp
|
|
1627
|
-
FROM console_logs cl
|
|
1628
|
-
JOIN frames f ON cl.frame_id = f.id
|
|
1629
|
-
WHERE f.recording_id = ? AND LOWER(cl.message) LIKE ?`;
|
|
1630
|
-
|
|
1631
|
-
let queryParams = [recording.id, `%${searchText}%`];
|
|
1632
|
-
|
|
1633
|
-
if (logLevel !== 'all') {
|
|
1634
|
-
searchQuery += ` AND cl.level = ?`;
|
|
1635
|
-
queryParams.push(logLevel);
|
|
1636
|
-
}
|
|
1637
|
-
|
|
1638
|
-
searchQuery += ` ORDER BY f.frame_index ASC, cl.relative_time ASC`;
|
|
1639
|
-
|
|
1640
|
-
if (maxResults > 0) {
|
|
1641
|
-
searchQuery += ` LIMIT ?`;
|
|
1642
|
-
queryParams.push(maxResults);
|
|
1643
|
-
}
|
|
1644
|
-
|
|
1645
|
-
const searchStmt = database.db.prepare(searchQuery);
|
|
1646
|
-
const results = searchStmt.all(...queryParams);
|
|
1647
|
-
|
|
1648
|
-
let output = `Search Results for "${args.searchText}" in Session ${args.sessionId}:\n`;
|
|
1649
|
-
output += `Found: ${results.length}${maxResults > 0 && results.length === maxResults ? '+' : ''} matching logs\n`;
|
|
1650
|
-
if (logLevel !== 'all') {
|
|
1651
|
-
output += `Filter: ${logLevel.toUpperCase()} level only\n`;
|
|
1652
|
-
}
|
|
1653
|
-
output += '\n';
|
|
1654
|
-
|
|
1655
|
-
if (results.length > 0) {
|
|
1656
|
-
let currentFrame = -1;
|
|
1657
|
-
results.forEach(result => {
|
|
1658
|
-
if (result.frame_index !== currentFrame) {
|
|
1659
|
-
if (currentFrame !== -1) output += '\n';
|
|
1660
|
-
output += `Frame ${result.frame_index} (${result.timestamp}ms):\n`;
|
|
1661
|
-
currentFrame = result.frame_index;
|
|
1662
|
-
}
|
|
1663
|
-
output += ` [${result.level.toUpperCase()}] ${result.message}\n`;
|
|
1664
|
-
});
|
|
1665
|
-
|
|
1666
|
-
if (maxResults > 0 && results.length === maxResults) {
|
|
1667
|
-
output += `\n... (truncated to ${maxResults} results, use maxResults=0 for all)\n`;
|
|
1668
|
-
}
|
|
1669
|
-
|
|
1670
|
-
output += `\nTip: Use get_frame with frameIndex to see full context for specific frames.\n`;
|
|
1671
|
-
} else {
|
|
1672
|
-
output += 'No matching logs found.\n';
|
|
1673
|
-
output += `Try different search terms or check available log levels with chrome_pilot_show_frames.\n`;
|
|
1674
|
-
}
|
|
1675
|
-
|
|
1676
|
-
return {
|
|
1677
|
-
content: [
|
|
1678
|
-
{
|
|
1679
|
-
type: 'text',
|
|
1680
|
-
text: output,
|
|
1681
|
-
},
|
|
1682
|
-
],
|
|
1683
|
-
};
|
|
1684
|
-
}
|
|
1685
|
-
|
|
1686
|
-
case 'get_frame_logs_paginated': {
|
|
1687
|
-
if (!args.sessionId || args.frameIndex === undefined) {
|
|
1688
|
-
throw new Error('Session ID and frame index are required');
|
|
1689
|
-
}
|
|
1690
|
-
|
|
1691
|
-
const logLevel = args.logLevel || 'all';
|
|
1692
|
-
const offset = args.offset || 0;
|
|
1693
|
-
const limit = args.limit || 100;
|
|
1694
|
-
const searchText = args.searchText;
|
|
1695
|
-
|
|
1696
|
-
const { database } = await import('./database.js');
|
|
1697
|
-
const recording = database.getRecording(args.sessionId);
|
|
1698
|
-
if (!recording) {
|
|
1699
|
-
throw new Error('Frame session not found');
|
|
1700
|
-
}
|
|
1701
|
-
|
|
1702
|
-
// Get the frame ID
|
|
1703
|
-
const frameRowStmt = database.db.prepare(`
|
|
1704
|
-
SELECT id FROM frames
|
|
1705
|
-
WHERE recording_id = ? AND frame_index = ?
|
|
1706
|
-
`);
|
|
1707
|
-
const frameRow = frameRowStmt.get(recording.id, args.frameIndex);
|
|
1708
|
-
|
|
1709
|
-
if (!frameRow) {
|
|
1710
|
-
throw new Error('Frame not found');
|
|
1711
|
-
}
|
|
1712
|
-
|
|
1713
|
-
// Build the query with filtering and pagination
|
|
1714
|
-
let logsQuery = `
|
|
1715
|
-
SELECT level, message, relative_time
|
|
1716
|
-
FROM console_logs
|
|
1717
|
-
WHERE frame_id = ?`;
|
|
1718
|
-
let queryParams = [frameRow.id];
|
|
1719
|
-
|
|
1720
|
-
// Add level filter
|
|
1721
|
-
if (logLevel !== 'all') {
|
|
1722
|
-
logsQuery += ` AND level = ?`;
|
|
1723
|
-
queryParams.push(logLevel);
|
|
1724
|
-
}
|
|
1725
|
-
|
|
1726
|
-
// Add search filter
|
|
1727
|
-
if (searchText) {
|
|
1728
|
-
logsQuery += ` AND LOWER(message) LIKE ?`;
|
|
1729
|
-
queryParams.push(`%${searchText.toLowerCase()}%`);
|
|
1730
|
-
}
|
|
1731
|
-
|
|
1732
|
-
// Get total count before pagination
|
|
1733
|
-
const countQuery = logsQuery.replace('SELECT level, message, relative_time', 'SELECT COUNT(*)');
|
|
1734
|
-
const totalCount = database.db.prepare(countQuery).get(...queryParams)['COUNT(*)'];
|
|
1735
|
-
|
|
1736
|
-
// Add pagination
|
|
1737
|
-
logsQuery += ` ORDER BY relative_time ASC LIMIT ? OFFSET ?`;
|
|
1738
|
-
queryParams.push(limit, offset);
|
|
1739
|
-
|
|
1740
|
-
const logsStmt = database.db.prepare(logsQuery);
|
|
1741
|
-
const logs = logsStmt.all(...queryParams);
|
|
1742
|
-
|
|
1743
|
-
let output = `Frame ${args.frameIndex} Logs (Paginated):\n`;
|
|
1744
|
-
output += `Total Matching Logs: ${totalCount}\n`;
|
|
1745
|
-
output += `Showing: ${Math.min(logs.length, limit)} logs (offset: ${offset})\n`;
|
|
1746
|
-
|
|
1747
|
-
if (logLevel !== 'all') {
|
|
1748
|
-
output += `Filter: ${logLevel.toUpperCase()} level only\n`;
|
|
1749
|
-
}
|
|
1750
|
-
if (searchText) {
|
|
1751
|
-
output += `Search: "${searchText}"\n`;
|
|
1752
|
-
}
|
|
1753
|
-
|
|
1754
|
-
output += `\n`;
|
|
1755
|
-
|
|
1756
|
-
if (logs.length > 0) {
|
|
1757
|
-
logs.forEach((log, index) => {
|
|
1758
|
-
const logNumber = offset + index + 1;
|
|
1759
|
-
output += `${logNumber}. [${log.level.toUpperCase()}] ${log.message}\n`;
|
|
1760
|
-
});
|
|
1761
|
-
|
|
1762
|
-
// Show pagination info
|
|
1763
|
-
const hasMore = (offset + logs.length) < totalCount;
|
|
1764
|
-
if (hasMore) {
|
|
1765
|
-
const nextOffset = offset + limit;
|
|
1766
|
-
output += `\n... ${totalCount - offset - logs.length} more logs available\n`;
|
|
1767
|
-
output += `Use get_frame_logs_paginated with offset=${nextOffset} to see more\n`;
|
|
1768
|
-
}
|
|
1769
|
-
} else {
|
|
1770
|
-
output += 'No logs found matching the criteria.\n';
|
|
1771
|
-
}
|
|
1772
|
-
|
|
1773
|
-
return {
|
|
1774
|
-
content: [
|
|
1775
|
-
{
|
|
1776
|
-
type: 'text',
|
|
1777
|
-
text: output,
|
|
1778
|
-
},
|
|
1779
|
-
],
|
|
1780
|
-
};
|
|
1781
|
-
}
|
|
1782
|
-
|
|
1783
|
-
case 'play_workflow_recording': {
|
|
1784
|
-
if (!args.sessionId) {
|
|
1785
|
-
throw new Error('Session ID is required');
|
|
1786
|
-
}
|
|
1787
|
-
|
|
1788
|
-
const speed = args.speed || 1;
|
|
1789
|
-
|
|
1790
|
-
// Get the workflow recording
|
|
1791
|
-
const recording = await chromeController.getWorkflowRecording(args.sessionId);
|
|
1792
|
-
if (recording.error) {
|
|
1793
|
-
throw new Error(recording.error);
|
|
1794
|
-
}
|
|
1795
|
-
|
|
1796
|
-
// Check if Chrome is connected
|
|
1797
|
-
const status = await chromeController.getConnectionStatus();
|
|
1798
|
-
if (!status.connected) {
|
|
1799
|
-
throw new Error('Chrome is not connected. Please launch Chrome or connect to an existing instance first.');
|
|
1800
|
-
}
|
|
1801
|
-
|
|
1802
|
-
// Play the workflow
|
|
1803
|
-
const result = await chromeController.playWorkflow(recording, recording.actions, speed);
|
|
1804
|
-
|
|
1805
|
-
return {
|
|
1806
|
-
content: [
|
|
1807
|
-
{
|
|
1808
|
-
type: 'text',
|
|
1809
|
-
text: `Workflow playback ${result.success ? 'completed successfully' : 'failed'}.\n` +
|
|
1810
|
-
`Executed ${result.executedActions}/${result.totalActions} actions.\n` +
|
|
1811
|
-
(result.error ? `Error: ${result.error}\n` : '') +
|
|
1812
|
-
`Duration: ${result.duration}ms`,
|
|
1813
|
-
},
|
|
1814
|
-
],
|
|
1815
|
-
};
|
|
1816
|
-
}
|
|
1817
|
-
|
|
1818
|
-
case 'play_workflow_by_name': {
|
|
1819
|
-
if (!args.name) {
|
|
1820
|
-
throw new Error('Workflow name is required');
|
|
1821
|
-
}
|
|
1822
|
-
|
|
1823
|
-
const speed = args.speed || 1;
|
|
1824
|
-
|
|
1825
|
-
// Find workflow by name
|
|
1826
|
-
const allRecordings = await chromeController.listWorkflowRecordings();
|
|
1827
|
-
const matchingRecording = allRecordings.recordings.find(r =>
|
|
1828
|
-
r.name && r.name.toLowerCase() === args.name.toLowerCase()
|
|
1829
|
-
);
|
|
1830
|
-
|
|
1831
|
-
if (!matchingRecording) {
|
|
1832
|
-
// Show available workflow names
|
|
1833
|
-
const availableNames = allRecordings.recordings
|
|
1834
|
-
.filter(r => r.name)
|
|
1835
|
-
.map(r => `- ${r.name}`)
|
|
1836
|
-
.join('\n');
|
|
1837
|
-
|
|
1838
|
-
throw new Error(`No workflow found with name: ${args.name}\n\nAvailable workflows:\n${availableNames || 'No named workflows found'}`);
|
|
1839
|
-
}
|
|
1840
|
-
|
|
1841
|
-
// Get the full workflow recording
|
|
1842
|
-
const recording = await chromeController.getWorkflowRecording(matchingRecording.session_id);
|
|
1843
|
-
if (recording.error) {
|
|
1844
|
-
throw new Error(recording.error);
|
|
1845
|
-
}
|
|
1846
|
-
|
|
1847
|
-
// Check if Chrome is connected
|
|
1848
|
-
const status = await chromeController.getConnectionStatus();
|
|
1849
|
-
if (!status.connected) {
|
|
1850
|
-
throw new Error('Chrome is not connected. Please launch Chrome or connect to an existing instance first.');
|
|
1851
|
-
}
|
|
1852
|
-
|
|
1853
|
-
// Play the workflow
|
|
1854
|
-
const result = await chromeController.playWorkflow(recording, recording.actions, speed);
|
|
1855
|
-
|
|
1856
|
-
return {
|
|
1857
|
-
content: [
|
|
1858
|
-
{
|
|
1859
|
-
type: 'text',
|
|
1860
|
-
text: `Playing workflow "${args.name}"...\n\n` +
|
|
1861
|
-
`Workflow playback ${result.success ? 'completed successfully' : 'failed'}.\n` +
|
|
1862
|
-
`Executed ${result.executedActions}/${result.totalActions} actions.\n` +
|
|
1863
|
-
(result.error ? `Error: ${result.error}\n` : '') +
|
|
1864
|
-
`Duration: ${result.duration}ms`,
|
|
1865
|
-
},
|
|
1866
|
-
],
|
|
1867
|
-
};
|
|
1868
|
-
}
|
|
1869
|
-
|
|
1870
|
-
case 'get_screen_interactions': {
|
|
1871
|
-
if (!args.sessionId) {
|
|
1872
|
-
throw new Error('Session ID is required');
|
|
1873
|
-
}
|
|
1874
|
-
|
|
1875
|
-
let interactions;
|
|
1876
|
-
|
|
1877
|
-
if (args.frameIndex !== undefined) {
|
|
1878
|
-
// Get interactions for specific frame
|
|
1879
|
-
interactions = await chromeController.getFrameInteractions(args.sessionId, args.frameIndex);
|
|
1880
|
-
} else {
|
|
1881
|
-
// Get all interactions for the recording
|
|
1882
|
-
interactions = await chromeController.getScreenInteractions(args.sessionId);
|
|
1883
|
-
}
|
|
1884
|
-
|
|
1885
|
-
// Filter by type if requested
|
|
1886
|
-
if (args.type) {
|
|
1887
|
-
interactions = interactions.filter(i => i.type === args.type);
|
|
1888
|
-
}
|
|
1889
|
-
|
|
1890
|
-
if (interactions.length === 0) {
|
|
1891
|
-
return {
|
|
1892
|
-
content: [
|
|
1893
|
-
{
|
|
1894
|
-
type: 'text',
|
|
1895
|
-
text: `No interactions found for session ${args.sessionId}${args.frameIndex !== undefined ? ` frame ${args.frameIndex}` : ''}${args.type ? ` of type '${args.type}'` : ''}.`,
|
|
1896
|
-
},
|
|
1897
|
-
],
|
|
1898
|
-
};
|
|
1899
|
-
}
|
|
1900
|
-
|
|
1901
|
-
let output = `Screen Interactions for session ${args.sessionId}`;
|
|
1902
|
-
if (args.frameIndex !== undefined) {
|
|
1903
|
-
output += ` (frame ${args.frameIndex})`;
|
|
1904
|
-
}
|
|
1905
|
-
if (args.type) {
|
|
1906
|
-
output += ` - Type: ${args.type}`;
|
|
1907
|
-
}
|
|
1908
|
-
output += `\nTotal: ${interactions.length} interactions\n\n`;
|
|
1909
|
-
|
|
1910
|
-
interactions.forEach((interaction, index) => {
|
|
1911
|
-
output += `${index + 1}. `;
|
|
1912
|
-
switch (interaction.type) {
|
|
1913
|
-
case 'click':
|
|
1914
|
-
output += `[CLICK] ${interaction.selector || `at (${interaction.x}, ${interaction.y})`}`;
|
|
1915
|
-
if (interaction.text) output += ` - "${interaction.text}"`;
|
|
1916
|
-
if (interaction.frame_index !== null) output += ` (frame ${interaction.frame_index})`;
|
|
1917
|
-
output += '\n';
|
|
1918
|
-
if (interaction.xpath) output += ` XPath: ${interaction.xpath}\n`;
|
|
1919
|
-
break;
|
|
1920
|
-
case 'input':
|
|
1921
|
-
output += `[INPUT] ${interaction.selector} = "${interaction.value}"`;
|
|
1922
|
-
if (interaction.placeholder) output += ` (placeholder: ${interaction.placeholder})`;
|
|
1923
|
-
if (interaction.frame_index !== null) output += ` (frame ${interaction.frame_index})`;
|
|
1924
|
-
output += '\n';
|
|
1925
|
-
break;
|
|
1926
|
-
case 'keypress':
|
|
1927
|
-
output += `[KEY] ${interaction.key}`;
|
|
1928
|
-
if (interaction.frame_index !== null) output += ` (frame ${interaction.frame_index})`;
|
|
1929
|
-
output += '\n';
|
|
1930
|
-
break;
|
|
1931
|
-
case 'scroll':
|
|
1932
|
-
output += `[SCROLL] to (${interaction.scrollX || interaction.x}, ${interaction.scrollY || interaction.y})`;
|
|
1933
|
-
if (interaction.frame_index !== null) output += ` (frame ${interaction.frame_index})`;
|
|
1934
|
-
output += '\n';
|
|
1935
|
-
break;
|
|
1936
|
-
default:
|
|
1937
|
-
output += `[${interaction.type.toUpperCase()}]`;
|
|
1938
|
-
if (interaction.frame_index !== null) output += ` (frame ${interaction.frame_index})`;
|
|
1939
|
-
output += '\n';
|
|
1940
|
-
}
|
|
1941
|
-
output += ` Time: ${new Date(interaction.timestamp).toISOString()}\n\n`;
|
|
1942
|
-
});
|
|
1943
|
-
|
|
1944
|
-
return {
|
|
1945
|
-
content: [
|
|
1946
|
-
{
|
|
1947
|
-
type: 'text',
|
|
1948
|
-
text: output,
|
|
1949
|
-
},
|
|
1950
|
-
],
|
|
1951
|
-
};
|
|
1952
|
-
}
|
|
1953
|
-
|
|
1954
|
-
default:
|
|
1955
|
-
throw new Error(`Unknown tool: ${name}`);
|
|
1956
|
-
}
|
|
1957
|
-
} catch (error) {
|
|
1958
|
-
return {
|
|
1959
|
-
content: [
|
|
1960
|
-
{
|
|
1961
|
-
type: 'text',
|
|
1962
|
-
text: `Error: ${error.message}`,
|
|
1963
|
-
},
|
|
1964
|
-
],
|
|
1965
|
-
isError: true,
|
|
1966
|
-
};
|
|
1967
|
-
}
|
|
1968
|
-
});
|
|
1969
|
-
|
|
1970
|
-
async function checkForExistingSingleServer() {
|
|
1971
|
-
try {
|
|
1972
|
-
const { database } = await import('./database.js');
|
|
1973
|
-
const pid = await database.getSingleServerInstance();
|
|
1974
|
-
|
|
1975
|
-
return { exists: !!pid, pid };
|
|
1976
|
-
} catch (error) {
|
|
1977
|
-
console.error('Error checking for single-server:', error.message);
|
|
1978
|
-
return { exists: false, pid: null };
|
|
1979
|
-
}
|
|
1980
|
-
}
|
|
1981
|
-
|
|
1982
|
-
/**
|
|
1983
|
-
* Safely validates a process ID to prevent injection attacks
|
|
1984
|
-
* @param {*} pid - The process ID to validate
|
|
1985
|
-
* @returns {number|null} - Validated PID or null if invalid
|
|
1986
|
-
*/
|
|
1987
|
-
function validateProcessId(pid) {
|
|
1988
|
-
// Security: Check for suspicious characters before parsing
|
|
1989
|
-
const pidStr = String(pid).trim();
|
|
1990
|
-
if (!/^\d+$/.test(pidStr)) {
|
|
1991
|
-
// Contains non-digit characters - potential injection attempt
|
|
1992
|
-
return null;
|
|
1993
|
-
}
|
|
1994
|
-
|
|
1995
|
-
const numericPid = parseInt(pidStr, 10);
|
|
1996
|
-
if (isNaN(numericPid) || numericPid <= 0 || numericPid > 4194304) { // Max PID on Linux/macOS
|
|
1997
|
-
return null;
|
|
1998
|
-
}
|
|
1999
|
-
return numericPid;
|
|
2000
|
-
}
|
|
2001
|
-
|
|
2002
|
-
/**
|
|
2003
|
-
* Safely kills a process using Node.js native APIs to prevent command injection
|
|
2004
|
-
* @param {number} pid - Validated process ID
|
|
2005
|
-
* @param {string} signal - Signal to send ('SIGTERM' or 'SIGKILL')
|
|
2006
|
-
* @returns {Promise<boolean>} - True if successful, false otherwise
|
|
2007
|
-
*/
|
|
2008
|
-
async function safeKillProcess(pid, signal = 'SIGTERM') {
|
|
2009
|
-
const validatedPid = validateProcessId(pid);
|
|
2010
|
-
if (!validatedPid) {
|
|
2011
|
-
console.error(`Invalid process ID: ${pid}`);
|
|
2012
|
-
return false;
|
|
2013
|
-
}
|
|
2014
|
-
|
|
2015
|
-
try {
|
|
2016
|
-
// Security: Use Node.js native process.kill() instead of shell commands
|
|
2017
|
-
process.kill(validatedPid, signal);
|
|
2018
|
-
return true;
|
|
2019
|
-
} catch (error) {
|
|
2020
|
-
if (error.code === 'ESRCH') {
|
|
2021
|
-
// Process doesn't exist
|
|
2022
|
-
return true;
|
|
2023
|
-
} else if (error.code === 'EPERM') {
|
|
2024
|
-
console.error(`Permission denied killing process ${validatedPid}`);
|
|
2025
|
-
return false;
|
|
2026
|
-
} else {
|
|
2027
|
-
console.error(`Error killing process ${validatedPid}: ${error.message}`);
|
|
2028
|
-
return false;
|
|
2029
|
-
}
|
|
2030
|
-
}
|
|
2031
|
-
}
|
|
2032
|
-
|
|
2033
|
-
/**
|
|
2034
|
-
* Find Chrome Debug processes using Node.js process list instead of shell commands
|
|
2035
|
-
* This prevents command injection while maintaining functionality
|
|
2036
|
-
*/
|
|
2037
|
-
async function findChromePilotProcesses() {
|
|
2038
|
-
const currentPid = process.pid;
|
|
2039
|
-
const pidsToKill = [];
|
|
2040
|
-
const processDescriptions = [];
|
|
2041
|
-
|
|
2042
|
-
try {
|
|
2043
|
-
// Security: Use Node.js process list instead of shell commands
|
|
2044
|
-
// This approach uses the 'ps-list' module or similar safe approach
|
|
2045
|
-
// For now, we'll use a safer implementation with spawn instead of exec
|
|
2046
|
-
const { spawn } = await import('child_process');
|
|
2047
|
-
|
|
2048
|
-
return new Promise((resolve) => {
|
|
2049
|
-
const ps = spawn('ps', ['aux'], { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
2050
|
-
let stdout = '';
|
|
2051
|
-
|
|
2052
|
-
ps.stdout.on('data', (data) => {
|
|
2053
|
-
stdout += data.toString();
|
|
2054
|
-
});
|
|
2055
|
-
|
|
2056
|
-
ps.on('close', (code) => {
|
|
2057
|
-
if (code !== 0) {
|
|
2058
|
-
console.error('Failed to get process list');
|
|
2059
|
-
resolve({ pidsToKill: [], processDescriptions: [] });
|
|
2060
|
-
return;
|
|
2061
|
-
}
|
|
2062
|
-
|
|
2063
|
-
const lines = stdout.trim().split('\n');
|
|
2064
|
-
|
|
2065
|
-
for (const line of lines) {
|
|
2066
|
-
// Security: Validate each line and extract PID safely
|
|
2067
|
-
const parts = line.trim().split(/\s+/);
|
|
2068
|
-
if (parts.length < 2) continue;
|
|
2069
|
-
|
|
2070
|
-
const pid = validateProcessId(parts[1]);
|
|
2071
|
-
if (!pid || pid === currentPid) continue;
|
|
2072
|
-
|
|
2073
|
-
// Check if it's a Chrome Debug related process
|
|
2074
|
-
let processType = '';
|
|
2075
|
-
if (line.includes('src/index.js')) {
|
|
2076
|
-
processType = 'MCP server';
|
|
2077
|
-
pidsToKill.push(pid);
|
|
2078
|
-
} else if (line.includes('standalone-server.js')) {
|
|
2079
|
-
processType = 'HTTP server';
|
|
2080
|
-
pidsToKill.push(pid);
|
|
2081
|
-
} else if (line.includes('chrome-pilot')) {
|
|
2082
|
-
processType = 'Chrome Debug process';
|
|
2083
|
-
pidsToKill.push(pid);
|
|
2084
|
-
}
|
|
2085
|
-
|
|
2086
|
-
if (processType) {
|
|
2087
|
-
processDescriptions.push(`${pid} (${processType})`);
|
|
2088
|
-
}
|
|
2089
|
-
}
|
|
2090
|
-
|
|
2091
|
-
resolve({ pidsToKill, processDescriptions });
|
|
2092
|
-
});
|
|
2093
|
-
|
|
2094
|
-
ps.on('error', (error) => {
|
|
2095
|
-
console.error('Error running ps command:', error.message);
|
|
2096
|
-
resolve({ pidsToKill: [], processDescriptions: [] });
|
|
2097
|
-
});
|
|
2098
|
-
});
|
|
2099
|
-
} catch (error) {
|
|
2100
|
-
console.error('Error finding processes:', error.message);
|
|
2101
|
-
return { pidsToKill: [], processDescriptions: [] };
|
|
2102
|
-
}
|
|
2103
|
-
}
|
|
2104
|
-
|
|
2105
|
-
async function killOtherChromePilotInstances() {
|
|
2106
|
-
try {
|
|
2107
|
-
console.error('Searching for other Chrome Debug instances...');
|
|
2108
|
-
|
|
2109
|
-
const { pidsToKill, processDescriptions } = await findChromePilotProcesses();
|
|
2110
|
-
|
|
2111
|
-
if (pidsToKill.length > 0) {
|
|
2112
|
-
console.error(`Found ${pidsToKill.length} other Chrome Debug instance(s) running:`);
|
|
2113
|
-
processDescriptions.forEach(desc => console.error(` - Process ${desc}`));
|
|
2114
|
-
console.error('Terminating processes...');
|
|
2115
|
-
|
|
2116
|
-
for (const pid of pidsToKill) {
|
|
2117
|
-
// Security: Use safe process killing with validated PIDs
|
|
2118
|
-
const termSuccess = await safeKillProcess(pid, 'SIGTERM');
|
|
2119
|
-
if (termSuccess) {
|
|
2120
|
-
console.error(`Sent TERM signal to process ${pid}`);
|
|
2121
|
-
} else {
|
|
2122
|
-
// Try force kill if graceful termination fails
|
|
2123
|
-
const killSuccess = await safeKillProcess(pid, 'SIGKILL');
|
|
2124
|
-
if (killSuccess) {
|
|
2125
|
-
console.error(`Force killed process ${pid}`);
|
|
2126
|
-
} else {
|
|
2127
|
-
console.error(`Failed to kill process ${pid}`);
|
|
2128
|
-
}
|
|
2129
|
-
}
|
|
2130
|
-
}
|
|
2131
|
-
|
|
2132
|
-
// Wait a moment for processes to terminate
|
|
2133
|
-
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
2134
|
-
console.error('Process cleanup completed');
|
|
2135
|
-
} else {
|
|
2136
|
-
console.error('No other Chrome Debug instances found');
|
|
2137
|
-
}
|
|
2138
|
-
} catch (error) {
|
|
2139
|
-
console.error('Error during process cleanup:', error.message);
|
|
2140
|
-
// Don't fail startup if cleanup fails
|
|
2141
|
-
}
|
|
2142
|
-
}
|
|
2143
|
-
|
|
2144
|
-
async function main() {
|
|
2145
|
-
const isSingleServerMode = process.argv.includes('--single-server');
|
|
2146
|
-
|
|
2147
|
-
if (isSingleServerMode) {
|
|
2148
|
-
console.error('Starting in single-server mode - killing other Chrome Debug instances...');
|
|
2149
|
-
await killOtherChromePilotInstances();
|
|
2150
|
-
|
|
2151
|
-
// Register this server instance in the database
|
|
2152
|
-
const { database } = await import('./database.js');
|
|
2153
|
-
await database.registerServerInstance(process.pid, 'single-server');
|
|
2154
|
-
|
|
2155
|
-
console.error(`Single-server mode active (PID: ${process.pid})`);
|
|
2156
|
-
} else {
|
|
2157
|
-
// Check if there's already a single-server running
|
|
2158
|
-
const singleServerCheck = await checkForExistingSingleServer();
|
|
2159
|
-
|
|
2160
|
-
if (singleServerCheck.exists) {
|
|
2161
|
-
console.error('⚠️ Detected existing single-server instance.');
|
|
2162
|
-
console.error(` Single-server PID: ${singleServerCheck.pid}`);
|
|
2163
|
-
console.error(' This Claude session will try to connect to the existing Chrome instance.');
|
|
2164
|
-
console.error(' If connection fails, this session will start its own Chrome instance.');
|
|
2165
|
-
}
|
|
2166
|
-
}
|
|
2167
|
-
|
|
2168
|
-
// Try to discover HTTP server
|
|
2169
|
-
httpServerPort = await discoverServer();
|
|
2170
|
-
if (httpServerPort) {
|
|
2171
|
-
console.error(`Found existing Chrome Debug HTTP server on port ${httpServerPort}`);
|
|
2172
|
-
} else {
|
|
2173
|
-
// Start embedded HTTP server
|
|
2174
|
-
console.error('Starting embedded HTTP server...');
|
|
2175
|
-
try {
|
|
2176
|
-
httpServerPort = await startHttpServer();
|
|
2177
|
-
console.error(`Started HTTP server on port ${httpServerPort}`);
|
|
2178
|
-
|
|
2179
|
-
// Also start WebSocket server for element selection
|
|
2180
|
-
const wsPort = await startWebSocketServer();
|
|
2181
|
-
console.error(`Started WebSocket server on port ${wsPort}`);
|
|
2182
|
-
} catch (error) {
|
|
2183
|
-
console.error('WARNING: Failed to start embedded HTTP server:', error.message);
|
|
2184
|
-
console.error('Chrome extension features will not be available');
|
|
2185
|
-
}
|
|
2186
|
-
}
|
|
2187
|
-
|
|
2188
|
-
// Start MCP server
|
|
2189
|
-
const transport = new StdioServerTransport();
|
|
2190
|
-
await server.connect(transport);
|
|
2191
|
-
console.error('Chrome Debug MCP server running');
|
|
2192
|
-
}
|
|
2193
|
-
|
|
2194
|
-
// Function to clean up single-server registration and port file
|
|
2195
|
-
async function cleanupSingleServerFlag() {
|
|
2196
|
-
try {
|
|
2197
|
-
const { database } = await import('./database.js');
|
|
2198
|
-
await database.unregisterServerInstance(process.pid);
|
|
2199
|
-
} catch (error) {
|
|
2200
|
-
// Ignore cleanup errors
|
|
2201
|
-
}
|
|
2202
|
-
|
|
2203
|
-
// Clean up port file if we started the HTTP server
|
|
2204
|
-
if (httpServerPort) {
|
|
2205
|
-
try {
|
|
2206
|
-
const { removePortFile } = await import('./port-discovery.js');
|
|
2207
|
-
removePortFile();
|
|
2208
|
-
} catch (error) {
|
|
2209
|
-
// Ignore cleanup errors
|
|
2210
|
-
}
|
|
2211
|
-
}
|
|
2212
|
-
}
|
|
2213
|
-
|
|
2214
|
-
// Clean shutdown
|
|
2215
|
-
process.on('SIGINT', async () => {
|
|
2216
|
-
await cleanupSingleServerFlag();
|
|
2217
|
-
await chromeController.close();
|
|
2218
|
-
process.exit(0);
|
|
2219
|
-
});
|
|
2220
|
-
|
|
2221
|
-
process.on('SIGTERM', async () => {
|
|
2222
|
-
await cleanupSingleServerFlag();
|
|
2223
|
-
await chromeController.close();
|
|
2224
|
-
process.exit(0);
|
|
2225
|
-
});
|
|
2226
|
-
|
|
2227
|
-
main().catch((error) => {
|
|
2228
|
-
console.error('Fatal error:', error);
|
|
2229
|
-
process.exit(1);
|
|
2230
|
-
});
|