@monoes/monobrowse 1.0.11 → 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/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 +5 -5
- 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 +44 -14
- 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 +1 -1
- 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 +11 -6
- 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 +60 -21
- 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/src/cli/commands.ts
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
* Provides ref-based element model and token-efficient accessibility snapshots
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type {
|
|
7
|
-
import { output } from './output.js';
|
|
6
|
+
import type { CdpClient, ElementRef, FindAction, NetworkRoute } from '../index.js';
|
|
8
7
|
import { createActionCommand } from './action.js';
|
|
8
|
+
import { output } from './output.js';
|
|
9
9
|
import { createPlatformCommand } from './platform.js';
|
|
10
|
-
import type {
|
|
10
|
+
import type { Command, CommandContext, CommandResult } from './types.js';
|
|
11
11
|
|
|
12
12
|
// Runtime state (single session per CLI process)
|
|
13
13
|
const DEFAULT_PORT = 9222;
|
|
@@ -51,7 +51,11 @@ function ensureSignalCleanupHandlers(): void {
|
|
|
51
51
|
} else {
|
|
52
52
|
const pid = browser.getLaunchedPid(_port);
|
|
53
53
|
if (pid !== undefined) {
|
|
54
|
-
try {
|
|
54
|
+
try {
|
|
55
|
+
process.kill(pid, 'SIGKILL');
|
|
56
|
+
} catch {
|
|
57
|
+
/* already exited */
|
|
58
|
+
}
|
|
55
59
|
}
|
|
56
60
|
}
|
|
57
61
|
} catch {
|
|
@@ -69,7 +73,9 @@ function ensureSignalCleanupHandlers(): void {
|
|
|
69
73
|
// ref-cache.ts's saveActivePort/loadActivePort) so subsequent commands
|
|
70
74
|
// attach to the browser the user actually opened instead of silently
|
|
71
75
|
// launching/attaching to a second, unrelated Chrome instance on 9222.
|
|
72
|
-
async function resolveDefaultPort(
|
|
76
|
+
async function resolveDefaultPort(
|
|
77
|
+
browser: Awaited<ReturnType<typeof getBrowser>>,
|
|
78
|
+
): Promise<number> {
|
|
73
79
|
const persisted = await browser.loadActivePortInfo();
|
|
74
80
|
if (!persisted) return DEFAULT_PORT;
|
|
75
81
|
if (!persisted.launched) {
|
|
@@ -78,10 +84,14 @@ async function resolveDefaultPort(browser: Awaited<ReturnType<typeof getBrowser>
|
|
|
78
84
|
// user's debug port and silently swap which browser commands act on —
|
|
79
85
|
// fail loudly instead.
|
|
80
86
|
try {
|
|
81
|
-
await fetch(`http://127.0.0.1:${persisted.port}/json/version`, {
|
|
87
|
+
await fetch(`http://127.0.0.1:${persisted.port}/json/version`, {
|
|
88
|
+
signal: AbortSignal.timeout(1500),
|
|
89
|
+
});
|
|
82
90
|
} catch {
|
|
83
91
|
await browser.clearActivePort();
|
|
84
|
-
throw new Error(
|
|
92
|
+
throw new Error(
|
|
93
|
+
`Connected browser on port ${persisted.port} is gone — re-run \`connect\` (or \`open\` to launch a fresh one).`,
|
|
94
|
+
);
|
|
85
95
|
}
|
|
86
96
|
}
|
|
87
97
|
return persisted.port;
|
|
@@ -89,7 +99,7 @@ async function resolveDefaultPort(browser: Awaited<ReturnType<typeof getBrowser>
|
|
|
89
99
|
|
|
90
100
|
async function ensureConnected(port: number, targetId?: string) {
|
|
91
101
|
const browser = await getBrowser();
|
|
92
|
-
if (!_client
|
|
102
|
+
if (!_client?.isConnected()) {
|
|
93
103
|
if (_client && _sessionId) {
|
|
94
104
|
browser.teardownRouteInterception(_sessionId);
|
|
95
105
|
browser.stopRequestCapture(_sessionId);
|
|
@@ -128,20 +138,20 @@ async function ensureConnected(port: number, targetId?: string) {
|
|
|
128
138
|
async function hydrateRefsFromCache(
|
|
129
139
|
browser: Awaited<ReturnType<typeof getBrowser>>,
|
|
130
140
|
targetId: string,
|
|
131
|
-
currentUrl: string
|
|
141
|
+
currentUrl: string,
|
|
132
142
|
): Promise<void> {
|
|
133
143
|
const cached = await browser.loadRefCache(targetId);
|
|
134
144
|
if (!cached) return;
|
|
135
145
|
if (currentUrl && cached.url && currentUrl !== cached.url) {
|
|
136
146
|
output.printError(
|
|
137
|
-
`Stale references — page has navigated (cache: ${cached.url} → current: ${currentUrl}). Re-run snapshot before using @eN refs
|
|
147
|
+
`Stale references — page has navigated (cache: ${cached.url} → current: ${currentUrl}). Re-run snapshot before using @eN refs.`,
|
|
138
148
|
);
|
|
139
149
|
return; // leave _refs empty — do not attempt to resolve refs against a different page
|
|
140
150
|
}
|
|
141
151
|
_refs = cached.refs;
|
|
142
152
|
if (cached.stale) {
|
|
143
153
|
output.printWarning(
|
|
144
|
-
`AX ref cache is ${Math.round(cached.ageMs / 1000)}s old — page may have changed since the last snapshot; re-run snapshot if refs don't resolve as expected
|
|
154
|
+
`AX ref cache is ${Math.round(cached.ageMs / 1000)}s old — page may have changed since the last snapshot; re-run snapshot if refs don't resolve as expected`,
|
|
145
155
|
);
|
|
146
156
|
}
|
|
147
157
|
}
|
|
@@ -150,23 +160,45 @@ async function hydrateRefsFromCache(
|
|
|
150
160
|
// Excludes 'auth', 'oauth', 'sso', 'saml' — their callback/ACS/token paths
|
|
151
161
|
// (/auth/callback, /sso/callback, /saml/acs) are completion endpoints, not walls.
|
|
152
162
|
// DOM detection (password field, CAPTCHA widgets) handles SSO/SAML login pages.
|
|
153
|
-
const ATTENTION_URL_RE =
|
|
163
|
+
const ATTENTION_URL_RE =
|
|
164
|
+
/\/(login|log-in|signin|sign-in|captcha|mfa|2fa|account\/login|accounts\/login|session\/new|users\/sign_in)(?:[/?#]|$)/i;
|
|
154
165
|
|
|
155
|
-
async function detectAttentionNeeded(
|
|
166
|
+
async function detectAttentionNeeded(
|
|
167
|
+
client: CdpClient,
|
|
168
|
+
sessionId: string,
|
|
169
|
+
url: string,
|
|
170
|
+
): Promise<'login' | 'captcha' | null> {
|
|
156
171
|
if (ATTENTION_URL_RE.test(url)) return 'login';
|
|
157
172
|
try {
|
|
158
|
-
const hasPassword = await client
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
173
|
+
const hasPassword = await client
|
|
174
|
+
.send<{ result: { value: boolean } }>(
|
|
175
|
+
'Runtime.evaluate',
|
|
176
|
+
{
|
|
177
|
+
expression: '!!document.querySelector("input[type=password]")',
|
|
178
|
+
returnByValue: true,
|
|
179
|
+
},
|
|
180
|
+
sessionId,
|
|
181
|
+
)
|
|
182
|
+
.then((r) => r.result?.value === true)
|
|
183
|
+
.catch(() => false);
|
|
162
184
|
if (hasPassword) return 'login';
|
|
163
185
|
|
|
164
|
-
const hasCaptcha = await client
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
186
|
+
const hasCaptcha = await client
|
|
187
|
+
.send<{ result: { value: boolean } }>(
|
|
188
|
+
'Runtime.evaluate',
|
|
189
|
+
{
|
|
190
|
+
expression:
|
|
191
|
+
'!!(document.querySelector("iframe[src*=recaptcha]") || document.querySelector("iframe[src*=hcaptcha]") || document.querySelector(".g-recaptcha") || document.querySelector(".h-captcha") || document.querySelector("[data-sitekey]"))',
|
|
192
|
+
returnByValue: true,
|
|
193
|
+
},
|
|
194
|
+
sessionId,
|
|
195
|
+
)
|
|
196
|
+
.then((r) => r.result?.value === true)
|
|
197
|
+
.catch(() => false);
|
|
168
198
|
if (hasCaptcha) return 'captcha';
|
|
169
|
-
} catch {
|
|
199
|
+
} catch {
|
|
200
|
+
/* ignore CDP errors */
|
|
201
|
+
}
|
|
170
202
|
return null;
|
|
171
203
|
}
|
|
172
204
|
|
|
@@ -174,13 +206,19 @@ function waitForEnter(): Promise<void> {
|
|
|
174
206
|
if (!process.stdin.isTTY) {
|
|
175
207
|
// Non-interactive context (MCP server, piped input, CI) — cannot safely read stdin.
|
|
176
208
|
// Auto-continue after a short grace period so automation isn't blocked.
|
|
177
|
-
output.printWarning(
|
|
178
|
-
|
|
209
|
+
output.printWarning(
|
|
210
|
+
'Non-interactive mode: auto-continuing in 30s. Switch to headed manually if needed.',
|
|
211
|
+
);
|
|
212
|
+
return new Promise((resolve) => setTimeout(resolve, 30_000));
|
|
179
213
|
}
|
|
180
|
-
return new Promise(resolve => {
|
|
214
|
+
return new Promise((resolve) => {
|
|
181
215
|
process.stdin.resume();
|
|
182
216
|
process.stdin.setEncoding('utf8');
|
|
183
|
-
const onData = () => {
|
|
217
|
+
const onData = () => {
|
|
218
|
+
process.stdin.pause();
|
|
219
|
+
process.stdin.off('data', onData);
|
|
220
|
+
resolve();
|
|
221
|
+
};
|
|
184
222
|
process.stdin.once('data', onData);
|
|
185
223
|
});
|
|
186
224
|
}
|
|
@@ -191,7 +229,11 @@ async function switchToHeaded(url: string, port: number): Promise<void> {
|
|
|
191
229
|
// Snapshot cookies from current headless session before closing
|
|
192
230
|
let savedCookies: unknown[] = [];
|
|
193
231
|
if (_client && _sessionId) {
|
|
194
|
-
try {
|
|
232
|
+
try {
|
|
233
|
+
savedCookies = await browser.getCookies(_client, _sessionId);
|
|
234
|
+
} catch {
|
|
235
|
+
/* ignore */
|
|
236
|
+
}
|
|
195
237
|
browser.teardownRouteInterception(_sessionId);
|
|
196
238
|
browser.stopRequestCapture(_sessionId);
|
|
197
239
|
browser.teardownDialogHandling(_sessionId);
|
|
@@ -215,7 +257,9 @@ async function switchToHeaded(url: string, port: number): Promise<void> {
|
|
|
215
257
|
_sessionId = fallback.sessionId;
|
|
216
258
|
_targetId = fallback.target.id;
|
|
217
259
|
_refs = new Map();
|
|
218
|
-
throw new Error(
|
|
260
|
+
throw new Error(
|
|
261
|
+
`Cannot open headed browser: ${err instanceof Error ? err.message : String(err)}`,
|
|
262
|
+
);
|
|
219
263
|
}
|
|
220
264
|
const conn = await browser.connectToTarget(headedPort);
|
|
221
265
|
_client = conn.client;
|
|
@@ -224,16 +268,28 @@ async function switchToHeaded(url: string, port: number): Promise<void> {
|
|
|
224
268
|
_refs = new Map();
|
|
225
269
|
|
|
226
270
|
if (savedCookies.length) {
|
|
227
|
-
try {
|
|
271
|
+
try {
|
|
272
|
+
await browser.setCookies(
|
|
273
|
+
_client,
|
|
274
|
+
_sessionId,
|
|
275
|
+
savedCookies as Parameters<typeof browser.setCookies>[2],
|
|
276
|
+
);
|
|
277
|
+
} catch {
|
|
278
|
+
/* ignore */
|
|
279
|
+
}
|
|
228
280
|
}
|
|
229
281
|
await browser.openUrl(_client, _sessionId, url);
|
|
230
282
|
|
|
231
|
-
output.printInfo(
|
|
283
|
+
output.printInfo(
|
|
284
|
+
'Browser window opened. Complete the required action (login / CAPTCHA), then press Enter to continue in headless mode...',
|
|
285
|
+
);
|
|
232
286
|
await waitForEnter();
|
|
233
287
|
|
|
234
288
|
// Capture post-auth cookies
|
|
235
289
|
const authCookies = await browser.getCookies(_client, _sessionId).catch(() => [] as unknown[]);
|
|
236
|
-
const authLocalStorage = await browser
|
|
290
|
+
const authLocalStorage = await browser
|
|
291
|
+
.getLocalStorage(_client, _sessionId)
|
|
292
|
+
.catch(() => ({}) as Record<string, string>);
|
|
237
293
|
|
|
238
294
|
// Close headed session — actually terminate the underlying Chrome process
|
|
239
295
|
// (Browser.close CDP command, PID-kill fallback), not just our CDP client
|
|
@@ -258,10 +314,22 @@ async function switchToHeaded(url: string, port: number): Promise<void> {
|
|
|
258
314
|
_refs = new Map();
|
|
259
315
|
|
|
260
316
|
if (authCookies.length) {
|
|
261
|
-
try {
|
|
317
|
+
try {
|
|
318
|
+
await browser.setCookies(
|
|
319
|
+
_client,
|
|
320
|
+
_sessionId,
|
|
321
|
+
authCookies as Parameters<typeof browser.setCookies>[2],
|
|
322
|
+
);
|
|
323
|
+
} catch {
|
|
324
|
+
/* ignore */
|
|
325
|
+
}
|
|
262
326
|
}
|
|
263
327
|
if (authLocalStorage && Object.keys(authLocalStorage).length) {
|
|
264
|
-
try {
|
|
328
|
+
try {
|
|
329
|
+
await browser.setLocalStorage(_client, _sessionId, authLocalStorage);
|
|
330
|
+
} catch {
|
|
331
|
+
/* ignore */
|
|
332
|
+
}
|
|
265
333
|
}
|
|
266
334
|
}
|
|
267
335
|
|
|
@@ -278,7 +346,7 @@ function imageFormat<T extends string>(value: unknown, allowed: readonly T[], fa
|
|
|
278
346
|
}
|
|
279
347
|
|
|
280
348
|
function print(msg: string) {
|
|
281
|
-
process.stdout.write(msg
|
|
349
|
+
process.stdout.write(`${msg}\n`);
|
|
282
350
|
}
|
|
283
351
|
|
|
284
352
|
// ---------------------------------------------------------------------------
|
|
@@ -290,7 +358,12 @@ const openCommand: Command = {
|
|
|
290
358
|
description: 'Open a URL in the browser. Usage: monomind browse open <url>',
|
|
291
359
|
options: [
|
|
292
360
|
{ name: 'port', short: 'p', type: 'number', description: 'CDP port', default: 9222 },
|
|
293
|
-
{
|
|
361
|
+
{
|
|
362
|
+
name: 'headed',
|
|
363
|
+
type: 'boolean',
|
|
364
|
+
description: 'Force visible browser window',
|
|
365
|
+
default: false,
|
|
366
|
+
},
|
|
294
367
|
{ name: 'session', short: 's', type: 'string', description: 'Session name to restore' },
|
|
295
368
|
{ name: 'state', type: 'string', description: 'State file to load' },
|
|
296
369
|
],
|
|
@@ -306,13 +379,25 @@ const openCommand: Command = {
|
|
|
306
379
|
const prevSid = _sessionId;
|
|
307
380
|
const prevClient = _client;
|
|
308
381
|
if (browser.getHarStatus(prevSid).recording) {
|
|
309
|
-
try {
|
|
382
|
+
try {
|
|
383
|
+
await browser.stopHarRecording(prevClient, prevSid);
|
|
384
|
+
} catch {
|
|
385
|
+
/* ignore */
|
|
386
|
+
}
|
|
310
387
|
}
|
|
311
388
|
if (browser.getTraceStatus(prevSid)) {
|
|
312
|
-
try {
|
|
389
|
+
try {
|
|
390
|
+
await browser.stopTrace(prevClient, prevSid);
|
|
391
|
+
} catch {
|
|
392
|
+
/* ignore */
|
|
393
|
+
}
|
|
313
394
|
}
|
|
314
395
|
if (browser.isProfilingActive(prevSid)) {
|
|
315
|
-
try {
|
|
396
|
+
try {
|
|
397
|
+
await browser.stopCpuProfile(prevClient, prevSid);
|
|
398
|
+
} catch {
|
|
399
|
+
/* ignore */
|
|
400
|
+
}
|
|
316
401
|
}
|
|
317
402
|
browser.teardownRouteInterception(prevSid);
|
|
318
403
|
browser.stopRequestCapture(prevSid);
|
|
@@ -352,7 +437,10 @@ const openCommand: Command = {
|
|
|
352
437
|
// launched it) instead of overwriting with undefined.
|
|
353
438
|
const existing = await browser.loadActivePortInfo();
|
|
354
439
|
const preserved = existing && existing.port === _port ? existing : undefined;
|
|
355
|
-
await browser.saveActivePort(_port, {
|
|
440
|
+
await browser.saveActivePort(_port, {
|
|
441
|
+
pid: preserved?.pid,
|
|
442
|
+
userDataDir: preserved?.userDataDir,
|
|
443
|
+
});
|
|
356
444
|
}
|
|
357
445
|
ensureSignalCleanupHandlers();
|
|
358
446
|
const conn = await browser.connectToTarget(_port);
|
|
@@ -381,10 +469,14 @@ const openCommand: Command = {
|
|
|
381
469
|
if (!forceHeaded) {
|
|
382
470
|
const attentionType = await detectAttentionNeeded(_client, _sessionId, currentUrl);
|
|
383
471
|
if (attentionType) {
|
|
384
|
-
output.printWarning(
|
|
472
|
+
output.printWarning(
|
|
473
|
+
`${attentionType === 'captcha' ? 'CAPTCHA' : 'Login'} detected — switching to headed mode`,
|
|
474
|
+
);
|
|
385
475
|
await switchToHeaded(currentUrl, port);
|
|
386
476
|
await browser.openUrl(_client!, _sessionId, currentUrl);
|
|
387
|
-
output.printSuccess(
|
|
477
|
+
output.printSuccess(
|
|
478
|
+
`Resumed headless after ${attentionType === 'captcha' ? 'CAPTCHA' : 'login'}`,
|
|
479
|
+
);
|
|
388
480
|
}
|
|
389
481
|
}
|
|
390
482
|
|
|
@@ -400,15 +492,50 @@ const snapshotCommand: Command = {
|
|
|
400
492
|
name: 'snapshot',
|
|
401
493
|
description: 'Capture accessibility snapshot with ref-based element handles (@e1, @e2, ...)',
|
|
402
494
|
options: [
|
|
403
|
-
{
|
|
404
|
-
|
|
495
|
+
{
|
|
496
|
+
name: 'interactive',
|
|
497
|
+
short: 'i',
|
|
498
|
+
type: 'boolean',
|
|
499
|
+
description: 'Interactive elements only (93% token reduction)',
|
|
500
|
+
default: false,
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
name: 'compact',
|
|
504
|
+
short: 'c',
|
|
505
|
+
type: 'boolean',
|
|
506
|
+
description: 'Compact output format',
|
|
507
|
+
default: false,
|
|
508
|
+
},
|
|
405
509
|
{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
|
|
406
510
|
{ name: 'depth', short: 'd', type: 'number', description: 'Max depth of AX tree to show' },
|
|
407
|
-
{
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
511
|
+
{
|
|
512
|
+
name: 'selector',
|
|
513
|
+
short: 's',
|
|
514
|
+
type: 'string',
|
|
515
|
+
description: 'Scope snapshot to a CSS selector',
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
name: 'save',
|
|
519
|
+
type: 'string',
|
|
520
|
+
description: 'Save snapshot text to file (baseline for --diff)',
|
|
521
|
+
},
|
|
522
|
+
{
|
|
523
|
+
name: 'diff',
|
|
524
|
+
type: 'string',
|
|
525
|
+
description: 'Compare current snapshot against a saved baseline file',
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
name: 'content-boundaries',
|
|
529
|
+
type: 'boolean',
|
|
530
|
+
description: 'Wrap output in sentinel markers to prevent page-content injection attacks',
|
|
531
|
+
default: false,
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
name: 'max-output',
|
|
535
|
+
type: 'number',
|
|
536
|
+
description:
|
|
537
|
+
'Truncate output to N characters (prevents context window blowout on large pages)',
|
|
538
|
+
},
|
|
412
539
|
],
|
|
413
540
|
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
414
541
|
const { client, sessionId } = await ensureConnected(_port);
|
|
@@ -436,8 +563,8 @@ const snapshotCommand: Command = {
|
|
|
436
563
|
|
|
437
564
|
// --save: write snapshot text to baseline file
|
|
438
565
|
if (ctx.flags.save) {
|
|
439
|
-
const { writeFile, mkdir } = await import('fs/promises');
|
|
440
|
-
const { dirname } = await import('path');
|
|
566
|
+
const { writeFile, mkdir } = await import('node:fs/promises');
|
|
567
|
+
const { dirname } = await import('node:path');
|
|
441
568
|
const savePath = ctx.flags.save as string;
|
|
442
569
|
await mkdir(dirname(savePath), { recursive: true }).catch(() => {});
|
|
443
570
|
await writeFile(savePath, result.text, 'utf8');
|
|
@@ -447,35 +574,58 @@ const snapshotCommand: Command = {
|
|
|
447
574
|
|
|
448
575
|
// --diff: compare against baseline file
|
|
449
576
|
if (ctx.flags.diff) {
|
|
450
|
-
const { readFile } = await import('fs/promises');
|
|
577
|
+
const { readFile } = await import('node:fs/promises');
|
|
451
578
|
const baselinePath = ctx.flags.diff as string;
|
|
452
579
|
let baseline: string;
|
|
453
|
-
try {
|
|
454
|
-
|
|
580
|
+
try {
|
|
581
|
+
baseline = await readFile(baselinePath, 'utf8');
|
|
582
|
+
} catch {
|
|
583
|
+
throw new Error(`Baseline not found: ${baselinePath}. Run snapshot --save first.`);
|
|
584
|
+
}
|
|
455
585
|
const currentLines = result.text.split('\n');
|
|
456
586
|
const baselineLines = baseline.split('\n');
|
|
457
|
-
const added: string[] = [],
|
|
587
|
+
const added: string[] = [],
|
|
588
|
+
removed: string[] = [];
|
|
458
589
|
const baseSet = new Set(baselineLines);
|
|
459
590
|
const curSet = new Set(currentLines);
|
|
460
591
|
for (const l of currentLines) if (!baseSet.has(l)) added.push(l);
|
|
461
592
|
for (const l of baselineLines) if (!curSet.has(l)) removed.push(l);
|
|
462
593
|
const changed = added.length > 0 || removed.length > 0;
|
|
463
594
|
if (ctx.flags.json) {
|
|
464
|
-
print(
|
|
595
|
+
print(
|
|
596
|
+
JSON.stringify({
|
|
597
|
+
changed,
|
|
598
|
+
additions: added.length,
|
|
599
|
+
removals: removed.length,
|
|
600
|
+
added,
|
|
601
|
+
removed,
|
|
602
|
+
}),
|
|
603
|
+
);
|
|
465
604
|
} else {
|
|
466
|
-
if (!changed) {
|
|
467
|
-
|
|
605
|
+
if (!changed) {
|
|
606
|
+
output.printSuccess('No snapshot changes detected');
|
|
607
|
+
} else {
|
|
468
608
|
output.printWarning(`Snapshot changed: +${added.length} lines, -${removed.length} lines`);
|
|
469
609
|
for (const l of added) print(`\x1b[32m+ ${l}\x1b[0m`);
|
|
470
610
|
for (const l of removed) print(`\x1b[31m- ${l}\x1b[0m`);
|
|
471
611
|
}
|
|
472
612
|
}
|
|
473
|
-
return {
|
|
613
|
+
return {
|
|
614
|
+
success: true,
|
|
615
|
+
data: { changed, additions: added.length, removals: removed.length },
|
|
616
|
+
};
|
|
474
617
|
}
|
|
475
618
|
|
|
476
619
|
if (ctx.flags.json) {
|
|
477
620
|
const refsObj = Object.fromEntries([...result.refs.entries()].map(([k, v]) => [k, v]));
|
|
478
|
-
print(
|
|
621
|
+
print(
|
|
622
|
+
JSON.stringify({
|
|
623
|
+
url: result.url,
|
|
624
|
+
title: result.title,
|
|
625
|
+
refs: refsObj,
|
|
626
|
+
snapshot: result.text,
|
|
627
|
+
}),
|
|
628
|
+
);
|
|
479
629
|
} else {
|
|
480
630
|
print(`[${result.title}] ${result.url}\n`);
|
|
481
631
|
print(applyOutputLimits(result.text));
|
|
@@ -563,11 +713,20 @@ const waitCommand: Command = {
|
|
|
563
713
|
{ name: 'text', type: 'string', description: 'Wait for text to appear in page' },
|
|
564
714
|
{ name: 'not-text', type: 'string', description: 'Wait for text to disappear from page' },
|
|
565
715
|
{ name: 'selector', type: 'string', description: 'Wait for CSS selector to appear' },
|
|
566
|
-
{
|
|
716
|
+
{
|
|
717
|
+
name: 'load',
|
|
718
|
+
type: 'string',
|
|
719
|
+
description: 'Wait for load event: load|networkidle|domcontentloaded',
|
|
720
|
+
},
|
|
567
721
|
{ name: 'fn', type: 'string', description: 'Wait until JS expression returns truthy' },
|
|
568
722
|
{ name: 'ms', type: 'number', description: 'Wait N milliseconds' },
|
|
569
723
|
{ name: 'timeout', short: 't', type: 'number', description: 'Timeout in ms', default: 30000 },
|
|
570
|
-
{
|
|
724
|
+
{
|
|
725
|
+
name: 'download',
|
|
726
|
+
type: 'string',
|
|
727
|
+
description:
|
|
728
|
+
'Wait for a file download to complete and save to path (monitors Browser.downloadProgress events)',
|
|
729
|
+
},
|
|
571
730
|
],
|
|
572
731
|
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
573
732
|
const { client, sessionId } = await ensureConnected(_port);
|
|
@@ -584,12 +743,17 @@ const waitCommand: Command = {
|
|
|
584
743
|
if (ctx.flags.fn) {
|
|
585
744
|
const expr = ctx.flags.fn as string;
|
|
586
745
|
const rawTimeout = (ctx.flags.timeout as number) ?? 30000;
|
|
587
|
-
const timeout = Number.isFinite(rawTimeout)
|
|
746
|
+
const timeout = Number.isFinite(rawTimeout)
|
|
747
|
+
? Math.max(100, Math.min(rawTimeout, 300_000))
|
|
748
|
+
: 30000; // cap at 5min
|
|
588
749
|
const interval = 200;
|
|
589
750
|
const deadline = Date.now() + timeout;
|
|
590
751
|
while (Date.now() < deadline) {
|
|
591
752
|
const result = await browser.evaluateJs(client, sessionId, expr);
|
|
592
|
-
if (result) {
|
|
753
|
+
if (result) {
|
|
754
|
+
output.printSuccess('Wait function returned truthy');
|
|
755
|
+
return { success: true };
|
|
756
|
+
}
|
|
593
757
|
await new Promise((r) => setTimeout(r, interval));
|
|
594
758
|
}
|
|
595
759
|
throw new Error(`Timeout waiting for --fn: ${expr}`);
|
|
@@ -601,8 +765,15 @@ const waitCommand: Command = {
|
|
|
601
765
|
const interval = 200;
|
|
602
766
|
const deadline = Date.now() + timeout;
|
|
603
767
|
while (Date.now() < deadline) {
|
|
604
|
-
const text = await browser.evaluateJs(
|
|
605
|
-
|
|
768
|
+
const text = (await browser.evaluateJs(
|
|
769
|
+
client,
|
|
770
|
+
sessionId,
|
|
771
|
+
'document.body?.innerText ?? ""',
|
|
772
|
+
)) as string;
|
|
773
|
+
if (!text.includes(target)) {
|
|
774
|
+
output.printSuccess('Text disappeared');
|
|
775
|
+
return { success: true };
|
|
776
|
+
}
|
|
606
777
|
await new Promise((r) => setTimeout(r, interval));
|
|
607
778
|
}
|
|
608
779
|
throw new Error(`Timeout waiting for text to disappear: "${target}"`);
|
|
@@ -610,28 +781,52 @@ const waitCommand: Command = {
|
|
|
610
781
|
|
|
611
782
|
if (ctx.flags.download) {
|
|
612
783
|
const savePath = ctx.flags.download as string;
|
|
613
|
-
const { mkdir } = await import('fs/promises');
|
|
614
|
-
const { dirname, join } = await import('path');
|
|
615
|
-
const { tmpdir } = await import('os');
|
|
784
|
+
const { mkdir } = await import('node:fs/promises');
|
|
785
|
+
const { dirname, join } = await import('node:path');
|
|
786
|
+
const { tmpdir } = await import('node:os');
|
|
616
787
|
const downloadDir = join(tmpdir(), `mm-dl-wait-${Date.now()}`);
|
|
617
788
|
await mkdir(downloadDir, { recursive: true });
|
|
618
|
-
await client
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
789
|
+
await client
|
|
790
|
+
.send(
|
|
791
|
+
'Browser.setDownloadBehavior',
|
|
792
|
+
{
|
|
793
|
+
behavior: 'allow',
|
|
794
|
+
downloadPath: downloadDir,
|
|
795
|
+
eventsEnabled: true,
|
|
796
|
+
},
|
|
797
|
+
undefined,
|
|
798
|
+
)
|
|
799
|
+
.catch(() =>
|
|
800
|
+
client
|
|
801
|
+
.send(
|
|
802
|
+
'Page.setDownloadBehavior',
|
|
803
|
+
{ behavior: 'allow', downloadPath: downloadDir },
|
|
804
|
+
sessionId,
|
|
805
|
+
)
|
|
806
|
+
.catch(() => {}),
|
|
807
|
+
);
|
|
623
808
|
const MAX_DOWNLOAD_TIMEOUT = 5 * 60 * 1000; // I6: cap at 5 minutes
|
|
624
809
|
const rawTimeout = Math.min((ctx.flags.timeout as number) ?? 30000, MAX_DOWNLOAD_TIMEOUT);
|
|
625
810
|
const finalPath = await new Promise<string>((resolve, reject) => {
|
|
626
811
|
let guid = '';
|
|
627
812
|
let settled = false;
|
|
628
813
|
// C2: capture off() functions to avoid listener leaks
|
|
629
|
-
const offBegin = client.on(
|
|
814
|
+
const offBegin = client.on(
|
|
815
|
+
'Browser.downloadWillBegin',
|
|
816
|
+
(params: Record<string, unknown>) => {
|
|
817
|
+
guid = params.guid as string;
|
|
818
|
+
},
|
|
819
|
+
);
|
|
630
820
|
let offProgress: (() => void) | undefined;
|
|
631
821
|
// cleanup defined before setTimeout so the timeout callback can call it
|
|
632
822
|
let tid: ReturnType<typeof setTimeout>;
|
|
633
823
|
let pollTid: ReturnType<typeof setInterval> | undefined;
|
|
634
|
-
const cleanup = () => {
|
|
824
|
+
const cleanup = () => {
|
|
825
|
+
clearTimeout(tid);
|
|
826
|
+
clearInterval(pollTid);
|
|
827
|
+
offBegin?.();
|
|
828
|
+
offProgress?.();
|
|
829
|
+
};
|
|
635
830
|
const finish = (path: string) => {
|
|
636
831
|
if (settled) return;
|
|
637
832
|
settled = true;
|
|
@@ -645,24 +840,27 @@ const waitCommand: Command = {
|
|
|
645
840
|
reject(err);
|
|
646
841
|
};
|
|
647
842
|
tid = setTimeout(() => fail(new Error('Download timed out')), rawTimeout);
|
|
648
|
-
offProgress = client.on(
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
const
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
843
|
+
offProgress = client.on(
|
|
844
|
+
'Browser.downloadProgress',
|
|
845
|
+
async (params: Record<string, unknown>) => {
|
|
846
|
+
if (params.guid === guid && params.state === 'completed') {
|
|
847
|
+
const { readdir, rename, rmdir } = await import('node:fs/promises');
|
|
848
|
+
const files = await readdir(downloadDir);
|
|
849
|
+
if (files.length > 0) {
|
|
850
|
+
const src = join(downloadDir, files[0]);
|
|
851
|
+
await mkdir(dirname(savePath), { recursive: true });
|
|
852
|
+
await rename(src, savePath);
|
|
853
|
+
await rmdir(downloadDir).catch(() => {}); // I1: cleanup temp dir
|
|
854
|
+
finish(savePath);
|
|
855
|
+
} else {
|
|
856
|
+
await rmdir(downloadDir).catch(() => {}); // I1: cleanup temp dir
|
|
857
|
+
fail(new Error('Download completed but no file found'));
|
|
858
|
+
}
|
|
859
|
+
} else if (params.guid === guid && params.state === 'canceled') {
|
|
860
|
+
fail(new Error('Download was canceled'));
|
|
661
861
|
}
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
}
|
|
665
|
-
});
|
|
862
|
+
},
|
|
863
|
+
);
|
|
666
864
|
|
|
667
865
|
// Fallback for the empty-guid race: this process's listener attaches
|
|
668
866
|
// AFTER a separate `click` process may have already started (and even
|
|
@@ -675,7 +873,7 @@ const waitCommand: Command = {
|
|
|
675
873
|
pollTid = setInterval(async () => {
|
|
676
874
|
if (settled || guid) return; // a real CDP event has taken over
|
|
677
875
|
try {
|
|
678
|
-
const { stat } = await import('fs/promises');
|
|
876
|
+
const { stat } = await import('node:fs/promises');
|
|
679
877
|
const st = await stat(savePath);
|
|
680
878
|
if (st.isFile() && st.size > 0 && st.size === lastSize) {
|
|
681
879
|
stableReads++;
|
|
@@ -708,13 +906,25 @@ const waitCommand: Command = {
|
|
|
708
906
|
|
|
709
907
|
const screenshotCommand: Command = {
|
|
710
908
|
name: 'screenshot',
|
|
711
|
-
description:
|
|
909
|
+
description:
|
|
910
|
+
'Capture a screenshot. Usage: monomind browse screenshot [path] [--annotate] [--hide-scrollbars]',
|
|
712
911
|
options: [
|
|
713
912
|
{ name: 'full', type: 'boolean', description: 'Full page screenshot', default: false },
|
|
714
913
|
{ name: 'format', type: 'string', description: 'Format: png|jpeg|webp', default: 'png' },
|
|
715
914
|
{ name: 'quality', type: 'number', description: 'Quality 0-100 for jpeg/webp', default: 80 },
|
|
716
|
-
{
|
|
717
|
-
|
|
915
|
+
{
|
|
916
|
+
name: 'annotate',
|
|
917
|
+
type: 'boolean',
|
|
918
|
+
description:
|
|
919
|
+
'Overlay numbered labels keyed to @eN refs from last snapshot (viewport-only; do not combine with --full)',
|
|
920
|
+
default: false,
|
|
921
|
+
},
|
|
922
|
+
{
|
|
923
|
+
name: 'hide-scrollbars',
|
|
924
|
+
type: 'boolean',
|
|
925
|
+
description: 'Hide native scrollbars via CSS injection before capture',
|
|
926
|
+
default: false,
|
|
927
|
+
},
|
|
718
928
|
{ name: 'json', type: 'boolean', description: 'Output JSON with path', default: false },
|
|
719
929
|
],
|
|
720
930
|
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
@@ -723,10 +933,16 @@ const screenshotCommand: Command = {
|
|
|
723
933
|
|
|
724
934
|
const hideScrollbars = ctx.flags['hide-scrollbars'] as boolean;
|
|
725
935
|
if (hideScrollbars) {
|
|
726
|
-
await client
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
936
|
+
await client
|
|
937
|
+
.send(
|
|
938
|
+
'Runtime.evaluate',
|
|
939
|
+
{
|
|
940
|
+
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);})()`,
|
|
941
|
+
returnByValue: false,
|
|
942
|
+
},
|
|
943
|
+
sessionId,
|
|
944
|
+
)
|
|
945
|
+
.catch(() => {});
|
|
730
946
|
}
|
|
731
947
|
|
|
732
948
|
const annotate = ctx.flags.annotate as boolean;
|
|
@@ -743,10 +959,16 @@ const screenshotCommand: Command = {
|
|
|
743
959
|
});
|
|
744
960
|
} finally {
|
|
745
961
|
if (hideScrollbars) {
|
|
746
|
-
await client
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
962
|
+
await client
|
|
963
|
+
.send(
|
|
964
|
+
'Runtime.evaluate',
|
|
965
|
+
{
|
|
966
|
+
expression: `(function(){var s=document.getElementById('__mm_noscroll__');if(s)s.remove();})()`,
|
|
967
|
+
returnByValue: false,
|
|
968
|
+
},
|
|
969
|
+
sessionId,
|
|
970
|
+
)
|
|
971
|
+
.catch(() => {});
|
|
750
972
|
}
|
|
751
973
|
}
|
|
752
974
|
|
|
@@ -762,16 +984,16 @@ const screenshotCommand: Command = {
|
|
|
762
984
|
|
|
763
985
|
const getCommand: Command = {
|
|
764
986
|
name: 'get',
|
|
765
|
-
description:
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
],
|
|
987
|
+
description:
|
|
988
|
+
'Get page info. Usage: monomind browse get url|title|text|html|value|attr|count|box|styles [@ref] [attrName]',
|
|
989
|
+
options: [{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false }],
|
|
769
990
|
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
770
991
|
const { client, sessionId } = await ensureConnected(_port);
|
|
771
992
|
const browser = await getBrowser();
|
|
772
993
|
|
|
773
994
|
const what = ctx.args[0] as string;
|
|
774
|
-
if (!what)
|
|
995
|
+
if (!what)
|
|
996
|
+
throw new Error('Usage: monomind browse get url|title|text|html|value|attr|count|box|styles');
|
|
775
997
|
|
|
776
998
|
let value: unknown;
|
|
777
999
|
|
|
@@ -790,19 +1012,32 @@ const getCommand: Command = {
|
|
|
790
1012
|
if (!ref) throw new Error(`Ref @${refKey} not found`);
|
|
791
1013
|
const objectId = await browser.getObjectIdForRef(client, sessionId, ref);
|
|
792
1014
|
if (!objectId) throw new Error('Element not in DOM');
|
|
793
|
-
const result = await client.send<{ result: { value?: string } }>(
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
1015
|
+
const result = await client.send<{ result: { value?: string } }>(
|
|
1016
|
+
'Runtime.callFunctionOn',
|
|
1017
|
+
{
|
|
1018
|
+
functionDeclaration:
|
|
1019
|
+
'function() { return this.innerText || this.textContent || ""; }',
|
|
1020
|
+
objectId,
|
|
1021
|
+
returnByValue: true,
|
|
1022
|
+
},
|
|
1023
|
+
sessionId,
|
|
1024
|
+
);
|
|
798
1025
|
value = result.result?.value ?? '';
|
|
799
1026
|
} else {
|
|
800
|
-
value = (await browser.evaluateJs(
|
|
1027
|
+
value = (await browser.evaluateJs(
|
|
1028
|
+
client,
|
|
1029
|
+
sessionId,
|
|
1030
|
+
'document.body?.innerText ?? ""',
|
|
1031
|
+
)) as string;
|
|
801
1032
|
}
|
|
802
1033
|
break;
|
|
803
1034
|
}
|
|
804
1035
|
case 'html':
|
|
805
|
-
value = (await browser.evaluateJs(
|
|
1036
|
+
value = (await browser.evaluateJs(
|
|
1037
|
+
client,
|
|
1038
|
+
sessionId,
|
|
1039
|
+
'document.documentElement.outerHTML',
|
|
1040
|
+
)) as string;
|
|
806
1041
|
break;
|
|
807
1042
|
case 'value': {
|
|
808
1043
|
const refArg = ctx.args[1] as string;
|
|
@@ -812,33 +1047,48 @@ const getCommand: Command = {
|
|
|
812
1047
|
if (!ref) throw new Error(`Ref @${refKey} not found`);
|
|
813
1048
|
const objectId = await browser.getObjectIdForRef(client, sessionId, ref);
|
|
814
1049
|
if (!objectId) throw new Error('Element not in DOM');
|
|
815
|
-
const r = await client.send<{ result: { value?: string } }>(
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
1050
|
+
const r = await client.send<{ result: { value?: string } }>(
|
|
1051
|
+
'Runtime.callFunctionOn',
|
|
1052
|
+
{
|
|
1053
|
+
functionDeclaration: 'function() { return this.value ?? null; }',
|
|
1054
|
+
objectId,
|
|
1055
|
+
returnByValue: true,
|
|
1056
|
+
},
|
|
1057
|
+
sessionId,
|
|
1058
|
+
);
|
|
819
1059
|
value = r.result?.value ?? null;
|
|
820
1060
|
break;
|
|
821
1061
|
}
|
|
822
1062
|
case 'attr': {
|
|
823
1063
|
const refArg = ctx.args[1] as string;
|
|
824
1064
|
const attrName = ctx.args[2] as string;
|
|
825
|
-
if (!refArg || !attrName)
|
|
1065
|
+
if (!refArg || !attrName)
|
|
1066
|
+
throw new Error('Usage: monomind browse get attr @ref <attrName>');
|
|
826
1067
|
const refKey = refArg.startsWith('@') ? refArg.slice(1) : refArg;
|
|
827
1068
|
const ref = _refs.get(refKey);
|
|
828
1069
|
if (!ref) throw new Error(`Ref @${refKey} not found`);
|
|
829
1070
|
const objectId = await browser.getObjectIdForRef(client, sessionId, ref);
|
|
830
1071
|
if (!objectId) throw new Error('Element not in DOM');
|
|
831
|
-
const r = await client.send<{ result: { value?: string } }>(
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
1072
|
+
const r = await client.send<{ result: { value?: string } }>(
|
|
1073
|
+
'Runtime.callFunctionOn',
|
|
1074
|
+
{
|
|
1075
|
+
functionDeclaration: `function() { return this.getAttribute(${JSON.stringify(attrName)}); }`,
|
|
1076
|
+
objectId,
|
|
1077
|
+
returnByValue: true,
|
|
1078
|
+
},
|
|
1079
|
+
sessionId,
|
|
1080
|
+
);
|
|
835
1081
|
value = r.result?.value ?? null;
|
|
836
1082
|
break;
|
|
837
1083
|
}
|
|
838
1084
|
case 'count': {
|
|
839
1085
|
const selector = ctx.args[1] as string;
|
|
840
1086
|
if (!selector) throw new Error('Usage: monomind browse get count <cssSelector>');
|
|
841
|
-
value = await browser.evaluateJs(
|
|
1087
|
+
value = await browser.evaluateJs(
|
|
1088
|
+
client,
|
|
1089
|
+
sessionId,
|
|
1090
|
+
`document.querySelectorAll(${JSON.stringify(selector)}).length`,
|
|
1091
|
+
);
|
|
842
1092
|
break;
|
|
843
1093
|
}
|
|
844
1094
|
case 'box': {
|
|
@@ -859,11 +1109,21 @@ const getCommand: Command = {
|
|
|
859
1109
|
if (!ref) throw new Error(`Ref @${refKey} not found`);
|
|
860
1110
|
const objectId = await browser.getObjectIdForRef(client, sessionId, ref);
|
|
861
1111
|
if (!objectId) throw new Error('Element not in DOM');
|
|
862
|
-
const r = await client.send<{ result: { value?: string } }>(
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
1112
|
+
const r = await client.send<{ result: { value?: string } }>(
|
|
1113
|
+
'Runtime.callFunctionOn',
|
|
1114
|
+
{
|
|
1115
|
+
functionDeclaration:
|
|
1116
|
+
'function() { const s = window.getComputedStyle(this); return JSON.stringify(Object.fromEntries([...s].map(k => [k, s.getPropertyValue(k)]))); }',
|
|
1117
|
+
objectId,
|
|
1118
|
+
returnByValue: true,
|
|
1119
|
+
},
|
|
1120
|
+
sessionId,
|
|
1121
|
+
);
|
|
1122
|
+
try {
|
|
1123
|
+
value = JSON.parse(r.result?.value ?? '{}');
|
|
1124
|
+
} catch {
|
|
1125
|
+
value = {};
|
|
1126
|
+
}
|
|
867
1127
|
break;
|
|
868
1128
|
}
|
|
869
1129
|
default:
|
|
@@ -882,11 +1142,17 @@ const getCommand: Command = {
|
|
|
882
1142
|
|
|
883
1143
|
const scrollCommand: Command = {
|
|
884
1144
|
name: 'scroll',
|
|
885
|
-
description:
|
|
1145
|
+
description:
|
|
1146
|
+
'Scroll the page. Usage: monomind browse scroll up|down|left|right [amount] [--selector ".sidebar"]',
|
|
886
1147
|
options: [
|
|
887
1148
|
{ name: 'amount', short: 'a', type: 'number', description: 'Pixels to scroll', default: 300 },
|
|
888
1149
|
{ name: 'ref', type: 'string', description: 'Element ref to scroll within' },
|
|
889
|
-
{
|
|
1150
|
+
{
|
|
1151
|
+
name: 'selector',
|
|
1152
|
+
short: 's',
|
|
1153
|
+
type: 'string',
|
|
1154
|
+
description: 'CSS selector of element to scroll within',
|
|
1155
|
+
},
|
|
890
1156
|
],
|
|
891
1157
|
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
892
1158
|
const { client, sessionId } = await ensureConnected(_port);
|
|
@@ -896,18 +1162,29 @@ const scrollCommand: Command = {
|
|
|
896
1162
|
if (!direction) throw new Error('Usage: monomind browse scroll up|down|left|right [amount]');
|
|
897
1163
|
|
|
898
1164
|
// Support positional amount: scroll down 300
|
|
899
|
-
const positionalAmount =
|
|
900
|
-
|
|
1165
|
+
const positionalAmount =
|
|
1166
|
+
ctx.args[1] !== undefined ? parseInt(ctx.args[1] as string, 10) : undefined;
|
|
1167
|
+
const amount =
|
|
1168
|
+
positionalAmount && Number.isFinite(positionalAmount)
|
|
1169
|
+
? positionalAmount
|
|
1170
|
+
: ((ctx.flags.amount as number) ?? 300);
|
|
901
1171
|
|
|
902
1172
|
if (ctx.flags.selector) {
|
|
903
1173
|
const sel = ctx.flags.selector as string;
|
|
904
1174
|
const dx = direction === 'right' ? amount : direction === 'left' ? -amount : 0;
|
|
905
1175
|
const dy = direction === 'down' ? amount : direction === 'up' ? -amount : 0;
|
|
906
|
-
const posJson = await browser.evaluateJs(
|
|
907
|
-
|
|
1176
|
+
const posJson = (await browser.evaluateJs(
|
|
1177
|
+
client,
|
|
1178
|
+
sessionId,
|
|
1179
|
+
`(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});})()`,
|
|
1180
|
+
)) as string | null;
|
|
908
1181
|
if (!posJson) throw new Error(`Selector not found: ${sel}`);
|
|
909
1182
|
const pos = JSON.parse(posJson) as { x: number; y: number };
|
|
910
|
-
await client.send(
|
|
1183
|
+
await client.send(
|
|
1184
|
+
'Input.dispatchMouseEvent',
|
|
1185
|
+
{ type: 'mouseWheel', x: pos.x, y: pos.y, deltaX: dx, deltaY: dy },
|
|
1186
|
+
sessionId,
|
|
1187
|
+
);
|
|
911
1188
|
output.printSuccess(`Scrolled ${direction} in ${sel}`);
|
|
912
1189
|
return { success: true };
|
|
913
1190
|
}
|
|
@@ -916,7 +1193,7 @@ const scrollCommand: Command = {
|
|
|
916
1193
|
if (ctx.flags.ref) {
|
|
917
1194
|
const refKey = (ctx.flags.ref as string).startsWith('@')
|
|
918
1195
|
? (ctx.flags.ref as string).slice(1)
|
|
919
|
-
: ctx.flags.ref as string;
|
|
1196
|
+
: (ctx.flags.ref as string);
|
|
920
1197
|
ref = _refs.get(refKey);
|
|
921
1198
|
}
|
|
922
1199
|
|
|
@@ -942,15 +1219,26 @@ const navigateCommand: Command = {
|
|
|
942
1219
|
let offFrameStarted: () => void = () => {};
|
|
943
1220
|
const frameStartedPromise = new Promise<void>((resolve) => {
|
|
944
1221
|
offFrameStarted = client.on('Page.frameStartedLoading', (_params, sid) => {
|
|
945
|
-
if (sid === sessionId) {
|
|
1222
|
+
if (sid === sessionId) {
|
|
1223
|
+
const off = offFrameStarted;
|
|
1224
|
+
offFrameStarted = () => {};
|
|
1225
|
+
off();
|
|
1226
|
+
resolve();
|
|
1227
|
+
}
|
|
946
1228
|
});
|
|
947
1229
|
});
|
|
948
1230
|
try {
|
|
949
|
-
await client.send(
|
|
950
|
-
|
|
951
|
-
|
|
1231
|
+
await client.send(
|
|
1232
|
+
'Runtime.evaluate',
|
|
1233
|
+
{
|
|
1234
|
+
expression: direction === 'back' ? 'history.back()' : 'history.forward()',
|
|
1235
|
+
},
|
|
1236
|
+
sessionId,
|
|
1237
|
+
);
|
|
952
1238
|
let fallbackHandle: ReturnType<typeof setTimeout> | undefined;
|
|
953
|
-
const fallbackPromise = new Promise<void>((r) => {
|
|
1239
|
+
const fallbackPromise = new Promise<void>((r) => {
|
|
1240
|
+
fallbackHandle = setTimeout(r, 2000);
|
|
1241
|
+
});
|
|
954
1242
|
await Promise.race([frameStartedPromise, fallbackPromise]);
|
|
955
1243
|
if (fallbackHandle !== undefined) clearTimeout(fallbackHandle);
|
|
956
1244
|
} finally {
|
|
@@ -975,29 +1263,44 @@ const navigateCommand: Command = {
|
|
|
975
1263
|
|
|
976
1264
|
const setCommand: Command = {
|
|
977
1265
|
name: 'set',
|
|
978
|
-
description:
|
|
1266
|
+
description:
|
|
1267
|
+
'Configure browser settings. Usage: monomind browse set viewport|device|geo|offline|media|credentials|useragent <args>',
|
|
979
1268
|
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
980
1269
|
const { client, sessionId } = await ensureConnected(_port);
|
|
981
1270
|
const browser = await getBrowser();
|
|
982
1271
|
|
|
983
1272
|
const setting = ctx.args[0] as string;
|
|
984
|
-
if (!setting)
|
|
1273
|
+
if (!setting)
|
|
1274
|
+
throw new Error(
|
|
1275
|
+
'Usage: monomind browse set viewport|device|geo|offline|media|credentials|useragent <args>',
|
|
1276
|
+
);
|
|
985
1277
|
|
|
986
1278
|
switch (setting) {
|
|
987
1279
|
case 'viewport': {
|
|
988
1280
|
const width = parseInt(ctx.args[1] as string, 10);
|
|
989
1281
|
const height = parseInt(ctx.args[2] as string, 10);
|
|
990
1282
|
const dpr = parseFloat(ctx.args[3] as string) || undefined;
|
|
991
|
-
if (isNaN(width) || isNaN(height))
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
1283
|
+
if (Number.isNaN(width) || Number.isNaN(height))
|
|
1284
|
+
throw new Error('Usage: set viewport <width> <height> [dpr]');
|
|
1285
|
+
await client.send(
|
|
1286
|
+
'Emulation.setDeviceMetricsOverride',
|
|
1287
|
+
{
|
|
1288
|
+
width,
|
|
1289
|
+
height,
|
|
1290
|
+
deviceScaleFactor: dpr ?? 1,
|
|
1291
|
+
mobile: false,
|
|
1292
|
+
},
|
|
1293
|
+
sessionId,
|
|
1294
|
+
);
|
|
995
1295
|
output.printSuccess(`Viewport set to ${width}x${height}${dpr ? ` @${dpr}x` : ''}`);
|
|
996
1296
|
break;
|
|
997
1297
|
}
|
|
998
1298
|
case 'device': {
|
|
999
1299
|
const deviceName = ctx.args[1] as string;
|
|
1000
|
-
if (!deviceName)
|
|
1300
|
+
if (!deviceName)
|
|
1301
|
+
throw new Error(
|
|
1302
|
+
`Usage: set device <name>. Available: ${browser.listDevices().join(', ')}`,
|
|
1303
|
+
);
|
|
1001
1304
|
await browser.emulateDevice(client, sessionId, deviceName);
|
|
1002
1305
|
output.printSuccess(`Emulating device: ${deviceName}`);
|
|
1003
1306
|
break;
|
|
@@ -1006,7 +1309,8 @@ const setCommand: Command = {
|
|
|
1006
1309
|
const lat = parseFloat(ctx.args[1] as string);
|
|
1007
1310
|
const lon = parseFloat(ctx.args[2] as string);
|
|
1008
1311
|
const acc = parseFloat(ctx.args[3] as string) || 100;
|
|
1009
|
-
if (isNaN(lat) || isNaN(lon))
|
|
1312
|
+
if (Number.isNaN(lat) || Number.isNaN(lon))
|
|
1313
|
+
throw new Error('Usage: set geo <latitude> <longitude> [accuracy]');
|
|
1010
1314
|
await browser.setGeolocation(client, sessionId, lat, lon, acc);
|
|
1011
1315
|
output.printSuccess(`Geolocation set: ${lat}, ${lon}`);
|
|
1012
1316
|
break;
|
|
@@ -1042,7 +1346,9 @@ const setCommand: Command = {
|
|
|
1042
1346
|
break;
|
|
1043
1347
|
}
|
|
1044
1348
|
default:
|
|
1045
|
-
throw new Error(
|
|
1349
|
+
throw new Error(
|
|
1350
|
+
`Unknown setting: ${setting}. Use: viewport|device|geo|offline|media|credentials|useragent`,
|
|
1351
|
+
);
|
|
1046
1352
|
}
|
|
1047
1353
|
|
|
1048
1354
|
return { success: true };
|
|
@@ -1051,9 +1357,14 @@ const setCommand: Command = {
|
|
|
1051
1357
|
|
|
1052
1358
|
const stateCommand: Command = {
|
|
1053
1359
|
name: 'state',
|
|
1054
|
-
description:
|
|
1360
|
+
description:
|
|
1361
|
+
'Manage browser session state. Usage: monomind browse state save|load|list|rename|clean [name]',
|
|
1055
1362
|
options: [
|
|
1056
|
-
{
|
|
1363
|
+
{
|
|
1364
|
+
name: 'older-than',
|
|
1365
|
+
type: 'number',
|
|
1366
|
+
description: 'For state clean: remove sessions older than N days',
|
|
1367
|
+
},
|
|
1057
1368
|
],
|
|
1058
1369
|
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
1059
1370
|
const browser = await getBrowser();
|
|
@@ -1104,7 +1415,13 @@ const stateCommand: Command = {
|
|
|
1104
1415
|
const title = await browser.getCurrentTitle(c, sid);
|
|
1105
1416
|
const cookies = await browser.getCookies(c, sid);
|
|
1106
1417
|
const ls = await browser.getAllLocalStorage(c, sid);
|
|
1107
|
-
const info = {
|
|
1418
|
+
const info = {
|
|
1419
|
+
url,
|
|
1420
|
+
title,
|
|
1421
|
+
cookies: cookies.length,
|
|
1422
|
+
localStorage: Object.keys(ls).length,
|
|
1423
|
+
refs: _refs.size,
|
|
1424
|
+
};
|
|
1108
1425
|
print(JSON.stringify(info, null, 2));
|
|
1109
1426
|
return { success: true, data: info };
|
|
1110
1427
|
}
|
|
@@ -1121,18 +1438,26 @@ const stateCommand: Command = {
|
|
|
1121
1438
|
case 'rename': {
|
|
1122
1439
|
const oldName = ctx.args[1] as string;
|
|
1123
1440
|
const newName = ctx.args[2] as string;
|
|
1124
|
-
if (!oldName || !newName)
|
|
1441
|
+
if (!oldName || !newName)
|
|
1442
|
+
throw new Error('Usage: monomind browse state rename <old-name> <new-name>');
|
|
1125
1443
|
const sessions = await browser.listSessions();
|
|
1126
1444
|
if (!sessions.includes(oldName)) throw new Error(`Session not found: ${oldName}`);
|
|
1127
1445
|
// W1: validate newName to prevent path traversal
|
|
1128
|
-
const { basename: basenameFn } = await import('path');
|
|
1446
|
+
const { basename: basenameFn } = await import('node:path');
|
|
1129
1447
|
const safeName = basenameFn(newName);
|
|
1130
1448
|
if (safeName !== newName || safeName.startsWith('.') || safeName.includes('/')) {
|
|
1131
|
-
throw new Error(
|
|
1449
|
+
throw new Error(
|
|
1450
|
+
'Invalid session name — must not contain path separators or start with "."',
|
|
1451
|
+
);
|
|
1132
1452
|
}
|
|
1133
|
-
const {
|
|
1134
|
-
|
|
1135
|
-
|
|
1453
|
+
const {
|
|
1454
|
+
unlink: unlinkRename,
|
|
1455
|
+
readFile,
|
|
1456
|
+
writeFile,
|
|
1457
|
+
mkdir: mkdirRename,
|
|
1458
|
+
} = await import('node:fs/promises');
|
|
1459
|
+
const { join: joinR } = await import('node:path');
|
|
1460
|
+
const { homedir } = await import('node:os');
|
|
1136
1461
|
const sessionDir = joinR(homedir(), '.monomind', 'browser-sessions');
|
|
1137
1462
|
const oldPath = joinR(sessionDir, `${oldName}.json`);
|
|
1138
1463
|
const newPath = joinR(sessionDir, `${newName}.json`);
|
|
@@ -1146,9 +1471,9 @@ const stateCommand: Command = {
|
|
|
1146
1471
|
}
|
|
1147
1472
|
case 'clean': {
|
|
1148
1473
|
const days = (ctx.flags['older-than'] as number) ?? 7;
|
|
1149
|
-
const { unlink, stat } = await import('fs/promises');
|
|
1150
|
-
const { join: joinC } = await import('path');
|
|
1151
|
-
const { homedir: homedirC } = await import('os');
|
|
1474
|
+
const { unlink, stat } = await import('node:fs/promises');
|
|
1475
|
+
const { join: joinC } = await import('node:path');
|
|
1476
|
+
const { homedir: homedirC } = await import('node:os');
|
|
1152
1477
|
const sessionDir = joinC(homedirC(), '.monomind', 'browser-sessions');
|
|
1153
1478
|
const sessions = await browser.listSessions();
|
|
1154
1479
|
const cutoff = Date.now() - days * 86400 * 1000;
|
|
@@ -1180,30 +1505,52 @@ const networkCommand: Command = {
|
|
|
1180
1505
|
{ name: 'status', type: 'number', description: 'HTTP status for fulfill', default: 200 },
|
|
1181
1506
|
{ name: 'headers', type: 'string', description: 'JSON headers object' },
|
|
1182
1507
|
{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
|
|
1183
|
-
{
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1508
|
+
{
|
|
1509
|
+
name: 'filter',
|
|
1510
|
+
type: 'string',
|
|
1511
|
+
description: 'Filter requests by URL substring (for network requests)',
|
|
1512
|
+
},
|
|
1513
|
+
{
|
|
1514
|
+
name: 'method',
|
|
1515
|
+
type: 'string',
|
|
1516
|
+
description: 'Filter by HTTP method, e.g. GET, POST (for network requests)',
|
|
1517
|
+
},
|
|
1518
|
+
{
|
|
1519
|
+
name: 'status-code',
|
|
1520
|
+
type: 'number',
|
|
1521
|
+
description: 'Filter by HTTP status code (for network requests)',
|
|
1522
|
+
},
|
|
1523
|
+
{
|
|
1524
|
+
name: 'type',
|
|
1525
|
+
type: 'string',
|
|
1526
|
+
description:
|
|
1527
|
+
'Filter by resource type: xhr|fetch|document|script|stylesheet|image (for network requests)',
|
|
1528
|
+
},
|
|
1187
1529
|
],
|
|
1188
1530
|
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
1189
1531
|
const { client, sessionId } = await ensureConnected(_port);
|
|
1190
1532
|
const browser = await getBrowser();
|
|
1191
1533
|
const action = ctx.args[0] as string;
|
|
1192
|
-
if (!action)
|
|
1534
|
+
if (!action)
|
|
1535
|
+
throw new Error('Usage: monomind browse network route|unroute|cookies|headers|requests');
|
|
1193
1536
|
|
|
1194
1537
|
switch (action) {
|
|
1195
1538
|
case 'route': {
|
|
1196
1539
|
const pattern = ctx.flags.pattern as string;
|
|
1197
1540
|
if (!pattern) throw new Error('--pattern required for network route');
|
|
1198
|
-
const routes: NetworkRoute[] = [
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1541
|
+
const routes: NetworkRoute[] = [
|
|
1542
|
+
{
|
|
1543
|
+
pattern,
|
|
1544
|
+
action: ctx.flags.abort ? 'abort' : ctx.flags.fulfill ? 'fulfill' : 'continue',
|
|
1545
|
+
response: ctx.flags.fulfill
|
|
1546
|
+
? {
|
|
1547
|
+
status: ctx.flags.status as number,
|
|
1548
|
+
body: ctx.flags.fulfill as string,
|
|
1549
|
+
headers: ctx.flags.headers ? JSON.parse(ctx.flags.headers as string) : {},
|
|
1550
|
+
}
|
|
1551
|
+
: undefined,
|
|
1552
|
+
},
|
|
1553
|
+
];
|
|
1207
1554
|
await browser.setupRoutes(client, sessionId, routes);
|
|
1208
1555
|
output.printSuccess(`Network route set: ${pattern}`);
|
|
1209
1556
|
break;
|
|
@@ -1225,7 +1572,7 @@ const networkCommand: Command = {
|
|
|
1225
1572
|
break;
|
|
1226
1573
|
}
|
|
1227
1574
|
case 'capture': {
|
|
1228
|
-
const subAction = ctx.args[1] as string ?? 'start';
|
|
1575
|
+
const subAction = (ctx.args[1] as string) ?? 'start';
|
|
1229
1576
|
if (subAction === 'start') {
|
|
1230
1577
|
browser.startRequestCapture(client, sessionId);
|
|
1231
1578
|
output.printSuccess('Request capture started');
|
|
@@ -1245,13 +1592,22 @@ const networkCommand: Command = {
|
|
|
1245
1592
|
const filterStatus = ctx.flags['status-code'] as number | undefined;
|
|
1246
1593
|
const filterType = ctx.flags.type as string | undefined;
|
|
1247
1594
|
if (filterUrl) reqs = reqs.filter((r) => r.url.includes(filterUrl));
|
|
1248
|
-
if (filterMethod)
|
|
1595
|
+
if (filterMethod)
|
|
1596
|
+
reqs = reqs.filter(
|
|
1597
|
+
(r) => (r.method ?? 'GET').toUpperCase() === filterMethod.toUpperCase(),
|
|
1598
|
+
);
|
|
1249
1599
|
if (filterStatus) reqs = reqs.filter((r) => r.status === filterStatus);
|
|
1250
|
-
if (filterType)
|
|
1600
|
+
if (filterType)
|
|
1601
|
+
reqs = reqs.filter(
|
|
1602
|
+
(r) =>
|
|
1603
|
+
(r as Record<string, unknown>).resourceType === filterType ||
|
|
1604
|
+
(r as Record<string, unknown>).type === filterType,
|
|
1605
|
+
);
|
|
1251
1606
|
if (ctx.flags.json) print(JSON.stringify({ data: reqs }));
|
|
1252
1607
|
else {
|
|
1253
|
-
if (reqs.length === 0) {
|
|
1254
|
-
|
|
1608
|
+
if (reqs.length === 0) {
|
|
1609
|
+
output.printInfo('No captured requests. Run: network capture start');
|
|
1610
|
+
} else for (const r of reqs) print(` ${r.method ?? 'GET'} ${r.status ?? '-'} ${r.url}`);
|
|
1255
1611
|
}
|
|
1256
1612
|
return { success: true, data: { requests: reqs } };
|
|
1257
1613
|
}
|
|
@@ -1259,14 +1615,23 @@ const networkCommand: Command = {
|
|
|
1259
1615
|
const reqId = ctx.args[1] as string;
|
|
1260
1616
|
if (!reqId) throw new Error('Usage: monomind browse network request <requestId>');
|
|
1261
1617
|
const reqs = browser.getCapturedRequests(sessionId);
|
|
1262
|
-
const req = reqs.find(
|
|
1263
|
-
|
|
1618
|
+
const req = reqs.find(
|
|
1619
|
+
(r) =>
|
|
1620
|
+
(r as Record<string, unknown>).requestId === reqId ||
|
|
1621
|
+
(r as Record<string, unknown>).id === reqId,
|
|
1622
|
+
);
|
|
1623
|
+
if (!req) {
|
|
1624
|
+
output.printWarning(`Request not found: ${reqId}`);
|
|
1625
|
+
return { success: false };
|
|
1626
|
+
}
|
|
1264
1627
|
if (ctx.flags.json) print(JSON.stringify({ data: req }));
|
|
1265
1628
|
else print(JSON.stringify(req, null, 2));
|
|
1266
1629
|
return { success: true, data: { request: req } };
|
|
1267
1630
|
}
|
|
1268
1631
|
default:
|
|
1269
|
-
throw new Error(
|
|
1632
|
+
throw new Error(
|
|
1633
|
+
`Unknown: ${action}. Use: route|unroute|cookies|headers|capture|requests|request`,
|
|
1634
|
+
);
|
|
1270
1635
|
}
|
|
1271
1636
|
|
|
1272
1637
|
return { success: true };
|
|
@@ -1282,7 +1647,7 @@ const DEFAULT_EVAL_MAX_OUTPUT = 50_000;
|
|
|
1282
1647
|
|
|
1283
1648
|
function truncateForOutput(text: string, maxOutput: number): string {
|
|
1284
1649
|
if (!(maxOutput > 0) || text.length <= maxOutput) return text;
|
|
1285
|
-
return text.slice(0, maxOutput)
|
|
1650
|
+
return `${text.slice(0, maxOutput)}\n[... truncated at ${maxOutput} chars]`;
|
|
1286
1651
|
}
|
|
1287
1652
|
|
|
1288
1653
|
const evalCommand: Command = {
|
|
@@ -1290,9 +1655,22 @@ const evalCommand: Command = {
|
|
|
1290
1655
|
description: 'Evaluate JavaScript in page context. Usage: monomind browse eval "document.title"',
|
|
1291
1656
|
options: [
|
|
1292
1657
|
{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
|
|
1293
|
-
{
|
|
1294
|
-
|
|
1295
|
-
|
|
1658
|
+
{
|
|
1659
|
+
name: 'stdin',
|
|
1660
|
+
type: 'boolean',
|
|
1661
|
+
description: 'Read JS expression from stdin (heredoc-friendly for multiline scripts)',
|
|
1662
|
+
default: false,
|
|
1663
|
+
},
|
|
1664
|
+
{
|
|
1665
|
+
name: 'max-output',
|
|
1666
|
+
type: 'number',
|
|
1667
|
+
description: `Truncate printed output to N characters (default ${DEFAULT_EVAL_MAX_OUTPUT}; 0 disables truncation)`,
|
|
1668
|
+
},
|
|
1669
|
+
{
|
|
1670
|
+
name: 'timeout',
|
|
1671
|
+
type: 'number',
|
|
1672
|
+
description: 'Max ms to wait for evaluation to settle (default 30000)',
|
|
1673
|
+
},
|
|
1296
1674
|
],
|
|
1297
1675
|
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
1298
1676
|
const { client, sessionId } = await ensureConnected(_port);
|
|
@@ -1330,13 +1708,25 @@ const closeCommand: Command = {
|
|
|
1330
1708
|
const client = _client;
|
|
1331
1709
|
// Tear down per-session Maps and listeners before closing
|
|
1332
1710
|
if (browser.getHarStatus(sid).recording) {
|
|
1333
|
-
try {
|
|
1711
|
+
try {
|
|
1712
|
+
await browser.stopHarRecording(client, sid);
|
|
1713
|
+
} catch {
|
|
1714
|
+
/* ignore */
|
|
1715
|
+
}
|
|
1334
1716
|
}
|
|
1335
1717
|
if (browser.getTraceStatus(sid)) {
|
|
1336
|
-
try {
|
|
1718
|
+
try {
|
|
1719
|
+
await browser.stopTrace(client, sid);
|
|
1720
|
+
} catch {
|
|
1721
|
+
/* ignore */
|
|
1722
|
+
}
|
|
1337
1723
|
}
|
|
1338
1724
|
if (browser.isProfilingActive(sid)) {
|
|
1339
|
-
try {
|
|
1725
|
+
try {
|
|
1726
|
+
await browser.stopCpuProfile(client, sid);
|
|
1727
|
+
} catch {
|
|
1728
|
+
/* ignore */
|
|
1729
|
+
}
|
|
1340
1730
|
}
|
|
1341
1731
|
browser.teardownRouteInterception(sid);
|
|
1342
1732
|
browser.stopRequestCapture(sid);
|
|
@@ -1370,7 +1760,11 @@ const closeCommand: Command = {
|
|
|
1370
1760
|
} finally {
|
|
1371
1761
|
// Always drop our websocket — a hung Browser.close must not keep
|
|
1372
1762
|
// this CLI process's event loop alive.
|
|
1373
|
-
try {
|
|
1763
|
+
try {
|
|
1764
|
+
conn.client.close();
|
|
1765
|
+
} catch {
|
|
1766
|
+
/* already gone */
|
|
1767
|
+
}
|
|
1374
1768
|
}
|
|
1375
1769
|
// closeBrowser has no PID fallback in a fresh process (launchedPids
|
|
1376
1770
|
// is per-process) — re-probe so we report what actually happened.
|
|
@@ -1380,19 +1774,28 @@ const closeCommand: Command = {
|
|
|
1380
1774
|
const probeDeadline = Date.now() + 3000;
|
|
1381
1775
|
while (stillUp && Date.now() < probeDeadline) {
|
|
1382
1776
|
try {
|
|
1383
|
-
await fetch(`http://127.0.0.1:${persisted.port}/json/version`, {
|
|
1384
|
-
|
|
1385
|
-
|
|
1777
|
+
await fetch(`http://127.0.0.1:${persisted.port}/json/version`, {
|
|
1778
|
+
signal: AbortSignal.timeout(800),
|
|
1779
|
+
});
|
|
1780
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
1781
|
+
} catch {
|
|
1782
|
+
stillUp = false;
|
|
1783
|
+
}
|
|
1386
1784
|
}
|
|
1387
|
-
if (stillUp)
|
|
1785
|
+
if (stillUp)
|
|
1786
|
+
output.printWarning(
|
|
1787
|
+
`Browser on port ${persisted.port} did not exit — kill it manually if needed`,
|
|
1788
|
+
);
|
|
1388
1789
|
else output.printSuccess(`Closed browser on port ${persisted.port}`);
|
|
1389
1790
|
} catch {
|
|
1390
1791
|
output.printInfo(`No browser answering on port ${persisted.port} — nothing to close`);
|
|
1391
1792
|
}
|
|
1392
1793
|
} else {
|
|
1393
|
-
output.printInfo(
|
|
1394
|
-
|
|
1395
|
-
|
|
1794
|
+
output.printInfo(
|
|
1795
|
+
persisted
|
|
1796
|
+
? `Detached from browser on port ${persisted.port} (attached via connect — left running)`
|
|
1797
|
+
: 'No active browser session',
|
|
1798
|
+
);
|
|
1396
1799
|
}
|
|
1397
1800
|
await browser.clearActivePort();
|
|
1398
1801
|
await browser.clearRefCache();
|
|
@@ -1439,7 +1842,8 @@ const focusCommand: Command = {
|
|
|
1439
1842
|
|
|
1440
1843
|
const typeCommand: Command = {
|
|
1441
1844
|
name: 'type',
|
|
1442
|
-
description:
|
|
1845
|
+
description:
|
|
1846
|
+
'Type text into element (appends, does not clear). Usage: monomind browse type @e1 "text"',
|
|
1443
1847
|
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
1444
1848
|
const { client, sessionId } = await ensureConnected(_port);
|
|
1445
1849
|
const browser = await getBrowser();
|
|
@@ -1566,7 +1970,7 @@ async function resolveElementObjectId(
|
|
|
1566
1970
|
client: import('../index.js').CdpClient,
|
|
1567
1971
|
sessionId: string,
|
|
1568
1972
|
refs: Map<string, import('../index.js').ElementRef>,
|
|
1569
|
-
refOrSelector: string
|
|
1973
|
+
refOrSelector: string,
|
|
1570
1974
|
): Promise<string> {
|
|
1571
1975
|
const browser = await getBrowser();
|
|
1572
1976
|
if (refOrSelector.startsWith('@') || /^e\d+$/.test(refOrSelector)) {
|
|
@@ -1577,11 +1981,16 @@ async function resolveElementObjectId(
|
|
|
1577
1981
|
return objectId;
|
|
1578
1982
|
}
|
|
1579
1983
|
// CSS selector path
|
|
1580
|
-
const res = await client.send<{ result: { objectId?: string; subtype?: string } }>(
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1984
|
+
const res = await client.send<{ result: { objectId?: string; subtype?: string } }>(
|
|
1985
|
+
'Runtime.evaluate',
|
|
1986
|
+
{
|
|
1987
|
+
expression: `document.querySelector(${JSON.stringify(refOrSelector)})`,
|
|
1988
|
+
returnByValue: false,
|
|
1989
|
+
},
|
|
1990
|
+
sessionId,
|
|
1991
|
+
);
|
|
1992
|
+
if (!res.result?.objectId || res.result?.subtype === 'null')
|
|
1993
|
+
throw new Error(`Selector not found: ${refOrSelector}`);
|
|
1585
1994
|
return res.result.objectId;
|
|
1586
1995
|
}
|
|
1587
1996
|
|
|
@@ -1594,63 +2003,87 @@ const isvisibleCommand: Command = {
|
|
|
1594
2003
|
const arg = ctx.args[0] as string;
|
|
1595
2004
|
if (!arg) throw new Error('Usage: monomind browse isvisible @e1|".selector"');
|
|
1596
2005
|
const objectId = await resolveElementObjectId(client, sessionId, _refs, arg);
|
|
1597
|
-
const r = await client.send<{ result: { value?: boolean } }>(
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
2006
|
+
const r = await client.send<{ result: { value?: boolean } }>(
|
|
2007
|
+
'Runtime.callFunctionOn',
|
|
2008
|
+
{
|
|
2009
|
+
functionDeclaration: `function(){var r=this.getBoundingClientRect(),s=window.getComputedStyle(this);return r.width>0&&r.height>0&&s.visibility!=='hidden'&&s.display!=='none'&&parseFloat(s.opacity)>0;}`,
|
|
2010
|
+
objectId,
|
|
2011
|
+
returnByValue: true,
|
|
2012
|
+
},
|
|
2013
|
+
sessionId,
|
|
2014
|
+
);
|
|
1602
2015
|
const visible = r.result?.value ?? false;
|
|
1603
|
-
if (ctx.flags.json) {
|
|
1604
|
-
|
|
2016
|
+
if (ctx.flags.json) {
|
|
2017
|
+
print(JSON.stringify({ visible }));
|
|
2018
|
+
} else {
|
|
2019
|
+
output.printSuccess(`isvisible: ${visible}`);
|
|
2020
|
+
}
|
|
1605
2021
|
return { success: true, data: { visible } };
|
|
1606
2022
|
},
|
|
1607
2023
|
};
|
|
1608
2024
|
|
|
1609
2025
|
const isenabledCommand: Command = {
|
|
1610
2026
|
name: 'isenabled',
|
|
1611
|
-
description:
|
|
2027
|
+
description:
|
|
2028
|
+
'Check if element is enabled (not disabled). Usage: monomind browse isenabled @e1|"selector"',
|
|
1612
2029
|
options: [{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false }],
|
|
1613
2030
|
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
1614
2031
|
const { client, sessionId } = await ensureConnected(_port);
|
|
1615
2032
|
const arg = ctx.args[0] as string;
|
|
1616
2033
|
if (!arg) throw new Error('Usage: monomind browse isenabled @e1|".selector"');
|
|
1617
2034
|
const objectId = await resolveElementObjectId(client, sessionId, _refs, arg);
|
|
1618
|
-
const r = await client.send<{ result: { value?: boolean } }>(
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
2035
|
+
const r = await client.send<{ result: { value?: boolean } }>(
|
|
2036
|
+
'Runtime.callFunctionOn',
|
|
2037
|
+
{
|
|
2038
|
+
functionDeclaration: `function(){return !this.disabled;}`,
|
|
2039
|
+
objectId,
|
|
2040
|
+
returnByValue: true,
|
|
2041
|
+
},
|
|
2042
|
+
sessionId,
|
|
2043
|
+
);
|
|
1623
2044
|
const enabled = r.result?.value ?? true;
|
|
1624
|
-
if (ctx.flags.json) {
|
|
1625
|
-
|
|
2045
|
+
if (ctx.flags.json) {
|
|
2046
|
+
print(JSON.stringify({ enabled }));
|
|
2047
|
+
} else {
|
|
2048
|
+
output.printSuccess(`isenabled: ${enabled}`);
|
|
2049
|
+
}
|
|
1626
2050
|
return { success: true, data: { enabled } };
|
|
1627
2051
|
},
|
|
1628
2052
|
};
|
|
1629
2053
|
|
|
1630
2054
|
const ischeckedCommand: Command = {
|
|
1631
2055
|
name: 'ischecked',
|
|
1632
|
-
description:
|
|
2056
|
+
description:
|
|
2057
|
+
'Check if checkbox/radio is checked. Usage: monomind browse ischecked @e1|"selector"',
|
|
1633
2058
|
options: [{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false }],
|
|
1634
2059
|
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
1635
2060
|
const { client, sessionId } = await ensureConnected(_port);
|
|
1636
2061
|
const arg = ctx.args[0] as string;
|
|
1637
2062
|
if (!arg) throw new Error('Usage: monomind browse ischecked @e1|".selector"');
|
|
1638
2063
|
const objectId = await resolveElementObjectId(client, sessionId, _refs, arg);
|
|
1639
|
-
const r = await client.send<{ result: { value?: boolean } }>(
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
2064
|
+
const r = await client.send<{ result: { value?: boolean } }>(
|
|
2065
|
+
'Runtime.callFunctionOn',
|
|
2066
|
+
{
|
|
2067
|
+
functionDeclaration: `function(){var el=this,tag=el.tagName&&el.tagName.toUpperCase();if(tag==='INPUT'&&(el.type==='checkbox'||el.type==='radio'))return el.checked;var role=el.getAttribute&&el.getAttribute('role');if(role&&['checkbox','radio','switch','menuitemcheckbox','menuitemradio','option','treeitem'].indexOf(role)!==-1)return el.getAttribute('aria-checked')==='true';var label=tag!=='LABEL'?el.closest&&el.closest('label'):el;if(label&&label.control&&(label.control.type==='checkbox'||label.control.type==='radio'))return label.control.checked;var inp=el.querySelector&&el.querySelector('input[type="checkbox"],input[type="radio"]');return inp?inp.checked:false;}`,
|
|
2068
|
+
objectId,
|
|
2069
|
+
returnByValue: true,
|
|
2070
|
+
},
|
|
2071
|
+
sessionId,
|
|
2072
|
+
);
|
|
1644
2073
|
const checked = r.result?.value ?? false;
|
|
1645
|
-
if (ctx.flags.json) {
|
|
1646
|
-
|
|
2074
|
+
if (ctx.flags.json) {
|
|
2075
|
+
print(JSON.stringify({ checked }));
|
|
2076
|
+
} else {
|
|
2077
|
+
output.printSuccess(`ischecked: ${checked}`);
|
|
2078
|
+
}
|
|
1647
2079
|
return { success: true, data: { checked } };
|
|
1648
2080
|
},
|
|
1649
2081
|
};
|
|
1650
2082
|
|
|
1651
2083
|
const tapCommand: Command = {
|
|
1652
2084
|
name: 'tap',
|
|
1653
|
-
description:
|
|
2085
|
+
description:
|
|
2086
|
+
'Tap element with a touch event (mobile testing). Usage: monomind browse tap @e1|"selector"',
|
|
1654
2087
|
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
1655
2088
|
const { client, sessionId } = await ensureConnected(_port);
|
|
1656
2089
|
const browser = await getBrowser();
|
|
@@ -1669,13 +2102,21 @@ const tapCommand: Command = {
|
|
|
1669
2102
|
x = Math.round(box.x);
|
|
1670
2103
|
y = Math.round(box.y);
|
|
1671
2104
|
} else {
|
|
1672
|
-
const posJson = await browser.evaluateJs(
|
|
1673
|
-
|
|
2105
|
+
const posJson = (await browser.evaluateJs(
|
|
2106
|
+
client,
|
|
2107
|
+
sessionId,
|
|
2108
|
+
`(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});})()`,
|
|
2109
|
+
)) as string | null;
|
|
1674
2110
|
if (!posJson) throw new Error(`Selector not found: ${arg}`);
|
|
1675
2111
|
const pos = JSON.parse(posJson) as { x: number; y: number };
|
|
1676
|
-
x = Math.round(pos.x);
|
|
2112
|
+
x = Math.round(pos.x);
|
|
2113
|
+
y = Math.round(pos.y);
|
|
1677
2114
|
}
|
|
1678
|
-
await client.send(
|
|
2115
|
+
await client.send(
|
|
2116
|
+
'Input.dispatchTouchEvent',
|
|
2117
|
+
{ type: 'touchStart', touchPoints: [{ x, y }] },
|
|
2118
|
+
sessionId,
|
|
2119
|
+
);
|
|
1679
2120
|
await client.send('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] }, sessionId);
|
|
1680
2121
|
output.printSuccess(`Tapped at (${x}, ${y})`);
|
|
1681
2122
|
return { success: true, data: { x, y } };
|
|
@@ -1684,31 +2125,62 @@ const tapCommand: Command = {
|
|
|
1684
2125
|
|
|
1685
2126
|
const swipeCommand: Command = {
|
|
1686
2127
|
name: 'swipe',
|
|
1687
|
-
description:
|
|
2128
|
+
description:
|
|
2129
|
+
'Swipe gesture (mobile). Usage: monomind browse swipe up|down|left|right [distance] [--x N] [--y N]',
|
|
1688
2130
|
options: [
|
|
1689
|
-
{
|
|
1690
|
-
|
|
1691
|
-
|
|
2131
|
+
{
|
|
2132
|
+
name: 'x',
|
|
2133
|
+
type: 'number',
|
|
2134
|
+
description: 'Start X coordinate (default: center)',
|
|
2135
|
+
default: 200,
|
|
2136
|
+
},
|
|
2137
|
+
{
|
|
2138
|
+
name: 'y',
|
|
2139
|
+
type: 'number',
|
|
2140
|
+
description: 'Start Y coordinate (default: center)',
|
|
2141
|
+
default: 400,
|
|
2142
|
+
},
|
|
2143
|
+
{
|
|
2144
|
+
name: 'distance',
|
|
2145
|
+
short: 'd',
|
|
2146
|
+
type: 'number',
|
|
2147
|
+
description: 'Swipe distance in pixels',
|
|
2148
|
+
default: 300,
|
|
2149
|
+
},
|
|
1692
2150
|
],
|
|
1693
2151
|
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
1694
2152
|
const { client, sessionId } = await ensureConnected(_port);
|
|
1695
|
-
const direction =
|
|
2153
|
+
const direction = ctx.args[0] as string as 'up' | 'down' | 'left' | 'right';
|
|
1696
2154
|
if (!['up', 'down', 'left', 'right'].includes(direction)) {
|
|
1697
|
-
throw new Error(
|
|
2155
|
+
throw new Error(
|
|
2156
|
+
'Usage: monomind browse swipe up|down|left|right [--x N] [--y N] [--distance N]',
|
|
2157
|
+
);
|
|
1698
2158
|
}
|
|
1699
2159
|
const startX = (ctx.flags.x as number) ?? 200;
|
|
1700
2160
|
const startY = (ctx.flags.y as number) ?? 400;
|
|
1701
|
-
const positionalDistance =
|
|
1702
|
-
|
|
2161
|
+
const positionalDistance =
|
|
2162
|
+
ctx.args[1] !== undefined ? parseInt(ctx.args[1] as string, 10) : undefined;
|
|
2163
|
+
const distance =
|
|
2164
|
+
positionalDistance && Number.isFinite(positionalDistance)
|
|
2165
|
+
? positionalDistance
|
|
2166
|
+
: ((ctx.flags.distance as number) ?? 300);
|
|
1703
2167
|
const dx = direction === 'right' ? distance : direction === 'left' ? -distance : 0;
|
|
1704
2168
|
const dy = direction === 'down' ? distance : direction === 'up' ? -distance : 0;
|
|
1705
2169
|
|
|
1706
|
-
await client.send(
|
|
2170
|
+
await client.send(
|
|
2171
|
+
'Input.dispatchTouchEvent',
|
|
2172
|
+
{ type: 'touchStart', touchPoints: [{ x: startX, y: startY }] },
|
|
2173
|
+
sessionId,
|
|
2174
|
+
);
|
|
1707
2175
|
const steps = 10;
|
|
1708
2176
|
for (let i = 1; i <= steps; i++) {
|
|
1709
|
-
const x = Math.round(startX + dx * i / steps);
|
|
1710
|
-
const y = Math.round(startY + dy * i / steps);
|
|
1711
|
-
await client.send(
|
|
2177
|
+
const x = Math.round(startX + (dx * i) / steps);
|
|
2178
|
+
const y = Math.round(startY + (dy * i) / steps);
|
|
2179
|
+
await client.send(
|
|
2180
|
+
'Input.dispatchTouchEvent',
|
|
2181
|
+
{ type: 'touchMove', touchPoints: [{ x, y }] },
|
|
2182
|
+
sessionId,
|
|
2183
|
+
);
|
|
1712
2184
|
await new Promise((r) => setTimeout(r, 16));
|
|
1713
2185
|
}
|
|
1714
2186
|
await client.send('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] }, sessionId);
|
|
@@ -1761,7 +2233,8 @@ const uploadCommand: Command = {
|
|
|
1761
2233
|
const browser = await getBrowser();
|
|
1762
2234
|
const refArg = ctx.args[0] as string;
|
|
1763
2235
|
const files = ctx.args.slice(1) as string[];
|
|
1764
|
-
if (!refArg || files.length === 0)
|
|
2236
|
+
if (!refArg || files.length === 0)
|
|
2237
|
+
throw new Error('Usage: monomind browse upload @e1 <file1> [file2...]');
|
|
1765
2238
|
const refKey = refArg.startsWith('@') ? refArg.slice(1) : refArg;
|
|
1766
2239
|
const ref = await browser.resolveRef(client, sessionId, _refs, refKey);
|
|
1767
2240
|
await browser.uploadFile(client, sessionId, ref, files);
|
|
@@ -1772,37 +2245,57 @@ const uploadCommand: Command = {
|
|
|
1772
2245
|
|
|
1773
2246
|
const downloadCommand: Command = {
|
|
1774
2247
|
name: 'download',
|
|
1775
|
-
description:
|
|
2248
|
+
description:
|
|
2249
|
+
'Click an element and capture the triggered file download. Usage: monomind browse download @e1 ./output.pdf',
|
|
1776
2250
|
options: [
|
|
1777
|
-
{
|
|
2251
|
+
{
|
|
2252
|
+
name: 'timeout',
|
|
2253
|
+
short: 't',
|
|
2254
|
+
type: 'number',
|
|
2255
|
+
description: 'Max wait for download in ms',
|
|
2256
|
+
default: 30000,
|
|
2257
|
+
},
|
|
1778
2258
|
{ name: 'json', type: 'boolean', description: 'Output result as JSON', default: false },
|
|
1779
2259
|
],
|
|
1780
2260
|
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
1781
2261
|
const { client, sessionId } = await ensureConnected(_port);
|
|
1782
|
-
const
|
|
2262
|
+
const _browser = await getBrowser();
|
|
1783
2263
|
const refOrSel = ctx.args[0] as string;
|
|
1784
2264
|
const savePath = ctx.args[1] as string;
|
|
1785
|
-
if (!refOrSel || !savePath)
|
|
2265
|
+
if (!refOrSel || !savePath)
|
|
2266
|
+
throw new Error('Usage: monomind browse download @e1|selector <save-path>');
|
|
1786
2267
|
|
|
1787
|
-
const { mkdir } = await import('fs/promises');
|
|
1788
|
-
const { dirname } = await import('path');
|
|
1789
|
-
const { tmpdir } = await import('os');
|
|
1790
|
-
const { join } = await import('path');
|
|
2268
|
+
const { mkdir } = await import('node:fs/promises');
|
|
2269
|
+
const { dirname } = await import('node:path');
|
|
2270
|
+
const { tmpdir } = await import('node:os');
|
|
2271
|
+
const { join } = await import('node:path');
|
|
1791
2272
|
const downloadDir = join(tmpdir(), `mm-download-${Date.now()}`);
|
|
1792
2273
|
await mkdir(downloadDir, { recursive: true });
|
|
1793
2274
|
|
|
1794
2275
|
// Enable Page.downloadWillBegin / Page.downloadProgress events
|
|
1795
|
-
await client
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
2276
|
+
await client
|
|
2277
|
+
.send(
|
|
2278
|
+
'Browser.setDownloadBehavior',
|
|
2279
|
+
{
|
|
2280
|
+
behavior: 'allow',
|
|
2281
|
+
downloadPath: downloadDir,
|
|
2282
|
+
eventsEnabled: true,
|
|
2283
|
+
},
|
|
2284
|
+
undefined,
|
|
2285
|
+
)
|
|
2286
|
+
.catch(() => {
|
|
2287
|
+
// Fallback: older Chrome API (session-scoped)
|
|
2288
|
+
return client
|
|
2289
|
+
.send(
|
|
2290
|
+
'Page.setDownloadBehavior',
|
|
2291
|
+
{
|
|
2292
|
+
behavior: 'allow',
|
|
2293
|
+
downloadPath: downloadDir,
|
|
2294
|
+
},
|
|
2295
|
+
sessionId,
|
|
2296
|
+
)
|
|
2297
|
+
.catch(() => {});
|
|
2298
|
+
});
|
|
1806
2299
|
|
|
1807
2300
|
// Track when download completes
|
|
1808
2301
|
const downloadPromise = new Promise<string>((resolve, reject) => {
|
|
@@ -1814,38 +2307,52 @@ const downloadCommand: Command = {
|
|
|
1814
2307
|
let offProgress: (() => void) | undefined;
|
|
1815
2308
|
// cleanup defined before setTimeout so the timeout callback can call it
|
|
1816
2309
|
let timeout: ReturnType<typeof setTimeout>;
|
|
1817
|
-
const cleanup = () => {
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
2310
|
+
const cleanup = () => {
|
|
2311
|
+
clearTimeout(timeout);
|
|
2312
|
+
offBegin?.();
|
|
2313
|
+
offProgress?.();
|
|
2314
|
+
};
|
|
2315
|
+
timeout = setTimeout(() => {
|
|
2316
|
+
cleanup();
|
|
2317
|
+
reject(new Error('Download timed out'));
|
|
2318
|
+
}, ctx.flags.timeout as number);
|
|
2319
|
+
offProgress = client.on(
|
|
2320
|
+
'Browser.downloadProgress',
|
|
2321
|
+
async (params: Record<string, unknown>) => {
|
|
2322
|
+
if (params.guid === guid && params.state === 'completed') {
|
|
2323
|
+
cleanup();
|
|
2324
|
+
// Find the downloaded file in downloadDir
|
|
2325
|
+
const { readdir, rename, rmdir } = await import('node:fs/promises');
|
|
2326
|
+
const files = await readdir(downloadDir);
|
|
2327
|
+
if (files.length > 0) {
|
|
2328
|
+
const src = join(downloadDir, files[0]);
|
|
2329
|
+
await mkdir(dirname(savePath), { recursive: true });
|
|
2330
|
+
await rename(src, savePath);
|
|
2331
|
+
await rmdir(downloadDir).catch(() => {}); // I1: cleanup temp dir
|
|
2332
|
+
resolve(savePath);
|
|
2333
|
+
} else {
|
|
2334
|
+
await rmdir(downloadDir).catch(() => {}); // I1: cleanup temp dir
|
|
2335
|
+
reject(new Error('Download completed but no file found'));
|
|
2336
|
+
}
|
|
2337
|
+
} else if (params.guid === guid && params.state === 'canceled') {
|
|
2338
|
+
cleanup();
|
|
2339
|
+
reject(new Error('Download was canceled'));
|
|
1834
2340
|
}
|
|
1835
|
-
}
|
|
1836
|
-
|
|
1837
|
-
reject(new Error('Download was canceled'));
|
|
1838
|
-
}
|
|
1839
|
-
});
|
|
2341
|
+
},
|
|
2342
|
+
);
|
|
1840
2343
|
});
|
|
1841
2344
|
|
|
1842
2345
|
// Click the element to trigger download
|
|
1843
2346
|
const objectId = await resolveElementObjectId(client, sessionId, _refs, refOrSel);
|
|
1844
|
-
await client.send<{ result: unknown }>(
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
2347
|
+
await client.send<{ result: unknown }>(
|
|
2348
|
+
'Runtime.callFunctionOn',
|
|
2349
|
+
{
|
|
2350
|
+
functionDeclaration: 'function(){ this.click(); }',
|
|
2351
|
+
objectId,
|
|
2352
|
+
returnByValue: true,
|
|
2353
|
+
},
|
|
2354
|
+
sessionId,
|
|
2355
|
+
);
|
|
1849
2356
|
|
|
1850
2357
|
const finalPath = await downloadPromise;
|
|
1851
2358
|
if (ctx.flags.json) print(JSON.stringify({ data: { path: finalPath } }));
|
|
@@ -1982,7 +2489,8 @@ const dialogCommand: Command = {
|
|
|
1982
2489
|
|
|
1983
2490
|
const frameCommand: Command = {
|
|
1984
2491
|
name: 'frame',
|
|
1985
|
-
description:
|
|
2492
|
+
description:
|
|
2493
|
+
'Switch to iframe or back to main. Usage: monomind browse frame "#frame-id" | frame main',
|
|
1986
2494
|
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
1987
2495
|
const { client, sessionId } = await ensureConnected(_port);
|
|
1988
2496
|
const browser = await getBrowser();
|
|
@@ -2015,9 +2523,7 @@ const frameCommand: Command = {
|
|
|
2015
2523
|
const tabCommand: Command = {
|
|
2016
2524
|
name: 'tab',
|
|
2017
2525
|
description: 'Tab management. Usage: monomind browse tab list|new|close [url]',
|
|
2018
|
-
options: [
|
|
2019
|
-
{ name: 'label', type: 'string', description: 'Label for new tab' },
|
|
2020
|
-
],
|
|
2526
|
+
options: [{ name: 'label', type: 'string', description: 'Label for new tab' }],
|
|
2021
2527
|
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
2022
2528
|
const { client, sessionId } = await ensureConnected(_port);
|
|
2023
2529
|
const browser = await getBrowser();
|
|
@@ -2042,13 +2548,25 @@ const tabCommand: Command = {
|
|
|
2042
2548
|
const sid = _sessionId;
|
|
2043
2549
|
// Stop profiling before closing the tab so CDP commands still reach the live session
|
|
2044
2550
|
if (browser.getHarStatus(sid).recording) {
|
|
2045
|
-
try {
|
|
2551
|
+
try {
|
|
2552
|
+
await browser.stopHarRecording(client, sid);
|
|
2553
|
+
} catch {
|
|
2554
|
+
/* ignore */
|
|
2555
|
+
}
|
|
2046
2556
|
}
|
|
2047
2557
|
if (browser.getTraceStatus(sid)) {
|
|
2048
|
-
try {
|
|
2558
|
+
try {
|
|
2559
|
+
await browser.stopTrace(client, sid);
|
|
2560
|
+
} catch {
|
|
2561
|
+
/* ignore */
|
|
2562
|
+
}
|
|
2049
2563
|
}
|
|
2050
2564
|
if (browser.isProfilingActive(sid)) {
|
|
2051
|
-
try {
|
|
2565
|
+
try {
|
|
2566
|
+
await browser.stopCpuProfile(client, sid);
|
|
2567
|
+
} catch {
|
|
2568
|
+
/* ignore */
|
|
2569
|
+
}
|
|
2052
2570
|
}
|
|
2053
2571
|
browser.teardownRouteInterception(sid);
|
|
2054
2572
|
browser.stopRequestCapture(sid);
|
|
@@ -2072,13 +2590,25 @@ const tabCommand: Command = {
|
|
|
2072
2590
|
const newSid = await browser.activateTab(client, sessionId, action);
|
|
2073
2591
|
const oldSid = _sessionId;
|
|
2074
2592
|
if (browser.getHarStatus(oldSid).recording) {
|
|
2075
|
-
try {
|
|
2593
|
+
try {
|
|
2594
|
+
await browser.stopHarRecording(client, oldSid);
|
|
2595
|
+
} catch {
|
|
2596
|
+
/* ignore */
|
|
2597
|
+
}
|
|
2076
2598
|
}
|
|
2077
2599
|
if (browser.getTraceStatus(oldSid)) {
|
|
2078
|
-
try {
|
|
2600
|
+
try {
|
|
2601
|
+
await browser.stopTrace(client, oldSid);
|
|
2602
|
+
} catch {
|
|
2603
|
+
/* ignore */
|
|
2604
|
+
}
|
|
2079
2605
|
}
|
|
2080
2606
|
if (browser.isProfilingActive(oldSid)) {
|
|
2081
|
-
try {
|
|
2607
|
+
try {
|
|
2608
|
+
await browser.stopCpuProfile(client, oldSid);
|
|
2609
|
+
} catch {
|
|
2610
|
+
/* ignore */
|
|
2611
|
+
}
|
|
2082
2612
|
}
|
|
2083
2613
|
await browser.disableInterception(client, oldSid).catch(() => {});
|
|
2084
2614
|
browser.stopRequestCapture(oldSid);
|
|
@@ -2105,23 +2635,47 @@ const windowCommand: Command = {
|
|
|
2105
2635
|
|
|
2106
2636
|
if (!action || action === 'new') {
|
|
2107
2637
|
// Create isolated browser context (incognito-like) with a fresh page
|
|
2108
|
-
const ctxResult = await client.send<{ browserContextId: string }>(
|
|
2638
|
+
const ctxResult = await client.send<{ browserContextId: string }>(
|
|
2639
|
+
'Target.createBrowserContext',
|
|
2640
|
+
{},
|
|
2641
|
+
undefined,
|
|
2642
|
+
);
|
|
2109
2643
|
const browserContextId = ctxResult.browserContextId;
|
|
2110
2644
|
const url = (ctx.args[1] as string) || 'about:blank';
|
|
2111
|
-
const targetResult = await client.send<{ targetId: string }>(
|
|
2645
|
+
const targetResult = await client.send<{ targetId: string }>(
|
|
2646
|
+
'Target.createTarget',
|
|
2647
|
+
{ url, browserContextId },
|
|
2648
|
+
undefined,
|
|
2649
|
+
);
|
|
2112
2650
|
const targetId = targetResult.targetId;
|
|
2113
|
-
const attachResult = await client.send<{ sessionId: string }>(
|
|
2651
|
+
const attachResult = await client.send<{ sessionId: string }>(
|
|
2652
|
+
'Target.attachToTarget',
|
|
2653
|
+
{ targetId, flatten: true },
|
|
2654
|
+
undefined,
|
|
2655
|
+
);
|
|
2114
2656
|
const newSessionId = attachResult.sessionId;
|
|
2115
2657
|
// W3: fully tear down old session before switching
|
|
2116
2658
|
const oldSid = _sessionId;
|
|
2117
2659
|
if (browser.getHarStatus(oldSid).recording) {
|
|
2118
|
-
try {
|
|
2660
|
+
try {
|
|
2661
|
+
await browser.stopHarRecording(client, oldSid);
|
|
2662
|
+
} catch {
|
|
2663
|
+
/* ignore */
|
|
2664
|
+
}
|
|
2119
2665
|
}
|
|
2120
2666
|
if (browser.getTraceStatus(oldSid)) {
|
|
2121
|
-
try {
|
|
2667
|
+
try {
|
|
2668
|
+
await browser.stopTrace(client, oldSid);
|
|
2669
|
+
} catch {
|
|
2670
|
+
/* ignore */
|
|
2671
|
+
}
|
|
2122
2672
|
}
|
|
2123
2673
|
if (browser.isProfilingActive(oldSid)) {
|
|
2124
|
-
try {
|
|
2674
|
+
try {
|
|
2675
|
+
await browser.stopCpuProfile(client, oldSid);
|
|
2676
|
+
} catch {
|
|
2677
|
+
/* ignore */
|
|
2678
|
+
}
|
|
2125
2679
|
}
|
|
2126
2680
|
browser.teardownRouteInterception(oldSid);
|
|
2127
2681
|
browser.stopRequestCapture(oldSid);
|
|
@@ -2196,7 +2750,8 @@ const errorsCommand: Command = {
|
|
|
2196
2750
|
|
|
2197
2751
|
const storageCommand: Command = {
|
|
2198
2752
|
name: 'storage',
|
|
2199
|
-
description:
|
|
2753
|
+
description:
|
|
2754
|
+
'localStorage/sessionStorage management. Usage: monomind browse storage local|session [key] [--set val] [--clear]',
|
|
2200
2755
|
options: [
|
|
2201
2756
|
{ name: 'set', type: 'string', description: 'Value to set for key' },
|
|
2202
2757
|
{ name: 'clear', type: 'boolean', description: 'Clear all storage', default: false },
|
|
@@ -2221,7 +2776,8 @@ const storageCommand: Command = {
|
|
|
2221
2776
|
}
|
|
2222
2777
|
|
|
2223
2778
|
if (key && ctx.flags.set !== undefined) {
|
|
2224
|
-
if (isLocal)
|
|
2779
|
+
if (isLocal)
|
|
2780
|
+
await browser.setLocalStorageKey(client, sessionId, key, ctx.flags.set as string);
|
|
2225
2781
|
else await browser.setSessionStorageKey(client, sessionId, key, ctx.flags.set as string);
|
|
2226
2782
|
output.printSuccess(`Set ${key}`);
|
|
2227
2783
|
return { success: true };
|
|
@@ -2281,11 +2837,13 @@ const cookiesCommand: Command = {
|
|
|
2281
2837
|
if (!name || value === undefined) {
|
|
2282
2838
|
throw new Error('Usage: monomind browse cookies set <name> <value> [--domain <d>]');
|
|
2283
2839
|
}
|
|
2284
|
-
await browser.setCookies(client, sessionId, [
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2840
|
+
await browser.setCookies(client, sessionId, [
|
|
2841
|
+
{
|
|
2842
|
+
name,
|
|
2843
|
+
value,
|
|
2844
|
+
domain: ctx.flags.domain as string,
|
|
2845
|
+
},
|
|
2846
|
+
]);
|
|
2289
2847
|
output.printSuccess(`Cookie set: ${name}`);
|
|
2290
2848
|
break;
|
|
2291
2849
|
}
|
|
@@ -2323,9 +2881,7 @@ const pdfCommand: Command = {
|
|
|
2323
2881
|
const isCommand: Command = {
|
|
2324
2882
|
name: 'is',
|
|
2325
2883
|
description: 'Check element state. Usage: monomind browse is visible|enabled|checked @e1',
|
|
2326
|
-
options: [
|
|
2327
|
-
{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
|
|
2328
|
-
],
|
|
2884
|
+
options: [{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false }],
|
|
2329
2885
|
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
2330
2886
|
const { client, sessionId } = await ensureConnected(_port);
|
|
2331
2887
|
const browser = await getBrowser();
|
|
@@ -2338,10 +2894,17 @@ const isCommand: Command = {
|
|
|
2338
2894
|
|
|
2339
2895
|
let result: boolean;
|
|
2340
2896
|
switch (check) {
|
|
2341
|
-
case 'visible':
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2897
|
+
case 'visible':
|
|
2898
|
+
result = await browser.isVisible(client, sessionId, ref);
|
|
2899
|
+
break;
|
|
2900
|
+
case 'enabled':
|
|
2901
|
+
result = await browser.isEnabled(client, sessionId, ref);
|
|
2902
|
+
break;
|
|
2903
|
+
case 'checked':
|
|
2904
|
+
result = await browser.isChecked(client, sessionId, ref);
|
|
2905
|
+
break;
|
|
2906
|
+
default:
|
|
2907
|
+
throw new Error(`Unknown check: ${check}. Use: visible|enabled|checked`);
|
|
2345
2908
|
}
|
|
2346
2909
|
|
|
2347
2910
|
if (ctx.flags.json) {
|
|
@@ -2355,7 +2918,8 @@ const isCommand: Command = {
|
|
|
2355
2918
|
|
|
2356
2919
|
const findCommand: Command = {
|
|
2357
2920
|
name: 'find',
|
|
2358
|
-
description:
|
|
2921
|
+
description:
|
|
2922
|
+
'Find elements by semantic locators. Usage: monomind browse find role|text|label|placeholder|testid|alttext|title|selector <value> [action]',
|
|
2359
2923
|
options: [
|
|
2360
2924
|
{ name: 'name', type: 'string', description: 'Filter by accessible name' },
|
|
2361
2925
|
{ name: 'exact', type: 'boolean', description: 'Require exact match', default: false },
|
|
@@ -2369,7 +2933,10 @@ const findCommand: Command = {
|
|
|
2369
2933
|
const value = ctx.args[1] as string;
|
|
2370
2934
|
const action = ctx.args[2] as FindAction | undefined;
|
|
2371
2935
|
|
|
2372
|
-
if (!locator || !value)
|
|
2936
|
+
if (!locator || !value)
|
|
2937
|
+
throw new Error(
|
|
2938
|
+
'Usage: monomind browse find role|text|label|placeholder|testid|alttext|title|selector <value> [action]',
|
|
2939
|
+
);
|
|
2373
2940
|
|
|
2374
2941
|
const opts = {
|
|
2375
2942
|
name: ctx.flags.name as string,
|
|
@@ -2382,9 +2949,15 @@ const findCommand: Command = {
|
|
|
2382
2949
|
if (locator === 'alttext') {
|
|
2383
2950
|
// I2: use JS attribute comparison to avoid broken CSS selectors for values with spaces/quotes
|
|
2384
2951
|
const valJson = JSON.stringify(value);
|
|
2385
|
-
const found = await browser.evaluateJs(
|
|
2386
|
-
|
|
2387
|
-
|
|
2952
|
+
const found = (await browser.evaluateJs(
|
|
2953
|
+
client,
|
|
2954
|
+
sessionId,
|
|
2955
|
+
`(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})`,
|
|
2956
|
+
)) as boolean;
|
|
2957
|
+
if (!found) {
|
|
2958
|
+
output.printWarning(`alttext not found: ${value}`);
|
|
2959
|
+
return { success: false };
|
|
2960
|
+
}
|
|
2388
2961
|
output.printSuccess(`Found element with alt="${value}"`);
|
|
2389
2962
|
return { success: true, data: { alttext: value } };
|
|
2390
2963
|
}
|
|
@@ -2393,28 +2966,49 @@ const findCommand: Command = {
|
|
|
2393
2966
|
if (locator === 'title') {
|
|
2394
2967
|
// I2: use JS attribute comparison to avoid broken CSS selectors for values with spaces/quotes
|
|
2395
2968
|
const valJson = JSON.stringify(value);
|
|
2396
|
-
const found = await browser.evaluateJs(
|
|
2397
|
-
|
|
2398
|
-
|
|
2969
|
+
const found = (await browser.evaluateJs(
|
|
2970
|
+
client,
|
|
2971
|
+
sessionId,
|
|
2972
|
+
`(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})`,
|
|
2973
|
+
)) as boolean;
|
|
2974
|
+
if (!found) {
|
|
2975
|
+
output.printWarning(`title not found: ${value}`);
|
|
2976
|
+
return { success: false };
|
|
2977
|
+
}
|
|
2399
2978
|
output.printSuccess(`Found element with title="${value}"`);
|
|
2400
2979
|
return { success: true, data: { title: value } };
|
|
2401
2980
|
}
|
|
2402
2981
|
|
|
2403
2982
|
let ref: ElementRef | null = null;
|
|
2404
2983
|
switch (locator) {
|
|
2405
|
-
case 'role':
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
case '
|
|
2409
|
-
|
|
2984
|
+
case 'role':
|
|
2985
|
+
ref = await browser.findByRole(client, sessionId, _refs, value, opts);
|
|
2986
|
+
break;
|
|
2987
|
+
case 'text':
|
|
2988
|
+
ref = await browser.findByText(client, sessionId, _refs, value, opts);
|
|
2989
|
+
break;
|
|
2990
|
+
case 'label':
|
|
2991
|
+
ref = await browser.findByLabel(client, sessionId, _refs, value, opts);
|
|
2992
|
+
break;
|
|
2993
|
+
case 'placeholder':
|
|
2994
|
+
ref = await browser.findByPlaceholder(client, sessionId, _refs, value, opts);
|
|
2995
|
+
break;
|
|
2996
|
+
case 'selector':
|
|
2997
|
+
ref = await browser.findBySelector(client, sessionId, _refs, value, opts);
|
|
2998
|
+
break;
|
|
2410
2999
|
case 'testid': {
|
|
2411
3000
|
const sel = await browser.findByTestId(client, sessionId, value);
|
|
2412
|
-
if (!sel) {
|
|
3001
|
+
if (!sel) {
|
|
3002
|
+
output.printWarning(`testid not found: ${value}`);
|
|
3003
|
+
return { success: false };
|
|
3004
|
+
}
|
|
2413
3005
|
output.printSuccess(`Found testid selector: ${sel}`);
|
|
2414
3006
|
return { success: true, data: { selector: sel } };
|
|
2415
3007
|
}
|
|
2416
3008
|
default:
|
|
2417
|
-
throw new Error(
|
|
3009
|
+
throw new Error(
|
|
3010
|
+
`Unknown locator: ${locator}. Use: role|text|label|placeholder|testid|alttext|title|selector`,
|
|
3011
|
+
);
|
|
2418
3012
|
}
|
|
2419
3013
|
|
|
2420
3014
|
if (!ref) {
|
|
@@ -2426,7 +3020,9 @@ const findCommand: Command = {
|
|
|
2426
3020
|
|
|
2427
3021
|
if (action) {
|
|
2428
3022
|
switch (action) {
|
|
2429
|
-
case 'click':
|
|
3023
|
+
case 'click':
|
|
3024
|
+
await browser.clickElement(client, sessionId, ref);
|
|
3025
|
+
break;
|
|
2430
3026
|
case 'fill': {
|
|
2431
3027
|
const fillValue = ctx.args[3] as string;
|
|
2432
3028
|
await browser.fillElement(client, sessionId, ref, fillValue ?? '');
|
|
@@ -2437,18 +3033,31 @@ const findCommand: Command = {
|
|
|
2437
3033
|
await browser.typeIntoElement(client, sessionId, ref, typeValue ?? '');
|
|
2438
3034
|
break;
|
|
2439
3035
|
}
|
|
2440
|
-
case 'hover':
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
case '
|
|
3036
|
+
case 'hover':
|
|
3037
|
+
await browser.hoverElement(client, sessionId, ref);
|
|
3038
|
+
break;
|
|
3039
|
+
case 'focus':
|
|
3040
|
+
await browser.focusElement(client, sessionId, ref);
|
|
3041
|
+
break;
|
|
3042
|
+
case 'check':
|
|
3043
|
+
await browser.checkElement(client, sessionId, ref, true);
|
|
3044
|
+
break;
|
|
3045
|
+
case 'uncheck':
|
|
3046
|
+
await browser.checkElement(client, sessionId, ref, false);
|
|
3047
|
+
break;
|
|
2444
3048
|
case 'text': {
|
|
2445
3049
|
const objectId = await browser.getObjectIdForRef(client, sessionId, ref);
|
|
2446
3050
|
if (objectId) {
|
|
2447
|
-
const r = await client.send<{ result: { value?: string } }>(
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
3051
|
+
const r = await client.send<{ result: { value?: string } }>(
|
|
3052
|
+
'Runtime.callFunctionOn',
|
|
3053
|
+
{
|
|
3054
|
+
functionDeclaration:
|
|
3055
|
+
'function() { return this.innerText || this.textContent || ""; }',
|
|
3056
|
+
objectId,
|
|
3057
|
+
returnByValue: true,
|
|
3058
|
+
},
|
|
3059
|
+
sessionId,
|
|
3060
|
+
);
|
|
2452
3061
|
print(r.result?.value ?? '');
|
|
2453
3062
|
}
|
|
2454
3063
|
break;
|
|
@@ -2478,9 +3087,16 @@ const highlightCommand: Command = {
|
|
|
2478
3087
|
|
|
2479
3088
|
const diffCommand: Command = {
|
|
2480
3089
|
name: 'diff',
|
|
2481
|
-
description:
|
|
3090
|
+
description:
|
|
3091
|
+
'Compare two URLs or snapshots. Usage: monomind browse diff url <url1> <url2> [--interactive] [--json]',
|
|
2482
3092
|
options: [
|
|
2483
|
-
{
|
|
3093
|
+
{
|
|
3094
|
+
name: 'interactive',
|
|
3095
|
+
short: 'i',
|
|
3096
|
+
type: 'boolean',
|
|
3097
|
+
description: 'Snapshot interactive elements only',
|
|
3098
|
+
default: false,
|
|
3099
|
+
},
|
|
2484
3100
|
{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
|
|
2485
3101
|
],
|
|
2486
3102
|
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
@@ -2496,12 +3112,16 @@ const diffCommand: Command = {
|
|
|
2496
3112
|
// Capture snapshot at url1
|
|
2497
3113
|
await browser.openUrl(client, sessionId, url1);
|
|
2498
3114
|
await browser.waitFor(client, sessionId, { load: 'load', timeout: 15000 });
|
|
2499
|
-
const snap1 = await browser.captureSnapshot(client, sessionId, {
|
|
3115
|
+
const snap1 = await browser.captureSnapshot(client, sessionId, {
|
|
3116
|
+
interactiveOnly: ctx.flags.interactive as boolean,
|
|
3117
|
+
});
|
|
2500
3118
|
|
|
2501
3119
|
// Capture snapshot at url2
|
|
2502
3120
|
await browser.openUrl(client, sessionId, url2);
|
|
2503
3121
|
await browser.waitFor(client, sessionId, { load: 'load', timeout: 15000 });
|
|
2504
|
-
const snap2 = await browser.captureSnapshot(client, sessionId, {
|
|
3122
|
+
const snap2 = await browser.captureSnapshot(client, sessionId, {
|
|
3123
|
+
interactiveOnly: ctx.flags.interactive as boolean,
|
|
3124
|
+
});
|
|
2505
3125
|
|
|
2506
3126
|
_refs = snap2.refs;
|
|
2507
3127
|
await browser.saveRefCache(_targetId, snap2.url, _refs);
|
|
@@ -2516,17 +3136,32 @@ const diffCommand: Command = {
|
|
|
2516
3136
|
const changed = onlyIn1.length > 0 || onlyIn2.length > 0;
|
|
2517
3137
|
|
|
2518
3138
|
if (ctx.flags.json) {
|
|
2519
|
-
print(
|
|
3139
|
+
print(
|
|
3140
|
+
JSON.stringify({
|
|
3141
|
+
changed,
|
|
3142
|
+
url1,
|
|
3143
|
+
url2,
|
|
3144
|
+
onlyIn1,
|
|
3145
|
+
onlyIn2,
|
|
3146
|
+
additions: onlyIn2.length,
|
|
3147
|
+
removals: onlyIn1.length,
|
|
3148
|
+
}),
|
|
3149
|
+
);
|
|
2520
3150
|
} else {
|
|
2521
3151
|
if (!changed) {
|
|
2522
3152
|
output.printSuccess(`No differences between ${url1} and ${url2}`);
|
|
2523
3153
|
} else {
|
|
2524
|
-
output.printWarning(
|
|
3154
|
+
output.printWarning(
|
|
3155
|
+
`Diff: ${url1} vs ${url2} — +${onlyIn2.length} lines, -${onlyIn1.length} lines`,
|
|
3156
|
+
);
|
|
2525
3157
|
for (const l of onlyIn1) print(`\x1b[31m- ${l}\x1b[0m`);
|
|
2526
3158
|
for (const l of onlyIn2) print(`\x1b[32m+ ${l}\x1b[0m`);
|
|
2527
3159
|
}
|
|
2528
3160
|
}
|
|
2529
|
-
return {
|
|
3161
|
+
return {
|
|
3162
|
+
success: true,
|
|
3163
|
+
data: { changed, url1, url2, additions: onlyIn2.length, removals: onlyIn1.length },
|
|
3164
|
+
};
|
|
2530
3165
|
}
|
|
2531
3166
|
|
|
2532
3167
|
throw new Error('Usage: monomind browse diff url <url1> <url2>');
|
|
@@ -2557,12 +3192,18 @@ function tokenizeBatchCommand(input: string): string[] {
|
|
|
2557
3192
|
let inQuote: '"' | "'" | null = null;
|
|
2558
3193
|
for (const ch of input.trim()) {
|
|
2559
3194
|
if (inQuote) {
|
|
2560
|
-
if (ch === inQuote) {
|
|
2561
|
-
|
|
3195
|
+
if (ch === inQuote) {
|
|
3196
|
+
inQuote = null;
|
|
3197
|
+
} else {
|
|
3198
|
+
current += ch;
|
|
3199
|
+
}
|
|
2562
3200
|
} else if (ch === '"' || ch === "'") {
|
|
2563
3201
|
inQuote = ch;
|
|
2564
3202
|
} else if (/\s/.test(ch)) {
|
|
2565
|
-
if (current) {
|
|
3203
|
+
if (current) {
|
|
3204
|
+
tokens.push(current);
|
|
3205
|
+
current = '';
|
|
3206
|
+
}
|
|
2566
3207
|
} else {
|
|
2567
3208
|
current += ch;
|
|
2568
3209
|
}
|
|
@@ -2597,8 +3238,15 @@ function tokenizeBatchCommand(input: string): string[] {
|
|
|
2597
3238
|
* Exported for direct unit testing — not part of the CLI's public API.
|
|
2598
3239
|
*/
|
|
2599
3240
|
export function deriveBoxOutput(
|
|
2600
|
-
center: { x: number; y: number; width: number; height: number } | null
|
|
2601
|
-
): {
|
|
3241
|
+
center: { x: number; y: number; width: number; height: number } | null,
|
|
3242
|
+
): {
|
|
3243
|
+
x: number;
|
|
3244
|
+
y: number;
|
|
3245
|
+
width: number;
|
|
3246
|
+
height: number;
|
|
3247
|
+
centerX: number;
|
|
3248
|
+
centerY: number;
|
|
3249
|
+
} | null {
|
|
2602
3250
|
if (!center) return null;
|
|
2603
3251
|
return {
|
|
2604
3252
|
x: center.x - center.width / 2,
|
|
@@ -2610,7 +3258,11 @@ export function deriveBoxOutput(
|
|
|
2610
3258
|
};
|
|
2611
3259
|
}
|
|
2612
3260
|
|
|
2613
|
-
export function parseBatchCommandLine(cmdStr: string): {
|
|
3261
|
+
export function parseBatchCommandLine(cmdStr: string): {
|
|
3262
|
+
subName: string;
|
|
3263
|
+
subArgs: string[];
|
|
3264
|
+
flags: Record<string, unknown>;
|
|
3265
|
+
} {
|
|
2614
3266
|
const trimmed = cmdStr.trim();
|
|
2615
3267
|
const evalMatch = trimmed.match(/^eval\b\s*/);
|
|
2616
3268
|
if (!evalMatch) {
|
|
@@ -2635,7 +3287,6 @@ export function parseBatchCommandLine(cmdStr: string): { subName: string; subArg
|
|
|
2635
3287
|
flags[boolFlag[1]] = true;
|
|
2636
3288
|
rest = rest.slice(boolFlag[0].length);
|
|
2637
3289
|
consumedAnother = true;
|
|
2638
|
-
continue;
|
|
2639
3290
|
}
|
|
2640
3291
|
}
|
|
2641
3292
|
return { subName: 'eval', subArgs: [rest], flags };
|
|
@@ -2643,7 +3294,8 @@ export function parseBatchCommandLine(cmdStr: string): { subName: string; subArg
|
|
|
2643
3294
|
|
|
2644
3295
|
const batchCommand: Command = {
|
|
2645
3296
|
name: 'batch',
|
|
2646
|
-
description:
|
|
3297
|
+
description:
|
|
3298
|
+
'Execute multiple commands. Usage: monomind browse batch "open url" "snapshot -i" "click @e1"',
|
|
2647
3299
|
options: [
|
|
2648
3300
|
{ name: 'bail', type: 'boolean', description: 'Stop on first error', default: false },
|
|
2649
3301
|
{ name: 'json', type: 'boolean', description: 'Input from JSON stdin', default: false },
|
|
@@ -2692,7 +3344,11 @@ const batchCommand: Command = {
|
|
|
2692
3344
|
} else {
|
|
2693
3345
|
parsedFlags[key] = true;
|
|
2694
3346
|
}
|
|
2695
|
-
} else if (
|
|
3347
|
+
} else if (
|
|
3348
|
+
subArgs[i].startsWith('-') &&
|
|
3349
|
+
subArgs[i].length === 2 &&
|
|
3350
|
+
/[a-zA-Z]/.test(subArgs[i][1])
|
|
3351
|
+
) {
|
|
2696
3352
|
consumedIndices.add(i);
|
|
2697
3353
|
const shortKey = subArgs[i][1];
|
|
2698
3354
|
const optDef = subCmd.options?.find((o) => o.short === shortKey);
|
|
@@ -2723,7 +3379,11 @@ const batchCommand: Command = {
|
|
|
2723
3379
|
|
|
2724
3380
|
const cmdResult = await subCmd.action(fakeCtx);
|
|
2725
3381
|
const succeeded = cmdResult?.success !== false;
|
|
2726
|
-
results.push({
|
|
3382
|
+
results.push({
|
|
3383
|
+
command: cmdStr,
|
|
3384
|
+
success: succeeded,
|
|
3385
|
+
error: succeeded ? undefined : 'Command returned failure',
|
|
3386
|
+
});
|
|
2727
3387
|
if (!succeeded && ctx.flags.bail) break;
|
|
2728
3388
|
} catch (e) {
|
|
2729
3389
|
const err = e instanceof Error ? e.message : String(e);
|
|
@@ -2741,7 +3401,8 @@ const batchCommand: Command = {
|
|
|
2741
3401
|
|
|
2742
3402
|
const addinitscriptCommand: Command = {
|
|
2743
3403
|
name: 'addinitscript',
|
|
2744
|
-
description:
|
|
3404
|
+
description:
|
|
3405
|
+
'Add script to run before page navigation. Usage: monomind browse addinitscript "window.x=1"',
|
|
2745
3406
|
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
2746
3407
|
const { client, sessionId } = await ensureConnected(_port);
|
|
2747
3408
|
const browser = await getBrowser();
|
|
@@ -2755,7 +3416,8 @@ const addinitscriptCommand: Command = {
|
|
|
2755
3416
|
|
|
2756
3417
|
const removeinitscriptCommand: Command = {
|
|
2757
3418
|
name: 'removeinitscript',
|
|
2758
|
-
description:
|
|
3419
|
+
description:
|
|
3420
|
+
'Remove a previously added init script. Usage: monomind browse removeinitscript <id>',
|
|
2759
3421
|
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
2760
3422
|
const { client, sessionId } = await ensureConnected(_port);
|
|
2761
3423
|
const browser = await getBrowser();
|
|
@@ -2769,11 +3431,17 @@ const removeinitscriptCommand: Command = {
|
|
|
2769
3431
|
|
|
2770
3432
|
const connectCommand: Command = {
|
|
2771
3433
|
name: 'connect',
|
|
2772
|
-
description:
|
|
3434
|
+
description:
|
|
3435
|
+
'Connect to existing Chrome instance; later commands reuse this session (note: `open` without --port still launches on its own default). Usage: monomind browse connect [--port 9222] [--target <id>] [--auto-connect]',
|
|
2773
3436
|
options: [
|
|
2774
3437
|
{ name: 'port', short: 'p', type: 'number', description: 'CDP port', default: 9222 },
|
|
2775
3438
|
{ name: 'target', type: 'string', description: 'Target ID to attach to' },
|
|
2776
|
-
{
|
|
3439
|
+
{
|
|
3440
|
+
name: 'auto-connect',
|
|
3441
|
+
type: 'boolean',
|
|
3442
|
+
description: 'Auto-discover running Chrome on ports 9222 and 9229',
|
|
3443
|
+
default: false,
|
|
3444
|
+
},
|
|
2777
3445
|
],
|
|
2778
3446
|
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
2779
3447
|
let port = (ctx.flags.port as number) ?? 9222;
|
|
@@ -2784,10 +3452,19 @@ const connectCommand: Command = {
|
|
|
2784
3452
|
for (const p of probePorts) {
|
|
2785
3453
|
try {
|
|
2786
3454
|
const r = await fetch(`http://127.0.0.1:${p}/json/version`);
|
|
2787
|
-
if (r.ok) {
|
|
2788
|
-
|
|
3455
|
+
if (r.ok) {
|
|
3456
|
+
port = p;
|
|
3457
|
+
found = true;
|
|
3458
|
+
break;
|
|
3459
|
+
}
|
|
3460
|
+
} catch {
|
|
3461
|
+
/* port not open */
|
|
3462
|
+
}
|
|
2789
3463
|
}
|
|
2790
|
-
if (!found)
|
|
3464
|
+
if (!found)
|
|
3465
|
+
throw new Error(
|
|
3466
|
+
'No running Chrome instance found. Launch Chrome with --remote-debugging-port or use --port.',
|
|
3467
|
+
);
|
|
2791
3468
|
}
|
|
2792
3469
|
|
|
2793
3470
|
const browser = await getBrowser();
|
|
@@ -2796,13 +3473,25 @@ const connectCommand: Command = {
|
|
|
2796
3473
|
const prevSid = _sessionId;
|
|
2797
3474
|
const prevClient = _client;
|
|
2798
3475
|
if (browser.getHarStatus(prevSid).recording) {
|
|
2799
|
-
try {
|
|
3476
|
+
try {
|
|
3477
|
+
await browser.stopHarRecording(prevClient, prevSid);
|
|
3478
|
+
} catch {
|
|
3479
|
+
/* ignore */
|
|
3480
|
+
}
|
|
2800
3481
|
}
|
|
2801
3482
|
if (browser.getTraceStatus(prevSid)) {
|
|
2802
|
-
try {
|
|
3483
|
+
try {
|
|
3484
|
+
await browser.stopTrace(prevClient, prevSid);
|
|
3485
|
+
} catch {
|
|
3486
|
+
/* ignore */
|
|
3487
|
+
}
|
|
2803
3488
|
}
|
|
2804
3489
|
if (browser.isProfilingActive(prevSid)) {
|
|
2805
|
-
try {
|
|
3490
|
+
try {
|
|
3491
|
+
await browser.stopCpuProfile(prevClient, prevSid);
|
|
3492
|
+
} catch {
|
|
3493
|
+
/* ignore */
|
|
3494
|
+
}
|
|
2806
3495
|
}
|
|
2807
3496
|
browser.teardownRouteInterception(prevSid);
|
|
2808
3497
|
browser.stopRequestCapture(prevSid);
|
|
@@ -2880,7 +3569,10 @@ const recordCommand: Command = {
|
|
|
2880
3569
|
case 'status': {
|
|
2881
3570
|
const status = browser.getRecordingStatus(sessionId);
|
|
2882
3571
|
if (ctx.flags.json) print(JSON.stringify({ data: status }));
|
|
2883
|
-
else
|
|
3572
|
+
else
|
|
3573
|
+
print(
|
|
3574
|
+
`Recording: ${status.recording} | Frames: ${status.frames}${status.autoStopped ? ' (auto-stopped: buffer limit reached — run "record stop" to save)' : ''}`,
|
|
3575
|
+
);
|
|
2884
3576
|
return { success: true, data: status };
|
|
2885
3577
|
}
|
|
2886
3578
|
default:
|
|
@@ -2904,7 +3596,9 @@ const traceCommand: Command = {
|
|
|
2904
3596
|
|
|
2905
3597
|
switch (action) {
|
|
2906
3598
|
case 'start':
|
|
2907
|
-
await browser.startTrace(client, sessionId, {
|
|
3599
|
+
await browser.startTrace(client, sessionId, {
|
|
3600
|
+
screenshots: ctx.flags.screenshots as boolean,
|
|
3601
|
+
});
|
|
2908
3602
|
output.printSuccess('Trace started');
|
|
2909
3603
|
return { success: true };
|
|
2910
3604
|
case 'stop': {
|
|
@@ -2937,7 +3631,9 @@ const profilerCommand: Command = {
|
|
|
2937
3631
|
|
|
2938
3632
|
switch (action) {
|
|
2939
3633
|
case 'start':
|
|
2940
|
-
await browser.startCpuProfile(client, sessionId, {
|
|
3634
|
+
await browser.startCpuProfile(client, sessionId, {
|
|
3635
|
+
samplingInterval: ctx.flags.interval as number,
|
|
3636
|
+
});
|
|
2941
3637
|
output.printSuccess('CPU profiler started');
|
|
2942
3638
|
return { success: true };
|
|
2943
3639
|
case 'stop': {
|
|
@@ -2997,7 +3693,12 @@ const harCommand: Command = {
|
|
|
2997
3693
|
output.printSuccess('HAR recording started');
|
|
2998
3694
|
return { success: true };
|
|
2999
3695
|
case 'stop': {
|
|
3000
|
-
const path = await browser.stopHarRecording(
|
|
3696
|
+
const path = await browser.stopHarRecording(
|
|
3697
|
+
client,
|
|
3698
|
+
sessionId,
|
|
3699
|
+
ctx.args[1] as string,
|
|
3700
|
+
ctx.flags.bodies as boolean,
|
|
3701
|
+
);
|
|
3001
3702
|
if (ctx.flags.json) print(JSON.stringify({ data: { path } }));
|
|
3002
3703
|
else output.printSuccess(`HAR saved: ${path}`);
|
|
3003
3704
|
return { success: true, data: { path } };
|
|
@@ -3022,7 +3723,8 @@ const resizeCommand: Command = {
|
|
|
3022
3723
|
const browser = await getBrowser();
|
|
3023
3724
|
const width = parseInt(ctx.args[0] as string, 10);
|
|
3024
3725
|
const height = parseInt(ctx.args[1] as string, 10);
|
|
3025
|
-
if (isNaN(width) || isNaN(height))
|
|
3726
|
+
if (Number.isNaN(width) || Number.isNaN(height))
|
|
3727
|
+
throw new Error('Usage: monomind browse resize <width> <height>');
|
|
3026
3728
|
await browser.setViewport(client, sessionId, width, height);
|
|
3027
3729
|
output.printSuccess(`Resized to ${width}x${height}`);
|
|
3028
3730
|
return { success: true, data: { width, height } };
|
|
@@ -3169,7 +3871,10 @@ const browseCommand: Command = {
|
|
|
3169
3871
|
],
|
|
3170
3872
|
examples: [
|
|
3171
3873
|
{ command: 'monomind browse open https://example.com', description: 'Open a URL' },
|
|
3172
|
-
{
|
|
3874
|
+
{
|
|
3875
|
+
command: 'monomind browse snapshot -i',
|
|
3876
|
+
description: 'Interactive-only snapshot (93% token reduction)',
|
|
3877
|
+
},
|
|
3173
3878
|
{ command: 'monomind browse click @e3', description: 'Click element by ref' },
|
|
3174
3879
|
{ command: 'monomind browse fill @e1 "user@example.com"', description: 'Fill an input' },
|
|
3175
3880
|
{ command: 'monomind browse press Enter', description: 'Press Enter key' },
|
|
@@ -3183,7 +3888,10 @@ const browseCommand: Command = {
|
|
|
3183
3888
|
{ command: 'monomind browse state save my-session', description: 'Save session state' },
|
|
3184
3889
|
{ command: 'monomind browse navigate back', description: 'Navigate back' },
|
|
3185
3890
|
{ command: 'monomind browse eval "document.title"', description: 'Evaluate JavaScript' },
|
|
3186
|
-
{
|
|
3891
|
+
{
|
|
3892
|
+
command: 'monomind browse network route --pattern "https://api.*" --abort',
|
|
3893
|
+
description: 'Abort API calls',
|
|
3894
|
+
},
|
|
3187
3895
|
{ command: 'monomind browse close', description: 'Close browser session' },
|
|
3188
3896
|
],
|
|
3189
3897
|
action: async (_ctx: CommandContext): Promise<CommandResult> => {
|