@romanmatena/browsermonitor 2.0.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 (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +558 -0
  3. package/package.json +53 -0
  4. package/src/agents.llm/browser-monitor-section.md +18 -0
  5. package/src/cli.mjs +202 -0
  6. package/src/http-server.mjs +536 -0
  7. package/src/init.mjs +162 -0
  8. package/src/intro.mjs +36 -0
  9. package/src/logging/LogBuffer.mjs +178 -0
  10. package/src/logging/constants.mjs +19 -0
  11. package/src/logging/dump.mjs +207 -0
  12. package/src/logging/index.mjs +13 -0
  13. package/src/logging/timestamps.mjs +13 -0
  14. package/src/monitor/README.md +10 -0
  15. package/src/monitor/index.mjs +18 -0
  16. package/src/monitor/interactive-mode.mjs +275 -0
  17. package/src/monitor/join-mode.mjs +654 -0
  18. package/src/monitor/open-mode.mjs +889 -0
  19. package/src/monitor/page-monitoring.mjs +199 -0
  20. package/src/monitor/tab-selection.mjs +53 -0
  21. package/src/monitor.mjs +39 -0
  22. package/src/os/README.md +4 -0
  23. package/src/os/wsl/chrome.mjs +503 -0
  24. package/src/os/wsl/detect.mjs +68 -0
  25. package/src/os/wsl/diagnostics.mjs +729 -0
  26. package/src/os/wsl/index.mjs +45 -0
  27. package/src/os/wsl/port-proxy.mjs +190 -0
  28. package/src/settings.mjs +101 -0
  29. package/src/templates/api-help.mjs +212 -0
  30. package/src/templates/cli-commands.mjs +51 -0
  31. package/src/templates/interactive-keys.mjs +33 -0
  32. package/src/templates/ready-help.mjs +33 -0
  33. package/src/templates/section-heading.mjs +141 -0
  34. package/src/templates/table-helper.mjs +73 -0
  35. package/src/templates/wait-for-chrome.mjs +19 -0
  36. package/src/utils/ask.mjs +49 -0
  37. package/src/utils/chrome-profile-path.mjs +37 -0
  38. package/src/utils/colors.mjs +49 -0
  39. package/src/utils/env.mjs +30 -0
  40. package/src/utils/profile-id.mjs +23 -0
  41. package/src/utils/status-line.mjs +47 -0
@@ -0,0 +1,654 @@
1
+ /**
2
+ * Join Mode - attach to existing Chrome (connect to running browser).
3
+ */
4
+
5
+ import puppeteer from 'puppeteer';
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import readline from 'readline';
9
+ import { execSync } from 'child_process';
10
+ import { C, log } from '../utils/colors.mjs';
11
+ import { getChromeProfileLocation } from '../utils/chrome-profile-path.mjs';
12
+ import {
13
+ getWindowsHostForWSL,
14
+ detectWindowsChromeCanaryPath,
15
+ printCanaryInstallInstructions,
16
+ scanChromeInstances,
17
+ findProjectChrome,
18
+ findFreeDebugPort,
19
+ startChromeOnWindows,
20
+ killPuppeteerMonitorChromes,
21
+ removePortProxyIfExists,
22
+ isPortBlocked,
23
+ runWslDiagnostics,
24
+ } from '../os/wsl/index.mjs';
25
+ import { LogBuffer } from '../logging/index.mjs';
26
+ import { createHttpServer } from '../http-server.mjs';
27
+ import { setupPageMonitoring as setupPageMonitoringShared } from './page-monitoring.mjs';
28
+ import { askUserToSelectPage } from './tab-selection.mjs';
29
+ import { askYesNo } from '../utils/ask.mjs';
30
+ import { printReadyHelp, printStatusBlock, KEYS_JOIN } from '../templates/ready-help.mjs';
31
+ import { printJoinConnectedBlock, printModeHeading } from '../templates/section-heading.mjs';
32
+ import { printApiHelpTable } from '../templates/api-help.mjs';
33
+ import { createTable, printTable } from '../templates/table-helper.mjs';
34
+ import { buildWaitForChromeContent } from '../templates/wait-for-chrome.mjs';
35
+ import { writeStatusLine, clearStatusLine } from '../utils/status-line.mjs';
36
+
37
+ /**
38
+ * Run in Join Mode - attach to an existing Chrome browser
39
+ * @param {number} port - Chrome debugging port (default: 9222)
40
+ * @param {Object} options - Monitor options
41
+ */
42
+ export async function runJoinMode(port, options = {}) {
43
+ const {
44
+ realtime = false,
45
+ outputDir = process.cwd(),
46
+ paths = null,
47
+ ignorePatterns = [],
48
+ hardTimeout = 0,
49
+ httpPort = 60001,
50
+ defaultUrl = '',
51
+ host = null, // Allow explicit host override
52
+ sharedHttpState = null,
53
+ sharedHttpServer = null,
54
+ skipModeHeading = false,
55
+ } = options;
56
+
57
+ if (!skipModeHeading) printModeHeading('Join mode');
58
+ const lazyMode = !realtime;
59
+ const connectHost = host || getWindowsHostForWSL({ quiet: true });
60
+ const browserURL = `http://${connectHost}:${port}`;
61
+ // Mutable URL that will be updated if auto-port-selection changes the port
62
+ let currentBrowserURL = browserURL;
63
+
64
+ // Create LogBuffer instance for centralized buffer management
65
+ const logBuffer = new LogBuffer({
66
+ outputDir,
67
+ paths,
68
+ lazyMode,
69
+ ignorePatterns,
70
+ });
71
+
72
+ let browser = null;
73
+ let monitoredPages = [];
74
+ let cleanupDone = false;
75
+ let httpServer = null;
76
+ let isSelectingTab = false; // Flag to pause main keypress handler during tab selection
77
+ let activePageCleanup = null;
78
+ let collectingPaused = false; // When true, console/network events are not recorded
79
+
80
+ // Output counter for periodic help reminder (same block as initial Ready, every HELP_INTERVAL)
81
+ let outputCounter = 0;
82
+ const HELP_INTERVAL = 5;
83
+ function maybeShowHelp() {
84
+ outputCounter++;
85
+ if (outputCounter % HELP_INTERVAL === 0) {
86
+ printReadyHelp(httpPort, KEYS_JOIN);
87
+ }
88
+ }
89
+
90
+ // In-session help (h key) – full table with session context for Claude Code
91
+ function printHelp() {
92
+ const currentUrl = monitoredPages[0]?.url?.() || defaultUrl || '';
93
+ const profileLoc = getChromeProfileLocation(outputDir);
94
+ console.log(`${C.cyan}Join mode${C.reset} Browser: ${C.brightGreen}${currentBrowserURL}${C.reset} │ Dir: ${outputDir}`);
95
+ printApiHelpTable({
96
+ port: httpPort,
97
+ showApi: true,
98
+ showInteractive: true,
99
+ showOutputFiles: true,
100
+ context: {
101
+ consoleLog: logBuffer.CONSOLE_LOG,
102
+ networkLog: logBuffer.NETWORK_LOG,
103
+ networkDir: logBuffer.NETWORK_DIR,
104
+ cookiesDir: logBuffer.COOKIES_DIR,
105
+ domHtml: logBuffer.DOM_HTML,
106
+ screenshot: logBuffer.SCREENSHOT,
107
+ },
108
+ sessionContext: {
109
+ currentUrl: currentUrl || undefined,
110
+ profilePath: profileLoc?.path,
111
+ },
112
+ });
113
+ }
114
+
115
+ // Use shared HTTP server (started in CLI) or create our own
116
+ if (sharedHttpState && sharedHttpServer) {
117
+ httpServer = sharedHttpServer;
118
+ sharedHttpState.mode = 'join';
119
+ sharedHttpState.logBuffer = logBuffer;
120
+ sharedHttpState.getPages = () => monitoredPages;
121
+ sharedHttpState.getCollectingPaused = () => collectingPaused;
122
+ sharedHttpState.setCollectingPaused = (v) => { collectingPaused = !!v; };
123
+ sharedHttpState.getAllTabs = async () => {
124
+ if (!browser) return [];
125
+ const allPages = await browser.pages();
126
+ const isUserPage = (pg) => {
127
+ const u = pg.url();
128
+ return !u.startsWith('chrome://') && !u.startsWith('devtools://') && !u.startsWith('chrome-extension://');
129
+ };
130
+ let pages = allPages.filter(isUserPage);
131
+ const nonBlank = pages.filter(pg => pg.url() !== 'about:blank');
132
+ if (nonBlank.length > 0) pages = nonBlank;
133
+ return pages.map((pg, i) => ({ index: i + 1, url: pg.url() }));
134
+ };
135
+ sharedHttpState.switchToTab = async (index) => {
136
+ if (!browser) return { success: false, error: 'Browser not connected' };
137
+ try {
138
+ const allPages = await browser.pages();
139
+ const isUserPage = (pg) => {
140
+ const u = pg.url();
141
+ return !u.startsWith('chrome://') && !u.startsWith('devtools://') && !u.startsWith('chrome-extension://');
142
+ };
143
+ let pages = allPages.filter(isUserPage);
144
+ const nonBlank = pages.filter(pg => pg.url() !== 'about:blank');
145
+ if (nonBlank.length > 0) pages = nonBlank;
146
+ if (index < 1 || index > pages.length) return { success: false, error: `Invalid index. Use 1-${pages.length}.` };
147
+ const selectedPage = pages[index - 1];
148
+ monitoredPages = [selectedPage];
149
+ setupPageMonitoring(selectedPage, 'Page');
150
+ logBuffer.printConsoleSeparator('TAB SWITCHED');
151
+ logBuffer.printNetworkSeparator('TAB SWITCHED');
152
+ return { success: true, url: selectedPage.url() };
153
+ } catch (e) {
154
+ return { success: false, error: e.message };
155
+ }
156
+ };
157
+ } else {
158
+ httpServer = createHttpServer({
159
+ port: httpPort,
160
+ mode: 'join',
161
+ logBuffer,
162
+ getPages: () => monitoredPages,
163
+ getCollectingPaused: () => collectingPaused,
164
+ setCollectingPaused: (v) => { collectingPaused = !!v; },
165
+ });
166
+ }
167
+
168
+ async function cleanup(code = 0, closeBrowser = false) {
169
+ if (cleanupDone) return;
170
+ cleanupDone = true;
171
+
172
+ console.log('');
173
+ log.info(closeBrowser ? 'Disconnecting and closing Chrome...' : 'Disconnecting...');
174
+
175
+ try {
176
+ if (httpServer) {
177
+ await new Promise((resolve) => httpServer.close(resolve));
178
+ log.dim('HTTP server closed');
179
+ }
180
+ } catch (e) {}
181
+
182
+ try {
183
+ if (browser) {
184
+ if (closeBrowser) {
185
+ await browser.close();
186
+ log.success('Browser closed');
187
+ } else {
188
+ browser.disconnect();
189
+ log.success('Disconnected from browser (Chrome still running)');
190
+ }
191
+ }
192
+ } catch (e) {}
193
+
194
+ process.exit(code);
195
+ }
196
+
197
+ process.on('SIGINT', () => cleanup(0, false));
198
+ process.on('SIGTERM', () => cleanup(0, false));
199
+ process.on('uncaughtException', (e) => {
200
+ const msg = (e && e.message) || String(e);
201
+ if (/Execution context was destroyed|Target closed|Protocol error/.test(msg)) {
202
+ log.dim(`Navigation/context closed: ${msg.slice(0, 60)}… (continuing)`);
203
+ return;
204
+ }
205
+ log.error(`Uncaught exception: ${e.message}`);
206
+ cleanup(1);
207
+ });
208
+
209
+ if (hardTimeout > 0) {
210
+ setTimeout(() => {
211
+ log.error(`HARD TIMEOUT (${hardTimeout}ms) - forcing exit`);
212
+ cleanup(1);
213
+ }, hardTimeout);
214
+ }
215
+
216
+ function setupKeyboardInput() {
217
+ readline.emitKeypressEvents(process.stdin);
218
+ if (process.stdin.isTTY) {
219
+ process.stdin.setRawMode(true);
220
+ }
221
+
222
+ process.stdin.on('keypress', async (str, key) => {
223
+ // Ctrl+C always works
224
+ if (key.ctrl && key.name === 'c') {
225
+ cleanup(0);
226
+ return;
227
+ }
228
+
229
+ // Ignore other keys during tab selection (the selection handler will process them)
230
+ if (isSelectingTab) {
231
+ return;
232
+ }
233
+
234
+ // Shortcuts only without modifiers (no Ctrl, Shift, Win)
235
+ if (key.ctrl || key.shift || key.meta) {
236
+ return;
237
+ }
238
+
239
+ if (key.name === 'd') {
240
+ const page = monitoredPages.length > 0 ? monitoredPages[0] : null;
241
+ await logBuffer.dumpBuffersToFiles({
242
+ dumpCookies: page ? () => logBuffer.dumpCookiesFromPage(page) : null,
243
+ dumpDom: page ? () => logBuffer.dumpDomFromPage(page) : null,
244
+ dumpScreenshot: page ? () => logBuffer.dumpScreenshotFromPage(page) : null,
245
+ });
246
+ maybeShowHelp();
247
+ } else if (key.name === 'c') {
248
+ logBuffer.clearAllBuffers();
249
+ maybeShowHelp();
250
+ } else if (key.name === 'q') {
251
+ cleanup(0, false);
252
+ } else if (key.name === 'k') {
253
+ log.warn('Closing Chrome and exiting...');
254
+ cleanup(0, true);
255
+ } else if (key.name === 's') {
256
+ const stats = logBuffer.getStats();
257
+ const urls = monitoredPages.map(p => p.url()).join(', ');
258
+ printStatusBlock(stats, urls, monitoredPages.length, collectingPaused);
259
+ maybeShowHelp();
260
+ } else if (key.name === 'p') {
261
+ collectingPaused = !collectingPaused;
262
+ log.info(collectingPaused ? 'Collecting stopped (paused). Press p or curl .../start to resume.' : 'Collecting started (resumed).');
263
+ maybeShowHelp();
264
+ } else if (key.name === 't') {
265
+ // Switch tabs
266
+ await switchTabs();
267
+ maybeShowHelp();
268
+ } else if (key.name === 'h') {
269
+ // Show full help
270
+ printHelp();
271
+ }
272
+ });
273
+ }
274
+
275
+ function setupPageMonitoring(page, pageLabel) {
276
+ setupPageMonitoringShared(page, {
277
+ logBuffer,
278
+ getCollectingPaused: () => collectingPaused,
279
+ setActivePageCleanup: (fn) => { activePageCleanup = fn; },
280
+ pageLabel: pageLabel || '',
281
+ });
282
+ }
283
+
284
+ async function switchTabs() {
285
+ if (!browser) {
286
+ log.error('Browser not connected');
287
+ return;
288
+ }
289
+
290
+ try {
291
+ const allPages = await browser.pages();
292
+
293
+ // Filter out internal Chrome pages
294
+ const isUserPage = (pg) => {
295
+ const pgUrl = pg.url();
296
+ return !pgUrl.startsWith('chrome://') &&
297
+ !pgUrl.startsWith('devtools://') &&
298
+ !pgUrl.startsWith('chrome-extension://');
299
+ };
300
+ let pages = allPages.filter(isUserPage);
301
+
302
+ // If we have pages other than about:blank, filter out about:blank
303
+ const nonBlankPages = pages.filter(pg => pg.url() !== 'about:blank');
304
+ if (nonBlankPages.length > 0) {
305
+ pages = nonBlankPages;
306
+ }
307
+
308
+ if (pages.length === 0) {
309
+ log.warn('No user tabs found');
310
+ return;
311
+ }
312
+
313
+ if (pages.length === 1) {
314
+ log.info('Only one user tab available');
315
+ return;
316
+ }
317
+
318
+ isSelectingTab = true;
319
+ const selectedPage = await askUserToSelectPage(pages);
320
+ isSelectingTab = false;
321
+
322
+ if (selectedPage === null) {
323
+ log.dim('Tab switch cancelled');
324
+ return;
325
+ }
326
+
327
+ // Clear old monitoring and setup new
328
+ monitoredPages = [selectedPage];
329
+ setupPageMonitoring(selectedPage, 'Page');
330
+ log.success(`Now monitoring: ${C.brightCyan}${selectedPage.url()}${C.reset}`);
331
+
332
+ logBuffer.printConsoleSeparator('TAB SWITCHED');
333
+ logBuffer.printNetworkSeparator('TAB SWITCHED');
334
+
335
+ } catch (e) {
336
+ log.error(`Error switching tabs: ${e.message}`);
337
+ isSelectingTab = false;
338
+ }
339
+ }
340
+
341
+ // ===== MAIN =====
342
+ // Detect WSL and show setup instructions proactively
343
+ const isWSL = (() => {
344
+ try {
345
+ const release = fs.readFileSync('/proc/version', 'utf8').toLowerCase();
346
+ return release.includes('microsoft') || release.includes('wsl');
347
+ } catch { return false; }
348
+ })();
349
+
350
+ // Track actual port to use (may change if auto-selecting free port)
351
+ let actualPort = port;
352
+
353
+ if (isWSL) {
354
+ writeStatusLine(`${C.dim}Detecting Chrome...${C.reset}`);
355
+ // Chrome Canary required for launching (isolated from regular Chrome singleton)
356
+ const chromePath = detectWindowsChromeCanaryPath();
357
+
358
+ // Profile path (same logic as open-mode: WSL → Windows LOCALAPPDATA, native → project dir)
359
+ const profileLoc = getChromeProfileLocation(outputDir);
360
+ const projectName = path.basename(outputDir);
361
+
362
+ const { instances, chromeRunning } = scanChromeInstances();
363
+ const projectMatch = findProjectChrome(instances, outputDir);
364
+
365
+ // Show block only when Chrome not found / not reachable (errors)
366
+ const showSetupBlock = !projectMatch.found || !chromePath;
367
+ if (showSetupBlock) {
368
+ clearStatusLine();
369
+ console.log('');
370
+ log.section('Join (WSL)');
371
+ console.log(` ${C.cyan}Project${C.reset} ${C.brightCyan}${projectName}${C.reset} ${C.cyan}Profile${C.reset} ${C.dim}${profileLoc.path}${C.reset}`);
372
+ if (instances.length > 0) {
373
+ const line = instances.map((inst) => {
374
+ const isOurs = projectMatch.found && projectMatch.instance === inst;
375
+ const mark = isOurs ? `${C.green}*${C.reset}` : '';
376
+ return `port ${inst.port}${mark}`;
377
+ }).join(', ');
378
+ console.log(` ${C.cyan}Instances${C.reset} ${line}`);
379
+ } else if (chromeRunning) {
380
+ console.log(` ${C.yellow}Chrome running without debug port${C.reset}`);
381
+ } else {
382
+ console.log(` ${C.dim}Chrome not running${C.reset}`);
383
+ }
384
+ console.log('');
385
+ }
386
+
387
+ // Decide what to do based on status
388
+ let shouldWaitForUser = false;
389
+ let shouldLaunchChrome = false;
390
+ let waitMessageContent = '';
391
+
392
+ if (projectMatch.found && projectMatch.instance) {
393
+ // Found Chrome with our project's profile - test if actually reachable
394
+ actualPort = projectMatch.instance.port;
395
+ writeStatusLine(`${C.dim}Checking connection to port ${actualPort}...${C.reset}`);
396
+
397
+ // Actually test connectivity (don't trust bindAddress from command line)
398
+ let isReachable = false;
399
+ try {
400
+ const testUrl = `http://${connectHost}:${actualPort}/json/version`;
401
+ const response = await fetch(testUrl, { signal: AbortSignal.timeout(2000) });
402
+ isReachable = response.ok;
403
+ } catch {
404
+ isReachable = false;
405
+ }
406
+
407
+ if (isReachable) {
408
+ clearStatusLine();
409
+ // Silent – compact block shown after connect
410
+ } else {
411
+ clearStatusLine();
412
+ // Chrome exists but not reachable from WSL - port proxy needed
413
+ shouldWaitForUser = true;
414
+ waitMessageContent = [
415
+ `${C.yellow}⚠ Chrome found but not accessible from WSL${C.reset}`,
416
+ `${C.dim}Port proxy required (Chrome M113+ binds to 127.0.0.1 only)${C.reset}`,
417
+ '',
418
+ `${C.yellow}Close Chrome and re-run to retry.${C.reset}`,
419
+ ].join('\n');
420
+ }
421
+ } else if (chromePath) {
422
+ actualPort = findFreeDebugPort(instances, port);
423
+ clearStatusLine();
424
+ console.log(` ${C.yellow}No Chrome for this project.${C.reset} Port ${actualPort}, profile ${C.dim}${profileLoc.path}${C.reset}`);
425
+ shouldLaunchChrome = await askYesNo(` Launch Chrome Canary for this project?`);
426
+ } else {
427
+ // Chrome Canary not installed
428
+ actualPort = findFreeDebugPort(instances, port);
429
+ clearStatusLine();
430
+ printCanaryInstallInstructions();
431
+ log.info('Install Chrome Canary and try again.');
432
+ process.exit(1);
433
+ }
434
+
435
+ // Launch Chrome if needed
436
+ if (shouldLaunchChrome && chromePath) {
437
+ // Check if port proxy is blocking our port and remove it
438
+ if (isPortBlocked(actualPort)) {
439
+ clearStatusLine();
440
+ log.info(`Port ${actualPort} is in use, checking for port proxy...`);
441
+ const removed = removePortProxyIfExists(actualPort);
442
+ if (!removed && isPortBlocked(actualPort)) {
443
+ // Port is still blocked - try next free port
444
+ clearStatusLine();
445
+ log.warn(`Port ${actualPort} is blocked, trying next available...`);
446
+ actualPort = findFreeDebugPort(instances, actualPort + 1);
447
+ // Check the new port too
448
+ if (isPortBlocked(actualPort)) {
449
+ removePortProxyIfExists(actualPort);
450
+ }
451
+ }
452
+ }
453
+
454
+ // Kill any existing Chrome with browsermonitor profile to prevent singleton hijacking
455
+ killPuppeteerMonitorChromes();
456
+
457
+ clearStatusLine();
458
+ log.info(`Launching Chrome on port ${actualPort}...`);
459
+ const launched = startChromeOnWindows(chromePath, actualPort, profileLoc.path);
460
+ if (launched) {
461
+ writeStatusLine(`${C.dim}Waiting for Chrome to start...${C.reset}`);
462
+ await new Promise(r => setTimeout(r, 2500));
463
+ clearStatusLine();
464
+ } else {
465
+ log.error('Failed to launch Chrome automatically');
466
+ shouldWaitForUser = true;
467
+ waitMessageContent = `${C.yellow}Failed to launch Chrome automatically.${C.reset}`;
468
+ }
469
+ }
470
+
471
+ // Wait for user if needed
472
+ if (shouldWaitForUser) {
473
+ clearStatusLine();
474
+ const content = buildWaitForChromeContent(waitMessageContent);
475
+ const table = createTable({ colWidths: [72], tableOpts: { wordWrap: true } });
476
+ table.push([content]);
477
+ printTable(table);
478
+ await new Promise((resolve) => {
479
+ process.stdin.setRawMode(false);
480
+ process.stdin.resume();
481
+ process.stdin.once('data', () => {
482
+ resolve();
483
+ });
484
+ });
485
+ console.log('');
486
+ }
487
+
488
+ }
489
+
490
+ // Use actual port for connection (may have been changed by auto-detection)
491
+ const finalBrowserURL = `http://${connectHost}:${actualPort}`;
492
+ currentBrowserURL = finalBrowserURL;
493
+
494
+ if (realtime) {
495
+ fs.writeFileSync(logBuffer.CONSOLE_LOG, '');
496
+ logBuffer.clearNetworkDir();
497
+ }
498
+
499
+ // HTTP server is already started via createHttpServer above
500
+
501
+ try {
502
+ writeStatusLine(`${C.dim}Connecting to browser...${C.reset}`);
503
+ // Connect to existing Chrome instance via Chrome DevTools Protocol (CDP).
504
+ // defaultViewport: null preserves the browser's actual viewport size.
505
+ // Without this, Puppeteer would resize the page to its default (800x600),
506
+ // causing the page content to shrink unexpectedly.
507
+ browser = await puppeteer.connect({ browserURL: finalBrowserURL, defaultViewport: null });
508
+
509
+ writeStatusLine(`${C.dim}Loading pages...${C.reset}`);
510
+ const allPages = await browser.pages();
511
+
512
+ // Filter out internal/dev pages that are not useful for monitoring
513
+ const isUserPage = (page) => {
514
+ const url = page.url();
515
+ const title = page.title ? page.title() : '';
516
+
517
+ // Skip Chrome internal pages
518
+ if (url.startsWith('chrome://')) return false;
519
+ if (url.startsWith('chrome-extension://')) return false;
520
+ if (url.startsWith('devtools://')) return false;
521
+ if (url === 'about:blank') return false;
522
+
523
+ // Skip React/Redux DevTools and similar extensions
524
+ if (url.includes('react-devtools') || url.includes('redux-devtools')) return false;
525
+ if (url.includes('__react_devtools__')) return false;
526
+
527
+ // Skip extension pages by pattern
528
+ if (/^moz-extension:\/\//.test(url)) return false; // Firefox extensions
529
+ if (/^extension:\/\//.test(url)) return false;
530
+
531
+ return true;
532
+ };
533
+
534
+ const userPages = allPages.filter(isUserPage);
535
+ const pages = userPages.length > 0 ? userPages : allPages; // Fallback to all if no user pages
536
+
537
+ if (pages.length === 0) {
538
+ clearStatusLine();
539
+ log.error('No tabs found in browser');
540
+ cleanup(1);
541
+ return;
542
+ }
543
+
544
+ let selectedPage;
545
+ if (pages.length === 1) {
546
+ selectedPage = pages[0];
547
+ clearStatusLine();
548
+ } else {
549
+ clearStatusLine();
550
+ log.section('Tab Selection');
551
+ console.log(` ${C.cyan}Tabs${C.reset} ${pages.length} – select which to monitor:`);
552
+ selectedPage = await askUserToSelectPage(pages);
553
+ if (!selectedPage) {
554
+ log.dim('Using first tab.');
555
+ selectedPage = pages[0];
556
+ }
557
+ }
558
+
559
+ monitoredPages = [selectedPage];
560
+ setupPageMonitoring(selectedPage, 'Page');
561
+
562
+ if (isWSL) {
563
+ printJoinConnectedBlock(connectHost, selectedPage.url());
564
+ }
565
+
566
+ // Watch for new tabs
567
+ browser.on('targetcreated', async (target) => {
568
+ if (target.type() === 'page') {
569
+ const newPage = await target.page();
570
+ if (newPage) log.dim(`New tab: ${newPage.url()}`);
571
+ }
572
+ });
573
+
574
+ logBuffer.printConsoleSeparator('CONNECTED - Listening for console output');
575
+ logBuffer.printNetworkSeparator('CONNECTED - Listening for network requests');
576
+
577
+ clearStatusLine(true);
578
+ // Ready block from template (same as periodic reminder)
579
+ printReadyHelp(httpPort, KEYS_JOIN);
580
+ setupKeyboardInput();
581
+
582
+ await new Promise(() => {});
583
+ } catch (e) {
584
+ clearStatusLine(true);
585
+ if (e.message.includes('ECONNREFUSED') || e.message.includes('fetch failed') || e.message.includes('ETIMEDOUT') || e.message.includes('timeout')) {
586
+ console.log('');
587
+ log.error(`Cannot connect to Chrome at ${C.brightRed}${currentBrowserURL}${C.reset}`);
588
+ console.log('');
589
+
590
+ // Check if we're in WSL - if so, run diagnostics
591
+ const isWSL = (() => {
592
+ try {
593
+ const release = fs.readFileSync('/proc/version', 'utf8').toLowerCase();
594
+ return release.includes('microsoft') || release.includes('wsl');
595
+ } catch { return false; }
596
+ })();
597
+
598
+ if (isWSL) {
599
+ // Run comprehensive WSL diagnostics
600
+ const diagResult = await runWslDiagnostics(port, connectHost);
601
+
602
+ // Handle port proxy conflict automatically
603
+ if (diagResult.hasPortProxyConflict) {
604
+ console.log('');
605
+ console.log(`${C.bold}${C.green}═══════════════════════════════════════════════════════════════════════════════${C.reset}`);
606
+ console.log(`${C.bold}${C.green} AUTOMATIC FIX${C.reset}`);
607
+ console.log(`${C.bold}${C.green}═══════════════════════════════════════════════════════════════════════════════${C.reset}`);
608
+ console.log('');
609
+
610
+ const shouldFix = await askYesNo('Do you want me to fix this automatically? (remove port proxy, restart Chrome)');
611
+
612
+ if (shouldFix) {
613
+ const fixPort = diagResult.actualPort || port;
614
+
615
+ console.log(`${C.cyan}[1/2]${C.reset} Removing port proxy for port ${fixPort}...`);
616
+ try {
617
+ execSync(`netsh.exe interface portproxy delete v4tov4 listenport=${fixPort} listenaddress=0.0.0.0`, { encoding: 'utf8', timeout: 5000 });
618
+ console.log(` ${C.green}✓${C.reset} Port proxy removed`);
619
+ } catch (err) {
620
+ console.log(` ${C.yellow}!${C.reset} Could not remove port proxy (may need admin): ${err.message}`);
621
+ }
622
+
623
+ console.log(`${C.cyan}[2/2]${C.reset} Stopping Chrome...`);
624
+ try {
625
+ killPuppeteerMonitorChromes(true); // Only kill browsermonitor Chrome, not user's browser!
626
+ console.log(` ${C.green}✓${C.reset} Chrome stopped`);
627
+ } catch (err) {
628
+ console.log(` ${C.yellow}!${C.reset} Could not stop Chrome: ${err.message}`);
629
+ }
630
+
631
+ console.log('');
632
+ console.log(`${C.green}Fix applied!${C.reset} Please run browsermonitor again.`);
633
+ console.log(`${C.dim}Chrome will now bind to 0.0.0.0 correctly (no port proxy needed).${C.reset}`);
634
+ console.log('');
635
+ process.exit(0);
636
+ }
637
+ }
638
+ } else {
639
+ // Non-WSL: show basic help
640
+ console.log(` ${C.yellow}Make sure Chrome is running with remote debugging enabled:${C.reset}`);
641
+ console.log(` ${C.dim}Windows:${C.reset} ${C.cyan}chrome.exe --remote-debugging-port=${port}${C.reset}`);
642
+ console.log(` ${C.dim}Linux:${C.reset} ${C.cyan}google-chrome --remote-debugging-port=${port}${C.reset}`);
643
+ console.log(` ${C.dim}Mac:${C.reset} ${C.cyan}/Applications/Google\\ Chrome.app/Contents/MacOS/Google\\ Chrome --remote-debugging-port=${port}${C.reset}`);
644
+ console.log('');
645
+ console.log(` ${C.yellow}If connecting from remote server, create SSH reverse tunnel first:${C.reset}`);
646
+ console.log(` ${C.cyan}ssh -R ${port}:localhost:${port} user@this-server${C.reset}`);
647
+ console.log('');
648
+ }
649
+ } else {
650
+ log.error(e.message);
651
+ }
652
+ process.exit(1);
653
+ }
654
+ }