@monoes/monobrowse 1.0.11 → 1.0.12

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