@dynamicu/chromedebug-mcp 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/CLAUDE.md +344 -0
  2. package/LICENSE +21 -0
  3. package/README.md +250 -0
  4. package/chrome-extension/README.md +41 -0
  5. package/chrome-extension/background.js +3917 -0
  6. package/chrome-extension/chrome-session-manager.js +706 -0
  7. package/chrome-extension/content.css +181 -0
  8. package/chrome-extension/content.js +3022 -0
  9. package/chrome-extension/data-buffer.js +435 -0
  10. package/chrome-extension/dom-tracker.js +411 -0
  11. package/chrome-extension/extension-config.js +78 -0
  12. package/chrome-extension/firebase-client.js +278 -0
  13. package/chrome-extension/firebase-config.js +32 -0
  14. package/chrome-extension/firebase-config.module.js +22 -0
  15. package/chrome-extension/firebase-config.module.template.js +27 -0
  16. package/chrome-extension/firebase-config.template.js +36 -0
  17. package/chrome-extension/frame-capture.js +407 -0
  18. package/chrome-extension/icon128.png +1 -0
  19. package/chrome-extension/icon16.png +1 -0
  20. package/chrome-extension/icon48.png +1 -0
  21. package/chrome-extension/license-helper.js +181 -0
  22. package/chrome-extension/logger.js +23 -0
  23. package/chrome-extension/manifest.json +73 -0
  24. package/chrome-extension/network-tracker.js +510 -0
  25. package/chrome-extension/offscreen.html +10 -0
  26. package/chrome-extension/options.html +203 -0
  27. package/chrome-extension/options.js +282 -0
  28. package/chrome-extension/pako.min.js +2 -0
  29. package/chrome-extension/performance-monitor.js +533 -0
  30. package/chrome-extension/pii-redactor.js +405 -0
  31. package/chrome-extension/popup.html +532 -0
  32. package/chrome-extension/popup.js +2446 -0
  33. package/chrome-extension/upload-manager.js +323 -0
  34. package/chrome-extension/web-vitals.iife.js +1 -0
  35. package/config/api-keys.json +11 -0
  36. package/config/chrome-pilot-config.json +45 -0
  37. package/package.json +126 -0
  38. package/scripts/cleanup-processes.js +109 -0
  39. package/scripts/config-manager.js +280 -0
  40. package/scripts/generate-extension-config.js +53 -0
  41. package/scripts/setup-security.js +64 -0
  42. package/src/capture/architecture.js +426 -0
  43. package/src/capture/error-handling-tests.md +38 -0
  44. package/src/capture/error-handling-types.ts +360 -0
  45. package/src/capture/index.js +508 -0
  46. package/src/capture/interfaces.js +625 -0
  47. package/src/capture/memory-manager.js +713 -0
  48. package/src/capture/types.js +342 -0
  49. package/src/chrome-controller.js +2658 -0
  50. package/src/cli.js +19 -0
  51. package/src/config-loader.js +303 -0
  52. package/src/database.js +2178 -0
  53. package/src/firebase-license-manager.js +462 -0
  54. package/src/firebase-privacy-guard.js +397 -0
  55. package/src/http-server.js +1516 -0
  56. package/src/index-direct.js +157 -0
  57. package/src/index-modular.js +219 -0
  58. package/src/index-monolithic-backup.js +2230 -0
  59. package/src/index.js +305 -0
  60. package/src/legacy/chrome-controller-old.js +1406 -0
  61. package/src/legacy/index-express.js +625 -0
  62. package/src/legacy/index-old.js +977 -0
  63. package/src/legacy/routes.js +260 -0
  64. package/src/legacy/shared-storage.js +101 -0
  65. package/src/logger.js +10 -0
  66. package/src/mcp/handlers/chrome-tool-handler.js +306 -0
  67. package/src/mcp/handlers/element-tool-handler.js +51 -0
  68. package/src/mcp/handlers/frame-tool-handler.js +957 -0
  69. package/src/mcp/handlers/request-handler.js +104 -0
  70. package/src/mcp/handlers/workflow-tool-handler.js +636 -0
  71. package/src/mcp/server.js +68 -0
  72. package/src/mcp/tools/index.js +701 -0
  73. package/src/middleware/auth.js +371 -0
  74. package/src/middleware/security.js +267 -0
  75. package/src/port-discovery.js +258 -0
  76. package/src/routes/admin.js +182 -0
  77. package/src/services/browser-daemon.js +494 -0
  78. package/src/services/chrome-service.js +375 -0
  79. package/src/services/failover-manager.js +412 -0
  80. package/src/services/git-safety-service.js +675 -0
  81. package/src/services/heartbeat-manager.js +200 -0
  82. package/src/services/http-client.js +195 -0
  83. package/src/services/process-manager.js +318 -0
  84. package/src/services/process-tracker.js +574 -0
  85. package/src/services/profile-manager.js +449 -0
  86. package/src/services/project-manager.js +415 -0
  87. package/src/services/session-manager.js +497 -0
  88. package/src/services/session-registry.js +491 -0
  89. package/src/services/unified-session-manager.js +678 -0
  90. package/src/shared-storage-old.js +267 -0
  91. package/src/standalone-server.js +53 -0
  92. package/src/utils/extension-path.js +145 -0
  93. package/src/utils.js +187 -0
  94. package/src/validation/log-transformer.js +125 -0
  95. package/src/validation/schemas.js +391 -0
