@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,45 @@
1
+ /**
2
+ * WSL utilities for browsermonitor.
3
+ *
4
+ * This module provides all functionality needed for running browsermonitor
5
+ * from WSL2 and connecting to Chrome on Windows.
6
+ */
7
+
8
+ // Detection utilities
9
+ export {
10
+ isWsl,
11
+ getWslDistroName,
12
+ getWindowsHostForWSL,
13
+ wslToWindowsPath,
14
+ } from './detect.mjs';
15
+
16
+ // Chrome detection and launch
17
+ export {
18
+ getLastCmdStderrAndClear,
19
+ getWindowsLocalAppData,
20
+ getWindowsProfilePath,
21
+ detectWindowsChromeCanaryPath,
22
+ detectWindowsChromePath,
23
+ printCanaryInstallInstructions,
24
+ scanChromeInstances,
25
+ findProjectChrome,
26
+ findFreeDebugPort,
27
+ startChromeOnWindows,
28
+ killPuppeteerMonitorChromes,
29
+ checkChromeRunning,
30
+ launchChromeFromWSL,
31
+ } from './chrome.mjs';
32
+
33
+ // Port proxy management
34
+ export {
35
+ removePortProxyIfExists,
36
+ isPortBlocked,
37
+ setupPortProxy,
38
+ detectChromeBindAddress,
39
+ getPortProxyConfig,
40
+ } from './port-proxy.mjs';
41
+
42
+ // Diagnostics
43
+ export {
44
+ runWslDiagnostics,
45
+ } from './diagnostics.mjs';
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Port proxy utilities for WSL→Windows connectivity.
3
+ *
4
+ * Chrome M113+ ignores --remote-debugging-address=0.0.0.0 for security.
5
+ * Chrome always binds to 127.0.0.1 or ::1, so we MUST use Windows port proxy
6
+ * (netsh interface portproxy) to forward connections from WSL.
7
+ *
8
+ * Port proxy types:
9
+ * - v4tov4: forward from 0.0.0.0:PORT to 127.0.0.1:PORT (for IPv4 Chrome)
10
+ * - v4tov6: forward from 0.0.0.0:PORT to ::1:PORT (for IPv6 Chrome)
11
+ */
12
+
13
+ import { execSync } from 'child_process';
14
+ import { log } from '../../utils/colors.mjs';
15
+
16
+ /**
17
+ * Check if port proxy exists on a given port and remove it.
18
+ * This is needed because old port proxy rules can block Chrome from binding.
19
+ *
20
+ * @param {number} port - Port to check
21
+ * @returns {boolean} true if port proxy was found and removed
22
+ */
23
+ export function removePortProxyIfExists(port) {
24
+ try {
25
+ // Check v4tov4 proxy
26
+ const portProxyV4 = execSync(
27
+ 'powershell.exe -NoProfile -Command "netsh interface portproxy show v4tov4"',
28
+ { encoding: 'utf8', timeout: 5000 }
29
+ );
30
+
31
+ if (portProxyV4 && portProxyV4.includes(String(port))) {
32
+ log.warn(`Found old v4tov4 port proxy on port ${port}, removing...`);
33
+ try {
34
+ execSync(
35
+ `powershell.exe -NoProfile -Command "netsh interface portproxy delete v4tov4 listenport=${port} listenaddress=0.0.0.0"`,
36
+ { encoding: 'utf8', timeout: 5000 }
37
+ );
38
+ log.success(`Port proxy (v4tov4) removed from port ${port}`);
39
+ return true;
40
+ } catch (e) {
41
+ log.warn(`Could not remove port proxy (may need admin rights): ${e.message}`);
42
+ }
43
+ }
44
+
45
+ // Check v4tov6 proxy
46
+ const portProxyV6 = execSync(
47
+ 'powershell.exe -NoProfile -Command "netsh interface portproxy show v4tov6"',
48
+ { encoding: 'utf8', timeout: 5000 }
49
+ );
50
+
51
+ if (portProxyV6 && portProxyV6.includes(String(port))) {
52
+ log.warn(`Found old v4tov6 port proxy on port ${port}, removing...`);
53
+ try {
54
+ execSync(
55
+ `powershell.exe -NoProfile -Command "netsh interface portproxy delete v4tov6 listenport=${port} listenaddress=0.0.0.0"`,
56
+ { encoding: 'utf8', timeout: 5000 }
57
+ );
58
+ log.success(`Port proxy (v4tov6) removed from port ${port}`);
59
+ return true;
60
+ } catch (e) {
61
+ log.warn(`Could not remove port proxy (may need admin rights): ${e.message}`);
62
+ }
63
+ }
64
+
65
+ return false;
66
+ } catch {
67
+ return false;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Check if a port is already in use by something other than Chrome debug.
73
+ *
74
+ * @param {number} port - Port to check
75
+ * @returns {boolean} true if port is blocked
76
+ */
77
+ export function isPortBlocked(port) {
78
+ try {
79
+ const netstat = execSync(
80
+ `netstat.exe -ano 2>/dev/null | grep -E ":${port}.*LISTEN"`,
81
+ { encoding: 'utf8', timeout: 5000 }
82
+ );
83
+ return netstat && netstat.includes('LISTEN');
84
+ } catch {
85
+ return false;
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Set up port proxy for WSL access to Chrome.
91
+ *
92
+ * @param {number} port - Port to forward
93
+ * @param {'v4tov4'|'v4tov6'} proxyType - Proxy type based on Chrome's bind address
94
+ * @returns {boolean} true if setup successful
95
+ */
96
+ export function setupPortProxy(port, proxyType = 'v4tov4') {
97
+ const connectAddress = proxyType === 'v4tov6' ? '::1' : '127.0.0.1';
98
+
99
+ try {
100
+ // Remove any existing proxy first
101
+ try {
102
+ execSync(
103
+ `powershell.exe -NoProfile -Command "netsh interface portproxy delete v4tov4 listenport=${port} listenaddress=0.0.0.0 2>\\$null; netsh interface portproxy delete v4tov6 listenport=${port} listenaddress=0.0.0.0 2>\\$null"`,
104
+ { encoding: 'utf8', timeout: 5000 }
105
+ );
106
+ } catch { /* ignore */ }
107
+
108
+ // Add new proxy
109
+ execSync(
110
+ `powershell.exe -NoProfile -Command "netsh interface portproxy add ${proxyType} listenport=${port} listenaddress=0.0.0.0 connectport=${port} connectaddress=${connectAddress}"`,
111
+ { encoding: 'utf8', timeout: 5000 }
112
+ );
113
+ log.success(`Port proxy configured: 0.0.0.0:${port} → ${connectAddress}:${port} (${proxyType})`);
114
+ return true;
115
+ } catch (e) {
116
+ log.warn(`Could not set up port proxy (need admin): ${e.message}`);
117
+ return false;
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Detect Chrome's bind address (IPv4 or IPv6) by checking netstat.
123
+ *
124
+ * @param {number} port - Port to check
125
+ * @returns {'127.0.0.1'|'::1'|null} Bind address or null if not found
126
+ */
127
+ export function detectChromeBindAddress(port) {
128
+ try {
129
+ const netstatOutput = execSync(`netstat.exe -ano`, { encoding: 'utf8', timeout: 5000 });
130
+ const lines = netstatOutput.split('\n').filter(l => l.includes(':' + port) && l.includes('LISTEN'));
131
+
132
+ for (const line of lines) {
133
+ if (line.includes('127.0.0.1:' + port)) {
134
+ return '127.0.0.1';
135
+ } else if (line.includes('[::1]:' + port)) {
136
+ return '::1';
137
+ }
138
+ }
139
+ return null;
140
+ } catch {
141
+ return null;
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Get current port proxy configuration.
147
+ *
148
+ * @returns {{v4tov4: Array<{listenPort: number, connectAddress: string}>, v4tov6: Array<{listenPort: number, connectAddress: string}>}}
149
+ */
150
+ export function getPortProxyConfig() {
151
+ const config = { v4tov4: [], v4tov6: [] };
152
+
153
+ try {
154
+ const v4tov4Output = execSync(
155
+ 'powershell.exe -NoProfile -Command "netsh interface portproxy show v4tov4"',
156
+ { encoding: 'utf8', timeout: 5000 }
157
+ );
158
+ // Parse output - format: "0.0.0.0 9222 127.0.0.1 9222"
159
+ const lines = v4tov4Output.split('\n').filter(l => l.match(/\d+\.\d+\.\d+\.\d+/));
160
+ for (const line of lines) {
161
+ const match = line.match(/(\d+\.\d+\.\d+\.\d+)\s+(\d+)\s+(\d+\.\d+\.\d+\.\d+)\s+(\d+)/);
162
+ if (match) {
163
+ config.v4tov4.push({
164
+ listenPort: parseInt(match[2], 10),
165
+ connectAddress: match[3],
166
+ });
167
+ }
168
+ }
169
+ } catch { /* ignore */ }
170
+
171
+ try {
172
+ const v4tov6Output = execSync(
173
+ 'powershell.exe -NoProfile -Command "netsh interface portproxy show v4tov6"',
174
+ { encoding: 'utf8', timeout: 5000 }
175
+ );
176
+ const lines = v4tov6Output.split('\n').filter(l => l.match(/\d+\.\d+\.\d+\.\d+/));
177
+ for (const line of lines) {
178
+ // v4tov6 format includes ::1 for IPv6
179
+ const match = line.match(/(\d+\.\d+\.\d+\.\d+)\s+(\d+)\s+([^\s]+)\s+(\d+)/);
180
+ if (match) {
181
+ config.v4tov6.push({
182
+ listenPort: parseInt(match[2], 10),
183
+ connectAddress: match[3],
184
+ });
185
+ }
186
+ }
187
+ } catch { /* ignore */ }
188
+
189
+ return config;
190
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Settings & paths module for browsermonitor.
3
+ * Replaces the old package.json config approach.
4
+ * All project-specific state lives in <projectRoot>/.browsermonitor/
5
+ */
6
+
7
+ import fs from 'fs';
8
+ import path from 'path';
9
+
10
+ // Directory and file names
11
+ export const BROWSERMONITOR_DIR = '.browsermonitor';
12
+ export const PUPPETEER_DIR = '.puppeteer';
13
+ export const CHROME_PROFILE_DIR = '.chrome-profile';
14
+ export const SETTINGS_FILE = 'settings.json';
15
+ export const PID_FILE = 'browsermonitor.pid';
16
+
17
+ /** Default settings for new projects */
18
+ export const DEFAULT_SETTINGS = {
19
+ defaultUrl: 'https://localhost:4000/',
20
+ headless: false,
21
+ navigationTimeout: 60000,
22
+ ignorePatterns: [],
23
+ httpPort: 60001,
24
+ realtime: false,
25
+ };
26
+
27
+ /**
28
+ * Get all resolved paths for a given project root.
29
+ * @param {string} projectRoot - Absolute path to the project directory
30
+ * @returns {Object} All paths used by browsermonitor
31
+ */
32
+ export function getPaths(projectRoot) {
33
+ const bmDir = path.join(projectRoot, BROWSERMONITOR_DIR);
34
+ const puppeteerDir = path.join(bmDir, PUPPETEER_DIR);
35
+ return {
36
+ bmDir,
37
+ settingsFile: path.join(bmDir, SETTINGS_FILE),
38
+ puppeteerDir,
39
+ chromeProfileDir: path.join(bmDir, CHROME_PROFILE_DIR),
40
+ pidFile: path.join(bmDir, PID_FILE),
41
+ // Dump outputs inside .puppeteer/
42
+ consoleLog: path.join(puppeteerDir, 'console.log'),
43
+ networkLog: path.join(puppeteerDir, 'network.log'),
44
+ networkDir: path.join(puppeteerDir, 'network-log'),
45
+ cookiesDir: path.join(puppeteerDir, 'cookies'),
46
+ domHtml: path.join(puppeteerDir, 'dom.html'),
47
+ screenshot: path.join(puppeteerDir, 'screenshot.png'),
48
+ };
49
+ }
50
+
51
+ /**
52
+ * Check if .browsermonitor/ directory exists (first-run detection).
53
+ * @param {string} projectRoot
54
+ * @returns {boolean}
55
+ */
56
+ export function isInitialized(projectRoot) {
57
+ const { bmDir } = getPaths(projectRoot);
58
+ return fs.existsSync(bmDir);
59
+ }
60
+
61
+ /**
62
+ * Load settings from .browsermonitor/settings.json, merged with defaults.
63
+ * @param {string} projectRoot
64
+ * @returns {Object} Merged settings
65
+ */
66
+ export function loadSettings(projectRoot) {
67
+ const { settingsFile } = getPaths(projectRoot);
68
+ try {
69
+ if (fs.existsSync(settingsFile)) {
70
+ const raw = fs.readFileSync(settingsFile, 'utf8');
71
+ const saved = JSON.parse(raw);
72
+ return { ...DEFAULT_SETTINGS, ...saved };
73
+ }
74
+ } catch {
75
+ // Ignore parse errors, fall through to defaults
76
+ }
77
+ return { ...DEFAULT_SETTINGS };
78
+ }
79
+
80
+ /**
81
+ * Write settings to .browsermonitor/settings.json.
82
+ * Creates .browsermonitor/ directory if needed.
83
+ * @param {string} projectRoot
84
+ * @param {Object} settings
85
+ */
86
+ export function saveSettings(projectRoot, settings) {
87
+ const { bmDir, settingsFile } = getPaths(projectRoot);
88
+ fs.mkdirSync(bmDir, { recursive: true });
89
+ fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + '\n');
90
+ }
91
+
92
+ /**
93
+ * Ensure all browsermonitor directories exist.
94
+ * @param {string} projectRoot
95
+ */
96
+ export function ensureDirectories(projectRoot) {
97
+ const { bmDir, puppeteerDir, chromeProfileDir } = getPaths(projectRoot);
98
+ fs.mkdirSync(bmDir, { recursive: true });
99
+ fs.mkdirSync(puppeteerDir, { recursive: true });
100
+ fs.mkdirSync(chromeProfileDir, { recursive: true });
101
+ }
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Single source of truth for HTTP API and output files reference.
3
+ * Renders tables via cli-table3.
4
+ */
5
+
6
+ import { C } from '../utils/colors.mjs';
7
+ import { printSectionHeading } from './section-heading.mjs';
8
+ import { createTable, printTable, INDENT } from './table-helper.mjs';
9
+ import { printInteractiveSection } from './interactive-keys.mjs';
10
+
11
+ // ─── Data (edit only here) ─────────────────────────────────────────────────
12
+
13
+ export const API_ENDPOINTS = [
14
+ { method: 'GET', path: '/dump', description: 'Dump logs, DOM, cookies, screenshot to files; response has output paths' },
15
+ { method: 'GET', path: '/status', description: 'Current status, monitored URLs, stats, output file paths' },
16
+ { method: 'GET', path: '/stop', description: 'Pause collecting (console/network)' },
17
+ { method: 'GET', path: '/start', description: 'Resume collecting' },
18
+ { method: 'GET', path: '/clear', description: 'Clear in-memory buffers' },
19
+ { method: 'GET', path: '/tabs', description: 'List all user tabs (index, url)' },
20
+ { method: 'GET', path: '/tab?index=N', description: 'Switch monitored tab (1-based index)' },
21
+ { method: 'GET', path: '/computed-styles?selector=...', description: 'Get computed CSS for first element matching selector (default: body)' },
22
+ { method: 'POST', path: '/puppeteer', description: 'Call Puppeteer page method. Body: { "method": "page.goto", "args": ["https://..."] }. Whitelist: content, click, focus, goto, hover, pdf, screenshot, select, setDefaultNavigationTimeout, setDefaultTimeout, setViewport, title, type, url' },
23
+ ];
24
+
25
+ export const OUTPUT_FILES = [
26
+ { path: '.browsermonitor/.puppeteer/console.log', description: 'Browser console output' },
27
+ { path: '.browsermonitor/.puppeteer/network.log', description: 'Network requests overview (IDs)' },
28
+ { path: '.browsermonitor/.puppeteer/network-log/', description: 'Per-request JSON (headers, payload, response)' },
29
+ { path: '.browsermonitor/.puppeteer/cookies/', description: 'Cookies per domain (JSON)' },
30
+ { path: '.browsermonitor/.puppeteer/dom.html', description: 'Current page DOM (for LLM / structure)' },
31
+ { path: '.browsermonitor/.puppeteer/screenshot.png', description: 'Screenshot of current tab viewport' },
32
+ ];
33
+
34
+ /** Full description for API section. */
35
+ const API_DESCRIPTION =
36
+ 'For LLM Coding agents: Browser Monitor captures live browser state (console, network, DOM) and writes it to files. You or an LLM read those files instead of copy-pasting from DevTools. For debugging or feeding context to AI you need the real console, DOM, and traffic—this tool gives a one-command snapshot. Frontend devs and teams using LLM agents need it; without it, a reliable live-browser snapshot is much harder. Essential for E2E and for feeding DOM and network data to LLM agents.';
37
+
38
+ const API_USAGE =
39
+ 'Use curl to communicate with the HTTP API over REST. GET for status/dump/tabs etc.; POST for /puppeteer with JSON body. Example: curl http://127.0.0.1:60001/status curl -X POST http://127.0.0.1:60001/puppeteer -H "Content-Type: application/json" -d \'{"method":"page.goto","args":["https://example.com"]}\'';
40
+
41
+ /**
42
+ * Print HTTP API and output files as readable tables.
43
+ * @param {Object} options
44
+ * @param {number} [options.port=60001]
45
+ * @param {string} [options.host='127.0.0.1']
46
+ * @param {boolean} [options.showApi=true]
47
+ * @param {boolean} [options.showInteractive=false] - Interactive help (keyboard shortcuts, human interaction)
48
+ * @param {boolean} [options.showOutputFiles=true]
49
+ * @param {Object} [options.context] - Override paths for session help: { consoleLog, networkLog, domHtml }
50
+ * @param {Object} [options.sessionContext] - Full help only: { currentUrl, profilePath } – copy-paste for Claude Code
51
+ * @param {boolean} [options.noLeadingNewline=false] - Skip blank line before first section (e.g. when right after CLI intro)
52
+ */
53
+ export function printApiHelpTable(options = {}) {
54
+ const {
55
+ port = 60001,
56
+ host = '127.0.0.1',
57
+ showApi = true,
58
+ showInteractive = false,
59
+ showOutputFiles = true,
60
+ context = null,
61
+ sessionContext = null,
62
+ noLeadingNewline = false,
63
+ } = options;
64
+
65
+ const baseUrl = `http://${host}:${port}`;
66
+
67
+ if (showApi) {
68
+ if (!noLeadingNewline) console.log('');
69
+ printSectionHeading('HTTP API', INDENT);
70
+
71
+ const hasSession = sessionContext && (sessionContext.currentUrl || sessionContext.profilePath);
72
+ const apiTable = createTable({
73
+ colWidths: hasSession ? [14, 82] : [14, 60],
74
+ tableOpts: { wordWrap: true, maxWidth: hasSession ? 100 : 90 },
75
+ });
76
+ const methodsContent = API_ENDPOINTS.map(
77
+ (r) => `${C.green}${r.method}${C.reset} ${C.brightCyan}${r.path}${C.reset} ${r.description}`
78
+ ).join('\n');
79
+ const urlLabel = sessionContext ? `${C.dim}HTTP API URL${C.reset}` : `${C.dim}Default URL${C.reset}`;
80
+
81
+ const rows = [];
82
+ if (sessionContext && (sessionContext.currentUrl || sessionContext.profilePath)) {
83
+ const lines = [];
84
+ if (sessionContext.currentUrl) {
85
+ lines.push(`${C.dim}Monitored URL:${C.reset}\n${C.brightCyan}${sessionContext.currentUrl}${C.reset}`);
86
+ }
87
+ if (sessionContext.profilePath) {
88
+ lines.push(`${C.dim}Chrome profile:${C.reset}\n${C.brightCyan}${sessionContext.profilePath}${C.reset}`);
89
+ }
90
+ rows.push([`${C.dim}Session${C.reset}`, lines.join('\n\n')]);
91
+ }
92
+ rows.push(
93
+ [`${C.dim}Description${C.reset}`, API_DESCRIPTION],
94
+ [`${C.dim}Usage${C.reset}`, API_USAGE],
95
+ [urlLabel, `${C.brightCyan}${baseUrl}${C.reset}`],
96
+ [`${C.dim}Methods${C.reset}`, methodsContent]
97
+ );
98
+ rows.forEach((r) => apiTable.push(r));
99
+ printTable(apiTable);
100
+ }
101
+
102
+ if (showOutputFiles) {
103
+ const files = context
104
+ ? [
105
+ { path: context.consoleLog, description: 'Console log' },
106
+ { path: context.networkLog, description: 'Network log' },
107
+ { path: context.networkDir, description: 'Per-request JSON (headers, payload, response)' },
108
+ { path: context.cookiesDir, description: 'Cookies per domain (JSON)' },
109
+ { path: context.domHtml, description: 'Current page DOM (LLM)' },
110
+ { path: context.screenshot, description: 'Screenshot of current tab' },
111
+ ]
112
+ : OUTPUT_FILES;
113
+
114
+ console.log('');
115
+ printSectionHeading('Output files', INDENT);
116
+ if (context) {
117
+ console.log(`${INDENT}${C.dim}(this run)${C.reset}`);
118
+ }
119
+ const content = files
120
+ .map((f) => `${C.dim}${f.description}${C.reset}\n${C.brightCyan}${f.path}${C.reset}`)
121
+ .join('\n\n');
122
+ const ofTable = createTable({
123
+ colWidths: [74],
124
+ tableOpts: { wordWrap: true, maxWidth: 80 },
125
+ });
126
+ ofTable.push([content]);
127
+ printTable(ofTable);
128
+ }
129
+
130
+ if (showInteractive) {
131
+ printInteractiveSection();
132
+ }
133
+
134
+ // Same info without tables, structured for LLM (no box-drawing / table chars; no keyboard shortcuts)
135
+ if (showApi || showOutputFiles) {
136
+ printApiHelpForLlm({ port, host, showApi, showOutputFiles, context, sessionContext });
137
+ }
138
+
139
+ console.log('');
140
+ }
141
+
142
+ /**
143
+ * Print API, output files and keys in plain structured text for LLM consumption (no tables).
144
+ * Same data as printApiHelpTable, format optimized for parsing by LLM.
145
+ */
146
+ function printApiHelpForLlm(options = {}) {
147
+ const {
148
+ port = 60001,
149
+ host = '127.0.0.1',
150
+ showApi = true,
151
+ showOutputFiles = true,
152
+ context = null,
153
+ sessionContext = null,
154
+ } = options;
155
+
156
+ const baseUrl = `http://${host}:${port}`;
157
+ const files = context
158
+ ? [
159
+ { path: context.consoleLog, description: 'Console log' },
160
+ { path: context.networkLog, description: 'Network log' },
161
+ { path: context.networkDir, description: 'Per-request JSON (headers, payload, response)' },
162
+ { path: context.cookiesDir, description: 'Cookies per domain (JSON)' },
163
+ { path: context.domHtml, description: 'Current page DOM (LLM)' },
164
+ { path: context.screenshot, description: 'Screenshot of current tab' },
165
+ ]
166
+ : OUTPUT_FILES;
167
+
168
+ console.log('');
169
+ console.log('--- LLM reference (plain text, no tables) ---');
170
+ console.log('');
171
+ console.log('Base URL: ' + baseUrl);
172
+ if (sessionContext?.currentUrl) {
173
+ console.log('Monitored URL: ' + sessionContext.currentUrl);
174
+ }
175
+ if (sessionContext?.profilePath) {
176
+ console.log('Chrome profile path: ' + sessionContext.profilePath);
177
+ }
178
+ if (sessionContext?.currentUrl || sessionContext?.profilePath) {
179
+ console.log('');
180
+ }
181
+
182
+ if (showApi) {
183
+ console.log('HTTP API endpoints:');
184
+ for (const e of API_ENDPOINTS) {
185
+ console.log(' ' + e.method + ' ' + e.path);
186
+ console.log(' ' + e.description);
187
+ }
188
+ console.log('');
189
+ }
190
+
191
+ if (showOutputFiles) {
192
+ console.log('Output files (paths and purpose):');
193
+ for (const f of files) {
194
+ console.log(' ' + f.path);
195
+ console.log(' ' + f.description);
196
+ }
197
+ console.log('');
198
+ }
199
+
200
+ console.log('--- end LLM reference ---');
201
+ console.log('');
202
+ }
203
+
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
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Single source of truth for CLI commands and usage.
3
+ * Used in intro and in --help so we never duplicate.
4
+ */
5
+
6
+ import { C } from '../utils/colors.mjs';
7
+ import { printSectionHeading } from './section-heading.mjs';
8
+ import { createTable, printTable, INDENT } from './table-helper.mjs';
9
+
10
+ // ─── Data (edit only here) ─────────────────────────────────────────────────
11
+
12
+ export const ENTRY_POINT = 'browsermonitor';
13
+
14
+ export const USAGE = 'browsermonitor [url] [options]';
15
+
16
+ export const CLI_EXAMPLES = [
17
+ { command: 'browsermonitor', description: 'Interactive menu, then o = open / j = join / q = quit' },
18
+ { command: 'browsermonitor --open', description: 'Launch Chrome and monitor (URL from settings or first arg)' },
19
+ { command: 'browsermonitor --open https://localhost:5173/', description: 'Open with URL' },
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' },
22
+ { command: 'browsermonitor --help', description: 'Show full help (options, API table, examples)' },
23
+ ];
24
+
25
+ /**
26
+ * Print CLI commands as a readable table (intro and --help).
27
+ */
28
+ export function printCliCommandsTable(options = {}) {
29
+ const { showEntry = true, showUsage = true } = options;
30
+
31
+ const table = createTable({
32
+ colWidths: [10, 68],
33
+ tableOpts: { wordWrap: true, maxWidth: 90 },
34
+ });
35
+
36
+ if (showEntry) {
37
+ table.push([`${C.dim}Entry${C.reset}`, `${C.green}${ENTRY_POINT}${C.reset}`]);
38
+ }
39
+ if (showUsage) {
40
+ table.push([`${C.dim}Usage${C.reset}`, `${C.brightCyan}${USAGE}${C.reset}`]);
41
+ }
42
+ const examplesContent = CLI_EXAMPLES.map(
43
+ (ex) => `${C.brightCyan}${ex.command}${C.reset} ${ex.description}`
44
+ ).join('\n');
45
+ table.push([`${C.dim}Examples${C.reset}`, examplesContent]);
46
+
47
+ console.log('');
48
+ printSectionHeading('CLI', INDENT);
49
+ printTable(table);
50
+ console.log('');
51
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Single source of truth for keyboard shortcuts (human interaction).
3
+ * Own section with same box heading as CLI / HTTP API.
4
+ */
5
+
6
+ import { C } from '../utils/colors.mjs';
7
+ import { printSectionHeading } from './section-heading.mjs';
8
+ import { createTable, printTable, INDENT } from './table-helper.mjs';
9
+
10
+ export const KEYBOARD_KEYS = [
11
+ { key: 'd', action: 'dump to files' },
12
+ { key: 'c', action: 'clear buffer' },
13
+ { key: 's', action: 'status' },
14
+ { key: 'p', action: 'pause/resume' },
15
+ { key: 't', action: 'switch tab' },
16
+ { key: 'h', action: 'this help' },
17
+ { key: 'k', action: 'kill / quit' },
18
+ { key: 'q', action: 'quit' },
19
+ ];
20
+
21
+ const DESCRIPTION = 'Human interaction: keyboard shortcuts while monitoring.';
22
+
23
+ export function printInteractiveSection() {
24
+ console.log('');
25
+ printSectionHeading('Interactive', INDENT);
26
+ console.log(`${INDENT}${C.dim}${DESCRIPTION}${C.reset}`);
27
+ console.log('');
28
+ const table = createTable({ colWidths: [6, 22] });
29
+ for (const r of KEYBOARD_KEYS) {
30
+ table.push([`${C.green}${r.key}${C.reset}`, r.action]);
31
+ }
32
+ printTable(table);
33
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Shared "Ready" help block – printed once when monitoring starts and repeated
3
+ * every HELP_INTERVAL (e.g. 5) lines of status/output. Used by open-mode and join-mode.
4
+ */
5
+
6
+ import { C } from '../utils/colors.mjs';
7
+ import { createTable, printTable } from './table-helper.mjs';
8
+
9
+ /** Keys line suffix: open = k+q with Chrome stays, join = k+q disconnect */
10
+ export const KEYS_OPEN = `${C.green}k${C.reset}=close Chrome+quit ${C.green}q${C.reset}=quit (Chrome stays)`;
11
+ export const KEYS_JOIN = `${C.green}k${C.reset}=kill ${C.green}q${C.reset}=quit`;
12
+
13
+ /**
14
+ * Print the Ready block (single-cell table).
15
+ */
16
+ export function printReadyHelp(httpPort, keysSuffix) {
17
+ const content = `${C.green}Ready${C.reset} │ ${C.green}d${C.reset}=dump ${C.green}c${C.reset}=clear ${C.green}s${C.reset}=status ${C.green}p${C.reset}=stop/start ${C.green}t${C.reset}=tab ${C.green}h${C.reset}=help │ ${keysSuffix} ${C.dim}│ full table: h${C.reset}`;
18
+ const table = createTable({ colWidths: [95] });
19
+ table.push([content]);
20
+ printTable(table);
21
+ }
22
+
23
+ /**
24
+ * Print status block (s key) – URL first, then status line. Single-cell table.
25
+ */
26
+ export function printStatusBlock(stats, urlLine, tabCount, collectingPaused) {
27
+ const urlDisplay = `${C.dim}URL:${C.reset} ${C.cyan}${urlLine}${C.reset}`;
28
+ const statusDisplay = `${C.cyan}[Status]${C.reset} ${C.brightCyan}${stats.consoleEntries}${C.reset} console │ ${C.brightCyan}${stats.networkEntries}${C.reset} network │ ${C.brightCyan}${stats.requestDetails}${C.reset} requests │ ${C.brightGreen}${tabCount}${C.reset} tab(s) │ collecting: ${collectingPaused ? C.yellow + 'paused' : C.green + 'running'}${C.reset}`;
29
+ const content = `${urlDisplay}\n${statusDisplay}`;
30
+ const table = createTable({ colWidths: [95] });
31
+ table.push([content]);
32
+ printTable(table);
33
+ }