@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.
- package/LICENSE +21 -0
- package/README.md +558 -0
- package/package.json +53 -0
- package/src/agents.llm/browser-monitor-section.md +18 -0
- package/src/cli.mjs +202 -0
- package/src/http-server.mjs +536 -0
- package/src/init.mjs +162 -0
- package/src/intro.mjs +36 -0
- package/src/logging/LogBuffer.mjs +178 -0
- package/src/logging/constants.mjs +19 -0
- package/src/logging/dump.mjs +207 -0
- package/src/logging/index.mjs +13 -0
- package/src/logging/timestamps.mjs +13 -0
- package/src/monitor/README.md +10 -0
- package/src/monitor/index.mjs +18 -0
- package/src/monitor/interactive-mode.mjs +275 -0
- package/src/monitor/join-mode.mjs +654 -0
- package/src/monitor/open-mode.mjs +889 -0
- package/src/monitor/page-monitoring.mjs +199 -0
- package/src/monitor/tab-selection.mjs +53 -0
- package/src/monitor.mjs +39 -0
- package/src/os/README.md +4 -0
- package/src/os/wsl/chrome.mjs +503 -0
- package/src/os/wsl/detect.mjs +68 -0
- package/src/os/wsl/diagnostics.mjs +729 -0
- package/src/os/wsl/index.mjs +45 -0
- package/src/os/wsl/port-proxy.mjs +190 -0
- package/src/settings.mjs +101 -0
- package/src/templates/api-help.mjs +212 -0
- package/src/templates/cli-commands.mjs +51 -0
- package/src/templates/interactive-keys.mjs +33 -0
- package/src/templates/ready-help.mjs +33 -0
- package/src/templates/section-heading.mjs +141 -0
- package/src/templates/table-helper.mjs +73 -0
- package/src/templates/wait-for-chrome.mjs +19 -0
- package/src/utils/ask.mjs +49 -0
- package/src/utils/chrome-profile-path.mjs +37 -0
- package/src/utils/colors.mjs +49 -0
- package/src/utils/env.mjs +30 -0
- package/src/utils/profile-id.mjs +23 -0
- package/src/utils/status-line.mjs +47 -0
package/src/intro.mjs
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared app intro – shown once at startup for all modes.
|
|
3
|
+
* Title, logo-style branding, what it is, why, what follows, repo.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { C } from './utils/colors.mjs';
|
|
7
|
+
import { printCliCommandsTable } from './templates/cli-commands.mjs';
|
|
8
|
+
|
|
9
|
+
const TITLE = 'Browser Monitor';
|
|
10
|
+
const TAGLINE = 'Browser console, network & DOM capture for debugging and LLM workflows';
|
|
11
|
+
const NPM_URL = 'https://www.npmjs.com/package/browsermonitor';
|
|
12
|
+
const GITHUB_URL = 'https://github.com/romanmatena/browsermonitor';
|
|
13
|
+
|
|
14
|
+
export function printAppIntro() {
|
|
15
|
+
console.log('');
|
|
16
|
+
console.log(`${C.bold}${C.brightCyan} ═══════════════════════════════════════════════════════════════════════════ ${C.reset}`);
|
|
17
|
+
console.log(`${C.bold}${C.brightCyan} ${C.reset}`);
|
|
18
|
+
console.log(`${C.bold}${C.brightCyan} ${C.white}${TITLE}${C.reset}${C.bold}${C.brightCyan} ${C.reset}`);
|
|
19
|
+
console.log(`${C.bold}${C.brightCyan} ${C.dim}npm ${NPM_URL}${C.reset}${C.bold}${C.brightCyan} ${C.reset}`);
|
|
20
|
+
console.log(`${C.bold}${C.brightCyan} ${C.dim}github ${GITHUB_URL}${C.reset}${C.bold}${C.brightCyan}${' '.repeat(12)}${C.reset}`);
|
|
21
|
+
console.log(`${C.bold}${C.brightCyan} ${C.reset}`);
|
|
22
|
+
console.log(`${C.bold}${C.brightCyan} ═══════════════════════════════════════════════════════════════════════════ ${C.reset}`);
|
|
23
|
+
console.log('');
|
|
24
|
+
console.log(` ${C.dim}${TAGLINE}${C.reset}`);
|
|
25
|
+
console.log('');
|
|
26
|
+
console.log(` ${C.cyan}What it is${C.reset} Connects to Chrome and records console output, network requests,`);
|
|
27
|
+
console.log(` ${C.dim} cookies, and the current page HTML. Logs go to files (on demand or live).${C.reset}`);
|
|
28
|
+
console.log('');
|
|
29
|
+
console.log(` ${C.cyan}Why use it${C.reset} Debug frontend apps without copy-paste. LLM agents can trigger a dump`);
|
|
30
|
+
console.log(` ${C.dim} and read the files to get the live DOM and traffic.${C.reset}`);
|
|
31
|
+
console.log('');
|
|
32
|
+
console.log(` ${C.cyan}What’s next${C.reset} Choose a mode: ${C.green}interactive${C.reset} (menu), ${C.green}open${C.reset} (launch Chrome), or`);
|
|
33
|
+
console.log(` ${C.dim} ${C.green}join${C.reset} (attach to existing). Then use the browser; dump to write logs.${C.reset}`);
|
|
34
|
+
console.log('');
|
|
35
|
+
printCliCommandsTable({ showEntry: true, showUsage: true });
|
|
36
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LogBuffer – in-memory or realtime buffers for console, network, and request details.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { C, log } from '../utils/colors.mjs';
|
|
8
|
+
import { DEFAULT_IGNORE_PATTERNS } from './constants.mjs';
|
|
9
|
+
import { HMR_PATTERNS } from './constants.mjs';
|
|
10
|
+
import { getTimestamp, getFullTimestamp } from './timestamps.mjs';
|
|
11
|
+
import {
|
|
12
|
+
dumpBuffersToFiles as doDumpBuffersToFiles,
|
|
13
|
+
dumpCookiesFromPage as doDumpCookiesFromPage,
|
|
14
|
+
dumpDomFromPage as doDumpDomFromPage,
|
|
15
|
+
dumpScreenshotFromPage as doDumpScreenshotFromPage,
|
|
16
|
+
DOM_DUMP_MAX_BYTES,
|
|
17
|
+
} from './dump.mjs';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* LogBuffer class - manages logging buffers for a monitoring session.
|
|
21
|
+
*/
|
|
22
|
+
export class LogBuffer {
|
|
23
|
+
static DOM_DUMP_MAX_BYTES = DOM_DUMP_MAX_BYTES;
|
|
24
|
+
|
|
25
|
+
constructor(options = {}) {
|
|
26
|
+
const {
|
|
27
|
+
outputDir = process.cwd(),
|
|
28
|
+
paths = null,
|
|
29
|
+
lazyMode = true,
|
|
30
|
+
ignorePatterns = [],
|
|
31
|
+
} = options;
|
|
32
|
+
|
|
33
|
+
this.outputDir = outputDir;
|
|
34
|
+
this.lazyMode = lazyMode;
|
|
35
|
+
|
|
36
|
+
if (paths) {
|
|
37
|
+
this.CONSOLE_LOG = paths.consoleLog;
|
|
38
|
+
this.NETWORK_LOG = paths.networkLog;
|
|
39
|
+
this.NETWORK_DIR = paths.networkDir;
|
|
40
|
+
this.COOKIES_DIR = paths.cookiesDir;
|
|
41
|
+
this.DOM_HTML = paths.domHtml;
|
|
42
|
+
this.SCREENSHOT = paths.screenshot;
|
|
43
|
+
} else {
|
|
44
|
+
// Fallback: use .browsermonitor/.puppeteer/ structure relative to outputDir
|
|
45
|
+
const bmPuppeteerDir = path.join(outputDir, '.browsermonitor', '.puppeteer');
|
|
46
|
+
this.CONSOLE_LOG = path.join(bmPuppeteerDir, 'console.log');
|
|
47
|
+
this.NETWORK_LOG = path.join(bmPuppeteerDir, 'network.log');
|
|
48
|
+
this.NETWORK_DIR = path.join(bmPuppeteerDir, 'network-log');
|
|
49
|
+
this.COOKIES_DIR = path.join(bmPuppeteerDir, 'cookies');
|
|
50
|
+
this.DOM_HTML = path.join(bmPuppeteerDir, 'dom.html');
|
|
51
|
+
this.SCREENSHOT = path.join(bmPuppeteerDir, 'screenshot.png');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.consoleBuffer = [];
|
|
55
|
+
this.networkBuffer = [];
|
|
56
|
+
this.requestDetails = new Map();
|
|
57
|
+
this.requestCounter = 0;
|
|
58
|
+
|
|
59
|
+
this.ignorePatterns = [...DEFAULT_IGNORE_PATTERNS, ...ignorePatterns];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
nextRequestId() {
|
|
63
|
+
this.requestCounter++;
|
|
64
|
+
return String(this.requestCounter).padStart(3, '0');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
shouldIgnore(message) {
|
|
68
|
+
return this.ignorePatterns.some(p => message.includes(p));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
isHmr(message) {
|
|
72
|
+
return HMR_PATTERNS.some(p => message.includes(p));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
logConsole(message) {
|
|
76
|
+
if (this.lazyMode) {
|
|
77
|
+
this.consoleBuffer.push(message);
|
|
78
|
+
} else {
|
|
79
|
+
console.log(message);
|
|
80
|
+
fs.appendFileSync(this.CONSOLE_LOG, message + '\n');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
clearConsoleBuffer() {
|
|
85
|
+
if (this.lazyMode) {
|
|
86
|
+
this.consoleBuffer.length = 0;
|
|
87
|
+
log.dim(`Console buffer cleared (${getTimestamp()})`);
|
|
88
|
+
} else {
|
|
89
|
+
fs.writeFileSync(this.CONSOLE_LOG, '');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
printConsoleSeparator(title) {
|
|
94
|
+
const line = '='.repeat(80);
|
|
95
|
+
this.logConsole(line);
|
|
96
|
+
this.logConsole(`[${getTimestamp()}] *** ${title} ***`);
|
|
97
|
+
this.logConsole(line);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
logNetwork(message) {
|
|
101
|
+
if (this.lazyMode) {
|
|
102
|
+
this.networkBuffer.push(message);
|
|
103
|
+
} else {
|
|
104
|
+
fs.appendFileSync(this.NETWORK_LOG, message + '\n');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
clearNetworkDir() {
|
|
109
|
+
if (fs.existsSync(this.NETWORK_DIR)) {
|
|
110
|
+
fs.rmSync(this.NETWORK_DIR, { recursive: true });
|
|
111
|
+
}
|
|
112
|
+
fs.mkdirSync(this.NETWORK_DIR, { recursive: true });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
printNetworkSeparator(title) {
|
|
116
|
+
const line = '='.repeat(80);
|
|
117
|
+
this.logNetwork(line);
|
|
118
|
+
this.logNetwork(`[${getFullTimestamp()}] ${title}`);
|
|
119
|
+
this.logNetwork(line);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
saveRequestDetail(id, data) {
|
|
123
|
+
if (this.lazyMode) {
|
|
124
|
+
this.requestDetails.set(id, data);
|
|
125
|
+
} else {
|
|
126
|
+
const filePath = path.join(this.NETWORK_DIR, `${id}.json`);
|
|
127
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
updateRequestDetail(id, updates) {
|
|
132
|
+
if (this.lazyMode) {
|
|
133
|
+
const existing = this.requestDetails.get(id) || {};
|
|
134
|
+
this.requestDetails.set(id, { ...existing, ...updates });
|
|
135
|
+
} else {
|
|
136
|
+
const filePath = path.join(this.NETWORK_DIR, `${id}.json`);
|
|
137
|
+
try {
|
|
138
|
+
const existing = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
139
|
+
const updated = { ...existing, ...updates };
|
|
140
|
+
fs.writeFileSync(filePath, JSON.stringify(updated, null, 2));
|
|
141
|
+
} catch (e) {
|
|
142
|
+
this.saveRequestDetail(id, updates);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
clearAllBuffers() {
|
|
148
|
+
this.consoleBuffer.length = 0;
|
|
149
|
+
this.networkBuffer.length = 0;
|
|
150
|
+
this.requestDetails.clear();
|
|
151
|
+
this.requestCounter = 0;
|
|
152
|
+
log.success(`All buffers cleared (${getTimestamp()})`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
getStats() {
|
|
156
|
+
return {
|
|
157
|
+
consoleEntries: this.consoleBuffer.length,
|
|
158
|
+
networkEntries: this.networkBuffer.length,
|
|
159
|
+
requestDetails: this.requestDetails.size,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async dumpBuffersToFiles(options = {}) {
|
|
164
|
+
return doDumpBuffersToFiles(this, options);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async dumpCookiesFromPage(page) {
|
|
168
|
+
return doDumpCookiesFromPage(this, page);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async dumpDomFromPage(page) {
|
|
172
|
+
return doDumpDomFromPage(this, page);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async dumpScreenshotFromPage(page) {
|
|
176
|
+
return doDumpScreenshotFromPage(this, page);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constants for console filtering and HMR detection.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** Default patterns to ignore in console output. */
|
|
6
|
+
export const DEFAULT_IGNORE_PATTERNS = [
|
|
7
|
+
'IndexedDBStorage',
|
|
8
|
+
'BackendSync',
|
|
9
|
+
'heartbeat',
|
|
10
|
+
'Sending ping',
|
|
11
|
+
'Received pong',
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
/** HMR (Hot Module Replacement) patterns to detect reloads. */
|
|
15
|
+
export const HMR_PATTERNS = [
|
|
16
|
+
'[vite] hot updated',
|
|
17
|
+
'[vite] page reloaded',
|
|
18
|
+
'[vite] connected',
|
|
19
|
+
];
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dump actions: write buffers and page data (cookies, DOM) to files.
|
|
3
|
+
* Used by LogBuffer; can be called with any LogBuffer instance.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { C, log } from '../utils/colors.mjs';
|
|
9
|
+
import { getTimestamp, getFullTimestamp } from './timestamps.mjs';
|
|
10
|
+
|
|
11
|
+
/** Max size for DOM dump (bytes). Larger output is truncated. */
|
|
12
|
+
export const DOM_DUMP_MAX_BYTES = 2 * 1024 * 1024; // 2MB
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Write all in-memory buffers to files and clear buffers.
|
|
16
|
+
* @param {import('./LogBuffer.mjs').LogBuffer} logBuffer
|
|
17
|
+
* @param {Object} options - { dumpCookies?: () => Promise<void>, dumpDom?: () => Promise<void>, dumpScreenshot?: () => Promise<void> }
|
|
18
|
+
* @returns {Promise<Object>} Stats before dump
|
|
19
|
+
*/
|
|
20
|
+
export async function dumpBuffersToFiles(logBuffer, options = {}) {
|
|
21
|
+
const { dumpCookies, dumpDom, dumpScreenshot } = options;
|
|
22
|
+
const timestamp = getTimestamp();
|
|
23
|
+
const statsBeforeDump = logBuffer.getStats();
|
|
24
|
+
|
|
25
|
+
log.section('Dumping Buffers');
|
|
26
|
+
|
|
27
|
+
if (logBuffer.consoleBuffer.length > 0) {
|
|
28
|
+
fs.writeFileSync(logBuffer.CONSOLE_LOG, logBuffer.consoleBuffer.join('\n') + '\n');
|
|
29
|
+
log.success(`${C.brightCyan}${logBuffer.consoleBuffer.length}${C.reset}${C.green} console entries → ${logBuffer.CONSOLE_LOG}${C.reset}`);
|
|
30
|
+
} else {
|
|
31
|
+
fs.writeFileSync(logBuffer.CONSOLE_LOG, '');
|
|
32
|
+
log.dim('Console buffer is empty');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (logBuffer.networkBuffer.length > 0) {
|
|
36
|
+
fs.writeFileSync(logBuffer.NETWORK_LOG, logBuffer.networkBuffer.join('\n') + '\n');
|
|
37
|
+
log.success(`${C.brightCyan}${logBuffer.networkBuffer.length}${C.reset}${C.green} network entries → ${logBuffer.NETWORK_LOG}${C.reset}`);
|
|
38
|
+
} else {
|
|
39
|
+
fs.writeFileSync(logBuffer.NETWORK_LOG, '');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
logBuffer.clearNetworkDir();
|
|
43
|
+
if (logBuffer.requestDetails.size > 0) {
|
|
44
|
+
for (const [id, data] of logBuffer.requestDetails) {
|
|
45
|
+
const filePath = path.join(logBuffer.NETWORK_DIR, `${id}.json`);
|
|
46
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
|
47
|
+
}
|
|
48
|
+
log.success(`${C.brightCyan}${logBuffer.requestDetails.size}${C.reset}${C.green} request details → ${logBuffer.NETWORK_DIR}/${C.reset}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (dumpCookies) await dumpCookies();
|
|
52
|
+
if (dumpDom) await dumpDom();
|
|
53
|
+
if (dumpScreenshot) await dumpScreenshot();
|
|
54
|
+
|
|
55
|
+
log.success(`Dump completed at ${timestamp}`);
|
|
56
|
+
logBuffer.clearAllBuffers();
|
|
57
|
+
|
|
58
|
+
return statsBeforeDump;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Dump cookies from a browser page into logBuffer.COOKIES_DIR.
|
|
63
|
+
* @param {import('./LogBuffer.mjs').LogBuffer} logBuffer
|
|
64
|
+
* @param {import('puppeteer').Page} page
|
|
65
|
+
* @returns {Promise<void>}
|
|
66
|
+
*/
|
|
67
|
+
export async function dumpCookiesFromPage(logBuffer, page) {
|
|
68
|
+
if (!page) {
|
|
69
|
+
log.dim('No page to dump cookies from');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const client = await page.createCDPSession();
|
|
75
|
+
const { cookies } = await client.send('Network.getAllCookies');
|
|
76
|
+
await client.detach();
|
|
77
|
+
|
|
78
|
+
if (cookies.length === 0) {
|
|
79
|
+
log.dim('No cookies found');
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const cookiesByDomain = new Map();
|
|
84
|
+
for (const c of cookies) {
|
|
85
|
+
const domain = c.domain.replace(/^\./, '');
|
|
86
|
+
if (!cookiesByDomain.has(domain)) cookiesByDomain.set(domain, []);
|
|
87
|
+
cookiesByDomain.get(domain).push(c);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (fs.existsSync(logBuffer.COOKIES_DIR)) fs.rmSync(logBuffer.COOKIES_DIR, { recursive: true });
|
|
91
|
+
fs.mkdirSync(logBuffer.COOKIES_DIR, { recursive: true });
|
|
92
|
+
|
|
93
|
+
const timestamp = getFullTimestamp();
|
|
94
|
+
for (const [domain, domainCookies] of cookiesByDomain) {
|
|
95
|
+
const safeDomain = domain.replace(/[^a-zA-Z0-9.-]/g, '_');
|
|
96
|
+
const cookieFile = path.join(logBuffer.COOKIES_DIR, `${safeDomain}.json`);
|
|
97
|
+
const cookieData = {
|
|
98
|
+
timestamp,
|
|
99
|
+
domain,
|
|
100
|
+
currentUrl: page.url(),
|
|
101
|
+
count: domainCookies.length,
|
|
102
|
+
cookies: domainCookies.map(c => ({
|
|
103
|
+
name: c.name,
|
|
104
|
+
value: c.value,
|
|
105
|
+
domain: c.domain,
|
|
106
|
+
path: c.path,
|
|
107
|
+
expires: c.expires ? new Date(c.expires * 1000).toISOString() : 'Session',
|
|
108
|
+
httpOnly: c.httpOnly,
|
|
109
|
+
secure: c.secure,
|
|
110
|
+
sameSite: c.sameSite || 'None',
|
|
111
|
+
})),
|
|
112
|
+
};
|
|
113
|
+
fs.writeFileSync(cookieFile, JSON.stringify(cookieData, null, 2));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
log.success(`${C.brightCyan}${cookies.length}${C.reset}${C.green} cookies (${cookiesByDomain.size} domains) → ${logBuffer.COOKIES_DIR}/${C.reset}`);
|
|
117
|
+
} catch (e) {
|
|
118
|
+
log.error(`Error dumping cookies: ${e.message}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Dump current document HTML (JS-modified DOM) to logBuffer.DOM_HTML.
|
|
124
|
+
* @param {import('./LogBuffer.mjs').LogBuffer} logBuffer
|
|
125
|
+
* @param {import('puppeteer').Page} page
|
|
126
|
+
* @returns {Promise<void>}
|
|
127
|
+
*/
|
|
128
|
+
export async function dumpDomFromPage(logBuffer, page) {
|
|
129
|
+
if (!page) {
|
|
130
|
+
log.dim('No page to dump DOM from');
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const html = await page.evaluate(() => document.documentElement.outerHTML);
|
|
136
|
+
const maxBytes = DOM_DUMP_MAX_BYTES;
|
|
137
|
+
let content = html;
|
|
138
|
+
const byteLength = Buffer.byteLength(html, 'utf8');
|
|
139
|
+
if (byteLength > maxBytes) {
|
|
140
|
+
content = content.slice(0, Math.floor(maxBytes / 2)) + '\n\n<!-- ... TRUNCATED for size ... -->\n';
|
|
141
|
+
log.dim(`DOM truncated to ~${Math.round(maxBytes / 1024)}KB (was ${Math.round(byteLength / 1024)}KB)`);
|
|
142
|
+
}
|
|
143
|
+
fs.writeFileSync(logBuffer.DOM_HTML, content, 'utf8');
|
|
144
|
+
log.success(`${C.green}DOM (current HTML) → ${logBuffer.DOM_HTML}${C.reset}`);
|
|
145
|
+
log.dim(`LLM: current page HTML / element structure is in: ${logBuffer.DOM_HTML}`);
|
|
146
|
+
} catch (e) {
|
|
147
|
+
log.error(`Error dumping DOM: ${e.message}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Capture screenshot of the current page viewport.
|
|
153
|
+
* @param {import('./LogBuffer.mjs').LogBuffer} logBuffer
|
|
154
|
+
* @param {import('puppeteer').Page} page
|
|
155
|
+
* @returns {Promise<void>}
|
|
156
|
+
*/
|
|
157
|
+
export async function dumpScreenshotFromPage(logBuffer, page) {
|
|
158
|
+
if (!page) {
|
|
159
|
+
log.dim('No page to capture screenshot from');
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
await page.screenshot({ path: logBuffer.SCREENSHOT, type: 'png' });
|
|
165
|
+
log.success(`${C.green}Screenshot → ${logBuffer.SCREENSHOT}${C.reset}`);
|
|
166
|
+
} catch (e) {
|
|
167
|
+
log.error(`Error capturing screenshot: ${e.message}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get computed CSS styles for the first element matching a selector.
|
|
173
|
+
* Uses window.getComputedStyle in the page context (via page.evaluate).
|
|
174
|
+
*
|
|
175
|
+
* @param {import('puppeteer').Page} page
|
|
176
|
+
* @param {string} selector - CSS selector (e.g. 'body', '.my-class', '#id')
|
|
177
|
+
* @returns {Promise<{ selector: string, tagName: string, computed: Record<string, string> } | { error: string }>}
|
|
178
|
+
*/
|
|
179
|
+
export async function getComputedStylesFromPage(page, selector) {
|
|
180
|
+
if (!page) {
|
|
181
|
+
return { error: 'No page' };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
const result = await page.evaluate((sel) => {
|
|
186
|
+
const el = document.querySelector(sel);
|
|
187
|
+
if (!el) {
|
|
188
|
+
return { error: `No element matching selector: ${sel}` };
|
|
189
|
+
}
|
|
190
|
+
const cs = window.getComputedStyle(el);
|
|
191
|
+
const computed = {};
|
|
192
|
+
for (let i = 0; i < cs.length; i++) {
|
|
193
|
+
const prop = cs[i];
|
|
194
|
+
computed[prop] = cs.getPropertyValue(prop);
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
selector: sel,
|
|
198
|
+
tagName: el.tagName.toLowerCase(),
|
|
199
|
+
computed,
|
|
200
|
+
};
|
|
201
|
+
}, selector);
|
|
202
|
+
|
|
203
|
+
return result;
|
|
204
|
+
} catch (e) {
|
|
205
|
+
return { error: e.message };
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logging submodule – buffer management, timestamps, dump actions.
|
|
3
|
+
*
|
|
4
|
+
* - constants.mjs – DEFAULT_IGNORE_PATTERNS, HMR_PATTERNS
|
|
5
|
+
* - timestamps.mjs – getTimestamp, getFullTimestamp
|
|
6
|
+
* - dump.mjs – dumpBuffersToFiles, dumpCookiesFromPage, dumpDomFromPage
|
|
7
|
+
* - LogBuffer.mjs – LogBuffer class
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export { DEFAULT_IGNORE_PATTERNS, HMR_PATTERNS } from './constants.mjs';
|
|
11
|
+
export { getTimestamp, getFullTimestamp } from './timestamps.mjs';
|
|
12
|
+
export { LogBuffer } from './LogBuffer.mjs';
|
|
13
|
+
export { DOM_DUMP_MAX_BYTES } from './dump.mjs';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timestamp helpers for console and file output.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** HH:MM:SS.mmm for console output */
|
|
6
|
+
export function getTimestamp() {
|
|
7
|
+
return new Date().toISOString().substring(11, 23);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Full ISO timestamp for file output */
|
|
11
|
+
export function getFullTimestamp() {
|
|
12
|
+
return new Date().toISOString();
|
|
13
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Monitor submodule
|
|
2
|
+
|
|
3
|
+
Sdílené moduly pro režimy join, open a interactive.
|
|
4
|
+
|
|
5
|
+
- **page-monitoring.mjs** – připojení console/network listenerů na stránku (sdílené pro join i open).
|
|
6
|
+
- **tab-selection.mjs** – výběr tabu (askUserToSelectPage, ensureKeypressEvents).
|
|
7
|
+
- **interactive-mode.mjs** – interaktivní menu (o = open, j = join, q = quit), výběr projektového adresáře, výběr Chrome instance.
|
|
8
|
+
- **index.mjs** – re-exporty pro přehled API.
|
|
9
|
+
|
|
10
|
+
Veřejné API (`runJoinMode`, `runOpenMode`, `runInteractiveMode`) zůstává v `../monitor.mjs` kvůli absenci cyklických závislostí.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Monitor submodule – re-exports and shared pieces.
|
|
3
|
+
* Public API (runJoinMode, runOpenMode, runInteractiveMode) lives in ../monitor.mjs
|
|
4
|
+
* to avoid circular deps. This index documents the monitor/ layout:
|
|
5
|
+
*
|
|
6
|
+
* - page-monitoring.mjs – shared console/network listeners for a page
|
|
7
|
+
* - tab-selection.mjs – askUserToSelectPage, ensureKeypressEvents
|
|
8
|
+
* - interactive-mode.mjs – runInteractiveMode(options, deps), getChromeInstances, askProjectDirForOpen
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export { setupPageMonitoring } from './page-monitoring.mjs';
|
|
12
|
+
export { askUserToSelectPage, ensureKeypressEvents } from './tab-selection.mjs';
|
|
13
|
+
export {
|
|
14
|
+
runInteractiveMode,
|
|
15
|
+
getChromeInstances,
|
|
16
|
+
askProjectDirForOpen,
|
|
17
|
+
askUserToSelectChromeInstance,
|
|
18
|
+
} from './interactive-mode.mjs';
|