@monoes/monobrowse 1.0.9 → 1.0.12
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 +1 -1
- package/dist/src/__tests__/adapters.test.js +5 -2
- package/dist/src/__tests__/adapters.test.js.map +1 -1
- package/dist/src/__tests__/batch-eval-parsing.test.js +4 -2
- package/dist/src/__tests__/batch-eval-parsing.test.js.map +1 -1
- package/dist/src/__tests__/box-model.test.js +9 -5
- package/dist/src/__tests__/box-model.test.js.map +1 -1
- package/dist/src/__tests__/browser-launch.test.d.ts +10 -0
- package/dist/src/__tests__/browser-launch.test.d.ts.map +1 -1
- package/dist/src/__tests__/browser-launch.test.js +17 -10
- package/dist/src/__tests__/browser-launch.test.js.map +1 -1
- package/dist/src/__tests__/cdp-client.test.js +9 -3
- package/dist/src/__tests__/cdp-client.test.js.map +1 -1
- package/dist/src/__tests__/close-browser.test.d.ts +13 -0
- package/dist/src/__tests__/close-browser.test.d.ts.map +1 -1
- package/dist/src/__tests__/close-browser.test.js +40 -26
- package/dist/src/__tests__/close-browser.test.js.map +1 -1
- package/dist/src/__tests__/console-log.test.js +17 -6
- package/dist/src/__tests__/console-log.test.js.map +1 -1
- package/dist/src/__tests__/find.test.js +26 -6
- package/dist/src/__tests__/find.test.js.map +1 -1
- package/dist/src/__tests__/frame-command.test.js +9 -3
- package/dist/src/__tests__/frame-command.test.js.map +1 -1
- package/dist/src/__tests__/open-url-navigate-error.test.js +1 -3
- package/dist/src/__tests__/open-url-navigate-error.test.js.map +1 -1
- package/dist/src/__tests__/output.test.js +1 -1
- package/dist/src/__tests__/output.test.js.map +1 -1
- package/dist/src/__tests__/reap-idle-browser.test.d.ts +15 -0
- package/dist/src/__tests__/reap-idle-browser.test.d.ts.map +1 -0
- package/dist/src/__tests__/reap-idle-browser.test.js +238 -0
- package/dist/src/__tests__/reap-idle-browser.test.js.map +1 -0
- package/dist/src/__tests__/ref-cache.test.js +18 -10
- package/dist/src/__tests__/ref-cache.test.js.map +1 -1
- package/dist/src/__tests__/session-state.test.d.ts +8 -0
- package/dist/src/__tests__/session-state.test.d.ts.map +1 -1
- package/dist/src/__tests__/session-state.test.js +10 -10
- package/dist/src/__tests__/session-state.test.js.map +1 -1
- package/dist/src/__tests__/snapshot.test.js +45 -15
- package/dist/src/__tests__/snapshot.test.js.map +1 -1
- package/dist/src/__tests__/storage-eval.test.js +14 -6
- package/dist/src/__tests__/storage-eval.test.js.map +1 -1
- package/dist/src/__tests__/tabs.test.js +25 -9
- package/dist/src/__tests__/tabs.test.js.map +1 -1
- package/dist/src/browser/action-builder/analyzer.d.ts +1 -1
- package/dist/src/browser/action-builder/analyzer.d.ts.map +1 -1
- package/dist/src/browser/action-builder/analyzer.js +25 -9
- package/dist/src/browser/action-builder/analyzer.js.map +1 -1
- package/dist/src/browser/action-builder/types.d.ts.map +1 -1
- package/dist/src/browser/actions.d.ts +1 -1
- package/dist/src/browser/actions.d.ts.map +1 -1
- package/dist/src/browser/actions.js +16 -6
- package/dist/src/browser/actions.js.map +1 -1
- package/dist/src/browser/adapters/gemini.d.ts.map +1 -1
- package/dist/src/browser/adapters/gemini.js.map +1 -1
- package/dist/src/browser/adapters/google.d.ts.map +1 -1
- package/dist/src/browser/adapters/google.js.map +1 -1
- package/dist/src/browser/adapters/index.d.ts.map +1 -1
- package/dist/src/browser/adapters/index.js +3 -3
- package/dist/src/browser/adapters/index.js.map +1 -1
- package/dist/src/browser/adapters/instagram.d.ts.map +1 -1
- package/dist/src/browser/adapters/instagram.js.map +1 -1
- package/dist/src/browser/adapters/linkedin.d.ts.map +1 -1
- package/dist/src/browser/adapters/linkedin.js.map +1 -1
- package/dist/src/browser/adapters/microsoft.d.ts.map +1 -1
- package/dist/src/browser/adapters/microsoft.js.map +1 -1
- package/dist/src/browser/adapters/x.d.ts.map +1 -1
- package/dist/src/browser/adapters/x.js.map +1 -1
- package/dist/src/browser/browser.d.ts +30 -0
- package/dist/src/browser/browser.d.ts.map +1 -1
- package/dist/src/browser/browser.js +122 -15
- package/dist/src/browser/browser.js.map +1 -1
- package/dist/src/browser/cdp.d.ts.map +1 -1
- package/dist/src/browser/cdp.js +21 -8
- package/dist/src/browser/cdp.js.map +1 -1
- package/dist/src/browser/console-log.d.ts.map +1 -1
- package/dist/src/browser/console-log.js +2 -2
- package/dist/src/browser/console-log.js.map +1 -1
- package/dist/src/browser/dashboard/server.d.ts.map +1 -1
- package/dist/src/browser/dashboard/server.js +29 -19
- package/dist/src/browser/dashboard/server.js.map +1 -1
- package/dist/src/browser/dashboard/ui.html +153 -165
- package/dist/src/browser/dialog.d.ts.map +1 -1
- package/dist/src/browser/dialog.js +3 -1
- package/dist/src/browser/dialog.js.map +1 -1
- package/dist/src/browser/emulation.d.ts.map +1 -1
- package/dist/src/browser/emulation.js +21 -6
- package/dist/src/browser/emulation.js.map +1 -1
- package/dist/src/browser/find.d.ts +4 -4
- package/dist/src/browser/find.d.ts.map +1 -1
- package/dist/src/browser/find.js +14 -6
- package/dist/src/browser/find.js.map +1 -1
- package/dist/src/browser/har.d.ts.map +1 -1
- package/dist/src/browser/har.js +14 -7
- package/dist/src/browser/har.js.map +1 -1
- package/dist/src/browser/index.d.ts +16 -16
- package/dist/src/browser/index.d.ts.map +1 -1
- package/dist/src/browser/index.js +16 -16
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/browser/network.d.ts.map +1 -1
- package/dist/src/browser/network.js +14 -4
- package/dist/src/browser/network.js.map +1 -1
- package/dist/src/browser/pdf.d.ts.map +1 -1
- package/dist/src/browser/pdf.js +3 -3
- package/dist/src/browser/pdf.js.map +1 -1
- package/dist/src/browser/profiler.d.ts.map +1 -1
- package/dist/src/browser/profiler.js +10 -5
- package/dist/src/browser/profiler.js.map +1 -1
- package/dist/src/browser/record.d.ts +1 -1
- package/dist/src/browser/record.d.ts.map +1 -1
- package/dist/src/browser/record.js +18 -7
- package/dist/src/browser/record.js.map +1 -1
- package/dist/src/browser/ref-cache.d.ts.map +1 -1
- package/dist/src/browser/ref-cache.js +9 -4
- package/dist/src/browser/ref-cache.js.map +1 -1
- package/dist/src/browser/screenshot.d.ts.map +1 -1
- package/dist/src/browser/screenshot.js +11 -7
- package/dist/src/browser/screenshot.js.map +1 -1
- package/dist/src/browser/session.d.ts +1 -1
- package/dist/src/browser/session.d.ts.map +1 -1
- package/dist/src/browser/session.js +27 -8
- package/dist/src/browser/session.js.map +1 -1
- package/dist/src/browser/snapshot.d.ts +1 -1
- package/dist/src/browser/snapshot.d.ts.map +1 -1
- package/dist/src/browser/snapshot.js +8 -4
- package/dist/src/browser/snapshot.js.map +1 -1
- package/dist/src/browser/storage.d.ts.map +1 -1
- package/dist/src/browser/storage.js.map +1 -1
- package/dist/src/browser/tabs.d.ts.map +1 -1
- package/dist/src/browser/tabs.js +9 -3
- package/dist/src/browser/tabs.js.map +1 -1
- package/dist/src/browser/trace.d.ts.map +1 -1
- package/dist/src/browser/trace.js +3 -3
- package/dist/src/browser/trace.js.map +1 -1
- package/dist/src/browser/types.d.ts.map +1 -1
- package/dist/src/browser/types.js +23 -4
- package/dist/src/browser/types.js.map +1 -1
- package/dist/src/browser/vitals.d.ts.map +1 -1
- package/dist/src/browser/vitals.js +1 -1
- package/dist/src/browser/vitals.js.map +1 -1
- package/dist/src/browser/wait.d.ts.map +1 -1
- package/dist/src/browser/wait.js +11 -5
- package/dist/src/browser/wait.js.map +1 -1
- package/dist/src/cli/action.d.ts.map +1 -1
- package/dist/src/cli/action.js +14 -11
- package/dist/src/cli/action.js.map +1 -1
- package/dist/src/cli/commands.d.ts.map +1 -1
- package/dist/src/cli/commands.js +453 -159
- package/dist/src/cli/commands.js.map +1 -1
- package/dist/src/cli/output.js +5 -5
- package/dist/src/cli/output.js.map +1 -1
- package/dist/src/cli/platform.d.ts.map +1 -1
- package/dist/src/cli/platform.js +15 -11
- package/dist/src/cli/platform.js.map +1 -1
- package/dist/src/cli/types.d.ts +1 -1
- package/dist/src/cli/types.d.ts.map +1 -1
- package/dist/src/cli.js +21 -9
- package/dist/src/cli.js.map +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -2
- package/dist/src/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/__tests__/adapters.test.ts +15 -8
- package/src/__tests__/batch-eval-parsing.test.ts +4 -2
- package/src/__tests__/box-model.test.ts +9 -5
- package/src/__tests__/browser-launch.test.ts +26 -15
- package/src/__tests__/cdp-client.test.ts +25 -9
- package/src/__tests__/close-browser.test.ts +49 -26
- package/src/__tests__/console-log.test.ts +36 -13
- package/src/__tests__/find.test.ts +58 -20
- package/src/__tests__/frame-command.test.ts +12 -6
- package/src/__tests__/open-url-navigate-error.test.ts +2 -4
- package/src/__tests__/output.test.ts +1 -1
- package/src/__tests__/reap-idle-browser.test.ts +276 -0
- package/src/__tests__/ref-cache.test.ts +25 -12
- package/src/__tests__/session-state.test.ts +40 -28
- package/src/__tests__/snapshot.test.ts +61 -22
- package/src/__tests__/storage-eval.test.ts +30 -16
- package/src/__tests__/tabs.test.ts +26 -10
- package/src/browser/action-builder/analyzer.ts +39 -15
- package/src/browser/action-builder/types.ts +45 -8
- package/src/browser/actions.ts +288 -123
- package/src/browser/adapters/gemini.ts +7 -3
- package/src/browser/adapters/google.ts +5 -3
- package/src/browser/adapters/index.ts +5 -4
- package/src/browser/adapters/instagram.ts +4 -2
- package/src/browser/adapters/linkedin.ts +4 -2
- package/src/browser/adapters/microsoft.ts +9 -5
- package/src/browser/adapters/x.ts +7 -3
- package/src/browser/browser.ts +188 -42
- package/src/browser/cdp.ts +43 -11
- package/src/browser/console-log.ts +24 -7
- package/src/browser/dashboard/server.ts +44 -25
- package/src/browser/dashboard/ui.html +153 -165
- package/src/browser/dialog.ts +25 -8
- package/src/browser/emulation.ts +86 -36
- package/src/browser/find.ts +110 -47
- package/src/browser/har.ts +58 -26
- package/src/browser/index.ts +16 -16
- package/src/browser/network.ts +147 -46
- package/src/browser/pdf.ts +4 -4
- package/src/browser/profiler.ts +23 -10
- package/src/browser/record.ts +38 -19
- package/src/browser/ref-cache.ts +38 -17
- package/src/browser/screenshot.ts +58 -32
- package/src/browser/session.ts +51 -18
- package/src/browser/snapshot.ts +34 -20
- package/src/browser/storage.ts +73 -17
- package/src/browser/tabs.ts +29 -17
- package/src/browser/trace.ts +14 -10
- package/src/browser/types.ts +23 -4
- package/src/browser/vitals.ts +33 -16
- package/src/browser/wait.ts +47 -21
- package/src/cli/action.ts +52 -21
- package/src/cli/commands.ts +1093 -385
- package/src/cli/output.ts +5 -5
- package/src/cli/platform.ts +44 -23
- package/src/cli/types.ts +1 -1
- package/src/cli.ts +34 -18
- package/src/index.ts +2 -2
package/dist/src/cli/commands.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Browse Command — Native browser automation via Chrome DevTools Protocol
|
|
3
3
|
* Provides ref-based element model and token-efficient accessibility snapshots
|
|
4
4
|
*/
|
|
5
|
-
import { output } from './output.js';
|
|
6
5
|
import { createActionCommand } from './action.js';
|
|
6
|
+
import { output } from './output.js';
|
|
7
7
|
import { createPlatformCommand } from './platform.js';
|
|
8
8
|
// Runtime state (single session per CLI process)
|
|
9
9
|
const DEFAULT_PORT = 9222;
|
|
@@ -50,7 +50,9 @@ function ensureSignalCleanupHandlers() {
|
|
|
50
50
|
try {
|
|
51
51
|
process.kill(pid, 'SIGKILL');
|
|
52
52
|
}
|
|
53
|
-
catch {
|
|
53
|
+
catch {
|
|
54
|
+
/* already exited */
|
|
55
|
+
}
|
|
54
56
|
}
|
|
55
57
|
}
|
|
56
58
|
}
|
|
@@ -78,7 +80,9 @@ async function resolveDefaultPort(browser) {
|
|
|
78
80
|
// user's debug port and silently swap which browser commands act on —
|
|
79
81
|
// fail loudly instead.
|
|
80
82
|
try {
|
|
81
|
-
await fetch(`http://127.0.0.1:${persisted.port}/json/version`, {
|
|
83
|
+
await fetch(`http://127.0.0.1:${persisted.port}/json/version`, {
|
|
84
|
+
signal: AbortSignal.timeout(1500),
|
|
85
|
+
});
|
|
82
86
|
}
|
|
83
87
|
catch {
|
|
84
88
|
await browser.clearActivePort();
|
|
@@ -89,7 +93,7 @@ async function resolveDefaultPort(browser) {
|
|
|
89
93
|
}
|
|
90
94
|
async function ensureConnected(port, targetId) {
|
|
91
95
|
const browser = await getBrowser();
|
|
92
|
-
if (!_client
|
|
96
|
+
if (!_client?.isConnected()) {
|
|
93
97
|
if (_client && _sessionId) {
|
|
94
98
|
browser.teardownRouteInterception(_sessionId);
|
|
95
99
|
browser.stopRequestCapture(_sessionId);
|
|
@@ -146,20 +150,28 @@ async function detectAttentionNeeded(client, sessionId, url) {
|
|
|
146
150
|
if (ATTENTION_URL_RE.test(url))
|
|
147
151
|
return 'login';
|
|
148
152
|
try {
|
|
149
|
-
const hasPassword = await client
|
|
153
|
+
const hasPassword = await client
|
|
154
|
+
.send('Runtime.evaluate', {
|
|
150
155
|
expression: '!!document.querySelector("input[type=password]")',
|
|
151
156
|
returnByValue: true,
|
|
152
|
-
}, sessionId)
|
|
157
|
+
}, sessionId)
|
|
158
|
+
.then((r) => r.result?.value === true)
|
|
159
|
+
.catch(() => false);
|
|
153
160
|
if (hasPassword)
|
|
154
161
|
return 'login';
|
|
155
|
-
const hasCaptcha = await client
|
|
162
|
+
const hasCaptcha = await client
|
|
163
|
+
.send('Runtime.evaluate', {
|
|
156
164
|
expression: '!!(document.querySelector("iframe[src*=recaptcha]") || document.querySelector("iframe[src*=hcaptcha]") || document.querySelector(".g-recaptcha") || document.querySelector(".h-captcha") || document.querySelector("[data-sitekey]"))',
|
|
157
165
|
returnByValue: true,
|
|
158
|
-
}, sessionId)
|
|
166
|
+
}, sessionId)
|
|
167
|
+
.then((r) => r.result?.value === true)
|
|
168
|
+
.catch(() => false);
|
|
159
169
|
if (hasCaptcha)
|
|
160
170
|
return 'captcha';
|
|
161
171
|
}
|
|
162
|
-
catch {
|
|
172
|
+
catch {
|
|
173
|
+
/* ignore CDP errors */
|
|
174
|
+
}
|
|
163
175
|
return null;
|
|
164
176
|
}
|
|
165
177
|
function waitForEnter() {
|
|
@@ -167,12 +179,16 @@ function waitForEnter() {
|
|
|
167
179
|
// Non-interactive context (MCP server, piped input, CI) — cannot safely read stdin.
|
|
168
180
|
// Auto-continue after a short grace period so automation isn't blocked.
|
|
169
181
|
output.printWarning('Non-interactive mode: auto-continuing in 30s. Switch to headed manually if needed.');
|
|
170
|
-
return new Promise(resolve => setTimeout(resolve, 30_000));
|
|
182
|
+
return new Promise((resolve) => setTimeout(resolve, 30_000));
|
|
171
183
|
}
|
|
172
|
-
return new Promise(resolve => {
|
|
184
|
+
return new Promise((resolve) => {
|
|
173
185
|
process.stdin.resume();
|
|
174
186
|
process.stdin.setEncoding('utf8');
|
|
175
|
-
const onData = () => {
|
|
187
|
+
const onData = () => {
|
|
188
|
+
process.stdin.pause();
|
|
189
|
+
process.stdin.off('data', onData);
|
|
190
|
+
resolve();
|
|
191
|
+
};
|
|
176
192
|
process.stdin.once('data', onData);
|
|
177
193
|
});
|
|
178
194
|
}
|
|
@@ -184,7 +200,9 @@ async function switchToHeaded(url, port) {
|
|
|
184
200
|
try {
|
|
185
201
|
savedCookies = await browser.getCookies(_client, _sessionId);
|
|
186
202
|
}
|
|
187
|
-
catch {
|
|
203
|
+
catch {
|
|
204
|
+
/* ignore */
|
|
205
|
+
}
|
|
188
206
|
browser.teardownRouteInterception(_sessionId);
|
|
189
207
|
browser.stopRequestCapture(_sessionId);
|
|
190
208
|
browser.teardownDialogHandling(_sessionId);
|
|
@@ -219,14 +237,18 @@ async function switchToHeaded(url, port) {
|
|
|
219
237
|
try {
|
|
220
238
|
await browser.setCookies(_client, _sessionId, savedCookies);
|
|
221
239
|
}
|
|
222
|
-
catch {
|
|
240
|
+
catch {
|
|
241
|
+
/* ignore */
|
|
242
|
+
}
|
|
223
243
|
}
|
|
224
244
|
await browser.openUrl(_client, _sessionId, url);
|
|
225
245
|
output.printInfo('Browser window opened. Complete the required action (login / CAPTCHA), then press Enter to continue in headless mode...');
|
|
226
246
|
await waitForEnter();
|
|
227
247
|
// Capture post-auth cookies
|
|
228
248
|
const authCookies = await browser.getCookies(_client, _sessionId).catch(() => []);
|
|
229
|
-
const authLocalStorage = await browser
|
|
249
|
+
const authLocalStorage = await browser
|
|
250
|
+
.getLocalStorage(_client, _sessionId)
|
|
251
|
+
.catch(() => ({}));
|
|
230
252
|
// Close headed session — actually terminate the underlying Chrome process
|
|
231
253
|
// (Browser.close CDP command, PID-kill fallback), not just our CDP client
|
|
232
254
|
// connection, so the visible authenticated window doesn't linger forever.
|
|
@@ -251,13 +273,17 @@ async function switchToHeaded(url, port) {
|
|
|
251
273
|
try {
|
|
252
274
|
await browser.setCookies(_client, _sessionId, authCookies);
|
|
253
275
|
}
|
|
254
|
-
catch {
|
|
276
|
+
catch {
|
|
277
|
+
/* ignore */
|
|
278
|
+
}
|
|
255
279
|
}
|
|
256
280
|
if (authLocalStorage && Object.keys(authLocalStorage).length) {
|
|
257
281
|
try {
|
|
258
282
|
await browser.setLocalStorage(_client, _sessionId, authLocalStorage);
|
|
259
283
|
}
|
|
260
|
-
catch {
|
|
284
|
+
catch {
|
|
285
|
+
/* ignore */
|
|
286
|
+
}
|
|
261
287
|
}
|
|
262
288
|
}
|
|
263
289
|
/**
|
|
@@ -272,7 +298,7 @@ function imageFormat(value, allowed, fallback) {
|
|
|
272
298
|
return allowed.includes(value) ? value : fallback;
|
|
273
299
|
}
|
|
274
300
|
function print(msg) {
|
|
275
|
-
process.stdout.write(msg
|
|
301
|
+
process.stdout.write(`${msg}\n`);
|
|
276
302
|
}
|
|
277
303
|
// ---------------------------------------------------------------------------
|
|
278
304
|
// Sub-commands
|
|
@@ -282,7 +308,12 @@ const openCommand = {
|
|
|
282
308
|
description: 'Open a URL in the browser. Usage: monomind browse open <url>',
|
|
283
309
|
options: [
|
|
284
310
|
{ name: 'port', short: 'p', type: 'number', description: 'CDP port', default: 9222 },
|
|
285
|
-
{
|
|
311
|
+
{
|
|
312
|
+
name: 'headed',
|
|
313
|
+
type: 'boolean',
|
|
314
|
+
description: 'Force visible browser window',
|
|
315
|
+
default: false,
|
|
316
|
+
},
|
|
286
317
|
{ name: 'session', short: 's', type: 'string', description: 'Session name to restore' },
|
|
287
318
|
{ name: 'state', type: 'string', description: 'State file to load' },
|
|
288
319
|
],
|
|
@@ -300,19 +331,25 @@ const openCommand = {
|
|
|
300
331
|
try {
|
|
301
332
|
await browser.stopHarRecording(prevClient, prevSid);
|
|
302
333
|
}
|
|
303
|
-
catch {
|
|
334
|
+
catch {
|
|
335
|
+
/* ignore */
|
|
336
|
+
}
|
|
304
337
|
}
|
|
305
338
|
if (browser.getTraceStatus(prevSid)) {
|
|
306
339
|
try {
|
|
307
340
|
await browser.stopTrace(prevClient, prevSid);
|
|
308
341
|
}
|
|
309
|
-
catch {
|
|
342
|
+
catch {
|
|
343
|
+
/* ignore */
|
|
344
|
+
}
|
|
310
345
|
}
|
|
311
346
|
if (browser.isProfilingActive(prevSid)) {
|
|
312
347
|
try {
|
|
313
348
|
await browser.stopCpuProfile(prevClient, prevSid);
|
|
314
349
|
}
|
|
315
|
-
catch {
|
|
350
|
+
catch {
|
|
351
|
+
/* ignore */
|
|
352
|
+
}
|
|
316
353
|
}
|
|
317
354
|
browser.teardownRouteInterception(prevSid);
|
|
318
355
|
browser.stopRequestCapture(prevSid);
|
|
@@ -352,7 +389,10 @@ const openCommand = {
|
|
|
352
389
|
// launched it) instead of overwriting with undefined.
|
|
353
390
|
const existing = await browser.loadActivePortInfo();
|
|
354
391
|
const preserved = existing && existing.port === _port ? existing : undefined;
|
|
355
|
-
await browser.saveActivePort(_port, {
|
|
392
|
+
await browser.saveActivePort(_port, {
|
|
393
|
+
pid: preserved?.pid,
|
|
394
|
+
userDataDir: preserved?.userDataDir,
|
|
395
|
+
});
|
|
356
396
|
}
|
|
357
397
|
ensureSignalCleanupHandlers();
|
|
358
398
|
const conn = await browser.connectToTarget(_port);
|
|
@@ -395,15 +435,49 @@ const snapshotCommand = {
|
|
|
395
435
|
name: 'snapshot',
|
|
396
436
|
description: 'Capture accessibility snapshot with ref-based element handles (@e1, @e2, ...)',
|
|
397
437
|
options: [
|
|
398
|
-
{
|
|
399
|
-
|
|
438
|
+
{
|
|
439
|
+
name: 'interactive',
|
|
440
|
+
short: 'i',
|
|
441
|
+
type: 'boolean',
|
|
442
|
+
description: 'Interactive elements only (93% token reduction)',
|
|
443
|
+
default: false,
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
name: 'compact',
|
|
447
|
+
short: 'c',
|
|
448
|
+
type: 'boolean',
|
|
449
|
+
description: 'Compact output format',
|
|
450
|
+
default: false,
|
|
451
|
+
},
|
|
400
452
|
{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
|
|
401
453
|
{ name: 'depth', short: 'd', type: 'number', description: 'Max depth of AX tree to show' },
|
|
402
|
-
{
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
454
|
+
{
|
|
455
|
+
name: 'selector',
|
|
456
|
+
short: 's',
|
|
457
|
+
type: 'string',
|
|
458
|
+
description: 'Scope snapshot to a CSS selector',
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
name: 'save',
|
|
462
|
+
type: 'string',
|
|
463
|
+
description: 'Save snapshot text to file (baseline for --diff)',
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
name: 'diff',
|
|
467
|
+
type: 'string',
|
|
468
|
+
description: 'Compare current snapshot against a saved baseline file',
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
name: 'content-boundaries',
|
|
472
|
+
type: 'boolean',
|
|
473
|
+
description: 'Wrap output in sentinel markers to prevent page-content injection attacks',
|
|
474
|
+
default: false,
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
name: 'max-output',
|
|
478
|
+
type: 'number',
|
|
479
|
+
description: 'Truncate output to N characters (prevents context window blowout on large pages)',
|
|
480
|
+
},
|
|
407
481
|
],
|
|
408
482
|
action: async (ctx) => {
|
|
409
483
|
const { client, sessionId } = await ensureConnected(_port);
|
|
@@ -427,8 +501,8 @@ const snapshotCommand = {
|
|
|
427
501
|
};
|
|
428
502
|
// --save: write snapshot text to baseline file
|
|
429
503
|
if (ctx.flags.save) {
|
|
430
|
-
const { writeFile, mkdir } = await import('fs/promises');
|
|
431
|
-
const { dirname } = await import('path');
|
|
504
|
+
const { writeFile, mkdir } = await import('node:fs/promises');
|
|
505
|
+
const { dirname } = await import('node:path');
|
|
432
506
|
const savePath = ctx.flags.save;
|
|
433
507
|
await mkdir(dirname(savePath), { recursive: true }).catch(() => { });
|
|
434
508
|
await writeFile(savePath, result.text, 'utf8');
|
|
@@ -437,7 +511,7 @@ const snapshotCommand = {
|
|
|
437
511
|
}
|
|
438
512
|
// --diff: compare against baseline file
|
|
439
513
|
if (ctx.flags.diff) {
|
|
440
|
-
const { readFile } = await import('fs/promises');
|
|
514
|
+
const { readFile } = await import('node:fs/promises');
|
|
441
515
|
const baselinePath = ctx.flags.diff;
|
|
442
516
|
let baseline;
|
|
443
517
|
try {
|
|
@@ -459,7 +533,13 @@ const snapshotCommand = {
|
|
|
459
533
|
removed.push(l);
|
|
460
534
|
const changed = added.length > 0 || removed.length > 0;
|
|
461
535
|
if (ctx.flags.json) {
|
|
462
|
-
print(JSON.stringify({
|
|
536
|
+
print(JSON.stringify({
|
|
537
|
+
changed,
|
|
538
|
+
additions: added.length,
|
|
539
|
+
removals: removed.length,
|
|
540
|
+
added,
|
|
541
|
+
removed,
|
|
542
|
+
}));
|
|
463
543
|
}
|
|
464
544
|
else {
|
|
465
545
|
if (!changed) {
|
|
@@ -473,11 +553,19 @@ const snapshotCommand = {
|
|
|
473
553
|
print(`\x1b[31m- ${l}\x1b[0m`);
|
|
474
554
|
}
|
|
475
555
|
}
|
|
476
|
-
return {
|
|
556
|
+
return {
|
|
557
|
+
success: true,
|
|
558
|
+
data: { changed, additions: added.length, removals: removed.length },
|
|
559
|
+
};
|
|
477
560
|
}
|
|
478
561
|
if (ctx.flags.json) {
|
|
479
562
|
const refsObj = Object.fromEntries([...result.refs.entries()].map(([k, v]) => [k, v]));
|
|
480
|
-
print(JSON.stringify({
|
|
563
|
+
print(JSON.stringify({
|
|
564
|
+
url: result.url,
|
|
565
|
+
title: result.title,
|
|
566
|
+
refs: refsObj,
|
|
567
|
+
snapshot: result.text,
|
|
568
|
+
}));
|
|
481
569
|
}
|
|
482
570
|
else {
|
|
483
571
|
print(`[${result.title}] ${result.url}\n`);
|
|
@@ -555,11 +643,19 @@ const waitCommand = {
|
|
|
555
643
|
{ name: 'text', type: 'string', description: 'Wait for text to appear in page' },
|
|
556
644
|
{ name: 'not-text', type: 'string', description: 'Wait for text to disappear from page' },
|
|
557
645
|
{ name: 'selector', type: 'string', description: 'Wait for CSS selector to appear' },
|
|
558
|
-
{
|
|
646
|
+
{
|
|
647
|
+
name: 'load',
|
|
648
|
+
type: 'string',
|
|
649
|
+
description: 'Wait for load event: load|networkidle|domcontentloaded',
|
|
650
|
+
},
|
|
559
651
|
{ name: 'fn', type: 'string', description: 'Wait until JS expression returns truthy' },
|
|
560
652
|
{ name: 'ms', type: 'number', description: 'Wait N milliseconds' },
|
|
561
653
|
{ name: 'timeout', short: 't', type: 'number', description: 'Timeout in ms', default: 30000 },
|
|
562
|
-
{
|
|
654
|
+
{
|
|
655
|
+
name: 'download',
|
|
656
|
+
type: 'string',
|
|
657
|
+
description: 'Wait for a file download to complete and save to path (monitors Browser.downloadProgress events)',
|
|
658
|
+
},
|
|
563
659
|
],
|
|
564
660
|
action: async (ctx) => {
|
|
565
661
|
const { client, sessionId } = await ensureConnected(_port);
|
|
@@ -574,7 +670,9 @@ const waitCommand = {
|
|
|
574
670
|
if (ctx.flags.fn) {
|
|
575
671
|
const expr = ctx.flags.fn;
|
|
576
672
|
const rawTimeout = ctx.flags.timeout ?? 30000;
|
|
577
|
-
const timeout = Number.isFinite(rawTimeout)
|
|
673
|
+
const timeout = Number.isFinite(rawTimeout)
|
|
674
|
+
? Math.max(100, Math.min(rawTimeout, 300_000))
|
|
675
|
+
: 30000; // cap at 5min
|
|
578
676
|
const interval = 200;
|
|
579
677
|
const deadline = Date.now() + timeout;
|
|
580
678
|
while (Date.now() < deadline) {
|
|
@@ -593,7 +691,7 @@ const waitCommand = {
|
|
|
593
691
|
const interval = 200;
|
|
594
692
|
const deadline = Date.now() + timeout;
|
|
595
693
|
while (Date.now() < deadline) {
|
|
596
|
-
const text = await browser.evaluateJs(client, sessionId, 'document.body?.innerText ?? ""');
|
|
694
|
+
const text = (await browser.evaluateJs(client, sessionId, 'document.body?.innerText ?? ""'));
|
|
597
695
|
if (!text.includes(target)) {
|
|
598
696
|
output.printSuccess('Text disappeared');
|
|
599
697
|
return { success: true };
|
|
@@ -604,26 +702,39 @@ const waitCommand = {
|
|
|
604
702
|
}
|
|
605
703
|
if (ctx.flags.download) {
|
|
606
704
|
const savePath = ctx.flags.download;
|
|
607
|
-
const { mkdir } = await import('fs/promises');
|
|
608
|
-
const { dirname, join } = await import('path');
|
|
609
|
-
const { tmpdir } = await import('os');
|
|
705
|
+
const { mkdir } = await import('node:fs/promises');
|
|
706
|
+
const { dirname, join } = await import('node:path');
|
|
707
|
+
const { tmpdir } = await import('node:os');
|
|
610
708
|
const downloadDir = join(tmpdir(), `mm-dl-wait-${Date.now()}`);
|
|
611
709
|
await mkdir(downloadDir, { recursive: true });
|
|
612
|
-
await client
|
|
613
|
-
|
|
614
|
-
|
|
710
|
+
await client
|
|
711
|
+
.send('Browser.setDownloadBehavior', {
|
|
712
|
+
behavior: 'allow',
|
|
713
|
+
downloadPath: downloadDir,
|
|
714
|
+
eventsEnabled: true,
|
|
715
|
+
}, undefined)
|
|
716
|
+
.catch(() => client
|
|
717
|
+
.send('Page.setDownloadBehavior', { behavior: 'allow', downloadPath: downloadDir }, sessionId)
|
|
718
|
+
.catch(() => { }));
|
|
615
719
|
const MAX_DOWNLOAD_TIMEOUT = 5 * 60 * 1000; // I6: cap at 5 minutes
|
|
616
720
|
const rawTimeout = Math.min(ctx.flags.timeout ?? 30000, MAX_DOWNLOAD_TIMEOUT);
|
|
617
721
|
const finalPath = await new Promise((resolve, reject) => {
|
|
618
722
|
let guid = '';
|
|
619
723
|
let settled = false;
|
|
620
724
|
// C2: capture off() functions to avoid listener leaks
|
|
621
|
-
const offBegin = client.on('Browser.downloadWillBegin', (params) => {
|
|
725
|
+
const offBegin = client.on('Browser.downloadWillBegin', (params) => {
|
|
726
|
+
guid = params.guid;
|
|
727
|
+
});
|
|
622
728
|
let offProgress;
|
|
623
729
|
// cleanup defined before setTimeout so the timeout callback can call it
|
|
624
730
|
let tid;
|
|
625
731
|
let pollTid;
|
|
626
|
-
const cleanup = () => {
|
|
732
|
+
const cleanup = () => {
|
|
733
|
+
clearTimeout(tid);
|
|
734
|
+
clearInterval(pollTid);
|
|
735
|
+
offBegin?.();
|
|
736
|
+
offProgress?.();
|
|
737
|
+
};
|
|
627
738
|
const finish = (path) => {
|
|
628
739
|
if (settled)
|
|
629
740
|
return;
|
|
@@ -641,7 +752,7 @@ const waitCommand = {
|
|
|
641
752
|
tid = setTimeout(() => fail(new Error('Download timed out')), rawTimeout);
|
|
642
753
|
offProgress = client.on('Browser.downloadProgress', async (params) => {
|
|
643
754
|
if (params.guid === guid && params.state === 'completed') {
|
|
644
|
-
const { readdir, rename, rmdir } = await import('fs/promises');
|
|
755
|
+
const { readdir, rename, rmdir } = await import('node:fs/promises');
|
|
645
756
|
const files = await readdir(downloadDir);
|
|
646
757
|
if (files.length > 0) {
|
|
647
758
|
const src = join(downloadDir, files[0]);
|
|
@@ -671,7 +782,7 @@ const waitCommand = {
|
|
|
671
782
|
if (settled || guid)
|
|
672
783
|
return; // a real CDP event has taken over
|
|
673
784
|
try {
|
|
674
|
-
const { stat } = await import('fs/promises');
|
|
785
|
+
const { stat } = await import('node:fs/promises');
|
|
675
786
|
const st = await stat(savePath);
|
|
676
787
|
if (st.isFile() && st.size > 0 && st.size === lastSize) {
|
|
677
788
|
stableReads++;
|
|
@@ -709,8 +820,18 @@ const screenshotCommand = {
|
|
|
709
820
|
{ name: 'full', type: 'boolean', description: 'Full page screenshot', default: false },
|
|
710
821
|
{ name: 'format', type: 'string', description: 'Format: png|jpeg|webp', default: 'png' },
|
|
711
822
|
{ name: 'quality', type: 'number', description: 'Quality 0-100 for jpeg/webp', default: 80 },
|
|
712
|
-
{
|
|
713
|
-
|
|
823
|
+
{
|
|
824
|
+
name: 'annotate',
|
|
825
|
+
type: 'boolean',
|
|
826
|
+
description: 'Overlay numbered labels keyed to @eN refs from last snapshot (viewport-only; do not combine with --full)',
|
|
827
|
+
default: false,
|
|
828
|
+
},
|
|
829
|
+
{
|
|
830
|
+
name: 'hide-scrollbars',
|
|
831
|
+
type: 'boolean',
|
|
832
|
+
description: 'Hide native scrollbars via CSS injection before capture',
|
|
833
|
+
default: false,
|
|
834
|
+
},
|
|
714
835
|
{ name: 'json', type: 'boolean', description: 'Output JSON with path', default: false },
|
|
715
836
|
],
|
|
716
837
|
action: async (ctx) => {
|
|
@@ -718,10 +839,12 @@ const screenshotCommand = {
|
|
|
718
839
|
const browser = await getBrowser();
|
|
719
840
|
const hideScrollbars = ctx.flags['hide-scrollbars'];
|
|
720
841
|
if (hideScrollbars) {
|
|
721
|
-
await client
|
|
842
|
+
await client
|
|
843
|
+
.send('Runtime.evaluate', {
|
|
722
844
|
expression: `(function(){var s=document.getElementById('__mm_noscroll__');if(s)return;var el=document.createElement('style');el.id='__mm_noscroll__';el.textContent='*::-webkit-scrollbar{display:none!important}*{scrollbar-width:none!important;-ms-overflow-style:none!important}';document.head.appendChild(el);})()`,
|
|
723
845
|
returnByValue: false,
|
|
724
|
-
}, sessionId)
|
|
846
|
+
}, sessionId)
|
|
847
|
+
.catch(() => { });
|
|
725
848
|
}
|
|
726
849
|
const annotate = ctx.flags.annotate;
|
|
727
850
|
// eslint-disable-next-line prefer-const
|
|
@@ -738,10 +861,12 @@ const screenshotCommand = {
|
|
|
738
861
|
}
|
|
739
862
|
finally {
|
|
740
863
|
if (hideScrollbars) {
|
|
741
|
-
await client
|
|
864
|
+
await client
|
|
865
|
+
.send('Runtime.evaluate', {
|
|
742
866
|
expression: `(function(){var s=document.getElementById('__mm_noscroll__');if(s)s.remove();})()`,
|
|
743
867
|
returnByValue: false,
|
|
744
|
-
}, sessionId)
|
|
868
|
+
}, sessionId)
|
|
869
|
+
.catch(() => { });
|
|
745
870
|
}
|
|
746
871
|
}
|
|
747
872
|
if (ctx.flags.json) {
|
|
@@ -756,9 +881,7 @@ const screenshotCommand = {
|
|
|
756
881
|
const getCommand = {
|
|
757
882
|
name: 'get',
|
|
758
883
|
description: 'Get page info. Usage: monomind browse get url|title|text|html|value|attr|count|box|styles [@ref] [attrName]',
|
|
759
|
-
options: [
|
|
760
|
-
{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
|
|
761
|
-
],
|
|
884
|
+
options: [{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false }],
|
|
762
885
|
action: async (ctx) => {
|
|
763
886
|
const { client, sessionId } = await ensureConnected(_port);
|
|
764
887
|
const browser = await getBrowser();
|
|
@@ -811,7 +934,8 @@ const getCommand = {
|
|
|
811
934
|
throw new Error('Element not in DOM');
|
|
812
935
|
const r = await client.send('Runtime.callFunctionOn', {
|
|
813
936
|
functionDeclaration: 'function() { return this.value ?? null; }',
|
|
814
|
-
objectId,
|
|
937
|
+
objectId,
|
|
938
|
+
returnByValue: true,
|
|
815
939
|
}, sessionId);
|
|
816
940
|
value = r.result?.value ?? null;
|
|
817
941
|
break;
|
|
@@ -830,7 +954,8 @@ const getCommand = {
|
|
|
830
954
|
throw new Error('Element not in DOM');
|
|
831
955
|
const r = await client.send('Runtime.callFunctionOn', {
|
|
832
956
|
functionDeclaration: `function() { return this.getAttribute(${JSON.stringify(attrName)}); }`,
|
|
833
|
-
objectId,
|
|
957
|
+
objectId,
|
|
958
|
+
returnByValue: true,
|
|
834
959
|
}, sessionId);
|
|
835
960
|
value = r.result?.value ?? null;
|
|
836
961
|
break;
|
|
@@ -867,7 +992,8 @@ const getCommand = {
|
|
|
867
992
|
throw new Error('Element not in DOM');
|
|
868
993
|
const r = await client.send('Runtime.callFunctionOn', {
|
|
869
994
|
functionDeclaration: 'function() { const s = window.getComputedStyle(this); return JSON.stringify(Object.fromEntries([...s].map(k => [k, s.getPropertyValue(k)]))); }',
|
|
870
|
-
objectId,
|
|
995
|
+
objectId,
|
|
996
|
+
returnByValue: true,
|
|
871
997
|
}, sessionId);
|
|
872
998
|
try {
|
|
873
999
|
value = JSON.parse(r.result?.value ?? '{}');
|
|
@@ -895,7 +1021,12 @@ const scrollCommand = {
|
|
|
895
1021
|
options: [
|
|
896
1022
|
{ name: 'amount', short: 'a', type: 'number', description: 'Pixels to scroll', default: 300 },
|
|
897
1023
|
{ name: 'ref', type: 'string', description: 'Element ref to scroll within' },
|
|
898
|
-
{
|
|
1024
|
+
{
|
|
1025
|
+
name: 'selector',
|
|
1026
|
+
short: 's',
|
|
1027
|
+
type: 'string',
|
|
1028
|
+
description: 'CSS selector of element to scroll within',
|
|
1029
|
+
},
|
|
899
1030
|
],
|
|
900
1031
|
action: async (ctx) => {
|
|
901
1032
|
const { client, sessionId } = await ensureConnected(_port);
|
|
@@ -905,12 +1036,14 @@ const scrollCommand = {
|
|
|
905
1036
|
throw new Error('Usage: monomind browse scroll up|down|left|right [amount]');
|
|
906
1037
|
// Support positional amount: scroll down 300
|
|
907
1038
|
const positionalAmount = ctx.args[1] !== undefined ? parseInt(ctx.args[1], 10) : undefined;
|
|
908
|
-
const amount =
|
|
1039
|
+
const amount = positionalAmount && Number.isFinite(positionalAmount)
|
|
1040
|
+
? positionalAmount
|
|
1041
|
+
: (ctx.flags.amount ?? 300);
|
|
909
1042
|
if (ctx.flags.selector) {
|
|
910
1043
|
const sel = ctx.flags.selector;
|
|
911
1044
|
const dx = direction === 'right' ? amount : direction === 'left' ? -amount : 0;
|
|
912
1045
|
const dy = direction === 'down' ? amount : direction === 'up' ? -amount : 0;
|
|
913
|
-
const posJson = await browser.evaluateJs(client, sessionId, `(function(){var el=document.querySelector(${JSON.stringify(sel)});if(!el)return null;var r=el.getBoundingClientRect();return JSON.stringify({x:r.left+r.width/2,y:r.top+r.height/2});})()`);
|
|
1046
|
+
const posJson = (await browser.evaluateJs(client, sessionId, `(function(){var el=document.querySelector(${JSON.stringify(sel)});if(!el)return null;var r=el.getBoundingClientRect();return JSON.stringify({x:r.left+r.width/2,y:r.top+r.height/2});})()`));
|
|
914
1047
|
if (!posJson)
|
|
915
1048
|
throw new Error(`Selector not found: ${sel}`);
|
|
916
1049
|
const pos = JSON.parse(posJson);
|
|
@@ -958,7 +1091,9 @@ const navigateCommand = {
|
|
|
958
1091
|
expression: direction === 'back' ? 'history.back()' : 'history.forward()',
|
|
959
1092
|
}, sessionId);
|
|
960
1093
|
let fallbackHandle;
|
|
961
|
-
const fallbackPromise = new Promise((r) => {
|
|
1094
|
+
const fallbackPromise = new Promise((r) => {
|
|
1095
|
+
fallbackHandle = setTimeout(r, 2000);
|
|
1096
|
+
});
|
|
962
1097
|
await Promise.race([frameStartedPromise, fallbackPromise]);
|
|
963
1098
|
if (fallbackHandle !== undefined)
|
|
964
1099
|
clearTimeout(fallbackHandle);
|
|
@@ -996,10 +1131,13 @@ const setCommand = {
|
|
|
996
1131
|
const width = parseInt(ctx.args[1], 10);
|
|
997
1132
|
const height = parseInt(ctx.args[2], 10);
|
|
998
1133
|
const dpr = parseFloat(ctx.args[3]) || undefined;
|
|
999
|
-
if (isNaN(width) || isNaN(height))
|
|
1134
|
+
if (Number.isNaN(width) || Number.isNaN(height))
|
|
1000
1135
|
throw new Error('Usage: set viewport <width> <height> [dpr]');
|
|
1001
1136
|
await client.send('Emulation.setDeviceMetricsOverride', {
|
|
1002
|
-
width,
|
|
1137
|
+
width,
|
|
1138
|
+
height,
|
|
1139
|
+
deviceScaleFactor: dpr ?? 1,
|
|
1140
|
+
mobile: false,
|
|
1003
1141
|
}, sessionId);
|
|
1004
1142
|
output.printSuccess(`Viewport set to ${width}x${height}${dpr ? ` @${dpr}x` : ''}`);
|
|
1005
1143
|
break;
|
|
@@ -1016,7 +1154,7 @@ const setCommand = {
|
|
|
1016
1154
|
const lat = parseFloat(ctx.args[1]);
|
|
1017
1155
|
const lon = parseFloat(ctx.args[2]);
|
|
1018
1156
|
const acc = parseFloat(ctx.args[3]) || 100;
|
|
1019
|
-
if (isNaN(lat) || isNaN(lon))
|
|
1157
|
+
if (Number.isNaN(lat) || Number.isNaN(lon))
|
|
1020
1158
|
throw new Error('Usage: set geo <latitude> <longitude> [accuracy]');
|
|
1021
1159
|
await browser.setGeolocation(client, sessionId, lat, lon, acc);
|
|
1022
1160
|
output.printSuccess(`Geolocation set: ${lat}, ${lon}`);
|
|
@@ -1066,7 +1204,11 @@ const stateCommand = {
|
|
|
1066
1204
|
name: 'state',
|
|
1067
1205
|
description: 'Manage browser session state. Usage: monomind browse state save|load|list|rename|clean [name]',
|
|
1068
1206
|
options: [
|
|
1069
|
-
{
|
|
1207
|
+
{
|
|
1208
|
+
name: 'older-than',
|
|
1209
|
+
type: 'number',
|
|
1210
|
+
description: 'For state clean: remove sessions older than N days',
|
|
1211
|
+
},
|
|
1070
1212
|
],
|
|
1071
1213
|
action: async (ctx) => {
|
|
1072
1214
|
const browser = await getBrowser();
|
|
@@ -1123,7 +1265,13 @@ const stateCommand = {
|
|
|
1123
1265
|
const title = await browser.getCurrentTitle(c, sid);
|
|
1124
1266
|
const cookies = await browser.getCookies(c, sid);
|
|
1125
1267
|
const ls = await browser.getAllLocalStorage(c, sid);
|
|
1126
|
-
const info = {
|
|
1268
|
+
const info = {
|
|
1269
|
+
url,
|
|
1270
|
+
title,
|
|
1271
|
+
cookies: cookies.length,
|
|
1272
|
+
localStorage: Object.keys(ls).length,
|
|
1273
|
+
refs: _refs.size,
|
|
1274
|
+
};
|
|
1127
1275
|
print(JSON.stringify(info, null, 2));
|
|
1128
1276
|
return { success: true, data: info };
|
|
1129
1277
|
}
|
|
@@ -1146,14 +1294,14 @@ const stateCommand = {
|
|
|
1146
1294
|
if (!sessions.includes(oldName))
|
|
1147
1295
|
throw new Error(`Session not found: ${oldName}`);
|
|
1148
1296
|
// W1: validate newName to prevent path traversal
|
|
1149
|
-
const { basename: basenameFn } = await import('path');
|
|
1297
|
+
const { basename: basenameFn } = await import('node:path');
|
|
1150
1298
|
const safeName = basenameFn(newName);
|
|
1151
1299
|
if (safeName !== newName || safeName.startsWith('.') || safeName.includes('/')) {
|
|
1152
1300
|
throw new Error('Invalid session name — must not contain path separators or start with "."');
|
|
1153
1301
|
}
|
|
1154
|
-
const { unlink: unlinkRename, readFile, writeFile, mkdir: mkdirRename } = await import('fs/promises');
|
|
1155
|
-
const { join: joinR } = await import('path');
|
|
1156
|
-
const { homedir } = await import('os');
|
|
1302
|
+
const { unlink: unlinkRename, readFile, writeFile, mkdir: mkdirRename, } = await import('node:fs/promises');
|
|
1303
|
+
const { join: joinR } = await import('node:path');
|
|
1304
|
+
const { homedir } = await import('node:os');
|
|
1157
1305
|
const sessionDir = joinR(homedir(), '.monomind', 'browser-sessions');
|
|
1158
1306
|
const oldPath = joinR(sessionDir, `${oldName}.json`);
|
|
1159
1307
|
const newPath = joinR(sessionDir, `${newName}.json`);
|
|
@@ -1167,9 +1315,9 @@ const stateCommand = {
|
|
|
1167
1315
|
}
|
|
1168
1316
|
case 'clean': {
|
|
1169
1317
|
const days = ctx.flags['older-than'] ?? 7;
|
|
1170
|
-
const { unlink, stat } = await import('fs/promises');
|
|
1171
|
-
const { join: joinC } = await import('path');
|
|
1172
|
-
const { homedir: homedirC } = await import('os');
|
|
1318
|
+
const { unlink, stat } = await import('node:fs/promises');
|
|
1319
|
+
const { join: joinC } = await import('node:path');
|
|
1320
|
+
const { homedir: homedirC } = await import('node:os');
|
|
1173
1321
|
const sessionDir = joinC(homedirC(), '.monomind', 'browser-sessions');
|
|
1174
1322
|
const sessions = await browser.listSessions();
|
|
1175
1323
|
const cutoff = Date.now() - days * 86400 * 1000;
|
|
@@ -1200,10 +1348,26 @@ const networkCommand = {
|
|
|
1200
1348
|
{ name: 'status', type: 'number', description: 'HTTP status for fulfill', default: 200 },
|
|
1201
1349
|
{ name: 'headers', type: 'string', description: 'JSON headers object' },
|
|
1202
1350
|
{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
|
|
1203
|
-
{
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1351
|
+
{
|
|
1352
|
+
name: 'filter',
|
|
1353
|
+
type: 'string',
|
|
1354
|
+
description: 'Filter requests by URL substring (for network requests)',
|
|
1355
|
+
},
|
|
1356
|
+
{
|
|
1357
|
+
name: 'method',
|
|
1358
|
+
type: 'string',
|
|
1359
|
+
description: 'Filter by HTTP method, e.g. GET, POST (for network requests)',
|
|
1360
|
+
},
|
|
1361
|
+
{
|
|
1362
|
+
name: 'status-code',
|
|
1363
|
+
type: 'number',
|
|
1364
|
+
description: 'Filter by HTTP status code (for network requests)',
|
|
1365
|
+
},
|
|
1366
|
+
{
|
|
1367
|
+
name: 'type',
|
|
1368
|
+
type: 'string',
|
|
1369
|
+
description: 'Filter by resource type: xhr|fetch|document|script|stylesheet|image (for network requests)',
|
|
1370
|
+
},
|
|
1207
1371
|
],
|
|
1208
1372
|
action: async (ctx) => {
|
|
1209
1373
|
const { client, sessionId } = await ensureConnected(_port);
|
|
@@ -1216,15 +1380,19 @@ const networkCommand = {
|
|
|
1216
1380
|
const pattern = ctx.flags.pattern;
|
|
1217
1381
|
if (!pattern)
|
|
1218
1382
|
throw new Error('--pattern required for network route');
|
|
1219
|
-
const routes = [
|
|
1383
|
+
const routes = [
|
|
1384
|
+
{
|
|
1220
1385
|
pattern,
|
|
1221
1386
|
action: ctx.flags.abort ? 'abort' : ctx.flags.fulfill ? 'fulfill' : 'continue',
|
|
1222
|
-
response: ctx.flags.fulfill
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1387
|
+
response: ctx.flags.fulfill
|
|
1388
|
+
? {
|
|
1389
|
+
status: ctx.flags.status,
|
|
1390
|
+
body: ctx.flags.fulfill,
|
|
1391
|
+
headers: ctx.flags.headers ? JSON.parse(ctx.flags.headers) : {},
|
|
1392
|
+
}
|
|
1393
|
+
: undefined,
|
|
1394
|
+
},
|
|
1395
|
+
];
|
|
1228
1396
|
await browser.setupRoutes(client, sessionId, routes);
|
|
1229
1397
|
output.printSuccess(`Network route set: ${pattern}`);
|
|
1230
1398
|
break;
|
|
@@ -1275,7 +1443,8 @@ const networkCommand = {
|
|
|
1275
1443
|
if (filterStatus)
|
|
1276
1444
|
reqs = reqs.filter((r) => r.status === filterStatus);
|
|
1277
1445
|
if (filterType)
|
|
1278
|
-
reqs = reqs.filter((r) => r.resourceType === filterType ||
|
|
1446
|
+
reqs = reqs.filter((r) => r.resourceType === filterType ||
|
|
1447
|
+
r.type === filterType);
|
|
1279
1448
|
if (ctx.flags.json)
|
|
1280
1449
|
print(JSON.stringify({ data: reqs }));
|
|
1281
1450
|
else {
|
|
@@ -1293,7 +1462,8 @@ const networkCommand = {
|
|
|
1293
1462
|
if (!reqId)
|
|
1294
1463
|
throw new Error('Usage: monomind browse network request <requestId>');
|
|
1295
1464
|
const reqs = browser.getCapturedRequests(sessionId);
|
|
1296
|
-
const req = reqs.find((r) => r.requestId === reqId ||
|
|
1465
|
+
const req = reqs.find((r) => r.requestId === reqId ||
|
|
1466
|
+
r.id === reqId);
|
|
1297
1467
|
if (!req) {
|
|
1298
1468
|
output.printWarning(`Request not found: ${reqId}`);
|
|
1299
1469
|
return { success: false };
|
|
@@ -1319,16 +1489,29 @@ const DEFAULT_EVAL_MAX_OUTPUT = 50_000;
|
|
|
1319
1489
|
function truncateForOutput(text, maxOutput) {
|
|
1320
1490
|
if (!(maxOutput > 0) || text.length <= maxOutput)
|
|
1321
1491
|
return text;
|
|
1322
|
-
return text.slice(0, maxOutput)
|
|
1492
|
+
return `${text.slice(0, maxOutput)}\n[... truncated at ${maxOutput} chars]`;
|
|
1323
1493
|
}
|
|
1324
1494
|
const evalCommand = {
|
|
1325
1495
|
name: 'eval',
|
|
1326
1496
|
description: 'Evaluate JavaScript in page context. Usage: monomind browse eval "document.title"',
|
|
1327
1497
|
options: [
|
|
1328
1498
|
{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
|
|
1329
|
-
{
|
|
1330
|
-
|
|
1331
|
-
|
|
1499
|
+
{
|
|
1500
|
+
name: 'stdin',
|
|
1501
|
+
type: 'boolean',
|
|
1502
|
+
description: 'Read JS expression from stdin (heredoc-friendly for multiline scripts)',
|
|
1503
|
+
default: false,
|
|
1504
|
+
},
|
|
1505
|
+
{
|
|
1506
|
+
name: 'max-output',
|
|
1507
|
+
type: 'number',
|
|
1508
|
+
description: `Truncate printed output to N characters (default ${DEFAULT_EVAL_MAX_OUTPUT}; 0 disables truncation)`,
|
|
1509
|
+
},
|
|
1510
|
+
{
|
|
1511
|
+
name: 'timeout',
|
|
1512
|
+
type: 'number',
|
|
1513
|
+
description: 'Max ms to wait for evaluation to settle (default 30000)',
|
|
1514
|
+
},
|
|
1332
1515
|
],
|
|
1333
1516
|
action: async (ctx) => {
|
|
1334
1517
|
const { client, sessionId } = await ensureConnected(_port);
|
|
@@ -1367,19 +1550,25 @@ const closeCommand = {
|
|
|
1367
1550
|
try {
|
|
1368
1551
|
await browser.stopHarRecording(client, sid);
|
|
1369
1552
|
}
|
|
1370
|
-
catch {
|
|
1553
|
+
catch {
|
|
1554
|
+
/* ignore */
|
|
1555
|
+
}
|
|
1371
1556
|
}
|
|
1372
1557
|
if (browser.getTraceStatus(sid)) {
|
|
1373
1558
|
try {
|
|
1374
1559
|
await browser.stopTrace(client, sid);
|
|
1375
1560
|
}
|
|
1376
|
-
catch {
|
|
1561
|
+
catch {
|
|
1562
|
+
/* ignore */
|
|
1563
|
+
}
|
|
1377
1564
|
}
|
|
1378
1565
|
if (browser.isProfilingActive(sid)) {
|
|
1379
1566
|
try {
|
|
1380
1567
|
await browser.stopCpuProfile(client, sid);
|
|
1381
1568
|
}
|
|
1382
|
-
catch {
|
|
1569
|
+
catch {
|
|
1570
|
+
/* ignore */
|
|
1571
|
+
}
|
|
1383
1572
|
}
|
|
1384
1573
|
browser.teardownRouteInterception(sid);
|
|
1385
1574
|
browser.stopRequestCapture(sid);
|
|
@@ -1418,7 +1607,9 @@ const closeCommand = {
|
|
|
1418
1607
|
try {
|
|
1419
1608
|
conn.client.close();
|
|
1420
1609
|
}
|
|
1421
|
-
catch {
|
|
1610
|
+
catch {
|
|
1611
|
+
/* already gone */
|
|
1612
|
+
}
|
|
1422
1613
|
}
|
|
1423
1614
|
// closeBrowser has no PID fallback in a fresh process (launchedPids
|
|
1424
1615
|
// is per-process) — re-probe so we report what actually happened.
|
|
@@ -1428,8 +1619,10 @@ const closeCommand = {
|
|
|
1428
1619
|
const probeDeadline = Date.now() + 3000;
|
|
1429
1620
|
while (stillUp && Date.now() < probeDeadline) {
|
|
1430
1621
|
try {
|
|
1431
|
-
await fetch(`http://127.0.0.1:${persisted.port}/json/version`, {
|
|
1432
|
-
|
|
1622
|
+
await fetch(`http://127.0.0.1:${persisted.port}/json/version`, {
|
|
1623
|
+
signal: AbortSignal.timeout(800),
|
|
1624
|
+
});
|
|
1625
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
1433
1626
|
}
|
|
1434
1627
|
catch {
|
|
1435
1628
|
stillUp = false;
|
|
@@ -1733,7 +1926,7 @@ const tapCommand = {
|
|
|
1733
1926
|
y = Math.round(box.y);
|
|
1734
1927
|
}
|
|
1735
1928
|
else {
|
|
1736
|
-
const posJson = await browser.evaluateJs(client, sessionId, `(function(){var el=document.querySelector(${JSON.stringify(arg)});if(!el)return null;var r=el.getBoundingClientRect();return JSON.stringify({x:r.left+r.width/2,y:r.top+r.height/2});})()`);
|
|
1929
|
+
const posJson = (await browser.evaluateJs(client, sessionId, `(function(){var el=document.querySelector(${JSON.stringify(arg)});if(!el)return null;var r=el.getBoundingClientRect();return JSON.stringify({x:r.left+r.width/2,y:r.top+r.height/2});})()`));
|
|
1737
1930
|
if (!posJson)
|
|
1738
1931
|
throw new Error(`Selector not found: ${arg}`);
|
|
1739
1932
|
const pos = JSON.parse(posJson);
|
|
@@ -1750,9 +1943,25 @@ const swipeCommand = {
|
|
|
1750
1943
|
name: 'swipe',
|
|
1751
1944
|
description: 'Swipe gesture (mobile). Usage: monomind browse swipe up|down|left|right [distance] [--x N] [--y N]',
|
|
1752
1945
|
options: [
|
|
1753
|
-
{
|
|
1754
|
-
|
|
1755
|
-
|
|
1946
|
+
{
|
|
1947
|
+
name: 'x',
|
|
1948
|
+
type: 'number',
|
|
1949
|
+
description: 'Start X coordinate (default: center)',
|
|
1950
|
+
default: 200,
|
|
1951
|
+
},
|
|
1952
|
+
{
|
|
1953
|
+
name: 'y',
|
|
1954
|
+
type: 'number',
|
|
1955
|
+
description: 'Start Y coordinate (default: center)',
|
|
1956
|
+
default: 400,
|
|
1957
|
+
},
|
|
1958
|
+
{
|
|
1959
|
+
name: 'distance',
|
|
1960
|
+
short: 'd',
|
|
1961
|
+
type: 'number',
|
|
1962
|
+
description: 'Swipe distance in pixels',
|
|
1963
|
+
default: 300,
|
|
1964
|
+
},
|
|
1756
1965
|
],
|
|
1757
1966
|
action: async (ctx) => {
|
|
1758
1967
|
const { client, sessionId } = await ensureConnected(_port);
|
|
@@ -1763,14 +1972,16 @@ const swipeCommand = {
|
|
|
1763
1972
|
const startX = ctx.flags.x ?? 200;
|
|
1764
1973
|
const startY = ctx.flags.y ?? 400;
|
|
1765
1974
|
const positionalDistance = ctx.args[1] !== undefined ? parseInt(ctx.args[1], 10) : undefined;
|
|
1766
|
-
const distance =
|
|
1975
|
+
const distance = positionalDistance && Number.isFinite(positionalDistance)
|
|
1976
|
+
? positionalDistance
|
|
1977
|
+
: (ctx.flags.distance ?? 300);
|
|
1767
1978
|
const dx = direction === 'right' ? distance : direction === 'left' ? -distance : 0;
|
|
1768
1979
|
const dy = direction === 'down' ? distance : direction === 'up' ? -distance : 0;
|
|
1769
1980
|
await client.send('Input.dispatchTouchEvent', { type: 'touchStart', touchPoints: [{ x: startX, y: startY }] }, sessionId);
|
|
1770
1981
|
const steps = 10;
|
|
1771
1982
|
for (let i = 1; i <= steps; i++) {
|
|
1772
|
-
const x = Math.round(startX + dx * i / steps);
|
|
1773
|
-
const y = Math.round(startY + dy * i / steps);
|
|
1983
|
+
const x = Math.round(startX + (dx * i) / steps);
|
|
1984
|
+
const y = Math.round(startY + (dy * i) / steps);
|
|
1774
1985
|
await client.send('Input.dispatchTouchEvent', { type: 'touchMove', touchPoints: [{ x, y }] }, sessionId);
|
|
1775
1986
|
await new Promise((r) => setTimeout(r, 16));
|
|
1776
1987
|
}
|
|
@@ -1835,33 +2046,43 @@ const downloadCommand = {
|
|
|
1835
2046
|
name: 'download',
|
|
1836
2047
|
description: 'Click an element and capture the triggered file download. Usage: monomind browse download @e1 ./output.pdf',
|
|
1837
2048
|
options: [
|
|
1838
|
-
{
|
|
2049
|
+
{
|
|
2050
|
+
name: 'timeout',
|
|
2051
|
+
short: 't',
|
|
2052
|
+
type: 'number',
|
|
2053
|
+
description: 'Max wait for download in ms',
|
|
2054
|
+
default: 30000,
|
|
2055
|
+
},
|
|
1839
2056
|
{ name: 'json', type: 'boolean', description: 'Output result as JSON', default: false },
|
|
1840
2057
|
],
|
|
1841
2058
|
action: async (ctx) => {
|
|
1842
2059
|
const { client, sessionId } = await ensureConnected(_port);
|
|
1843
|
-
const
|
|
2060
|
+
const _browser = await getBrowser();
|
|
1844
2061
|
const refOrSel = ctx.args[0];
|
|
1845
2062
|
const savePath = ctx.args[1];
|
|
1846
2063
|
if (!refOrSel || !savePath)
|
|
1847
2064
|
throw new Error('Usage: monomind browse download @e1|selector <save-path>');
|
|
1848
|
-
const { mkdir } = await import('fs/promises');
|
|
1849
|
-
const { dirname } = await import('path');
|
|
1850
|
-
const { tmpdir } = await import('os');
|
|
1851
|
-
const { join } = await import('path');
|
|
2065
|
+
const { mkdir } = await import('node:fs/promises');
|
|
2066
|
+
const { dirname } = await import('node:path');
|
|
2067
|
+
const { tmpdir } = await import('node:os');
|
|
2068
|
+
const { join } = await import('node:path');
|
|
1852
2069
|
const downloadDir = join(tmpdir(), `mm-download-${Date.now()}`);
|
|
1853
2070
|
await mkdir(downloadDir, { recursive: true });
|
|
1854
2071
|
// Enable Page.downloadWillBegin / Page.downloadProgress events
|
|
1855
|
-
await client
|
|
2072
|
+
await client
|
|
2073
|
+
.send('Browser.setDownloadBehavior', {
|
|
1856
2074
|
behavior: 'allow',
|
|
1857
2075
|
downloadPath: downloadDir,
|
|
1858
2076
|
eventsEnabled: true,
|
|
1859
|
-
}, undefined)
|
|
2077
|
+
}, undefined)
|
|
2078
|
+
.catch(() => {
|
|
1860
2079
|
// Fallback: older Chrome API (session-scoped)
|
|
1861
|
-
return client
|
|
2080
|
+
return client
|
|
2081
|
+
.send('Page.setDownloadBehavior', {
|
|
1862
2082
|
behavior: 'allow',
|
|
1863
2083
|
downloadPath: downloadDir,
|
|
1864
|
-
}, sessionId)
|
|
2084
|
+
}, sessionId)
|
|
2085
|
+
.catch(() => { });
|
|
1865
2086
|
});
|
|
1866
2087
|
// Track when download completes
|
|
1867
2088
|
const downloadPromise = new Promise((resolve, reject) => {
|
|
@@ -1873,13 +2094,20 @@ const downloadCommand = {
|
|
|
1873
2094
|
let offProgress;
|
|
1874
2095
|
// cleanup defined before setTimeout so the timeout callback can call it
|
|
1875
2096
|
let timeout;
|
|
1876
|
-
const cleanup = () => {
|
|
1877
|
-
|
|
2097
|
+
const cleanup = () => {
|
|
2098
|
+
clearTimeout(timeout);
|
|
2099
|
+
offBegin?.();
|
|
2100
|
+
offProgress?.();
|
|
2101
|
+
};
|
|
2102
|
+
timeout = setTimeout(() => {
|
|
2103
|
+
cleanup();
|
|
2104
|
+
reject(new Error('Download timed out'));
|
|
2105
|
+
}, ctx.flags.timeout);
|
|
1878
2106
|
offProgress = client.on('Browser.downloadProgress', async (params) => {
|
|
1879
2107
|
if (params.guid === guid && params.state === 'completed') {
|
|
1880
2108
|
cleanup();
|
|
1881
2109
|
// Find the downloaded file in downloadDir
|
|
1882
|
-
const { readdir, rename, rmdir } = await import('fs/promises');
|
|
2110
|
+
const { readdir, rename, rmdir } = await import('node:fs/promises');
|
|
1883
2111
|
const files = await readdir(downloadDir);
|
|
1884
2112
|
if (files.length > 0) {
|
|
1885
2113
|
const src = join(downloadDir, files[0]);
|
|
@@ -2071,9 +2299,7 @@ const frameCommand = {
|
|
|
2071
2299
|
const tabCommand = {
|
|
2072
2300
|
name: 'tab',
|
|
2073
2301
|
description: 'Tab management. Usage: monomind browse tab list|new|close [url]',
|
|
2074
|
-
options: [
|
|
2075
|
-
{ name: 'label', type: 'string', description: 'Label for new tab' },
|
|
2076
|
-
],
|
|
2302
|
+
options: [{ name: 'label', type: 'string', description: 'Label for new tab' }],
|
|
2077
2303
|
action: async (ctx) => {
|
|
2078
2304
|
const { client, sessionId } = await ensureConnected(_port);
|
|
2079
2305
|
const browser = await getBrowser();
|
|
@@ -2102,19 +2328,25 @@ const tabCommand = {
|
|
|
2102
2328
|
try {
|
|
2103
2329
|
await browser.stopHarRecording(client, sid);
|
|
2104
2330
|
}
|
|
2105
|
-
catch {
|
|
2331
|
+
catch {
|
|
2332
|
+
/* ignore */
|
|
2333
|
+
}
|
|
2106
2334
|
}
|
|
2107
2335
|
if (browser.getTraceStatus(sid)) {
|
|
2108
2336
|
try {
|
|
2109
2337
|
await browser.stopTrace(client, sid);
|
|
2110
2338
|
}
|
|
2111
|
-
catch {
|
|
2339
|
+
catch {
|
|
2340
|
+
/* ignore */
|
|
2341
|
+
}
|
|
2112
2342
|
}
|
|
2113
2343
|
if (browser.isProfilingActive(sid)) {
|
|
2114
2344
|
try {
|
|
2115
2345
|
await browser.stopCpuProfile(client, sid);
|
|
2116
2346
|
}
|
|
2117
|
-
catch {
|
|
2347
|
+
catch {
|
|
2348
|
+
/* ignore */
|
|
2349
|
+
}
|
|
2118
2350
|
}
|
|
2119
2351
|
browser.teardownRouteInterception(sid);
|
|
2120
2352
|
browser.stopRequestCapture(sid);
|
|
@@ -2142,19 +2374,25 @@ const tabCommand = {
|
|
|
2142
2374
|
try {
|
|
2143
2375
|
await browser.stopHarRecording(client, oldSid);
|
|
2144
2376
|
}
|
|
2145
|
-
catch {
|
|
2377
|
+
catch {
|
|
2378
|
+
/* ignore */
|
|
2379
|
+
}
|
|
2146
2380
|
}
|
|
2147
2381
|
if (browser.getTraceStatus(oldSid)) {
|
|
2148
2382
|
try {
|
|
2149
2383
|
await browser.stopTrace(client, oldSid);
|
|
2150
2384
|
}
|
|
2151
|
-
catch {
|
|
2385
|
+
catch {
|
|
2386
|
+
/* ignore */
|
|
2387
|
+
}
|
|
2152
2388
|
}
|
|
2153
2389
|
if (browser.isProfilingActive(oldSid)) {
|
|
2154
2390
|
try {
|
|
2155
2391
|
await browser.stopCpuProfile(client, oldSid);
|
|
2156
2392
|
}
|
|
2157
|
-
catch {
|
|
2393
|
+
catch {
|
|
2394
|
+
/* ignore */
|
|
2395
|
+
}
|
|
2158
2396
|
}
|
|
2159
2397
|
await browser.disableInterception(client, oldSid).catch(() => { });
|
|
2160
2398
|
browser.stopRequestCapture(oldSid);
|
|
@@ -2192,19 +2430,25 @@ const windowCommand = {
|
|
|
2192
2430
|
try {
|
|
2193
2431
|
await browser.stopHarRecording(client, oldSid);
|
|
2194
2432
|
}
|
|
2195
|
-
catch {
|
|
2433
|
+
catch {
|
|
2434
|
+
/* ignore */
|
|
2435
|
+
}
|
|
2196
2436
|
}
|
|
2197
2437
|
if (browser.getTraceStatus(oldSid)) {
|
|
2198
2438
|
try {
|
|
2199
2439
|
await browser.stopTrace(client, oldSid);
|
|
2200
2440
|
}
|
|
2201
|
-
catch {
|
|
2441
|
+
catch {
|
|
2442
|
+
/* ignore */
|
|
2443
|
+
}
|
|
2202
2444
|
}
|
|
2203
2445
|
if (browser.isProfilingActive(oldSid)) {
|
|
2204
2446
|
try {
|
|
2205
2447
|
await browser.stopCpuProfile(client, oldSid);
|
|
2206
2448
|
}
|
|
2207
|
-
catch {
|
|
2449
|
+
catch {
|
|
2450
|
+
/* ignore */
|
|
2451
|
+
}
|
|
2208
2452
|
}
|
|
2209
2453
|
browser.teardownRouteInterception(oldSid);
|
|
2210
2454
|
browser.stopRequestCapture(oldSid);
|
|
@@ -2366,11 +2610,13 @@ const cookiesCommand = {
|
|
|
2366
2610
|
if (!name || value === undefined) {
|
|
2367
2611
|
throw new Error('Usage: monomind browse cookies set <name> <value> [--domain <d>]');
|
|
2368
2612
|
}
|
|
2369
|
-
await browser.setCookies(client, sessionId, [
|
|
2613
|
+
await browser.setCookies(client, sessionId, [
|
|
2614
|
+
{
|
|
2370
2615
|
name,
|
|
2371
2616
|
value,
|
|
2372
2617
|
domain: ctx.flags.domain,
|
|
2373
|
-
}
|
|
2618
|
+
},
|
|
2619
|
+
]);
|
|
2374
2620
|
output.printSuccess(`Cookie set: ${name}`);
|
|
2375
2621
|
break;
|
|
2376
2622
|
}
|
|
@@ -2406,9 +2652,7 @@ const pdfCommand = {
|
|
|
2406
2652
|
const isCommand = {
|
|
2407
2653
|
name: 'is',
|
|
2408
2654
|
description: 'Check element state. Usage: monomind browse is visible|enabled|checked @e1',
|
|
2409
|
-
options: [
|
|
2410
|
-
{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
|
|
2411
|
-
],
|
|
2655
|
+
options: [{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false }],
|
|
2412
2656
|
action: async (ctx) => {
|
|
2413
2657
|
const { client, sessionId } = await ensureConnected(_port);
|
|
2414
2658
|
const browser = await getBrowser();
|
|
@@ -2429,7 +2673,8 @@ const isCommand = {
|
|
|
2429
2673
|
case 'checked':
|
|
2430
2674
|
result = await browser.isChecked(client, sessionId, ref);
|
|
2431
2675
|
break;
|
|
2432
|
-
default:
|
|
2676
|
+
default:
|
|
2677
|
+
throw new Error(`Unknown check: ${check}. Use: visible|enabled|checked`);
|
|
2433
2678
|
}
|
|
2434
2679
|
if (ctx.flags.json) {
|
|
2435
2680
|
print(JSON.stringify({ data: { [check]: result } }));
|
|
@@ -2467,7 +2712,7 @@ const findCommand = {
|
|
|
2467
2712
|
if (locator === 'alttext') {
|
|
2468
2713
|
// I2: use JS attribute comparison to avoid broken CSS selectors for values with spaces/quotes
|
|
2469
2714
|
const valJson = JSON.stringify(value);
|
|
2470
|
-
const found = await browser.evaluateJs(client, sessionId, `(function(v){var el=document.querySelector('img[alt]')||null;var all=document.querySelectorAll('[alt]');for(var i=0;i<all.length;i++){if(all[i].getAttribute('alt')===v){all[i].setAttribute('data-mm-located','true');return true;}}return false;})(${valJson})`);
|
|
2715
|
+
const found = (await browser.evaluateJs(client, sessionId, `(function(v){var el=document.querySelector('img[alt]')||null;var all=document.querySelectorAll('[alt]');for(var i=0;i<all.length;i++){if(all[i].getAttribute('alt')===v){all[i].setAttribute('data-mm-located','true');return true;}}return false;})(${valJson})`));
|
|
2471
2716
|
if (!found) {
|
|
2472
2717
|
output.printWarning(`alttext not found: ${value}`);
|
|
2473
2718
|
return { success: false };
|
|
@@ -2479,7 +2724,7 @@ const findCommand = {
|
|
|
2479
2724
|
if (locator === 'title') {
|
|
2480
2725
|
// I2: use JS attribute comparison to avoid broken CSS selectors for values with spaces/quotes
|
|
2481
2726
|
const valJson = JSON.stringify(value);
|
|
2482
|
-
const found = await browser.evaluateJs(client, sessionId, `(function(v){var all=document.querySelectorAll('[title]');for(var i=0;i<all.length;i++){if(all[i].getAttribute('title')===v){all[i].setAttribute('data-mm-located','true');return true;}}return false;})(${valJson})`);
|
|
2727
|
+
const found = (await browser.evaluateJs(client, sessionId, `(function(v){var all=document.querySelectorAll('[title]');for(var i=0;i<all.length;i++){if(all[i].getAttribute('title')===v){all[i].setAttribute('data-mm-located','true');return true;}}return false;})(${valJson})`));
|
|
2483
2728
|
if (!found) {
|
|
2484
2729
|
output.printWarning(`title not found: ${value}`);
|
|
2485
2730
|
return { success: false };
|
|
@@ -2585,7 +2830,13 @@ const diffCommand = {
|
|
|
2585
2830
|
name: 'diff',
|
|
2586
2831
|
description: 'Compare two URLs or snapshots. Usage: monomind browse diff url <url1> <url2> [--interactive] [--json]',
|
|
2587
2832
|
options: [
|
|
2588
|
-
{
|
|
2833
|
+
{
|
|
2834
|
+
name: 'interactive',
|
|
2835
|
+
short: 'i',
|
|
2836
|
+
type: 'boolean',
|
|
2837
|
+
description: 'Snapshot interactive elements only',
|
|
2838
|
+
default: false,
|
|
2839
|
+
},
|
|
2589
2840
|
{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
|
|
2590
2841
|
],
|
|
2591
2842
|
action: async (ctx) => {
|
|
@@ -2600,11 +2851,15 @@ const diffCommand = {
|
|
|
2600
2851
|
// Capture snapshot at url1
|
|
2601
2852
|
await browser.openUrl(client, sessionId, url1);
|
|
2602
2853
|
await browser.waitFor(client, sessionId, { load: 'load', timeout: 15000 });
|
|
2603
|
-
const snap1 = await browser.captureSnapshot(client, sessionId, {
|
|
2854
|
+
const snap1 = await browser.captureSnapshot(client, sessionId, {
|
|
2855
|
+
interactiveOnly: ctx.flags.interactive,
|
|
2856
|
+
});
|
|
2604
2857
|
// Capture snapshot at url2
|
|
2605
2858
|
await browser.openUrl(client, sessionId, url2);
|
|
2606
2859
|
await browser.waitFor(client, sessionId, { load: 'load', timeout: 15000 });
|
|
2607
|
-
const snap2 = await browser.captureSnapshot(client, sessionId, {
|
|
2860
|
+
const snap2 = await browser.captureSnapshot(client, sessionId, {
|
|
2861
|
+
interactiveOnly: ctx.flags.interactive,
|
|
2862
|
+
});
|
|
2608
2863
|
_refs = snap2.refs;
|
|
2609
2864
|
await browser.saveRefCache(_targetId, snap2.url, _refs);
|
|
2610
2865
|
// Text diff
|
|
@@ -2616,7 +2871,15 @@ const diffCommand = {
|
|
|
2616
2871
|
const onlyIn2 = lines2.filter((l) => !set1.has(l));
|
|
2617
2872
|
const changed = onlyIn1.length > 0 || onlyIn2.length > 0;
|
|
2618
2873
|
if (ctx.flags.json) {
|
|
2619
|
-
print(JSON.stringify({
|
|
2874
|
+
print(JSON.stringify({
|
|
2875
|
+
changed,
|
|
2876
|
+
url1,
|
|
2877
|
+
url2,
|
|
2878
|
+
onlyIn1,
|
|
2879
|
+
onlyIn2,
|
|
2880
|
+
additions: onlyIn2.length,
|
|
2881
|
+
removals: onlyIn1.length,
|
|
2882
|
+
}));
|
|
2620
2883
|
}
|
|
2621
2884
|
else {
|
|
2622
2885
|
if (!changed) {
|
|
@@ -2630,7 +2893,10 @@ const diffCommand = {
|
|
|
2630
2893
|
print(`\x1b[32m+ ${l}\x1b[0m`);
|
|
2631
2894
|
}
|
|
2632
2895
|
}
|
|
2633
|
-
return {
|
|
2896
|
+
return {
|
|
2897
|
+
success: true,
|
|
2898
|
+
data: { changed, url1, url2, additions: onlyIn2.length, removals: onlyIn1.length },
|
|
2899
|
+
};
|
|
2634
2900
|
}
|
|
2635
2901
|
throw new Error('Usage: monomind browse diff url <url1> <url2>');
|
|
2636
2902
|
},
|
|
@@ -2744,7 +3010,6 @@ export function parseBatchCommandLine(cmdStr) {
|
|
|
2744
3010
|
flags[boolFlag[1]] = true;
|
|
2745
3011
|
rest = rest.slice(boolFlag[0].length);
|
|
2746
3012
|
consumedAnother = true;
|
|
2747
|
-
continue;
|
|
2748
3013
|
}
|
|
2749
3014
|
}
|
|
2750
3015
|
return { subName: 'eval', subArgs: [rest], flags };
|
|
@@ -2803,7 +3068,9 @@ const batchCommand = {
|
|
|
2803
3068
|
parsedFlags[key] = true;
|
|
2804
3069
|
}
|
|
2805
3070
|
}
|
|
2806
|
-
else if (subArgs[i].startsWith('-') &&
|
|
3071
|
+
else if (subArgs[i].startsWith('-') &&
|
|
3072
|
+
subArgs[i].length === 2 &&
|
|
3073
|
+
/[a-zA-Z]/.test(subArgs[i][1])) {
|
|
2807
3074
|
consumedIndices.add(i);
|
|
2808
3075
|
const shortKey = subArgs[i][1];
|
|
2809
3076
|
const optDef = subCmd.options?.find((o) => o.short === shortKey);
|
|
@@ -2835,7 +3102,11 @@ const batchCommand = {
|
|
|
2835
3102
|
};
|
|
2836
3103
|
const cmdResult = await subCmd.action(fakeCtx);
|
|
2837
3104
|
const succeeded = cmdResult?.success !== false;
|
|
2838
|
-
results.push({
|
|
3105
|
+
results.push({
|
|
3106
|
+
command: cmdStr,
|
|
3107
|
+
success: succeeded,
|
|
3108
|
+
error: succeeded ? undefined : 'Command returned failure',
|
|
3109
|
+
});
|
|
2839
3110
|
if (!succeeded && ctx.flags.bail)
|
|
2840
3111
|
break;
|
|
2841
3112
|
}
|
|
@@ -2886,7 +3157,12 @@ const connectCommand = {
|
|
|
2886
3157
|
options: [
|
|
2887
3158
|
{ name: 'port', short: 'p', type: 'number', description: 'CDP port', default: 9222 },
|
|
2888
3159
|
{ name: 'target', type: 'string', description: 'Target ID to attach to' },
|
|
2889
|
-
{
|
|
3160
|
+
{
|
|
3161
|
+
name: 'auto-connect',
|
|
3162
|
+
type: 'boolean',
|
|
3163
|
+
description: 'Auto-discover running Chrome on ports 9222 and 9229',
|
|
3164
|
+
default: false,
|
|
3165
|
+
},
|
|
2890
3166
|
],
|
|
2891
3167
|
action: async (ctx) => {
|
|
2892
3168
|
let port = ctx.flags.port ?? 9222;
|
|
@@ -2902,7 +3178,9 @@ const connectCommand = {
|
|
|
2902
3178
|
break;
|
|
2903
3179
|
}
|
|
2904
3180
|
}
|
|
2905
|
-
catch {
|
|
3181
|
+
catch {
|
|
3182
|
+
/* port not open */
|
|
3183
|
+
}
|
|
2906
3184
|
}
|
|
2907
3185
|
if (!found)
|
|
2908
3186
|
throw new Error('No running Chrome instance found. Launch Chrome with --remote-debugging-port or use --port.');
|
|
@@ -2915,19 +3193,25 @@ const connectCommand = {
|
|
|
2915
3193
|
try {
|
|
2916
3194
|
await browser.stopHarRecording(prevClient, prevSid);
|
|
2917
3195
|
}
|
|
2918
|
-
catch {
|
|
3196
|
+
catch {
|
|
3197
|
+
/* ignore */
|
|
3198
|
+
}
|
|
2919
3199
|
}
|
|
2920
3200
|
if (browser.getTraceStatus(prevSid)) {
|
|
2921
3201
|
try {
|
|
2922
3202
|
await browser.stopTrace(prevClient, prevSid);
|
|
2923
3203
|
}
|
|
2924
|
-
catch {
|
|
3204
|
+
catch {
|
|
3205
|
+
/* ignore */
|
|
3206
|
+
}
|
|
2925
3207
|
}
|
|
2926
3208
|
if (browser.isProfilingActive(prevSid)) {
|
|
2927
3209
|
try {
|
|
2928
3210
|
await browser.stopCpuProfile(prevClient, prevSid);
|
|
2929
3211
|
}
|
|
2930
|
-
catch {
|
|
3212
|
+
catch {
|
|
3213
|
+
/* ignore */
|
|
3214
|
+
}
|
|
2931
3215
|
}
|
|
2932
3216
|
browser.teardownRouteInterception(prevSid);
|
|
2933
3217
|
browser.stopRequestCapture(prevSid);
|
|
@@ -3030,7 +3314,9 @@ const traceCommand = {
|
|
|
3030
3314
|
throw new Error('Usage: monomind browse trace start|stop [path]');
|
|
3031
3315
|
switch (action) {
|
|
3032
3316
|
case 'start':
|
|
3033
|
-
await browser.startTrace(client, sessionId, {
|
|
3317
|
+
await browser.startTrace(client, sessionId, {
|
|
3318
|
+
screenshots: ctx.flags.screenshots,
|
|
3319
|
+
});
|
|
3034
3320
|
output.printSuccess('Trace started');
|
|
3035
3321
|
return { success: true };
|
|
3036
3322
|
case 'stop': {
|
|
@@ -3064,7 +3350,9 @@ const profilerCommand = {
|
|
|
3064
3350
|
throw new Error('Usage: monomind browse profiler start|stop|heap [path]');
|
|
3065
3351
|
switch (action) {
|
|
3066
3352
|
case 'start':
|
|
3067
|
-
await browser.startCpuProfile(client, sessionId, {
|
|
3353
|
+
await browser.startCpuProfile(client, sessionId, {
|
|
3354
|
+
samplingInterval: ctx.flags.interval,
|
|
3355
|
+
});
|
|
3068
3356
|
output.printSuccess('CPU profiler started');
|
|
3069
3357
|
return { success: true };
|
|
3070
3358
|
case 'stop': {
|
|
@@ -3155,7 +3443,7 @@ const resizeCommand = {
|
|
|
3155
3443
|
const browser = await getBrowser();
|
|
3156
3444
|
const width = parseInt(ctx.args[0], 10);
|
|
3157
3445
|
const height = parseInt(ctx.args[1], 10);
|
|
3158
|
-
if (isNaN(width) || isNaN(height))
|
|
3446
|
+
if (Number.isNaN(width) || Number.isNaN(height))
|
|
3159
3447
|
throw new Error('Usage: monomind browse resize <width> <height>');
|
|
3160
3448
|
await browser.setViewport(client, sessionId, width, height);
|
|
3161
3449
|
output.printSuccess(`Resized to ${width}x${height}`);
|
|
@@ -3303,7 +3591,10 @@ const browseCommand = {
|
|
|
3303
3591
|
],
|
|
3304
3592
|
examples: [
|
|
3305
3593
|
{ command: 'monomind browse open https://example.com', description: 'Open a URL' },
|
|
3306
|
-
{
|
|
3594
|
+
{
|
|
3595
|
+
command: 'monomind browse snapshot -i',
|
|
3596
|
+
description: 'Interactive-only snapshot (93% token reduction)',
|
|
3597
|
+
},
|
|
3307
3598
|
{ command: 'monomind browse click @e3', description: 'Click element by ref' },
|
|
3308
3599
|
{ command: 'monomind browse fill @e1 "user@example.com"', description: 'Fill an input' },
|
|
3309
3600
|
{ command: 'monomind browse press Enter', description: 'Press Enter key' },
|
|
@@ -3317,7 +3608,10 @@ const browseCommand = {
|
|
|
3317
3608
|
{ command: 'monomind browse state save my-session', description: 'Save session state' },
|
|
3318
3609
|
{ command: 'monomind browse navigate back', description: 'Navigate back' },
|
|
3319
3610
|
{ command: 'monomind browse eval "document.title"', description: 'Evaluate JavaScript' },
|
|
3320
|
-
{
|
|
3611
|
+
{
|
|
3612
|
+
command: 'monomind browse network route --pattern "https://api.*" --abort',
|
|
3613
|
+
description: 'Abort API calls',
|
|
3614
|
+
},
|
|
3321
3615
|
{ command: 'monomind browse close', description: 'Close browser session' },
|
|
3322
3616
|
],
|
|
3323
3617
|
action: async (_ctx) => {
|