@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,200 @@
1
+ /**
2
+ * HeartbeatManager - Manages heartbeats for multiple Chrome Debug sessions
3
+ *
4
+ * Coordinates heartbeat mechanisms across different session managers
5
+ * to ensure sessions stay alive and detect stale sessions for cleanup.
6
+ *
7
+ * Key Features:
8
+ * - Multi-session heartbeat coordination
9
+ * - Stale heartbeat detection
10
+ * - Automatic cleanup integration
11
+ * - Thread-safe heartbeat operations
12
+ */
13
+
14
+ export class HeartbeatManager {
15
+ constructor() {
16
+ this.heartbeats = new Map(); // sessionId -> { interval, lastUpdate }
17
+ this.HEARTBEAT_INTERVAL = 30000; // 30 seconds
18
+ this.STALE_THRESHOLD = 5 * 60 * 1000; // 5 minutes
19
+ }
20
+
21
+ /**
22
+ * Starts heartbeat for a session
23
+ * @param {string} sessionId - Session ID to start heartbeat for
24
+ * @param {Function} updateCallback - Optional callback to call on each heartbeat
25
+ */
26
+ startHeartbeat(sessionId, updateCallback = null) {
27
+ // Stop existing heartbeat if running
28
+ this.stopHeartbeat(sessionId);
29
+
30
+ const heartbeatData = {
31
+ lastUpdate: new Date(),
32
+ callback: updateCallback,
33
+ interval: setInterval(() => {
34
+ heartbeatData.lastUpdate = new Date();
35
+ if (updateCallback && typeof updateCallback === 'function') {
36
+ updateCallback(sessionId).catch(error => {
37
+ console.error(`[HeartbeatManager] Heartbeat callback error for ${sessionId}:`, error);
38
+ });
39
+ }
40
+ }, this.HEARTBEAT_INTERVAL)
41
+ };
42
+
43
+ this.heartbeats.set(sessionId, heartbeatData);
44
+ console.log(`[HeartbeatManager] Started heartbeat for session ${sessionId}`);
45
+ }
46
+
47
+ /**
48
+ * Stops heartbeat for a session
49
+ * @param {string} sessionId - Session ID to stop heartbeat for
50
+ */
51
+ stopHeartbeat(sessionId) {
52
+ const heartbeatData = this.heartbeats.get(sessionId);
53
+ if (heartbeatData) {
54
+ clearInterval(heartbeatData.interval);
55
+ this.heartbeats.delete(sessionId);
56
+ console.log(`[HeartbeatManager] Stopped heartbeat for session ${sessionId}`);
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Checks if heartbeat is running for a session
62
+ * @param {string} sessionId - Session ID to check
63
+ * @returns {boolean} True if heartbeat is running
64
+ */
65
+ isHeartbeatRunning(sessionId) {
66
+ return this.heartbeats.has(sessionId);
67
+ }
68
+
69
+ /**
70
+ * Updates heartbeat timestamp for a session (manual update)
71
+ * @param {string} sessionId - Session ID to update
72
+ * @returns {Promise<void>}
73
+ */
74
+ async updateHeartbeat(sessionId) {
75
+ const heartbeatData = this.heartbeats.get(sessionId);
76
+ if (heartbeatData) {
77
+ heartbeatData.lastUpdate = new Date();
78
+ console.log(`[HeartbeatManager] Manual heartbeat update for session ${sessionId}`);
79
+ } else {
80
+ console.warn(`[HeartbeatManager] Cannot update heartbeat for unknown session ${sessionId}`);
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Gets last heartbeat time for a session
86
+ * @param {string} sessionId - Session ID to check
87
+ * @returns {Date|null} Last heartbeat time or null if not found
88
+ */
89
+ getLastHeartbeat(sessionId) {
90
+ const heartbeatData = this.heartbeats.get(sessionId);
91
+ return heartbeatData ? heartbeatData.lastUpdate : null;
92
+ }
93
+
94
+ /**
95
+ * Checks for stale heartbeats across all sessions
96
+ * @returns {Promise<string[]>} Array of session IDs with stale heartbeats
97
+ */
98
+ async checkStaleHeartbeats() {
99
+ const now = new Date();
100
+ const staleSessionIds = [];
101
+
102
+ for (const [sessionId, heartbeatData] of this.heartbeats.entries()) {
103
+ const timeSinceHeartbeat = now.getTime() - heartbeatData.lastUpdate.getTime();
104
+
105
+ if (timeSinceHeartbeat > this.STALE_THRESHOLD) {
106
+ staleSessionIds.push(sessionId);
107
+ }
108
+ }
109
+
110
+ if (staleSessionIds.length > 0) {
111
+ console.log(`[HeartbeatManager] Found ${staleSessionIds.length} stale heartbeats:`, staleSessionIds);
112
+ }
113
+
114
+ return staleSessionIds;
115
+ }
116
+
117
+ /**
118
+ * Gets heartbeat status for all sessions
119
+ * @returns {Object} Heartbeat status summary
120
+ */
121
+ getHeartbeatStatus() {
122
+ const now = new Date();
123
+ const status = {
124
+ totalSessions: this.heartbeats.size,
125
+ activeSessions: 0,
126
+ staleSessions: 0,
127
+ sessionDetails: []
128
+ };
129
+
130
+ for (const [sessionId, heartbeatData] of this.heartbeats.entries()) {
131
+ const timeSinceHeartbeat = now.getTime() - heartbeatData.lastUpdate.getTime();
132
+ const isStale = timeSinceHeartbeat > this.STALE_THRESHOLD;
133
+
134
+ if (isStale) {
135
+ status.staleSessions++;
136
+ } else {
137
+ status.activeSessions++;
138
+ }
139
+
140
+ status.sessionDetails.push({
141
+ sessionId,
142
+ lastHeartbeat: heartbeatData.lastUpdate,
143
+ timeSinceHeartbeat,
144
+ isStale
145
+ });
146
+ }
147
+
148
+ return status;
149
+ }
150
+
151
+ /**
152
+ * Stops all heartbeats (cleanup method)
153
+ */
154
+ stopAllHeartbeats() {
155
+ console.log(`[HeartbeatManager] Stopping all heartbeats (${this.heartbeats.size} sessions)`);
156
+
157
+ for (const [sessionId, heartbeatData] of this.heartbeats.entries()) {
158
+ clearInterval(heartbeatData.interval);
159
+ }
160
+
161
+ this.heartbeats.clear();
162
+ }
163
+
164
+ /**
165
+ * Automatically cleans up stale heartbeats
166
+ * @returns {Promise<string[]>} Array of cleaned up session IDs
167
+ */
168
+ async autoCleanupStaleHeartbeats() {
169
+ const staleSessionIds = await this.checkStaleHeartbeats();
170
+
171
+ for (const sessionId of staleSessionIds) {
172
+ this.stopHeartbeat(sessionId);
173
+ }
174
+
175
+ if (staleSessionIds.length > 0) {
176
+ console.log(`[HeartbeatManager] Auto-cleaned ${staleSessionIds.length} stale heartbeats`);
177
+ }
178
+
179
+ return staleSessionIds;
180
+ }
181
+
182
+ /**
183
+ * Gets statistics about heartbeat performance
184
+ * @returns {Object} Heartbeat statistics
185
+ */
186
+ getStatistics() {
187
+ const status = this.getHeartbeatStatus();
188
+
189
+ return {
190
+ totalSessions: status.totalSessions,
191
+ activeSessions: status.activeSessions,
192
+ staleSessions: status.staleSessions,
193
+ stalePercentage: status.totalSessions > 0
194
+ ? Math.round((status.staleSessions / status.totalSessions) * 100)
195
+ : 0,
196
+ heartbeatInterval: this.HEARTBEAT_INTERVAL,
197
+ staleThreshold: this.STALE_THRESHOLD
198
+ };
199
+ }
200
+ }
@@ -0,0 +1,195 @@
1
+ /**
2
+ * HTTP Client Service for MCP handlers to access authenticated endpoints
3
+ * This service handles service-to-service authentication
4
+ */
5
+
6
+ // Use built-in fetch (Node.js 18+)
7
+
8
+ class HttpClientService {
9
+ constructor() {
10
+ this.baseUrl = null;
11
+ this.serviceToken = process.env.MCP_SERVICE_TOKEN;
12
+ this.initialized = false;
13
+ }
14
+
15
+ /**
16
+ * Initialize the HTTP client with server port
17
+ */
18
+ async initialize() {
19
+ if (this.initialized) return;
20
+
21
+ try {
22
+ // Get the current server port from configuration
23
+ let port = process.env.CHROME_PILOT_PORT || 3000;
24
+
25
+ // Try to load configLoader dynamically
26
+ try {
27
+ const { configLoader } = await import('../config-loader.js');
28
+ if (configLoader && typeof configLoader.getConfig === 'function') {
29
+ const config = configLoader.getConfig();
30
+ port = config.httpPort || port;
31
+ }
32
+ } catch (configError) {
33
+ console.warn('[HttpClient] Config loader not available, using default port:', port);
34
+ }
35
+
36
+ this.baseUrl = `http://localhost:${port}`;
37
+ this.initialized = true;
38
+ console.log(`[HttpClient] Initialized with base URL: ${this.baseUrl}`);
39
+ } catch (error) {
40
+ console.error('[HttpClient] Failed to initialize:', error);
41
+ throw error;
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Make authenticated request to HTTP server
47
+ * @param {string} endpoint - API endpoint path
48
+ * @param {Object} options - Request options
49
+ * @returns {Promise<Object>} Response data
50
+ */
51
+ async request(endpoint, options = {}) {
52
+ await this.initialize();
53
+
54
+ if (!this.serviceToken) {
55
+ throw new Error('MCP service token not available. Cannot make authenticated requests.');
56
+ }
57
+
58
+ const url = `${this.baseUrl}${endpoint}`;
59
+ const requestOptions = {
60
+ method: 'GET',
61
+ headers: {
62
+ 'Content-Type': 'application/json',
63
+ 'x-service-token': this.serviceToken,
64
+ ...options.headers
65
+ },
66
+ ...options
67
+ };
68
+
69
+ if (options.body && typeof options.body === 'object') {
70
+ requestOptions.body = JSON.stringify(options.body);
71
+ }
72
+
73
+ try {
74
+ console.log(`[HttpClient] Making ${requestOptions.method} request to ${url}`);
75
+ const response = await fetch(url, requestOptions);
76
+
77
+ if (!response.ok) {
78
+ const errorText = await response.text();
79
+ throw new Error(`HTTP ${response.status}: ${errorText}`);
80
+ }
81
+
82
+ const data = await response.json();
83
+ return data;
84
+ } catch (error) {
85
+ console.error(`[HttpClient] Request failed for ${url}:`, error.message);
86
+ throw error;
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Get workflow recording via HTTP
92
+ * @param {string} sessionId - Session ID
93
+ * @returns {Promise<Object>} Workflow recording data
94
+ */
95
+ async getWorkflowRecording(sessionId) {
96
+ return this.request(`/chromedebug/workflow-recording/${sessionId}`);
97
+ }
98
+
99
+ /**
100
+ * List workflow recordings via HTTP
101
+ * @returns {Promise<Object>} List of recordings
102
+ */
103
+ async listWorkflowRecordings() {
104
+ return this.request('/chromedebug/workflow-recordings');
105
+ }
106
+
107
+ /**
108
+ * Get frame session info via HTTP
109
+ * @param {string} sessionId - Session ID
110
+ * @returns {Promise<Object>} Frame session info
111
+ */
112
+ async getFrameSessionInfo(sessionId) {
113
+ return this.request(`/chromedebug/frame-session/${sessionId}`);
114
+ }
115
+
116
+ /**
117
+ * Get specific frame via HTTP
118
+ * @param {string} sessionId - Session ID
119
+ * @param {number} frameIndex - Frame index
120
+ * @returns {Promise<Object>} Frame data
121
+ */
122
+ async getFrame(sessionId, frameIndex) {
123
+ return this.request(`/chromedebug/frame/${sessionId}/${frameIndex}`);
124
+ }
125
+
126
+ /**
127
+ * Search frame logs via HTTP
128
+ * @param {string} sessionId - Session ID
129
+ * @param {string} searchText - Search text
130
+ * @param {string} logLevel - Log level filter
131
+ * @param {number} maxResults - Maximum results
132
+ * @returns {Promise<Object>} Search results
133
+ */
134
+ async searchFrameLogs(sessionId, searchText, logLevel = 'all', maxResults = 50) {
135
+ const params = new URLSearchParams({
136
+ searchText,
137
+ logLevel,
138
+ maxResults: maxResults.toString()
139
+ });
140
+ return this.request(`/chromedebug/frame-logs/search/${sessionId}?${params}`);
141
+ }
142
+
143
+ /**
144
+ * Get frame logs paginated via HTTP
145
+ * @param {string} sessionId - Session ID
146
+ * @param {number} frameIndex - Frame index
147
+ * @param {number} offset - Offset
148
+ * @param {number} limit - Limit
149
+ * @param {string} logLevel - Log level filter
150
+ * @param {string} searchText - Search text
151
+ * @returns {Promise<Object>} Paginated logs
152
+ */
153
+ async getFrameLogsPaginated(sessionId, frameIndex, offset = 0, limit = 100, logLevel = 'all', searchText = null) {
154
+ const params = new URLSearchParams({
155
+ offset: offset.toString(),
156
+ limit: limit.toString(),
157
+ logLevel
158
+ });
159
+ if (searchText) {
160
+ params.append('searchText', searchText);
161
+ }
162
+ return this.request(`/chromedebug/frame-logs/${sessionId}/${frameIndex}?${params}`);
163
+ }
164
+
165
+ /**
166
+ * Get screen interactions via HTTP
167
+ * @param {string} sessionId - Session ID
168
+ * @param {number} frameIndex - Optional frame index
169
+ * @param {string} type - Optional interaction type filter
170
+ * @returns {Promise<Object>} Screen interactions
171
+ */
172
+ async getScreenInteractions(sessionId, frameIndex = null, type = null) {
173
+ const params = new URLSearchParams();
174
+ if (frameIndex !== null) {
175
+ params.append('frameIndex', frameIndex.toString());
176
+ }
177
+ if (type) {
178
+ params.append('type', type);
179
+ }
180
+ const queryString = params.toString();
181
+ const endpoint = `/chromedebug/screen-interactions/${sessionId}${queryString ? `?${queryString}` : ''}`;
182
+ return this.request(endpoint);
183
+ }
184
+
185
+ /**
186
+ * List frame sessions via HTTP
187
+ * @returns {Promise<Object>} List of frame sessions
188
+ */
189
+ async listFrameSessions() {
190
+ return this.request('/chromedebug/frame-sessions');
191
+ }
192
+ }
193
+
194
+ // Export singleton instance
195
+ export const httpClient = new HttpClientService();
@@ -0,0 +1,318 @@
1
+ /**
2
+ * Process Manager Service - Secure process management with input validation
3
+ * Extracted from original index.js with preserved security measures
4
+ */
5
+
6
+ /**
7
+ * Safely validates a process ID to prevent injection attacks
8
+ * @param {*} pid - The process ID to validate
9
+ * @returns {number|null} - Validated PID or null if invalid
10
+ */
11
+ function validateProcessId(pid) {
12
+ // Security: Check for suspicious characters before parsing
13
+ const pidStr = String(pid).trim();
14
+ if (!/^\d+$/.test(pidStr)) {
15
+ // Contains non-digit characters - potential injection attempt
16
+ return null;
17
+ }
18
+
19
+ const numericPid = parseInt(pidStr, 10);
20
+ if (isNaN(numericPid) || numericPid <= 0 || numericPid > 4194304) { // Max PID on Linux/macOS
21
+ return null;
22
+ }
23
+ return numericPid;
24
+ }
25
+
26
+ /**
27
+ * Safely kills a process using Node.js native APIs to prevent command injection
28
+ * @param {number} pid - Validated process ID
29
+ * @param {string} signal - Signal to send ('SIGTERM' or 'SIGKILL')
30
+ * @returns {Promise<boolean>} - True if successful, false otherwise
31
+ */
32
+ async function safeKillProcess(pid, signal = 'SIGTERM') {
33
+ const validatedPid = validateProcessId(pid);
34
+ if (!validatedPid) {
35
+ console.error(`Invalid process ID: ${pid}`);
36
+ return false;
37
+ }
38
+
39
+ try {
40
+ // Security: Use Node.js native process.kill() instead of shell commands
41
+ process.kill(validatedPid, signal);
42
+ return true;
43
+ } catch (error) {
44
+ if (error.code === 'ESRCH') {
45
+ // Process doesn't exist
46
+ return true;
47
+ } else if (error.code === 'EPERM') {
48
+ console.error(`Permission denied killing process ${validatedPid}`);
49
+ return false;
50
+ } else {
51
+ console.error(`Error killing process ${validatedPid}: ${error.message}`);
52
+ return false;
53
+ }
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Find ChromeDebug MCP processes using Node.js process list instead of shell commands
59
+ * This prevents command injection while maintaining functionality
60
+ */
61
+ async function findChromePilotProcesses() {
62
+ const currentPid = process.pid;
63
+ const pidsToKill = [];
64
+ const processDescriptions = [];
65
+
66
+ try {
67
+ // Security: Use Node.js process list instead of shell commands
68
+ // This approach uses the 'ps-list' module or similar safe approach
69
+ // For now, we'll use a safer implementation with spawn instead of exec
70
+ const { spawn } = await import('child_process');
71
+
72
+ return new Promise((resolve) => {
73
+ const ps = spawn('ps', ['aux'], { stdio: ['ignore', 'pipe', 'ignore'] });
74
+ let stdout = '';
75
+
76
+ ps.stdout.on('data', (data) => {
77
+ stdout += data.toString();
78
+ });
79
+
80
+ ps.on('close', (code) => {
81
+ if (code !== 0) {
82
+ console.error('Failed to get process list');
83
+ resolve({ pidsToKill: [], processDescriptions: [] });
84
+ return;
85
+ }
86
+
87
+ const lines = stdout.trim().split('\n');
88
+
89
+ for (const line of lines) {
90
+ // Security: Validate each line and extract PID safely
91
+ const parts = line.trim().split(/\\s+/);
92
+ if (parts.length < 2) continue;
93
+
94
+ const pid = validateProcessId(parts[1]);
95
+ if (!pid || pid === currentPid) continue;
96
+
97
+ // Check if it's a ChromeDebug MCP related process
98
+ let processType = '';
99
+ if (line.includes('src/index.js')) {
100
+ processType = 'MCP server';
101
+ pidsToKill.push(pid);
102
+ } else if (line.includes('standalone-server.js')) {
103
+ processType = 'HTTP server';
104
+ pidsToKill.push(pid);
105
+ } else if (line.includes('http-server.js')) {
106
+ processType = 'HTTP server';
107
+ pidsToKill.push(pid);
108
+ } else if (line.includes('chrome-pilot') && line.includes('node')) {
109
+ processType = 'ChromeDebug MCP process';
110
+ pidsToKill.push(pid);
111
+ }
112
+
113
+ if (processType) {
114
+ processDescriptions.push(`${processType} (PID: ${pid})`);
115
+ }
116
+ }
117
+
118
+ resolve({ pidsToKill, processDescriptions });
119
+ });
120
+
121
+ ps.on('error', (error) => {
122
+ console.error('Error running ps command:', error);
123
+ resolve({ pidsToKill: [], processDescriptions: [] });
124
+ });
125
+ });
126
+ } catch (error) {
127
+ console.error('Error finding ChromeDebug MCP processes:', error);
128
+ return { pidsToKill: [], processDescriptions: [] };
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Process Manager class for secure process lifecycle management
134
+ */
135
+ export class ProcessManager {
136
+ constructor() {
137
+ this.managedProcesses = new Set();
138
+ this.shutdownHandlers = [];
139
+ this.setupGracefulShutdown();
140
+ }
141
+
142
+ /**
143
+ * Validates and registers a process ID for management
144
+ * @param {number} pid - Process ID to manage
145
+ * @returns {boolean} True if successfully registered
146
+ */
147
+ registerProcess(pid) {
148
+ const validatedPid = validateProcessId(pid);
149
+ if (!validatedPid) {
150
+ console.error(`Cannot register invalid process ID: ${pid}`);
151
+ return false;
152
+ }
153
+
154
+ this.managedProcesses.add(validatedPid);
155
+ return true;
156
+ }
157
+
158
+ /**
159
+ * Unregisters a process from management
160
+ * @param {number} pid - Process ID to unregister
161
+ */
162
+ unregisterProcess(pid) {
163
+ const validatedPid = validateProcessId(pid);
164
+ if (validatedPid) {
165
+ this.managedProcesses.delete(validatedPid);
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Safely kills a managed process
171
+ * @param {number} pid - Process ID to kill
172
+ * @param {string} signal - Signal to send
173
+ * @returns {Promise<boolean>} Success status
174
+ */
175
+ async killProcess(pid, signal = 'SIGTERM') {
176
+ const result = await safeKillProcess(pid, signal);
177
+ if (result) {
178
+ this.unregisterProcess(pid);
179
+ }
180
+ return result;
181
+ }
182
+
183
+ /**
184
+ * Kills all managed processes
185
+ * @param {string} signal - Signal to send
186
+ * @returns {Promise<Object>} Kill results
187
+ */
188
+ async killAllManagedProcesses(signal = 'SIGTERM') {
189
+ const results = {
190
+ killed: [],
191
+ failed: [],
192
+ total: this.managedProcesses.size
193
+ };
194
+
195
+ for (const pid of this.managedProcesses) {
196
+ const success = await safeKillProcess(pid, signal);
197
+ if (success) {
198
+ results.killed.push(pid);
199
+ this.managedProcesses.delete(pid);
200
+ } else {
201
+ results.failed.push(pid);
202
+ }
203
+ }
204
+
205
+ return results;
206
+ }
207
+
208
+ /**
209
+ * Finds and kills ChromeDebug MCP processes
210
+ * @returns {Promise<Object>} Cleanup results
211
+ */
212
+ async cleanupChromePilotProcesses() {
213
+ const { pidsToKill, processDescriptions } = await findChromePilotProcesses();
214
+
215
+ const results = {
216
+ found: processDescriptions,
217
+ killed: [],
218
+ failed: [],
219
+ total: pidsToKill.length
220
+ };
221
+
222
+ if (pidsToKill.length === 0) {
223
+ return results;
224
+ }
225
+
226
+ console.error('Found existing ChromeDebug MCP processes, cleaning up...');
227
+
228
+ // Try SIGTERM first
229
+ for (const pid of pidsToKill) {
230
+ const success = await safeKillProcess(pid, 'SIGTERM');
231
+ if (success) {
232
+ results.killed.push(pid);
233
+ } else {
234
+ results.failed.push(pid);
235
+ }
236
+ }
237
+
238
+ // Wait a bit for graceful shutdown
239
+ if (results.failed.length > 0) {
240
+ await new Promise(resolve => setTimeout(resolve, 2000));
241
+
242
+ // Try SIGKILL for remaining processes
243
+ const finalAttempts = [];
244
+ for (const pid of results.failed) {
245
+ const success = await safeKillProcess(pid, 'SIGKILL');
246
+ if (success) {
247
+ // Move from failed to killed
248
+ const index = results.failed.indexOf(pid);
249
+ results.failed.splice(index, 1);
250
+ results.killed.push(pid);
251
+ }
252
+ }
253
+ }
254
+
255
+ return results;
256
+ }
257
+
258
+ /**
259
+ * Adds a shutdown handler
260
+ * @param {Function} handler - Cleanup function to call on shutdown
261
+ */
262
+ addShutdownHandler(handler) {
263
+ this.shutdownHandlers.push(handler);
264
+ }
265
+
266
+ /**
267
+ * Sets up graceful shutdown handling
268
+ */
269
+ setupGracefulShutdown() {
270
+ const cleanup = async (signal) => {
271
+ console.error(`Received ${signal}, cleaning up...`);
272
+
273
+ // Run custom shutdown handlers
274
+ for (const handler of this.shutdownHandlers) {
275
+ try {
276
+ await handler();
277
+ } catch (error) {
278
+ console.error('Error in shutdown handler:', error);
279
+ }
280
+ }
281
+
282
+ // Kill all managed processes
283
+ await this.killAllManagedProcesses('SIGTERM');
284
+
285
+ process.exit(0);
286
+ };
287
+
288
+ // Register signal handlers
289
+ process.on('SIGINT', () => cleanup('SIGINT'));
290
+ process.on('SIGTERM', () => cleanup('SIGTERM'));
291
+
292
+ // Handle uncaught exceptions
293
+ process.on('uncaughtException', (error) => {
294
+ console.error('Uncaught exception:', error);
295
+ cleanup('uncaughtException');
296
+ });
297
+
298
+ process.on('unhandledRejection', (reason, promise) => {
299
+ console.error('Unhandled rejection at:', promise, 'reason:', reason);
300
+ cleanup('unhandledRejection');
301
+ });
302
+ }
303
+
304
+ /**
305
+ * Gets statistics about managed processes
306
+ * @returns {Object} Process statistics
307
+ */
308
+ getStatistics() {
309
+ return {
310
+ managedProcesses: this.managedProcesses.size,
311
+ shutdownHandlers: this.shutdownHandlers.length,
312
+ currentPid: process.pid
313
+ };
314
+ }
315
+ }
316
+
317
+ // Export utility functions for backward compatibility
318
+ export { validateProcessId, safeKillProcess, findChromePilotProcesses };