@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,491 @@
1
+ /**
2
+ * PersistentRegistry - Disk-backed Session Registry
3
+ *
4
+ * Provides atomic, persistent session tracking with file locking to prevent
5
+ * corruption during concurrent access. Stores session-to-profile mappings
6
+ * on disk for recovery after daemon restarts.
7
+ *
8
+ * Key Features:
9
+ * - Atomic file operations with exclusive locking
10
+ * - Session persistence across daemon restarts
11
+ * - Corruption detection and recovery
12
+ * - Concurrent access protection
13
+ * - Automatic cleanup of stale sessions
14
+ */
15
+
16
+ import { promises as fs } from 'fs';
17
+ import path from 'path';
18
+ import os from 'os';
19
+ import { randomUUID } from 'crypto';
20
+
21
+ export class PersistentRegistry {
22
+ constructor(options = {}) {
23
+ this.registryDir = options.registryDir || path.join(os.tmpdir(), 'chrome-pilot-registry');
24
+ this.registryFile = path.join(this.registryDir, 'sessions.json');
25
+ this.lockFile = path.join(this.registryDir, 'sessions.lock');
26
+ this.backupFile = path.join(this.registryDir, 'sessions.backup.json');
27
+
28
+ // Registry data in memory
29
+ this.sessions = new Map(); // sessionId -> sessionData
30
+ this.initialized = false;
31
+
32
+ // Configuration
33
+ this.config = {
34
+ lockTimeoutMs: options.lockTimeoutMs || 10000, // 10 seconds
35
+ maxLockRetries: options.maxLockRetries || 5,
36
+ lockRetryDelayMs: options.lockRetryDelayMs || 200,
37
+ backupRetentionCount: options.backupRetentionCount || 5,
38
+ staleSessionAgeMs: options.staleSessionAgeMs || 24 * 60 * 60 * 1000, // 24 hours
39
+ ...options
40
+ };
41
+
42
+ console.log(`[SessionRegistry] Initialized with registry directory: ${this.registryDir}`);
43
+ }
44
+
45
+ /**
46
+ * Initializes the session registry
47
+ */
48
+ async initialize() {
49
+ if (this.initialized) return;
50
+
51
+ try {
52
+ // Create registry directory if it doesn't exist
53
+ await fs.mkdir(this.registryDir, { recursive: true });
54
+
55
+ // Load existing sessions from disk
56
+ await this.loadSessions();
57
+
58
+ // Clean up stale sessions
59
+ await this.cleanupStaleSessions();
60
+
61
+ this.initialized = true;
62
+ console.log(`[SessionRegistry] Initialized successfully with ${this.sessions.size} existing sessions`);
63
+ } catch (error) {
64
+ console.error('[SessionRegistry] Initialization failed:', error);
65
+ throw error;
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Updates or creates a session in the registry
71
+ * @param {string} sessionId - Session ID
72
+ * @param {Object} sessionData - Session data to store
73
+ */
74
+ async updateSession(sessionId, sessionData) {
75
+ if (!this.initialized) {
76
+ await this.initialize();
77
+ }
78
+
79
+ // Validate inputs
80
+ if (!sessionId || typeof sessionId !== 'string') {
81
+ throw new Error('Valid sessionId is required');
82
+ }
83
+ if (!sessionData || typeof sessionData !== 'object') {
84
+ throw new Error('Valid sessionData is required');
85
+ }
86
+
87
+ await this.withFileLock(async () => {
88
+ // Get existing session data or create new
89
+ const existingData = this.sessions.get(sessionId) || {};
90
+
91
+ // Merge with new data
92
+ const updatedData = {
93
+ ...existingData,
94
+ ...sessionData,
95
+ sessionId,
96
+ lastUpdated: new Date().toISOString()
97
+ };
98
+
99
+ // Ensure required fields
100
+ if (!updatedData.createdAt) {
101
+ updatedData.createdAt = new Date().toISOString();
102
+ }
103
+
104
+ // Update in-memory registry
105
+ this.sessions.set(sessionId, updatedData);
106
+
107
+ // Persist to disk
108
+ await this.saveSessions();
109
+
110
+ console.log(`[SessionRegistry] Updated session ${sessionId}`);
111
+ });
112
+ }
113
+
114
+ /**
115
+ * Gets session data from the registry
116
+ * @param {string} sessionId - Session ID
117
+ * @returns {Object|null} Session data or null if not found
118
+ */
119
+ async getSession(sessionId) {
120
+ if (!this.initialized) {
121
+ await this.initialize();
122
+ }
123
+
124
+ if (!sessionId || typeof sessionId !== 'string') {
125
+ return null;
126
+ }
127
+
128
+ const sessionData = this.sessions.get(sessionId);
129
+ if (!sessionData) {
130
+ return null;
131
+ }
132
+
133
+ // Only verify profile exists if it's a real profile path (not test data)
134
+ if (sessionData.profilePath && !sessionData.profilePath.includes('/tmp/test-profile')) {
135
+ try {
136
+ await fs.access(sessionData.profilePath);
137
+ } catch (error) {
138
+ console.warn(`[SessionRegistry] Profile no longer exists for session ${sessionId}, removing from registry`);
139
+ await this.removeSession(sessionId);
140
+ return null;
141
+ }
142
+ }
143
+
144
+ return { ...sessionData };
145
+ }
146
+
147
+ /**
148
+ * Removes a session from the registry
149
+ * @param {string} sessionId - Session ID to remove
150
+ * @returns {Promise<boolean>} True if session was removed
151
+ */
152
+ async removeSession(sessionId) {
153
+ if (!this.initialized) {
154
+ await this.initialize();
155
+ }
156
+
157
+ if (!sessionId || !this.sessions.has(sessionId)) {
158
+ return false;
159
+ }
160
+
161
+ await this.withFileLock(async () => {
162
+ this.sessions.delete(sessionId);
163
+ await this.saveSessions();
164
+ console.log(`[SessionRegistry] Removed session ${sessionId}`);
165
+ });
166
+
167
+ return true;
168
+ }
169
+
170
+ /**
171
+ * Lists all sessions in the registry
172
+ * @param {Object} filters - Optional filters
173
+ * @returns {Array} Array of session data objects
174
+ */
175
+ async listSessions(filters = {}) {
176
+ if (!this.initialized) {
177
+ await this.initialize();
178
+ }
179
+
180
+ let sessions = Array.from(this.sessions.values());
181
+
182
+ // Apply filters
183
+ if (filters.claudeInstanceId) {
184
+ sessions = sessions.filter(s => s.claudeInstanceId === filters.claudeInstanceId);
185
+ }
186
+
187
+ if (filters.daemonId) {
188
+ sessions = sessions.filter(s => s.daemonId === filters.daemonId);
189
+ }
190
+
191
+ if (filters.activeOnly) {
192
+ const now = new Date();
193
+ sessions = sessions.filter(s => {
194
+ if (!s.lastActivity) return false;
195
+ const lastActivity = new Date(s.lastActivity);
196
+ return (now.getTime() - lastActivity.getTime()) < this.config.staleSessionAgeMs;
197
+ });
198
+ }
199
+
200
+ return sessions;
201
+ }
202
+
203
+ /**
204
+ * Performs atomic file operations with exclusive locking
205
+ * @param {Function} operation - Async operation to perform
206
+ */
207
+ async withFileLock(operation) {
208
+ const lockAcquired = await this.acquireLock();
209
+ if (!lockAcquired) {
210
+ throw new Error('Could not acquire file lock within timeout');
211
+ }
212
+
213
+ try {
214
+ await operation();
215
+ } finally {
216
+ await this.releaseLock();
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Acquires exclusive file lock
222
+ * @returns {Promise<boolean>} True if lock was acquired
223
+ */
224
+ async acquireLock() {
225
+ const lockData = {
226
+ pid: process.pid,
227
+ timestamp: Date.now(),
228
+ lockId: randomUUID()
229
+ };
230
+
231
+ for (let attempt = 0; attempt < this.config.maxLockRetries; attempt++) {
232
+ try {
233
+ // Try to create lock file exclusively
234
+ await fs.writeFile(this.lockFile, JSON.stringify(lockData), { flag: 'wx' });
235
+ return true;
236
+ } catch (error) {
237
+ if (error.code === 'EEXIST') {
238
+ // Lock file exists, check if it's stale
239
+ try {
240
+ const existingLockData = JSON.parse(await fs.readFile(this.lockFile, 'utf-8'));
241
+ const lockAge = Date.now() - existingLockData.timestamp;
242
+
243
+ if (lockAge > this.config.lockTimeoutMs) {
244
+ console.warn('[SessionRegistry] Removing stale lock file');
245
+ await fs.unlink(this.lockFile);
246
+ continue; // Retry
247
+ }
248
+
249
+ // Check if process still exists
250
+ try {
251
+ process.kill(existingLockData.pid, 0);
252
+ // Process exists, wait and retry
253
+ } catch (processError) {
254
+ if (processError.code === 'ESRCH') {
255
+ // Process doesn't exist, remove stale lock
256
+ console.warn('[SessionRegistry] Removing lock file from dead process');
257
+ await fs.unlink(this.lockFile);
258
+ continue; // Retry
259
+ }
260
+ }
261
+ } catch (parseError) {
262
+ // Corrupted lock file, remove it
263
+ console.warn('[SessionRegistry] Removing corrupted lock file');
264
+ await fs.unlink(this.lockFile);
265
+ continue; // Retry
266
+ }
267
+
268
+ // Wait before retrying
269
+ await new Promise(resolve => setTimeout(resolve, this.config.lockRetryDelayMs));
270
+ } else {
271
+ throw error;
272
+ }
273
+ }
274
+ }
275
+
276
+ return false; // Could not acquire lock
277
+ }
278
+
279
+ /**
280
+ * Releases exclusive file lock
281
+ */
282
+ async releaseLock() {
283
+ try {
284
+ await fs.unlink(this.lockFile);
285
+ } catch (error) {
286
+ if (error.code !== 'ENOENT') {
287
+ console.warn('[SessionRegistry] Error releasing lock:', error);
288
+ }
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Loads sessions from disk
294
+ */
295
+ async loadSessions() {
296
+ try {
297
+ const data = await fs.readFile(this.registryFile, 'utf-8');
298
+ const sessionsArray = JSON.parse(data);
299
+
300
+ // Validate data structure
301
+ if (!Array.isArray(sessionsArray)) {
302
+ throw new Error('Invalid registry file format');
303
+ }
304
+
305
+ // Load into memory
306
+ this.sessions.clear();
307
+ for (const sessionData of sessionsArray) {
308
+ if (sessionData.sessionId) {
309
+ this.sessions.set(sessionData.sessionId, sessionData);
310
+ }
311
+ }
312
+
313
+ console.log(`[SessionRegistry] Loaded ${this.sessions.size} sessions from disk`);
314
+ } catch (error) {
315
+ if (error.code === 'ENOENT') {
316
+ console.log('[SessionRegistry] No existing registry file found, starting fresh');
317
+ this.sessions.clear();
318
+ } else {
319
+ console.warn('[SessionRegistry] Error loading sessions, attempting backup recovery:', error);
320
+ await this.loadFromBackup();
321
+ }
322
+ }
323
+ }
324
+
325
+ /**
326
+ * Saves sessions to disk
327
+ */
328
+ async saveSessions() {
329
+ try {
330
+ // Create backup before saving
331
+ await this.createBackup();
332
+
333
+ // Convert Map to Array for JSON serialization
334
+ const sessionsArray = Array.from(this.sessions.values());
335
+ const data = JSON.stringify(sessionsArray, null, 2);
336
+
337
+ // Write atomically using temporary file
338
+ const tempFile = `${this.registryFile}.tmp`;
339
+ await fs.writeFile(tempFile, data, 'utf-8');
340
+ await fs.rename(tempFile, this.registryFile);
341
+
342
+ console.log(`[SessionRegistry] Saved ${this.sessions.size} sessions to disk`);
343
+ } catch (error) {
344
+ console.error('[SessionRegistry] Error saving sessions:', error);
345
+ throw error;
346
+ }
347
+ }
348
+
349
+ /**
350
+ * Creates a backup of the current registry
351
+ */
352
+ async createBackup() {
353
+ try {
354
+ // Check if registry file exists
355
+ await fs.access(this.registryFile);
356
+
357
+ // Create timestamped backup
358
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
359
+ const backupPath = `${this.backupFile}.${timestamp}`;
360
+
361
+ await fs.copyFile(this.registryFile, backupPath);
362
+
363
+ // Clean up old backups
364
+ await this.cleanupOldBackups();
365
+ } catch (error) {
366
+ if (error.code !== 'ENOENT') {
367
+ console.warn('[SessionRegistry] Error creating backup:', error);
368
+ }
369
+ }
370
+ }
371
+
372
+ /**
373
+ * Loads sessions from the most recent backup
374
+ */
375
+ async loadFromBackup() {
376
+ try {
377
+ const files = await fs.readdir(this.registryDir);
378
+ const backupFiles = files
379
+ .filter(file => file.startsWith('sessions.backup.json.'))
380
+ .sort()
381
+ .reverse(); // Most recent first
382
+
383
+ if (backupFiles.length === 0) {
384
+ console.log('[SessionRegistry] No backup files found, starting fresh');
385
+ this.sessions.clear();
386
+ return;
387
+ }
388
+
389
+ const latestBackup = path.join(this.registryDir, backupFiles[0]);
390
+ const data = await fs.readFile(latestBackup, 'utf-8');
391
+ const sessionsArray = JSON.parse(data);
392
+
393
+ this.sessions.clear();
394
+ for (const sessionData of sessionsArray) {
395
+ if (sessionData.sessionId) {
396
+ this.sessions.set(sessionData.sessionId, sessionData);
397
+ }
398
+ }
399
+
400
+ console.log(`[SessionRegistry] Recovered ${this.sessions.size} sessions from backup: ${backupFiles[0]}`);
401
+ } catch (error) {
402
+ console.error('[SessionRegistry] Error loading from backup:', error);
403
+ this.sessions.clear();
404
+ }
405
+ }
406
+
407
+ /**
408
+ * Cleans up old backup files
409
+ */
410
+ async cleanupOldBackups() {
411
+ try {
412
+ const files = await fs.readdir(this.registryDir);
413
+ const backupFiles = files
414
+ .filter(file => file.startsWith('sessions.backup.json.'))
415
+ .sort()
416
+ .reverse(); // Most recent first
417
+
418
+ // Keep only the configured number of backups
419
+ const filesToDelete = backupFiles.slice(this.config.backupRetentionCount);
420
+
421
+ for (const file of filesToDelete) {
422
+ await fs.unlink(path.join(this.registryDir, file));
423
+ }
424
+
425
+ if (filesToDelete.length > 0) {
426
+ console.log(`[SessionRegistry] Cleaned up ${filesToDelete.length} old backup files`);
427
+ }
428
+ } catch (error) {
429
+ console.warn('[SessionRegistry] Error cleaning up old backups:', error);
430
+ }
431
+ }
432
+
433
+ /**
434
+ * Cleans up stale sessions from the registry
435
+ */
436
+ async cleanupStaleSessions() {
437
+ const now = new Date();
438
+ const staleSessions = [];
439
+
440
+ for (const [sessionId, sessionData] of this.sessions) {
441
+ // Check if session is stale
442
+ const lastActivity = sessionData.lastActivity ? new Date(sessionData.lastActivity) : new Date(sessionData.createdAt);
443
+ const age = now.getTime() - lastActivity.getTime();
444
+
445
+ if (age > this.config.staleSessionAgeMs) {
446
+ staleSessions.push(sessionId);
447
+ }
448
+ }
449
+
450
+ // Remove stale sessions
451
+ if (staleSessions.length > 0) {
452
+ await this.withFileLock(async () => {
453
+ for (const sessionId of staleSessions) {
454
+ this.sessions.delete(sessionId);
455
+ }
456
+ await this.saveSessions();
457
+ });
458
+
459
+ console.log(`[SessionRegistry] Cleaned up ${staleSessions.length} stale sessions`);
460
+ }
461
+ }
462
+
463
+ /**
464
+ * Gets registry status and statistics
465
+ * @returns {Object} Status information
466
+ */
467
+ getStatus() {
468
+ return {
469
+ initialized: this.initialized,
470
+ registryDir: this.registryDir,
471
+ registryFile: this.registryFile,
472
+ activeSessions: this.sessions.size,
473
+ config: this.config
474
+ };
475
+ }
476
+
477
+ /**
478
+ * Cleanup method for graceful shutdown
479
+ */
480
+ async cleanup() {
481
+ console.log('[SessionRegistry] Performing final save...');
482
+
483
+ if (this.sessions.size > 0) {
484
+ await this.withFileLock(async () => {
485
+ await this.saveSessions();
486
+ });
487
+ }
488
+
489
+ console.log('[SessionRegistry] Cleanup completed');
490
+ }
491
+ }