@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,678 @@
1
+ /**
2
+ * Unified Session Manager - Single source of truth for Chrome Debug MCP sessions
3
+ * Consolidates process tracking, port discovery, and resource management
4
+ */
5
+
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import os from 'os';
9
+ import crypto from 'crypto';
10
+ import net from 'net';
11
+
12
+ /**
13
+ * Unified Session Manager - handles all session-related resources atomically
14
+ */
15
+ export class UnifiedSessionManager {
16
+ constructor(options = {}) {
17
+ this.sessionId = options.sessionId || this.generateSessionId();
18
+ this.skipCleanup = options.skipCleanup || false;
19
+ this.verbose = options.verbose || false;
20
+
21
+ // Session file paths
22
+ this.sessionDir = path.join(os.tmpdir(), 'chromedebug-sessions');
23
+ this.sessionFile = path.join(this.sessionDir, `${this.sessionId}.json`);
24
+ this.lockFile = null;
25
+
26
+ // Session state
27
+ this.state = {
28
+ sessionId: this.sessionId,
29
+ pid: process.pid,
30
+ startTime: Date.now(),
31
+ port: null,
32
+ processes: [],
33
+ locks: [],
34
+ heartbeat: Date.now()
35
+ };
36
+
37
+ this.heartbeatInterval = null;
38
+ this.initialized = false;
39
+ this.cleanupHandlersRegistered = false;
40
+ this.eventHandlers = new Map(); // Track event handlers for cleanup
41
+ }
42
+
43
+ /**
44
+ * Generate cryptographically secure session ID
45
+ */
46
+ generateSessionId() {
47
+ return 'session_' + crypto.randomBytes(16).toString('hex');
48
+ }
49
+
50
+ /**
51
+ * Initialize session with atomic operations
52
+ */
53
+ async initialize() {
54
+ if (this.initialized) return this.state;
55
+
56
+ try {
57
+ // Ensure session directory exists
58
+ await this.ensureSessionDirectory();
59
+
60
+ // Clean up stale sessions first (if not skipping cleanup)
61
+ if (!this.skipCleanup) {
62
+ await this.cleanupStaleSessions();
63
+ }
64
+
65
+ // Atomically create session file
66
+ await this.createSessionFile();
67
+
68
+ // Find and allocate port
69
+ const port = await this.allocatePort();
70
+ this.state.port = port;
71
+
72
+ // Update session file with port
73
+ await this.updateSessionFile();
74
+
75
+ // Start heartbeat
76
+ this.startHeartbeat();
77
+
78
+ // Setup cleanup handlers
79
+ this.setupCleanupHandlers();
80
+
81
+ this.initialized = true;
82
+
83
+ if (this.verbose) {
84
+ console.log(`Session initialized: ${this.sessionId}`);
85
+ console.log(` PID: ${this.state.pid}`);
86
+ console.log(` Port: ${this.state.port}`);
87
+ console.log(` Session file: ${this.sessionFile}`);
88
+ }
89
+
90
+ return this.state;
91
+ } catch (error) {
92
+ await this.cleanup();
93
+ throw new Error(`Failed to initialize session: ${error.message}`);
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Ensure session directory exists with proper permissions
99
+ */
100
+ async ensureSessionDirectory() {
101
+ try {
102
+ await fs.promises.mkdir(this.sessionDir, {
103
+ recursive: true,
104
+ mode: 0o700 // Only owner can read/write
105
+ });
106
+ } catch (error) {
107
+ if (error.code !== 'EEXIST') {
108
+ throw error;
109
+ }
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Atomically create session file
115
+ */
116
+ async createSessionFile() {
117
+ try {
118
+ const sessionData = JSON.stringify(this.state, null, 2);
119
+
120
+ // Atomic write - fails if file exists
121
+ await fs.promises.writeFile(this.sessionFile, sessionData, {
122
+ flag: 'wx',
123
+ mode: 0o600 // Only owner can read/write
124
+ });
125
+
126
+ } catch (error) {
127
+ if (error.code === 'EEXIST') {
128
+ throw new Error(`Session ${this.sessionId} already exists`);
129
+ }
130
+ throw error;
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Update session file atomically
136
+ */
137
+ async updateSessionFile() {
138
+ this.state.heartbeat = Date.now();
139
+
140
+ const tempFile = `${this.sessionFile}.tmp`;
141
+ const sessionData = JSON.stringify(this.state, null, 2);
142
+
143
+ try {
144
+ // Defensive check: verify session file exists before updating
145
+ await fs.promises.access(this.sessionFile);
146
+
147
+ // Write to temp file first
148
+ await fs.promises.writeFile(tempFile, sessionData, { mode: 0o600 });
149
+
150
+ // Atomic rename
151
+ await fs.promises.rename(tempFile, this.sessionFile);
152
+ } catch (error) {
153
+ // Clean up temp file if it exists
154
+ try {
155
+ await fs.promises.unlink(tempFile);
156
+ } catch {}
157
+
158
+ // Handle specific error cases
159
+ if (error.code === 'ENOENT') {
160
+ throw new Error(`Session file does not exist: ${this.sessionFile}`);
161
+ } else if (error.code === 'EPERM' || error.code === 'EACCES') {
162
+ throw new Error(`Permission denied accessing session file: ${this.sessionFile}`);
163
+ }
164
+
165
+ throw error;
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Allocate an available port with atomic check-and-lock to prevent race conditions
171
+ */
172
+ async allocatePort() {
173
+ const preferredPorts = [
174
+ // Original range (maintain backward compatibility)
175
+ 3000, 3001, 3002, 3028, 3029, 3030, 3031, 3032, 3033, 3034, 3035, 3036,
176
+ // Safer fallback ranges (avoid common service conflicts)
177
+ 8080, 8081, 8082, 8083, 8084, 8085, 8086, 8087, 8088, 8089, 8090,
178
+ 9000, 9001, 9002, 9003, 9004, 9005, 9006, 9007, 9008, 9009, 9010
179
+ ];
180
+
181
+ for (const port of preferredPorts) {
182
+ // ATOMIC: Check availability AND lock in single operation to prevent race conditions
183
+ const lockResult = await this.atomicPortCheckAndLock(port);
184
+ if (lockResult.success) {
185
+ this.lockFile = lockResult.lockFile;
186
+ return port;
187
+ }
188
+ }
189
+
190
+ throw new Error('No available ports found');
191
+ }
192
+
193
+ /**
194
+ * Check if port is available
195
+ */
196
+ async isPortAvailable(port) {
197
+ return new Promise((resolve) => {
198
+ const server = net.createServer();
199
+
200
+ server.once('error', () => resolve(false));
201
+ server.once('listening', () => {
202
+ server.close();
203
+ resolve(true);
204
+ });
205
+
206
+ server.listen(port);
207
+ });
208
+ }
209
+
210
+ /**
211
+ * Try to atomically lock a port
212
+ */
213
+ async tryLockPort(port) {
214
+ const lockDir = path.join(this.sessionDir, 'port-locks');
215
+ const lockFile = path.join(lockDir, `port-${port}.lock`);
216
+
217
+ try {
218
+ await fs.promises.mkdir(lockDir, { recursive: true, mode: 0o700 });
219
+
220
+ const lockData = {
221
+ sessionId: this.sessionId,
222
+ port,
223
+ pid: process.pid,
224
+ timestamp: Date.now()
225
+ };
226
+
227
+ // Atomic lock creation
228
+ await fs.promises.writeFile(lockFile, JSON.stringify(lockData), {
229
+ flag: 'wx',
230
+ mode: 0o600
231
+ });
232
+
233
+ return { success: true, lockFile };
234
+ } catch (error) {
235
+ if (error.code === 'EEXIST') {
236
+ // Check if lock is stale
237
+ return await this.handleStaleLock(lockFile, port);
238
+ }
239
+ return { success: false };
240
+ }
241
+ }
242
+
243
+ /**
244
+ * Handle potentially stale port locks
245
+ */
246
+ async handleStaleLock(lockFile, port) {
247
+ try {
248
+ const lockData = JSON.parse(await fs.promises.readFile(lockFile, 'utf8'));
249
+ const lockAge = Date.now() - lockData.timestamp;
250
+
251
+ // 30 second timeout for locks
252
+ if (lockAge > 30000) {
253
+ await fs.promises.unlink(lockFile);
254
+ return await this.tryLockPort(port);
255
+ }
256
+
257
+ return { success: false };
258
+ } catch (error) {
259
+ // Corrupted lock file, try to remove and retry
260
+ try {
261
+ await fs.promises.unlink(lockFile);
262
+ return await this.tryLockPort(port);
263
+ } catch {
264
+ return { success: false };
265
+ }
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Atomically check port availability and acquire lock to prevent race conditions
271
+ * This combines the port availability check with lock acquisition in one operation
272
+ */
273
+ async atomicPortCheckAndLock(port) {
274
+ const lockDir = path.join(this.sessionDir, 'port-locks');
275
+ const lockFile = path.join(lockDir, `port-${port}.lock`);
276
+
277
+ try {
278
+ // Ensure lock directory exists
279
+ await fs.promises.mkdir(lockDir, { recursive: true, mode: 0o700 });
280
+
281
+ // First, attempt to acquire the lock atomically
282
+ // If this fails, the port is already locked by another session
283
+ const lockData = {
284
+ sessionId: this.sessionId,
285
+ port,
286
+ pid: process.pid,
287
+ timestamp: Date.now()
288
+ };
289
+
290
+ try {
291
+ // Atomic lock creation - will fail if file already exists
292
+ await fs.promises.writeFile(lockFile, JSON.stringify(lockData), {
293
+ flag: 'wx', // Create file, fail if exists (atomic)
294
+ mode: 0o600
295
+ });
296
+ } catch (error) {
297
+ if (error.code === 'EEXIST') {
298
+ // Lock already exists, check if it's stale
299
+ return await this.handleStaleLock(lockFile, port);
300
+ }
301
+ return { success: false, error: error.message };
302
+ }
303
+
304
+ // Lock acquired, now verify port is actually available
305
+ const isAvailable = await this.isPortAvailable(port);
306
+
307
+ if (!isAvailable) {
308
+ // Port is locked by us but not actually available, release lock
309
+ try {
310
+ await fs.promises.unlink(lockFile);
311
+ } catch {}
312
+ return { success: false, error: 'Port not available despite successful lock' };
313
+ }
314
+
315
+ // Success: port is available and locked by us
316
+ return { success: true, lockFile };
317
+
318
+ } catch (error) {
319
+ // Clean up any partial state
320
+ try {
321
+ await fs.promises.unlink(lockFile);
322
+ } catch {}
323
+ return { success: false, error: error.message };
324
+ }
325
+ }
326
+
327
+ /**
328
+ * Register a process for tracking
329
+ */
330
+ async registerProcess(pid, type = 'unknown') {
331
+ const processInfo = {
332
+ pid,
333
+ type,
334
+ startTime: Date.now()
335
+ };
336
+
337
+ this.state.processes.push(processInfo);
338
+ await this.updateSessionFile();
339
+
340
+ if (this.verbose) {
341
+ console.log(`Registered process: PID ${pid} (${type}) [Session: ${this.sessionId}]`);
342
+ }
343
+ }
344
+
345
+ /**
346
+ * Unregister a process
347
+ */
348
+ async unregisterProcess(pid) {
349
+ this.state.processes = this.state.processes.filter(p => p.pid !== pid);
350
+ await this.updateSessionFile();
351
+
352
+ if (this.verbose) {
353
+ console.log(`Unregistered process: PID ${pid} [Session: ${this.sessionId}]`);
354
+ }
355
+ }
356
+
357
+ /**
358
+ * Start heartbeat to keep session alive
359
+ */
360
+ startHeartbeat() {
361
+ this.heartbeatInterval = setInterval(async () => {
362
+ try {
363
+ await this.updateSessionFile();
364
+ } catch (error) {
365
+ // Handle specific error cases that indicate session cleanup
366
+ if (error.code === 'ENOENT' || error.message.includes('Session file does not exist')) {
367
+ if (this.verbose) {
368
+ console.log(`Session file removed, stopping heartbeat for session ${this.sessionId}`);
369
+ }
370
+ this.stopHeartbeat();
371
+ return;
372
+ }
373
+
374
+ // Handle permission errors gracefully
375
+ if (error.code === 'EPERM' || error.code === 'EACCES' || error.message.includes('Permission denied')) {
376
+ console.warn(`Heartbeat permission error for session ${this.sessionId}:`, error.message);
377
+ this.stopHeartbeat();
378
+ return;
379
+ }
380
+
381
+ // Log other errors but don't stop heartbeat
382
+ console.error(`Heartbeat failed for session ${this.sessionId}:`, error.message);
383
+ }
384
+ }, 10000); // 10 second heartbeat
385
+ }
386
+
387
+ /**
388
+ * Stop heartbeat timer and update state
389
+ */
390
+ stopHeartbeat() {
391
+ if (this.heartbeatInterval) {
392
+ clearInterval(this.heartbeatInterval);
393
+ this.heartbeatInterval = null;
394
+
395
+ // Update state consistency when heartbeat stops
396
+ this.initialized = false;
397
+
398
+ if (this.verbose) {
399
+ console.log(`Heartbeat stopped for session ${this.sessionId}`);
400
+ }
401
+ }
402
+ }
403
+
404
+ /**
405
+ * Get session information
406
+ */
407
+ getSessionInfo() {
408
+ return {
409
+ ...this.state,
410
+ sessionFile: this.sessionFile,
411
+ lockFile: this.lockFile,
412
+ initialized: this.initialized
413
+ };
414
+ }
415
+
416
+ /**
417
+ * Find all active sessions
418
+ */
419
+ static async findActiveSessions() {
420
+ const sessionDir = path.join(os.tmpdir(), 'chromedebug-sessions');
421
+ const sessions = [];
422
+
423
+ try {
424
+ const files = await fs.promises.readdir(sessionDir);
425
+ const sessionFiles = files.filter(f => f.endsWith('.json') && f.startsWith('session_'));
426
+
427
+ for (const file of sessionFiles) {
428
+ try {
429
+ const sessionData = JSON.parse(
430
+ await fs.promises.readFile(path.join(sessionDir, file), 'utf8')
431
+ );
432
+
433
+ // Check if session is still alive (heartbeat within 30 seconds)
434
+ const heartbeatAge = Date.now() - sessionData.heartbeat;
435
+ if (heartbeatAge < 30000) {
436
+ sessions.push(sessionData);
437
+ }
438
+ } catch (error) {
439
+ // Skip corrupted session files
440
+ }
441
+ }
442
+ } catch (error) {
443
+ // Directory might not exist
444
+ }
445
+
446
+ return sessions;
447
+ }
448
+
449
+ /**
450
+ * Clean up stale sessions
451
+ */
452
+ async cleanupStaleSessions() {
453
+ const sessionDir = path.join(os.tmpdir(), 'chromedebug-sessions');
454
+
455
+ try {
456
+ const files = await fs.promises.readdir(sessionDir);
457
+ let cleanedCount = 0;
458
+
459
+ for (const file of files) {
460
+ if (!file.endsWith('.json') || !file.startsWith('session_')) continue;
461
+
462
+ const sessionFile = path.join(sessionDir, file);
463
+
464
+ try {
465
+ const sessionData = JSON.parse(await fs.promises.readFile(sessionFile, 'utf8'));
466
+ const heartbeatAge = Date.now() - sessionData.heartbeat;
467
+
468
+ // Clean up sessions with heartbeat older than 1 minute
469
+ if (heartbeatAge > 60000) {
470
+ await fs.promises.unlink(sessionFile);
471
+ cleanedCount++;
472
+
473
+ if (this.verbose) {
474
+ console.log(`Cleaned up stale session: ${sessionData.sessionId}`);
475
+ }
476
+ }
477
+ } catch (error) {
478
+ // Remove corrupted session files
479
+ try {
480
+ await fs.promises.unlink(sessionFile);
481
+ cleanedCount++;
482
+ } catch {}
483
+ }
484
+ }
485
+
486
+ // Also clean up stale port locks
487
+ await this.cleanupStalePortLocks();
488
+
489
+ if (cleanedCount > 0 && this.verbose) {
490
+ console.log(`Cleaned up ${cleanedCount} stale sessions`);
491
+ }
492
+ } catch (error) {
493
+ // Directory might not exist
494
+ }
495
+ }
496
+
497
+ /**
498
+ * Clean up stale port locks
499
+ */
500
+ async cleanupStalePortLocks() {
501
+ const lockDir = path.join(this.sessionDir, 'port-locks');
502
+
503
+ try {
504
+ const files = await fs.promises.readdir(lockDir);
505
+
506
+ for (const file of files) {
507
+ if (!file.startsWith('port-') || !file.endsWith('.lock')) continue;
508
+
509
+ const lockFile = path.join(lockDir, file);
510
+
511
+ try {
512
+ const lockData = JSON.parse(await fs.promises.readFile(lockFile, 'utf8'));
513
+ const lockAge = Date.now() - lockData.timestamp;
514
+
515
+ // Clean up locks older than 30 seconds
516
+ if (lockAge > 30000) {
517
+ await fs.promises.unlink(lockFile);
518
+ }
519
+ } catch (error) {
520
+ // Remove corrupted lock files
521
+ try {
522
+ await fs.promises.unlink(lockFile);
523
+ } catch {}
524
+ }
525
+ }
526
+ } catch (error) {
527
+ // Directory might not exist
528
+ }
529
+ }
530
+
531
+ /**
532
+ * Setup cleanup handlers for graceful shutdown (prevents memory leaks)
533
+ */
534
+ setupCleanupHandlers() {
535
+ // Prevent duplicate event handlers
536
+ if (this.cleanupHandlersRegistered) {
537
+ return;
538
+ }
539
+
540
+ const cleanup = async (reason) => {
541
+ if (this.verbose) {
542
+ console.log(`Session ${this.sessionId} exiting (${reason}), cleaning up...`);
543
+ }
544
+ await this.cleanup();
545
+ };
546
+
547
+ // Create bound handlers and store them for removal
548
+ const exitHandler = () => cleanup('exit');
549
+ const sigintHandler = async () => {
550
+ await cleanup('SIGINT');
551
+ process.exit(0);
552
+ };
553
+ const sigtermHandler = async () => {
554
+ await cleanup('SIGTERM');
555
+ process.exit(0);
556
+ };
557
+ const uncaughtExceptionHandler = async (error) => {
558
+ console.error('Uncaught exception:', error);
559
+ await cleanup('uncaughtException');
560
+ process.exit(1);
561
+ };
562
+
563
+ // Store handlers for cleanup
564
+ this.eventHandlers.set('exit', exitHandler);
565
+ this.eventHandlers.set('SIGINT', sigintHandler);
566
+ this.eventHandlers.set('SIGTERM', sigtermHandler);
567
+ this.eventHandlers.set('uncaughtException', uncaughtExceptionHandler);
568
+
569
+ // Register event handlers
570
+ process.on('exit', exitHandler);
571
+ process.on('SIGINT', sigintHandler);
572
+ process.on('SIGTERM', sigtermHandler);
573
+ process.on('uncaughtException', uncaughtExceptionHandler);
574
+
575
+ this.cleanupHandlersRegistered = true;
576
+ }
577
+
578
+ /**
579
+ * Remove event handlers to prevent memory leaks
580
+ */
581
+ removeCleanupHandlers() {
582
+ if (!this.cleanupHandlersRegistered) {
583
+ return;
584
+ }
585
+
586
+ // Remove all registered event handlers
587
+ for (const [event, handler] of this.eventHandlers) {
588
+ process.removeListener(event, handler);
589
+ }
590
+
591
+ // Clear the handlers map
592
+ this.eventHandlers.clear();
593
+ this.cleanupHandlersRegistered = false;
594
+ }
595
+
596
+ /**
597
+ * Clean up all session resources
598
+ */
599
+ async cleanup() {
600
+ try {
601
+ // Remove event handlers to prevent memory leaks
602
+ this.removeCleanupHandlers();
603
+
604
+ // Stop heartbeat
605
+ this.stopHeartbeat();
606
+
607
+ // Release port lock
608
+ if (this.lockFile) {
609
+ try {
610
+ await fs.promises.unlink(this.lockFile);
611
+ } catch {}
612
+ }
613
+
614
+ // Remove session file
615
+ try {
616
+ await fs.promises.unlink(this.sessionFile);
617
+ } catch {}
618
+
619
+ if (this.verbose) {
620
+ console.log(`Session ${this.sessionId} cleaned up`);
621
+ }
622
+ } catch (error) {
623
+ console.error(`Error during session cleanup:`, error.message);
624
+ }
625
+ }
626
+ }
627
+
628
+ // Global session manager instance
629
+ let globalSessionManager = null;
630
+
631
+ /**
632
+ * Get or create the global session manager
633
+ */
634
+ export function getSessionManager(options = {}) {
635
+ if (!globalSessionManager) {
636
+ globalSessionManager = new UnifiedSessionManager(options);
637
+ }
638
+ return globalSessionManager;
639
+ }
640
+
641
+ /**
642
+ * Initialize session management with options
643
+ */
644
+ export async function initializeSessionManager(options = {}) {
645
+ const sessionManager = getSessionManager(options);
646
+ await sessionManager.initialize();
647
+ return sessionManager;
648
+ }
649
+
650
+ /**
651
+ * Get current session information
652
+ */
653
+ export function getSessionInfo() {
654
+ return globalSessionManager ? globalSessionManager.getSessionInfo() : null;
655
+ }
656
+
657
+ /**
658
+ * Register a process for tracking
659
+ */
660
+ export async function registerProcess(pid, type = 'unknown') {
661
+ if (globalSessionManager) {
662
+ await globalSessionManager.registerProcess(pid, type);
663
+ }
664
+ }
665
+
666
+ /**
667
+ * Unregister a process
668
+ */
669
+ export async function unregisterProcess(pid) {
670
+ if (globalSessionManager) {
671
+ await globalSessionManager.unregisterProcess(pid);
672
+ }
673
+ }
674
+
675
+ /**
676
+ * Find all active sessions
677
+ */
678
+ export const findActiveSessions = UnifiedSessionManager.findActiveSessions;