@@ -0,0 +1,494 @@
1
+ /**
2
+ * BrowserDaemon - Shared Chrome Browser Instance Manager
3
+ *
4
+ * Provides centralized Chrome browser management with enhanced security isolation.
5
+ * Multiple Claude instances can share a single Chrome browser while maintaining
6
+ * complete profile isolation and secure session management.
7
+ *
8
+ * Key Features:
9
+ * - Single shared Chrome browser with multiple isolated profiles
10
+ * - Session-based security isolation (no data leakage between sessions)
11
+ * - Automatic health monitoring and recovery
12
+ * - Graceful fallback to individual browsers on daemon issues
13
+ * - Resource limits to prevent DoS attacks
14
+ * - Persistent session registry with atomic operations
15
+ */
16
+
17
+ import puppeteer from 'puppeteer';
18
+ import { randomUUID } from 'crypto';
19
+ import { promises as fs } from 'fs';
20
+ import path from 'path';
21
+ import os from 'os';
22
+ import { ProfileManager } from './profile-manager.js';
23
+ import { PersistentRegistry } from './session-registry.js';
24
+ import { FailoverManager } from './failover-manager.js';
25
+ import { findAvailablePort } from '../utils.js';
26
+ import { getExtensionPath } from '../utils/extension-path.js';
27
+
28
+ export class BrowserDaemon {
29
+ constructor(options = {}) {
30
+ this.browser = null;
31
+ this.isRunning = false;
32
+ this.startTime = null;
33
+ this.daemonId = randomUUID();
34
+
35
+ // Core managers for security and isolation
36
+ this.profileManager = new ProfileManager();
37
+ this.sessionRegistry = new PersistentRegistry();
38
+ this.failoverManager = new FailoverManager();
39
+
40
+ // Configuration with security defaults
41
+ this.config = {
42
+ maxConcurrentSessions: options.maxConcurrentSessions || 10,
43
+ healthCheckInterval: options.healthCheckInterval || 10000,
44
+ sessionTimeoutMs: options.sessionTimeoutMs || 30 * 60 * 1000, // 30 minutes
45
+ daemonPort: options.daemonPort || null, // Auto-detect
46
+ enableHeadless: options.enableHeadless || false,
47
+ ...options
48
+ };
49
+
50
+ // Active sessions tracking
51
+ this.activeSessions = new Map(); // sessionId -> { contextId, profilePath, claudeInstanceId, lastActivity }
52
+ this.healthCheckTimer = null;
53
+ this.isShuttingDown = false;
54
+
55
+ // Bind methods to preserve context
56
+ this.performHealthCheck = this.performHealthCheck.bind(this);
57
+ this.gracefulShutdown = this.gracefulShutdown.bind(this);
58
+
59
+ // Setup cleanup handlers
60
+ this.setupProcessCleanupHandlers();
61
+ }
62
+
63
+ /**
64
+ * Setup process cleanup handlers to ensure graceful shutdown
65
+ */
66
+ setupProcessCleanupHandlers() {
67
+ const signals = ['SIGINT', 'SIGTERM', 'SIGQUIT'];
68
+ signals.forEach(signal => {
69
+ process.on(signal, async () => {
70
+ console.log(`[BrowserDaemon] Received ${signal}, initiating graceful shutdown...`);
71
+ await this.gracefulShutdown();
72
+ process.exit(0);
73
+ });
74
+ });
75
+
76
+ process.on('uncaughtException', async (error) => {
77
+ console.error('[BrowserDaemon] Uncaught exception:', error);
78
+ await this.gracefulShutdown();
79
+ process.exit(1);
80
+ });
81
+
82
+ process.on('unhandledRejection', async (reason, promise) => {
83
+ console.error('[BrowserDaemon] Unhandled rejection at:', promise, 'reason:', reason);
84
+ await this.gracefulShutdown();
85
+ process.exit(1);
86
+ });
87
+ }
88
+
89
+ /**
90
+ * Starts the shared browser daemon
91
+ * @returns {Promise<DaemonStartResult>}
92
+ */
93
+ async start() {
94
+ if (this.isRunning) {
95
+ throw new Error('Browser daemon is already running');
96
+ }
97
+
98
+ try {
99
+ console.log(`[BrowserDaemon] Starting daemon ${this.daemonId}...`);
100
+
101
+ // Initialize core managers
102
+ await this.profileManager.initialize();
103
+ await this.sessionRegistry.initialize();
104
+ await this.failoverManager.initialize();
105
+
106
+ // Find available port for debugging
107
+ this.config.daemonPort = this.config.daemonPort || await findAvailablePort(9222, 9300);
108
+
109
+ // Launch shared Chrome browser with security flags
110
+ this.browser = await this.launchSharedBrowser();
111
+ this.isRunning = true;
112
+ this.startTime = new Date();
113
+
114
+ // Note: Auto-restart functionality removed - browser disconnects will not trigger automatic restarts
115
+
116
+ // Start health monitoring
117
+ this.startHealthCheck();
118
+
119
+ console.log(`[BrowserDaemon] Daemon started successfully on port ${this.config.daemonPort}`);
120
+
121
+ return {
122
+ success: true,
123
+ daemonId: this.daemonId,
124
+ port: this.config.daemonPort,
125
+ startTime: this.startTime,
126
+ maxSessions: this.config.maxConcurrentSessions
127
+ };
128
+ } catch (error) {
129
+ console.error('[BrowserDaemon] Failed to start daemon:', error);
130
+ await this.cleanup();
131
+ throw new Error(`Daemon startup failed: ${error.message}`);
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Launches the shared Chrome browser with security configurations
137
+ * @returns {Promise<Browser>}
138
+ */
139
+ async launchSharedBrowser() {
140
+ // Get Chrome extension path with fallback logic
141
+ const extensionPath = getExtensionPath();
142
+
143
+ // Build launch args
144
+ const args = [
145
+ `--remote-debugging-port=${this.config.daemonPort}`,
146
+ '--no-first-run',
147
+ '--no-default-browser-check',
148
+ '--disable-features=VizDisplayCompositor',
149
+ '--disable-setuid-sandbox',
150
+ '--no-sandbox',
151
+ '--disable-dev-shm-usage',
152
+ // Security: Disable potentially dangerous features
153
+ '--disable-plugins',
154
+ '--disable-java',
155
+ // Performance optimizations for shared use
156
+ '--max_old_space_size=4096',
157
+ '--no-zygote',
158
+ '--disable-background-timer-throttling',
159
+ '--disable-renderer-backgrounding'
160
+ ];
161
+
162
+ // Add extension flags if extension found
163
+ if (extensionPath) {
164
+ args.push(
165
+ '--enable-extensions',
166
+ '--extensions-on-chrome-urls',
167
+ `--disable-extensions-except=${extensionPath}`,
168
+ `--load-extension=${extensionPath}`
169
+ );
170
+ console.log(`[BrowserDaemon] Loading ChromeDebug extension from: ${extensionPath}`);
171
+ } else {
172
+ console.warn('[BrowserDaemon] ChromeDebug extension not found. Visual selection features will be unavailable.');
173
+ }
174
+
175
+ const launchOptions = {
176
+ headless: this.config.enableHeadless,
177
+ args,
178
+ defaultViewport: null,
179
+ ignoreDefaultArgs: ['--disable-extensions', '--disable-default-apps', '--disable-component-extensions-with-background-pages'],
180
+ timeout: 60000,
181
+ // Use a temporary user data dir for the shared browser
182
+ userDataDir: await this.createSharedUserDataDir()
183
+ };
184
+
185
+ console.log(`[BrowserDaemon] Launching Chrome with debugging port ${this.config.daemonPort}`);
186
+ return await puppeteer.launch(launchOptions);
187
+ }
188
+
189
+ /**
190
+ * Creates a temporary user data directory for the shared browser
191
+ * @returns {Promise<string>}
192
+ */
193
+ async createSharedUserDataDir() {
194
+ const tmpDir = os.tmpdir();
195
+ const sharedDirName = `chrome-daemon-${this.daemonId}`;
196
+ const sharedUserDataDir = path.join(tmpDir, sharedDirName);
197
+
198
+ await fs.mkdir(sharedUserDataDir, { recursive: true });
199
+ console.log(`[BrowserDaemon] Created shared user data directory: ${sharedUserDataDir}`);
200
+
201
+ return sharedUserDataDir;
202
+ }
203
+
204
+ /**
205
+ * Allocates a new isolated session for a Claude instance
206
+ * @param {string} claudeInstanceId - Unique identifier for the Claude instance
207
+ * @param {Object} options - Session allocation options
208
+ * @returns {Promise<SessionAllocation>}
209
+ */
210
+ async allocateSession(claudeInstanceId, options = {}) {
211
+ if (!this.isRunning || !this.browser) {
212
+ throw new Error('Browser daemon is not running');
213
+ }
214
+
215
+ // Check session limits
216
+ if (this.activeSessions.size >= this.config.maxConcurrentSessions) {
217
+ throw new Error(`Maximum concurrent sessions reached (${this.config.maxConcurrentSessions})`);
218
+ }
219
+
220
+ // Validate Claude instance ID
221
+ if (!claudeInstanceId || typeof claudeInstanceId !== 'string') {
222
+ throw new Error('Valid claudeInstanceId is required');
223
+ }
224
+
225
+ try {
226
+ const sessionId = randomUUID();
227
+
228
+ // Create isolated profile for this session
229
+ const profilePath = await this.profileManager.createSessionProfile(sessionId, claudeInstanceId);
230
+
231
+ // Create isolated browser context
232
+ const context = await this.browser.createBrowserContext({
233
+ // Each session gets its own isolated context
234
+ // Note: Puppeteer's createBrowserContext doesn't accept userDataDir
235
+ // Instead, we use Chrome's profile management
236
+ });
237
+
238
+ // Create a page in the context
239
+ const page = await context.newPage();
240
+
241
+ // Store session information
242
+ const sessionData = {
243
+ sessionId,
244
+ claudeInstanceId,
245
+ contextId: context._id || sessionId, // Use context ID or fallback to session ID
246
+ profilePath,
247
+ context,
248
+ page,
249
+ createdAt: new Date(),
250
+ lastActivity: new Date()
251
+ };
252
+
253
+ this.activeSessions.set(sessionId, sessionData);
254
+
255
+ // Register session persistently
256
+ await this.sessionRegistry.updateSession(sessionId, {
257
+ claudeInstanceId,
258
+ profilePath,
259
+ contextId: sessionData.contextId,
260
+ daemonId: this.daemonId,
261
+ createdAt: sessionData.createdAt,
262
+ lastActivity: sessionData.lastActivity
263
+ });
264
+
265
+ console.log(`[BrowserDaemon] Allocated session ${sessionId} for Claude instance ${claudeInstanceId}`);
266
+
267
+ return {
268
+ success: true,
269
+ sessionId,
270
+ contextId: sessionData.contextId,
271
+ profilePath,
272
+ browserWSEndpoint: this.browser.wsEndpoint(),
273
+ debugPort: this.config.daemonPort,
274
+ connectionInfo: {
275
+ type: 'shared-daemon',
276
+ daemonId: this.daemonId,
277
+ sessionId
278
+ }
279
+ };
280
+ } catch (error) {
281
+ console.error('[BrowserDaemon] Session allocation failed:', error);
282
+ throw new Error(`Session allocation failed: ${error.message}`);
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Deallocates a session and cleans up resources
288
+ * @param {string} sessionId - Session ID to deallocate
289
+ * @returns {Promise<boolean>}
290
+ */
291
+ async deallocateSession(sessionId) {
292
+ if (!sessionId || !this.activeSessions.has(sessionId)) {
293
+ console.warn(`[BrowserDaemon] Attempted to deallocate non-existent session: ${sessionId}`);
294
+ return false;
295
+ }
296
+
297
+ try {
298
+ const sessionData = this.activeSessions.get(sessionId);
299
+
300
+ // Close browser context
301
+ if (sessionData.context) {
302
+ await sessionData.context.close();
303
+ }
304
+
305
+ // Cleanup profile
306
+ await this.profileManager.cleanupProfile(sessionId);
307
+
308
+ // Remove from active sessions
309
+ this.activeSessions.delete(sessionId);
310
+
311
+ // Remove from persistent registry
312
+ await this.sessionRegistry.removeSession(sessionId);
313
+
314
+ console.log(`[BrowserDaemon] Deallocated session ${sessionId}`);
315
+ return true;
316
+ } catch (error) {
317
+ console.error(`[BrowserDaemon] Error deallocating session ${sessionId}:`, error);
318
+ return false;
319
+ }
320
+ }
321
+
322
+ /**
323
+ * Updates session activity timestamp
324
+ * @param {string} sessionId - Session ID to update
325
+ */
326
+ async updateSessionActivity(sessionId) {
327
+ if (this.activeSessions.has(sessionId)) {
328
+ const sessionData = this.activeSessions.get(sessionId);
329
+ sessionData.lastActivity = new Date();
330
+
331
+ // Update persistent registry
332
+ await this.sessionRegistry.updateSession(sessionId, {
333
+ lastActivity: sessionData.lastActivity
334
+ });
335
+ }
336
+ }
337
+
338
+ /**
339
+ * Gets connection information for an existing session
340
+ * @param {string} sessionId - Session ID
341
+ * @returns {Object|null} Connection info or null if not found
342
+ */
343
+ getSessionConnection(sessionId) {
344
+ if (!this.activeSessions.has(sessionId)) {
345
+ return null;
346
+ }
347
+
348
+ const sessionData = this.activeSessions.get(sessionId);
349
+ return {
350
+ sessionId,
351
+ contextId: sessionData.contextId,
352
+ profilePath: sessionData.profilePath,
353
+ browserWSEndpoint: this.browser?.wsEndpoint(),
354
+ debugPort: this.config.daemonPort,
355
+ connectionInfo: {
356
+ type: 'shared-daemon',
357
+ daemonId: this.daemonId,
358
+ sessionId
359
+ }
360
+ };
361
+ }
362
+
363
+ /**
364
+ * Starts health check monitoring
365
+ */
366
+ startHealthCheck() {
367
+ if (this.healthCheckTimer) {
368
+ clearInterval(this.healthCheckTimer);
369
+ }
370
+
371
+ this.healthCheckTimer = setInterval(this.performHealthCheck, this.config.healthCheckInterval);
372
+ console.log(`[BrowserDaemon] Started health check with ${this.config.healthCheckInterval}ms interval`);
373
+ }
374
+
375
+ /**
376
+ * Performs periodic health checks and cleanup
377
+ */
378
+ async performHealthCheck() {
379
+ if (this.isShuttingDown) return;
380
+
381
+ try {
382
+ // Check browser health - log status but don't restart
383
+ if (!this.browser || this.browser.process()?.killed) {
384
+ console.warn('[BrowserDaemon] Browser process is down. Claude can launch a new browser when needed.');
385
+ // Don't restart - let Claude handle recovery via MCP tools
386
+ return;
387
+ }
388
+
389
+ // Clean up inactive sessions
390
+ const now = new Date();
391
+ const expiredSessions = [];
392
+
393
+ for (const [sessionId, sessionData] of this.activeSessions) {
394
+ const inactiveTime = now.getTime() - sessionData.lastActivity.getTime();
395
+ if (inactiveTime > this.config.sessionTimeoutMs) {
396
+ expiredSessions.push(sessionId);
397
+ }
398
+ }
399
+
400
+ // Clean up expired sessions
401
+ for (const sessionId of expiredSessions) {
402
+ console.log(`[BrowserDaemon] Cleaning up expired session: ${sessionId}`);
403
+ await this.deallocateSession(sessionId);
404
+ }
405
+
406
+ if (expiredSessions.length > 0) {
407
+ console.log(`[BrowserDaemon] Cleaned up ${expiredSessions.length} expired sessions`);
408
+ }
409
+
410
+ } catch (error) {
411
+ console.error('[BrowserDaemon] Health check failed:', error);
412
+ }
413
+ }
414
+
415
+
416
+ /**
417
+ * Gets daemon status and statistics
418
+ * @returns {Object} Daemon status information
419
+ */
420
+ getStatus() {
421
+ return {
422
+ daemonId: this.daemonId,
423
+ isRunning: this.isRunning,
424
+ startTime: this.startTime,
425
+ activeSessions: this.activeSessions.size,
426
+ maxSessions: this.config.maxConcurrentSessions,
427
+ debugPort: this.config.daemonPort,
428
+ browserConnected: this.browser && !this.browser.process()?.killed,
429
+ uptime: this.startTime ? Date.now() - this.startTime.getTime() : 0
430
+ };
431
+ }
432
+
433
+ /**
434
+ * Performs graceful shutdown of the daemon
435
+ */
436
+ async gracefulShutdown() {
437
+ if (this.isShuttingDown) return;
438
+
439
+ this.isShuttingDown = true;
440
+ console.log('[BrowserDaemon] Initiating graceful shutdown...');
441
+
442
+ try {
443
+ // Stop health checks
444
+ if (this.healthCheckTimer) {
445
+ clearInterval(this.healthCheckTimer);
446
+ this.healthCheckTimer = null;
447
+ }
448
+
449
+ // Clean up all active sessions
450
+ console.log(`[BrowserDaemon] Cleaning up ${this.activeSessions.size} active sessions...`);
451
+ for (const sessionId of this.activeSessions.keys()) {
452
+ await this.deallocateSession(sessionId);
453
+ }
454
+
455
+ // Close browser
456
+ if (this.browser) {
457
+ await this.browser.close();
458
+ this.browser = null;
459
+ }
460
+
461
+ // Cleanup managers
462
+ await this.cleanup();
463
+
464
+ this.isRunning = false;
465
+ console.log('[BrowserDaemon] Graceful shutdown completed');
466
+ } catch (error) {
467
+ console.error('[BrowserDaemon] Error during graceful shutdown:', error);
468
+ }
469
+ }
470
+
471
+ /**
472
+ * Internal cleanup method
473
+ */
474
+ async cleanup() {
475
+ try {
476
+ await this.profileManager.cleanup();
477
+ await this.sessionRegistry.cleanup();
478
+ await this.failoverManager.cleanup();
479
+ } catch (error) {
480
+ console.error('[BrowserDaemon] Error during cleanup:', error);
481
+ }
482
+ }
483
+
484
+ /**
485
+ * Checks if the daemon is healthy and ready to accept sessions
486
+ * @returns {boolean}
487
+ */
488
+ isHealthy() {
489
+ return this.isRunning &&
490
+ this.browser &&
491
+ !this.browser.process()?.killed &&
492
+ !this.isShuttingDown;
493
+ }
494
+ }