@monoes/monobrowse 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/dist/package.json +75 -0
  2. package/dist/src/browser/adapters/index.d.ts.map +1 -1
  3. package/dist/src/browser/adapters/index.js +4 -0
  4. package/dist/src/browser/adapters/index.js.map +1 -1
  5. package/dist/src/browser/browser.js +2 -2
  6. package/dist/src/browser/browser.js.map +1 -1
  7. package/dist/src/browser/cdp.d.ts +6 -0
  8. package/dist/src/browser/cdp.d.ts.map +1 -1
  9. package/dist/src/browser/cdp.js +56 -1
  10. package/dist/src/browser/cdp.js.map +1 -1
  11. package/dist/src/browser/dashboard/api-types.d.ts +1 -1
  12. package/dist/src/browser/dashboard/server.d.ts +1 -1
  13. package/dist/src/browser/dashboard/server.d.ts.map +1 -1
  14. package/dist/src/browser/dashboard/server.js +204 -15
  15. package/dist/src/browser/dashboard/server.js.map +1 -1
  16. package/dist/src/browser/dashboard/ui.html +1811 -0
  17. package/dist/src/browser/playbook/browser-handlers.d.ts +3 -0
  18. package/dist/src/browser/playbook/browser-handlers.d.ts.map +1 -0
  19. package/dist/src/browser/playbook/browser-handlers.js +377 -0
  20. package/dist/src/browser/playbook/browser-handlers.js.map +1 -0
  21. package/dist/src/browser/playbook/builtin-handlers.d.ts +3 -0
  22. package/dist/src/browser/playbook/builtin-handlers.d.ts.map +1 -0
  23. package/dist/src/browser/playbook/builtin-handlers.js +364 -0
  24. package/dist/src/browser/playbook/builtin-handlers.js.map +1 -0
  25. package/dist/src/browser/playbook/index.d.ts +3 -0
  26. package/dist/src/browser/playbook/index.d.ts.map +1 -0
  27. package/dist/src/browser/playbook/index.js +3 -0
  28. package/dist/src/browser/playbook/index.js.map +1 -0
  29. package/dist/src/browser/playbook/store.d.ts +8 -0
  30. package/dist/src/browser/playbook/store.d.ts.map +1 -0
  31. package/dist/src/browser/playbook/store.js +87 -0
  32. package/dist/src/browser/playbook/store.js.map +1 -0
  33. package/dist/src/browser/screenshot.d.ts +2 -0
  34. package/dist/src/browser/screenshot.d.ts.map +1 -1
  35. package/dist/src/browser/screenshot.js +69 -2
  36. package/dist/src/browser/screenshot.js.map +1 -1
  37. package/dist/src/browser/types.d.ts.map +1 -1
  38. package/dist/src/browser/types.js +11 -0
  39. package/dist/src/browser/types.js.map +1 -1
  40. package/dist/src/cli/action.d.ts +3 -0
  41. package/dist/src/cli/action.d.ts.map +1 -0
  42. package/dist/src/cli/action.js +139 -0
  43. package/dist/src/cli/action.js.map +1 -0
  44. package/dist/src/cli/commands.d.ts +8 -0
  45. package/dist/src/cli/commands.d.ts.map +1 -0
  46. package/dist/src/cli/commands.js +3021 -0
  47. package/dist/src/cli/commands.js.map +1 -0
  48. package/dist/src/cli/output.d.ts +11 -0
  49. package/dist/src/cli/output.d.ts.map +1 -0
  50. package/dist/src/cli/output.js +33 -0
  51. package/dist/src/cli/output.js.map +1 -0
  52. package/dist/src/cli/platform.d.ts +7 -0
  53. package/dist/src/cli/platform.d.ts.map +1 -0
  54. package/dist/src/cli/platform.js +193 -0
  55. package/dist/src/cli/platform.js.map +1 -0
  56. package/dist/src/cli/playbook.d.ts +3 -0
  57. package/dist/src/cli/playbook.d.ts.map +1 -0
  58. package/dist/src/cli/playbook.js +265 -0
  59. package/dist/src/cli/playbook.js.map +1 -0
  60. package/dist/src/cli/types.d.ts +42 -0
  61. package/dist/src/cli/types.d.ts.map +1 -0
  62. package/dist/src/cli/types.js +2 -0
  63. package/dist/src/cli/types.js.map +1 -0
  64. package/dist/src/cli/workflow.d.ts +2 -0
  65. package/dist/src/cli/workflow.d.ts.map +1 -0
  66. package/dist/src/cli/workflow.js +3 -0
  67. package/dist/src/cli/workflow.js.map +1 -0
  68. package/dist/src/cli.d.ts +7 -0
  69. package/dist/src/cli.d.ts.map +1 -0
  70. package/dist/src/cli.js +187 -0
  71. package/dist/src/cli.js.map +1 -0
  72. package/dist/src/index.d.ts +5 -1
  73. package/dist/src/index.d.ts.map +1 -1
  74. package/dist/src/index.js +16 -1
  75. package/dist/src/index.js.map +1 -1
  76. package/dist/tsconfig.tsbuildinfo +1 -1
  77. package/package.json +24 -5
  78. package/src/browser/adapters/index.ts +4 -0
  79. package/src/browser/browser.ts +2 -2
  80. package/src/browser/cdp.ts +68 -1
  81. package/src/browser/dashboard/api-types.ts +1 -1
  82. package/src/browser/dashboard/server.ts +210 -17
  83. package/src/browser/dashboard/ui.html +1412 -45
  84. package/src/browser/playbook/browser-handlers.ts +449 -0
  85. package/src/browser/{workflow → playbook}/builtin-handlers.ts +35 -14
  86. package/src/browser/playbook/index.ts +2 -0
  87. package/src/browser/{workflow → playbook}/store.ts +14 -14
  88. package/src/browser/screenshot.ts +78 -3
  89. package/src/browser/types.ts +11 -0
  90. package/src/cli/action.ts +145 -0
  91. package/src/cli/commands.ts +2895 -0
  92. package/src/cli/output.ts +37 -0
  93. package/src/cli/platform.ts +211 -0
  94. package/src/cli/playbook.ts +280 -0
  95. package/src/cli/types.ts +47 -0
  96. package/src/cli.ts +208 -0
  97. package/src/index.ts +20 -1
  98. package/src/browser/workflow/engine.ts +0 -202
  99. package/src/browser/workflow/expression.ts +0 -107
  100. package/src/browser/workflow/index.ts +0 -5
  101. package/src/browser/workflow/types.ts +0 -62
