@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,574 @@
1
+ /**
2
+ * Process Tracker - Safe process management for ChromeDebug MCP
3
+ * Only tracks and manages processes explicitly registered by this application
4
+ */
5
+
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import os from 'os';
9
+ import crypto from 'crypto';
10
+
11
+ // Global session ID for this process
12
+ let currentSessionId = null;
13
+
14
+ /**
15
+ * Get or generate a session ID for this process
16
+ */
17
+ function getSessionId() {
18
+ if (!currentSessionId) {
19
+ currentSessionId = crypto.randomBytes(8).toString('hex');
20
+ }
21
+ return currentSessionId;
22
+ }
23
+
24
+ /**
25
+ * Set a custom session ID (used for coordinated multi-session environments)
26
+ */
27
+ export function setSessionId(sessionId) {
28
+ if (typeof sessionId === 'string' && sessionId.match(/^[a-zA-Z0-9_-]+$/)) {
29
+ currentSessionId = sessionId;
30
+ console.log(`ChromeDebug session ID set to: ${sessionId}`);
31
+ } else {
32
+ throw new Error('Session ID must be alphanumeric with dashes/underscores only');
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Get the session-specific PID file path
38
+ */
39
+ function getPidFilePath() {
40
+ const sessionId = getSessionId();
41
+ return path.join(os.tmpdir(), `.chromedebug-pids-${sessionId}.json`);
42
+ }
43
+
44
+ /**
45
+ * Safely validates a process ID to prevent injection attacks
46
+ */
47
+ function validateProcessId(pid) {
48
+ const pidStr = String(pid).trim();
49
+ if (!/^\d+$/.test(pidStr)) {
50
+ return null;
51
+ }
52
+
53
+ const numericPid = parseInt(pidStr, 10);
54
+ if (isNaN(numericPid) || numericPid <= 0 || numericPid > 4194304) {
55
+ return null;
56
+ }
57
+ return numericPid;
58
+ }
59
+
60
+ /**
61
+ * Read the PID tracking file for this session
62
+ */
63
+ function readPidFile() {
64
+ const pidFilePath = getPidFilePath();
65
+ try {
66
+ if (fs.existsSync(pidFilePath)) {
67
+ const data = fs.readFileSync(pidFilePath, 'utf8');
68
+ const parsed = JSON.parse(data);
69
+ return {
70
+ processes: parsed.processes || [],
71
+ sessionId: parsed.sessionId || getSessionId(),
72
+ lastUpdated: parsed.lastUpdated || new Date().toISOString()
73
+ };
74
+ }
75
+ } catch (error) {
76
+ console.error(`Failed to read PID file ${pidFilePath}:`, error);
77
+ }
78
+ return {
79
+ processes: [],
80
+ sessionId: getSessionId(),
81
+ lastUpdated: new Date().toISOString()
82
+ };
83
+ }
84
+
85
+ /**
86
+ * Write the PID tracking file for this session
87
+ */
88
+ function writePidFile(data) {
89
+ const pidFilePath = getPidFilePath();
90
+ try {
91
+ const content = {
92
+ ...data,
93
+ sessionId: getSessionId(),
94
+ lastUpdated: new Date().toISOString()
95
+ };
96
+ fs.writeFileSync(pidFilePath, JSON.stringify(content, null, 2));
97
+ } catch (error) {
98
+ console.error(`Failed to write PID file ${pidFilePath}:`, error);
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Register a process for tracking
104
+ */
105
+ export function registerProcess(pid, type = 'unknown') {
106
+ const validatedPid = validateProcessId(pid);
107
+ if (!validatedPid) {
108
+ console.error(`Cannot register invalid process ID: ${pid}`);
109
+ return false;
110
+ }
111
+
112
+ const data = readPidFile();
113
+
114
+ // Check if already registered
115
+ const existing = data.processes.find(p => p.pid === validatedPid);
116
+ if (existing) {
117
+ return true;
118
+ }
119
+
120
+ // Add new process
121
+ data.processes.push({
122
+ pid: validatedPid,
123
+ type,
124
+ startTime: new Date().toISOString(),
125
+ command: process.argv.join(' ')
126
+ });
127
+
128
+ writePidFile(data);
129
+ console.log(`Registered ChromeDebug process: PID ${validatedPid} (${type})`);
130
+ return true;
131
+ }
132
+
133
+ /**
134
+ * Unregister a process from tracking
135
+ */
136
+ export function unregisterProcess(pid) {
137
+ const validatedPid = validateProcessId(pid);
138
+ if (!validatedPid) {
139
+ return false;
140
+ }
141
+
142
+ const data = readPidFile();
143
+ const initialLength = data.processes.length;
144
+ data.processes = data.processes.filter(p => p.pid !== validatedPid);
145
+
146
+ if (data.processes.length < initialLength) {
147
+ writePidFile(data);
148
+ console.log(`Unregistered ChromeDebug process: PID ${validatedPid}`);
149
+ return true;
150
+ }
151
+
152
+ return false;
153
+ }
154
+
155
+ /**
156
+ * Get all registered processes
157
+ */
158
+ export function getRegisteredProcesses() {
159
+ return readPidFile().processes;
160
+ }
161
+
162
+ /**
163
+ * Check if a process is still running
164
+ */
165
+ function isProcessRunning(pid) {
166
+ try {
167
+ // process.kill with signal 0 checks if process exists without killing it
168
+ process.kill(pid, 0);
169
+ return true;
170
+ } catch (error) {
171
+ return false;
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Clean up dead processes from tracking file
177
+ */
178
+ export function cleanupDeadProcesses() {
179
+ const data = readPidFile();
180
+ const initialCount = data.processes.length;
181
+
182
+ if (initialCount === 0) {
183
+ return 0;
184
+ }
185
+
186
+ const aliveProcesses = [];
187
+ let removedCount = 0;
188
+
189
+ for (const proc of data.processes) {
190
+ if (isProcessRunning(proc.pid)) {
191
+ aliveProcesses.push(proc);
192
+ } else {
193
+ console.log(`Removing dead process from tracking: PID ${proc.pid} (${proc.type})`);
194
+ removedCount++;
195
+ }
196
+ }
197
+
198
+ if (removedCount > 0) {
199
+ data.processes = aliveProcesses;
200
+ writePidFile(data);
201
+ console.log(`Cleaned up ${removedCount} dead processes from tracking`);
202
+ }
203
+
204
+ return removedCount;
205
+ }
206
+
207
+ /**
208
+ * Kill a registered process safely
209
+ */
210
+ export async function killRegisteredProcess(pid, signal = 'SIGTERM') {
211
+ const validatedPid = validateProcessId(pid);
212
+ if (!validatedPid) {
213
+ return false;
214
+ }
215
+
216
+ // Check if process is registered
217
+ const data = readPidFile();
218
+ const registered = data.processes.find(p => p.pid === validatedPid);
219
+ if (!registered) {
220
+ console.error(`Process ${validatedPid} is not registered with ChromeDebug`);
221
+ return false;
222
+ }
223
+
224
+ try {
225
+ process.kill(validatedPid, signal);
226
+ console.log(`Sent ${signal} to registered ChromeDebug process: PID ${validatedPid}`);
227
+
228
+ // If successful and using SIGKILL, unregister immediately
229
+ if (signal === 'SIGKILL') {
230
+ unregisterProcess(validatedPid);
231
+ }
232
+
233
+ return true;
234
+ } catch (error) {
235
+ if (error.code === 'ESRCH') {
236
+ // Process doesn't exist, remove from tracking
237
+ unregisterProcess(validatedPid);
238
+ return true;
239
+ } else if (error.code === 'EPERM') {
240
+ console.error(`Permission denied killing process ${validatedPid}`);
241
+ return false;
242
+ } else {
243
+ console.error(`Error killing process ${validatedPid}: ${error.message}`);
244
+ return false;
245
+ }
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Kill all registered ChromeDebug processes
251
+ */
252
+ export async function killAllRegisteredProcesses() {
253
+ const processes = getRegisteredProcesses();
254
+ const results = {
255
+ killed: [],
256
+ failed: [],
257
+ total: processes.length
258
+ };
259
+
260
+ if (processes.length === 0) {
261
+ console.log('No registered ChromeDebug processes to kill');
262
+ return results;
263
+ }
264
+
265
+ console.log(`Found ${processes.length} registered ChromeDebug processes, cleaning up...`);
266
+
267
+ // First try SIGTERM for graceful shutdown
268
+ for (const proc of processes) {
269
+ const success = await killRegisteredProcess(proc.pid, 'SIGTERM');
270
+ if (success) {
271
+ results.killed.push(proc.pid);
272
+ } else {
273
+ results.failed.push(proc.pid);
274
+ }
275
+ }
276
+
277
+ // Wait for graceful shutdown
278
+ if (results.failed.length > 0) {
279
+ console.log('Waiting 2 seconds for graceful shutdown...');
280
+ await new Promise(resolve => setTimeout(resolve, 2000));
281
+
282
+ // Try SIGKILL for remaining processes
283
+ const stillRunning = results.failed.slice();
284
+ results.failed = [];
285
+
286
+ for (const pid of stillRunning) {
287
+ const success = await killRegisteredProcess(pid, 'SIGKILL');
288
+ if (success) {
289
+ const index = results.killed.indexOf(pid);
290
+ if (index === -1) {
291
+ results.killed.push(pid);
292
+ }
293
+ } else {
294
+ results.failed.push(pid);
295
+ }
296
+ }
297
+ }
298
+
299
+ // Clean up the tracking file
300
+ cleanupDeadProcesses();
301
+
302
+ return results;
303
+ }
304
+
305
+ /**
306
+ * Find ChromeDebug processes using pattern matching (for untracked processes)
307
+ * This is a fallback for processes started before tracking was implemented
308
+ */
309
+ export async function findUntrackedChromePilotProcesses() {
310
+ const currentPid = process.pid;
311
+ const foundProcesses = [];
312
+
313
+ try {
314
+ const { spawn } = await import('child_process');
315
+
316
+ return new Promise((resolve) => {
317
+ const ps = spawn('ps', ['aux'], { stdio: ['ignore', 'pipe', 'ignore'] });
318
+ let stdout = '';
319
+
320
+ ps.stdout.on('data', (data) => {
321
+ stdout += data.toString();
322
+ });
323
+
324
+ ps.on('close', (code) => {
325
+ if (code !== 0) {
326
+ console.error('Failed to get process list');
327
+ resolve([]);
328
+ return;
329
+ }
330
+
331
+ const lines = stdout.trim().split('\n');
332
+ const registeredPids = new Set(getRegisteredProcesses().map(p => p.pid));
333
+
334
+ for (const line of lines) {
335
+ const parts = line.trim().split(/\s+/);
336
+ if (parts.length < 2) continue;
337
+
338
+ const pid = validateProcessId(parts[1]);
339
+ if (!pid || pid === currentPid) continue;
340
+
341
+ // Skip processes that are already registered
342
+ if (registeredPids.has(pid)) continue;
343
+
344
+ // Check if it's a ChromeDebug/ChromePilot process
345
+ let processType = '';
346
+ if (line.includes('src/index.js')) {
347
+ processType = 'untracked-mcp-server';
348
+ } else if (line.includes('src/http-server.js')) {
349
+ processType = 'untracked-http-server';
350
+ } else if (line.includes('standalone-server.js')) {
351
+ processType = 'untracked-standalone-server';
352
+ } else if (line.includes('chromedebug-mcp') && line.includes('node')) {
353
+ processType = 'untracked-chromedebug-process';
354
+ }
355
+
356
+ if (processType) {
357
+ foundProcesses.push({
358
+ pid,
359
+ type: processType,
360
+ description: `${processType} (PID: ${pid})`,
361
+ commandLine: line
362
+ });
363
+ }
364
+ }
365
+
366
+ resolve(foundProcesses);
367
+ });
368
+
369
+ ps.on('error', (error) => {
370
+ console.error('Error running ps command:', error);
371
+ resolve([]);
372
+ });
373
+ });
374
+ } catch (error) {
375
+ console.error('Error finding untracked ChromePilot processes:', error);
376
+ return [];
377
+ }
378
+ }
379
+
380
+ /**
381
+ * Kill untracked ChromeDebug processes (emergency cleanup)
382
+ */
383
+ export async function killUntrackedChromePilotProcesses() {
384
+ const untrackedProcesses = await findUntrackedChromePilotProcesses();
385
+ const results = {
386
+ found: untrackedProcesses,
387
+ killed: [],
388
+ failed: [],
389
+ total: untrackedProcesses.length
390
+ };
391
+
392
+ if (untrackedProcesses.length === 0) {
393
+ console.log('No untracked ChromeDebug processes found');
394
+ return results;
395
+ }
396
+
397
+ console.log(`Found ${untrackedProcesses.length} untracked ChromeDebug processes:`);
398
+ untrackedProcesses.forEach(proc => {
399
+ console.log(` - ${proc.description}`);
400
+ });
401
+
402
+ // Try SIGTERM first
403
+ for (const proc of untrackedProcesses) {
404
+ try {
405
+ process.kill(proc.pid, 'SIGTERM');
406
+ console.log(`Sent SIGTERM to untracked process: PID ${proc.pid}`);
407
+ results.killed.push(proc.pid);
408
+ } catch (error) {
409
+ if (error.code === 'ESRCH') {
410
+ // Process doesn't exist, count as success
411
+ results.killed.push(proc.pid);
412
+ } else {
413
+ console.error(`Failed to kill process ${proc.pid}: ${error.message}`);
414
+ results.failed.push(proc.pid);
415
+ }
416
+ }
417
+ }
418
+
419
+ // Wait for graceful shutdown
420
+ if (results.failed.length > 0) {
421
+ console.log('Waiting 2 seconds for graceful shutdown...');
422
+ await new Promise(resolve => setTimeout(resolve, 2000));
423
+
424
+ // Try SIGKILL for remaining processes
425
+ const stillFailed = [];
426
+ for (const pid of results.failed) {
427
+ try {
428
+ process.kill(pid, 'SIGKILL');
429
+ console.log(`Sent SIGKILL to stubborn process: PID ${pid}`);
430
+ // Move from failed to killed
431
+ const index = results.failed.indexOf(pid);
432
+ results.failed.splice(index, 1);
433
+ if (!results.killed.includes(pid)) {
434
+ results.killed.push(pid);
435
+ }
436
+ } catch (error) {
437
+ if (error.code !== 'ESRCH') {
438
+ stillFailed.push(pid);
439
+ }
440
+ }
441
+ }
442
+ results.failed = stillFailed;
443
+ }
444
+
445
+ return results;
446
+ }
447
+
448
+ /**
449
+ * Register current process and set up cleanup on exit
450
+ */
451
+ export function setupProcessTracking(type = 'mcp-server') {
452
+ const currentPid = process.pid;
453
+
454
+ // Clean up any dead processes first
455
+ cleanupDeadProcesses();
456
+
457
+ // Register this process
458
+ const registered = registerProcess(currentPid, type);
459
+ if (!registered) {
460
+ console.error(`Failed to register process ${currentPid} for tracking`);
461
+ return currentPid;
462
+ }
463
+
464
+ // Set up robust cleanup on process exit
465
+ const cleanup = (reason = 'unknown') => {
466
+ try {
467
+ console.log(`Process ${currentPid} exiting (${reason}), cleaning up...`);
468
+ unregisterProcess(currentPid);
469
+ } catch (error) {
470
+ console.error(`Error during process cleanup: ${error.message}`);
471
+ }
472
+ };
473
+
474
+ // Handle various exit scenarios
475
+ process.on('exit', () => cleanup('exit'));
476
+ process.on('SIGINT', () => {
477
+ cleanup('SIGINT');
478
+ process.exit(0);
479
+ });
480
+ process.on('SIGTERM', () => {
481
+ cleanup('SIGTERM');
482
+ process.exit(0);
483
+ });
484
+ process.on('SIGHUP', () => {
485
+ cleanup('SIGHUP');
486
+ process.exit(0);
487
+ });
488
+ process.on('uncaughtException', (error) => {
489
+ console.error('Uncaught exception:', error);
490
+ cleanup('uncaughtException');
491
+ process.exit(1);
492
+ });
493
+ process.on('unhandledRejection', (reason, promise) => {
494
+ console.error('Unhandled rejection at:', promise, 'reason:', reason);
495
+ cleanup('unhandledRejection');
496
+ process.exit(1);
497
+ });
498
+
499
+ console.log(`Process tracking setup complete for PID ${currentPid} (${type}) [Session: ${getSessionId()}]`);
500
+ return currentPid;
501
+ }
502
+
503
+ /**
504
+ * Setup process tracking with enhanced session isolation options
505
+ */
506
+ export function setupProcessTrackingWithOptions(type = 'mcp-server', options = {}) {
507
+ const {
508
+ sessionId,
509
+ skipCleanup = false,
510
+ verbose = false
511
+ } = options;
512
+
513
+ // Set session ID if provided
514
+ if (sessionId) {
515
+ setSessionId(sessionId);
516
+ }
517
+
518
+ const currentSessionId = getSessionId();
519
+
520
+ if (verbose) {
521
+ console.log(`ChromeDebug MCP starting with session ID: ${currentSessionId}`);
522
+ console.log(`Skip cleanup: ${skipCleanup}`);
523
+ }
524
+
525
+ // Optionally skip cleanup of dead processes
526
+ if (!skipCleanup) {
527
+ const removedCount = cleanupDeadProcesses();
528
+ if (removedCount > 0 && verbose) {
529
+ console.log(`Cleaned up ${removedCount} dead processes from session ${currentSessionId}`);
530
+ }
531
+ }
532
+
533
+ // Setup tracking for this process
534
+ return setupProcessTracking(type);
535
+ }
536
+
537
+ /**
538
+ * Get current session information
539
+ */
540
+ export function getSessionInfo() {
541
+ const sessionId = getSessionId();
542
+ const data = readPidFile();
543
+ return {
544
+ sessionId,
545
+ pidFile: getPidFilePath(),
546
+ processes: data.processes,
547
+ processCount: data.processes.length,
548
+ lastUpdated: data.lastUpdated
549
+ };
550
+ }
551
+
552
+ /**
553
+ * Clean up all session files and processes for this session
554
+ */
555
+ export async function cleanupCurrentSession() {
556
+ const sessionId = getSessionId();
557
+ console.log(`Cleaning up session: ${sessionId}`);
558
+
559
+ // Kill all processes in this session
560
+ const results = await killAllRegisteredProcesses();
561
+
562
+ // Remove the PID file
563
+ const pidFilePath = getPidFilePath();
564
+ try {
565
+ if (fs.existsSync(pidFilePath)) {
566
+ fs.unlinkSync(pidFilePath);
567
+ console.log(`Removed session PID file: ${pidFilePath}`);
568
+ }
569
+ } catch (error) {
570
+ console.error(`Failed to remove PID file: ${error.message}`);
571
+ }
572
+
573
+ return results;
574
+ }