@romanmatena/browsermonitor 2.0.0 → 2.1.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.
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Cleanup function and signal handler installation.
3
+ */
4
+
5
+ import { log } from '../../utils/colors.mjs';
6
+
7
+ /**
8
+ * Create cleanup function and install signal/error handlers.
9
+ * @param {Object} deps
10
+ * @param {() => import('puppeteer').Browser|null} deps.getBrowser
11
+ * @param {() => import('http').Server|null} deps.getHttpServer
12
+ * @param {(browser: import('puppeteer').Browser, closeBrowser: boolean) => Promise<void>} deps.closeBrowserFn
13
+ * @param {() => void} [deps.afterCleanup] - e.g. PID file removal
14
+ * @param {number} [deps.hardTimeout=0]
15
+ * @returns {{ cleanup: (code?: number, closeBrowser?: boolean) => Promise<void> }}
16
+ */
17
+ export function createCleanup({ getBrowser, getHttpServer, closeBrowserFn, afterCleanup, hardTimeout = 0 }) {
18
+ let cleanupDone = false;
19
+
20
+ async function cleanup(code = 0, closeBrowser = false) {
21
+ if (cleanupDone) return;
22
+ cleanupDone = true;
23
+
24
+ console.log('');
25
+ log.info(closeBrowser ? 'Cleaning up and closing Chrome...' : 'Cleaning up...');
26
+
27
+ try {
28
+ const httpServer = getHttpServer();
29
+ if (httpServer) {
30
+ await new Promise((resolve) => httpServer.close(resolve));
31
+ log.dim('HTTP server closed');
32
+ }
33
+ } catch (e) {
34
+ log.error(`Error closing HTTP server: ${e.message}`);
35
+ }
36
+
37
+ try {
38
+ const browser = getBrowser();
39
+ if (browser) {
40
+ await closeBrowserFn(browser, closeBrowser);
41
+ }
42
+ } catch (e) {
43
+ log.error(`Error closing browser: ${e.message}`);
44
+ }
45
+
46
+ if (afterCleanup) {
47
+ try { afterCleanup(); } catch {}
48
+ }
49
+
50
+ process.exit(code);
51
+ }
52
+
53
+ // Signal handlers
54
+ process.on('SIGINT', () => {
55
+ console.log('');
56
+ log.dim('Received SIGINT (Ctrl+C)');
57
+ cleanup(0, false);
58
+ });
59
+
60
+ process.on('SIGTERM', () => {
61
+ console.log('');
62
+ log.dim('Received SIGTERM');
63
+ cleanup(0, false);
64
+ });
65
+
66
+ process.on('uncaughtException', (e) => {
67
+ const msg = (e && e.message) || String(e);
68
+ if (/Execution context was destroyed|Target closed|Protocol error/.test(msg)) {
69
+ log.dim(`Navigation/context closed: ${msg.slice(0, 60)}… (continuing)`);
70
+ return;
71
+ }
72
+ log.error(`Uncaught exception: ${e.message}`);
73
+ console.error(e.stack);
74
+ cleanup(1);
75
+ });
76
+
77
+ process.on('unhandledRejection', (e) => {
78
+ const msg = (e && e.message) || String(e);
79
+ if (/Execution context was destroyed|Target closed|Protocol error/.test(msg)) {
80
+ log.dim(`Navigation/context closed: ${msg.slice(0, 60)}… (continuing)`);
81
+ return;
82
+ }
83
+ log.error(`Unhandled rejection: ${e}`);
84
+ cleanup(1);
85
+ });
86
+
87
+ // Hard timeout
88
+ if (hardTimeout > 0) {
89
+ setTimeout(() => {
90
+ log.error(`HARD TIMEOUT (${hardTimeout}ms) - forcing exit`);
91
+ cleanup(1);
92
+ }, hardTimeout);
93
+ }
94
+
95
+ return { cleanup };
96
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Help display factories: periodic help reminder + full in-session help.
3
+ */
4
+
5
+ import { C } from '../../utils/colors.mjs';
6
+ import { printReadyHelp } from '../../templates/ready-help.mjs';
7
+ import { printApiHelpTable } from '../../templates/api-help.mjs';
8
+
9
+ /**
10
+ * Create help display functions bound to session state.
11
+ * @param {Object} deps
12
+ * @param {number} deps.httpPort
13
+ * @param {string} deps.keysVariant - KEYS_OPEN or KEYS_JOIN constant
14
+ * @param {Object} deps.logBuffer
15
+ * @param {string} deps.outputDir
16
+ * @param {string} deps.modeLabel - 'Launch mode' or 'Join mode'
17
+ * @param {() => string} deps.getCurrentUrl
18
+ * @param {() => string|null} deps.getProfilePath
19
+ * @param {() => string|null} deps.getBrowserUrl - join shows browserURL, open null
20
+ * @returns {{ maybeShowHelp: () => void, printHelp: () => void }}
21
+ */
22
+ export function createHelpHandlers({
23
+ httpPort, keysVariant, logBuffer, outputDir,
24
+ modeLabel, getCurrentUrl, getProfilePath, getBrowserUrl,
25
+ }) {
26
+ let outputCounter = 0;
27
+ const HELP_INTERVAL = 5;
28
+
29
+ function maybeShowHelp() {
30
+ outputCounter++;
31
+ if (outputCounter % HELP_INTERVAL === 0) {
32
+ printReadyHelp(httpPort, keysVariant);
33
+ }
34
+ }
35
+
36
+ function printHelp() {
37
+ const currentUrl = getCurrentUrl();
38
+ const browserUrl = getBrowserUrl();
39
+ const connInfo = browserUrl
40
+ ? `Browser: ${C.brightGreen}${browserUrl}${C.reset} │ Dir: ${outputDir}`
41
+ : `URL: ${C.brightGreen}${currentUrl}${C.reset} │ Dir: ${outputDir}`;
42
+ console.log(`${C.cyan}${modeLabel}${C.reset} ${connInfo}`);
43
+ printApiHelpTable({
44
+ port: httpPort,
45
+ showApi: true,
46
+ showInteractive: true,
47
+ showOutputFiles: true,
48
+ context: {
49
+ consoleLog: logBuffer.CONSOLE_LOG,
50
+ networkLog: logBuffer.NETWORK_LOG,
51
+ networkDir: logBuffer.NETWORK_DIR,
52
+ cookiesDir: logBuffer.COOKIES_DIR,
53
+ domHtml: logBuffer.DOM_HTML,
54
+ screenshot: logBuffer.SCREENSHOT,
55
+ },
56
+ sessionContext: {
57
+ currentUrl: currentUrl || undefined,
58
+ profilePath: getProfilePath(),
59
+ },
60
+ });
61
+ }
62
+
63
+ return { maybeShowHelp, printHelp };
64
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Wire sharedHttpState callbacks to mode-local state.
3
+ */
4
+
5
+ import { filterUserPages } from './user-page-filter.mjs';
6
+
7
+ /**
8
+ * @param {Object} sharedHttpState - mutable state object from CLI
9
+ * @param {Object} deps
10
+ * @param {string} deps.mode - 'launch' | 'join'
11
+ * @param {Object} deps.logBuffer
12
+ * @param {() => import('puppeteer').Page[]} deps.getMonitoredPages
13
+ * @param {() => boolean} deps.getCollectingPaused
14
+ * @param {(v: boolean) => void} deps.setCollectingPaused
15
+ * @param {() => import('puppeteer').Browser|null} deps.getBrowser
16
+ * @param {(page: import('puppeteer').Page, label?: string) => void} deps.setupPageMonitoring
17
+ * @param {(page: import('puppeteer').Page) => void} deps.onTabSwitched - mode-specific side effect
18
+ */
19
+ export function wireHttpState(sharedHttpState, {
20
+ mode, logBuffer, getMonitoredPages, getCollectingPaused, setCollectingPaused,
21
+ getBrowser, setupPageMonitoring, onTabSwitched,
22
+ }) {
23
+ sharedHttpState.mode = mode;
24
+ sharedHttpState.logBuffer = logBuffer;
25
+ sharedHttpState.getPages = getMonitoredPages;
26
+ sharedHttpState.getCollectingPaused = getCollectingPaused;
27
+ sharedHttpState.setCollectingPaused = setCollectingPaused;
28
+
29
+ sharedHttpState.getAllTabs = async () => {
30
+ const browser = getBrowser();
31
+ if (!browser) return [];
32
+ const allPages = await browser.pages();
33
+ return filterUserPages(allPages).map((p, i) => ({ index: i + 1, url: p.url() }));
34
+ };
35
+
36
+ sharedHttpState.switchToTab = async (index) => {
37
+ const browser = getBrowser();
38
+ if (!browser) return { success: false, error: 'Browser not connected' };
39
+ try {
40
+ const allPages = await browser.pages();
41
+ const pages = filterUserPages(allPages);
42
+ if (index < 1 || index > pages.length) {
43
+ return { success: false, error: `Invalid index. Use 1-${pages.length}.` };
44
+ }
45
+ const selectedPage = pages[index - 1];
46
+ onTabSwitched(selectedPage);
47
+ setupPageMonitoring(selectedPage);
48
+ logBuffer.printConsoleSeparator('TAB SWITCHED');
49
+ logBuffer.printNetworkSeparator('TAB SWITCHED');
50
+ return { success: true, url: selectedPage.url() };
51
+ } catch (e) {
52
+ return { success: false, error: e.message };
53
+ }
54
+ };
55
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Shared utilities for open-mode and join-mode.
3
+ */
4
+
5
+ export { filterUserPages } from './user-page-filter.mjs';
6
+ export { createSetupPageMonitoring } from './monitoring-wrapper.mjs';
7
+ export { createHelpHandlers } from './help.mjs';
8
+ export { wireHttpState } from './http-state-setup.mjs';
9
+ export { createSwitchTabs } from './tab-switching.mjs';
10
+ export { createCleanup } from './cleanup.mjs';
11
+ export { setupKeyboardInput } from './keyboard-handler.mjs';
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Interactive keyboard input handler (raw mode stdin).
3
+ * Shared between open-mode and join-mode.
4
+ */
5
+
6
+ import readline from 'readline';
7
+ import { log } from '../../utils/colors.mjs';
8
+ import { printStatusBlock } from '../../templates/ready-help.mjs';
9
+
10
+ /**
11
+ * Setup keyboard input for lazy (buffered) mode.
12
+ * @param {Object} deps
13
+ * @param {() => import('puppeteer').Page|null} deps.getActivePage
14
+ * @param {Object} deps.logBuffer
15
+ * @param {(code?: number, closeBrowser?: boolean) => Promise<void>} deps.cleanup
16
+ * @param {() => Promise<void>} deps.switchTabs
17
+ * @param {() => void} deps.printHelp
18
+ * @param {() => void} deps.maybeShowHelp
19
+ * @param {() => boolean} deps.isSelectingTab
20
+ * @param {() => boolean} deps.getCollectingPaused
21
+ * @param {(v: boolean) => void} deps.setCollectingPaused
22
+ * @param {() => Promise<{ currentUrl: string, tabCount: number }>} deps.getStatusInfo
23
+ */
24
+ export function setupKeyboardInput({
25
+ getActivePage, logBuffer, cleanup, switchTabs,
26
+ printHelp, maybeShowHelp, isSelectingTab,
27
+ getCollectingPaused, setCollectingPaused, getStatusInfo,
28
+ }) {
29
+ readline.emitKeypressEvents(process.stdin);
30
+ if (process.stdin.isTTY) {
31
+ process.stdin.setRawMode(true);
32
+ }
33
+
34
+ process.stdin.on('keypress', async (str, key) => {
35
+ if (key.ctrl && key.name === 'c') {
36
+ cleanup(0);
37
+ return;
38
+ }
39
+
40
+ // Ignore keys during tab selection
41
+ if (isSelectingTab()) return;
42
+
43
+ // Shortcuts only without modifiers
44
+ if (key.ctrl || key.shift || key.meta) return;
45
+
46
+ if (key.name === 'd') {
47
+ const page = getActivePage();
48
+ await logBuffer.dumpBuffersToFiles({
49
+ dumpCookies: page ? () => logBuffer.dumpCookiesFromPage(page) : null,
50
+ dumpDom: page ? () => logBuffer.dumpDomFromPage(page) : null,
51
+ dumpScreenshot: page ? () => logBuffer.dumpScreenshotFromPage(page) : null,
52
+ });
53
+ maybeShowHelp();
54
+ } else if (key.name === 'c') {
55
+ logBuffer.clearAllBuffers();
56
+ maybeShowHelp();
57
+ } else if (key.name === 'q') {
58
+ cleanup(0, false);
59
+ } else if (key.name === 'k') {
60
+ log.warn('Closing Chrome and exiting...');
61
+ cleanup(0, true);
62
+ } else if (key.name === 's') {
63
+ const { currentUrl, tabCount } = await getStatusInfo();
64
+ const stats = logBuffer.getStats();
65
+ printStatusBlock(stats, currentUrl, tabCount, getCollectingPaused());
66
+ maybeShowHelp();
67
+ } else if (key.name === 'p') {
68
+ const paused = !getCollectingPaused();
69
+ setCollectingPaused(paused);
70
+ log.info(paused ? 'Collecting stopped (paused). Press p or curl .../start to resume.' : 'Collecting started (resumed).');
71
+ maybeShowHelp();
72
+ } else if (key.name === 't') {
73
+ await switchTabs();
74
+ maybeShowHelp();
75
+ } else if (key.name === 'h') {
76
+ printHelp();
77
+ }
78
+ });
79
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Factory for page monitoring setup, bound to local session state.
3
+ * Manages listener lifecycle: each call to setupPageMonitoring automatically
4
+ * cleans up listeners from the previously monitored page.
5
+ */
6
+
7
+ import { setupPageMonitoring as setupPageMonitoringShared } from '../page-monitoring.mjs';
8
+
9
+ /**
10
+ * Create a setupPageMonitoring function bound to the current session's state.
11
+ * @param {Object} deps
12
+ * @param {Object} deps.logBuffer
13
+ * @param {() => boolean} deps.getCollectingPaused
14
+ * @returns {{ setupPageMonitoring: (page: import('puppeteer').Page, pageLabel?: string) => void, cleanupActivePageListeners: () => void }}
15
+ */
16
+ export function createSetupPageMonitoring({ logBuffer, getCollectingPaused }) {
17
+ let activePageCleanup = null;
18
+
19
+ function cleanupActivePageListeners() {
20
+ if (activePageCleanup) {
21
+ activePageCleanup();
22
+ activePageCleanup = null;
23
+ }
24
+ }
25
+
26
+ function setupPageMonitoring(targetPage, pageLabel = '') {
27
+ // Remove listeners from previous page before attaching to new one
28
+ cleanupActivePageListeners();
29
+
30
+ setupPageMonitoringShared(targetPage, {
31
+ logBuffer,
32
+ getCollectingPaused,
33
+ setActivePageCleanup: (fn) => { activePageCleanup = fn; },
34
+ pageLabel,
35
+ });
36
+ }
37
+
38
+ return { setupPageMonitoring, cleanupActivePageListeners };
39
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Interactive tab switching (t key).
3
+ */
4
+
5
+ import { C, log } from '../../utils/colors.mjs';
6
+ import { askUserToSelectPage } from '../tab-selection.mjs';
7
+ import { filterUserPages } from './user-page-filter.mjs';
8
+
9
+ /**
10
+ * Create interactive tab-switch function.
11
+ * @param {Object} deps
12
+ * @param {() => import('puppeteer').Browser|null} deps.getBrowser
13
+ * @param {(page: import('puppeteer').Page) => void} deps.onTabSwitched
14
+ * @param {(page: import('puppeteer').Page, label?: string) => void} deps.setupPageMonitoring
15
+ * @param {Object} deps.logBuffer
16
+ * @param {(v: boolean) => void} deps.setSelectingTab
17
+ * @returns {() => Promise<void>}
18
+ */
19
+ export function createSwitchTabs({ getBrowser, onTabSwitched, setupPageMonitoring, logBuffer, setSelectingTab }) {
20
+ return async () => {
21
+ const browser = getBrowser();
22
+ if (!browser) {
23
+ log.error('Browser not connected');
24
+ return;
25
+ }
26
+
27
+ try {
28
+ const allPages = await browser.pages();
29
+ const pages = filterUserPages(allPages);
30
+
31
+ if (pages.length === 0) {
32
+ log.warn('No user tabs found');
33
+ return;
34
+ }
35
+
36
+ if (pages.length === 1) {
37
+ log.info('Only one user tab available');
38
+ return;
39
+ }
40
+
41
+ setSelectingTab(true);
42
+ const selectedPage = await askUserToSelectPage(pages);
43
+ setSelectingTab(false);
44
+
45
+ if (selectedPage === null) {
46
+ log.dim('Tab switch cancelled');
47
+ return;
48
+ }
49
+
50
+ onTabSwitched(selectedPage);
51
+ setupPageMonitoring(selectedPage);
52
+ log.success(`Now monitoring: ${C.brightCyan}${selectedPage.url()}${C.reset}`);
53
+
54
+ logBuffer.printConsoleSeparator('TAB SWITCHED');
55
+ logBuffer.printNetworkSeparator('TAB SWITCHED');
56
+ } catch (e) {
57
+ log.error(`Error switching tabs: ${e.message}`);
58
+ setSelectingTab(false);
59
+ }
60
+ };
61
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Filter browser internal / devtools pages from a page list.
3
+ * Single source of truth for "is this a user page?" across all modes.
4
+ *
5
+ * Strips: chrome://, devtools://, chrome-extension://, moz-extension://,
6
+ * extension://, React/Redux devtools background pages.
7
+ * Also strips about:blank unless it's the only remaining page.
8
+ *
9
+ * @param {import('puppeteer').Page[]} allPages
10
+ * @returns {import('puppeteer').Page[]}
11
+ */
12
+ export function filterUserPages(allPages) {
13
+ const isUserPage = (p) => {
14
+ const u = p.url();
15
+ if (u.startsWith('chrome://')) return false;
16
+ if (u.startsWith('devtools://')) return false;
17
+ if (u.startsWith('chrome-extension://')) return false;
18
+ if (u.startsWith('moz-extension://')) return false;
19
+ if (u.startsWith('extension://')) return false;
20
+ if (u.includes('react-devtools') || u.includes('redux-devtools')) return false;
21
+ if (u.includes('__react_devtools__')) return false;
22
+ return true;
23
+ };
24
+ let pages = allPages.filter(isUserPage);
25
+ const nonBlank = pages.filter(p => p.url() !== 'about:blank');
26
+ if (nonBlank.length > 0) pages = nonBlank;
27
+ return pages;
28
+ }
@@ -41,7 +41,6 @@ export function getLastCmdStderrAndClear() {
41
41
  _lastCmdStderrLines = [];
42
42
  return lines;
43
43
  }
44
- import { getWslDistroName } from './detect.mjs';
45
44
 
46
45
  // Cache for LOCALAPPDATA path
47
46
  let _cachedLocalAppData = null;
@@ -329,12 +328,9 @@ export function startChromeOnWindows(chromePath, port, profileDir) {
329
328
  );
330
329
  } catch { /* ignore */ }
331
330
 
332
- const args = [
333
- `--remote-debugging-port=${port}`,
334
- `--user-data-dir="${profileDir}"`,
335
- ].join("','");
336
-
337
- const psCommand = `Start-Process -FilePath '${chromePath}' -ArgumentList '${args}'`;
331
+ // Escape single quotes for PowerShell literal strings (double them)
332
+ const psEsc = (s) => s.replace(/'/g, "''");
333
+ const psCommand = `Start-Process -FilePath '${psEsc(chromePath)}' -ArgumentList @('--remote-debugging-port=${port}','--user-data-dir=${psEsc(profileDir)}')`;
338
334
  execSync(`powershell.exe -NoProfile -Command "${psCommand}"`, { encoding: 'utf8', timeout: 10000 });
339
335
 
340
336
  log.dim('Waiting for Chrome to start...');
@@ -472,32 +468,3 @@ export function killPuppeteerMonitorChromes(usePowerShell = false) {
472
468
  }
473
469
  }
474
470
 
475
- /**
476
- * Legacy wrapper for backward compatibility.
477
- */
478
- export function checkChromeRunning() {
479
- const { instances, chromeRunning } = scanChromeInstances();
480
- if (!chromeRunning) {
481
- return { running: false, withDebugPort: false, debugPort: null };
482
- }
483
- if (instances.length === 0) {
484
- return { running: true, withDebugPort: false, debugPort: null };
485
- }
486
- return {
487
- running: true,
488
- withDebugPort: true,
489
- debugPort: instances[0].port,
490
- instances,
491
- };
492
- }
493
-
494
- /**
495
- * Legacy wrapper for backward compatibility.
496
- */
497
- export function launchChromeFromWSL(chromePath, port) {
498
- const distroName = getWslDistroName();
499
- const wslPath = process.cwd();
500
- const winPath = wslPath.replace(/\//g, '\\');
501
- const profileDir = `\\\\wsl$\\${distroName}${winPath}\\.browsermonitor-profile`;
502
- return startChromeOnWindows(chromePath, port, profileDir);
503
- }
@@ -54,15 +54,3 @@ export function getWindowsHostForWSL(opts = {}) {
54
54
  return 'localhost';
55
55
  }
56
56
 
57
- /**
58
- * Convert WSL path to Windows UNC path.
59
- * Note: UNC paths don't work well with Chrome profiles due to singleton detection.
60
- *
61
- * @param {string} wslPath - WSL path (e.g., /srv/project)
62
- * @returns {string} Windows UNC path (e.g., \\wsl$\Ubuntu\srv\project)
63
- */
64
- export function wslToWindowsPath(wslPath) {
65
- const distroName = getWslDistroName();
66
- const winPath = wslPath.replace(/\//g, '\\');
67
- return `\\\\wsl$\\${distroName}${winPath}`;
68
- }
@@ -10,7 +10,6 @@ export {
10
10
  isWsl,
11
11
  getWslDistroName,
12
12
  getWindowsHostForWSL,
13
- wslToWindowsPath,
14
13
  } from './detect.mjs';
15
14
 
16
15
  // Chrome detection and launch
@@ -26,8 +25,6 @@ export {
26
25
  findFreeDebugPort,
27
26
  startChromeOnWindows,
28
27
  killPuppeteerMonitorChromes,
29
- checkChromeRunning,
30
- launchChromeFromWSL,
31
28
  } from './chrome.mjs';
32
29
 
33
30
  // Port proxy management
package/src/settings.mjs CHANGED
@@ -16,7 +16,7 @@ export const PID_FILE = 'browsermonitor.pid';
16
16
 
17
17
  /** Default settings for new projects */
18
18
  export const DEFAULT_SETTINGS = {
19
- defaultUrl: 'https://localhost:4000/',
19
+ defaultUrl: 'about:blank',
20
20
  headless: false,
21
21
  navigationTimeout: 60000,
22
22
  ignorePatterns: [],
@@ -30,6 +30,7 @@ export const DEFAULT_SETTINGS = {
30
30
  * @returns {Object} All paths used by browsermonitor
31
31
  */
32
32
  export function getPaths(projectRoot) {
33
+ projectRoot = path.resolve(projectRoot);
33
34
  const bmDir = path.join(projectRoot, BROWSERMONITOR_DIR);
34
35
  const puppeteerDir = path.join(bmDir, PUPPETEER_DIR);
35
36
  return {
@@ -49,32 +50,33 @@ export function getPaths(projectRoot) {
49
50
  }
50
51
 
51
52
  /**
52
- * Check if .browsermonitor/ directory exists (first-run detection).
53
+ * Check if settings.json exists (first-run detection).
53
54
  * @param {string} projectRoot
54
55
  * @returns {boolean}
55
56
  */
56
57
  export function isInitialized(projectRoot) {
57
- const { bmDir } = getPaths(projectRoot);
58
- return fs.existsSync(bmDir);
58
+ const { settingsFile } = getPaths(projectRoot);
59
+ return fs.existsSync(settingsFile);
59
60
  }
60
61
 
61
62
  /**
62
- * Load settings from .browsermonitor/settings.json, merged with defaults.
63
+ * Load settings from .browsermonitor/settings.json.
64
+ * Returns only what is actually saved — no default merging.
65
+ * Missing keys mean "not configured yet" so the caller can prompt the user.
63
66
  * @param {string} projectRoot
64
- * @returns {Object} Merged settings
67
+ * @returns {Object} Saved settings (may be empty)
65
68
  */
66
69
  export function loadSettings(projectRoot) {
67
70
  const { settingsFile } = getPaths(projectRoot);
68
71
  try {
69
72
  if (fs.existsSync(settingsFile)) {
70
73
  const raw = fs.readFileSync(settingsFile, 'utf8');
71
- const saved = JSON.parse(raw);
72
- return { ...DEFAULT_SETTINGS, ...saved };
74
+ return JSON.parse(raw);
73
75
  }
74
76
  } catch {
75
- // Ignore parse errors, fall through to defaults
77
+ // Ignore parse errors, fall through to empty
76
78
  }
77
- return { ...DEFAULT_SETTINGS };
79
+ return {};
78
80
  }
79
81
 
80
82
  /**
@@ -43,6 +43,7 @@ const API_USAGE =
43
43
  * @param {Object} options
44
44
  * @param {number} [options.port=60001]
45
45
  * @param {string} [options.host='127.0.0.1']
46
+ * @param {string} [options.url] - Browser URL being monitored (shown in header)
46
47
  * @param {boolean} [options.showApi=true]
47
48
  * @param {boolean} [options.showInteractive=false] - Interactive help (keyboard shortcuts, human interaction)
48
49
  * @param {boolean} [options.showOutputFiles=true]
@@ -54,6 +55,7 @@ export function printApiHelpTable(options = {}) {
54
55
  const {
55
56
  port = 60001,
56
57
  host = '127.0.0.1',
58
+ url = null,
57
59
  showApi = true,
58
60
  showInteractive = false,
59
61
  showOutputFiles = true,
@@ -66,7 +68,8 @@ export function printApiHelpTable(options = {}) {
66
68
 
67
69
  if (showApi) {
68
70
  if (!noLeadingNewline) console.log('');
69
- printSectionHeading('HTTP API', INDENT);
71
+ const heading = url ? `HTTP API ${C.dim}→${C.reset} ${C.brightCyan}${url}${C.reset}` : 'HTTP API';
72
+ printSectionHeading(heading, INDENT);
70
73
 
71
74
  const hasSession = sessionContext && (sessionContext.currentUrl || sessionContext.profilePath);
72
75
  const apiTable = createTable({
@@ -133,7 +136,7 @@ export function printApiHelpTable(options = {}) {
133
136
 
134
137
  // Same info without tables, structured for LLM (no box-drawing / table chars; no keyboard shortcuts)
135
138
  if (showApi || showOutputFiles) {
136
- printApiHelpForLlm({ port, host, showApi, showOutputFiles, context, sessionContext });
139
+ printApiHelpForLlm({ port, host, url, showApi, showOutputFiles, context, sessionContext });
137
140
  }
138
141
 
139
142
  console.log('');
@@ -147,6 +150,7 @@ function printApiHelpForLlm(options = {}) {
147
150
  const {
148
151
  port = 60001,
149
152
  host = '127.0.0.1',
153
+ url = null,
150
154
  showApi = true,
151
155
  showOutputFiles = true,
152
156
  context = null,
@@ -168,6 +172,9 @@ function printApiHelpForLlm(options = {}) {
168
172
  console.log('');
169
173
  console.log('--- LLM reference (plain text, no tables) ---');
170
174
  console.log('');
175
+ if (url) {
176
+ console.log('Browser URL: ' + url);
177
+ }
171
178
  console.log('Base URL: ' + baseUrl);
172
179
  if (sessionContext?.currentUrl) {
173
180
  console.log('Monitored URL: ' + sessionContext.currentUrl);
@@ -201,12 +208,3 @@ function printApiHelpForLlm(options = {}) {
201
208
  console.log('');
202
209
  }
203
210
 
204
- /**
205
- * One-line API hint (for Ready bar and intro).
206
- * @param {number} port
207
- * @param {string} [host='127.0.0.1']
208
- */
209
- export function apiHintOneLine(port, host = '127.0.0.1') {
210
- const base = `http://${host}:${port}`;
211
- return `${C.dim}curl ${base}/dump ${base}/status ${base}/stop ${base}/start${C.reset}`;
212
- }
@@ -18,7 +18,7 @@ export const CLI_EXAMPLES = [
18
18
  { command: 'browsermonitor --open', description: 'Launch Chrome and monitor (URL from settings or first arg)' },
19
19
  { command: 'browsermonitor --open https://localhost:5173/', description: 'Open with URL' },
20
20
  { command: 'browsermonitor --join=9222', description: 'Attach to Chrome with remote debugging on port 9222' },
21
- { command: 'browsermonitor init', description: 'Run setup: create .browsermonitor/, settings.json, update agent files' },
21
+ { command: 'browsermonitor init', description: 'Create .browsermonitor/, settings.json with defaults, update agent files' },
22
22
  { command: 'browsermonitor --help', description: 'Show full help (options, API table, examples)' },
23
23
  ];
24
24