@@ -0,0 +1,3021 @@
1
+ /**
2
+ * Browse Command — Native browser automation via Chrome DevTools Protocol
3
+ * Provides ref-based element model and token-efficient accessibility snapshots
4
+ */
5
+ import { output } from './output.js';
6
+ import { createPlaybookCommand } from './playbook.js';
7
+ import { createActionCommand } from './action.js';
8
+ import { createPlatformCommand } from './platform.js';
9
+ // Runtime state (single session per CLI process)
10
+ let _client = null;
11
+ let _sessionId = '';
12
+ let _targetId = '';
13
+ let _port = 9222;
14
+ let _refs = new Map();
15
+ async function getBrowser() {
16
+ return import('../index.js');
17
+ }
18
+ async function ensureConnected(port, targetId) {
19
+ const browser = await getBrowser();
20
+ if (!_client || !_client.isConnected()) {
21
+ if (_client && _sessionId) {
22
+ browser.teardownRouteInterception(_sessionId);
23
+ browser.stopRequestCapture(_sessionId);
24
+ browser.teardownDialogHandling(_sessionId);
25
+ browser.teardownConsoleCapture(_sessionId);
26
+ _client.close();
27
+ }
28
+ _port = await browser.launchBrowser({ port, headless: true });
29
+ const conn = await browser.connectToTarget(_port, targetId);
30
+ _client = conn.client;
31
+ _sessionId = conn.sessionId;
32
+ _targetId = conn.target.id;
33
+ _refs = new Map();
34
+ }
35
+ return { client: _client, sessionId: _sessionId, targetId: _targetId };
36
+ }
37
+ // URL patterns that signal an unambiguous login/auth wall.
38
+ // Excludes 'auth', 'oauth', 'sso', 'saml' — their callback/ACS/token paths
39
+ // (/auth/callback, /sso/callback, /saml/acs) are completion endpoints, not walls.
40
+ // DOM detection (password field, CAPTCHA widgets) handles SSO/SAML login pages.
41
+ const ATTENTION_URL_RE = /\/(login|log-in|signin|sign-in|captcha|mfa|2fa|account\/login|accounts\/login|session\/new|users\/sign_in)(?:[/?#]|$)/i;
42
+ async function detectAttentionNeeded(client, sessionId, url) {
43
+ if (ATTENTION_URL_RE.test(url))
44
+ return 'login';
45
+ try {
46
+ const hasPassword = await client.send('Runtime.evaluate', {
47
+ expression: '!!document.querySelector("input[type=password]")',
48
+ returnByValue: true,
49
+ }, sessionId).then(r => r.result?.value === true).catch(() => false);
50
+ if (hasPassword)
51
+ return 'login';
52
+ const hasCaptcha = await client.send('Runtime.evaluate', {
53
+ expression: '!!(document.querySelector("iframe[src*=recaptcha]") || document.querySelector("iframe[src*=hcaptcha]") || document.querySelector(".g-recaptcha") || document.querySelector(".h-captcha") || document.querySelector("[data-sitekey]"))',
54
+ returnByValue: true,
55
+ }, sessionId).then(r => r.result?.value === true).catch(() => false);
56
+ if (hasCaptcha)
57
+ return 'captcha';
58
+ }
59
+ catch { /* ignore CDP errors */ }
60
+ return null;
61
+ }
62
+ function waitForEnter() {
63
+ if (!process.stdin.isTTY) {
64
+ // Non-interactive context (MCP server, piped input, CI) — cannot safely read stdin.
65
+ // Auto-continue after a short grace period so automation isn't blocked.
66
+ output.printWarning('Non-interactive mode: auto-continuing in 30s. Switch to headed manually if needed.');
67
+ return new Promise(resolve => setTimeout(resolve, 30_000));
68
+ }
69
+ return new Promise(resolve => {
70
+ process.stdin.resume();
71
+ process.stdin.setEncoding('utf8');
72
+ const onData = () => { process.stdin.pause(); process.stdin.off('data', onData); resolve(); };
73
+ process.stdin.once('data', onData);
74
+ });
75
+ }
76
+ async function switchToHeaded(url, port) {
77
+ const browser = await getBrowser();
78
+ // Snapshot cookies from current headless session before closing
79
+ let savedCookies = [];
80
+ if (_client && _sessionId) {
81
+ try {
82
+ savedCookies = await browser.getCookies(_client, _sessionId);
83
+ }
84
+ catch { /* ignore */ }
85
+ browser.teardownRouteInterception(_sessionId);
86
+ browser.stopRequestCapture(_sessionId);
87
+ browser.teardownDialogHandling(_sessionId);
88
+ browser.teardownConsoleCapture(_sessionId);
89
+ _client.close();
90
+ _client = null;
91
+ _sessionId = '';
92
+ _targetId = '';
93
+ }
94
+ // Launch headed on a neighbouring port to avoid collision
95
+ const headedPort = port + 10;
96
+ try {
97
+ await browser.launchBrowser({ port: headedPort, headless: false });
98
+ }
99
+ catch (err) {
100
+ // Headed launch failed (no display, locked down env). Restore headless and rethrow.
101
+ _port = await browser.launchBrowser({ port, headless: true });
102
+ const fallback = await browser.connectToTarget(_port);
103
+ _client = fallback.client;
104
+ _sessionId = fallback.sessionId;
105
+ _targetId = fallback.target.id;
106
+ _refs = new Map();
107
+ throw new Error(`Cannot open headed browser: ${err instanceof Error ? err.message : String(err)}`);
108
+ }
109
+ const conn = await browser.connectToTarget(headedPort);
110
+ _client = conn.client;
111
+ _sessionId = conn.sessionId;
112
+ _targetId = conn.target.id;
113
+ _refs = new Map();
114
+ if (savedCookies.length) {
115
+ try {
116
+ await browser.setCookies(_client, _sessionId, savedCookies);
117
+ }
118
+ catch { /* ignore */ }
119
+ }
120
+ await browser.openUrl(_client, _sessionId, url);
121
+ output.printInfo('Browser window opened. Complete the required action (login / CAPTCHA), then press Enter to continue in headless mode...');
122
+ await waitForEnter();
123
+ // Capture post-auth cookies
124
+ const authCookies = await browser.getCookies(_client, _sessionId).catch(() => []);
125
+ const authLocalStorage = await browser.getLocalStorage(_client, _sessionId).catch(() => ({}));
126
+ // Close headed session
127
+ browser.teardownRouteInterception(_sessionId);
128
+ browser.stopRequestCapture(_sessionId);
129
+ browser.teardownDialogHandling(_sessionId);
130
+ browser.teardownConsoleCapture(_sessionId);
131
+ _client.close();
132
+ _client = null;
133
+ _sessionId = '';
134
+ _targetId = '';
135
+ // Relaunch headless and restore session
136
+ _port = await browser.launchBrowser({ port, headless: true });
137
+ const headlessConn = await browser.connectToTarget(_port);
138
+ _client = headlessConn.client;
139
+ _sessionId = headlessConn.sessionId;
140
+ _targetId = headlessConn.target.id;
141
+ _refs = new Map();
142
+ if (authCookies.length) {
143
+ try {
144
+ await browser.setCookies(_client, _sessionId, authCookies);
145
+ }
146
+ catch { /* ignore */ }
147
+ }
148
+ if (authLocalStorage && Object.keys(authLocalStorage).length) {
149
+ try {
150
+ await browser.setLocalStorage(_client, _sessionId, authLocalStorage);
151
+ }
152
+ catch { /* ignore */ }
153
+ }
154
+ }
155
+ function print(msg) {
156
+ process.stdout.write(msg + '\n');
157
+ }
158
+ // ---------------------------------------------------------------------------
159
+ // Sub-commands
160
+ // ---------------------------------------------------------------------------
161
+ const openCommand = {
162
+ name: 'open',
163
+ description: 'Open a URL in the browser. Usage: monomind browse open <url>',
164
+ options: [
165
+ { name: 'port', short: 'p', type: 'number', description: 'CDP port', default: 9222 },
166
+ { name: 'headed', type: 'boolean', description: 'Force visible browser window', default: false },
167
+ { name: 'session', short: 's', type: 'string', description: 'Session name to restore' },
168
+ { name: 'state', type: 'string', description: 'State file to load' },
169
+ ],
170
+ action: async (ctx) => {
171
+ const url = ctx.args[0];
172
+ if (!url)
173
+ throw new Error('URL required. Usage: monomind browse open <url>');
174
+ const port = ctx.flags.port ?? 9222;
175
+ const forceHeaded = ctx.flags.headed;
176
+ const browser = await getBrowser();
177
+ if (_client) {
178
+ const prevSid = _sessionId;
179
+ const prevClient = _client;
180
+ if (browser.getHarStatus(prevSid).recording) {
181
+ try {
182
+ await browser.stopHarRecording(prevClient, prevSid);
183
+ }
184
+ catch { /* ignore */ }
185
+ }
186
+ if (browser.getTraceStatus(prevSid)) {
187
+ try {
188
+ await browser.stopTrace(prevClient, prevSid);
189
+ }
190
+ catch { /* ignore */ }
191
+ }
192
+ if (browser.isProfilingActive(prevSid)) {
193
+ try {
194
+ await browser.stopCpuProfile(prevClient, prevSid);
195
+ }
196
+ catch { /* ignore */ }
197
+ }
198
+ browser.teardownRouteInterception(prevSid);
199
+ browser.stopRequestCapture(prevSid);
200
+ browser.teardownDialogHandling(prevSid);
201
+ browser.teardownConsoleCapture(prevSid);
202
+ prevClient.close();
203
+ _client = null;
204
+ _sessionId = '';
205
+ _targetId = '';
206
+ _refs = new Map();
207
+ }
208
+ _port = await browser.launchBrowser({ port, headless: !forceHeaded });
209
+ const conn = await browser.connectToTarget(_port);
210
+ _client = conn.client;
211
+ _sessionId = conn.sessionId;
212
+ _targetId = conn.target.id;
213
+ _refs = new Map();
214
+ if (ctx.flags.state && ctx.flags.session) {
215
+ output.printWarning('Both --state and --session provided; --state takes precedence');
216
+ }
217
+ if (ctx.flags.state) {
218
+ await browser.loadStateFile(_client, _sessionId, ctx.flags.state);
219
+ }
220
+ else if (ctx.flags.session) {
221
+ await browser.loadSession(_client, _sessionId, ctx.flags.session);
222
+ }
223
+ await browser.openUrl(_client, _sessionId, url);
224
+ const currentUrl = await browser.getCurrentUrl(_client, _sessionId);
225
+ // Auto-detect login/CAPTCHA walls and switch to headed if needed
226
+ if (!forceHeaded) {
227
+ const attentionType = await detectAttentionNeeded(_client, _sessionId, currentUrl);
228
+ if (attentionType) {
229
+ output.printWarning(`${attentionType === 'captcha' ? 'CAPTCHA' : 'Login'} detected — switching to headed mode`);
230
+ await switchToHeaded(currentUrl, port);
231
+ await browser.openUrl(_client, _sessionId, currentUrl);
232
+ output.printSuccess(`Resumed headless after ${attentionType === 'captcha' ? 'CAPTCHA' : 'login'}`);
233
+ }
234
+ }
235
+ const finalUrl = await browser.getCurrentUrl(_client, _sessionId);
236
+ const title = await browser.getCurrentTitle(_client, _sessionId);
237
+ output.printSuccess(`Opened: ${title} (${finalUrl})`);
238
+ return { success: true, data: { url: finalUrl, title } };
239
+ },
240
+ };
241
+ const snapshotCommand = {
242
+ name: 'snapshot',
243
+ description: 'Capture accessibility snapshot with ref-based element handles (@e1, @e2, ...)',
244
+ options: [
245
+ { name: 'interactive', short: 'i', type: 'boolean', description: 'Interactive elements only (93% token reduction)', default: false },
246
+ { name: 'compact', short: 'c', type: 'boolean', description: 'Compact output format', default: false },
247
+ { name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
248
+ { name: 'depth', short: 'd', type: 'number', description: 'Max depth of AX tree to show' },
249
+ { name: 'selector', short: 's', type: 'string', description: 'Scope snapshot to a CSS selector' },
250
+ { name: 'save', type: 'string', description: 'Save snapshot text to file (baseline for --diff)' },
251
+ { name: 'diff', type: 'string', description: 'Compare current snapshot against a saved baseline file' },
252
+ { name: 'content-boundaries', type: 'boolean', description: 'Wrap output in sentinel markers to prevent page-content injection attacks', default: false },
253
+ { name: 'max-output', type: 'number', description: 'Truncate output to N characters (prevents context window blowout on large pages)' },
254
+ ],
255
+ action: async (ctx) => {
256
+ const { client, sessionId } = await ensureConnected(_port);
257
+ const browser = await getBrowser();
258
+ const result = await browser.captureSnapshot(client, sessionId, {
259
+ interactiveOnly: ctx.flags.interactive,
260
+ compact: ctx.flags.compact,
261
+ maxDepth: ctx.flags.depth,
262
+ selector: ctx.flags.selector,
263
+ });
264
+ _refs = result.refs;
265
+ const applyOutputLimits = (text) => {
266
+ let out = text;
267
+ const maxOutput = ctx.flags['max-output'];
268
+ if (maxOutput && out.length > maxOutput) {
269
+ out = out.slice(0, maxOutput) + `\n[... truncated at ${maxOutput} chars]`;
270
+ }
271
+ if (ctx.flags['content-boundaries']) {
272
+ const nonce = Math.random().toString(36).slice(2, 10);
273
+ out = `MONOMIND_PAGE_CONTENT nonce=${nonce} origin=${result.url}\n${out}\nEND_MONOMIND_PAGE_CONTENT nonce=${nonce}`;
274
+ }
275
+ return out;
276
+ };
277
+ // --save: write snapshot text to baseline file
278
+ if (ctx.flags.save) {
279
+ const { writeFile, mkdir } = await import('fs/promises');
280
+ const { dirname } = await import('path');
281
+ const savePath = ctx.flags.save;
282
+ await mkdir(dirname(savePath), { recursive: true }).catch(() => { });
283
+ await writeFile(savePath, result.text, 'utf8');
284
+ output.printSuccess(`Snapshot saved to: ${savePath}`);
285
+ return { success: true, data: { path: savePath } };
286
+ }
287
+ // --diff: compare against baseline file
288
+ if (ctx.flags.diff) {
289
+ const { readFile } = await import('fs/promises');
290
+ const baselinePath = ctx.flags.diff;
291
+ let baseline;
292
+ try {
293
+ baseline = await readFile(baselinePath, 'utf8');
294
+ }
295
+ catch {
296
+ throw new Error(`Baseline not found: ${baselinePath}. Run snapshot --save first.`);
297
+ }
298
+ const currentLines = result.text.split('\n');
299
+ const baselineLines = baseline.split('\n');
300
+ const added = [], removed = [];
301
+ const baseSet = new Set(baselineLines);
302
+ const curSet = new Set(currentLines);
303
+ for (const l of currentLines)
304
+ if (!baseSet.has(l))
305
+ added.push(l);
306
+ for (const l of baselineLines)
307
+ if (!curSet.has(l))
308
+ removed.push(l);
309
+ const changed = added.length > 0 || removed.length > 0;
310
+ if (ctx.flags.json) {
311
+ print(JSON.stringify({ changed, additions: added.length, removals: removed.length, added, removed }));
312
+ }
313
+ else {
314
+ if (!changed) {
315
+ output.printSuccess('No snapshot changes detected');
316
+ }
317
+ else {
318
+ output.printWarning(`Snapshot changed: +${added.length} lines, -${removed.length} lines`);
319
+ for (const l of added)
320
+ print(`\x1b[32m+ ${l}\x1b[0m`);
321
+ for (const l of removed)
322
+ print(`\x1b[31m- ${l}\x1b[0m`);
323
+ }
324
+ }
325
+ return { success: true, data: { changed, additions: added.length, removals: removed.length } };
326
+ }
327
+ if (ctx.flags.json) {
328
+ const refsObj = Object.fromEntries([...result.refs.entries()].map(([k, v]) => [k, v]));
329
+ print(JSON.stringify({ url: result.url, title: result.title, refs: refsObj, snapshot: result.text }));
330
+ }
331
+ else {
332
+ print(`[${result.title}] ${result.url}\n`);
333
+ print(applyOutputLimits(result.text));
334
+ }
335
+ return { success: true, data: result };
336
+ },
337
+ };
338
+ const clickCommand = {
339
+ name: 'click',
340
+ description: 'Click an element by ref (@e1) or coordinates. Usage: monomind browse click @e1',
341
+ options: [
342
+ { name: 'right', type: 'boolean', description: 'Right-click', default: false },
343
+ { name: 'double', type: 'boolean', description: 'Double-click', default: false },
344
+ { name: 'x', type: 'number', description: 'X coordinate (for point click)' },
345
+ { name: 'y', type: 'number', description: 'Y coordinate (for point click)' },
346
+ ],
347
+ action: async (ctx) => {
348
+ const { client, sessionId } = await ensureConnected(_port);
349
+ const browser = await getBrowser();
350
+ const refArg = ctx.args[0];
351
+ if (!refArg && ctx.flags.x === undefined)
352
+ throw new Error('Ref (@e1) or --x/--y required');
353
+ if (ctx.flags.x !== undefined && ctx.flags.y !== undefined) {
354
+ await browser.clickPoint(client, sessionId, ctx.flags.x, ctx.flags.y);
355
+ output.printSuccess(`Clicked at (${ctx.flags.x}, ${ctx.flags.y})`);
356
+ return { success: true };
357
+ }
358
+ const refKey = refArg.startsWith('@') ? refArg.slice(1) : refArg;
359
+ const ref = await browser.resolveRef(client, sessionId, _refs, refKey);
360
+ await browser.clickElement(client, sessionId, ref, {
361
+ button: ctx.flags.right ? 'right' : 'left',
362
+ clickCount: ctx.flags.double ? 2 : 1,
363
+ });
364
+ output.printSuccess(`Clicked: ${ref.role} "${ref.name}"`);
365
+ return { success: true };
366
+ },
367
+ };
368
+ const fillCommand = {
369
+ name: 'fill',
370
+ description: 'Fill an input element. Usage: monomind browse fill @e1 "text value"',
371
+ action: async (ctx) => {
372
+ const { client, sessionId } = await ensureConnected(_port);
373
+ const browser = await getBrowser();
374
+ const refArg = ctx.args[0];
375
+ const value = ctx.args[1];
376
+ if (!refArg || value === undefined)
377
+ throw new Error('Usage: monomind browse fill @e1 "value"');
378
+ const refKey = refArg.startsWith('@') ? refArg.slice(1) : refArg;
379
+ const ref = await browser.resolveRef(client, sessionId, _refs, refKey);
380
+ await browser.fillElement(client, sessionId, ref, value);
381
+ output.printSuccess(`Filled: ${ref.role} "${ref.name}"`);
382
+ return { success: true };
383
+ },
384
+ };
385
+ const pressCommand = {
386
+ name: 'press',
387
+ description: 'Press a keyboard key. Usage: monomind browse press Enter',
388
+ action: async (ctx) => {
389
+ const { client, sessionId } = await ensureConnected(_port);
390
+ const browser = await getBrowser();
391
+ const key = ctx.args[0];
392
+ if (!key)
393
+ throw new Error('Key required. E.g.: monomind browse press Enter');
394
+ await browser.pressKey(client, sessionId, key);
395
+ output.printSuccess(`Pressed: ${key}`);
396
+ return { success: true };
397
+ },
398
+ };
399
+ const waitCommand = {
400
+ name: 'wait',
401
+ description: 'Wait for a condition to be met before proceeding',
402
+ options: [
403
+ { name: 'url', type: 'string', description: 'Wait for URL matching glob pattern' },
404
+ { name: 'text', type: 'string', description: 'Wait for text to appear in page' },
405
+ { name: 'not-text', type: 'string', description: 'Wait for text to disappear from page' },
406
+ { name: 'selector', type: 'string', description: 'Wait for CSS selector to appear' },
407
+ { name: 'load', type: 'string', description: 'Wait for load event: load|networkidle|domcontentloaded' },
408
+ { name: 'fn', type: 'string', description: 'Wait until JS expression returns truthy' },
409
+ { name: 'ms', type: 'number', description: 'Wait N milliseconds' },
410
+ { name: 'timeout', short: 't', type: 'number', description: 'Timeout in ms', default: 30000 },
411
+ { name: 'download', type: 'string', description: 'Wait for a file download to complete and save to path (monitors Browser.downloadProgress events)' },
412
+ ],
413
+ action: async (ctx) => {
414
+ const { client, sessionId } = await ensureConnected(_port);
415
+ const browser = await getBrowser();
416
+ if (ctx.flags.ms) {
417
+ const rawMs = ctx.flags.ms;
418
+ const waitMs = Number.isFinite(rawMs) ? Math.max(0, Math.min(rawMs, 60_000)) : 0; // cap at 60s
419
+ await new Promise((r) => setTimeout(r, waitMs));
420
+ output.printSuccess(`Waited ${ctx.flags.ms}ms`);
421
+ return { success: true };
422
+ }
423
+ if (ctx.flags.fn) {
424
+ const expr = ctx.flags.fn;
425
+ const rawTimeout = ctx.flags.timeout ?? 30000;
426
+ const timeout = Number.isFinite(rawTimeout) ? Math.max(100, Math.min(rawTimeout, 300_000)) : 30000; // cap at 5min
427
+ const interval = 200;
428
+ const deadline = Date.now() + timeout;
429
+ while (Date.now() < deadline) {
430
+ const result = await browser.evaluateJs(client, sessionId, expr);
431
+ if (result) {
432
+ output.printSuccess('Wait function returned truthy');
433
+ return { success: true };
434
+ }
435
+ await new Promise((r) => setTimeout(r, interval));
436
+ }
437
+ throw new Error(`Timeout waiting for --fn: ${expr}`);
438
+ }
439
+ if (ctx.flags['not-text']) {
440
+ const target = ctx.flags['not-text'];
441
+ const timeout = ctx.flags.timeout ?? 30000;
442
+ const interval = 200;
443
+ const deadline = Date.now() + timeout;
444
+ while (Date.now() < deadline) {
445
+ const text = await browser.evaluateJs(client, sessionId, 'document.body?.innerText ?? ""');
446
+ if (!text.includes(target)) {
447
+ output.printSuccess('Text disappeared');
448
+ return { success: true };
449
+ }
450
+ await new Promise((r) => setTimeout(r, interval));
451
+ }
452
+ throw new Error(`Timeout waiting for text to disappear: "${target}"`);
453
+ }
454
+ if (ctx.flags.download) {
455
+ const savePath = ctx.flags.download;
456
+ const { mkdir } = await import('fs/promises');
457
+ const { dirname, join } = await import('path');
458
+ const { tmpdir } = await import('os');
459
+ const downloadDir = join(tmpdir(), `mm-dl-wait-${Date.now()}`);
460
+ await mkdir(downloadDir, { recursive: true });
461
+ await client.send('Browser.setDownloadBehavior', {
462
+ behavior: 'allow', downloadPath: downloadDir, eventsEnabled: true,
463
+ }, undefined).catch(() => client.send('Page.setDownloadBehavior', { behavior: 'allow', downloadPath: downloadDir }, sessionId).catch(() => { }));
464
+ const MAX_DOWNLOAD_TIMEOUT = 5 * 60 * 1000; // I6: cap at 5 minutes
465
+ const rawTimeout = Math.min(ctx.flags.timeout ?? 30000, MAX_DOWNLOAD_TIMEOUT);
466
+ const finalPath = await new Promise((resolve, reject) => {
467
+ let guid = '';
468
+ // C2: capture off() functions to avoid listener leaks
469
+ const offBegin = client.on('Browser.downloadWillBegin', (params) => { guid = params.guid; });
470
+ let offProgress;
471
+ // cleanup defined before setTimeout so the timeout callback can call it
472
+ let tid;
473
+ const cleanup = () => { clearTimeout(tid); offBegin?.(); offProgress?.(); };
474
+ tid = setTimeout(() => { cleanup(); reject(new Error('Download timed out')); }, rawTimeout);
475
+ offProgress = client.on('Browser.downloadProgress', async (params) => {
476
+ if (params.guid === guid && params.state === 'completed') {
477
+ cleanup();
478
+ const { readdir, rename, rmdir } = await import('fs/promises');
479
+ const files = await readdir(downloadDir);
480
+ if (files.length > 0) {
481
+ const src = join(downloadDir, files[0]);
482
+ await mkdir(dirname(savePath), { recursive: true });
483
+ await rename(src, savePath);
484
+ await rmdir(downloadDir).catch(() => { }); // I1: cleanup temp dir
485
+ resolve(savePath);
486
+ }
487
+ else {
488
+ await rmdir(downloadDir).catch(() => { }); // I1: cleanup temp dir
489
+ reject(new Error('Download completed but no file found'));
490
+ }
491
+ }
492
+ else if (params.guid === guid && params.state === 'canceled') {
493
+ cleanup();
494
+ reject(new Error('Download was canceled'));
495
+ }
496
+ });
497
+ });
498
+ output.printSuccess(`Download saved: ${finalPath}`);
499
+ return { success: true, data: { path: finalPath } };
500
+ }
501
+ await browser.waitFor(client, sessionId, {
502
+ url: ctx.flags.url,
503
+ text: ctx.flags.text,
504
+ selector: ctx.flags.selector,
505
+ load: ctx.flags.load,
506
+ timeout: ctx.flags.timeout,
507
+ });
508
+ output.printSuccess('Wait condition met');
509
+ return { success: true };
510
+ },
511
+ };
512
+ const screenshotCommand = {
513
+ name: 'screenshot',
514
+ description: 'Capture a screenshot. Usage: monomind browse screenshot [path] [--annotate] [--hide-scrollbars]',
515
+ options: [
516
+ { name: 'full', type: 'boolean', description: 'Full page screenshot', default: false },
517
+ { name: 'format', type: 'string', description: 'Format: png|jpeg|webp', default: 'png' },
518
+ { name: 'quality', type: 'number', description: 'Quality 0-100 for jpeg/webp', default: 80 },
519
+ { name: 'annotate', type: 'boolean', description: 'Overlay numbered labels keyed to @eN refs from last snapshot (viewport-only; do not combine with --full)', default: false },
520
+ { name: 'hide-scrollbars', type: 'boolean', description: 'Hide native scrollbars via CSS injection before capture', default: false },
521
+ { name: 'json', type: 'boolean', description: 'Output JSON with path', default: false },
522
+ ],
523
+ action: async (ctx) => {
524
+ const { client, sessionId } = await ensureConnected(_port);
525
+ const browser = await getBrowser();
526
+ const hideScrollbars = ctx.flags['hide-scrollbars'];
527
+ if (hideScrollbars) {
528
+ await client.send('Runtime.evaluate', {
529
+ 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);})()`,
530
+ returnByValue: false,
531
+ }, sessionId).catch(() => { });
532
+ }
533
+ const annotate = ctx.flags.annotate;
534
+ // eslint-disable-next-line prefer-const
535
+ let result;
536
+ try {
537
+ result = await browser.captureScreenshot(client, sessionId, {
538
+ path: ctx.args[0],
539
+ fullPage: ctx.flags.full,
540
+ format: ctx.flags.format,
541
+ quality: ctx.flags.quality,
542
+ annotate,
543
+ refs: annotate ? _refs : undefined,
544
+ });
545
+ }
546
+ finally {
547
+ if (hideScrollbars) {
548
+ await client.send('Runtime.evaluate', {
549
+ expression: `(function(){var s=document.getElementById('__mm_noscroll__');if(s)s.remove();})()`,
550
+ returnByValue: false,
551
+ }, sessionId).catch(() => { });
552
+ }
553
+ }
554
+ if (ctx.flags.json) {
555
+ print(JSON.stringify({ data: { path: result.path } }));
556
+ }
557
+ else {
558
+ output.printSuccess(`Screenshot saved: ${result.path}`);
559
+ }
560
+ return { success: true, data: result };
561
+ },
562
+ };
563
+ const getCommand = {
564
+ name: 'get',
565
+ description: 'Get page info. Usage: monomind browse get url|title|text|html|value|attr|count|box|styles [@ref] [attrName]',
566
+ options: [
567
+ { name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
568
+ ],
569
+ action: async (ctx) => {
570
+ const { client, sessionId } = await ensureConnected(_port);
571
+ const browser = await getBrowser();
572
+ const what = ctx.args[0];
573
+ if (!what)
574
+ throw new Error('Usage: monomind browse get url|title|text|html|value|attr|count|box|styles');
575
+ let value;
576
+ switch (what) {
577
+ case 'url':
578
+ value = await browser.getCurrentUrl(client, sessionId);
579
+ break;
580
+ case 'title':
581
+ value = await browser.getCurrentTitle(client, sessionId);
582
+ break;
583
+ case 'text': {
584
+ const refArg = ctx.args[1];
585
+ if (refArg) {
586
+ const refKey = refArg.startsWith('@') ? refArg.slice(1) : refArg;
587
+ const ref = _refs.get(refKey);
588
+ if (!ref)
589
+ throw new Error(`Ref @${refKey} not found`);
590
+ const objectId = await browser.getObjectIdForRef(client, sessionId, ref);
591
+ if (!objectId)
592
+ throw new Error('Element not in DOM');
593
+ const result = await client.send('Runtime.callFunctionOn', {
594
+ functionDeclaration: 'function() { return this.innerText || this.textContent || ""; }',
595
+ objectId,
596
+ returnByValue: true,
597
+ }, sessionId);
598
+ value = result.result?.value ?? '';
599
+ }
600
+ else {
601
+ value = (await browser.evaluateJs(client, sessionId, 'document.body?.innerText ?? ""'));
602
+ }
603
+ break;
604
+ }
605
+ case 'html':
606
+ value = (await browser.evaluateJs(client, sessionId, 'document.documentElement.outerHTML'));
607
+ break;
608
+ case 'value': {
609
+ const refArg = ctx.args[1];
610
+ if (!refArg)
611
+ throw new Error('Usage: monomind browse get value @ref');
612
+ const refKey = refArg.startsWith('@') ? refArg.slice(1) : refArg;
613
+ const ref = _refs.get(refKey);
614
+ if (!ref)
615
+ throw new Error(`Ref @${refKey} not found`);
616
+ const objectId = await browser.getObjectIdForRef(client, sessionId, ref);
617
+ if (!objectId)
618
+ throw new Error('Element not in DOM');
619
+ const r = await client.send('Runtime.callFunctionOn', {
620
+ functionDeclaration: 'function() { return this.value ?? null; }',
621
+ objectId, returnByValue: true,
622
+ }, sessionId);
623
+ value = r.result?.value ?? null;
624
+ break;
625
+ }
626
+ case 'attr': {
627
+ const refArg = ctx.args[1];
628
+ const attrName = ctx.args[2];
629
+ if (!refArg || !attrName)
630
+ throw new Error('Usage: monomind browse get attr @ref <attrName>');
631
+ const refKey = refArg.startsWith('@') ? refArg.slice(1) : refArg;
632
+ const ref = _refs.get(refKey);
633
+ if (!ref)
634
+ throw new Error(`Ref @${refKey} not found`);
635
+ const objectId = await browser.getObjectIdForRef(client, sessionId, ref);
636
+ if (!objectId)
637
+ throw new Error('Element not in DOM');
638
+ const r = await client.send('Runtime.callFunctionOn', {
639
+ functionDeclaration: `function() { return this.getAttribute(${JSON.stringify(attrName)}); }`,
640
+ objectId, returnByValue: true,
641
+ }, sessionId);
642
+ value = r.result?.value ?? null;
643
+ break;
644
+ }
645
+ case 'count': {
646
+ const selector = ctx.args[1];
647
+ if (!selector)
648
+ throw new Error('Usage: monomind browse get count <cssSelector>');
649
+ value = await browser.evaluateJs(client, sessionId, `document.querySelectorAll(${JSON.stringify(selector)}).length`);
650
+ break;
651
+ }
652
+ case 'box': {
653
+ const refArg = ctx.args[1];
654
+ if (!refArg)
655
+ throw new Error('Usage: monomind browse get box @ref');
656
+ const refKey = refArg.startsWith('@') ? refArg.slice(1) : refArg;
657
+ const ref = _refs.get(refKey);
658
+ if (!ref)
659
+ throw new Error(`Ref @${refKey} not found`);
660
+ value = await browser.getElementBox(client, sessionId, ref);
661
+ break;
662
+ }
663
+ case 'styles': {
664
+ const refArg = ctx.args[1];
665
+ if (!refArg)
666
+ throw new Error('Usage: monomind browse get styles @ref');
667
+ const refKey = refArg.startsWith('@') ? refArg.slice(1) : refArg;
668
+ const ref = _refs.get(refKey);
669
+ if (!ref)
670
+ throw new Error(`Ref @${refKey} not found`);
671
+ const objectId = await browser.getObjectIdForRef(client, sessionId, ref);
672
+ if (!objectId)
673
+ throw new Error('Element not in DOM');
674
+ const r = await client.send('Runtime.callFunctionOn', {
675
+ functionDeclaration: 'function() { const s = window.getComputedStyle(this); return JSON.stringify(Object.fromEntries([...s].map(k => [k, s.getPropertyValue(k)]))); }',
676
+ objectId, returnByValue: true,
677
+ }, sessionId);
678
+ try {
679
+ value = JSON.parse(r.result?.value ?? '{}');
680
+ }
681
+ catch {
682
+ value = {};
683
+ }
684
+ break;
685
+ }
686
+ default:
687
+ throw new Error(`Unknown: ${what}. Use: url|title|text|html|value|attr|count|box|styles`);
688
+ }
689
+ if (ctx.flags.json) {
690
+ print(JSON.stringify({ data: { [what]: value } }));
691
+ }
692
+ else {
693
+ print(typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value ?? ''));
694
+ }
695
+ return { success: true, data: { [what]: value } };
696
+ },
697
+ };
698
+ const scrollCommand = {
699
+ name: 'scroll',
700
+ description: 'Scroll the page. Usage: monomind browse scroll up|down|left|right [amount] [--selector ".sidebar"]',
701
+ options: [
702
+ { name: 'amount', short: 'a', type: 'number', description: 'Pixels to scroll', default: 300 },
703
+ { name: 'ref', type: 'string', description: 'Element ref to scroll within' },
704
+ { name: 'selector', short: 's', type: 'string', description: 'CSS selector of element to scroll within' },
705
+ ],
706
+ action: async (ctx) => {
707
+ const { client, sessionId } = await ensureConnected(_port);
708
+ const browser = await getBrowser();
709
+ const direction = ctx.args[0];
710
+ if (!direction)
711
+ throw new Error('Usage: monomind browse scroll up|down|left|right [amount]');
712
+ // Support positional amount: scroll down 300
713
+ const positionalAmount = ctx.args[1] !== undefined ? parseInt(ctx.args[1], 10) : undefined;
714
+ const amount = (positionalAmount && Number.isFinite(positionalAmount)) ? positionalAmount : ctx.flags.amount ?? 300;
715
+ if (ctx.flags.selector) {
716
+ const sel = ctx.flags.selector;
717
+ const dx = direction === 'right' ? amount : direction === 'left' ? -amount : 0;
718
+ const dy = direction === 'down' ? amount : direction === 'up' ? -amount : 0;
719
+ const posJson = await browser.evaluateJs(client, sessionId, `(function(){var el=document.querySelector(${JSON.stringify(sel)});if(!el)return null;var r=el.getBoundingClientRect();return JSON.stringify({x:r.left+r.width/2,y:r.top+r.height/2});})()`);
720
+ if (!posJson)
721
+ throw new Error(`Selector not found: ${sel}`);
722
+ const pos = JSON.parse(posJson);
723
+ await client.send('Input.dispatchMouseEvent', { type: 'mouseWheel', x: pos.x, y: pos.y, deltaX: dx, deltaY: dy }, sessionId);
724
+ output.printSuccess(`Scrolled ${direction} in ${sel}`);
725
+ return { success: true };
726
+ }
727
+ let ref;
728
+ if (ctx.flags.ref) {
729
+ const refKey = ctx.flags.ref.startsWith('@')
730
+ ? ctx.flags.ref.slice(1)
731
+ : ctx.flags.ref;
732
+ ref = _refs.get(refKey);
733
+ }
734
+ await browser.scrollElement(client, sessionId, direction, amount, ref);
735
+ output.printSuccess(`Scrolled ${direction}`);
736
+ return { success: true };
737
+ },
738
+ };
739
+ const navigateCommand = {
740
+ name: 'navigate',
741
+ description: 'Navigate browser history. Usage: monomind browse navigate back|forward|reload',
742
+ action: async (ctx) => {
743
+ const { client, sessionId } = await ensureConnected(_port);
744
+ const browser = await getBrowser();
745
+ const direction = ctx.args[0];
746
+ if (!direction)
747
+ throw new Error('Usage: monomind browse navigate back|forward|reload');
748
+ if (direction === 'back' || direction === 'forward') {
749
+ // Pre-register frame-start listener BEFORE JS navigation to avoid the race
750
+ // where history.back/forward() returns before the browser issues any requests
751
+ let offFrameStarted = () => { };
752
+ const frameStartedPromise = new Promise((resolve) => {
753
+ offFrameStarted = client.on('Page.frameStartedLoading', (_params, sid) => {
754
+ if (sid === sessionId) {
755
+ const off = offFrameStarted;
756
+ offFrameStarted = () => { };
757
+ off();
758
+ resolve();
759
+ }
760
+ });
761
+ });
762
+ try {
763
+ await client.send('Runtime.evaluate', {
764
+ expression: direction === 'back' ? 'history.back()' : 'history.forward()',
765
+ }, sessionId);
766
+ let fallbackHandle;
767
+ const fallbackPromise = new Promise((r) => { fallbackHandle = setTimeout(r, 2000); });
768
+ await Promise.race([frameStartedPromise, fallbackPromise]);
769
+ if (fallbackHandle !== undefined)
770
+ clearTimeout(fallbackHandle);
771
+ }
772
+ finally {
773
+ offFrameStarted();
774
+ }
775
+ await browser.waitForLoad(client, sessionId, 'networkidle');
776
+ }
777
+ else if (direction === 'reload') {
778
+ await client.send('Page.reload', {}, sessionId);
779
+ await browser.waitForLoad(client, sessionId, 'load');
780
+ }
781
+ else {
782
+ throw new Error(`Unknown direction: ${direction}. Use: back|forward|reload`);
783
+ }
784
+ output.printSuccess(`Navigated: ${direction}`);
785
+ return { success: true };
786
+ },
787
+ };
788
+ const setCommand = {
789
+ name: 'set',
790
+ description: 'Configure browser settings. Usage: monomind browse set viewport|device|geo|offline|media|credentials|useragent <args>',
791
+ action: async (ctx) => {
792
+ const { client, sessionId } = await ensureConnected(_port);
793
+ const browser = await getBrowser();
794
+ const setting = ctx.args[0];
795
+ if (!setting)
796
+ throw new Error('Usage: monomind browse set viewport|device|geo|offline|media|credentials|useragent <args>');
797
+ switch (setting) {
798
+ case 'viewport': {
799
+ const width = parseInt(ctx.args[1], 10);
800
+ const height = parseInt(ctx.args[2], 10);
801
+ const dpr = parseFloat(ctx.args[3]) || undefined;
802
+ if (isNaN(width) || isNaN(height))
803
+ throw new Error('Usage: set viewport <width> <height> [dpr]');
804
+ await client.send('Emulation.setDeviceMetricsOverride', {
805
+ width, height, deviceScaleFactor: dpr ?? 1, mobile: false,
806
+ }, sessionId);
807
+ output.printSuccess(`Viewport set to ${width}x${height}${dpr ? ` @${dpr}x` : ''}`);
808
+ break;
809
+ }
810
+ case 'device': {
811
+ const deviceName = ctx.args[1];
812
+ if (!deviceName)
813
+ throw new Error(`Usage: set device <name>. Available: ${browser.listDevices().join(', ')}`);
814
+ await browser.emulateDevice(client, sessionId, deviceName);
815
+ output.printSuccess(`Emulating device: ${deviceName}`);
816
+ break;
817
+ }
818
+ case 'geo': {
819
+ const lat = parseFloat(ctx.args[1]);
820
+ const lon = parseFloat(ctx.args[2]);
821
+ const acc = parseFloat(ctx.args[3]) || 100;
822
+ if (isNaN(lat) || isNaN(lon))
823
+ throw new Error('Usage: set geo <latitude> <longitude> [accuracy]');
824
+ await browser.setGeolocation(client, sessionId, lat, lon, acc);
825
+ output.printSuccess(`Geolocation set: ${lat}, ${lon}`);
826
+ break;
827
+ }
828
+ case 'offline': {
829
+ const offlineArg = ctx.args[1];
830
+ if (offlineArg === undefined)
831
+ throw new Error('Usage: set offline <true|false>');
832
+ const enabled = offlineArg === 'true';
833
+ await browser.setOfflineMode(client, sessionId, enabled);
834
+ output.printSuccess(`Offline mode: ${enabled}`);
835
+ break;
836
+ }
837
+ case 'media': {
838
+ const scheme = ctx.args[1];
839
+ if (!scheme)
840
+ throw new Error('Usage: set media dark|light|no-preference');
841
+ await browser.setColorScheme(client, sessionId, scheme);
842
+ output.printSuccess(`Color scheme: ${scheme}`);
843
+ break;
844
+ }
845
+ case 'credentials': {
846
+ const username = ctx.args[1];
847
+ const password = ctx.args[2];
848
+ if (!username || !password)
849
+ throw new Error('Usage: set credentials <username> <password>');
850
+ await browser.setBasicAuth(client, sessionId, username, password);
851
+ output.printSuccess('Basic auth credentials set');
852
+ break;
853
+ }
854
+ case 'useragent': {
855
+ const ua = ctx.args[1];
856
+ if (!ua)
857
+ throw new Error('Usage: set useragent "<user-agent-string>"');
858
+ await browser.setUserAgent(client, sessionId, ua);
859
+ output.printSuccess('User agent set');
860
+ break;
861
+ }
862
+ default:
863
+ throw new Error(`Unknown setting: ${setting}. Use: viewport|device|geo|offline|media|credentials|useragent`);
864
+ }
865
+ return { success: true };
866
+ },
867
+ };
868
+ const stateCommand = {
869
+ name: 'state',
870
+ description: 'Manage browser session state. Usage: monomind browse state save|load|list|rename|clean [name]',
871
+ options: [
872
+ { name: 'older-than', type: 'number', description: 'For state clean: remove sessions older than N days' },
873
+ ],
874
+ action: async (ctx) => {
875
+ const browser = await getBrowser();
876
+ const action = ctx.args[0];
877
+ if (!action)
878
+ throw new Error('Usage: monomind browse state save|load|list [name]');
879
+ switch (action) {
880
+ case 'list': {
881
+ const sessions = await browser.listSessions();
882
+ if (sessions.length === 0) {
883
+ output.printInfo('No saved sessions');
884
+ }
885
+ else {
886
+ output.printInfo('Saved sessions:');
887
+ for (const s of sessions)
888
+ print(` ${s}`);
889
+ }
890
+ return { success: true, data: { sessions } };
891
+ }
892
+ case 'save': {
893
+ const { client, sessionId } = await ensureConnected(_port);
894
+ const target = ctx.args[1];
895
+ if (!target)
896
+ throw new Error('Usage: monomind browse state save <name-or-file>');
897
+ const url = await browser.getCurrentUrl(client, sessionId);
898
+ const title = await browser.getCurrentTitle(client, sessionId);
899
+ if (target.endsWith('.json')) {
900
+ await browser.saveStateFile(client, sessionId, _targetId, target, url, title);
901
+ output.printSuccess(`State saved to ${target}`);
902
+ }
903
+ else {
904
+ const path = await browser.saveSession(client, sessionId, _targetId, target, url, title);
905
+ output.printSuccess(`Session "${target}" saved to ${path}`);
906
+ }
907
+ return { success: true };
908
+ }
909
+ case 'load': {
910
+ const { client, sessionId } = await ensureConnected(_port);
911
+ const target = ctx.args[1];
912
+ if (!target)
913
+ throw new Error('Usage: monomind browse state load <name-or-file>');
914
+ if (target.endsWith('.json')) {
915
+ await browser.loadStateFile(client, sessionId, target);
916
+ }
917
+ else {
918
+ await browser.loadSession(client, sessionId, target);
919
+ }
920
+ output.printSuccess(`State loaded from ${target}`);
921
+ return { success: true };
922
+ }
923
+ case 'show': {
924
+ const { client: c, sessionId: sid } = await ensureConnected(_port);
925
+ const url = await browser.getCurrentUrl(c, sid);
926
+ const title = await browser.getCurrentTitle(c, sid);
927
+ const cookies = await browser.getCookies(c, sid);
928
+ const ls = await browser.getAllLocalStorage(c, sid);
929
+ const info = { url, title, cookies: cookies.length, localStorage: Object.keys(ls).length, refs: _refs.size };
930
+ print(JSON.stringify(info, null, 2));
931
+ return { success: true, data: info };
932
+ }
933
+ case 'clear': {
934
+ const { client: c, sessionId: sid } = await ensureConnected(_port);
935
+ await browser.clearCookies(c, sid);
936
+ await browser.clearLocalStorage(c, sid);
937
+ await browser.clearSessionStorage(c, sid);
938
+ _refs = new Map();
939
+ output.printSuccess('Browser state cleared (cookies, localStorage, sessionStorage, refs)');
940
+ return { success: true };
941
+ }
942
+ case 'rename': {
943
+ const oldName = ctx.args[1];
944
+ const newName = ctx.args[2];
945
+ if (!oldName || !newName)
946
+ throw new Error('Usage: monomind browse state rename <old-name> <new-name>');
947
+ const sessions = await browser.listSessions();
948
+ if (!sessions.includes(oldName))
949
+ throw new Error(`Session not found: ${oldName}`);
950
+ // W1: validate newName to prevent path traversal
951
+ const { basename: basenameFn } = await import('path');
952
+ const safeName = basenameFn(newName);
953
+ if (safeName !== newName || safeName.startsWith('.') || safeName.includes('/')) {
954
+ throw new Error('Invalid session name — must not contain path separators or start with "."');
955
+ }
956
+ const { unlink: unlinkRename, readFile, writeFile, mkdir: mkdirRename } = await import('fs/promises');
957
+ const { join: joinR } = await import('path');
958
+ const { homedir } = await import('os');
959
+ const sessionDir = joinR(homedir(), '.monomind', 'browser-sessions');
960
+ const oldPath = joinR(sessionDir, `${oldName}.json`);
961
+ const newPath = joinR(sessionDir, `${newName}.json`);
962
+ const data = JSON.parse(await readFile(oldPath, 'utf8'));
963
+ data.name = newName;
964
+ await mkdirRename(sessionDir, { recursive: true });
965
+ await writeFile(newPath, JSON.stringify(data, null, 2), 'utf8');
966
+ await unlinkRename(oldPath).catch(() => { }); // C1: delete old file (not rename to /dev/null)
967
+ output.printSuccess(`Session renamed: ${oldName} → ${newName}`);
968
+ return { success: true };
969
+ }
970
+ case 'clean': {
971
+ const days = ctx.flags['older-than'] ?? 7;
972
+ const { unlink, stat } = await import('fs/promises');
973
+ const { join: joinC } = await import('path');
974
+ const { homedir: homedirC } = await import('os');
975
+ const sessionDir = joinC(homedirC(), '.monomind', 'browser-sessions');
976
+ const sessions = await browser.listSessions();
977
+ const cutoff = Date.now() - days * 86400 * 1000;
978
+ let removed = 0;
979
+ for (const name of sessions) {
980
+ const p = joinC(sessionDir, `${name}.json`);
981
+ const s = await stat(p).catch(() => null);
982
+ if (s && s.mtimeMs < cutoff) {
983
+ await unlink(p).catch(() => { });
984
+ removed++;
985
+ }
986
+ }
987
+ output.printSuccess(`Cleaned ${removed} session(s) older than ${days} days`);
988
+ return { success: true, data: { removed } };
989
+ }
990
+ default:
991
+ throw new Error(`Unknown action: ${action}. Use: save|load|list|show|clear|rename|clean`);
992
+ }
993
+ },
994
+ };
995
+ const networkCommand = {
996
+ name: 'network',
997
+ description: 'Network interception and cookie management',
998
+ options: [
999
+ { name: 'pattern', type: 'string', description: 'URL pattern for route (glob)' },
1000
+ { name: 'abort', type: 'boolean', description: 'Abort matching requests' },
1001
+ { name: 'fulfill', type: 'string', description: 'JSON response body' },
1002
+ { name: 'status', type: 'number', description: 'HTTP status for fulfill', default: 200 },
1003
+ { name: 'headers', type: 'string', description: 'JSON headers object' },
1004
+ { name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
1005
+ { name: 'filter', type: 'string', description: 'Filter requests by URL substring (for network requests)' },
1006
+ { name: 'method', type: 'string', description: 'Filter by HTTP method, e.g. GET, POST (for network requests)' },
1007
+ { name: 'status-code', type: 'number', description: 'Filter by HTTP status code (for network requests)' },
1008
+ { name: 'type', type: 'string', description: 'Filter by resource type: xhr|fetch|document|script|stylesheet|image (for network requests)' },
1009
+ ],
1010
+ action: async (ctx) => {
1011
+ const { client, sessionId } = await ensureConnected(_port);
1012
+ const browser = await getBrowser();
1013
+ const action = ctx.args[0];
1014
+ if (!action)
1015
+ throw new Error('Usage: monomind browse network route|unroute|cookies|headers|requests');
1016
+ switch (action) {
1017
+ case 'route': {
1018
+ const pattern = ctx.flags.pattern;
1019
+ if (!pattern)
1020
+ throw new Error('--pattern required for network route');
1021
+ const routes = [{
1022
+ pattern,
1023
+ action: ctx.flags.abort ? 'abort' : ctx.flags.fulfill ? 'fulfill' : 'continue',
1024
+ response: ctx.flags.fulfill ? {
1025
+ status: ctx.flags.status,
1026
+ body: ctx.flags.fulfill,
1027
+ headers: ctx.flags.headers ? JSON.parse(ctx.flags.headers) : {},
1028
+ } : undefined,
1029
+ }];
1030
+ await browser.setupRoutes(client, sessionId, routes);
1031
+ output.printSuccess(`Network route set: ${pattern}`);
1032
+ break;
1033
+ }
1034
+ case 'unroute':
1035
+ await browser.disableInterception(client, sessionId);
1036
+ output.printSuccess('Network interception disabled');
1037
+ break;
1038
+ case 'cookies': {
1039
+ const cookies = await browser.getCookies(client, sessionId);
1040
+ print(JSON.stringify(cookies, null, 2));
1041
+ return { success: true, data: { cookies } };
1042
+ }
1043
+ case 'headers': {
1044
+ const headers = ctx.flags.headers;
1045
+ if (!headers)
1046
+ throw new Error('--headers required (JSON string)');
1047
+ await browser.setExtraHeaders(client, sessionId, JSON.parse(headers));
1048
+ output.printSuccess('Extra headers set');
1049
+ break;
1050
+ }
1051
+ case 'capture': {
1052
+ const subAction = ctx.args[1] ?? 'start';
1053
+ if (subAction === 'start') {
1054
+ browser.startRequestCapture(client, sessionId);
1055
+ output.printSuccess('Request capture started');
1056
+ }
1057
+ else if (subAction === 'stop') {
1058
+ browser.stopRequestCapture(sessionId);
1059
+ output.printSuccess('Request capture stopped');
1060
+ }
1061
+ else if (subAction === 'clear') {
1062
+ browser.clearCapturedRequests(sessionId);
1063
+ output.printSuccess('Captured requests cleared');
1064
+ }
1065
+ break;
1066
+ }
1067
+ case 'requests': {
1068
+ let reqs = browser.getCapturedRequests(sessionId);
1069
+ const filterUrl = ctx.flags.filter;
1070
+ const filterMethod = ctx.flags.method;
1071
+ const filterStatus = ctx.flags['status-code'];
1072
+ const filterType = ctx.flags.type;
1073
+ if (filterUrl)
1074
+ reqs = reqs.filter((r) => r.url.includes(filterUrl));
1075
+ if (filterMethod)
1076
+ reqs = reqs.filter((r) => (r.method ?? 'GET').toUpperCase() === filterMethod.toUpperCase());
1077
+ if (filterStatus)
1078
+ reqs = reqs.filter((r) => r.status === filterStatus);
1079
+ if (filterType)
1080
+ reqs = reqs.filter((r) => r.resourceType === filterType || r.type === filterType);
1081
+ if (ctx.flags.json)
1082
+ print(JSON.stringify({ data: reqs }));
1083
+ else {
1084
+ if (reqs.length === 0) {
1085
+ output.printInfo('No captured requests. Run: network capture start');
1086
+ }
1087
+ else
1088
+ for (const r of reqs)
1089
+ print(` ${r.method ?? 'GET'} ${r.status ?? '-'} ${r.url}`);
1090
+ }
1091
+ return { success: true, data: { requests: reqs } };
1092
+ }
1093
+ case 'request': {
1094
+ const reqId = ctx.args[1];
1095
+ if (!reqId)
1096
+ throw new Error('Usage: monomind browse network request <requestId>');
1097
+ const reqs = browser.getCapturedRequests(sessionId);
1098
+ const req = reqs.find((r) => r.requestId === reqId || r.id === reqId);
1099
+ if (!req) {
1100
+ output.printWarning(`Request not found: ${reqId}`);
1101
+ return { success: false };
1102
+ }
1103
+ if (ctx.flags.json)
1104
+ print(JSON.stringify({ data: req }));
1105
+ else
1106
+ print(JSON.stringify(req, null, 2));
1107
+ return { success: true, data: { request: req } };
1108
+ }
1109
+ default:
1110
+ throw new Error(`Unknown: ${action}. Use: route|unroute|cookies|headers|capture|requests|request`);
1111
+ }
1112
+ return { success: true };
1113
+ },
1114
+ };
1115
+ const evalCommand = {
1116
+ name: 'eval',
1117
+ description: 'Evaluate JavaScript in page context. Usage: monomind browse eval "document.title"',
1118
+ options: [
1119
+ { name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
1120
+ { name: 'stdin', type: 'boolean', description: 'Read JS expression from stdin (heredoc-friendly for multiline scripts)', default: false },
1121
+ ],
1122
+ action: async (ctx) => {
1123
+ const { client, sessionId } = await ensureConnected(_port);
1124
+ const browser = await getBrowser();
1125
+ let expr = ctx.args[0];
1126
+ if (ctx.flags.stdin) {
1127
+ const chunks = [];
1128
+ for await (const chunk of process.stdin)
1129
+ chunks.push(chunk);
1130
+ expr = Buffer.concat(chunks).toString('utf8').trim();
1131
+ }
1132
+ if (!expr)
1133
+ throw new Error('Usage: monomind browse eval "<expression>" (or pipe with --stdin)');
1134
+ const result = await browser.evaluateJs(client, sessionId, expr);
1135
+ if (ctx.flags.json) {
1136
+ print(JSON.stringify({ data: result }));
1137
+ }
1138
+ else {
1139
+ print(String(result ?? ''));
1140
+ }
1141
+ return { success: true, data: { result } };
1142
+ },
1143
+ };
1144
+ const closeCommand = {
1145
+ name: 'close',
1146
+ description: 'Close the active browser session',
1147
+ action: async (_ctx) => {
1148
+ if (_client) {
1149
+ const browser = await getBrowser();
1150
+ const sid = _sessionId;
1151
+ const client = _client;
1152
+ // Tear down per-session Maps and listeners before closing
1153
+ if (browser.getHarStatus(sid).recording) {
1154
+ try {
1155
+ await browser.stopHarRecording(client, sid);
1156
+ }
1157
+ catch { /* ignore */ }
1158
+ }
1159
+ if (browser.getTraceStatus(sid)) {
1160
+ try {
1161
+ await browser.stopTrace(client, sid);
1162
+ }
1163
+ catch { /* ignore */ }
1164
+ }
1165
+ if (browser.isProfilingActive(sid)) {
1166
+ try {
1167
+ await browser.stopCpuProfile(client, sid);
1168
+ }
1169
+ catch { /* ignore */ }
1170
+ }
1171
+ browser.teardownRouteInterception(sid);
1172
+ browser.stopRequestCapture(sid);
1173
+ browser.teardownDialogHandling(sid);
1174
+ browser.teardownConsoleCapture(sid);
1175
+ client.close();
1176
+ _client = null;
1177
+ _sessionId = '';
1178
+ _targetId = '';
1179
+ _refs = new Map();
1180
+ output.printSuccess('Browser session closed');
1181
+ }
1182
+ else {
1183
+ output.printInfo('No active browser session');
1184
+ }
1185
+ return { success: true };
1186
+ },
1187
+ };
1188
+ // ---------------------------------------------------------------------------
1189
+ // Additional subcommands
1190
+ // ---------------------------------------------------------------------------
1191
+ const dblclickCommand = {
1192
+ name: 'dblclick',
1193
+ description: 'Double-click an element. Usage: monomind browse dblclick @e1',
1194
+ action: async (ctx) => {
1195
+ const { client, sessionId } = await ensureConnected(_port);
1196
+ const browser = await getBrowser();
1197
+ const refArg = ctx.args[0];
1198
+ if (!refArg)
1199
+ throw new Error('Usage: monomind browse dblclick @e1');
1200
+ const refKey = refArg.startsWith('@') ? refArg.slice(1) : refArg;
1201
+ const ref = await browser.resolveRef(client, sessionId, _refs, refKey);
1202
+ await browser.clickElement(client, sessionId, ref, { clickCount: 2 });
1203
+ output.printSuccess(`Double-clicked: ${ref.role} "${ref.name}"`);
1204
+ return { success: true };
1205
+ },
1206
+ };
1207
+ const focusCommand = {
1208
+ name: 'focus',
1209
+ description: 'Focus an element. Usage: monomind browse focus @e1',
1210
+ action: async (ctx) => {
1211
+ const { client, sessionId } = await ensureConnected(_port);
1212
+ const browser = await getBrowser();
1213
+ const refArg = ctx.args[0];
1214
+ if (!refArg)
1215
+ throw new Error('Usage: monomind browse focus @e1');
1216
+ const refKey = refArg.startsWith('@') ? refArg.slice(1) : refArg;
1217
+ const ref = await browser.resolveRef(client, sessionId, _refs, refKey);
1218
+ await browser.focusElement(client, sessionId, ref);
1219
+ output.printSuccess(`Focused: ${ref.role} "${ref.name}"`);
1220
+ return { success: true };
1221
+ },
1222
+ };
1223
+ const typeCommand = {
1224
+ name: 'type',
1225
+ description: 'Type text into element (appends, does not clear). Usage: monomind browse type @e1 "text"',
1226
+ action: async (ctx) => {
1227
+ const { client, sessionId } = await ensureConnected(_port);
1228
+ const browser = await getBrowser();
1229
+ const refArg = ctx.args[0];
1230
+ const value = ctx.args[1];
1231
+ if (!refArg || value === undefined)
1232
+ throw new Error('Usage: monomind browse type @e1 "value"');
1233
+ const refKey = refArg.startsWith('@') ? refArg.slice(1) : refArg;
1234
+ const ref = await browser.resolveRef(client, sessionId, _refs, refKey);
1235
+ await browser.typeIntoElement(client, sessionId, ref, value);
1236
+ output.printSuccess(`Typed into: ${ref.role} "${ref.name}"`);
1237
+ return { success: true };
1238
+ },
1239
+ };
1240
+ const keyboardCommand = {
1241
+ name: 'keyboard',
1242
+ description: 'Keyboard commands. Usage: monomind browse keyboard type "text" | inserttext "text"',
1243
+ action: async (ctx) => {
1244
+ const { client, sessionId } = await ensureConnected(_port);
1245
+ const browser = await getBrowser();
1246
+ const action = ctx.args[0];
1247
+ const text = ctx.args[1];
1248
+ if (!action || !text)
1249
+ throw new Error('Usage: monomind browse keyboard type|inserttext "text"');
1250
+ await browser.typeText(client, sessionId, text);
1251
+ output.printSuccess(`Keyboard ${action}: ${text.length} chars`);
1252
+ return { success: true };
1253
+ },
1254
+ };
1255
+ const keydownCommand = {
1256
+ name: 'keydown',
1257
+ description: 'Hold key down. Usage: monomind browse keydown Shift',
1258
+ action: async (ctx) => {
1259
+ const { client, sessionId } = await ensureConnected(_port);
1260
+ const browser = await getBrowser();
1261
+ const key = ctx.args[0];
1262
+ if (!key)
1263
+ throw new Error('Usage: monomind browse keydown <key>');
1264
+ await browser.keyDown(client, sessionId, key);
1265
+ output.printSuccess(`Key down: ${key}`);
1266
+ return { success: true };
1267
+ },
1268
+ };
1269
+ const keyupCommand = {
1270
+ name: 'keyup',
1271
+ description: 'Release held key. Usage: monomind browse keyup Shift',
1272
+ action: async (ctx) => {
1273
+ const { client, sessionId } = await ensureConnected(_port);
1274
+ const browser = await getBrowser();
1275
+ const key = ctx.args[0];
1276
+ if (!key)
1277
+ throw new Error('Usage: monomind browse keyup <key>');
1278
+ await browser.keyUp(client, sessionId, key);
1279
+ output.printSuccess(`Key up: ${key}`);
1280
+ return { success: true };
1281
+ },
1282
+ };
1283
+ const hoverCommand = {
1284
+ name: 'hover',
1285
+ description: 'Hover over an element. Usage: monomind browse hover @e1',
1286
+ action: async (ctx) => {
1287
+ const { client, sessionId } = await ensureConnected(_port);
1288
+ const browser = await getBrowser();
1289
+ const refArg = ctx.args[0];
1290
+ if (!refArg)
1291
+ throw new Error('Usage: monomind browse hover @e1');
1292
+ const refKey = refArg.startsWith('@') ? refArg.slice(1) : refArg;
1293
+ const ref = await browser.resolveRef(client, sessionId, _refs, refKey);
1294
+ await browser.hoverElement(client, sessionId, ref);
1295
+ output.printSuccess(`Hovered: ${ref.role} "${ref.name}"`);
1296
+ return { success: true };
1297
+ },
1298
+ };
1299
+ const selectCommand = {
1300
+ name: 'select',
1301
+ description: 'Select a dropdown option. Usage: monomind browse select @e1 "Option text"',
1302
+ action: async (ctx) => {
1303
+ const { client, sessionId } = await ensureConnected(_port);
1304
+ const browser = await getBrowser();
1305
+ const refArg = ctx.args[0];
1306
+ const value = ctx.args[1];
1307
+ if (!refArg || !value)
1308
+ throw new Error('Usage: monomind browse select @e1 "value"');
1309
+ const refKey = refArg.startsWith('@') ? refArg.slice(1) : refArg;
1310
+ const ref = await browser.resolveRef(client, sessionId, _refs, refKey);
1311
+ await browser.selectOption(client, sessionId, ref, value);
1312
+ output.printSuccess(`Selected: "${value}"`);
1313
+ return { success: true };
1314
+ },
1315
+ };
1316
+ const checkCommand = {
1317
+ name: 'check',
1318
+ description: 'Check a checkbox. Usage: monomind browse check @e1',
1319
+ action: async (ctx) => {
1320
+ const { client, sessionId } = await ensureConnected(_port);
1321
+ const browser = await getBrowser();
1322
+ const refArg = ctx.args[0];
1323
+ if (!refArg)
1324
+ throw new Error('Usage: monomind browse check @e1');
1325
+ const refKey = refArg.startsWith('@') ? refArg.slice(1) : refArg;
1326
+ const ref = await browser.resolveRef(client, sessionId, _refs, refKey);
1327
+ await browser.checkElement(client, sessionId, ref, true);
1328
+ output.printSuccess(`Checked: ${ref.role} "${ref.name}"`);
1329
+ return { success: true };
1330
+ },
1331
+ };
1332
+ const uncheckCommand = {
1333
+ name: 'uncheck',
1334
+ description: 'Uncheck a checkbox. Usage: monomind browse uncheck @e1',
1335
+ action: async (ctx) => {
1336
+ const { client, sessionId } = await ensureConnected(_port);
1337
+ const browser = await getBrowser();
1338
+ const refArg = ctx.args[0];
1339
+ if (!refArg)
1340
+ throw new Error('Usage: monomind browse uncheck @e1');
1341
+ const refKey = refArg.startsWith('@') ? refArg.slice(1) : refArg;
1342
+ const ref = await browser.resolveRef(client, sessionId, _refs, refKey);
1343
+ await browser.checkElement(client, sessionId, ref, false);
1344
+ output.printSuccess(`Unchecked: ${ref.role} "${ref.name}"`);
1345
+ return { success: true };
1346
+ },
1347
+ };
1348
+ async function resolveElementObjectId(client, sessionId, refs, refOrSelector) {
1349
+ const browser = await getBrowser();
1350
+ if (refOrSelector.startsWith('@') || /^e\d+$/.test(refOrSelector)) {
1351
+ const key = refOrSelector.startsWith('@') ? refOrSelector.slice(1) : refOrSelector;
1352
+ const ref = await browser.resolveRef(client, sessionId, refs, key);
1353
+ const objectId = await browser.getObjectIdForRef(client, sessionId, ref);
1354
+ if (!objectId)
1355
+ throw new Error(`Element @${key} not found in DOM`);
1356
+ return objectId;
1357
+ }
1358
+ // CSS selector path
1359
+ const res = await client.send('Runtime.evaluate', {
1360
+ expression: `document.querySelector(${JSON.stringify(refOrSelector)})`,
1361
+ returnByValue: false,
1362
+ }, sessionId);
1363
+ if (!res.result?.objectId || res.result?.subtype === 'null')
1364
+ throw new Error(`Selector not found: ${refOrSelector}`);
1365
+ return res.result.objectId;
1366
+ }
1367
+ const isvisibleCommand = {
1368
+ name: 'isvisible',
1369
+ description: 'Check if element is visible. Usage: monomind browse isvisible @e1|"selector"',
1370
+ options: [{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false }],
1371
+ action: async (ctx) => {
1372
+ const { client, sessionId } = await ensureConnected(_port);
1373
+ const arg = ctx.args[0];
1374
+ if (!arg)
1375
+ throw new Error('Usage: monomind browse isvisible @e1|".selector"');
1376
+ const objectId = await resolveElementObjectId(client, sessionId, _refs, arg);
1377
+ const r = await client.send('Runtime.callFunctionOn', {
1378
+ 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;}`,
1379
+ objectId,
1380
+ returnByValue: true,
1381
+ }, sessionId);
1382
+ const visible = r.result?.value ?? false;
1383
+ if (ctx.flags.json) {
1384
+ print(JSON.stringify({ visible }));
1385
+ }
1386
+ else {
1387
+ output.printSuccess(`isvisible: ${visible}`);
1388
+ }
1389
+ return { success: true, data: { visible } };
1390
+ },
1391
+ };
1392
+ const isenabledCommand = {
1393
+ name: 'isenabled',
1394
+ description: 'Check if element is enabled (not disabled). Usage: monomind browse isenabled @e1|"selector"',
1395
+ options: [{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false }],
1396
+ action: async (ctx) => {
1397
+ const { client, sessionId } = await ensureConnected(_port);
1398
+ const arg = ctx.args[0];
1399
+ if (!arg)
1400
+ throw new Error('Usage: monomind browse isenabled @e1|".selector"');
1401
+ const objectId = await resolveElementObjectId(client, sessionId, _refs, arg);
1402
+ const r = await client.send('Runtime.callFunctionOn', {
1403
+ functionDeclaration: `function(){return !this.disabled;}`,
1404
+ objectId,
1405
+ returnByValue: true,
1406
+ }, sessionId);
1407
+ const enabled = r.result?.value ?? true;
1408
+ if (ctx.flags.json) {
1409
+ print(JSON.stringify({ enabled }));
1410
+ }
1411
+ else {
1412
+ output.printSuccess(`isenabled: ${enabled}`);
1413
+ }
1414
+ return { success: true, data: { enabled } };
1415
+ },
1416
+ };
1417
+ const ischeckedCommand = {
1418
+ name: 'ischecked',
1419
+ description: 'Check if checkbox/radio is checked. Usage: monomind browse ischecked @e1|"selector"',
1420
+ options: [{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false }],
1421
+ action: async (ctx) => {
1422
+ const { client, sessionId } = await ensureConnected(_port);
1423
+ const arg = ctx.args[0];
1424
+ if (!arg)
1425
+ throw new Error('Usage: monomind browse ischecked @e1|".selector"');
1426
+ const objectId = await resolveElementObjectId(client, sessionId, _refs, arg);
1427
+ const r = await client.send('Runtime.callFunctionOn', {
1428
+ 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;}`,
1429
+ objectId,
1430
+ returnByValue: true,
1431
+ }, sessionId);
1432
+ const checked = r.result?.value ?? false;
1433
+ if (ctx.flags.json) {
1434
+ print(JSON.stringify({ checked }));
1435
+ }
1436
+ else {
1437
+ output.printSuccess(`ischecked: ${checked}`);
1438
+ }
1439
+ return { success: true, data: { checked } };
1440
+ },
1441
+ };
1442
+ const tapCommand = {
1443
+ name: 'tap',
1444
+ description: 'Tap element with a touch event (mobile testing). Usage: monomind browse tap @e1|"selector"',
1445
+ action: async (ctx) => {
1446
+ const { client, sessionId } = await ensureConnected(_port);
1447
+ const browser = await getBrowser();
1448
+ const arg = ctx.args[0];
1449
+ if (!arg)
1450
+ throw new Error('Usage: monomind browse tap @e1|".selector"');
1451
+ // Get element center position
1452
+ let x, y;
1453
+ if (arg.startsWith('@') || /^e\d+$/.test(arg)) {
1454
+ const key = arg.startsWith('@') ? arg.slice(1) : arg;
1455
+ const ref = await browser.resolveRef(client, sessionId, _refs, key);
1456
+ const box = await browser.getElementBox(client, sessionId, ref);
1457
+ if (!box)
1458
+ throw new Error(`Cannot get bounds for @${key}`);
1459
+ x = Math.round(box.x + box.width / 2);
1460
+ y = Math.round(box.y + box.height / 2);
1461
+ }
1462
+ else {
1463
+ const posJson = await browser.evaluateJs(client, sessionId, `(function(){var el=document.querySelector(${JSON.stringify(arg)});if(!el)return null;var r=el.getBoundingClientRect();return JSON.stringify({x:r.left+r.width/2,y:r.top+r.height/2});})()`);
1464
+ if (!posJson)
1465
+ throw new Error(`Selector not found: ${arg}`);
1466
+ const pos = JSON.parse(posJson);
1467
+ x = Math.round(pos.x);
1468
+ y = Math.round(pos.y);
1469
+ }
1470
+ await client.send('Input.dispatchTouchEvent', { type: 'touchStart', touchPoints: [{ x, y }] }, sessionId);
1471
+ await client.send('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] }, sessionId);
1472
+ output.printSuccess(`Tapped at (${x}, ${y})`);
1473
+ return { success: true, data: { x, y } };
1474
+ },
1475
+ };
1476
+ const swipeCommand = {
1477
+ name: 'swipe',
1478
+ description: 'Swipe gesture (mobile). Usage: monomind browse swipe up|down|left|right [distance] [--x N] [--y N]',
1479
+ options: [
1480
+ { name: 'x', type: 'number', description: 'Start X coordinate (default: center)', default: 200 },
1481
+ { name: 'y', type: 'number', description: 'Start Y coordinate (default: center)', default: 400 },
1482
+ { name: 'distance', short: 'd', type: 'number', description: 'Swipe distance in pixels', default: 300 },
1483
+ ],
1484
+ action: async (ctx) => {
1485
+ const { client, sessionId } = await ensureConnected(_port);
1486
+ const direction = ctx.args[0];
1487
+ if (!['up', 'down', 'left', 'right'].includes(direction)) {
1488
+ throw new Error('Usage: monomind browse swipe up|down|left|right [--x N] [--y N] [--distance N]');
1489
+ }
1490
+ const startX = ctx.flags.x ?? 200;
1491
+ const startY = ctx.flags.y ?? 400;
1492
+ const positionalDistance = ctx.args[1] !== undefined ? parseInt(ctx.args[1], 10) : undefined;
1493
+ const distance = (positionalDistance && Number.isFinite(positionalDistance)) ? positionalDistance : ctx.flags.distance ?? 300;
1494
+ const dx = direction === 'right' ? distance : direction === 'left' ? -distance : 0;
1495
+ const dy = direction === 'down' ? distance : direction === 'up' ? -distance : 0;
1496
+ await client.send('Input.dispatchTouchEvent', { type: 'touchStart', touchPoints: [{ x: startX, y: startY }] }, sessionId);
1497
+ const steps = 10;
1498
+ for (let i = 1; i <= steps; i++) {
1499
+ const x = Math.round(startX + dx * i / steps);
1500
+ const y = Math.round(startY + dy * i / steps);
1501
+ await client.send('Input.dispatchTouchEvent', { type: 'touchMove', touchPoints: [{ x, y }] }, sessionId);
1502
+ await new Promise((r) => setTimeout(r, 16));
1503
+ }
1504
+ await client.send('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] }, sessionId);
1505
+ output.printSuccess(`Swiped ${direction} ${distance}px from (${startX},${startY})`);
1506
+ return { success: true, data: { direction, distance, startX, startY } };
1507
+ },
1508
+ };
1509
+ const scrollIntoViewCommand = {
1510
+ name: 'scrollintoview',
1511
+ description: 'Scroll element into view. Usage: monomind browse scrollintoview @e1',
1512
+ action: async (ctx) => {
1513
+ const { client, sessionId } = await ensureConnected(_port);
1514
+ const browser = await getBrowser();
1515
+ const refArg = ctx.args[0];
1516
+ if (!refArg)
1517
+ throw new Error('Usage: monomind browse scrollintoview @e1');
1518
+ const refKey = refArg.startsWith('@') ? refArg.slice(1) : refArg;
1519
+ const ref = await browser.resolveRef(client, sessionId, _refs, refKey);
1520
+ await browser.scrollIntoView(client, sessionId, ref);
1521
+ output.printSuccess(`Scrolled into view: ${ref.role} "${ref.name}"`);
1522
+ return { success: true };
1523
+ },
1524
+ };
1525
+ const dragCommand = {
1526
+ name: 'drag',
1527
+ description: 'Drag element to another element. Usage: monomind browse drag @e1 @e2',
1528
+ action: async (ctx) => {
1529
+ const { client, sessionId } = await ensureConnected(_port);
1530
+ const browser = await getBrowser();
1531
+ const srcArg = ctx.args[0];
1532
+ const tgtArg = ctx.args[1];
1533
+ if (!srcArg || !tgtArg)
1534
+ throw new Error('Usage: monomind browse drag @e1 @e2');
1535
+ const srcKey = srcArg.startsWith('@') ? srcArg.slice(1) : srcArg;
1536
+ const tgtKey = tgtArg.startsWith('@') ? tgtArg.slice(1) : tgtArg;
1537
+ const src = await browser.resolveRef(client, sessionId, _refs, srcKey);
1538
+ const tgt = await browser.resolveRef(client, sessionId, _refs, tgtKey);
1539
+ await browser.dragAndDrop(client, sessionId, src, tgt);
1540
+ output.printSuccess(`Dragged @${srcKey} to @${tgtKey}`);
1541
+ return { success: true };
1542
+ },
1543
+ };
1544
+ const uploadCommand = {
1545
+ name: 'upload',
1546
+ description: 'Upload files to a file input. Usage: monomind browse upload @e1 ./file.pdf',
1547
+ action: async (ctx) => {
1548
+ const { client, sessionId } = await ensureConnected(_port);
1549
+ const browser = await getBrowser();
1550
+ const refArg = ctx.args[0];
1551
+ const files = ctx.args.slice(1);
1552
+ if (!refArg || files.length === 0)
1553
+ throw new Error('Usage: monomind browse upload @e1 <file1> [file2...]');
1554
+ const refKey = refArg.startsWith('@') ? refArg.slice(1) : refArg;
1555
+ const ref = await browser.resolveRef(client, sessionId, _refs, refKey);
1556
+ await browser.uploadFile(client, sessionId, ref, files);
1557
+ output.printSuccess(`Uploaded ${files.length} file(s) to @${refKey}`);
1558
+ return { success: true };
1559
+ },
1560
+ };
1561
+ const downloadCommand = {
1562
+ name: 'download',
1563
+ description: 'Click an element and capture the triggered file download. Usage: monomind browse download @e1 ./output.pdf',
1564
+ options: [
1565
+ { name: 'timeout', short: 't', type: 'number', description: 'Max wait for download in ms', default: 30000 },
1566
+ { name: 'json', type: 'boolean', description: 'Output result as JSON', default: false },
1567
+ ],
1568
+ action: async (ctx) => {
1569
+ const { client, sessionId } = await ensureConnected(_port);
1570
+ const browser = await getBrowser();
1571
+ const refOrSel = ctx.args[0];
1572
+ const savePath = ctx.args[1];
1573
+ if (!refOrSel || !savePath)
1574
+ throw new Error('Usage: monomind browse download @e1|selector <save-path>');
1575
+ const { mkdir } = await import('fs/promises');
1576
+ const { dirname } = await import('path');
1577
+ const { tmpdir } = await import('os');
1578
+ const { join } = await import('path');
1579
+ const downloadDir = join(tmpdir(), `mm-download-${Date.now()}`);
1580
+ await mkdir(downloadDir, { recursive: true });
1581
+ // Enable Page.downloadWillBegin / Page.downloadProgress events
1582
+ await client.send('Browser.setDownloadBehavior', {
1583
+ behavior: 'allow',
1584
+ downloadPath: downloadDir,
1585
+ eventsEnabled: true,
1586
+ }, undefined).catch(() => {
1587
+ // Fallback: older Chrome API (session-scoped)
1588
+ return client.send('Page.setDownloadBehavior', {
1589
+ behavior: 'allow',
1590
+ downloadPath: downloadDir,
1591
+ }, sessionId).catch(() => { });
1592
+ });
1593
+ // Track when download completes
1594
+ const downloadPromise = new Promise((resolve, reject) => {
1595
+ let guid = '';
1596
+ // C2: capture off() functions to avoid listener leaks in batch mode
1597
+ const offBegin = client.on('Browser.downloadWillBegin', (params) => {
1598
+ guid = params.guid;
1599
+ });
1600
+ let offProgress;
1601
+ // cleanup defined before setTimeout so the timeout callback can call it
1602
+ let timeout;
1603
+ const cleanup = () => { clearTimeout(timeout); offBegin?.(); offProgress?.(); };
1604
+ timeout = setTimeout(() => { cleanup(); reject(new Error('Download timed out')); }, ctx.flags.timeout);
1605
+ offProgress = client.on('Browser.downloadProgress', async (params) => {
1606
+ if (params.guid === guid && params.state === 'completed') {
1607
+ cleanup();
1608
+ // Find the downloaded file in downloadDir
1609
+ const { readdir, rename, rmdir } = await import('fs/promises');
1610
+ const files = await readdir(downloadDir);
1611
+ if (files.length > 0) {
1612
+ const src = join(downloadDir, files[0]);
1613
+ await mkdir(dirname(savePath), { recursive: true });
1614
+ await rename(src, savePath);
1615
+ await rmdir(downloadDir).catch(() => { }); // I1: cleanup temp dir
1616
+ resolve(savePath);
1617
+ }
1618
+ else {
1619
+ await rmdir(downloadDir).catch(() => { }); // I1: cleanup temp dir
1620
+ reject(new Error('Download completed but no file found'));
1621
+ }
1622
+ }
1623
+ else if (params.guid === guid && params.state === 'canceled') {
1624
+ cleanup();
1625
+ reject(new Error('Download was canceled'));
1626
+ }
1627
+ });
1628
+ });
1629
+ // Click the element to trigger download
1630
+ const objectId = await resolveElementObjectId(client, sessionId, _refs, refOrSel);
1631
+ await client.send('Runtime.callFunctionOn', {
1632
+ functionDeclaration: 'function(){ this.click(); }',
1633
+ objectId,
1634
+ returnByValue: true,
1635
+ }, sessionId);
1636
+ const finalPath = await downloadPromise;
1637
+ if (ctx.flags.json)
1638
+ print(JSON.stringify({ data: { path: finalPath } }));
1639
+ else
1640
+ output.printSuccess(`Downloaded: ${finalPath}`);
1641
+ return { success: true, data: { path: finalPath } };
1642
+ },
1643
+ };
1644
+ const mouseCommand = {
1645
+ name: 'mouse',
1646
+ description: 'Fine-grained mouse control. Usage: monomind browse mouse move|down|up|wheel <args>',
1647
+ options: [
1648
+ { name: 'button', type: 'string', description: 'Button: left|right|middle', default: 'left' },
1649
+ ],
1650
+ action: async (ctx) => {
1651
+ const { client, sessionId } = await ensureConnected(_port);
1652
+ const browser = await getBrowser();
1653
+ const action = ctx.args[0];
1654
+ switch (action) {
1655
+ case 'move': {
1656
+ const x = parseFloat(ctx.args[1]);
1657
+ const y = parseFloat(ctx.args[2]);
1658
+ await browser.mouseMove(client, sessionId, x, y);
1659
+ output.printSuccess(`Mouse moved to (${x}, ${y})`);
1660
+ break;
1661
+ }
1662
+ case 'down': {
1663
+ const x = parseFloat(ctx.args[1]) || 0;
1664
+ const y = parseFloat(ctx.args[2]) || 0;
1665
+ const button = ctx.flags.button ?? 'left';
1666
+ await browser.mouseDown(client, sessionId, x, y, button);
1667
+ output.printSuccess(`Mouse down at (${x}, ${y})`);
1668
+ break;
1669
+ }
1670
+ case 'up': {
1671
+ const x = parseFloat(ctx.args[1]) || 0;
1672
+ const y = parseFloat(ctx.args[2]) || 0;
1673
+ const button = ctx.flags.button ?? 'left';
1674
+ await browser.mouseUp(client, sessionId, x, y, button);
1675
+ output.printSuccess(`Mouse up at (${x}, ${y})`);
1676
+ break;
1677
+ }
1678
+ case 'wheel': {
1679
+ const x = parseFloat(ctx.args[1]) || 0;
1680
+ const y = parseFloat(ctx.args[2]) || 0;
1681
+ const dy = parseFloat(ctx.args[3]) || 0;
1682
+ const dx = parseFloat(ctx.args[4]) || 0;
1683
+ await browser.mouseWheel(client, sessionId, x, y, dy, dx);
1684
+ output.printSuccess(`Mouse wheel (${dx}, ${dy})`);
1685
+ break;
1686
+ }
1687
+ default:
1688
+ throw new Error('Usage: monomind browse mouse move|down|up|wheel <args>');
1689
+ }
1690
+ return { success: true };
1691
+ },
1692
+ };
1693
+ const clipboardCommand = {
1694
+ name: 'clipboard',
1695
+ description: 'Clipboard operations. Usage: monomind browse clipboard read|write|copy|paste',
1696
+ action: async (ctx) => {
1697
+ const { client, sessionId } = await ensureConnected(_port);
1698
+ const browser = await getBrowser();
1699
+ const action = ctx.args[0];
1700
+ switch (action) {
1701
+ case 'read': {
1702
+ const text = await browser.readClipboard(client, sessionId);
1703
+ print(text);
1704
+ return { success: true, data: { text } };
1705
+ }
1706
+ case 'write': {
1707
+ const text = ctx.args[1];
1708
+ if (!text)
1709
+ throw new Error('Usage: monomind browse clipboard write "text"');
1710
+ await browser.writeClipboard(client, sessionId, text);
1711
+ output.printSuccess('Clipboard written');
1712
+ break;
1713
+ }
1714
+ case 'copy': {
1715
+ const mod = process.platform === 'darwin' ? 4 : 2; // Meta/Cmd on macOS, Ctrl elsewhere
1716
+ await browser.pressKeyCombo(client, sessionId, 'c', mod);
1717
+ output.printSuccess('Copy sent');
1718
+ break;
1719
+ }
1720
+ case 'paste': {
1721
+ const mod = process.platform === 'darwin' ? 4 : 2;
1722
+ await browser.pressKeyCombo(client, sessionId, 'v', mod);
1723
+ output.printSuccess('Paste sent');
1724
+ break;
1725
+ }
1726
+ default:
1727
+ throw new Error('Usage: monomind browse clipboard read|write|copy|paste');
1728
+ }
1729
+ return { success: true };
1730
+ },
1731
+ };
1732
+ const dialogCommand = {
1733
+ name: 'dialog',
1734
+ description: 'Handle browser dialogs. Usage: monomind browse dialog accept|dismiss|status',
1735
+ action: async (ctx) => {
1736
+ const { client, sessionId } = await ensureConnected(_port);
1737
+ const browser = await getBrowser();
1738
+ const action = ctx.args[0];
1739
+ switch (action) {
1740
+ case 'accept': {
1741
+ const text = ctx.args[1];
1742
+ await browser.acceptDialog(client, sessionId, text);
1743
+ output.printSuccess('Dialog accepted');
1744
+ break;
1745
+ }
1746
+ case 'dismiss':
1747
+ await browser.dismissDialog(client, sessionId);
1748
+ output.printSuccess('Dialog dismissed');
1749
+ break;
1750
+ case 'status': {
1751
+ const info = browser.getDialogStatus(sessionId);
1752
+ if (info) {
1753
+ print(`Dialog open: type=${info.type} message="${info.message}"`);
1754
+ }
1755
+ else {
1756
+ print('No dialog open');
1757
+ }
1758
+ return { success: true, data: { dialog: info } };
1759
+ }
1760
+ default:
1761
+ throw new Error('Usage: monomind browse dialog accept|dismiss|status');
1762
+ }
1763
+ return { success: true };
1764
+ },
1765
+ };
1766
+ const frameCommand = {
1767
+ name: 'frame',
1768
+ description: 'Switch to iframe or back to main. Usage: monomind browse frame "#frame-id" | frame main',
1769
+ action: async (ctx) => {
1770
+ const { client, sessionId } = await ensureConnected(_port);
1771
+ const browser = await getBrowser();
1772
+ const target = ctx.args[0];
1773
+ if (!target)
1774
+ throw new Error('Usage: monomind browse frame <selector>|main');
1775
+ if (target === 'main') {
1776
+ output.printSuccess('Switched to main frame');
1777
+ }
1778
+ else {
1779
+ const frameSrc = await browser.switchToFrame(client, sessionId, target);
1780
+ output.printSuccess(`Switched to frame: ${frameSrc ?? target}`);
1781
+ }
1782
+ return { success: true };
1783
+ },
1784
+ };
1785
+ const tabCommand = {
1786
+ name: 'tab',
1787
+ description: 'Tab management. Usage: monomind browse tab list|new|close [url]',
1788
+ options: [
1789
+ { name: 'label', type: 'string', description: 'Label for new tab' },
1790
+ ],
1791
+ action: async (ctx) => {
1792
+ const { client, sessionId } = await ensureConnected(_port);
1793
+ const browser = await getBrowser();
1794
+ const action = ctx.args[0];
1795
+ switch (action ?? 'list') {
1796
+ case 'list': {
1797
+ const tabs = await browser.listTabs(_port);
1798
+ for (const t of tabs)
1799
+ print(` ${t.id}: ${t.title} (${t.url})`);
1800
+ return { success: true, data: { tabs } };
1801
+ }
1802
+ case 'new': {
1803
+ const url = ctx.args[1];
1804
+ const tab = await browser.newTab(_port, url);
1805
+ output.printSuccess(`New tab: ${tab.id} ${url ?? ''}`);
1806
+ return { success: true, data: { tab } };
1807
+ }
1808
+ case 'close': {
1809
+ const tabId = ctx.args[1];
1810
+ if (!tabId)
1811
+ throw new Error('Usage: monomind browse tab close <tabId>');
1812
+ if (tabId === _targetId) {
1813
+ const sid = _sessionId;
1814
+ // Stop profiling before closing the tab so CDP commands still reach the live session
1815
+ if (browser.getHarStatus(sid).recording) {
1816
+ try {
1817
+ await browser.stopHarRecording(client, sid);
1818
+ }
1819
+ catch { /* ignore */ }
1820
+ }
1821
+ if (browser.getTraceStatus(sid)) {
1822
+ try {
1823
+ await browser.stopTrace(client, sid);
1824
+ }
1825
+ catch { /* ignore */ }
1826
+ }
1827
+ if (browser.isProfilingActive(sid)) {
1828
+ try {
1829
+ await browser.stopCpuProfile(client, sid);
1830
+ }
1831
+ catch { /* ignore */ }
1832
+ }
1833
+ browser.teardownRouteInterception(sid);
1834
+ browser.stopRequestCapture(sid);
1835
+ browser.teardownDialogHandling(sid);
1836
+ browser.teardownConsoleCapture(sid);
1837
+ await browser.closeTab(client, sessionId, tabId);
1838
+ client.close();
1839
+ _client = null;
1840
+ _sessionId = '';
1841
+ _targetId = '';
1842
+ _refs = new Map();
1843
+ }
1844
+ else {
1845
+ await browser.closeTab(client, sessionId, tabId);
1846
+ }
1847
+ output.printSuccess(`Closed tab: ${tabId}`);
1848
+ break;
1849
+ }
1850
+ default: {
1851
+ // Attach to new tab FIRST — only tear down old session if that succeeds
1852
+ const newSid = await browser.activateTab(client, sessionId, action);
1853
+ const oldSid = _sessionId;
1854
+ if (browser.getHarStatus(oldSid).recording) {
1855
+ try {
1856
+ await browser.stopHarRecording(client, oldSid);
1857
+ }
1858
+ catch { /* ignore */ }
1859
+ }
1860
+ if (browser.getTraceStatus(oldSid)) {
1861
+ try {
1862
+ await browser.stopTrace(client, oldSid);
1863
+ }
1864
+ catch { /* ignore */ }
1865
+ }
1866
+ if (browser.isProfilingActive(oldSid)) {
1867
+ try {
1868
+ await browser.stopCpuProfile(client, oldSid);
1869
+ }
1870
+ catch { /* ignore */ }
1871
+ }
1872
+ await browser.disableInterception(client, oldSid).catch(() => { });
1873
+ browser.stopRequestCapture(oldSid);
1874
+ browser.teardownDialogHandling(oldSid);
1875
+ browser.teardownConsoleCapture(oldSid);
1876
+ _sessionId = newSid;
1877
+ _targetId = action;
1878
+ _refs = new Map();
1879
+ await browser.enableSessionDomains(client, _sessionId);
1880
+ output.printSuccess(`Switched to tab: ${action}`);
1881
+ }
1882
+ }
1883
+ return { success: true };
1884
+ },
1885
+ };
1886
+ const windowCommand = {
1887
+ name: 'window',
1888
+ description: 'Browser window management. Usage: monomind browse window new [url]',
1889
+ action: async (ctx) => {
1890
+ const { client, sessionId: _sid } = await ensureConnected(_port);
1891
+ const browser = await getBrowser();
1892
+ const action = ctx.args[0];
1893
+ if (!action || action === 'new') {
1894
+ // Create isolated browser context (incognito-like) with a fresh page
1895
+ const ctxResult = await client.send('Target.createBrowserContext', {}, undefined);
1896
+ const browserContextId = ctxResult.browserContextId;
1897
+ const url = ctx.args[1] || 'about:blank';
1898
+ const targetResult = await client.send('Target.createTarget', { url, browserContextId }, undefined);
1899
+ const targetId = targetResult.targetId;
1900
+ const attachResult = await client.send('Target.attachToTarget', { targetId, flatten: true }, undefined);
1901
+ const newSessionId = attachResult.sessionId;
1902
+ // W3: fully tear down old session before switching
1903
+ const oldSid = _sessionId;
1904
+ if (browser.getHarStatus(oldSid).recording) {
1905
+ try {
1906
+ await browser.stopHarRecording(client, oldSid);
1907
+ }
1908
+ catch { /* ignore */ }
1909
+ }
1910
+ if (browser.getTraceStatus(oldSid)) {
1911
+ try {
1912
+ await browser.stopTrace(client, oldSid);
1913
+ }
1914
+ catch { /* ignore */ }
1915
+ }
1916
+ if (browser.isProfilingActive(oldSid)) {
1917
+ try {
1918
+ await browser.stopCpuProfile(client, oldSid);
1919
+ }
1920
+ catch { /* ignore */ }
1921
+ }
1922
+ browser.teardownRouteInterception(oldSid);
1923
+ browser.stopRequestCapture(oldSid);
1924
+ browser.teardownDialogHandling(oldSid);
1925
+ browser.teardownConsoleCapture(oldSid);
1926
+ _sessionId = newSessionId;
1927
+ _targetId = targetId;
1928
+ _refs = new Map();
1929
+ await browser.enableSessionDomains(client, _sessionId);
1930
+ output.printSuccess(`Opened new window (isolated context): ${targetId} [${url}]`);
1931
+ return { success: true, data: { targetId, browserContextId, sessionId: newSessionId } };
1932
+ }
1933
+ throw new Error(`Unknown window action: ${action}. Use: new`);
1934
+ },
1935
+ };
1936
+ const consoleLogCommand = {
1937
+ name: 'console',
1938
+ description: 'View captured console messages. Usage: monomind browse console [--clear] [--json]',
1939
+ options: [
1940
+ { name: 'clear', type: 'boolean', description: 'Clear console messages', default: false },
1941
+ { name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
1942
+ { name: 'errors-only', type: 'boolean', description: 'Show only errors', default: false },
1943
+ ],
1944
+ action: async (ctx) => {
1945
+ const browser = await getBrowser();
1946
+ if (ctx.flags.clear) {
1947
+ browser.clearConsoleMessages(_sessionId);
1948
+ output.printSuccess('Console cleared');
1949
+ return { success: true };
1950
+ }
1951
+ const allMsgs = browser.getConsoleMessages(_sessionId);
1952
+ const msgs = ctx.flags['errors-only'] ? allMsgs.filter((m) => m.type === 'error') : allMsgs;
1953
+ if (ctx.flags.json) {
1954
+ print(JSON.stringify(msgs));
1955
+ }
1956
+ else {
1957
+ for (const m of msgs) {
1958
+ const prefix = m.type === 'error' ? '[ERROR]' : m.type === 'warn' ? '[WARN]' : '[LOG]';
1959
+ print(`${prefix} ${m.text}`);
1960
+ }
1961
+ }
1962
+ return { success: true, data: { messages: msgs } };
1963
+ },
1964
+ };
1965
+ const errorsCommand = {
1966
+ name: 'errors',
1967
+ description: 'View page errors (uncaught JS exceptions). Usage: monomind browse errors [--clear]',
1968
+ options: [
1969
+ { name: 'clear', type: 'boolean', description: 'Clear errors', default: false },
1970
+ { name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
1971
+ ],
1972
+ action: async (ctx) => {
1973
+ const browser = await getBrowser();
1974
+ if (ctx.flags.clear) {
1975
+ browser.clearPageErrors(_sessionId);
1976
+ output.printSuccess('Errors cleared');
1977
+ return { success: true };
1978
+ }
1979
+ const errs = browser.getPageErrors(_sessionId);
1980
+ if (ctx.flags.json) {
1981
+ print(JSON.stringify(errs));
1982
+ }
1983
+ else if (errs.length === 0) {
1984
+ output.printSuccess('No page errors');
1985
+ }
1986
+ else {
1987
+ for (const e of errs)
1988
+ print(`[ERROR] ${e.text} (${e.url}:${e.lineNumber})`);
1989
+ }
1990
+ return { success: true, data: { errors: errs } };
1991
+ },
1992
+ };
1993
+ const storageCommand = {
1994
+ name: 'storage',
1995
+ description: 'localStorage/sessionStorage management. Usage: monomind browse storage local|session [key] [--set val] [--clear]',
1996
+ options: [
1997
+ { name: 'set', type: 'string', description: 'Value to set for key' },
1998
+ { name: 'clear', type: 'boolean', description: 'Clear all storage', default: false },
1999
+ { name: 'remove', type: 'boolean', description: 'Remove a specific key', default: false },
2000
+ { name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
2001
+ ],
2002
+ action: async (ctx) => {
2003
+ const { client, sessionId } = await ensureConnected(_port);
2004
+ const browser = await getBrowser();
2005
+ const storageType = ctx.args[0];
2006
+ const key = ctx.args[1];
2007
+ if (!storageType)
2008
+ throw new Error('Usage: monomind browse storage local|session [key]');
2009
+ const isLocal = storageType === 'local';
2010
+ if (ctx.flags.clear) {
2011
+ if (isLocal)
2012
+ await browser.clearLocalStorage(client, sessionId);
2013
+ else
2014
+ await browser.clearSessionStorage(client, sessionId);
2015
+ output.printSuccess(`${storageType}Storage cleared`);
2016
+ return { success: true };
2017
+ }
2018
+ if (key && ctx.flags.set !== undefined) {
2019
+ if (isLocal)
2020
+ await browser.setLocalStorageKey(client, sessionId, key, ctx.flags.set);
2021
+ else
2022
+ await browser.setSessionStorageKey(client, sessionId, key, ctx.flags.set);
2023
+ output.printSuccess(`Set ${key}`);
2024
+ return { success: true };
2025
+ }
2026
+ if (key && ctx.flags.remove) {
2027
+ if (isLocal)
2028
+ await browser.removeLocalStorageKey(client, sessionId, key);
2029
+ else
2030
+ await browser.removeSessionStorageKey(client, sessionId, key);
2031
+ output.printSuccess(`Removed ${key}`);
2032
+ return { success: true };
2033
+ }
2034
+ if (key) {
2035
+ const val = isLocal
2036
+ ? await browser.getLocalStorageKey(client, sessionId, key)
2037
+ : await browser.getSessionStorageKey(client, sessionId, key);
2038
+ if (ctx.flags.json)
2039
+ print(JSON.stringify({ data: val }));
2040
+ else
2041
+ print(val ?? '(null)');
2042
+ return { success: true, data: { value: val } };
2043
+ }
2044
+ const all = isLocal
2045
+ ? await browser.getAllLocalStorage(client, sessionId)
2046
+ : await browser.getAllSessionStorage(client, sessionId);
2047
+ if (ctx.flags.json)
2048
+ print(JSON.stringify(all));
2049
+ else {
2050
+ for (const [k, v] of Object.entries(all))
2051
+ print(` ${k}: ${v}`);
2052
+ }
2053
+ return { success: true, data: { storage: all } };
2054
+ },
2055
+ };
2056
+ const cookiesCommand = {
2057
+ name: 'cookies',
2058
+ description: 'Cookie management. Usage: monomind browse cookies [list|set|clear]',
2059
+ options: [
2060
+ { name: 'name', type: 'string', description: 'Cookie name' },
2061
+ { name: 'value', type: 'string', description: 'Cookie value' },
2062
+ { name: 'domain', type: 'string', description: 'Cookie domain' },
2063
+ { name: 'curl', type: 'string', description: 'Import cookies from cURL dump file' },
2064
+ ],
2065
+ action: async (ctx) => {
2066
+ const { client, sessionId } = await ensureConnected(_port);
2067
+ const browser = await getBrowser();
2068
+ const action = ctx.args[0] ?? 'list';
2069
+ switch (action) {
2070
+ case 'list': {
2071
+ const cookies = await browser.getCookies(client, sessionId);
2072
+ print(JSON.stringify(cookies, null, 2));
2073
+ return { success: true, data: { cookies } };
2074
+ }
2075
+ case 'set': {
2076
+ // Support both: cookies set --name n --value v AND cookies set <name> <value>
2077
+ const name = ctx.flags.name ?? ctx.args[1];
2078
+ const value = ctx.flags.value ?? ctx.args[2];
2079
+ if (!name || value === undefined) {
2080
+ throw new Error('Usage: monomind browse cookies set <name> <value> [--domain <d>]');
2081
+ }
2082
+ await browser.setCookies(client, sessionId, [{
2083
+ name,
2084
+ value,
2085
+ domain: ctx.flags.domain,
2086
+ }]);
2087
+ output.printSuccess(`Cookie set: ${name}`);
2088
+ break;
2089
+ }
2090
+ case 'clear':
2091
+ await browser.clearCookies(client, sessionId);
2092
+ output.printSuccess('Cookies cleared');
2093
+ break;
2094
+ default:
2095
+ throw new Error('Usage: monomind browse cookies list|set|clear');
2096
+ }
2097
+ return { success: true };
2098
+ },
2099
+ };
2100
+ const pdfCommand = {
2101
+ name: 'pdf',
2102
+ description: 'Save page as PDF. Usage: monomind browse pdf [path]',
2103
+ options: [
2104
+ { name: 'landscape', type: 'boolean', description: 'Landscape orientation', default: false },
2105
+ { name: 'background', type: 'boolean', description: 'Print background', default: true },
2106
+ ],
2107
+ action: async (ctx) => {
2108
+ const { client, sessionId } = await ensureConnected(_port);
2109
+ const browser = await getBrowser();
2110
+ const path = await browser.capturePdf(client, sessionId, {
2111
+ path: ctx.args[0],
2112
+ landscape: ctx.flags.landscape,
2113
+ printBackground: ctx.flags.background,
2114
+ });
2115
+ output.printSuccess(`PDF saved: ${path}`);
2116
+ return { success: true, data: { path } };
2117
+ },
2118
+ };
2119
+ const isCommand = {
2120
+ name: 'is',
2121
+ description: 'Check element state. Usage: monomind browse is visible|enabled|checked @e1',
2122
+ options: [
2123
+ { name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
2124
+ ],
2125
+ action: async (ctx) => {
2126
+ const { client, sessionId } = await ensureConnected(_port);
2127
+ const browser = await getBrowser();
2128
+ const check = ctx.args[0];
2129
+ const refArg = ctx.args[1];
2130
+ if (!check || !refArg)
2131
+ throw new Error('Usage: monomind browse is visible|enabled|checked @e1');
2132
+ const refKey = refArg.startsWith('@') ? refArg.slice(1) : refArg;
2133
+ const ref = await browser.resolveRef(client, sessionId, _refs, refKey);
2134
+ let result;
2135
+ switch (check) {
2136
+ case 'visible':
2137
+ result = await browser.isVisible(client, sessionId, ref);
2138
+ break;
2139
+ case 'enabled':
2140
+ result = await browser.isEnabled(client, sessionId, ref);
2141
+ break;
2142
+ case 'checked':
2143
+ result = await browser.isChecked(client, sessionId, ref);
2144
+ break;
2145
+ default: throw new Error(`Unknown check: ${check}. Use: visible|enabled|checked`);
2146
+ }
2147
+ if (ctx.flags.json) {
2148
+ print(JSON.stringify({ data: { [check]: result } }));
2149
+ }
2150
+ else {
2151
+ print(result ? 'true' : 'false');
2152
+ }
2153
+ return { success: true, data: { [check]: result } };
2154
+ },
2155
+ };
2156
+ const findCommand = {
2157
+ name: 'find',
2158
+ description: 'Find elements by semantic locators. Usage: monomind browse find role|text|label|placeholder|testid|alttext|title|selector <value> [action]',
2159
+ options: [
2160
+ { name: 'name', type: 'string', description: 'Filter by accessible name' },
2161
+ { name: 'exact', type: 'boolean', description: 'Require exact match', default: false },
2162
+ { name: 'nth', type: 'number', description: 'Find nth match' },
2163
+ { name: 'last', type: 'boolean', description: 'Find last match', default: false },
2164
+ ],
2165
+ action: async (ctx) => {
2166
+ const { client, sessionId } = await ensureConnected(_port);
2167
+ const browser = await getBrowser();
2168
+ const locator = ctx.args[0];
2169
+ const value = ctx.args[1];
2170
+ const action = ctx.args[2];
2171
+ if (!locator || !value)
2172
+ throw new Error('Usage: monomind browse find role|text|label|placeholder|testid|alttext|title|selector <value> [action]');
2173
+ const opts = {
2174
+ name: ctx.flags.name,
2175
+ exact: ctx.flags.exact,
2176
+ nth: ctx.flags.nth,
2177
+ last: ctx.flags.last,
2178
+ };
2179
+ // alttext: find element by alt attribute (images, icons)
2180
+ if (locator === 'alttext') {
2181
+ // I2: use JS attribute comparison to avoid broken CSS selectors for values with spaces/quotes
2182
+ const valJson = JSON.stringify(value);
2183
+ const found = await browser.evaluateJs(client, sessionId, `(function(v){var el=document.querySelector('img[alt]')||null;var all=document.querySelectorAll('[alt]');for(var i=0;i<all.length;i++){if(all[i].getAttribute('alt')===v){all[i].setAttribute('data-mm-located','true');return true;}}return false;})(${valJson})`);
2184
+ if (!found) {
2185
+ output.printWarning(`alttext not found: ${value}`);
2186
+ return { success: false };
2187
+ }
2188
+ output.printSuccess(`Found element with alt="${value}"`);
2189
+ return { success: true, data: { alttext: value } };
2190
+ }
2191
+ // title: find element by title attribute
2192
+ if (locator === 'title') {
2193
+ // I2: use JS attribute comparison to avoid broken CSS selectors for values with spaces/quotes
2194
+ const valJson = JSON.stringify(value);
2195
+ const found = await browser.evaluateJs(client, sessionId, `(function(v){var all=document.querySelectorAll('[title]');for(var i=0;i<all.length;i++){if(all[i].getAttribute('title')===v){all[i].setAttribute('data-mm-located','true');return true;}}return false;})(${valJson})`);
2196
+ if (!found) {
2197
+ output.printWarning(`title not found: ${value}`);
2198
+ return { success: false };
2199
+ }
2200
+ output.printSuccess(`Found element with title="${value}"`);
2201
+ return { success: true, data: { title: value } };
2202
+ }
2203
+ let ref = null;
2204
+ switch (locator) {
2205
+ case 'role':
2206
+ ref = await browser.findByRole(client, sessionId, _refs, value, opts);
2207
+ break;
2208
+ case 'text':
2209
+ ref = await browser.findByText(client, sessionId, _refs, value, opts);
2210
+ break;
2211
+ case 'label':
2212
+ ref = await browser.findByLabel(client, sessionId, _refs, value, opts);
2213
+ break;
2214
+ case 'placeholder':
2215
+ ref = await browser.findByPlaceholder(client, sessionId, _refs, value, opts);
2216
+ break;
2217
+ case 'selector':
2218
+ ref = await browser.findBySelector(client, sessionId, _refs, value, opts);
2219
+ break;
2220
+ case 'testid': {
2221
+ const sel = await browser.findByTestId(client, sessionId, value);
2222
+ if (!sel) {
2223
+ output.printWarning(`testid not found: ${value}`);
2224
+ return { success: false };
2225
+ }
2226
+ output.printSuccess(`Found testid selector: ${sel}`);
2227
+ return { success: true, data: { selector: sel } };
2228
+ }
2229
+ default:
2230
+ throw new Error(`Unknown locator: ${locator}. Use: role|text|label|placeholder|testid|alttext|title|selector`);
2231
+ }
2232
+ if (!ref) {
2233
+ output.printWarning(`No element found: ${locator}="${value}"`);
2234
+ return { success: false };
2235
+ }
2236
+ output.printSuccess(`Found: ${ref.role} "${ref.name}" [@${ref.ref}]`);
2237
+ if (action) {
2238
+ switch (action) {
2239
+ case 'click':
2240
+ await browser.clickElement(client, sessionId, ref);
2241
+ break;
2242
+ case 'fill': {
2243
+ const fillValue = ctx.args[3];
2244
+ await browser.fillElement(client, sessionId, ref, fillValue ?? '');
2245
+ break;
2246
+ }
2247
+ case 'type': {
2248
+ const typeValue = ctx.args[3];
2249
+ await browser.typeIntoElement(client, sessionId, ref, typeValue ?? '');
2250
+ break;
2251
+ }
2252
+ case 'hover':
2253
+ await browser.hoverElement(client, sessionId, ref);
2254
+ break;
2255
+ case 'focus':
2256
+ await browser.focusElement(client, sessionId, ref);
2257
+ break;
2258
+ case 'check':
2259
+ await browser.checkElement(client, sessionId, ref, true);
2260
+ break;
2261
+ case 'uncheck':
2262
+ await browser.checkElement(client, sessionId, ref, false);
2263
+ break;
2264
+ case 'text': {
2265
+ const objectId = await browser.getObjectIdForRef(client, sessionId, ref);
2266
+ if (objectId) {
2267
+ const r = await client.send('Runtime.callFunctionOn', {
2268
+ functionDeclaration: 'function() { return this.innerText || this.textContent || ""; }',
2269
+ objectId,
2270
+ returnByValue: true,
2271
+ }, sessionId);
2272
+ print(r.result?.value ?? '');
2273
+ }
2274
+ break;
2275
+ }
2276
+ }
2277
+ }
2278
+ return { success: true, data: { ref } };
2279
+ },
2280
+ };
2281
+ const highlightCommand = {
2282
+ name: 'highlight',
2283
+ description: 'Highlight an element for 2 seconds. Usage: monomind browse highlight @e1',
2284
+ action: async (ctx) => {
2285
+ const { client, sessionId } = await ensureConnected(_port);
2286
+ const browser = await getBrowser();
2287
+ const refArg = ctx.args[0];
2288
+ if (!refArg)
2289
+ throw new Error('Usage: monomind browse highlight @e1');
2290
+ const refKey = refArg.startsWith('@') ? refArg.slice(1) : refArg;
2291
+ const ref = await browser.resolveRef(client, sessionId, _refs, refKey);
2292
+ await browser.highlightElement(client, sessionId, ref);
2293
+ output.printSuccess(`Highlighted: ${ref.role} "${ref.name}"`);
2294
+ return { success: true };
2295
+ },
2296
+ };
2297
+ const diffCommand = {
2298
+ name: 'diff',
2299
+ description: 'Compare two URLs or snapshots. Usage: monomind browse diff url <url1> <url2> [--interactive] [--json]',
2300
+ options: [
2301
+ { name: 'interactive', short: 'i', type: 'boolean', description: 'Snapshot interactive elements only', default: false },
2302
+ { name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
2303
+ ],
2304
+ action: async (ctx) => {
2305
+ const { client, sessionId } = await ensureConnected(_port);
2306
+ const browser = await getBrowser();
2307
+ const subAction = ctx.args[0];
2308
+ if (subAction === 'url') {
2309
+ const url1 = ctx.args[1];
2310
+ const url2 = ctx.args[2];
2311
+ if (!url1 || !url2)
2312
+ throw new Error('Usage: monomind browse diff url <url1> <url2>');
2313
+ // Capture snapshot at url1
2314
+ await browser.openUrl(client, sessionId, url1);
2315
+ await browser.waitFor(client, sessionId, { load: 'load', timeout: 15000 });
2316
+ const snap1 = await browser.captureSnapshot(client, sessionId, { interactiveOnly: ctx.flags.interactive });
2317
+ // Capture snapshot at url2
2318
+ await browser.openUrl(client, sessionId, url2);
2319
+ await browser.waitFor(client, sessionId, { load: 'load', timeout: 15000 });
2320
+ const snap2 = await browser.captureSnapshot(client, sessionId, { interactiveOnly: ctx.flags.interactive });
2321
+ _refs = snap2.refs;
2322
+ // Text diff
2323
+ const lines1 = snap1.text.split('\n');
2324
+ const lines2 = snap2.text.split('\n');
2325
+ const set1 = new Set(lines1);
2326
+ const set2 = new Set(lines2);
2327
+ const onlyIn1 = lines1.filter((l) => !set2.has(l));
2328
+ const onlyIn2 = lines2.filter((l) => !set1.has(l));
2329
+ const changed = onlyIn1.length > 0 || onlyIn2.length > 0;
2330
+ if (ctx.flags.json) {
2331
+ print(JSON.stringify({ changed, url1, url2, onlyIn1, onlyIn2, additions: onlyIn2.length, removals: onlyIn1.length }));
2332
+ }
2333
+ else {
2334
+ if (!changed) {
2335
+ output.printSuccess(`No differences between ${url1} and ${url2}`);
2336
+ }
2337
+ else {
2338
+ output.printWarning(`Diff: ${url1} vs ${url2} — +${onlyIn2.length} lines, -${onlyIn1.length} lines`);
2339
+ for (const l of onlyIn1)
2340
+ print(`\x1b[31m- ${l}\x1b[0m`);
2341
+ for (const l of onlyIn2)
2342
+ print(`\x1b[32m+ ${l}\x1b[0m`);
2343
+ }
2344
+ }
2345
+ return { success: true, data: { changed, url1, url2, additions: onlyIn2.length, removals: onlyIn1.length } };
2346
+ }
2347
+ throw new Error('Usage: monomind browse diff url <url1> <url2>');
2348
+ },
2349
+ };
2350
+ const pushstateCommand = {
2351
+ name: 'pushstate',
2352
+ description: 'SPA navigation via pushState. Usage: monomind browse pushstate /path',
2353
+ action: async (ctx) => {
2354
+ const { client, sessionId } = await ensureConnected(_port);
2355
+ const browser = await getBrowser();
2356
+ const url = ctx.args[0];
2357
+ if (!url)
2358
+ throw new Error('Usage: monomind browse pushstate <url>');
2359
+ await browser.pushState(client, sessionId, url);
2360
+ output.printSuccess(`pushState: ${url}`);
2361
+ return { success: true };
2362
+ },
2363
+ };
2364
+ function tokenizeBatchCommand(input) {
2365
+ const tokens = [];
2366
+ let current = '';
2367
+ let inQuote = null;
2368
+ for (const ch of input.trim()) {
2369
+ if (inQuote) {
2370
+ if (ch === inQuote) {
2371
+ inQuote = null;
2372
+ }
2373
+ else {
2374
+ current += ch;
2375
+ }
2376
+ }
2377
+ else if (ch === '"' || ch === "'") {
2378
+ inQuote = ch;
2379
+ }
2380
+ else if (/\s/.test(ch)) {
2381
+ if (current) {
2382
+ tokens.push(current);
2383
+ current = '';
2384
+ }
2385
+ }
2386
+ else {
2387
+ current += ch;
2388
+ }
2389
+ }
2390
+ if (current)
2391
+ tokens.push(current);
2392
+ return tokens;
2393
+ }
2394
+ const batchCommand = {
2395
+ name: 'batch',
2396
+ description: 'Execute multiple commands. Usage: monomind browse batch "open url" "snapshot -i" "click @e1"',
2397
+ options: [
2398
+ { name: 'bail', type: 'boolean', description: 'Stop on first error', default: false },
2399
+ { name: 'json', type: 'boolean', description: 'Input from JSON stdin', default: false },
2400
+ ],
2401
+ action: async (ctx) => {
2402
+ const commands = ctx.args;
2403
+ if (commands.length === 0)
2404
+ throw new Error('Usage: monomind browse batch "cmd1" "cmd2" ...');
2405
+ const results = [];
2406
+ for (const cmdStr of commands) {
2407
+ const parts = tokenizeBatchCommand(cmdStr);
2408
+ const subName = parts[0];
2409
+ const subArgs = parts.slice(1);
2410
+ const subCmd = browseCommand.subcommands?.find((s) => s.name === subName);
2411
+ if (!subCmd?.action) {
2412
+ const err = `Unknown command: ${subName}`;
2413
+ results.push({ command: cmdStr, success: false, error: err });
2414
+ if (ctx.flags.bail)
2415
+ break;
2416
+ continue;
2417
+ }
2418
+ try {
2419
+ const parsedFlags = { _: [] };
2420
+ const consumedIndices = new Set();
2421
+ // Parse --flags from subArgs, tracking which indices are flag names/values
2422
+ for (let i = 0; i < subArgs.length; i++) {
2423
+ if (subArgs[i].startsWith('--')) {
2424
+ consumedIndices.add(i);
2425
+ const key = subArgs[i].slice(2);
2426
+ const next = subArgs[i + 1];
2427
+ const optDef = subCmd.options?.find((o) => o.name === key);
2428
+ const isBooleanFlag = optDef?.type === 'boolean';
2429
+ if (next && (!next.startsWith('-') || /^-\d/.test(next)) && !isBooleanFlag) {
2430
+ // Non-boolean flags consume the next token as their value (allow negative numbers like -1)
2431
+ consumedIndices.add(i + 1);
2432
+ if (optDef?.type === 'number') {
2433
+ parsedFlags[key] = Number(next);
2434
+ }
2435
+ else {
2436
+ parsedFlags[key] = next;
2437
+ }
2438
+ i++;
2439
+ }
2440
+ else if (isBooleanFlag && (next === 'true' || next === 'false')) {
2441
+ // Explicit boolean value token
2442
+ consumedIndices.add(i + 1);
2443
+ parsedFlags[key] = next !== 'false';
2444
+ i++;
2445
+ }
2446
+ else {
2447
+ parsedFlags[key] = true;
2448
+ }
2449
+ }
2450
+ else if (subArgs[i].startsWith('-') && subArgs[i].length === 2 && /[a-zA-Z]/.test(subArgs[i][1])) {
2451
+ consumedIndices.add(i);
2452
+ const shortKey = subArgs[i][1];
2453
+ const optDef = subCmd.options?.find((o) => o.short === shortKey);
2454
+ if (optDef) {
2455
+ const key = optDef.name;
2456
+ const next = subArgs[i + 1];
2457
+ const isBooleanFlag = optDef.type === 'boolean';
2458
+ if (next && (!next.startsWith('-') || /^-\d/.test(next)) && !isBooleanFlag) {
2459
+ consumedIndices.add(i + 1);
2460
+ parsedFlags[key] = optDef.type === 'number' ? Number(next) : next;
2461
+ i++;
2462
+ }
2463
+ else if (isBooleanFlag && (next === 'true' || next === 'false')) {
2464
+ consumedIndices.add(i + 1);
2465
+ parsedFlags[key] = next !== 'false';
2466
+ i++;
2467
+ }
2468
+ else {
2469
+ parsedFlags[key] = true;
2470
+ }
2471
+ }
2472
+ }
2473
+ }
2474
+ const fakeCtx = {
2475
+ args: subArgs.filter((_, i) => !consumedIndices.has(i)),
2476
+ flags: parsedFlags,
2477
+ cwd: ctx.cwd,
2478
+ interactive: false,
2479
+ };
2480
+ const cmdResult = await subCmd.action(fakeCtx);
2481
+ const succeeded = cmdResult?.success !== false;
2482
+ results.push({ command: cmdStr, success: succeeded, error: succeeded ? undefined : 'Command returned failure' });
2483
+ if (!succeeded && ctx.flags.bail)
2484
+ break;
2485
+ }
2486
+ catch (e) {
2487
+ const err = e instanceof Error ? e.message : String(e);
2488
+ results.push({ command: cmdStr, success: false, error: err });
2489
+ output.printWarning(`Batch error in "${cmdStr}": ${err}`);
2490
+ if (ctx.flags.bail)
2491
+ break;
2492
+ }
2493
+ }
2494
+ const failed = results.filter((r) => !r.success).length;
2495
+ output.printInfo(`Batch: ${results.length - failed}/${results.length} succeeded`);
2496
+ return { success: failed === 0, data: { results } };
2497
+ },
2498
+ };
2499
+ const addinitscriptCommand = {
2500
+ name: 'addinitscript',
2501
+ description: 'Add script to run before page navigation. Usage: monomind browse addinitscript "window.x=1"',
2502
+ action: async (ctx) => {
2503
+ const { client, sessionId } = await ensureConnected(_port);
2504
+ const browser = await getBrowser();
2505
+ const script = ctx.args[0];
2506
+ if (!script)
2507
+ throw new Error('Usage: monomind browse addinitscript "<js>"');
2508
+ const id = await browser.addInitScript(client, sessionId, script);
2509
+ output.printSuccess(`Init script added: ${id}`);
2510
+ return { success: true, data: { identifier: id } };
2511
+ },
2512
+ };
2513
+ const removeinitscriptCommand = {
2514
+ name: 'removeinitscript',
2515
+ description: 'Remove a previously added init script. Usage: monomind browse removeinitscript <id>',
2516
+ action: async (ctx) => {
2517
+ const { client, sessionId } = await ensureConnected(_port);
2518
+ const browser = await getBrowser();
2519
+ const id = ctx.args[0];
2520
+ if (!id)
2521
+ throw new Error('Usage: monomind browse removeinitscript <identifier>');
2522
+ await browser.removeInitScript(client, sessionId, id);
2523
+ output.printSuccess(`Init script removed: ${id}`);
2524
+ return { success: true };
2525
+ },
2526
+ };
2527
+ const connectCommand = {
2528
+ name: 'connect',
2529
+ description: 'Connect to existing Chrome instance. Usage: monomind browse connect [--port 9222] [--target <id>] [--auto-connect]',
2530
+ options: [
2531
+ { name: 'port', short: 'p', type: 'number', description: 'CDP port', default: 9222 },
2532
+ { name: 'target', type: 'string', description: 'Target ID to attach to' },
2533
+ { name: 'auto-connect', type: 'boolean', description: 'Auto-discover running Chrome on ports 9222 and 9229', default: false },
2534
+ ],
2535
+ action: async (ctx) => {
2536
+ let port = ctx.flags.port ?? 9222;
2537
+ if (ctx.flags['auto-connect']) {
2538
+ const probePorts = [9222, 9229];
2539
+ let found = false;
2540
+ for (const p of probePorts) {
2541
+ try {
2542
+ const r = await fetch(`http://127.0.0.1:${p}/json/version`);
2543
+ if (r.ok) {
2544
+ port = p;
2545
+ found = true;
2546
+ break;
2547
+ }
2548
+ }
2549
+ catch { /* port not open */ }
2550
+ }
2551
+ if (!found)
2552
+ throw new Error('No running Chrome instance found. Launch Chrome with --remote-debugging-port or use --port.');
2553
+ }
2554
+ const browser = await getBrowser();
2555
+ if (_client) {
2556
+ const prevSid = _sessionId;
2557
+ const prevClient = _client;
2558
+ if (browser.getHarStatus(prevSid).recording) {
2559
+ try {
2560
+ await browser.stopHarRecording(prevClient, prevSid);
2561
+ }
2562
+ catch { /* ignore */ }
2563
+ }
2564
+ if (browser.getTraceStatus(prevSid)) {
2565
+ try {
2566
+ await browser.stopTrace(prevClient, prevSid);
2567
+ }
2568
+ catch { /* ignore */ }
2569
+ }
2570
+ if (browser.isProfilingActive(prevSid)) {
2571
+ try {
2572
+ await browser.stopCpuProfile(prevClient, prevSid);
2573
+ }
2574
+ catch { /* ignore */ }
2575
+ }
2576
+ browser.teardownRouteInterception(prevSid);
2577
+ browser.stopRequestCapture(prevSid);
2578
+ browser.teardownDialogHandling(prevSid);
2579
+ browser.teardownConsoleCapture(prevSid);
2580
+ prevClient.close();
2581
+ _client = null;
2582
+ _sessionId = '';
2583
+ _targetId = '';
2584
+ _refs = new Map();
2585
+ }
2586
+ const conn = await browser.connectToTarget(port, ctx.flags.target);
2587
+ _client = conn.client;
2588
+ _sessionId = conn.sessionId;
2589
+ _targetId = conn.target.id;
2590
+ _port = port;
2591
+ _refs = new Map();
2592
+ const url = await browser.getCurrentUrl(_client, _sessionId);
2593
+ const title = await browser.getCurrentTitle(_client, _sessionId);
2594
+ output.printSuccess(`Connected: ${title} (${url})`);
2595
+ return { success: true, data: { targetId: _targetId, url, title } };
2596
+ },
2597
+ };
2598
+ const recordCommand = {
2599
+ name: 'record',
2600
+ description: 'Screen recording. Usage: monomind browse record start|stop|restart|status [path]',
2601
+ options: [
2602
+ { name: 'format', type: 'string', description: 'jpeg|png', default: 'jpeg' },
2603
+ { name: 'quality', type: 'number', description: 'Quality 0-100', default: 80 },
2604
+ { name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
2605
+ ],
2606
+ action: async (ctx) => {
2607
+ const { client, sessionId } = await ensureConnected(_port);
2608
+ const browser = await getBrowser();
2609
+ const action = ctx.args[0];
2610
+ if (!action)
2611
+ throw new Error('Usage: monomind browse record start|stop|restart|status');
2612
+ switch (action) {
2613
+ case 'start':
2614
+ await browser.startRecording(client, sessionId, {
2615
+ format: ctx.flags.format,
2616
+ quality: ctx.flags.quality,
2617
+ });
2618
+ output.printSuccess('Recording started');
2619
+ return { success: true };
2620
+ case 'stop': {
2621
+ const path = await browser.stopRecording(client, sessionId, ctx.args[1]);
2622
+ if (ctx.flags.json)
2623
+ print(JSON.stringify({ data: { path } }));
2624
+ else
2625
+ output.printSuccess(`Recording saved: ${path}`);
2626
+ return { success: true, data: { path } };
2627
+ }
2628
+ case 'restart': {
2629
+ const prevStatus = browser.getRecordingStatus(sessionId);
2630
+ let prevPath;
2631
+ if (prevStatus.recording) {
2632
+ prevPath = await browser.stopRecording(client, sessionId, ctx.args[1]);
2633
+ output.printInfo(`Previous recording saved: ${prevPath}`);
2634
+ }
2635
+ await browser.startRecording(client, sessionId, {
2636
+ format: ctx.flags.format,
2637
+ quality: ctx.flags.quality,
2638
+ });
2639
+ output.printSuccess('Recording restarted');
2640
+ return { success: true, data: { previous: prevPath } };
2641
+ }
2642
+ case 'status': {
2643
+ const status = browser.getRecordingStatus(sessionId);
2644
+ if (ctx.flags.json)
2645
+ print(JSON.stringify({ data: status }));
2646
+ else
2647
+ print(`Recording: ${status.recording} | Frames: ${status.frames}`);
2648
+ return { success: true, data: status };
2649
+ }
2650
+ default:
2651
+ throw new Error('Usage: monomind browse record start|stop|restart|status [path]');
2652
+ }
2653
+ },
2654
+ };
2655
+ const traceCommand = {
2656
+ name: 'trace',
2657
+ description: 'CDP performance trace. Usage: monomind browse trace start|stop [path]',
2658
+ options: [
2659
+ { name: 'screenshots', type: 'boolean', description: 'Include screenshots', default: false },
2660
+ { name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
2661
+ ],
2662
+ action: async (ctx) => {
2663
+ const { client, sessionId } = await ensureConnected(_port);
2664
+ const browser = await getBrowser();
2665
+ const action = ctx.args[0];
2666
+ if (!action)
2667
+ throw new Error('Usage: monomind browse trace start|stop [path]');
2668
+ switch (action) {
2669
+ case 'start':
2670
+ await browser.startTrace(client, sessionId, { screenshots: ctx.flags.screenshots });
2671
+ output.printSuccess('Trace started');
2672
+ return { success: true };
2673
+ case 'stop': {
2674
+ const path = await browser.stopTrace(client, sessionId, ctx.args[1]);
2675
+ if (ctx.flags.json)
2676
+ print(JSON.stringify({ data: { path } }));
2677
+ else
2678
+ output.printSuccess(`Trace saved: ${path}`);
2679
+ return { success: true, data: { path } };
2680
+ }
2681
+ case 'status':
2682
+ print(browser.getTraceStatus(sessionId) ? 'Tracing active' : 'Not tracing');
2683
+ return { success: true };
2684
+ default:
2685
+ throw new Error('Usage: monomind browse trace start|stop|status [path]');
2686
+ }
2687
+ },
2688
+ };
2689
+ const profilerCommand = {
2690
+ name: 'profiler',
2691
+ description: 'CPU profiler. Usage: monomind browse profiler start|stop|heap [path]',
2692
+ options: [
2693
+ { name: 'interval', type: 'number', description: 'Sampling interval µs', default: 1000 },
2694
+ { name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
2695
+ ],
2696
+ action: async (ctx) => {
2697
+ const { client, sessionId } = await ensureConnected(_port);
2698
+ const browser = await getBrowser();
2699
+ const action = ctx.args[0];
2700
+ if (!action)
2701
+ throw new Error('Usage: monomind browse profiler start|stop|heap [path]');
2702
+ switch (action) {
2703
+ case 'start':
2704
+ await browser.startCpuProfile(client, sessionId, { samplingInterval: ctx.flags.interval });
2705
+ output.printSuccess('CPU profiler started');
2706
+ return { success: true };
2707
+ case 'stop': {
2708
+ const path = await browser.stopCpuProfile(client, sessionId, ctx.args[1]);
2709
+ if (ctx.flags.json)
2710
+ print(JSON.stringify({ data: { path } }));
2711
+ else
2712
+ output.printSuccess(`Profile saved: ${path}`);
2713
+ return { success: true, data: { path } };
2714
+ }
2715
+ case 'heap': {
2716
+ const path = await browser.startHeapSnapshot(client, sessionId, ctx.args[1]);
2717
+ if (ctx.flags.json)
2718
+ print(JSON.stringify({ data: { path } }));
2719
+ else
2720
+ output.printSuccess(`Heap snapshot saved: ${path}`);
2721
+ return { success: true, data: { path } };
2722
+ }
2723
+ default:
2724
+ throw new Error('Usage: monomind browse profiler start|stop|heap [path]');
2725
+ }
2726
+ },
2727
+ };
2728
+ const vitalsCommand = {
2729
+ name: 'vitals',
2730
+ description: 'Collect Core Web Vitals. Usage: monomind browse vitals [--wait 2000]',
2731
+ options: [
2732
+ { name: 'wait', type: 'number', description: 'Wait ms for observers', default: 2000 },
2733
+ { name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
2734
+ ],
2735
+ action: async (ctx) => {
2736
+ const { client, sessionId } = await ensureConnected(_port);
2737
+ const browser = await getBrowser();
2738
+ const vitals = await browser.collectVitals(client, sessionId, ctx.flags.wait);
2739
+ if (ctx.flags.json) {
2740
+ print(JSON.stringify({ data: vitals }));
2741
+ }
2742
+ else {
2743
+ print(browser.formatVitals(vitals));
2744
+ }
2745
+ return { success: true, data: vitals };
2746
+ },
2747
+ };
2748
+ const harCommand = {
2749
+ name: 'har',
2750
+ description: 'HAR network recording. Usage: monomind browse har start|stop|status [path]',
2751
+ options: [
2752
+ { name: 'bodies', type: 'boolean', description: 'Capture response bodies', default: false },
2753
+ { name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
2754
+ ],
2755
+ action: async (ctx) => {
2756
+ const { client, sessionId } = await ensureConnected(_port);
2757
+ const browser = await getBrowser();
2758
+ const action = ctx.args[0];
2759
+ if (!action)
2760
+ throw new Error('Usage: monomind browse har start|stop|status [path]');
2761
+ switch (action) {
2762
+ case 'start':
2763
+ await browser.startHarRecording(client, sessionId);
2764
+ output.printSuccess('HAR recording started');
2765
+ return { success: true };
2766
+ case 'stop': {
2767
+ const path = await browser.stopHarRecording(client, sessionId, ctx.args[1], ctx.flags.bodies);
2768
+ if (ctx.flags.json)
2769
+ print(JSON.stringify({ data: { path } }));
2770
+ else
2771
+ output.printSuccess(`HAR saved: ${path}`);
2772
+ return { success: true, data: { path } };
2773
+ }
2774
+ case 'status': {
2775
+ const status = browser.getHarStatus(sessionId);
2776
+ if (ctx.flags.json)
2777
+ print(JSON.stringify({ data: status }));
2778
+ else
2779
+ print(`Recording: ${status.recording} | Requests: ${status.requestCount}`);
2780
+ return { success: true, data: status };
2781
+ }
2782
+ default:
2783
+ throw new Error('Usage: monomind browse har start|stop|status [path]');
2784
+ }
2785
+ },
2786
+ };
2787
+ const resizeCommand = {
2788
+ name: 'resize',
2789
+ description: 'Resize browser window. Usage: monomind browse resize <width> <height>',
2790
+ action: async (ctx) => {
2791
+ const { client, sessionId } = await ensureConnected(_port);
2792
+ const browser = await getBrowser();
2793
+ const width = parseInt(ctx.args[0], 10);
2794
+ const height = parseInt(ctx.args[1], 10);
2795
+ if (isNaN(width) || isNaN(height))
2796
+ throw new Error('Usage: monomind browse resize <width> <height>');
2797
+ await browser.setViewport(client, sessionId, width, height);
2798
+ output.printSuccess(`Resized to ${width}x${height}`);
2799
+ return { success: true, data: { width, height } };
2800
+ },
2801
+ };
2802
+ // ---------------------------------------------------------------------------
2803
+ // Commander-to-internal adapter
2804
+ // ---------------------------------------------------------------------------
2805
+ /**
2806
+ * Wraps a commander.Command instance as an internal Command object so it can
2807
+ * participate in the browse command's subcommands array. The adapter
2808
+ * reconstructs a raw argv string from the context that was parsed by the
2809
+ * internal CLI, then hands it off to commander's parseAsync.
2810
+ */
2811
+ function wrapCommanderCommand(factory) {
2812
+ // Lazily instantiate so we don't pay import cost at startup
2813
+ let _cmd = null;
2814
+ const getCmd = () => {
2815
+ if (!_cmd)
2816
+ _cmd = factory();
2817
+ return _cmd;
2818
+ };
2819
+ const cmd = getCmd();
2820
+ const subcommandDefs = cmd.commands.map((sub) => ({
2821
+ name: sub.name(),
2822
+ description: sub.description(),
2823
+ action: async (ctx) => {
2824
+ // Rebuild argv: node <cmd> <sub> [positional args] [--flag [value] ...]
2825
+ const argv = ['node', cmd.name(), sub.name(), ...ctx.args];
2826
+ for (const [key, val] of Object.entries(ctx.flags)) {
2827
+ if (key === '_')
2828
+ continue;
2829
+ if (typeof val === 'boolean') {
2830
+ if (val)
2831
+ argv.push(`--${key}`);
2832
+ }
2833
+ else if (val !== undefined && val !== null) {
2834
+ argv.push(`--${key}`, String(val));
2835
+ }
2836
+ }
2837
+ // Re-use the same commander instance to keep state (e.g. option defaults)
2838
+ await getCmd().parseAsync(argv, { from: 'user' });
2839
+ return { success: true };
2840
+ },
2841
+ }));
2842
+ return {
2843
+ name: cmd.name(),
2844
+ description: cmd.description(),
2845
+ subcommands: subcommandDefs,
2846
+ action: async (ctx) => {
2847
+ // No subcommand provided — show commander help
2848
+ const argv = ['node', cmd.name(), ...ctx.args];
2849
+ for (const [key, val] of Object.entries(ctx.flags)) {
2850
+ if (key === '_')
2851
+ continue;
2852
+ if (typeof val === 'boolean') {
2853
+ if (val)
2854
+ argv.push(`--${key}`);
2855
+ }
2856
+ else if (val !== undefined && val !== null) {
2857
+ argv.push(`--${key}`, String(val));
2858
+ }
2859
+ }
2860
+ await getCmd().parseAsync(argv, { from: 'user' });
2861
+ return { success: true };
2862
+ },
2863
+ };
2864
+ }
2865
+ const playbookSubcommand = wrapCommanderCommand(createPlaybookCommand);
2866
+ const actionSubcommand = wrapCommanderCommand(createActionCommand);
2867
+ const platformSubcommand = wrapCommanderCommand(createPlatformCommand);
2868
+ // ---------------------------------------------------------------------------
2869
+ // Root browse command
2870
+ // ---------------------------------------------------------------------------
2871
+ const browseCommand = {
2872
+ name: 'browse',
2873
+ description: 'Native browser automation via Chrome DevTools Protocol',
2874
+ subcommands: [
2875
+ openCommand,
2876
+ snapshotCommand,
2877
+ clickCommand,
2878
+ dblclickCommand,
2879
+ fillCommand,
2880
+ typeCommand,
2881
+ pressCommand,
2882
+ keyboardCommand,
2883
+ keydownCommand,
2884
+ keyupCommand,
2885
+ hoverCommand,
2886
+ focusCommand,
2887
+ selectCommand,
2888
+ checkCommand,
2889
+ uncheckCommand,
2890
+ isvisibleCommand,
2891
+ isenabledCommand,
2892
+ ischeckedCommand,
2893
+ tapCommand,
2894
+ swipeCommand,
2895
+ scrollIntoViewCommand,
2896
+ dragCommand,
2897
+ uploadCommand,
2898
+ downloadCommand,
2899
+ mouseCommand,
2900
+ clipboardCommand,
2901
+ waitCommand,
2902
+ screenshotCommand,
2903
+ getCommand,
2904
+ scrollCommand,
2905
+ navigateCommand,
2906
+ setCommand,
2907
+ stateCommand,
2908
+ networkCommand,
2909
+ evalCommand,
2910
+ dialogCommand,
2911
+ frameCommand,
2912
+ tabCommand,
2913
+ windowCommand,
2914
+ consoleLogCommand,
2915
+ errorsCommand,
2916
+ storageCommand,
2917
+ cookiesCommand,
2918
+ pdfCommand,
2919
+ isCommand,
2920
+ findCommand,
2921
+ highlightCommand,
2922
+ diffCommand,
2923
+ pushstateCommand,
2924
+ batchCommand,
2925
+ addinitscriptCommand,
2926
+ removeinitscriptCommand,
2927
+ connectCommand,
2928
+ recordCommand,
2929
+ traceCommand,
2930
+ profilerCommand,
2931
+ vitalsCommand,
2932
+ harCommand,
2933
+ resizeCommand,
2934
+ closeCommand,
2935
+ playbookSubcommand,
2936
+ actionSubcommand,
2937
+ platformSubcommand,
2938
+ ],
2939
+ options: [
2940
+ { name: 'port', short: 'p', type: 'number', description: 'CDP debug port', default: 9222 },
2941
+ { name: 'session', short: 's', type: 'string', description: 'Named session to use' },
2942
+ ],
2943
+ examples: [
2944
+ { command: 'monomind browse open https://example.com', description: 'Open a URL' },
2945
+ { command: 'monomind browse snapshot -i', description: 'Interactive-only snapshot (93% token reduction)' },
2946
+ { command: 'monomind browse click @e3', description: 'Click element by ref' },
2947
+ { command: 'monomind browse fill @e1 "user@example.com"', description: 'Fill an input' },
2948
+ { command: 'monomind browse press Enter', description: 'Press Enter key' },
2949
+ { command: 'monomind browse wait --url "**/dashboard"', description: 'Wait for URL pattern' },
2950
+ { command: 'monomind browse wait --text "Success"', description: 'Wait for text' },
2951
+ { command: 'monomind browse wait --load networkidle', description: 'Wait for network idle' },
2952
+ { command: 'monomind browse screenshot ./output.png', description: 'Take screenshot' },
2953
+ { command: 'monomind browse get url', description: 'Get current URL' },
2954
+ { command: 'monomind browse scroll down', description: 'Scroll down 300px' },
2955
+ { command: 'monomind browse set viewport 375 812', description: 'Set mobile viewport' },
2956
+ { command: 'monomind browse state save my-session', description: 'Save session state' },
2957
+ { command: 'monomind browse navigate back', description: 'Navigate back' },
2958
+ { command: 'monomind browse eval "document.title"', description: 'Evaluate JavaScript' },
2959
+ { command: 'monomind browse network route --pattern "https://api.*" --abort', description: 'Abort API calls' },
2960
+ { command: 'monomind browse close', description: 'Close browser session' },
2961
+ ],
2962
+ action: async (_ctx) => {
2963
+ output.printInfo('Native browser automation via Chrome DevTools Protocol.');
2964
+ output.printInfo('');
2965
+ output.printInfo('Usage: monomind browse <subcommand> [options]');
2966
+ output.printInfo('');
2967
+ output.printInfo('Subcommands:');
2968
+ output.printInfo(' open Open a URL');
2969
+ output.printInfo(' snapshot Capture accessibility snapshot with refs');
2970
+ output.printInfo(' click Click an element by ref');
2971
+ output.printInfo(' dblclick Double-click an element');
2972
+ output.printInfo(' fill Fill an input (clears first)');
2973
+ output.printInfo(' type Type into element (appends)');
2974
+ output.printInfo(' press Press a keyboard key');
2975
+ output.printInfo(' keyboard Insert text directly');
2976
+ output.printInfo(' keydown Hold a key down');
2977
+ output.printInfo(' keyup Release a held key');
2978
+ output.printInfo(' hover Hover over element');
2979
+ output.printInfo(' focus Focus an element');
2980
+ output.printInfo(' select Select a dropdown option');
2981
+ output.printInfo(' check Check a checkbox');
2982
+ output.printInfo(' uncheck Uncheck a checkbox');
2983
+ output.printInfo(' isvisible Check if element is visible');
2984
+ output.printInfo(' isenabled Check if element is enabled');
2985
+ output.printInfo(' ischecked Check if checkbox/radio is checked');
2986
+ output.printInfo(' tap Tap element with touch event (mobile)');
2987
+ output.printInfo(' scrollintoview Scroll element into view');
2988
+ output.printInfo(' drag Drag element to another element');
2989
+ output.printInfo(' upload Upload file(s) to file input');
2990
+ output.printInfo(' mouse Fine-grained mouse control');
2991
+ output.printInfo(' clipboard Read/write clipboard');
2992
+ output.printInfo(' wait Wait for a condition');
2993
+ output.printInfo(' screenshot Take a screenshot');
2994
+ output.printInfo(' get Get page info (url, title, text, html)');
2995
+ output.printInfo(' scroll Scroll the page');
2996
+ output.printInfo(' navigate Navigate history (back/forward/reload)');
2997
+ output.printInfo(' set Configure viewport, device, user agent');
2998
+ output.printInfo(' state Save/load/list session state');
2999
+ output.printInfo(' network Network interception and cookies');
3000
+ output.printInfo(' eval Evaluate JavaScript');
3001
+ output.printInfo(' dialog Handle browser dialogs');
3002
+ output.printInfo(' frame Switch to iframe');
3003
+ output.printInfo(' tab Tab management');
3004
+ output.printInfo(' console View captured console messages');
3005
+ output.printInfo(' errors View page JS errors');
3006
+ output.printInfo(' storage localStorage/sessionStorage management');
3007
+ output.printInfo(' cookies Cookie management');
3008
+ output.printInfo(' pdf Save page as PDF');
3009
+ output.printInfo(' is Check element state (visible/enabled/checked)');
3010
+ output.printInfo(' find Find elements by semantic locators');
3011
+ output.printInfo(' highlight Highlight an element visually');
3012
+ output.printInfo(' pushstate SPA navigation via pushState');
3013
+ output.printInfo(' batch Execute multiple commands');
3014
+ output.printInfo(' addinitscript Add script to run before page navigation');
3015
+ output.printInfo(' removeinitscript Remove a previously added init script');
3016
+ output.printInfo(' close Close the browser session');
3017
+ return { success: true };
3018
+ },
3019
+ };
3020
+ export default browseCommand;
3021
+ //# sourceMappingURL=commands.js.map