@dynamicu/chromedebug-mcp 2.6.6 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CLAUDE.md +1 -1
  2. package/README.md +1 -1
  3. package/chrome-extension/activation-manager.js +18 -4
  4. package/chrome-extension/background.js +1044 -552
  5. package/chrome-extension/browser-recording-manager.js +256 -0
  6. package/chrome-extension/chrome-debug-logger.js +168 -0
  7. package/chrome-extension/console-interception-library.js +430 -0
  8. package/chrome-extension/content.css +16 -16
  9. package/chrome-extension/content.js +617 -215
  10. package/chrome-extension/data-buffer.js +206 -17
  11. package/chrome-extension/extension-config.js +1 -1
  12. package/chrome-extension/frame-capture.js +52 -15
  13. package/chrome-extension/license-helper.js +26 -0
  14. package/chrome-extension/manifest.free.json +3 -6
  15. package/chrome-extension/options.js +1 -1
  16. package/chrome-extension/popup.html +315 -181
  17. package/chrome-extension/popup.js +673 -526
  18. package/chrome-extension/pro/enhanced-capture.js +406 -0
  19. package/chrome-extension/pro/frame-editor.html +410 -0
  20. package/chrome-extension/pro/frame-editor.js +1496 -0
  21. package/chrome-extension/pro/function-tracker.js +843 -0
  22. package/chrome-extension/pro/jszip.min.js +13 -0
  23. package/config/chromedebug-config.json +101 -0
  24. package/dist/chromedebug-extension-free.zip +0 -0
  25. package/package.json +3 -1
  26. package/scripts/package-pro-extension.js +1 -1
  27. package/scripts/webpack.config.free.cjs +11 -8
  28. package/scripts/webpack.config.pro.cjs +5 -0
  29. package/src/chrome-controller.js +7 -7
  30. package/src/cli.js +2 -2
  31. package/src/database.js +61 -9
  32. package/src/http-server.js +3 -2
  33. package/src/index.js +9 -6
  34. package/src/mcp/server.js +2 -2
  35. package/src/services/process-manager.js +10 -6
  36. package/src/services/process-tracker.js +10 -5
  37. package/src/services/profile-manager.js +17 -2
  38. package/src/validation/schemas.js +36 -6
  39. package/src/index-direct.js +0 -157
  40. package/src/index-modular.js +0 -219
  41. package/src/index-monolithic-backup.js +0 -2230
  42. package/src/legacy/chrome-controller-old.js +0 -1406
  43. package/src/legacy/index-express.js +0 -625
  44. package/src/legacy/index-old.js +0 -977
  45. package/src/legacy/routes.js +0 -260
  46. package/src/legacy/shared-storage.js +0 -101
