@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,199 @@
1
+ /**
2
+ * Shared page monitoring: console, network (request/response) listeners.
3
+ * Used by both connect-mode and launch-mode.
4
+ */
5
+
6
+ import { getTimestamp, getFullTimestamp } from '../logging/index.mjs';
7
+
8
+ /** Non-fatal: navigation destroyed the context; we skip logging instead of crashing. */
9
+ function isContextDestroyedError(e) {
10
+ const msg = (e && e.message) || String(e);
11
+ return /Execution context was destroyed|Target closed|Protocol error/.test(msg);
12
+ }
13
+
14
+ /**
15
+ * Attach console and network monitoring to a page.
16
+ * @param {import('puppeteer').Page} targetPage
17
+ * @param {Object} context
18
+ * @param {import('../logging.mjs').LogBuffer} context.logBuffer
19
+ * @param {() => boolean} context.getCollectingPaused
20
+ * @param {(fn: () => void) => void} context.setActivePageCleanup - setter for cleanup callback
21
+ * @param {string} [context.pageLabel] - optional label for multi-tab (e.g. "[tab 2] ")
22
+ */
23
+ export function setupPageMonitoring(targetPage, context) {
24
+ const { logBuffer, getCollectingPaused, setActivePageCleanup, pageLabel = '' } = context;
25
+
26
+ const requestData = new Map();
27
+
28
+ const onConsole = async (msg) => {
29
+ try {
30
+ if (getCollectingPaused()) return;
31
+ const type = msg.type();
32
+ const timestamp = getTimestamp();
33
+
34
+ if (type === 'clear') {
35
+ logBuffer.clearConsoleBuffer();
36
+ logBuffer.printConsoleSeparator(pageLabel ? 'CONSOLE CLEARED' : 'CONSOLE CLEARED - Output reset');
37
+ return;
38
+ }
39
+
40
+ let text;
41
+ try {
42
+ const args = msg.args();
43
+ const serialized = await Promise.all(
44
+ args.map(async (arg) => {
45
+ try {
46
+ const val = await arg.jsonValue();
47
+ if (typeof val === 'object' && val !== null) return JSON.stringify(val);
48
+ return String(val);
49
+ } catch {
50
+ return arg.toString();
51
+ }
52
+ })
53
+ );
54
+ text = serialized.join(' ');
55
+ } catch {
56
+ text = msg.text();
57
+ }
58
+
59
+ if (logBuffer.shouldIgnore(text)) return;
60
+ if (logBuffer.isHmr(text)) {
61
+ logBuffer.printConsoleSeparator('HMR UPDATE - Code change detected');
62
+ }
63
+ const typeStr = type.toUpperCase().padEnd(7);
64
+ logBuffer.logConsole(`[${timestamp}] ${pageLabel}${typeStr} ${text}`);
65
+ } catch (e) {
66
+ if (isContextDestroyedError(e)) return;
67
+ throw e;
68
+ }
69
+ };
70
+
71
+ const onPageError = (error) => {
72
+ try {
73
+ if (getCollectingPaused()) return;
74
+ logBuffer.logConsole(`[${getTimestamp()}] ${pageLabel}[PAGE ERROR] ${error.message}`);
75
+ } catch (e) {
76
+ if (isContextDestroyedError(e)) return;
77
+ throw e;
78
+ }
79
+ };
80
+
81
+ const onRequest = (request) => {
82
+ try {
83
+ if (getCollectingPaused()) return;
84
+ const reqUrl = request.url();
85
+ const method = request.method();
86
+ const resourceType = request.resourceType();
87
+ const timestamp = getFullTimestamp();
88
+ const id = logBuffer.nextRequestId();
89
+ let postData = null;
90
+ try {
91
+ postData = request.postData();
92
+ } catch (e) {}
93
+ requestData.set(request, { id, startTime: Date.now(), method, resourceType });
94
+ logBuffer.saveRequestDetail(id, {
95
+ id, timestamp, method, resourceType, url: reqUrl,
96
+ request: { headers: request.headers(), postData },
97
+ });
98
+ logBuffer.logNetwork(`[${timestamp}] --> ${id} ${method} ${resourceType.toUpperCase().padEnd(10)} ${reqUrl}`);
99
+ } catch (e) {
100
+ if (isContextDestroyedError(e)) return;
101
+ throw e;
102
+ }
103
+ };
104
+
105
+ const onResponse = async (response) => {
106
+ try {
107
+ if (getCollectingPaused()) return;
108
+ const respUrl = response.url();
109
+ const status = response.status();
110
+ const timestamp = getFullTimestamp();
111
+ const req = response.request();
112
+ const data = req ? requestData.get(req) : null;
113
+ let duration = '';
114
+ let id = '?????';
115
+ if (data) {
116
+ const ms = Date.now() - data.startTime;
117
+ duration = ` (${ms}ms)`;
118
+ id = data.id;
119
+ requestData.delete(req);
120
+ const responseHeaders = response.headers();
121
+ let responseBody = null;
122
+ const contentType = responseHeaders['content-type'] || '';
123
+ const isTextBased =
124
+ contentType.includes('json') || contentType.includes('text') ||
125
+ contentType.includes('javascript') || contentType.includes('xml');
126
+ if (isTextBased) {
127
+ try {
128
+ responseBody = await response.text();
129
+ if (responseBody.length > 100000) {
130
+ responseBody = responseBody.substring(0, 100000) + '\n... [TRUNCATED]';
131
+ }
132
+ } catch (e) {
133
+ responseBody = `[Error reading body: ${e.message}]`;
134
+ }
135
+ } else {
136
+ responseBody = `[Binary content: ${contentType}]`;
137
+ }
138
+ logBuffer.updateRequestDetail(id, {
139
+ response: {
140
+ status,
141
+ statusText: response.statusText(),
142
+ headers: responseHeaders,
143
+ body: responseBody,
144
+ duration: ms,
145
+ },
146
+ });
147
+ }
148
+ logBuffer.logNetwork(`[${timestamp}] <-- ${id} ${status.toString().padStart(3)} ${respUrl}${duration}`);
149
+ } catch (e) {
150
+ if (isContextDestroyedError(e)) return;
151
+ throw e;
152
+ }
153
+ };
154
+
155
+ const onRequestFailed = (request) => {
156
+ try {
157
+ if (getCollectingPaused()) return;
158
+ const reqUrl = request.url();
159
+ const timestamp = getFullTimestamp();
160
+ const failure = request.failure();
161
+ const data = requestData.get(request);
162
+ let duration = '';
163
+ let id = '?????';
164
+ if (data) {
165
+ const ms = Date.now() - data.startTime;
166
+ duration = ` (${ms}ms)`;
167
+ id = data.id;
168
+ requestData.delete(request);
169
+ logBuffer.updateRequestDetail(id, { failed: { errorText: failure?.errorText, duration: ms } });
170
+ }
171
+ logBuffer.logNetwork(`[${timestamp}] [FAILED] ${id} ${reqUrl}: ${failure?.errorText}${duration}`);
172
+ if (!reqUrl.includes('oauth2/sign_in')) {
173
+ logBuffer.logConsole(`[${getTimestamp()}] [FAILED] ${reqUrl}: ${failure?.errorText}`);
174
+ }
175
+ } catch (e) {
176
+ if (isContextDestroyedError(e)) return;
177
+ throw e;
178
+ }
179
+ };
180
+
181
+ targetPage.on('console', onConsole);
182
+ targetPage.on('pageerror', onPageError);
183
+ targetPage.on('request', onRequest);
184
+ targetPage.on('response', onResponse);
185
+ targetPage.on('requestfailed', onRequestFailed);
186
+
187
+ const removeListeners = (p) => {
188
+ if (typeof p?.removeListener !== 'function') return;
189
+ p.removeListener('console', onConsole);
190
+ p.removeListener('pageerror', onPageError);
191
+ p.removeListener('request', onRequest);
192
+ p.removeListener('response', onResponse);
193
+ p.removeListener('requestfailed', onRequestFailed);
194
+ };
195
+ setActivePageCleanup(() => {
196
+ removeListeners(targetPage);
197
+ requestData.clear();
198
+ });
199
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Tab selection UI: let user pick one page from a list (for connect/launch mode).
3
+ */
4
+
5
+ import readline from 'readline';
6
+ import { C, log } from '../utils/colors.mjs';
7
+
8
+ export function ensureKeypressEvents() {
9
+ readline.emitKeypressEvents(process.stdin);
10
+ if (process.stdin.isTTY && !process.stdin.isRaw) {
11
+ process.stdin.setRawMode(true);
12
+ }
13
+ }
14
+
15
+ /**
16
+ * @param {import('puppeteer').Page[]} pages
17
+ * @returns {Promise<import('puppeteer').Page|null>}
18
+ */
19
+ export function askUserToSelectPage(pages) {
20
+ ensureKeypressEvents();
21
+ console.log('');
22
+ pages.forEach((p, index) => {
23
+ const pageUrl = p.url();
24
+ const num = (index + 1).toString();
25
+ const displayUrl = pageUrl.length > 70 ? pageUrl.substring(0, 67) + '...' : pageUrl;
26
+ console.log(` ${C.brightGreen}${num}${C.reset}. ${C.cyan}${displayUrl}${C.reset}`);
27
+ });
28
+ console.log('');
29
+ console.log(` ${C.red}q${C.reset}. Cancel`);
30
+ console.log('');
31
+
32
+ return new Promise((resolve) => {
33
+ process.stdout.write(`${C.cyan}Select tab${C.reset} (${C.green}1-${pages.length}${C.reset}, ${C.red}q${C.reset}=cancel): `);
34
+ const handleKey = (str, key) => {
35
+ if (!key) return;
36
+ process.stdin.removeListener('keypress', handleKey);
37
+ const char = key.name || str;
38
+ process.stdout.write(char + '\n');
39
+ if (char === 'q') {
40
+ resolve(null);
41
+ } else {
42
+ const num = parseInt(char, 10);
43
+ if (num >= 1 && num <= pages.length) {
44
+ resolve(pages[num - 1]);
45
+ } else {
46
+ log.warn('Invalid selection');
47
+ resolve(null);
48
+ }
49
+ }
50
+ };
51
+ process.stdin.once('keypress', handleKey);
52
+ });
53
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Browser Monitor - monitors browser console and network on dev server
3
+ *
4
+ * Modes:
5
+ * - Default (lazy): Stores logs in memory, writes to file on demand
6
+ * Press 'd' to dump logs to files (auto-clears buffer after)
7
+ * Press 'c' to clear memory buffer
8
+ * Press 's' to show status
9
+ * Press 'k' to kill Chrome and exit
10
+ * Press 'q' to quit
11
+ * - Realtime (--realtime): Writes every event immediately to files
12
+ *
13
+ * Features:
14
+ * - Console output -> .browsermonitor/.puppeteer/console.log (filtered, with HMR detection)
15
+ * - Network requests -> .browsermonitor/.puppeteer/network-log/ directory
16
+ * - .browsermonitor/.puppeteer/network.log - main log with request IDs
17
+ * - {id}.json - detailed request/response for each request
18
+ * - DOM dump on /dump or key 'd' -> .browsermonitor/.puppeteer/dom.html (current JS-modified HTML structure)
19
+ * - Console clear detection (clears buffer/log file)
20
+ * - GUI browser mode (user can interact)
21
+ * - HTTP API for LLM integration (default port 60001)
22
+ * - GET/POST /dump - Trigger dump of buffered logs
23
+ * - GET /status - Get current buffer status
24
+ * - Proper cleanup on exit (SIGINT, SIGTERM, uncaughtException)
25
+ */
26
+
27
+ // Re-export modes (implementations live in monitor/ to keep this file small)
28
+ export { runJoinMode } from './monitor/join-mode.mjs';
29
+ export { runOpenMode } from './monitor/open-mode.mjs';
30
+
31
+ /**
32
+ * Run in Interactive Mode – delegates to monitor/interactive-mode.mjs with runOpenMode/runJoinMode to avoid circular deps.
33
+ */
34
+ export async function runInteractiveMode(options = {}) {
35
+ const { runInteractiveMode: runInteractive } = await import('./monitor/interactive-mode.mjs');
36
+ const { runOpenMode } = await import('./monitor/open-mode.mjs');
37
+ const { runJoinMode } = await import('./monitor/join-mode.mjs');
38
+ return runInteractive(options, { runOpenMode, runJoinMode });
39
+ }
@@ -0,0 +1,4 @@
1
+ # Platform-specific code (os)
2
+
3
+ - **wsl/** – WSL2 → Windows (Chrome detection, port proxy, diagnostics). Used when running from WSL and connecting to Chrome on Windows.
4
+ - *(Later: linux, win, ssh as needed.)*