@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.
Files changed (49) hide show
  1. package/CLAUDE.md +17 -1
  2. package/README.md +1 -1
  3. package/chrome-extension/activation-manager.js +10 -10
  4. package/chrome-extension/background.js +1045 -736
  5. package/chrome-extension/browser-recording-manager.js +1 -1
  6. package/chrome-extension/chrome-debug-logger.js +168 -0
  7. package/chrome-extension/chrome-session-manager.js +5 -5
  8. package/chrome-extension/console-interception-library.js +430 -0
  9. package/chrome-extension/content.css +16 -16
  10. package/chrome-extension/content.js +739 -221
  11. package/chrome-extension/data-buffer.js +5 -5
  12. package/chrome-extension/dom-tracker.js +9 -9
  13. package/chrome-extension/extension-config.js +1 -1
  14. package/chrome-extension/firebase-client.js +13 -13
  15. package/chrome-extension/frame-capture.js +20 -38
  16. package/chrome-extension/license-helper.js +33 -7
  17. package/chrome-extension/manifest.free.json +3 -6
  18. package/chrome-extension/network-tracker.js +9 -9
  19. package/chrome-extension/options.html +10 -0
  20. package/chrome-extension/options.js +21 -8
  21. package/chrome-extension/performance-monitor.js +17 -17
  22. package/chrome-extension/popup.html +230 -193
  23. package/chrome-extension/popup.js +146 -458
  24. package/chrome-extension/pro/enhanced-capture.js +406 -0
  25. package/chrome-extension/pro/frame-editor.html +433 -0
  26. package/chrome-extension/pro/frame-editor.js +1567 -0
  27. package/chrome-extension/pro/function-tracker.js +843 -0
  28. package/chrome-extension/pro/jszip.min.js +13 -0
  29. package/chrome-extension/upload-manager.js +7 -7
  30. package/dist/chromedebug-extension-free.zip +0 -0
  31. package/package.json +3 -1
  32. package/scripts/webpack.config.free.cjs +8 -8
  33. package/scripts/webpack.config.pro.cjs +2 -0
  34. package/src/cli.js +2 -2
  35. package/src/database.js +55 -7
  36. package/src/index.js +9 -6
  37. package/src/mcp/server.js +2 -2
  38. package/src/services/process-manager.js +10 -6
  39. package/src/services/process-tracker.js +10 -5
  40. package/src/services/profile-manager.js +17 -2
  41. package/src/validation/schemas.js +12 -11
  42. package/src/index-direct.js +0 -157
  43. package/src/index-modular.js +0 -219
  44. package/src/index-monolithic-backup.js +0 -2230
  45. package/src/legacy/chrome-controller-old.js +0 -1406
  46. package/src/legacy/index-express.js +0 -625
  47. package/src/legacy/index-old.js +0 -977
  48. package/src/legacy/routes.js +0 -260
  49. 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
- });