@@ -1,1406 +0,0 @@
1
- import puppeteer from 'puppeteer';
2
- import multer from 'multer';
3
- import fs from 'fs/promises';
4
- import path from 'path';
5
- import { fileURLToPath } from 'url';
6
- import { findAvailablePort } from './utils.js';
7
- import { sharedStorage } from './shared-storage.js';
8
-
9
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
-
11
- export class ChromeController {
12
- constructor() {
13
- this.browser = null;
14
- this.page = null;
15
- this.client = null;
16
- this.paused = false;
17
- this.logs = [];
18
- this.debugPort = null;
19
- this.defaultTimeout = 10000; // 10 seconds default timeout
20
-
21
- // Store currently selected element information
22
- this.selectedElement = null;
23
-
24
- // Recording storage
25
- this.recordings = new Map();
26
-
27
- // Connection monitoring
28
- this.heartbeatInterval = null;
29
-
30
- // Setup multer for handling file uploads
31
- this.upload = multer({
32
- storage: multer.memoryStorage(),
33
- limits: { fileSize: 100 * 1024 * 1024 } // 100MB limit
34
- }).fields([
35
- { name: 'logs', maxCount: 1 },
36
- { name: 'duration', maxCount: 1 }
37
- ]);
38
- }
39
-
40
- // Helper method to add timeout to any promise
41
- withTimeout(promise, timeoutMs = this.defaultTimeout, operation = 'operation') {
42
- return Promise.race([
43
- promise,
44
- new Promise((_, reject) => {
45
- setTimeout(() => {
46
- reject(new Error(`${operation} timed out after ${timeoutMs}ms`));
47
- }, timeoutMs);
48
- })
49
- ]);
50
- }
51
-
52
- // Helper method to kill Chrome processes using the debug port
53
- async killChromeProcesses() {
54
- try {
55
- if (this.debugPort) {
56
- const { exec } = await import('child_process');
57
- const { promisify } = await import('util');
58
- const execAsync = promisify(exec);
59
-
60
- // Kill any Chrome processes using our debug port
61
- await execAsync(`lsof -ti:${this.debugPort} | xargs kill -9`).catch(() => {
62
- // Ignore errors if no processes found
63
- });
64
- }
65
- } catch (error) {
66
- console.error('Error killing Chrome processes:', error.message);
67
- }
68
- }
69
-
70
- async launch() {
71
- try {
72
- // Check if browser is still connected
73
- if (this.browser) {
74
- try {
75
- // Test if the browser is still responsive
76
- await this.browser.version();
77
- } catch (e) {
78
- // Browser is disconnected, clean up
79
- console.error('Previous browser instance is disconnected, cleaning up...');
80
- this.browser = null;
81
- this.page = null;
82
- this.client = null;
83
- }
84
- }
85
-
86
- // Close any existing browser instance
87
- if (this.browser) {
88
- await this.close();
89
- }
90
-
91
- // Try to connect to existing Chrome instance first
92
- console.error('Attempting to connect to existing Chrome instance...');
93
-
94
- // Common debugging ports
95
- const commonPorts = [9222, 9223, 9224, 9225, 9229];
96
- let connected = false;
97
-
98
- for (const port of commonPorts) {
99
- try {
100
- console.error(`Trying to connect to Chrome on port ${port}...`);
101
- this.browser = await puppeteer.connect({
102
- browserURL: `http://localhost:${port}`,
103
- defaultViewport: null
104
- });
105
-
106
- this.debugPort = port;
107
- connected = true;
108
- console.error(`Successfully connected to existing Chrome on port ${port}`);
109
- break;
110
- } catch (e) {
111
- // Continue to next port
112
- }
113
- }
114
-
115
- if (!connected) {
116
- // If no existing Chrome found, launch a new one
117
- console.error('No existing Chrome found with debugging enabled. Launching new instance...');
118
- console.error('To connect to existing Chrome, start it with: --remote-debugging-port=9222');
119
-
120
- // Find an available port for Chrome debugging
121
- this.debugPort = await findAvailablePort(9222, 9322);
122
-
123
- // Kill any stuck Chrome processes on this port
124
- await this.killChromeProcesses();
125
-
126
- console.error(`Launching Chrome with debugging port ${this.debugPort}`);
127
-
128
- // Detect Chrome executable path based on platform
129
- let executablePath;
130
- const platform = process.platform;
131
-
132
- if (platform === 'darwin') {
133
- // macOS
134
- executablePath = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
135
- } else if (platform === 'win32') {
136
- // Windows
137
- executablePath = 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe';
138
- // Try alternative paths if the first doesn't exist
139
- const fs = await import('fs');
140
- if (!fs.existsSync(executablePath)) {
141
- executablePath = 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe';
142
- }
143
- } else {
144
- // Linux
145
- executablePath = '/usr/bin/google-chrome';
146
- }
147
-
148
- console.error(`Using Chrome executable: ${executablePath}`);
149
-
150
- this.browser = await this.withTimeout(
151
- puppeteer.launch({
152
- headless: false,
153
- executablePath: executablePath,
154
- args: [
155
- `--remote-debugging-port=${this.debugPort}`,
156
- '--no-first-run',
157
- '--no-default-browser-check'
158
- ],
159
- defaultViewport: null,
160
- ignoreDefaultArgs: ['--disable-extensions']
161
- }),
162
- 30000, // 30 seconds for browser launch
163
- 'browser launch'
164
- );
165
- }
166
-
167
- // Set up disconnect handler
168
- this.browser.on('disconnected', () => {
169
- console.error('Browser disconnected');
170
- this.browser = null;
171
- this.page = null;
172
- this.client = null;
173
- this.stopHeartbeat();
174
- });
175
-
176
- // Start heartbeat monitoring
177
- this.startHeartbeat();
178
- } catch (error) {
179
- throw new Error(`Failed to connect to Chrome: ${error.message}. Make sure Chrome is running with --remote-debugging-port=9222`);
180
- }
181
-
182
- const pages = await this.withTimeout(
183
- this.browser.pages(),
184
- this.defaultTimeout,
185
- 'get browser pages'
186
- );
187
- this.page = pages[0] || await this.withTimeout(
188
- this.browser.newPage(),
189
- this.defaultTimeout,
190
- 'create new page'
191
- );
192
-
193
- this.client = await this.withTimeout(
194
- this.page.createCDPSession(),
195
- this.defaultTimeout,
196
- 'create CDP session'
197
- );
198
-
199
- await this.withTimeout(
200
- Promise.all([
201
- this.client.send('Debugger.enable'),
202
- this.client.send('Runtime.enable'),
203
- this.client.send('Console.enable')
204
- ]),
205
- this.defaultTimeout,
206
- 'enable CDP domains'
207
- );
208
-
209
- this.client.on('Debugger.paused', () => {
210
- this.paused = true;
211
- });
212
-
213
- this.client.on('Debugger.resumed', () => {
214
- this.paused = false;
215
- });
216
-
217
- this.client.on('Console.messageAdded', (params) => {
218
- this.logs.push({
219
- level: params.message.level,
220
- text: params.message.text,
221
- timestamp: new Date().toISOString()
222
- });
223
- if (this.logs.length > 100) {
224
- this.logs.shift();
225
- }
226
- });
227
-
228
- const browserProcess = this.browser.process();
229
- const processId = browserProcess ? browserProcess.pid : 'unknown';
230
-
231
- return {
232
- browserWSEndpoint: this.browser.wsEndpoint(),
233
- debuggerUrl: `ws://localhost:${this.debugPort}/devtools/browser/${processId}`,
234
- debugPort: this.debugPort
235
- };
236
- }
237
-
238
- async connectToExisting(port = 9222) {
239
- try {
240
- // Close any existing connection
241
- if (this.browser) {
242
- await this.close();
243
- }
244
-
245
- console.error(`Attempting to connect to Chrome on port ${port}...`);
246
-
247
- // Try to connect to the specified port
248
- this.browser = await puppeteer.connect({
249
- browserURL: `http://localhost:${port}`,
250
- defaultViewport: null
251
- });
252
-
253
- this.debugPort = port;
254
- console.error(`Successfully connected to existing Chrome on port ${port}`);
255
-
256
- // Set up disconnect handler
257
- this.browser.on('disconnected', () => {
258
- console.error('Browser disconnected');
259
- this.browser = null;
260
- this.page = null;
261
- this.client = null;
262
- this.stopHeartbeat();
263
- });
264
-
265
- // Start heartbeat monitoring
266
- this.startHeartbeat();
267
-
268
- // Get pages and set up CDP
269
- const pages = await this.withTimeout(
270
- this.browser.pages(),
271
- this.defaultTimeout,
272
- 'get browser pages'
273
- );
274
- this.page = pages[0] || await this.withTimeout(
275
- this.browser.newPage(),
276
- this.defaultTimeout,
277
- 'create new page'
278
- );
279
-
280
- this.client = await this.withTimeout(
281
- this.page.createCDPSession(),
282
- this.defaultTimeout,
283
- 'create CDP session'
284
- );
285
-
286
- await this.withTimeout(
287
- Promise.all([
288
- this.client.send('Debugger.enable'),
289
- this.client.send('Runtime.enable'),
290
- this.client.send('Console.enable')
291
- ]),
292
- this.defaultTimeout,
293
- 'enable CDP domains'
294
- );
295
-
296
- this.client.on('Debugger.paused', () => {
297
- this.paused = true;
298
- });
299
-
300
- this.client.on('Debugger.resumed', () => {
301
- this.paused = false;
302
- });
303
-
304
- this.client.on('Console.messageAdded', (params) => {
305
- this.logs.push({
306
- level: params.message.level,
307
- text: params.message.text,
308
- timestamp: new Date().toISOString()
309
- });
310
- if (this.logs.length > 100) {
311
- this.logs.shift();
312
- }
313
- });
314
-
315
- return {
316
- success: true,
317
- message: `Connected to existing Chrome instance on port ${port}`,
318
- browserWSEndpoint: this.browser.wsEndpoint(),
319
- debugPort: this.debugPort
320
- };
321
- } catch (error) {
322
- throw new Error(`Failed to connect to Chrome on port ${port}: ${error.message}. Make sure Chrome is running with --remote-debugging-port=${port}`);
323
- }
324
- }
325
-
326
- async isConnected() {
327
- if (!this.browser) return false;
328
- try {
329
- await this.withTimeout(
330
- this.browser.version(),
331
- 5000,
332
- 'browser version check'
333
- );
334
- return true;
335
- } catch (e) {
336
- return false;
337
- }
338
- }
339
-
340
- async ensureConnected() {
341
- if (!await this.isConnected()) {
342
- // Try to reconnect to the last known port if we had a connection before
343
- if (this.debugPort) {
344
- console.log(`Connection lost. Attempting to reconnect to Chrome on port ${this.debugPort}...`);
345
- try {
346
- await this.connectToExisting(this.debugPort);
347
- console.log('Successfully reconnected to Chrome');
348
- return;
349
- } catch (error) {
350
- console.error(`Failed to reconnect: ${error.message}`);
351
- }
352
- }
353
-
354
- throw new Error('Chrome not connected. Please launch Chrome first using launch_chrome or connect to an existing instance using connect_to_existing_chrome.');
355
- }
356
- }
357
-
358
- async navigateTo(url) {
359
- await this.ensureConnected();
360
- if (!this.page) throw new Error('No page available');
361
-
362
- try {
363
- await this.withTimeout(
364
- this.page.goto(url, { waitUntil: 'domcontentloaded' }),
365
- 30000,
366
- `navigate to ${url}`
367
- );
368
- return { status: 'success', url };
369
- } catch (error) {
370
- throw new Error(`Failed to navigate to ${url}: ${error.message}`);
371
- }
372
- }
373
-
374
- async pause() {
375
- await this.ensureConnected();
376
- if (!this.client) throw new Error('Chrome not connected');
377
- await this.withTimeout(
378
- this.client.send('Debugger.pause'),
379
- this.defaultTimeout,
380
- 'pause execution'
381
- );
382
- return { status: 'paused' };
383
- }
384
-
385
- async resume() {
386
- await this.ensureConnected();
387
- if (!this.client) throw new Error('Chrome not connected');
388
- await this.withTimeout(
389
- this.client.send('Debugger.resume'),
390
- this.defaultTimeout,
391
- 'resume execution'
392
- );
393
- return { status: 'resumed' };
394
- }
395
-
396
- async stepOver() {
397
- await this.ensureConnected();
398
- if (!this.client) throw new Error('Chrome not connected');
399
- await this.withTimeout(
400
- this.client.send('Debugger.stepOver'),
401
- this.defaultTimeout,
402
- 'step over'
403
- );
404
- return { status: 'stepped' };
405
- }
406
-
407
- async evaluate(expression) {
408
- await this.ensureConnected();
409
- if (!this.client) throw new Error('Chrome not connected');
410
-
411
- try {
412
- const result = await this.withTimeout(
413
- this.client.send('Runtime.evaluate', {
414
- expression: expression,
415
- returnByValue: true,
416
- generatePreview: true
417
- }),
418
- this.defaultTimeout,
419
- 'JavaScript evaluation'
420
- );
421
-
422
- if (result.exceptionDetails) {
423
- return {
424
- error: true,
425
- message: result.exceptionDetails.text || 'Evaluation error',
426
- exception: result.exceptionDetails
427
- };
428
- }
429
-
430
- return {
431
- result: result.result.value,
432
- type: result.result.type,
433
- className: result.result.className
434
- };
435
- } catch (error) {
436
- return {
437
- error: true,
438
- message: error.message
439
- };
440
- }
441
- }
442
-
443
- async getScopes() {
444
- await this.ensureConnected();
445
- if (!this.client) throw new Error('Chrome not connected');
446
-
447
- try {
448
- const { callFrames } = await this.client.send('Debugger.getStackTrace');
449
-
450
- if (!callFrames || callFrames.length === 0) {
451
- return { scopes: [], message: 'No active call frames' };
452
- }
453
-
454
- const topFrame = callFrames[0];
455
- const scopes = [];
456
-
457
- for (const scope of topFrame.scopeChain) {
458
- const scopeInfo = {
459
- type: scope.type,
460
- name: scope.name,
461
- variables: {}
462
- };
463
-
464
- if (scope.object && scope.object.objectId) {
465
- const { result } = await this.client.send('Runtime.getProperties', {
466
- objectId: scope.object.objectId,
467
- ownProperties: true
468
- });
469
-
470
- for (const prop of result) {
471
- if (prop.value) {
472
- scopeInfo.variables[prop.name] = {
473
- value: prop.value.value,
474
- type: prop.value.type
475
- };
476
- }
477
- }
478
- }
479
-
480
- scopes.push(scopeInfo);
481
- }
482
-
483
- return { scopes };
484
- } catch (error) {
485
- return {
486
- error: true,
487
- message: error.message || 'Failed to get scopes'
488
- };
489
- }
490
- }
491
-
492
- async setBreakpoint(url, lineNumber) {
493
- await this.ensureConnected();
494
- if (!this.client) throw new Error('Chrome not connected');
495
-
496
- try {
497
- const result = await this.withTimeout(
498
- this.client.send('Debugger.setBreakpointByUrl', {
499
- url: url,
500
- lineNumber: lineNumber
501
- }),
502
- this.defaultTimeout,
503
- 'set breakpoint'
504
- );
505
-
506
- return {
507
- breakpointId: result.breakpointId,
508
- locations: result.locations
509
- };
510
- } catch (error) {
511
- return {
512
- error: true,
513
- message: error.message
514
- };
515
- }
516
- }
517
-
518
- getLogs() {
519
- return { logs: this.logs };
520
- }
521
-
522
- async takeScreenshot(options = {}) {
523
- await this.ensureConnected();
524
- if (!this.page) throw new Error('Chrome not connected');
525
-
526
- try {
527
- const screenshotOptions = {
528
- type: options.type || 'jpeg', // Default to JPEG for better compression
529
- fullPage: options.fullPage !== false
530
- };
531
-
532
- // Always use lowRes mode unless explicitly disabled
533
- // This MCP is designed for AI processing, not human viewing
534
- if (options.lowRes !== false) {
535
- // Get current viewport
536
- const viewport = this.page.viewport();
537
-
538
- // Set ultra aggressive quality for JPEG (matching frame capture settings)
539
- if (screenshotOptions.type === 'jpeg' || screenshotOptions.type === 'jpg') {
540
- screenshotOptions.quality = options.quality || 30; // 30% quality for consistency with frame capture
541
- }
542
-
543
- // For low-res mode, always resize viewport to small size
544
- let originalViewport = null;
545
- const targetWidth = 320; // Mobile-sized width for minimal tokens
546
- const targetHeight = 568; // iPhone SE sized height
547
-
548
- if (viewport) {
549
- originalViewport = viewport;
550
- // Always set to our target size for consistency
551
- await this.page.setViewport({
552
- width: targetWidth,
553
- height: targetHeight
554
- });
555
- }
556
-
557
- // For full page screenshots, aggressively limit the capture area
558
- if (screenshotOptions.fullPage) {
559
- // Inject JavaScript to limit page height for screenshot
560
- await this.page.evaluate(() => {
561
- const maxHeight = 600; // Very limited height for AI
562
- // Force document to be smaller
563
- document.documentElement.style.maxHeight = `${maxHeight}px`;
564
- document.documentElement.style.overflow = 'hidden';
565
- document.body.style.maxHeight = `${maxHeight}px`;
566
- document.body.style.overflow = 'hidden';
567
- });
568
-
569
- // Also limit fullPage to just capture visible viewport
570
- screenshotOptions.fullPage = false;
571
- screenshotOptions.clip = {
572
- x: 0,
573
- y: 0,
574
- width: targetWidth,
575
- height: Math.min(targetHeight, 600)
576
- };
577
- }
578
-
579
- screenshotOptions.encoding = 'base64';
580
- const screenshot = await this.withTimeout(
581
- this.page.screenshot(screenshotOptions),
582
- 30000,
583
- 'take screenshot'
584
- );
585
-
586
- // Restore page state
587
- if (options.fullPage !== false) {
588
- await this.page.evaluate(() => {
589
- document.documentElement.style.maxHeight = '';
590
- document.documentElement.style.overflow = '';
591
- document.body.style.maxHeight = '';
592
- document.body.style.overflow = '';
593
- });
594
- }
595
-
596
- // Restore original viewport if it was changed
597
- if (originalViewport) {
598
- await this.page.setViewport(originalViewport);
599
- }
600
-
601
- const sizeInBytes = Math.ceil(screenshot.length * 3/4);
602
- const sizeInKB = Math.round(sizeInBytes / 1024);
603
-
604
- // If still too large, return error with size info
605
- if (sizeInKB > 50) { // 50KB limit for AI processing
606
- return {
607
- error: true,
608
- message: `Screenshot too large for AI processing (${sizeInKB}KB). Maximum recommended size is 50KB. Try disabling fullPage or using path parameter to save to file.`,
609
- size: `${sizeInKB}KB`,
610
- recommendation: 'Use fullPage: false or path parameter to save to file'
611
- };
612
- }
613
-
614
- return {
615
- screenshot: screenshot,
616
- size: `${sizeInKB}KB`,
617
- type: screenshotOptions.type,
618
- fullPage: screenshotOptions.fullPage,
619
- lowRes: true,
620
- quality: screenshotOptions.quality,
621
- message: 'Low-resolution screenshot optimized for AI parsing'
622
- };
623
- }
624
-
625
- // If a path is provided, save to file and return metadata only
626
- if (options.path) {
627
- screenshotOptions.path = options.path;
628
- if (options.quality && (screenshotOptions.type === 'jpeg' || screenshotOptions.type === 'jpg')) {
629
- screenshotOptions.quality = options.quality || 80; // Higher quality for file saves
630
- }
631
-
632
- const screenshot = await this.withTimeout(
633
- this.page.screenshot(screenshotOptions),
634
- 30000,
635
- 'take screenshot'
636
- );
637
-
638
- // When saving to file, just return metadata
639
- return {
640
- saved: true,
641
- path: options.path,
642
- type: screenshotOptions.type,
643
- fullPage: screenshotOptions.fullPage
644
- };
645
- }
646
-
647
- // For regular screenshots without path (non-lowRes mode)
648
- // This should rarely be used as we default to lowRes for AI
649
- screenshotOptions.encoding = 'base64';
650
- if (options.quality && (screenshotOptions.type === 'jpeg' || screenshotOptions.type === 'jpg')) {
651
- screenshotOptions.quality = options.quality || 80;
652
- }
653
-
654
- const screenshot = await this.withTimeout(
655
- this.page.screenshot(screenshotOptions),
656
- 30000,
657
- 'take screenshot'
658
- );
659
-
660
- // Calculate size
661
- const sizeInBytes = Math.ceil(screenshot.length * 3/4);
662
- const sizeInKB = Math.round(sizeInBytes / 1024);
663
-
664
- // For large screenshots, warn the user
665
- if (sizeInKB > 100) {
666
- return {
667
- screenshot: screenshot.substring(0, 1000) + '... [truncated]',
668
- size: `${sizeInKB}KB`,
669
- type: screenshotOptions.type,
670
- fullPage: screenshotOptions.fullPage,
671
- truncated: true,
672
- message: 'Screenshot too large. Use path parameter to save to file or lowRes: true for AI parsing.'
673
- };
674
- }
675
-
676
- return {
677
- screenshot: screenshot,
678
- size: `${sizeInKB}KB`,
679
- type: screenshotOptions.type,
680
- fullPage: screenshotOptions.fullPage
681
- };
682
- } catch (error) {
683
- return {
684
- error: true,
685
- message: error.message
686
- };
687
- }
688
- }
689
-
690
- async forceReset() {
691
- console.error('Force resetting Chrome...');
692
-
693
- // Kill all Chrome processes
694
- await this.killChromeProcesses();
695
-
696
- // Clear internal state
697
- this.browser = null;
698
- this.page = null;
699
- this.client = null;
700
- this.paused = false;
701
- this.logs = [];
702
-
703
- return { status: 'reset complete' };
704
- }
705
-
706
- async close() {
707
- this.stopHeartbeat(); // Stop monitoring when closing
708
- if (this.browser) {
709
- try {
710
- await this.browser.close();
711
- } catch (e) {
712
- // Browser might already be closed
713
- console.error('Error closing browser:', e.message);
714
- } finally {
715
- this.browser = null;
716
- this.page = null;
717
- this.client = null;
718
- }
719
- }
720
- }
721
-
722
- async isConnected() {
723
- try {
724
- if (!this.browser || !this.page) {
725
- return false;
726
- }
727
- await this.browser.version();
728
- return true;
729
- } catch (e) {
730
- return false;
731
- }
732
- }
733
-
734
- async selectElement(selector, instruction, elementInfo) {
735
- await this.ensureConnected();
736
- if (!this.client || !this.page) throw new Error('Chrome not connected');
737
-
738
- try {
739
- // Sanitize and validate inputs
740
- if (!selector || typeof selector !== 'string') {
741
- throw new Error('Invalid selector provided');
742
- }
743
-
744
- // Extract element information using CDP
745
- const elementData = await this.page.evaluate((sel) => {
746
- try {
747
- const element = document.querySelector(sel);
748
- if (!element) {
749
- return { error: `Element not found: ${sel}` };
750
- }
751
-
752
- // Highlight the selected element
753
- element.style.outline = '3px solid #E91E63';
754
- element.style.outlineOffset = '2px';
755
-
756
- // Get computed styles
757
- const computedStyles = window.getComputedStyle(element);
758
- const relevantStyles = {};
759
-
760
- // Extract key style properties
761
- const styleProps = [
762
- 'width', 'height', 'padding', 'margin', 'backgroundColor',
763
- 'color', 'fontSize', 'fontWeight', 'border', 'borderRadius',
764
- 'display', 'position', 'top', 'left', 'right', 'bottom',
765
- 'transform', 'opacity', 'boxShadow', 'textAlign'
766
- ];
767
-
768
- styleProps.forEach(prop => {
769
- relevantStyles[prop] = computedStyles[prop];
770
- });
771
-
772
- return {
773
- outerHTML: element.outerHTML,
774
- tagName: element.tagName.toLowerCase(),
775
- id: element.id,
776
- className: element.className,
777
- computedStyles: relevantStyles,
778
- boundingBox: element.getBoundingClientRect(),
779
- textContent: element.textContent.substring(0, 100)
780
- };
781
- } catch (error) {
782
- return { error: error.message };
783
- }
784
- }, selector);
785
-
786
- if (elementData.error) {
787
- throw new Error(elementData.error);
788
- }
789
-
790
- // Include React component info if provided
791
- if (elementInfo && elementInfo.reactComponent) {
792
- elementData.reactComponent = elementInfo.reactComponent;
793
- }
794
-
795
- // Store the selected element information
796
- this.selectedElement = {
797
- selector,
798
- instruction: instruction || '',
799
- elementData,
800
- timestamp: new Date().toISOString()
801
- };
802
-
803
- // Log the selection
804
- const reactInfo = elementData.reactComponent ?
805
- ` [React: ${elementData.reactComponent.componentName}]` : '';
806
- this.logs.push({
807
- timestamp: new Date().toISOString(),
808
- type: 'element-selected',
809
- message: `Selected: ${selector} - ${elementData.tagName}${elementData.id ? '#' + elementData.id : ''}${reactInfo}`
810
- });
811
-
812
- const response = {
813
- success: true,
814
- message: 'Element selected and ready for MCP commands',
815
- element: {
816
- selector,
817
- tagName: elementData.tagName,
818
- id: elementData.id,
819
- classes: elementData.className,
820
- size: `${elementData.boundingBox.width}px × ${elementData.boundingBox.height}px`,
821
- instruction: instruction || 'No instruction provided'
822
- }
823
- };
824
-
825
- // Add React component info to response if available
826
- if (elementData.reactComponent) {
827
- response.element.reactComponent = elementData.reactComponent;
828
- }
829
-
830
- return response;
831
- } catch (error) {
832
- this.logs.push({
833
- timestamp: new Date().toISOString(),
834
- type: 'selection-error',
835
- message: error.message
836
- });
837
-
838
- return {
839
- success: false,
840
- error: error.message
841
- };
842
- }
843
- }
844
-
845
- async getSelectedElement() {
846
- if (!this.selectedElement) {
847
- return {
848
- selected: false,
849
- message: 'No element currently selected'
850
- };
851
- }
852
-
853
- return {
854
- selected: true,
855
- ...this.selectedElement
856
- };
857
- }
858
-
859
- async applyToSelectedElement(cssRules) {
860
- if (!this.selectedElement) {
861
- throw new Error('No element currently selected');
862
- }
863
-
864
- await this.ensureConnected();
865
- if (!this.page) throw new Error('Chrome not connected');
866
-
867
- const { selector } = this.selectedElement;
868
-
869
- try {
870
- // Inject styles using CDP Runtime.evaluate
871
- const injectionResult = await this.page.evaluate((sel, cssRules) => {
872
- try {
873
- // Check if our style element already exists
874
- let styleEl = document.getElementById('chrome-pilot-styles');
875
- if (!styleEl) {
876
- styleEl = document.createElement('style');
877
- styleEl.id = 'chrome-pilot-styles';
878
- document.head.appendChild(styleEl);
879
- }
880
-
881
- // Append new rules
882
- styleEl.innerHTML += `\n/* Chrome Debug MCP - ${new Date().toISOString()} */\n${sel} { ${cssRules} }\n`;
883
-
884
- return { success: true, message: 'Styles applied successfully' };
885
- } catch (error) {
886
- return { success: false, error: error.message };
887
- }
888
- }, selector, cssRules);
889
-
890
- if (!injectionResult.success) {
891
- throw new Error(injectionResult.error);
892
- }
893
-
894
- // Log the result
895
- this.logs.push({
896
- timestamp: new Date().toISOString(),
897
- type: 'css-applied',
898
- message: `Applied to ${selector}: ${cssRules}`
899
- });
900
-
901
- return {
902
- success: true,
903
- applied: {
904
- selector,
905
- css: cssRules
906
- }
907
- };
908
- } catch (error) {
909
- return {
910
- success: false,
911
- error: error.message
912
- };
913
- }
914
- }
915
-
916
- async executeOnSelectedElement(jsCode) {
917
- if (!this.selectedElement) {
918
- throw new Error('No element currently selected');
919
- }
920
-
921
- await this.ensureConnected();
922
- if (!this.page) throw new Error('Chrome not connected');
923
-
924
- const { selector } = this.selectedElement;
925
-
926
- try {
927
- const result = await this.page.evaluate((sel, code) => {
928
- try {
929
- const element = document.querySelector(sel);
930
- if (!element) {
931
- throw new Error(`Element not found: ${sel}`);
932
- }
933
-
934
- const fn = new Function('element', code);
935
- return {
936
- success: true,
937
- result: fn(element)
938
- };
939
- } catch (error) {
940
- return {
941
- success: false,
942
- error: error.message
943
- };
944
- }
945
- }, selector, jsCode);
946
-
947
- if (!result.success) {
948
- throw new Error(result.error);
949
- }
950
-
951
- return {
952
- success: true,
953
- result: result.result
954
- };
955
- } catch (error) {
956
- return {
957
- success: false,
958
- error: error.message
959
- };
960
- }
961
- }
962
-
963
- async clearSelectedElement() {
964
- if (!this.selectedElement) {
965
- return {
966
- success: true,
967
- message: 'No element was selected'
968
- };
969
- }
970
-
971
- // Remove highlight from the element
972
- if (this.page) {
973
- await this.page.evaluate((sel) => {
974
- const element = document.querySelector(sel);
975
- if (element) {
976
- element.style.outline = '';
977
- element.style.outlineOffset = '';
978
- }
979
- }, this.selectedElement.selector).catch(() => {});
980
- }
981
-
982
- this.selectedElement = null;
983
-
984
- return {
985
- success: true,
986
- message: 'Selected element cleared'
987
- };
988
- }
989
-
990
-
991
- generateCssFromInstruction(instruction, elementInfo) {
992
- const lowerInstruction = instruction.toLowerCase();
993
- const styles = {};
994
- let css = '';
995
-
996
- // Size modifications
997
- if (lowerInstruction.includes('larger') || lowerInstruction.includes('bigger')) {
998
- // If element has explicit width/height, increase them
999
- if (elementInfo.computedStyles && elementInfo.computedStyles.width !== 'auto') {
1000
- const currentWidth = parseFloat(elementInfo.computedStyles.width);
1001
- if (!isNaN(currentWidth)) {
1002
- styles.width = `${currentWidth * 1.2}px !important`;
1003
- }
1004
- }
1005
- if (elementInfo.computedStyles && elementInfo.computedStyles.height !== 'auto') {
1006
- const currentHeight = parseFloat(elementInfo.computedStyles.height);
1007
- if (!isNaN(currentHeight)) {
1008
- styles.height = `${currentHeight * 1.2}px !important`;
1009
- }
1010
- }
1011
- // Fallback to transform if no explicit dimensions
1012
- if (!styles.width && !styles.height) {
1013
- styles.transform = 'scale(1.2)';
1014
- styles.transformOrigin = 'center';
1015
- }
1016
- } else if (lowerInstruction.includes('smaller')) {
1017
- if (elementInfo.computedStyles && elementInfo.computedStyles.width !== 'auto') {
1018
- const currentWidth = parseFloat(elementInfo.computedStyles.width);
1019
- if (!isNaN(currentWidth)) {
1020
- styles.width = `${currentWidth * 0.8}px !important`;
1021
- }
1022
- }
1023
- if (elementInfo.computedStyles && elementInfo.computedStyles.height !== 'auto') {
1024
- const currentHeight = parseFloat(elementInfo.computedStyles.height);
1025
- if (!isNaN(currentHeight)) {
1026
- styles.height = `${currentHeight * 0.8}px !important`;
1027
- }
1028
- }
1029
- if (!styles.width && !styles.height) {
1030
- styles.transform = 'scale(0.8)';
1031
- styles.transformOrigin = 'center';
1032
- }
1033
- }
1034
-
1035
- // Color modifications
1036
- if (lowerInstruction.includes('blue')) {
1037
- styles.backgroundColor = '#2196F3 !important';
1038
- if (elementInfo.tagName === 'button' || elementInfo.tagName === 'a') {
1039
- styles.color = 'white !important';
1040
- }
1041
- } else if (lowerInstruction.includes('red')) {
1042
- styles.backgroundColor = '#F44336 !important';
1043
- styles.color = 'white !important';
1044
- } else if (lowerInstruction.includes('green')) {
1045
- styles.backgroundColor = '#4CAF50 !important';
1046
- styles.color = 'white !important';
1047
- } else if (lowerInstruction.includes('yellow')) {
1048
- styles.backgroundColor = '#FFEB3B !important';
1049
- styles.color = '#333 !important';
1050
- } else if (lowerInstruction.includes('purple')) {
1051
- styles.backgroundColor = '#9C27B0 !important';
1052
- styles.color = 'white !important';
1053
- } else if (lowerInstruction.includes('orange')) {
1054
- styles.backgroundColor = '#FF9800 !important';
1055
- styles.color = 'white !important';
1056
- }
1057
-
1058
- // Layout modifications
1059
- if (lowerInstruction.includes('center')) {
1060
- if (lowerInstruction.includes('text')) {
1061
- styles.textAlign = 'center !important';
1062
- } else {
1063
- // For block-level centering
1064
- if (elementInfo.computedStyles && elementInfo.computedStyles.display !== 'inline') {
1065
- styles.margin = '0 auto !important';
1066
- styles.display = 'block !important';
1067
- } else {
1068
- // For inline elements, center the parent
1069
- styles.display = 'block !important';
1070
- styles.textAlign = 'center !important';
1071
- }
1072
- }
1073
- }
1074
-
1075
- // Visibility
1076
- if (lowerInstruction.includes('hide')) {
1077
- styles.display = 'none !important';
1078
- } else if (lowerInstruction.includes('show')) {
1079
- styles.display = 'block !important';
1080
- }
1081
-
1082
- // Border modifications
1083
- if (lowerInstruction.includes('border')) {
1084
- if (lowerInstruction.includes('remove') || lowerInstruction.includes('no')) {
1085
- styles.border = 'none !important';
1086
- } else {
1087
- styles.border = '2px solid #333 !important';
1088
- }
1089
- }
1090
-
1091
- // Border radius
1092
- if (lowerInstruction.includes('round')) {
1093
- if (lowerInstruction.includes('full')) {
1094
- styles.borderRadius = '50% !important';
1095
- } else {
1096
- styles.borderRadius = '8px !important';
1097
- }
1098
- }
1099
-
1100
- // Shadow
1101
- if (lowerInstruction.includes('shadow')) {
1102
- if (lowerInstruction.includes('remove') || lowerInstruction.includes('no')) {
1103
- styles.boxShadow = 'none !important';
1104
- } else if (lowerInstruction.includes('big') || lowerInstruction.includes('large')) {
1105
- styles.boxShadow = '0 10px 20px rgba(0, 0, 0, 0.2) !important';
1106
- } else {
1107
- styles.boxShadow = '0 4px 6px rgba(0, 0, 0, 0.1) !important';
1108
- }
1109
- }
1110
-
1111
- // Text modifications
1112
- if (lowerInstruction.includes('bold')) {
1113
- styles.fontWeight = 'bold !important';
1114
- }
1115
- if (lowerInstruction.includes('italic')) {
1116
- styles.fontStyle = 'italic !important';
1117
- }
1118
- if (lowerInstruction.includes('underline')) {
1119
- styles.textDecoration = 'underline !important';
1120
- }
1121
-
1122
- // Spacing
1123
- if (lowerInstruction.includes('padding')) {
1124
- if (lowerInstruction.includes('more') || lowerInstruction.includes('add')) {
1125
- styles.padding = '20px !important';
1126
- } else if (lowerInstruction.includes('remove') || lowerInstruction.includes('no')) {
1127
- styles.padding = '0 !important';
1128
- } else {
1129
- styles.padding = '10px !important';
1130
- }
1131
- }
1132
-
1133
- if (lowerInstruction.includes('margin')) {
1134
- if (lowerInstruction.includes('more') || lowerInstruction.includes('add')) {
1135
- styles.margin = '20px !important';
1136
- } else if (lowerInstruction.includes('remove') || lowerInstruction.includes('no')) {
1137
- styles.margin = '0 !important';
1138
- } else {
1139
- styles.margin = '10px !important';
1140
- }
1141
- }
1142
-
1143
- // Animation hints
1144
- if (lowerInstruction.includes('animate') || lowerInstruction.includes('transition')) {
1145
- styles.transition = 'all 0.3s ease !important';
1146
- }
1147
-
1148
- // Generate CSS string
1149
- if (Object.keys(styles).length > 0) {
1150
- css = Object.entries(styles).map(([k, v]) => {
1151
- // Convert camelCase to kebab-case
1152
- const kebab = k.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$1-$2').toLowerCase();
1153
- return `${kebab}: ${v}`;
1154
- }).join('; ');
1155
- }
1156
-
1157
- return { styles, css };
1158
- }
1159
-
1160
- // Handle recording upload
1161
-
1162
- // Get recording for analysis
1163
- async getRecording(recordingId) {
1164
- console.log(`Looking for recording: ${recordingId}`);
1165
- const recording = await sharedStorage.get(recordingId);
1166
-
1167
- if (!recording) {
1168
- // List available recordings for debugging
1169
- const availableRecordings = await sharedStorage.list();
1170
- console.log('Available recordings:', availableRecordings.map(r => r.id));
1171
- return { error: true, message: 'Recording not found' };
1172
- }
1173
-
1174
- // Handle frame capture sessions
1175
- if (recording.type === 'frame_capture') {
1176
- const sessionInfo = await sharedStorage.getFrameSessionInfo(recordingId);
1177
- return {
1178
- success: true,
1179
- type: 'frame_capture',
1180
- sessionInfo: sessionInfo,
1181
- message: 'This is a frame capture recording. Use get_frame_session_info and get_frame to access the frames.',
1182
- recording: {
1183
- id: recordingId,
1184
- type: 'frame_capture',
1185
- totalFrames: sessionInfo ? sessionInfo.totalFrames : 0
1186
- }
1187
- };
1188
- }
1189
-
1190
- // Handle chunked recordings (legacy - no longer supported)
1191
- if (recording.type === 'chunked_recording') {
1192
- return {
1193
- error: true,
1194
- message: 'Chunked recordings are no longer supported. Only frame capture recordings are available.'
1195
- };
1196
- }
1197
-
1198
- // Handle old single-file recordings (if any exist)
1199
- if (recording.data && recording.data.toString) {
1200
- return {
1201
- error: true,
1202
- message: 'Old recording format no longer supported. Only frame capture recordings are available.'
1203
- };
1204
- }
1205
-
1206
- // Handle frame capture recordings
1207
- if (recording.type === 'frame_capture') {
1208
- return {
1209
- success: true,
1210
- recording: {
1211
- id: recording.sessionId,
1212
- frames: recording.frames,
1213
- totalFrames: recording.frames.length,
1214
- timestamp: recording.timestamp,
1215
- timestamp: recording.timestamp,
1216
- filename: recording.filename
1217
- }
1218
- };
1219
- }
1220
-
1221
- return { error: true, message: 'Unknown recording format' };
1222
- }
1223
-
1224
- // Delete recording
1225
- async deleteRecording(recordingId) {
1226
- const recording = await sharedStorage.get(recordingId);
1227
- if (!recording) {
1228
- return { error: true, message: 'Recording not found' };
1229
- }
1230
-
1231
- await sharedStorage.delete(recordingId);
1232
- console.log(`Deleted recording ${recordingId}`);
1233
-
1234
- return {
1235
- success: true,
1236
- message: 'Recording deleted successfully'
1237
- };
1238
- }
1239
-
1240
- // Store frame batch
1241
- async storeFrameBatch(sessionId, frames) {
1242
- console.log(`[storeFrameBatch] Received sessionId: ${sessionId}, and a batch of ${frames ? frames.length : 'undefined'} frames.`);
1243
-
1244
- if (!sessionId || !Array.isArray(frames)) {
1245
- console.error('[storeFrameBatch] Invalid arguments received.');
1246
- throw new Error('Invalid arguments: sessionId and frames array are required.');
1247
- }
1248
-
1249
- try {
1250
- console.log(`Storing ${frames.length} frames for session ${sessionId}`);
1251
-
1252
- // Store frames in shared storage
1253
- const result = await sharedStorage.storeFrameBatch(sessionId, frames);
1254
- console.log('Storage result:', result ? 'success' : 'failed');
1255
-
1256
- return {
1257
- success: true,
1258
- sessionId: sessionId,
1259
- framesStored: frames.length
1260
- };
1261
- } catch (error) {
1262
- console.error('Error storing frame batch:', error);
1263
- throw error; // Re-throw to be caught by route handler
1264
- }
1265
- }
1266
-
1267
- async checkFrameSession(sessionId) {
1268
- try {
1269
- const sessionInfo = await sharedStorage.getFrameSessionInfo(sessionId);
1270
- if (sessionInfo && !sessionInfo.error) {
1271
- return {
1272
- found: true,
1273
- sessionId: sessionId,
1274
- totalFrames: sessionInfo.totalFrames,
1275
- duration: sessionInfo.duration,
1276
- serverPort: process.env.PORT || 3000
1277
- };
1278
- } else {
1279
- return { found: false };
1280
- }
1281
- } catch (error) {
1282
- console.error('Error checking frame session:', error);
1283
- return { found: false, error: error.message };
1284
- }
1285
- }
1286
-
1287
- async associateLogsWithFrames(sessionId, logs) {
1288
- try {
1289
- console.log(`Associating ${logs.length} logs with frames for session ${sessionId}`);
1290
-
1291
- // Use the database's built-in log association method
1292
- const result = await sharedStorage.associateLogsWithFrames(sessionId, logs);
1293
-
1294
- return result;
1295
- } catch (error) {
1296
- console.error('Error associating logs:', error);
1297
- return { error: true, message: error.message };
1298
- }
1299
- }
1300
-
1301
- // Get frame session info
1302
- async getFrameSessionInfo(sessionId) {
1303
- return await sharedStorage.getFrameSessionInfo(sessionId);
1304
- }
1305
-
1306
- // Get specific frame
1307
- async getFrame(sessionId, frameIndex) {
1308
- return await sharedStorage.getFrame(sessionId, frameIndex);
1309
- }
1310
-
1311
- // Get chunked recording session info (legacy - no longer supported)
1312
- async getRecordingInfo(sessionId) {
1313
- return { error: true, message: 'Chunked recordings are no longer supported. Use get_frame_session_info for frame capture recordings.' };
1314
- }
1315
-
1316
- // Get specific chunk from recording session (legacy - no longer supported)
1317
- async getRecordingChunk(sessionId, chunkIndex) {
1318
- return { error: true, message: 'Chunked recordings are no longer supported. Use get_frame for frame capture recordings.' };
1319
- }
1320
-
1321
- // Get entire frame session
1322
- async getFrameSession(sessionId) {
1323
- return await sharedStorage.getFrameSession(sessionId);
1324
- }
1325
-
1326
- // Save edited frame session
1327
- async saveEditedSession(sessionId, sessionData) {
1328
- try {
1329
- await sharedStorage.ensureInitialized();
1330
-
1331
- if (!sessionData.frames || !Array.isArray(sessionData.frames)) {
1332
- return { error: true, message: 'Invalid session data: frames array required' };
1333
- }
1334
-
1335
- // Store the frames using the database
1336
- const result = await sharedStorage.storeFrameBatch(sessionId, sessionData.frames);
1337
-
1338
- console.log(`Saved edited session ${sessionId} with ${sessionData.frames.length} frames`);
1339
-
1340
- return {
1341
- success: true,
1342
- sessionId: sessionId,
1343
- totalFrames: sessionData.frames.length
1344
- };
1345
- } catch (error) {
1346
- console.error('Error saving edited session:', error);
1347
- return { error: true, message: error.message };
1348
- }
1349
- }
1350
-
1351
- // Import session from Chrome extension storage format
1352
- async importSessionFromChrome(sessionId, sessionData) {
1353
- try {
1354
- await sharedStorage.ensureInitialized();
1355
-
1356
- if (!sessionData.frames || !Array.isArray(sessionData.frames)) {
1357
- return { error: true, message: 'Invalid session data: frames array required' };
1358
- }
1359
-
1360
- // Store the frames using the database
1361
- const result = await sharedStorage.storeFrameBatch(sessionId, sessionData.frames);
1362
-
1363
- console.log(`Imported session ${sessionId} with ${sessionData.frames.length} frames`);
1364
-
1365
- return {
1366
- success: true,
1367
- sessionId: sessionId,
1368
- totalFrames: sessionData.frames.length,
1369
- message: 'Session imported successfully'
1370
- };
1371
- } catch (error) {
1372
- console.error('Error importing session:', error);
1373
- return { error: true, message: error.message };
1374
- }
1375
- }
1376
-
1377
- // Heartbeat methods for connection monitoring
1378
- startHeartbeat() {
1379
- this.stopHeartbeat(); // Clear any existing interval
1380
-
1381
- this.heartbeatInterval = setInterval(async () => {
1382
- try {
1383
- if (this.browser && !await this.isConnected()) {
1384
- console.log('Heartbeat detected disconnection, attempting to reconnect...');
1385
- if (this.debugPort) {
1386
- try {
1387
- await this.connectToExisting(this.debugPort);
1388
- console.log('Heartbeat reconnection successful');
1389
- } catch (error) {
1390
- console.error('Heartbeat reconnection failed:', error.message);
1391
- }
1392
- }
1393
- }
1394
- } catch (error) {
1395
- console.error('Heartbeat error:', error);
1396
- }
1397
- }, 5000); // Check every 5 seconds
1398
- }
1399
-
1400
- stopHeartbeat() {
1401
- if (this.heartbeatInterval) {
1402
- clearInterval(this.heartbeatInterval);
1403
- this.heartbeatInterval = null;
1404
- }
1405
- }
1406
- }