@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,497 @@
1
+ /**
2
+ * SessionManager - Core session isolation for Chrome Debug
3
+ *
4
+ * Provides UUID-based session management to prevent conflicts when multiple
5
+ * Claude Code sessions launch Chrome simultaneously.
6
+ *
7
+ * Key Features:
8
+ * - UUID-based session identification
9
+ * - Isolated Chrome profile directories
10
+ * - Process tracking and cleanup
11
+ * - Heartbeat mechanism integration
12
+ * - Safe cleanup of only dead sessions
13
+ */
14
+
15
+ import { randomUUID } from 'crypto';
16
+ import { promises as fs } from 'fs';
17
+ import path from 'path';
18
+ import os from 'os';
19
+ import { findAvailablePort, findAvailablePortWithLock, releasePortLock } from '../utils.js';
20
+ import { ProjectManager } from './project-manager.js';
21
+
22
+ export class SessionManager {
23
+ constructor(database = null) {
24
+ this.database = database;
25
+ this.sessionId = randomUUID();
26
+ this.sessionData = null;
27
+ this.heartbeatInterval = null;
28
+
29
+ // Project management for path resolution
30
+ this.projectManager = new ProjectManager();
31
+ this._projectModeInitialized = false;
32
+
33
+ // Session configuration
34
+ this.HEARTBEAT_INTERVAL = 30000; // 30 seconds
35
+ this.STALE_THRESHOLD = 5 * 60 * 1000; // 5 minutes
36
+
37
+ // Add process cleanup handlers to ensure port locks are released
38
+ this.setupProcessCleanupHandlers();
39
+ }
40
+
41
+ /**
42
+ * Setup process cleanup handlers to ensure port locks are released on exit
43
+ */
44
+ setupProcessCleanupHandlers() {
45
+ // Handle graceful shutdown
46
+ const cleanupHandler = async () => {
47
+ if (this.sessionData && this.sessionData.portLockFile) {
48
+ try {
49
+ await releasePortLock(this.sessionData.portLockFile);
50
+ console.log(`[SessionManager] Released port lock on process exit for session ${this.sessionData.sessionId}`);
51
+ } catch (error) {
52
+ console.warn('[SessionManager] Error releasing port lock on exit:', error);
53
+ }
54
+ }
55
+ };
56
+
57
+ process.on('exit', cleanupHandler);
58
+ process.on('SIGINT', cleanupHandler);
59
+ process.on('SIGTERM', cleanupHandler);
60
+ process.on('uncaughtException', cleanupHandler);
61
+ }
62
+
63
+ /**
64
+ * Initializes project mode if available (called automatically when needed)
65
+ */
66
+ async initializeProjectMode() {
67
+ if (this._projectModeInitialized) {
68
+ return; // Already initialized
69
+ }
70
+
71
+ try {
72
+ const projectInfo = this.projectManager.detectProject();
73
+ if (projectInfo) {
74
+ await this.projectManager.initializeProjectStructure();
75
+ console.log(`[SessionManager] Using project-local storage: ${this.projectManager.getCurrentMode()}`);
76
+ } else {
77
+ console.log('[SessionManager] Using global storage mode');
78
+ }
79
+ } catch (error) {
80
+ console.warn('[SessionManager] Failed to initialize project mode, falling back to global:', error.message);
81
+ } finally {
82
+ this._projectModeInitialized = true;
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Creates a new isolated session with UUID-based profile path
88
+ * @param {number} preferredPort - Preferred debug port (optional)
89
+ * @returns {Promise<SessionData>} Created session data
90
+ */
91
+ async createSession(preferredPort = null) {
92
+ // Ensure project mode is initialized
93
+ await this.initializeProjectMode();
94
+
95
+ try {
96
+ // Generate session metadata
97
+ const sessionId = this.sessionId;
98
+ const startTime = new Date();
99
+ const lastHeartbeat = new Date();
100
+
101
+ // Find available debug port with atomic lock coordination to prevent race conditions
102
+ const pidOffset = process.pid % 100;
103
+ const sessionOffset = parseInt(sessionId.split('-')[0], 16) % 50; // Use UUID for randomization
104
+ const basePort = preferredPort || (9222 + pidOffset + sessionOffset);
105
+ const portLockResult = await findAvailablePortWithLock(basePort, basePort + 200, sessionId);
106
+ const debugPort = portLockResult.port;
107
+
108
+ // Create UUID-based profile path using project-local or global storage
109
+ const sessionPrefix = sessionId.split('-')[0]; // First part of UUID for brevity
110
+ const timestamp = Date.now();
111
+
112
+ let profilePath;
113
+ if (this.projectManager.isProjectModeAvailable()) {
114
+ // Use project-local sessions directory
115
+ const sessionsDir = this.projectManager.getProjectSessionsDir();
116
+ profilePath = path.join(sessionsDir, `${sessionPrefix}-${timestamp}`, 'profile');
117
+ } else {
118
+ // Use global temp directory (existing behavior)
119
+ profilePath = path.join(
120
+ os.tmpdir(),
121
+ `chromedebug-${sessionPrefix}-${timestamp}`
122
+ );
123
+ }
124
+
125
+ // Create the profile directory
126
+ await fs.mkdir(profilePath, { recursive: true });
127
+
128
+ // Store session data
129
+ this.sessionData = {
130
+ sessionId,
131
+ processId: null, // Will be set when Chrome launches
132
+ startTime,
133
+ lastHeartbeat,
134
+ profilePath,
135
+ debugPort,
136
+ portLockFile: portLockResult.lockFile // Store lock file for cleanup
137
+ };
138
+
139
+ console.log(`[SessionManager] Created session ${sessionId} with profile: ${profilePath}`);
140
+
141
+ // Start heartbeat automatically
142
+ this.startHeartbeat();
143
+
144
+ return { ...this.sessionData };
145
+ } catch (error) {
146
+ console.error('[SessionManager] Failed to create session:', error);
147
+ throw new Error(`Session creation failed: ${error.message}`);
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Updates the heartbeat timestamp for this session
153
+ * @returns {Promise<void>}
154
+ */
155
+ async updateHeartbeat() {
156
+ if (this.sessionData) {
157
+ this.sessionData.lastHeartbeat = new Date();
158
+ console.log(`[SessionManager] Updated heartbeat for session ${this.sessionData.sessionId}`);
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Starts automatic heartbeat updates for this session
164
+ */
165
+ startHeartbeat() {
166
+ if (this.heartbeatInterval) {
167
+ clearInterval(this.heartbeatInterval);
168
+ }
169
+
170
+ this.heartbeatInterval = setInterval(async () => {
171
+ await this.updateHeartbeat();
172
+ }, this.HEARTBEAT_INTERVAL);
173
+
174
+ console.log(`[SessionManager] Started heartbeat for session ${this.sessionId}`);
175
+ }
176
+
177
+ /**
178
+ * Stops heartbeat for this session
179
+ */
180
+ stopHeartbeat() {
181
+ if (this.heartbeatInterval) {
182
+ clearInterval(this.heartbeatInterval);
183
+ this.heartbeatInterval = null;
184
+ console.log(`[SessionManager] Stopped heartbeat for session ${this.sessionId}`);
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Checks if a process is still alive
190
+ * @param {number} pid - Process ID to check
191
+ * @returns {boolean} True if process is alive
192
+ */
193
+ isProcessAlive(pid) {
194
+ if (!pid) return false;
195
+
196
+ try {
197
+ process.kill(pid, 0); // Signal 0 just checks if process exists
198
+ return true;
199
+ } catch (error) {
200
+ if (error.code === 'ESRCH') {
201
+ return false; // Process does not exist
202
+ }
203
+ // Other errors (like EPERM) mean process exists but we can't signal it
204
+ return true;
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Checks if this session is alive (process + heartbeat)
210
+ * @param {string} sessionId - Session ID to check
211
+ * @returns {Promise<boolean>} True if session is alive
212
+ */
213
+ async isSessionAlive(sessionId = null) {
214
+ const targetSessionId = sessionId || this.sessionId;
215
+
216
+ if (!this.sessionData || this.sessionData.sessionId !== targetSessionId) {
217
+ return false;
218
+ }
219
+
220
+ // Check process health
221
+ if (this.sessionData.processId && !this.isProcessAlive(this.sessionData.processId)) {
222
+ return false;
223
+ }
224
+
225
+ // Check heartbeat freshness
226
+ const now = new Date();
227
+ const timeSinceHeartbeat = now.getTime() - this.sessionData.lastHeartbeat.getTime();
228
+
229
+ if (timeSinceHeartbeat > this.STALE_THRESHOLD) {
230
+ return false;
231
+ }
232
+
233
+ return true;
234
+ }
235
+
236
+ /**
237
+ * Sets the Chrome process ID for this session
238
+ * @param {number} processId - Chrome process ID
239
+ */
240
+ setProcessId(processId) {
241
+ if (this.sessionData) {
242
+ this.sessionData.processId = processId;
243
+ console.log(`[SessionManager] Set process ID ${processId} for session ${this.sessionId}`);
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Gets current session data
249
+ * @param {string} sessionId - Session ID to get data for
250
+ * @returns {Promise<SessionData|null>} Session data or null if not found
251
+ */
252
+ async getSessionData(sessionId = null) {
253
+ const targetSessionId = sessionId || this.sessionId;
254
+
255
+ if (this.sessionData && this.sessionData.sessionId === targetSessionId) {
256
+ return { ...this.sessionData };
257
+ }
258
+
259
+ return null;
260
+ }
261
+
262
+ /**
263
+ * Safely cleans up a directory if it's a Chrome Debug directory
264
+ * @param {string} dirPath - Directory path to clean up
265
+ * @param {boolean} isProjectLocal - Whether this is a project-local directory
266
+ * @returns {Promise<boolean>} True if cleaned successfully
267
+ */
268
+ async safeCleanupDirectory(dirPath, isProjectLocal = false) {
269
+ if (!dirPath || typeof dirPath !== 'string') {
270
+ return false;
271
+ }
272
+
273
+ try {
274
+ const resolvedPath = path.resolve(dirPath);
275
+
276
+ // Security validation based on directory type
277
+ if (isProjectLocal) {
278
+ // Validate project-local directory is within project sessions directory
279
+ if (this.projectManager.isProjectModeAvailable()) {
280
+ const projectSessionsDir = path.resolve(this.projectManager.getProjectSessionsDir());
281
+ if (!resolvedPath.startsWith(projectSessionsDir)) {
282
+ console.warn(`[SessionManager] Refusing to clean unsafe project directory: ${dirPath}`);
283
+ return false;
284
+ }
285
+ } else {
286
+ console.warn(`[SessionManager] Cannot clean project directory when not in project mode: ${dirPath}`);
287
+ return false;
288
+ }
289
+ } else {
290
+ // Validate global directory is within temp and has chromedebug prefix
291
+ const tmpDir = os.tmpdir();
292
+ const resolvedTmpDir = path.resolve(tmpDir);
293
+
294
+ if (!resolvedPath.startsWith(resolvedTmpDir) ||
295
+ !path.basename(resolvedPath).startsWith('chromedebug-')) {
296
+ console.warn(`[SessionManager] Refusing to clean unsafe global directory: ${dirPath}`);
297
+ return false;
298
+ }
299
+ }
300
+
301
+ await fs.rm(resolvedPath, { recursive: true, force: true });
302
+ console.log(`[SessionManager] Cleaned up ${isProjectLocal ? 'project-local' : 'global'} directory: ${resolvedPath}`);
303
+ return true;
304
+ } catch (error) {
305
+ if (error.code === 'ENOENT') {
306
+ return true; // Directory doesn't exist, consider it cleaned
307
+ }
308
+ console.error(`[SessionManager] Failed to cleanup directory ${dirPath}:`, error);
309
+ return false;
310
+ }
311
+ }
312
+
313
+ /**
314
+ * Cleans up stale Chrome Debug sessions
315
+ * Only cleans sessions with dead processes AND stale heartbeats
316
+ * @returns {Promise<CleanupResult>} Cleanup results
317
+ */
318
+ async cleanupStaleSessions() {
319
+ const cleanupResult = {
320
+ cleanedSessions: [],
321
+ failedCleanups: [],
322
+ activeSessionsRemaining: 0
323
+ };
324
+
325
+ try {
326
+ const dirsToCheck = [];
327
+
328
+ // Check project-local sessions if available
329
+ if (this.projectManager.isProjectModeAvailable()) {
330
+ try {
331
+ const projectSessionsDir = this.projectManager.getProjectSessionsDir();
332
+ const projectEntries = await fs.readdir(projectSessionsDir);
333
+ dirsToCheck.push(...projectEntries.map(entry => ({
334
+ name: entry,
335
+ fullPath: path.join(projectSessionsDir, entry),
336
+ isProjectLocal: true
337
+ })));
338
+ } catch (error) {
339
+ console.warn('[SessionManager] Could not read project sessions directory:', error.message);
340
+ }
341
+ }
342
+
343
+ // Also check global temp directory for compatibility
344
+ const tmpDir = os.tmpdir();
345
+ const tmpEntries = await fs.readdir(tmpDir);
346
+ const chromePilotDirs = tmpEntries.filter(entry => entry.startsWith('chromedebug-'));
347
+ dirsToCheck.push(...chromePilotDirs.map(entry => ({
348
+ name: entry,
349
+ fullPath: path.join(tmpDir, entry),
350
+ isProjectLocal: false
351
+ })));
352
+
353
+ console.log(`[SessionManager] Found ${dirsToCheck.length} directories to evaluate`);
354
+
355
+ for (const dirInfo of dirsToCheck) {
356
+ try {
357
+ const dirPath = dirInfo.fullPath;
358
+ const dirName = dirInfo.name;
359
+
360
+ // Skip our own active session
361
+ if (this.sessionData &&
362
+ (dirPath === this.sessionData.profilePath ||
363
+ dirPath === path.dirname(this.sessionData.profilePath))) {
364
+ const isAlive = await this.isSessionAlive();
365
+ if (isAlive) {
366
+ cleanupResult.activeSessionsRemaining++;
367
+ continue;
368
+ }
369
+ }
370
+
371
+ // Try to extract session info from directory name
372
+ const parts = dirName.split('-');
373
+ let timestamp = null;
374
+
375
+ if (dirInfo.isProjectLocal) {
376
+ // Project-local format: {sessionPrefix}-{timestamp}
377
+ if (parts.length >= 2) {
378
+ timestamp = parseInt(parts[parts.length - 1], 10);
379
+ }
380
+ } else {
381
+ // Global format: chromedebug-{sessionPrefix}-{timestamp}
382
+ if (parts.length >= 3) {
383
+ timestamp = parseInt(parts[parts.length - 1], 10);
384
+ }
385
+ }
386
+
387
+ if (!isNaN(timestamp)) {
388
+ const age = Date.now() - timestamp;
389
+ if (age > this.STALE_THRESHOLD) {
390
+ const cleaned = await this.safeCleanupDirectory(dirPath, dirInfo.isProjectLocal);
391
+ if (cleaned) {
392
+ cleanupResult.cleanedSessions.push(dirName);
393
+ } else {
394
+ cleanupResult.failedCleanups.push({ sessionId: dirName, reason: 'Cleanup failed' });
395
+ }
396
+ } else {
397
+ cleanupResult.activeSessionsRemaining++;
398
+ }
399
+ }
400
+ } catch (error) {
401
+ console.error(`[SessionManager] Error evaluating directory ${dirInfo.name}:`, error);
402
+ cleanupResult.failedCleanups.push({ sessionId: dirInfo.name, reason: error.message });
403
+ }
404
+ }
405
+
406
+ console.log(`[SessionManager] Cleanup complete: ${cleanupResult.cleanedSessions.length} cleaned, ${cleanupResult.activeSessionsRemaining} active remaining`);
407
+ } catch (error) {
408
+ console.error('[SessionManager] Error during stale session cleanup:', error);
409
+ throw error;
410
+ }
411
+
412
+ return cleanupResult;
413
+ }
414
+
415
+ /**
416
+ * Forces cleanup of a specific session (for error recovery)
417
+ * @param {string} sessionId - Session ID to clean up
418
+ * @returns {Promise<boolean>} True if cleaned successfully
419
+ */
420
+ async forceCleanupSession(sessionId) {
421
+ if (!sessionId || typeof sessionId !== 'string') {
422
+ throw new Error('Invalid session ID provided for cleanup');
423
+ }
424
+
425
+ try {
426
+ // If it's our own session, clean it up directly
427
+ if (this.sessionData && this.sessionData.sessionId === sessionId) {
428
+ this.stopHeartbeat();
429
+
430
+ if (this.sessionData.processId) {
431
+ try {
432
+ process.kill(this.sessionData.processId, 'SIGTERM');
433
+ await new Promise(resolve => setTimeout(resolve, 1000));
434
+
435
+ // Force kill if still alive
436
+ if (this.isProcessAlive(this.sessionData.processId)) {
437
+ process.kill(this.sessionData.processId, 'SIGKILL');
438
+ }
439
+ } catch (error) {
440
+ console.warn(`[SessionManager] Error killing process ${this.sessionData.processId}:`, error);
441
+ }
442
+ }
443
+
444
+ const cleaned = await this.safeCleanupDirectory(this.sessionData.profilePath);
445
+
446
+ // Release port lock
447
+ if (this.sessionData.portLockFile) {
448
+ try {
449
+ await releasePortLock(this.sessionData.portLockFile);
450
+ console.log(`[SessionManager] Released port lock for session ${sessionId}`);
451
+ } catch (error) {
452
+ console.warn(`[SessionManager] Error releasing port lock for session ${sessionId}:`, error);
453
+ }
454
+ }
455
+
456
+ // Reset session data
457
+ this.sessionData = null;
458
+
459
+ return cleaned;
460
+ } else {
461
+ throw new Error(`Cannot force cleanup session ${sessionId}: session not found or not owned by this manager`);
462
+ }
463
+ } catch (error) {
464
+ console.error(`[SessionManager] Error in force cleanup of session ${sessionId}:`, error);
465
+ throw error;
466
+ }
467
+ }
468
+
469
+ /**
470
+ * Gets current storage mode information
471
+ * @returns {object} Storage mode info
472
+ */
473
+ getStorageInfo() {
474
+ return {
475
+ mode: this.projectManager.getCurrentMode(),
476
+ projectInfo: this.projectManager.projectInfo,
477
+ isProjectModeAvailable: this.projectManager.isProjectModeAvailable(),
478
+ projectStructure: this.projectManager.isProjectModeAvailable()
479
+ ? this.projectManager.getProjectStructure()
480
+ : null,
481
+ globalStructure: this.projectManager.getGlobalStructure()
482
+ };
483
+ }
484
+
485
+ /**
486
+ * Cleanup method called when SessionManager is destroyed
487
+ */
488
+ async cleanup() {
489
+ console.log(`[SessionManager] Cleaning up session manager for ${this.sessionId}`);
490
+
491
+ this.stopHeartbeat();
492
+
493
+ if (this.sessionData) {
494
+ await this.forceCleanupSession(this.sessionData.sessionId);
495
+ }
496
+ }
497
+ }