@romanmatena/browsermonitor 2.0.1 → 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.
- package/README.md +3 -3
- package/package.json +1 -1
- package/src/agents.llm/browser-monitor-section.md +6 -4
- package/src/cli.mjs +103 -105
- package/src/http-server.mjs +12 -38
- package/src/init.mjs +48 -103
- package/src/logging/LogBuffer.mjs +1 -1
- package/src/monitor/README.md +18 -5
- package/src/monitor/join-mode.mjs +127 -335
- package/src/monitor/open-mode.mjs +141 -384
- package/src/monitor/shared/cleanup.mjs +96 -0
- package/src/monitor/shared/help.mjs +64 -0
- package/src/monitor/shared/http-state-setup.mjs +55 -0
- package/src/monitor/shared/index.mjs +11 -0
- package/src/monitor/shared/keyboard-handler.mjs +79 -0
- package/src/monitor/shared/monitoring-wrapper.mjs +39 -0
- package/src/monitor/shared/tab-switching.mjs +61 -0
- package/src/monitor/shared/user-page-filter.mjs +28 -0
- package/src/os/wsl/chrome.mjs +3 -36
- package/src/os/wsl/detect.mjs +0 -12
- package/src/os/wsl/index.mjs +0 -3
- package/src/settings.mjs +12 -10
- package/src/templates/api-help.mjs +9 -11
- package/src/templates/cli-commands.mjs +1 -1
- package/src/templates/section-heading.mjs +10 -16
- package/src/utils/ask.mjs +94 -28
- package/src/utils/chrome-instances.mjs +81 -0
- package/src/utils/chrome-profile-path.mjs +9 -4
- package/src/utils/status-line.mjs +0 -8
- package/src/monitor/index.mjs +0 -18
- package/src/monitor/interactive-mode.mjs +0 -275
- package/src/monitor.mjs +0 -39
package/README.md
CHANGED
|
@@ -58,9 +58,9 @@ pnpm add -g @romanmatena/browsermonitor
|
|
|
58
58
|
|
|
59
59
|
**Note:** Chromium download is skipped — browsermonitor uses your system Chrome/Chromium. No extra 300 MB download.
|
|
60
60
|
|
|
61
|
-
**First run:** When you run `browsermonitor` in a project directory
|
|
61
|
+
**First run (interactive):** When you run `browsermonitor` for the first time in a project directory, interactive mode asks for HTTP API port, saves `settings.json`, and updates agent files (`CLAUDE.md`, `AGENTS.md`, `memory.md`). When you press `o` (open), it asks for the default URL.
|
|
62
62
|
|
|
63
|
-
**
|
|
63
|
+
**Re-init:** Run `browsermonitor init` to recreate settings and update agent files.
|
|
64
64
|
|
|
65
65
|
## Quick Start
|
|
66
66
|
|
|
@@ -74,7 +74,7 @@ browsermonitor --join=9222 # Join mode: attach to existing Chrome on port
|
|
|
74
74
|
|
|
75
75
|
| Mode | How to run | When to use |
|
|
76
76
|
|------------|--------------------------|-------------|
|
|
77
|
-
| **Interactive** | `browsermonitor` (no flags) |
|
|
77
|
+
| **Interactive** | `browsermonitor` (no flags) | First run asks HTTP port. Then menu: **o** = open Chrome (asks URL on first use), **j** = join running Chrome, **q** = quit. |
|
|
78
78
|
| **Open** | `browsermonitor --open [url]` | Launch a new Chrome and monitor it. Uses current dir for logs. |
|
|
79
79
|
| **Join** | `browsermonitor --join=PORT` | Attach to an existing Chrome with remote debugging on PORT (e.g. 9222). Port is required. |
|
|
80
80
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@romanmatena/browsermonitor",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Browser console, network, DOM monitoring for debugging and LLM workflows. Captures console, network, cookies, screenshot, DOM. HTTP API for LLM/scripts. WSL+Windows, Linux, Open/Join modes.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -3,16 +3,18 @@
|
|
|
3
3
|
|
|
4
4
|
**Why use this:** Do not ask the user to copy or paste content from the browser (DOM, console, network). You can obtain the current page state yourself: the project has browsermonitor; after a dump you read the generated files.
|
|
5
5
|
|
|
6
|
+
**Monitored URL:** `{{DEFAULT_URL}}`
|
|
7
|
+
|
|
6
8
|
**What to do when you need the live page (elements, console, requests):**
|
|
7
|
-
1. Trigger a dump: `curl -s http://localhost:
|
|
9
|
+
1. Trigger a dump: `curl -s http://localhost:{{HTTP_PORT}}/dump` (monitor must be running; user runs `browsermonitor` or `browsermonitor --open` / `--join=9222` to start it).
|
|
8
10
|
2. Read the written files in `.browsermonitor/.puppeteer/`:
|
|
9
11
|
- **.browsermonitor/.puppeteer/dom.html** – current page HTML (JS-modified DOM). Use this for the element tree and structure.
|
|
10
12
|
- **.browsermonitor/.puppeteer/screenshot.png** – screenshot of the current tab viewport.
|
|
11
13
|
- **.browsermonitor/.puppeteer/console.log** – browser console output.
|
|
12
14
|
- **.browsermonitor/.puppeteer/network.log** and **.browsermonitor/.puppeteer/network-log/*.json** – network requests and responses.
|
|
13
15
|
- **.browsermonitor/.puppeteer/cookies/** – cookies per domain (JSON files).
|
|
14
|
-
3. Optional: `curl -s http://localhost:
|
|
15
|
-
4. Optional: `curl -s http://localhost:
|
|
16
|
-
5. Optional: `curl -s http://localhost:
|
|
16
|
+
3. Optional: `curl -s http://localhost:{{HTTP_PORT}}/status` for buffer status.
|
|
17
|
+
4. Optional: `curl -s http://localhost:{{HTTP_PORT}}/tabs` to list tabs; `curl -s "http://localhost:{{HTTP_PORT}}/tab?index=2"` to switch tab.
|
|
18
|
+
5. Optional: `curl -s http://localhost:{{HTTP_PORT}}/clear` to clear buffers.
|
|
17
19
|
|
|
18
20
|
<!-- END browser-monitor-llm-section - LLM: do not remove this block; required for automatic section updates -->
|
package/src/cli.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Configuration in .browsermonitor/settings.json (created on first run).
|
|
7
7
|
*
|
|
8
8
|
* Subcommands:
|
|
9
|
-
* init →
|
|
9
|
+
* init → Create .browsermonitor/, settings.json, update agent files
|
|
10
10
|
*
|
|
11
11
|
* Mode is chosen by arguments:
|
|
12
12
|
* (none) → Interactive: menu (o = open, j = join, q = quit)
|
|
@@ -21,29 +21,49 @@
|
|
|
21
21
|
* --help Show help
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
import {
|
|
24
|
+
import { parseArgs } from 'node:util';
|
|
25
|
+
import { runJoinMode } from './monitor/join-mode.mjs';
|
|
26
|
+
import { runOpenMode } from './monitor/open-mode.mjs';
|
|
25
27
|
import { printAppIntro } from './intro.mjs';
|
|
26
28
|
import { createHttpServer } from './http-server.mjs';
|
|
27
29
|
import { printApiHelpTable } from './templates/api-help.mjs';
|
|
28
30
|
import { printCliCommandsTable } from './templates/cli-commands.mjs';
|
|
29
|
-
import {
|
|
30
|
-
import { loadSettings, isInitialized,
|
|
31
|
+
import { printModeHeading } from './templates/section-heading.mjs';
|
|
32
|
+
import { loadSettings, getPaths, ensureDirectories, isInitialized, DEFAULT_SETTINGS, saveSettings } from './settings.mjs';
|
|
31
33
|
import { runInit } from './init.mjs';
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
34
|
+
import { resolveHttpPort, resolveDefaultUrl, askMode } from './utils/ask.mjs';
|
|
35
|
+
|
|
36
|
+
// ---- Parse CLI arguments ----
|
|
37
|
+
const { values: flags, positionals } = parseArgs({
|
|
38
|
+
args: process.argv.slice(2),
|
|
39
|
+
options: {
|
|
40
|
+
open: { type: 'boolean', default: false },
|
|
41
|
+
join: { type: 'string' },
|
|
42
|
+
port: { type: 'string' },
|
|
43
|
+
realtime: { type: 'boolean', default: false },
|
|
44
|
+
headless: { type: 'boolean', default: false },
|
|
45
|
+
timeout: { type: 'string', default: '0' },
|
|
46
|
+
'nav-timeout': { type: 'string' },
|
|
47
|
+
help: { type: 'boolean', short: 'h', default: false },
|
|
48
|
+
},
|
|
49
|
+
allowPositionals: true,
|
|
50
|
+
strict: false,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const subcommand = positionals[0];
|
|
54
|
+
const urlFromArgs = positionals.find((a) => /^https?:\/\//.test(a) || a.includes('localhost'));
|
|
55
|
+
const joinPort = flags.join ? parseInt(flags.join, 10) : null;
|
|
56
|
+
const httpPortFromArgs = flags.port ? parseInt(flags.port, 10) : null;
|
|
57
|
+
const hardTimeout = parseInt(flags.timeout, 10) || 0;
|
|
58
|
+
|
|
59
|
+
// Handle `browsermonitor init`
|
|
60
|
+
if (subcommand === 'init') {
|
|
61
|
+
await runInit(process.cwd());
|
|
42
62
|
process.exit(0);
|
|
43
63
|
}
|
|
44
64
|
|
|
45
65
|
// Show help
|
|
46
|
-
if (
|
|
66
|
+
if (flags.help) {
|
|
47
67
|
console.log(`
|
|
48
68
|
Browser Monitor – capture browser console, network, and DOM for debugging and LLM workflows.
|
|
49
69
|
|
|
@@ -57,29 +77,23 @@ What it does:
|
|
|
57
77
|
printCliCommandsTable({ showEntry: true, showUsage: true });
|
|
58
78
|
console.log(`
|
|
59
79
|
Subcommands:
|
|
60
|
-
init
|
|
80
|
+
init Create .browsermonitor/, settings.json with defaults, update agent files
|
|
61
81
|
|
|
62
82
|
Modes (chosen by flags; only one applies):
|
|
63
|
-
INTERACTIVE (default) No flag.
|
|
64
|
-
o = open Chrome
|
|
65
|
-
j = join running Chrome (pick instance/tab)
|
|
66
|
-
q = quit
|
|
83
|
+
INTERACTIVE (default) No flag. First run asks HTTP port and URL. Then menu:
|
|
84
|
+
o = open Chrome, j = join running Chrome, q = quit
|
|
67
85
|
|
|
68
86
|
OPEN (--open) Launch a new Chrome and monitor it. URL = first positional or config.
|
|
69
|
-
Uses current directory for logs. Good for local dev with a fresh profile.
|
|
70
87
|
|
|
71
|
-
JOIN (--join=PORT) Attach to
|
|
72
|
-
|
|
73
|
-
running (e.g. started by a script or on another machine via tunnel).
|
|
88
|
+
JOIN (--join=PORT) Attach to existing Chrome with remote debugging on PORT.
|
|
89
|
+
If PORT omitted, scans for running instances.
|
|
74
90
|
|
|
75
91
|
Options:
|
|
76
92
|
--port=PORT HTTP API port (default: from settings or 60001)
|
|
77
|
-
--realtime Write each event to files immediately (default: lazy
|
|
93
|
+
--realtime Write each event to files immediately (default: lazy)
|
|
78
94
|
--headless Run Chrome without GUI
|
|
79
|
-
--open Go directly to open mode
|
|
80
|
-
--join=PORT Go directly to join mode (PORT required)
|
|
81
95
|
--timeout=MS Hard timeout in ms; process exits after (0 = disabled)
|
|
82
|
-
--nav-timeout=MS Navigation timeout in ms (default: from settings
|
|
96
|
+
--nav-timeout=MS Navigation timeout in ms (default: from settings)
|
|
83
97
|
--help, -h Show this help
|
|
84
98
|
|
|
85
99
|
Config (.browsermonitor/settings.json):
|
|
@@ -90,67 +104,42 @@ Config (.browsermonitor/settings.json):
|
|
|
90
104
|
process.exit(0);
|
|
91
105
|
}
|
|
92
106
|
|
|
93
|
-
// Auto-init on first run
|
|
94
|
-
if (!isInitialized(projectRoot)) {
|
|
95
|
-
console.log('[browsermonitor] First run detected. Setting up .browsermonitor/ ...');
|
|
96
|
-
await runInit(projectRoot, { askForUrl: process.stdin.isTTY, updateAgentFiles: true });
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Ensure directories exist (in case user deleted .puppeteer/ subdir)
|
|
100
|
-
ensureDirectories(projectRoot);
|
|
101
|
-
|
|
102
|
-
// Load settings from .browsermonitor/settings.json
|
|
103
|
-
const config = loadSettings(projectRoot);
|
|
104
|
-
const paths = getPaths(projectRoot);
|
|
105
|
-
|
|
106
|
-
// ---- Mode dispatch: --open | --join=PORT | interactive ----
|
|
107
|
-
const openMode = args.some((a) => a === '--open' || a.startsWith('--open='));
|
|
108
|
-
const joinArg = args.find((a) => a.startsWith('--join'));
|
|
109
|
-
let joinPort = null;
|
|
110
|
-
if (joinArg) {
|
|
111
|
-
if (joinArg === '--join' || !joinArg.includes('=')) {
|
|
112
|
-
console.error('Error: --join requires a port (e.g. --join=9222)');
|
|
113
|
-
process.exit(1);
|
|
114
|
-
}
|
|
115
|
-
const portStr = joinArg.split('=')[1];
|
|
116
|
-
joinPort = parseInt(portStr, 10);
|
|
117
|
-
if (Number.isNaN(joinPort) || joinPort < 1 || joinPort > 65535) {
|
|
118
|
-
console.error(`Error: invalid port for --join: ${portStr}`);
|
|
119
|
-
process.exit(1);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Shared options (CLI args override settings.json)
|
|
124
|
-
const realtimeMode = args.includes('--realtime') || config.realtime;
|
|
125
|
-
const headlessCli = args.includes('--headless');
|
|
126
|
-
const timeoutArg = args.find((a) => a.startsWith('--timeout='));
|
|
127
|
-
const hardTimeout = timeoutArg ? parseInt(timeoutArg.split('=')[1], 10) : 0;
|
|
128
|
-
const navTimeoutArg = args.find((a) => a.startsWith('--nav-timeout='));
|
|
129
|
-
const navigationTimeout = navTimeoutArg
|
|
130
|
-
? parseInt(navTimeoutArg.split('=')[1], 10)
|
|
131
|
-
: (config.navigationTimeout !== undefined ? config.navigationTimeout : 60_000);
|
|
132
|
-
const urlFromArgs = args.find((a) => !a.startsWith('--'));
|
|
133
|
-
const url = urlFromArgs || config.defaultUrl || 'https://localhost:4000/';
|
|
134
|
-
const headless = headlessCli || config.headless || false;
|
|
135
|
-
const ignorePatterns = config.ignorePatterns || [];
|
|
136
|
-
const outputDir = projectRoot;
|
|
137
|
-
|
|
138
|
-
const DEFAULT_HTTP_PORT = config.httpPort || 60001;
|
|
139
|
-
const portArg = args.find((a) => a === '--port' || a.startsWith('--port='));
|
|
140
|
-
let httpPortFromArgs = null;
|
|
141
|
-
if (portArg) {
|
|
142
|
-
const val = portArg.includes('=') ? portArg.split('=')[1] : '';
|
|
143
|
-
const num = parseInt(val, 10);
|
|
144
|
-
if (!Number.isNaN(num) && num >= 1 && num <= 65535) httpPortFromArgs = num;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
107
|
(async () => {
|
|
108
|
+
// 1. Intro
|
|
148
109
|
printAppIntro();
|
|
149
110
|
|
|
150
|
-
|
|
111
|
+
// 2. Project root = cwd, load existing settings (may be empty/missing)
|
|
112
|
+
const projectRoot = process.cwd();
|
|
113
|
+
ensureDirectories(projectRoot);
|
|
114
|
+
let config = loadSettings(projectRoot);
|
|
115
|
+
const paths = getPaths(projectRoot);
|
|
116
|
+
|
|
117
|
+
// 3. CLI args override settings.json
|
|
118
|
+
const realtimeMode = flags.realtime || config.realtime;
|
|
119
|
+
const navTimeoutFromArgs = flags['nav-timeout'] ? parseInt(flags['nav-timeout'], 10) : null;
|
|
120
|
+
const navigationTimeout = navTimeoutFromArgs
|
|
121
|
+
?? (config.navigationTimeout !== undefined ? config.navigationTimeout : 60_000);
|
|
122
|
+
const headless = flags.headless || config.headless || false;
|
|
123
|
+
const httpPort = await resolveHttpPort(httpPortFromArgs ?? config.httpPort, DEFAULT_SETTINGS.httpPort);
|
|
124
|
+
const url = await resolveDefaultUrl(urlFromArgs || config.defaultUrl, DEFAULT_SETTINGS.defaultUrl);
|
|
125
|
+
|
|
126
|
+
// 4. Need to initialize project (create .browsermonitor/, settings.json) before showing API info, because API port is part of config
|
|
127
|
+
if (!isInitialized(projectRoot)) {
|
|
128
|
+
saveSettings(projectRoot, { ...DEFAULT_SETTINGS, httpPort, defaultUrl: url, headless, navigationTimeout, realtime: realtimeMode });
|
|
129
|
+
config = loadSettings(projectRoot);
|
|
130
|
+
await runInit(projectRoot, config);
|
|
131
|
+
}
|
|
151
132
|
|
|
152
|
-
|
|
153
|
-
|
|
133
|
+
// 5. Show API/output info (now httpPort is known)
|
|
134
|
+
printApiHelpTable({
|
|
135
|
+
url: config.defaultUrl,
|
|
136
|
+
port: config.httpPort,
|
|
137
|
+
showApi: true,
|
|
138
|
+
showInteractive: false,
|
|
139
|
+
showOutputFiles: true,
|
|
140
|
+
noLeadingNewline: true,
|
|
141
|
+
context: paths,
|
|
142
|
+
});
|
|
154
143
|
|
|
155
144
|
const sharedHttpState = {
|
|
156
145
|
mode: 'interactive',
|
|
@@ -162,41 +151,50 @@ if (portArg) {
|
|
|
162
151
|
getAllTabs: async () => [],
|
|
163
152
|
};
|
|
164
153
|
const sharedHttpServer = createHttpServer({
|
|
165
|
-
port: httpPort,
|
|
166
|
-
defaultPort:
|
|
154
|
+
port: config.httpPort,
|
|
155
|
+
defaultPort: config.httpPort,
|
|
167
156
|
getState: () => sharedHttpState,
|
|
168
157
|
});
|
|
169
158
|
|
|
170
159
|
const commonOptions = {
|
|
171
|
-
|
|
172
|
-
outputDir,
|
|
160
|
+
outputDir: projectRoot,
|
|
173
161
|
paths,
|
|
174
|
-
|
|
162
|
+
realtime: realtimeMode,
|
|
163
|
+
ignorePatterns: config.ignorePatterns,
|
|
175
164
|
hardTimeout,
|
|
176
|
-
httpPort,
|
|
165
|
+
httpPort: config.httpPort,
|
|
166
|
+
joinPort,
|
|
177
167
|
sharedHttpState,
|
|
178
168
|
sharedHttpServer,
|
|
179
169
|
};
|
|
180
170
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
await runOpenMode(
|
|
171
|
+
// 8. Dispatch to mode
|
|
172
|
+
if (flags.open) {
|
|
173
|
+
await runOpenMode(config.defaultUrl, {
|
|
184
174
|
...commonOptions,
|
|
185
175
|
headless,
|
|
186
176
|
navigationTimeout,
|
|
187
177
|
});
|
|
188
178
|
} else if (joinPort !== null) {
|
|
189
|
-
|
|
190
|
-
await runJoinMode(joinPort, {
|
|
191
|
-
...commonOptions,
|
|
192
|
-
defaultUrl: url,
|
|
193
|
-
});
|
|
179
|
+
await runJoinMode(config.defaultUrl, commonOptions);
|
|
194
180
|
} else {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
181
|
+
// No mode flag → ask user
|
|
182
|
+
printModeHeading('Choose mode');
|
|
183
|
+
const mode = await askMode();
|
|
184
|
+
if (mode === 'q') process.exit(0);
|
|
185
|
+
|
|
186
|
+
if (mode === 'o') {
|
|
187
|
+
await runOpenMode(config.defaultUrl, {
|
|
188
|
+
...commonOptions,
|
|
189
|
+
headless,
|
|
190
|
+
navigationTimeout,
|
|
191
|
+
skipModeHeading: true,
|
|
192
|
+
});
|
|
193
|
+
} else if (mode === 'j') {
|
|
194
|
+
await runJoinMode(config.defaultUrl, {
|
|
195
|
+
...commonOptions,
|
|
196
|
+
skipModeHeading: true,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
201
199
|
}
|
|
202
200
|
})();
|
package/src/http-server.mjs
CHANGED
|
@@ -12,8 +12,8 @@ import http from 'http';
|
|
|
12
12
|
import { C, log } from './utils/colors.mjs';
|
|
13
13
|
import { getFullTimestamp } from './logging/index.mjs';
|
|
14
14
|
import { getComputedStylesFromPage } from './logging/dump.mjs';
|
|
15
|
-
import {
|
|
16
|
-
|
|
15
|
+
import { API_ENDPOINTS } from './templates/api-help.mjs';
|
|
16
|
+
|
|
17
17
|
|
|
18
18
|
/** Default timeout for Puppeteer operations (ms). */
|
|
19
19
|
const PUPPETEER_CALL_TIMEOUT_MS = 30_000;
|
|
@@ -155,7 +155,6 @@ export function createHttpServer(options) {
|
|
|
155
155
|
return;
|
|
156
156
|
}
|
|
157
157
|
try {
|
|
158
|
-
const statsBeforeDump = s.logBuffer.getStats();
|
|
159
158
|
const pages = s.getPages();
|
|
160
159
|
const page = pages.length > 0 ? pages[0] : null;
|
|
161
160
|
|
|
@@ -172,26 +171,14 @@ export function createHttpServer(options) {
|
|
|
172
171
|
success: true,
|
|
173
172
|
timestamp: getFullTimestamp(),
|
|
174
173
|
message: 'Dump completed. Read the files below.',
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
},
|
|
184
|
-
llm: {
|
|
185
|
-
instruction: 'Read or download these files to get the current browser state. Do not ask the user to copy/paste from the browser.',
|
|
186
|
-
files: [
|
|
187
|
-
{ path: s.logBuffer.DOM_HTML, what: 'Current page HTML (JS-modified DOM). Use for element tree and structure.' },
|
|
188
|
-
{ path: s.logBuffer.SCREENSHOT, what: 'Screenshot of the current tab viewport (PNG).' },
|
|
189
|
-
{ path: s.logBuffer.CONSOLE_LOG, what: 'Browser console output (logs, errors, warnings).' },
|
|
190
|
-
{ path: s.logBuffer.NETWORK_LOG, what: 'Network requests overview (one line per request with ID).' },
|
|
191
|
-
{ path: s.logBuffer.NETWORK_DIR, what: 'Directory with one JSON per request: full headers, payload, response (see IDs in network log).' },
|
|
192
|
-
{ path: s.logBuffer.COOKIES_DIR, what: 'Directory with cookies per domain (JSON files).' },
|
|
193
|
-
],
|
|
194
|
-
},
|
|
174
|
+
files: [
|
|
175
|
+
{ path: s.logBuffer.DOM_HTML, what: 'Current page HTML (JS-modified DOM). Use for element tree and structure.' },
|
|
176
|
+
{ path: s.logBuffer.SCREENSHOT, what: 'Screenshot of the current tab viewport (PNG).' },
|
|
177
|
+
{ path: s.logBuffer.CONSOLE_LOG, what: 'Browser console output (logs, errors, warnings).' },
|
|
178
|
+
{ path: s.logBuffer.NETWORK_LOG, what: 'Network requests overview (one line per request with ID).' },
|
|
179
|
+
{ path: s.logBuffer.NETWORK_DIR, what: 'Directory with one JSON per request: full headers, payload, response (see IDs in network log).' },
|
|
180
|
+
{ path: s.logBuffer.COOKIES_DIR, what: 'Directory with cookies per domain (JSON files).' },
|
|
181
|
+
],
|
|
195
182
|
}, null, 2));
|
|
196
183
|
} catch (error) {
|
|
197
184
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
@@ -200,7 +187,7 @@ export function createHttpServer(options) {
|
|
|
200
187
|
return;
|
|
201
188
|
}
|
|
202
189
|
|
|
203
|
-
// GET /status
|
|
190
|
+
// GET /status — state only (no file paths; use GET /dump for that)
|
|
204
191
|
if (req.url === '/status' && req.method === 'GET') {
|
|
205
192
|
const pages = s.getPages();
|
|
206
193
|
const collectingPaused = s.getCollectingPaused();
|
|
@@ -215,14 +202,6 @@ export function createHttpServer(options) {
|
|
|
215
202
|
if (!noBrowser) {
|
|
216
203
|
payload.collecting = collectingPaused ? 'paused' : 'running';
|
|
217
204
|
payload.stats = s.logBuffer.getStats();
|
|
218
|
-
payload.outputFiles = {
|
|
219
|
-
consoleLog: s.logBuffer.CONSOLE_LOG,
|
|
220
|
-
networkLog: s.logBuffer.NETWORK_LOG,
|
|
221
|
-
networkDir: s.logBuffer.NETWORK_DIR,
|
|
222
|
-
cookiesDir: s.logBuffer.COOKIES_DIR,
|
|
223
|
-
domHtml: s.logBuffer.DOM_HTML,
|
|
224
|
-
screenshot: s.logBuffer.SCREENSHOT,
|
|
225
|
-
};
|
|
226
205
|
} else {
|
|
227
206
|
payload.message = 'No browser. Use interactive (o/j) or --open / --join.';
|
|
228
207
|
}
|
|
@@ -500,12 +479,7 @@ export function createHttpServer(options) {
|
|
|
500
479
|
}, null, 2));
|
|
501
480
|
});
|
|
502
481
|
|
|
503
|
-
server.listen(port, host
|
|
504
|
-
const url = `http://${host}:${port}`;
|
|
505
|
-
const changed = port !== defaultPort ? ` ${C.red}(changed)${C.reset}` : '';
|
|
506
|
-
const title = `HTTP API URL: ${url}${changed}`;
|
|
507
|
-
printSectionHeading(title, ' ');
|
|
508
|
-
});
|
|
482
|
+
server.listen(port, host);
|
|
509
483
|
|
|
510
484
|
server.on('error', (err) => {
|
|
511
485
|
if (err.code === 'EADDRINUSE') {
|
package/src/init.mjs
CHANGED
|
@@ -1,26 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* browsermonitor init –
|
|
3
|
-
*
|
|
4
|
-
* Called by:
|
|
5
|
-
* - `browsermonitor init` subcommand (explicit)
|
|
6
|
-
* - Auto-init on first run when .browsermonitor/ does not exist
|
|
7
|
-
*
|
|
8
|
-
* What it does:
|
|
9
|
-
* 1. Creates .browsermonitor/ directory structure
|
|
10
|
-
* 2. Creates settings.json with defaults (prompts for URL if TTY)
|
|
11
|
-
* 3. Updates CLAUDE.md, AGENTS.md, memory.md with Browser Monitor section
|
|
12
|
-
* 4. Suggests adding .browsermonitor/ to .gitignore
|
|
2
|
+
* browsermonitor init – creates settings.json with defaults, updates agent files.
|
|
13
3
|
*/
|
|
14
4
|
|
|
15
5
|
import fs from 'fs';
|
|
16
6
|
import path from 'path';
|
|
17
7
|
import { fileURLToPath } from 'url';
|
|
18
8
|
import {
|
|
19
|
-
DEFAULT_SETTINGS,
|
|
20
9
|
ensureDirectories,
|
|
21
|
-
|
|
22
|
-
|
|
10
|
+
loadSettings,
|
|
11
|
+
DEFAULT_SETTINGS,
|
|
23
12
|
} from './settings.mjs';
|
|
13
|
+
import { C } from './utils/colors.mjs';
|
|
14
|
+
import { printBulletBox } from './templates/section-heading.mjs';
|
|
24
15
|
|
|
25
16
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
26
17
|
|
|
@@ -31,132 +22,86 @@ const TEMPLATE_PATH = path.resolve(__dirname, 'agents.llm/browser-monitor-sectio
|
|
|
31
22
|
|
|
32
23
|
/**
|
|
33
24
|
* Replace existing tagged block or append template to a doc file.
|
|
34
|
-
* Section is identified by BEGIN/END tags.
|
|
35
|
-
* @param {string} hostDir
|
|
36
|
-
* @param {string} docFilename - e.g. 'CLAUDE.md', 'AGENTS.md', 'memory.md'
|
|
37
|
-
* @param {string} templateContent - full block including BEGIN and END tags
|
|
38
|
-
* @returns {boolean} true if file was updated
|
|
39
25
|
*/
|
|
40
26
|
function replaceOrAppendSection(hostDir, docFilename, templateContent) {
|
|
41
27
|
const hostPath = path.join(hostDir, docFilename);
|
|
42
|
-
if (!fs.existsSync(hostPath)) return
|
|
28
|
+
if (!fs.existsSync(hostPath)) return null;
|
|
43
29
|
|
|
44
30
|
const content = fs.readFileSync(hostPath, 'utf8');
|
|
45
31
|
const trimmedTemplate = templateContent.trimEnd();
|
|
46
32
|
const beginIndex = content.indexOf(BEGIN_TAG_PREFIX);
|
|
47
33
|
|
|
48
34
|
let newContent;
|
|
35
|
+
let action;
|
|
49
36
|
if (beginIndex === -1) {
|
|
50
37
|
newContent = content.trimEnd() + '\n\n' + trimmedTemplate + '\n';
|
|
51
|
-
|
|
38
|
+
action = 'appended';
|
|
52
39
|
} else {
|
|
53
40
|
const endTagStartIndex = content.indexOf(END_TAG_PREFIX, beginIndex);
|
|
54
|
-
if (endTagStartIndex === -1)
|
|
55
|
-
console.error(`[browsermonitor] ${docFilename}: BEGIN tag found but no END tag; skipping`);
|
|
56
|
-
return false;
|
|
57
|
-
}
|
|
41
|
+
if (endTagStartIndex === -1) return null;
|
|
58
42
|
const afterEndComment = content.indexOf('-->', endTagStartIndex) + 3;
|
|
59
43
|
const lineEnd = content.indexOf('\n', afterEndComment);
|
|
60
44
|
const endIndex = lineEnd === -1 ? content.length : lineEnd + 1;
|
|
61
45
|
newContent = content.slice(0, beginIndex) + trimmedTemplate + '\n' + content.slice(endIndex);
|
|
62
|
-
|
|
46
|
+
action = 'replaced';
|
|
63
47
|
}
|
|
64
48
|
|
|
65
49
|
try {
|
|
66
50
|
fs.writeFileSync(hostPath, newContent);
|
|
67
|
-
return
|
|
68
|
-
} catch
|
|
69
|
-
|
|
70
|
-
return false;
|
|
51
|
+
return action;
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
71
54
|
}
|
|
72
55
|
}
|
|
73
56
|
|
|
74
|
-
/**
|
|
75
|
-
* Prompt user for default URL (stdin line read).
|
|
76
|
-
* @param {string} defaultValue
|
|
77
|
-
* @returns {Promise<string>}
|
|
78
|
-
*/
|
|
79
|
-
function askDefaultUrl(defaultValue) {
|
|
80
|
-
return new Promise((resolve) => {
|
|
81
|
-
process.stdout.write(` Default URL [${defaultValue}]: `);
|
|
82
|
-
process.stdin.resume();
|
|
83
|
-
process.stdin.setEncoding('utf8');
|
|
84
|
-
process.stdin.once('data', (chunk) => {
|
|
85
|
-
process.stdin.pause();
|
|
86
|
-
const trimmed = chunk.toString().trim().split('\n')[0].trim();
|
|
87
|
-
resolve(trimmed || defaultValue);
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Suggest adding .browsermonitor/ to .gitignore if not already present.
|
|
94
|
-
* @param {string} projectRoot
|
|
95
|
-
*/
|
|
96
|
-
function suggestGitignore(projectRoot) {
|
|
97
|
-
const gitignorePath = path.join(projectRoot, '.gitignore');
|
|
98
|
-
if (!fs.existsSync(gitignorePath)) return;
|
|
99
|
-
|
|
100
|
-
const content = fs.readFileSync(gitignorePath, 'utf8');
|
|
101
|
-
if (content.includes('.browsermonitor')) return;
|
|
102
|
-
|
|
103
|
-
console.log(`[browsermonitor] Tip: add .browsermonitor/ to your .gitignore`);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
57
|
/**
|
|
107
58
|
* Run browsermonitor initialization.
|
|
108
|
-
* @param {string} projectRoot - Absolute path to the project
|
|
109
|
-
* @param {Object} [options]
|
|
110
|
-
* @param {boolean} [options.askForUrl=true] - Prompt for default URL
|
|
111
|
-
* @param {boolean} [options.updateAgentFiles=true] - Update CLAUDE.md/AGENTS.md/memory.md
|
|
112
59
|
*/
|
|
113
|
-
export async function runInit(projectRoot,
|
|
114
|
-
|
|
60
|
+
export async function runInit(projectRoot, config = {}) {
|
|
61
|
+
// Merge: explicit config > saved settings > defaults
|
|
62
|
+
const saved = loadSettings(projectRoot);
|
|
63
|
+
config = { ...DEFAULT_SETTINGS, ...saved, ...config };
|
|
64
|
+
const { updateAgentFiles = true } = config;
|
|
115
65
|
|
|
116
|
-
console.log('');
|
|
117
|
-
console.log('========================================');
|
|
118
|
-
console.log(' Browser Monitor - Setup');
|
|
119
|
-
console.log('========================================');
|
|
120
|
-
console.log('');
|
|
121
|
-
console.log(`[browsermonitor] Project: ${projectRoot}`);
|
|
122
|
-
console.log('');
|
|
123
|
-
|
|
124
|
-
// 1. Create directory structure
|
|
125
66
|
ensureDirectories(projectRoot);
|
|
126
|
-
console.log('[browsermonitor] Created .browsermonitor/ directory structure');
|
|
127
67
|
|
|
128
|
-
//
|
|
129
|
-
const
|
|
130
|
-
if (
|
|
131
|
-
let
|
|
132
|
-
|
|
133
|
-
|
|
68
|
+
// Update agent files (render template with settings values)
|
|
69
|
+
const agentUpdates = [];
|
|
70
|
+
if (updateAgentFiles && fs.existsSync(TEMPLATE_PATH)) {
|
|
71
|
+
let templateContent = fs.readFileSync(TEMPLATE_PATH, 'utf8');
|
|
72
|
+
templateContent = templateContent
|
|
73
|
+
.replace(/\{\{DEFAULT_URL\}\}/g, config.defaultUrl)
|
|
74
|
+
.replace(/\{\{HTTP_PORT\}\}/g, String(config.httpPort));
|
|
75
|
+
for (const docFile of ['CLAUDE.md', 'AGENTS.md', 'memory.md']) {
|
|
76
|
+
const action = replaceOrAppendSection(projectRoot, docFile, templateContent);
|
|
77
|
+
if (action) agentUpdates.push(`${action} ${C.cyan}${docFile}${C.reset}`);
|
|
134
78
|
}
|
|
135
|
-
const settings = { ...DEFAULT_SETTINGS, defaultUrl };
|
|
136
|
-
saveSettings(projectRoot, settings);
|
|
137
|
-
console.log(`[browsermonitor] Created settings.json (defaultUrl: ${defaultUrl})`);
|
|
138
|
-
} else {
|
|
139
|
-
console.log('[browsermonitor] settings.json already exists, skipping');
|
|
140
79
|
}
|
|
141
80
|
|
|
142
|
-
//
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
replaceOrAppendSection(projectRoot, 'AGENTS.md', templateContent);
|
|
150
|
-
replaceOrAppendSection(projectRoot, 'memory.md', templateContent);
|
|
81
|
+
// Gitignore check
|
|
82
|
+
let gitignoreHint = null;
|
|
83
|
+
const gitignorePath = path.join(projectRoot, '.gitignore');
|
|
84
|
+
if (fs.existsSync(gitignorePath)) {
|
|
85
|
+
const content = fs.readFileSync(gitignorePath, 'utf8');
|
|
86
|
+
if (!content.includes('.browsermonitor')) {
|
|
87
|
+
gitignoreHint = `${C.yellow}Tip:${C.reset} add ${C.cyan}.browsermonitor/${C.reset} to .gitignore`;
|
|
151
88
|
}
|
|
152
89
|
}
|
|
153
90
|
|
|
154
|
-
//
|
|
155
|
-
|
|
91
|
+
// Display results
|
|
92
|
+
const lines = [
|
|
93
|
+
`${C.cyan}Project:${C.reset} ${projectRoot}`,
|
|
94
|
+
`${C.green}Created${C.reset} .browsermonitor/`,
|
|
95
|
+
];
|
|
96
|
+
if (config.defaultUrl) {
|
|
97
|
+
lines[1] += ` → ${C.cyan}${config.defaultUrl}${C.reset}`;
|
|
98
|
+
}
|
|
99
|
+
if (agentUpdates.length > 0) {
|
|
100
|
+
lines.push(`${C.green}Agent docs:${C.reset} ${agentUpdates.join(', ')}`);
|
|
101
|
+
}
|
|
102
|
+
if (gitignoreHint) lines.push(gitignoreHint);
|
|
156
103
|
|
|
157
104
|
console.log('');
|
|
158
|
-
|
|
159
|
-
console.log(' browsermonitor - Interactive menu (o=open, j=join, q=quit)');
|
|
160
|
-
console.log(' browsermonitor --open - Launch Chrome at default URL');
|
|
105
|
+
printBulletBox(lines);
|
|
161
106
|
console.log('');
|
|
162
107
|
}
|