@monoes/monobrowse 1.0.11 → 1.0.13

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 (380) 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__/profiler-heap.test.d.ts +2 -0
  28. package/dist/src/__tests__/profiler-heap.test.d.ts.map +1 -0
  29. package/dist/src/__tests__/profiler-heap.test.js +154 -0
  30. package/dist/src/__tests__/profiler-heap.test.js.map +1 -0
  31. package/dist/src/__tests__/reap-idle-browser.test.d.ts +15 -0
  32. package/dist/src/__tests__/reap-idle-browser.test.d.ts.map +1 -0
  33. package/dist/src/__tests__/reap-idle-browser.test.js +238 -0
  34. package/dist/src/__tests__/reap-idle-browser.test.js.map +1 -0
  35. package/dist/src/__tests__/ref-cache.test.js +18 -10
  36. package/dist/src/__tests__/ref-cache.test.js.map +1 -1
  37. package/dist/src/__tests__/report-a11y.test.d.ts +2 -0
  38. package/dist/src/__tests__/report-a11y.test.d.ts.map +1 -0
  39. package/dist/src/__tests__/report-a11y.test.js +147 -0
  40. package/dist/src/__tests__/report-a11y.test.js.map +1 -0
  41. package/dist/src/__tests__/report-analyze.test.d.ts +2 -0
  42. package/dist/src/__tests__/report-analyze.test.d.ts.map +1 -0
  43. package/dist/src/__tests__/report-analyze.test.js +157 -0
  44. package/dist/src/__tests__/report-analyze.test.js.map +1 -0
  45. package/dist/src/__tests__/report-budget.test.d.ts +2 -0
  46. package/dist/src/__tests__/report-budget.test.d.ts.map +1 -0
  47. package/dist/src/__tests__/report-budget.test.js +80 -0
  48. package/dist/src/__tests__/report-budget.test.js.map +1 -0
  49. package/dist/src/__tests__/report-diff.test.d.ts +2 -0
  50. package/dist/src/__tests__/report-diff.test.d.ts.map +1 -0
  51. package/dist/src/__tests__/report-diff.test.js +231 -0
  52. package/dist/src/__tests__/report-diff.test.js.map +1 -0
  53. package/dist/src/__tests__/report-evidence.test.d.ts +2 -0
  54. package/dist/src/__tests__/report-evidence.test.d.ts.map +1 -0
  55. package/dist/src/__tests__/report-evidence.test.js +214 -0
  56. package/dist/src/__tests__/report-evidence.test.js.map +1 -0
  57. package/dist/src/__tests__/report-flake.test.d.ts +2 -0
  58. package/dist/src/__tests__/report-flake.test.d.ts.map +1 -0
  59. package/dist/src/__tests__/report-flake.test.js +182 -0
  60. package/dist/src/__tests__/report-flake.test.js.map +1 -0
  61. package/dist/src/__tests__/report-history.test.d.ts +2 -0
  62. package/dist/src/__tests__/report-history.test.d.ts.map +1 -0
  63. package/dist/src/__tests__/report-history.test.js +229 -0
  64. package/dist/src/__tests__/report-history.test.js.map +1 -0
  65. package/dist/src/__tests__/report-output.test.d.ts +2 -0
  66. package/dist/src/__tests__/report-output.test.d.ts.map +1 -0
  67. package/dist/src/__tests__/report-output.test.js +169 -0
  68. package/dist/src/__tests__/report-output.test.js.map +1 -0
  69. package/dist/src/__tests__/report-png.test.d.ts +2 -0
  70. package/dist/src/__tests__/report-png.test.d.ts.map +1 -0
  71. package/dist/src/__tests__/report-png.test.js +84 -0
  72. package/dist/src/__tests__/report-png.test.js.map +1 -0
  73. package/dist/src/__tests__/report-render-extras.test.d.ts +2 -0
  74. package/dist/src/__tests__/report-render-extras.test.d.ts.map +1 -0
  75. package/dist/src/__tests__/report-render-extras.test.js +285 -0
  76. package/dist/src/__tests__/report-render-extras.test.js.map +1 -0
  77. package/dist/src/__tests__/report-render.test.d.ts +2 -0
  78. package/dist/src/__tests__/report-render.test.d.ts.map +1 -0
  79. package/dist/src/__tests__/report-render.test.js +143 -0
  80. package/dist/src/__tests__/report-render.test.js.map +1 -0
  81. package/dist/src/__tests__/report-run.test.d.ts +2 -0
  82. package/dist/src/__tests__/report-run.test.d.ts.map +1 -0
  83. package/dist/src/__tests__/report-run.test.js +234 -0
  84. package/dist/src/__tests__/report-run.test.js.map +1 -0
  85. package/dist/src/__tests__/session-state.test.d.ts +8 -0
  86. package/dist/src/__tests__/session-state.test.d.ts.map +1 -1
  87. package/dist/src/__tests__/session-state.test.js +10 -10
  88. package/dist/src/__tests__/session-state.test.js.map +1 -1
  89. package/dist/src/__tests__/snapshot.test.js +44 -14
  90. package/dist/src/__tests__/snapshot.test.js.map +1 -1
  91. package/dist/src/__tests__/storage-eval.test.js +14 -6
  92. package/dist/src/__tests__/storage-eval.test.js.map +1 -1
  93. package/dist/src/__tests__/tabs.test.js +25 -9
  94. package/dist/src/__tests__/tabs.test.js.map +1 -1
  95. package/dist/src/browser/action-builder/analyzer.d.ts +1 -1
  96. package/dist/src/browser/action-builder/analyzer.d.ts.map +1 -1
  97. package/dist/src/browser/action-builder/analyzer.js +25 -9
  98. package/dist/src/browser/action-builder/analyzer.js.map +1 -1
  99. package/dist/src/browser/action-builder/types.d.ts.map +1 -1
  100. package/dist/src/browser/actions.d.ts +1 -1
  101. package/dist/src/browser/actions.d.ts.map +1 -1
  102. package/dist/src/browser/actions.js +16 -6
  103. package/dist/src/browser/actions.js.map +1 -1
  104. package/dist/src/browser/adapters/gemini.d.ts.map +1 -1
  105. package/dist/src/browser/adapters/gemini.js.map +1 -1
  106. package/dist/src/browser/adapters/google.d.ts.map +1 -1
  107. package/dist/src/browser/adapters/google.js.map +1 -1
  108. package/dist/src/browser/adapters/index.d.ts.map +1 -1
  109. package/dist/src/browser/adapters/index.js +3 -3
  110. package/dist/src/browser/adapters/index.js.map +1 -1
  111. package/dist/src/browser/adapters/instagram.d.ts.map +1 -1
  112. package/dist/src/browser/adapters/instagram.js.map +1 -1
  113. package/dist/src/browser/adapters/linkedin.d.ts.map +1 -1
  114. package/dist/src/browser/adapters/linkedin.js.map +1 -1
  115. package/dist/src/browser/adapters/microsoft.d.ts.map +1 -1
  116. package/dist/src/browser/adapters/microsoft.js.map +1 -1
  117. package/dist/src/browser/adapters/x.d.ts.map +1 -1
  118. package/dist/src/browser/adapters/x.js.map +1 -1
  119. package/dist/src/browser/browser.d.ts +30 -0
  120. package/dist/src/browser/browser.d.ts.map +1 -1
  121. package/dist/src/browser/browser.js +122 -15
  122. package/dist/src/browser/browser.js.map +1 -1
  123. package/dist/src/browser/cdp.d.ts.map +1 -1
  124. package/dist/src/browser/cdp.js +21 -8
  125. package/dist/src/browser/cdp.js.map +1 -1
  126. package/dist/src/browser/console-log.d.ts.map +1 -1
  127. package/dist/src/browser/console-log.js +2 -2
  128. package/dist/src/browser/console-log.js.map +1 -1
  129. package/dist/src/browser/dashboard/server.d.ts.map +1 -1
  130. package/dist/src/browser/dashboard/server.js +29 -19
  131. package/dist/src/browser/dashboard/server.js.map +1 -1
  132. package/dist/src/browser/dashboard/ui.html +153 -165
  133. package/dist/src/browser/dialog.d.ts.map +1 -1
  134. package/dist/src/browser/dialog.js +3 -1
  135. package/dist/src/browser/dialog.js.map +1 -1
  136. package/dist/src/browser/emulation.d.ts.map +1 -1
  137. package/dist/src/browser/emulation.js +21 -6
  138. package/dist/src/browser/emulation.js.map +1 -1
  139. package/dist/src/browser/find.d.ts +4 -4
  140. package/dist/src/browser/find.d.ts.map +1 -1
  141. package/dist/src/browser/find.js +14 -6
  142. package/dist/src/browser/find.js.map +1 -1
  143. package/dist/src/browser/har.d.ts.map +1 -1
  144. package/dist/src/browser/har.js +14 -7
  145. package/dist/src/browser/har.js.map +1 -1
  146. package/dist/src/browser/index.d.ts +17 -16
  147. package/dist/src/browser/index.d.ts.map +1 -1
  148. package/dist/src/browser/index.js +17 -16
  149. package/dist/src/browser/index.js.map +1 -1
  150. package/dist/src/browser/network.d.ts.map +1 -1
  151. package/dist/src/browser/network.js +14 -4
  152. package/dist/src/browser/network.js.map +1 -1
  153. package/dist/src/browser/pdf.d.ts.map +1 -1
  154. package/dist/src/browser/pdf.js +3 -3
  155. package/dist/src/browser/pdf.js.map +1 -1
  156. package/dist/src/browser/profiler.d.ts.map +1 -1
  157. package/dist/src/browser/profiler.js +22 -20
  158. package/dist/src/browser/profiler.js.map +1 -1
  159. package/dist/src/browser/record.d.ts +1 -1
  160. package/dist/src/browser/record.d.ts.map +1 -1
  161. package/dist/src/browser/record.js +18 -7
  162. package/dist/src/browser/record.js.map +1 -1
  163. package/dist/src/browser/ref-cache.d.ts.map +1 -1
  164. package/dist/src/browser/ref-cache.js +9 -4
  165. package/dist/src/browser/ref-cache.js.map +1 -1
  166. package/dist/src/browser/screenshot.d.ts.map +1 -1
  167. package/dist/src/browser/screenshot.js +11 -7
  168. package/dist/src/browser/screenshot.js.map +1 -1
  169. package/dist/src/browser/session.d.ts +1 -1
  170. package/dist/src/browser/session.d.ts.map +1 -1
  171. package/dist/src/browser/session.js +27 -8
  172. package/dist/src/browser/session.js.map +1 -1
  173. package/dist/src/browser/snapshot.d.ts +1 -1
  174. package/dist/src/browser/snapshot.d.ts.map +1 -1
  175. package/dist/src/browser/snapshot.js +8 -4
  176. package/dist/src/browser/snapshot.js.map +1 -1
  177. package/dist/src/browser/storage.d.ts.map +1 -1
  178. package/dist/src/browser/storage.js.map +1 -1
  179. package/dist/src/browser/tabs.d.ts.map +1 -1
  180. package/dist/src/browser/tabs.js +9 -3
  181. package/dist/src/browser/tabs.js.map +1 -1
  182. package/dist/src/browser/trace.d.ts.map +1 -1
  183. package/dist/src/browser/trace.js +3 -3
  184. package/dist/src/browser/trace.js.map +1 -1
  185. package/dist/src/browser/types.d.ts.map +1 -1
  186. package/dist/src/browser/types.js +23 -4
  187. package/dist/src/browser/types.js.map +1 -1
  188. package/dist/src/browser/vitals.d.ts.map +1 -1
  189. package/dist/src/browser/vitals.js +1 -1
  190. package/dist/src/browser/vitals.js.map +1 -1
  191. package/dist/src/browser/wait.d.ts.map +1 -1
  192. package/dist/src/browser/wait.js +11 -5
  193. package/dist/src/browser/wait.js.map +1 -1
  194. package/dist/src/cli/action.d.ts.map +1 -1
  195. package/dist/src/cli/action.js +14 -11
  196. package/dist/src/cli/action.js.map +1 -1
  197. package/dist/src/cli/commands.d.ts.map +1 -1
  198. package/dist/src/cli/commands.js +707 -159
  199. package/dist/src/cli/commands.js.map +1 -1
  200. package/dist/src/cli/output.js +5 -5
  201. package/dist/src/cli/output.js.map +1 -1
  202. package/dist/src/cli/platform.d.ts.map +1 -1
  203. package/dist/src/cli/platform.js +15 -11
  204. package/dist/src/cli/platform.js.map +1 -1
  205. package/dist/src/cli/types.d.ts +1 -1
  206. package/dist/src/cli/types.d.ts.map +1 -1
  207. package/dist/src/cli.js +21 -9
  208. package/dist/src/cli.js.map +1 -1
  209. package/dist/src/index.d.ts +2 -2
  210. package/dist/src/index.d.ts.map +1 -1
  211. package/dist/src/index.js +2 -2
  212. package/dist/src/index.js.map +1 -1
  213. package/dist/src/report/a11y.d.ts +52 -0
  214. package/dist/src/report/a11y.d.ts.map +1 -0
  215. package/dist/src/report/a11y.js +231 -0
  216. package/dist/src/report/a11y.js.map +1 -0
  217. package/dist/src/report/analyze.d.ts +20 -0
  218. package/dist/src/report/analyze.d.ts.map +1 -0
  219. package/dist/src/report/analyze.js +136 -0
  220. package/dist/src/report/analyze.js.map +1 -0
  221. package/dist/src/report/budget.d.ts +27 -0
  222. package/dist/src/report/budget.d.ts.map +1 -0
  223. package/dist/src/report/budget.js +143 -0
  224. package/dist/src/report/budget.js.map +1 -0
  225. package/dist/src/report/collect-a11y.d.ts +25 -0
  226. package/dist/src/report/collect-a11y.d.ts.map +1 -0
  227. package/dist/src/report/collect-a11y.js +154 -0
  228. package/dist/src/report/collect-a11y.js.map +1 -0
  229. package/dist/src/report/collect.d.ts +45 -0
  230. package/dist/src/report/collect.d.ts.map +1 -0
  231. package/dist/src/report/collect.js +275 -0
  232. package/dist/src/report/collect.js.map +1 -0
  233. package/dist/src/report/evidence.d.ts +80 -0
  234. package/dist/src/report/evidence.d.ts.map +1 -0
  235. package/dist/src/report/evidence.js +279 -0
  236. package/dist/src/report/evidence.js.map +1 -0
  237. package/dist/src/report/flake.d.ts +88 -0
  238. package/dist/src/report/flake.d.ts.map +1 -0
  239. package/dist/src/report/flake.js +232 -0
  240. package/dist/src/report/flake.js.map +1 -0
  241. package/dist/src/report/history.d.ts +85 -0
  242. package/dist/src/report/history.d.ts.map +1 -0
  243. package/dist/src/report/history.js +176 -0
  244. package/dist/src/report/history.js.map +1 -0
  245. package/dist/src/report/index.d.ts +102 -0
  246. package/dist/src/report/index.d.ts.map +1 -0
  247. package/dist/src/report/index.js +216 -0
  248. package/dist/src/report/index.js.map +1 -0
  249. package/dist/src/report/pixel-diff.d.ts +30 -0
  250. package/dist/src/report/pixel-diff.d.ts.map +1 -0
  251. package/dist/src/report/pixel-diff.js +175 -0
  252. package/dist/src/report/pixel-diff.js.map +1 -0
  253. package/dist/src/report/png.d.ts +28 -0
  254. package/dist/src/report/png.d.ts.map +1 -0
  255. package/dist/src/report/png.js +259 -0
  256. package/dist/src/report/png.js.map +1 -0
  257. package/dist/src/report/render-evidence.d.ts +16 -0
  258. package/dist/src/report/render-evidence.d.ts.map +1 -0
  259. package/dist/src/report/render-evidence.js +174 -0
  260. package/dist/src/report/render-evidence.js.map +1 -0
  261. package/dist/src/report/render-trend.d.ts +16 -0
  262. package/dist/src/report/render-trend.d.ts.map +1 -0
  263. package/dist/src/report/render-trend.js +194 -0
  264. package/dist/src/report/render-trend.js.map +1 -0
  265. package/dist/src/report/render.d.ts +21 -0
  266. package/dist/src/report/render.d.ts.map +1 -0
  267. package/dist/src/report/render.js +286 -0
  268. package/dist/src/report/render.js.map +1 -0
  269. package/dist/src/report/run-diff.d.ts +18 -0
  270. package/dist/src/report/run-diff.d.ts.map +1 -0
  271. package/dist/src/report/run-diff.js +51 -0
  272. package/dist/src/report/run-diff.js.map +1 -0
  273. package/dist/src/report/structure.d.ts +41 -0
  274. package/dist/src/report/structure.d.ts.map +1 -0
  275. package/dist/src/report/structure.js +210 -0
  276. package/dist/src/report/structure.js.map +1 -0
  277. package/dist/src/report/trend.d.ts +26 -0
  278. package/dist/src/report/trend.d.ts.map +1 -0
  279. package/dist/src/report/trend.js +164 -0
  280. package/dist/src/report/trend.js.map +1 -0
  281. package/dist/src/report/types.d.ts +320 -0
  282. package/dist/src/report/types.d.ts.map +1 -0
  283. package/dist/src/report/types.js +11 -0
  284. package/dist/src/report/types.js.map +1 -0
  285. package/dist/src/report/util.d.ts +17 -0
  286. package/dist/src/report/util.d.ts.map +1 -0
  287. package/dist/src/report/util.js +45 -0
  288. package/dist/src/report/util.js.map +1 -0
  289. package/dist/tsconfig.tsbuildinfo +1 -1
  290. package/package.json +1 -1
  291. package/src/__tests__/adapters.test.ts +15 -8
  292. package/src/__tests__/batch-eval-parsing.test.ts +4 -2
  293. package/src/__tests__/box-model.test.ts +9 -5
  294. package/src/__tests__/browser-launch.test.ts +26 -15
  295. package/src/__tests__/cdp-client.test.ts +25 -9
  296. package/src/__tests__/close-browser.test.ts +11 -6
  297. package/src/__tests__/console-log.test.ts +36 -13
  298. package/src/__tests__/find.test.ts +58 -20
  299. package/src/__tests__/frame-command.test.ts +12 -6
  300. package/src/__tests__/open-url-navigate-error.test.ts +2 -4
  301. package/src/__tests__/output.test.ts +1 -1
  302. package/src/__tests__/profiler-heap.test.ts +191 -0
  303. package/src/__tests__/reap-idle-browser.test.ts +276 -0
  304. package/src/__tests__/ref-cache.test.ts +25 -12
  305. package/src/__tests__/report-a11y.test.ts +180 -0
  306. package/src/__tests__/report-analyze.test.ts +202 -0
  307. package/src/__tests__/report-budget.test.ts +92 -0
  308. package/src/__tests__/report-diff.test.ts +272 -0
  309. package/src/__tests__/report-evidence.test.ts +276 -0
  310. package/src/__tests__/report-flake.test.ts +214 -0
  311. package/src/__tests__/report-history.test.ts +293 -0
  312. package/src/__tests__/report-output.test.ts +210 -0
  313. package/src/__tests__/report-png.test.ts +95 -0
  314. package/src/__tests__/report-render-extras.test.ts +315 -0
  315. package/src/__tests__/report-render.test.ts +175 -0
  316. package/src/__tests__/report-run.test.ts +264 -0
  317. package/src/__tests__/session-state.test.ts +40 -28
  318. package/src/__tests__/snapshot.test.ts +60 -21
  319. package/src/__tests__/storage-eval.test.ts +30 -16
  320. package/src/__tests__/tabs.test.ts +26 -10
  321. package/src/browser/action-builder/analyzer.ts +39 -15
  322. package/src/browser/action-builder/types.ts +45 -8
  323. package/src/browser/actions.ts +288 -123
  324. package/src/browser/adapters/gemini.ts +7 -3
  325. package/src/browser/adapters/google.ts +5 -3
  326. package/src/browser/adapters/index.ts +5 -4
  327. package/src/browser/adapters/instagram.ts +4 -2
  328. package/src/browser/adapters/linkedin.ts +4 -2
  329. package/src/browser/adapters/microsoft.ts +9 -5
  330. package/src/browser/adapters/x.ts +7 -3
  331. package/src/browser/browser.ts +188 -42
  332. package/src/browser/cdp.ts +43 -11
  333. package/src/browser/console-log.ts +24 -7
  334. package/src/browser/dashboard/server.ts +44 -25
  335. package/src/browser/dashboard/ui.html +153 -165
  336. package/src/browser/dialog.ts +25 -8
  337. package/src/browser/emulation.ts +86 -36
  338. package/src/browser/find.ts +110 -47
  339. package/src/browser/har.ts +58 -26
  340. package/src/browser/index.ts +17 -16
  341. package/src/browser/network.ts +147 -46
  342. package/src/browser/pdf.ts +4 -4
  343. package/src/browser/profiler.ts +39 -19
  344. package/src/browser/record.ts +38 -19
  345. package/src/browser/ref-cache.ts +38 -17
  346. package/src/browser/screenshot.ts +58 -32
  347. package/src/browser/session.ts +51 -18
  348. package/src/browser/snapshot.ts +34 -20
  349. package/src/browser/storage.ts +73 -17
  350. package/src/browser/tabs.ts +29 -17
  351. package/src/browser/trace.ts +14 -10
  352. package/src/browser/types.ts +23 -4
  353. package/src/browser/vitals.ts +33 -16
  354. package/src/browser/wait.ts +47 -21
  355. package/src/cli/action.ts +52 -21
  356. package/src/cli/commands.ts +1365 -385
  357. package/src/cli/output.ts +5 -5
  358. package/src/cli/platform.ts +44 -23
  359. package/src/cli/types.ts +1 -1
  360. package/src/cli.ts +34 -18
  361. package/src/index.ts +2 -2
  362. package/src/report/a11y.ts +242 -0
  363. package/src/report/analyze.ts +181 -0
  364. package/src/report/budget.ts +155 -0
  365. package/src/report/collect-a11y.ts +200 -0
  366. package/src/report/collect.ts +408 -0
  367. package/src/report/evidence.ts +379 -0
  368. package/src/report/flake.ts +303 -0
  369. package/src/report/history.ts +219 -0
  370. package/src/report/index.ts +307 -0
  371. package/src/report/pixel-diff.ts +197 -0
  372. package/src/report/png.ts +290 -0
  373. package/src/report/render-evidence.ts +194 -0
  374. package/src/report/render-trend.ts +225 -0
  375. package/src/report/render.ts +314 -0
  376. package/src/report/run-diff.ts +63 -0
  377. package/src/report/structure.ts +212 -0
  378. package/src/report/trend.ts +189 -0
  379. package/src/report/types.ts +356 -0
  380. package/src/report/util.ts +48 -0
@@ -2,8 +2,8 @@
2
2
  * Browse Command — Native browser automation via Chrome DevTools Protocol
3
3
  * Provides ref-based element model and token-efficient accessibility snapshots
4
4
  */
5
- import { output } from './output.js';
6
5
  import { createActionCommand } from './action.js';
6
+ import { output } from './output.js';
7
7
  import { createPlatformCommand } from './platform.js';
8
8
  // Runtime state (single session per CLI process)
9
9
  const DEFAULT_PORT = 9222;
@@ -50,7 +50,9 @@ function ensureSignalCleanupHandlers() {
50
50
  try {
51
51
  process.kill(pid, 'SIGKILL');
52
52
  }
53
- catch { /* already exited */ }
53
+ catch {
54
+ /* already exited */
55
+ }
54
56
  }
55
57
  }
56
58
  }
@@ -78,7 +80,9 @@ async function resolveDefaultPort(browser) {
78
80
  // user's debug port and silently swap which browser commands act on —
79
81
  // fail loudly instead.
80
82
  try {
81
- await fetch(`http://127.0.0.1:${persisted.port}/json/version`, { signal: AbortSignal.timeout(1500) });
83
+ await fetch(`http://127.0.0.1:${persisted.port}/json/version`, {
84
+ signal: AbortSignal.timeout(1500),
85
+ });
82
86
  }
83
87
  catch {
84
88
  await browser.clearActivePort();
@@ -89,7 +93,7 @@ async function resolveDefaultPort(browser) {
89
93
  }
90
94
  async function ensureConnected(port, targetId) {
91
95
  const browser = await getBrowser();
92
- if (!_client || !_client.isConnected()) {
96
+ if (!_client?.isConnected()) {
93
97
  if (_client && _sessionId) {
94
98
  browser.teardownRouteInterception(_sessionId);
95
99
  browser.stopRequestCapture(_sessionId);
@@ -146,20 +150,28 @@ async function detectAttentionNeeded(client, sessionId, url) {
146
150
  if (ATTENTION_URL_RE.test(url))
147
151
  return 'login';
148
152
  try {
149
- const hasPassword = await client.send('Runtime.evaluate', {
153
+ const hasPassword = await client
154
+ .send('Runtime.evaluate', {
150
155
  expression: '!!document.querySelector("input[type=password]")',
151
156
  returnByValue: true,
152
- }, sessionId).then(r => r.result?.value === true).catch(() => false);
157
+ }, sessionId)
158
+ .then((r) => r.result?.value === true)
159
+ .catch(() => false);
153
160
  if (hasPassword)
154
161
  return 'login';
155
- const hasCaptcha = await client.send('Runtime.evaluate', {
162
+ const hasCaptcha = await client
163
+ .send('Runtime.evaluate', {
156
164
  expression: '!!(document.querySelector("iframe[src*=recaptcha]") || document.querySelector("iframe[src*=hcaptcha]") || document.querySelector(".g-recaptcha") || document.querySelector(".h-captcha") || document.querySelector("[data-sitekey]"))',
157
165
  returnByValue: true,
158
- }, sessionId).then(r => r.result?.value === true).catch(() => false);
166
+ }, sessionId)
167
+ .then((r) => r.result?.value === true)
168
+ .catch(() => false);
159
169
  if (hasCaptcha)
160
170
  return 'captcha';
161
171
  }
162
- catch { /* ignore CDP errors */ }
172
+ catch {
173
+ /* ignore CDP errors */
174
+ }
163
175
  return null;
164
176
  }
165
177
  function waitForEnter() {
@@ -167,12 +179,16 @@ function waitForEnter() {
167
179
  // Non-interactive context (MCP server, piped input, CI) — cannot safely read stdin.
168
180
  // Auto-continue after a short grace period so automation isn't blocked.
169
181
  output.printWarning('Non-interactive mode: auto-continuing in 30s. Switch to headed manually if needed.');
170
- return new Promise(resolve => setTimeout(resolve, 30_000));
182
+ return new Promise((resolve) => setTimeout(resolve, 30_000));
171
183
  }
172
- return new Promise(resolve => {
184
+ return new Promise((resolve) => {
173
185
  process.stdin.resume();
174
186
  process.stdin.setEncoding('utf8');
175
- const onData = () => { process.stdin.pause(); process.stdin.off('data', onData); resolve(); };
187
+ const onData = () => {
188
+ process.stdin.pause();
189
+ process.stdin.off('data', onData);
190
+ resolve();
191
+ };
176
192
  process.stdin.once('data', onData);
177
193
  });
178
194
  }
@@ -184,7 +200,9 @@ async function switchToHeaded(url, port) {
184
200
  try {
185
201
  savedCookies = await browser.getCookies(_client, _sessionId);
186
202
  }
187
- catch { /* ignore */ }
203
+ catch {
204
+ /* ignore */
205
+ }
188
206
  browser.teardownRouteInterception(_sessionId);
189
207
  browser.stopRequestCapture(_sessionId);
190
208
  browser.teardownDialogHandling(_sessionId);
@@ -219,14 +237,18 @@ async function switchToHeaded(url, port) {
219
237
  try {
220
238
  await browser.setCookies(_client, _sessionId, savedCookies);
221
239
  }
222
- catch { /* ignore */ }
240
+ catch {
241
+ /* ignore */
242
+ }
223
243
  }
224
244
  await browser.openUrl(_client, _sessionId, url);
225
245
  output.printInfo('Browser window opened. Complete the required action (login / CAPTCHA), then press Enter to continue in headless mode...');
226
246
  await waitForEnter();
227
247
  // Capture post-auth cookies
228
248
  const authCookies = await browser.getCookies(_client, _sessionId).catch(() => []);
229
- const authLocalStorage = await browser.getLocalStorage(_client, _sessionId).catch(() => ({}));
249
+ const authLocalStorage = await browser
250
+ .getLocalStorage(_client, _sessionId)
251
+ .catch(() => ({}));
230
252
  // Close headed session — actually terminate the underlying Chrome process
231
253
  // (Browser.close CDP command, PID-kill fallback), not just our CDP client
232
254
  // connection, so the visible authenticated window doesn't linger forever.
@@ -251,13 +273,17 @@ async function switchToHeaded(url, port) {
251
273
  try {
252
274
  await browser.setCookies(_client, _sessionId, authCookies);
253
275
  }
254
- catch { /* ignore */ }
276
+ catch {
277
+ /* ignore */
278
+ }
255
279
  }
256
280
  if (authLocalStorage && Object.keys(authLocalStorage).length) {
257
281
  try {
258
282
  await browser.setLocalStorage(_client, _sessionId, authLocalStorage);
259
283
  }
260
- catch { /* ignore */ }
284
+ catch {
285
+ /* ignore */
286
+ }
261
287
  }
262
288
  }
263
289
  /**
@@ -272,7 +298,7 @@ function imageFormat(value, allowed, fallback) {
272
298
  return allowed.includes(value) ? value : fallback;
273
299
  }
274
300
  function print(msg) {
275
- process.stdout.write(msg + '\n');
301
+ process.stdout.write(`${msg}\n`);
276
302
  }
277
303
  // ---------------------------------------------------------------------------
278
304
  // Sub-commands
@@ -282,7 +308,12 @@ const openCommand = {
282
308
  description: 'Open a URL in the browser. Usage: monomind browse open <url>',
283
309
  options: [
284
310
  { name: 'port', short: 'p', type: 'number', description: 'CDP port', default: 9222 },
285
- { name: 'headed', type: 'boolean', description: 'Force visible browser window', default: false },
311
+ {
312
+ name: 'headed',
313
+ type: 'boolean',
314
+ description: 'Force visible browser window',
315
+ default: false,
316
+ },
286
317
  { name: 'session', short: 's', type: 'string', description: 'Session name to restore' },
287
318
  { name: 'state', type: 'string', description: 'State file to load' },
288
319
  ],
@@ -300,19 +331,25 @@ const openCommand = {
300
331
  try {
301
332
  await browser.stopHarRecording(prevClient, prevSid);
302
333
  }
303
- catch { /* ignore */ }
334
+ catch {
335
+ /* ignore */
336
+ }
304
337
  }
305
338
  if (browser.getTraceStatus(prevSid)) {
306
339
  try {
307
340
  await browser.stopTrace(prevClient, prevSid);
308
341
  }
309
- catch { /* ignore */ }
342
+ catch {
343
+ /* ignore */
344
+ }
310
345
  }
311
346
  if (browser.isProfilingActive(prevSid)) {
312
347
  try {
313
348
  await browser.stopCpuProfile(prevClient, prevSid);
314
349
  }
315
- catch { /* ignore */ }
350
+ catch {
351
+ /* ignore */
352
+ }
316
353
  }
317
354
  browser.teardownRouteInterception(prevSid);
318
355
  browser.stopRequestCapture(prevSid);
@@ -352,7 +389,10 @@ const openCommand = {
352
389
  // launched it) instead of overwriting with undefined.
353
390
  const existing = await browser.loadActivePortInfo();
354
391
  const preserved = existing && existing.port === _port ? existing : undefined;
355
- await browser.saveActivePort(_port, { pid: preserved?.pid, userDataDir: preserved?.userDataDir });
392
+ await browser.saveActivePort(_port, {
393
+ pid: preserved?.pid,
394
+ userDataDir: preserved?.userDataDir,
395
+ });
356
396
  }
357
397
  ensureSignalCleanupHandlers();
358
398
  const conn = await browser.connectToTarget(_port);
@@ -395,15 +435,49 @@ const snapshotCommand = {
395
435
  name: 'snapshot',
396
436
  description: 'Capture accessibility snapshot with ref-based element handles (@e1, @e2, ...)',
397
437
  options: [
398
- { name: 'interactive', short: 'i', type: 'boolean', description: 'Interactive elements only (93% token reduction)', default: false },
399
- { name: 'compact', short: 'c', type: 'boolean', description: 'Compact output format', default: false },
438
+ {
439
+ name: 'interactive',
440
+ short: 'i',
441
+ type: 'boolean',
442
+ description: 'Interactive elements only (93% token reduction)',
443
+ default: false,
444
+ },
445
+ {
446
+ name: 'compact',
447
+ short: 'c',
448
+ type: 'boolean',
449
+ description: 'Compact output format',
450
+ default: false,
451
+ },
400
452
  { name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
401
453
  { name: 'depth', short: 'd', type: 'number', description: 'Max depth of AX tree to show' },
402
- { name: 'selector', short: 's', type: 'string', description: 'Scope snapshot to a CSS selector' },
403
- { name: 'save', type: 'string', description: 'Save snapshot text to file (baseline for --diff)' },
404
- { name: 'diff', type: 'string', description: 'Compare current snapshot against a saved baseline file' },
405
- { name: 'content-boundaries', type: 'boolean', description: 'Wrap output in sentinel markers to prevent page-content injection attacks', default: false },
406
- { name: 'max-output', type: 'number', description: 'Truncate output to N characters (prevents context window blowout on large pages)' },
454
+ {
455
+ name: 'selector',
456
+ short: 's',
457
+ type: 'string',
458
+ description: 'Scope snapshot to a CSS selector',
459
+ },
460
+ {
461
+ name: 'save',
462
+ type: 'string',
463
+ description: 'Save snapshot text to file (baseline for --diff)',
464
+ },
465
+ {
466
+ name: 'diff',
467
+ type: 'string',
468
+ description: 'Compare current snapshot against a saved baseline file',
469
+ },
470
+ {
471
+ name: 'content-boundaries',
472
+ type: 'boolean',
473
+ description: 'Wrap output in sentinel markers to prevent page-content injection attacks',
474
+ default: false,
475
+ },
476
+ {
477
+ name: 'max-output',
478
+ type: 'number',
479
+ description: 'Truncate output to N characters (prevents context window blowout on large pages)',
480
+ },
407
481
  ],
408
482
  action: async (ctx) => {
409
483
  const { client, sessionId } = await ensureConnected(_port);
@@ -427,8 +501,8 @@ const snapshotCommand = {
427
501
  };
428
502
  // --save: write snapshot text to baseline file
429
503
  if (ctx.flags.save) {
430
- const { writeFile, mkdir } = await import('fs/promises');
431
- const { dirname } = await import('path');
504
+ const { writeFile, mkdir } = await import('node:fs/promises');
505
+ const { dirname } = await import('node:path');
432
506
  const savePath = ctx.flags.save;
433
507
  await mkdir(dirname(savePath), { recursive: true }).catch(() => { });
434
508
  await writeFile(savePath, result.text, 'utf8');
@@ -437,7 +511,7 @@ const snapshotCommand = {
437
511
  }
438
512
  // --diff: compare against baseline file
439
513
  if (ctx.flags.diff) {
440
- const { readFile } = await import('fs/promises');
514
+ const { readFile } = await import('node:fs/promises');
441
515
  const baselinePath = ctx.flags.diff;
442
516
  let baseline;
443
517
  try {
@@ -459,7 +533,13 @@ const snapshotCommand = {
459
533
  removed.push(l);
460
534
  const changed = added.length > 0 || removed.length > 0;
461
535
  if (ctx.flags.json) {
462
- print(JSON.stringify({ changed, additions: added.length, removals: removed.length, added, removed }));
536
+ print(JSON.stringify({
537
+ changed,
538
+ additions: added.length,
539
+ removals: removed.length,
540
+ added,
541
+ removed,
542
+ }));
463
543
  }
464
544
  else {
465
545
  if (!changed) {
@@ -473,11 +553,19 @@ const snapshotCommand = {
473
553
  print(`\x1b[31m- ${l}\x1b[0m`);
474
554
  }
475
555
  }
476
- return { success: true, data: { changed, additions: added.length, removals: removed.length } };
556
+ return {
557
+ success: true,
558
+ data: { changed, additions: added.length, removals: removed.length },
559
+ };
477
560
  }
478
561
  if (ctx.flags.json) {
479
562
  const refsObj = Object.fromEntries([...result.refs.entries()].map(([k, v]) => [k, v]));
480
- print(JSON.stringify({ url: result.url, title: result.title, refs: refsObj, snapshot: result.text }));
563
+ print(JSON.stringify({
564
+ url: result.url,
565
+ title: result.title,
566
+ refs: refsObj,
567
+ snapshot: result.text,
568
+ }));
481
569
  }
482
570
  else {
483
571
  print(`[${result.title}] ${result.url}\n`);
@@ -555,11 +643,19 @@ const waitCommand = {
555
643
  { name: 'text', type: 'string', description: 'Wait for text to appear in page' },
556
644
  { name: 'not-text', type: 'string', description: 'Wait for text to disappear from page' },
557
645
  { name: 'selector', type: 'string', description: 'Wait for CSS selector to appear' },
558
- { name: 'load', type: 'string', description: 'Wait for load event: load|networkidle|domcontentloaded' },
646
+ {
647
+ name: 'load',
648
+ type: 'string',
649
+ description: 'Wait for load event: load|networkidle|domcontentloaded',
650
+ },
559
651
  { name: 'fn', type: 'string', description: 'Wait until JS expression returns truthy' },
560
652
  { name: 'ms', type: 'number', description: 'Wait N milliseconds' },
561
653
  { name: 'timeout', short: 't', type: 'number', description: 'Timeout in ms', default: 30000 },
562
- { name: 'download', type: 'string', description: 'Wait for a file download to complete and save to path (monitors Browser.downloadProgress events)' },
654
+ {
655
+ name: 'download',
656
+ type: 'string',
657
+ description: 'Wait for a file download to complete and save to path (monitors Browser.downloadProgress events)',
658
+ },
563
659
  ],
564
660
  action: async (ctx) => {
565
661
  const { client, sessionId } = await ensureConnected(_port);
@@ -574,7 +670,9 @@ const waitCommand = {
574
670
  if (ctx.flags.fn) {
575
671
  const expr = ctx.flags.fn;
576
672
  const rawTimeout = ctx.flags.timeout ?? 30000;
577
- const timeout = Number.isFinite(rawTimeout) ? Math.max(100, Math.min(rawTimeout, 300_000)) : 30000; // cap at 5min
673
+ const timeout = Number.isFinite(rawTimeout)
674
+ ? Math.max(100, Math.min(rawTimeout, 300_000))
675
+ : 30000; // cap at 5min
578
676
  const interval = 200;
579
677
  const deadline = Date.now() + timeout;
580
678
  while (Date.now() < deadline) {
@@ -593,7 +691,7 @@ const waitCommand = {
593
691
  const interval = 200;
594
692
  const deadline = Date.now() + timeout;
595
693
  while (Date.now() < deadline) {
596
- const text = await browser.evaluateJs(client, sessionId, 'document.body?.innerText ?? ""');
694
+ const text = (await browser.evaluateJs(client, sessionId, 'document.body?.innerText ?? ""'));
597
695
  if (!text.includes(target)) {
598
696
  output.printSuccess('Text disappeared');
599
697
  return { success: true };
@@ -604,26 +702,39 @@ const waitCommand = {
604
702
  }
605
703
  if (ctx.flags.download) {
606
704
  const savePath = ctx.flags.download;
607
- const { mkdir } = await import('fs/promises');
608
- const { dirname, join } = await import('path');
609
- const { tmpdir } = await import('os');
705
+ const { mkdir } = await import('node:fs/promises');
706
+ const { dirname, join } = await import('node:path');
707
+ const { tmpdir } = await import('node:os');
610
708
  const downloadDir = join(tmpdir(), `mm-dl-wait-${Date.now()}`);
611
709
  await mkdir(downloadDir, { recursive: true });
612
- await client.send('Browser.setDownloadBehavior', {
613
- behavior: 'allow', downloadPath: downloadDir, eventsEnabled: true,
614
- }, undefined).catch(() => client.send('Page.setDownloadBehavior', { behavior: 'allow', downloadPath: downloadDir }, sessionId).catch(() => { }));
710
+ await client
711
+ .send('Browser.setDownloadBehavior', {
712
+ behavior: 'allow',
713
+ downloadPath: downloadDir,
714
+ eventsEnabled: true,
715
+ }, undefined)
716
+ .catch(() => client
717
+ .send('Page.setDownloadBehavior', { behavior: 'allow', downloadPath: downloadDir }, sessionId)
718
+ .catch(() => { }));
615
719
  const MAX_DOWNLOAD_TIMEOUT = 5 * 60 * 1000; // I6: cap at 5 minutes
616
720
  const rawTimeout = Math.min(ctx.flags.timeout ?? 30000, MAX_DOWNLOAD_TIMEOUT);
617
721
  const finalPath = await new Promise((resolve, reject) => {
618
722
  let guid = '';
619
723
  let settled = false;
620
724
  // C2: capture off() functions to avoid listener leaks
621
- const offBegin = client.on('Browser.downloadWillBegin', (params) => { guid = params.guid; });
725
+ const offBegin = client.on('Browser.downloadWillBegin', (params) => {
726
+ guid = params.guid;
727
+ });
622
728
  let offProgress;
623
729
  // cleanup defined before setTimeout so the timeout callback can call it
624
730
  let tid;
625
731
  let pollTid;
626
- const cleanup = () => { clearTimeout(tid); clearInterval(pollTid); offBegin?.(); offProgress?.(); };
732
+ const cleanup = () => {
733
+ clearTimeout(tid);
734
+ clearInterval(pollTid);
735
+ offBegin?.();
736
+ offProgress?.();
737
+ };
627
738
  const finish = (path) => {
628
739
  if (settled)
629
740
  return;
@@ -641,7 +752,7 @@ const waitCommand = {
641
752
  tid = setTimeout(() => fail(new Error('Download timed out')), rawTimeout);
642
753
  offProgress = client.on('Browser.downloadProgress', async (params) => {
643
754
  if (params.guid === guid && params.state === 'completed') {
644
- const { readdir, rename, rmdir } = await import('fs/promises');
755
+ const { readdir, rename, rmdir } = await import('node:fs/promises');
645
756
  const files = await readdir(downloadDir);
646
757
  if (files.length > 0) {
647
758
  const src = join(downloadDir, files[0]);
@@ -671,7 +782,7 @@ const waitCommand = {
671
782
  if (settled || guid)
672
783
  return; // a real CDP event has taken over
673
784
  try {
674
- const { stat } = await import('fs/promises');
785
+ const { stat } = await import('node:fs/promises');
675
786
  const st = await stat(savePath);
676
787
  if (st.isFile() && st.size > 0 && st.size === lastSize) {
677
788
  stableReads++;
@@ -709,8 +820,18 @@ const screenshotCommand = {
709
820
  { name: 'full', type: 'boolean', description: 'Full page screenshot', default: false },
710
821
  { name: 'format', type: 'string', description: 'Format: png|jpeg|webp', default: 'png' },
711
822
  { name: 'quality', type: 'number', description: 'Quality 0-100 for jpeg/webp', default: 80 },
712
- { name: 'annotate', type: 'boolean', description: 'Overlay numbered labels keyed to @eN refs from last snapshot (viewport-only; do not combine with --full)', default: false },
713
- { name: 'hide-scrollbars', type: 'boolean', description: 'Hide native scrollbars via CSS injection before capture', default: false },
823
+ {
824
+ name: 'annotate',
825
+ type: 'boolean',
826
+ description: 'Overlay numbered labels keyed to @eN refs from last snapshot (viewport-only; do not combine with --full)',
827
+ default: false,
828
+ },
829
+ {
830
+ name: 'hide-scrollbars',
831
+ type: 'boolean',
832
+ description: 'Hide native scrollbars via CSS injection before capture',
833
+ default: false,
834
+ },
714
835
  { name: 'json', type: 'boolean', description: 'Output JSON with path', default: false },
715
836
  ],
716
837
  action: async (ctx) => {
@@ -718,10 +839,12 @@ const screenshotCommand = {
718
839
  const browser = await getBrowser();
719
840
  const hideScrollbars = ctx.flags['hide-scrollbars'];
720
841
  if (hideScrollbars) {
721
- await client.send('Runtime.evaluate', {
842
+ await client
843
+ .send('Runtime.evaluate', {
722
844
  expression: `(function(){var s=document.getElementById('__mm_noscroll__');if(s)return;var el=document.createElement('style');el.id='__mm_noscroll__';el.textContent='*::-webkit-scrollbar{display:none!important}*{scrollbar-width:none!important;-ms-overflow-style:none!important}';document.head.appendChild(el);})()`,
723
845
  returnByValue: false,
724
- }, sessionId).catch(() => { });
846
+ }, sessionId)
847
+ .catch(() => { });
725
848
  }
726
849
  const annotate = ctx.flags.annotate;
727
850
  // eslint-disable-next-line prefer-const
@@ -738,10 +861,12 @@ const screenshotCommand = {
738
861
  }
739
862
  finally {
740
863
  if (hideScrollbars) {
741
- await client.send('Runtime.evaluate', {
864
+ await client
865
+ .send('Runtime.evaluate', {
742
866
  expression: `(function(){var s=document.getElementById('__mm_noscroll__');if(s)s.remove();})()`,
743
867
  returnByValue: false,
744
- }, sessionId).catch(() => { });
868
+ }, sessionId)
869
+ .catch(() => { });
745
870
  }
746
871
  }
747
872
  if (ctx.flags.json) {
@@ -756,9 +881,7 @@ const screenshotCommand = {
756
881
  const getCommand = {
757
882
  name: 'get',
758
883
  description: 'Get page info. Usage: monomind browse get url|title|text|html|value|attr|count|box|styles [@ref] [attrName]',
759
- options: [
760
- { name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
761
- ],
884
+ options: [{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false }],
762
885
  action: async (ctx) => {
763
886
  const { client, sessionId } = await ensureConnected(_port);
764
887
  const browser = await getBrowser();
@@ -811,7 +934,8 @@ const getCommand = {
811
934
  throw new Error('Element not in DOM');
812
935
  const r = await client.send('Runtime.callFunctionOn', {
813
936
  functionDeclaration: 'function() { return this.value ?? null; }',
814
- objectId, returnByValue: true,
937
+ objectId,
938
+ returnByValue: true,
815
939
  }, sessionId);
816
940
  value = r.result?.value ?? null;
817
941
  break;
@@ -830,7 +954,8 @@ const getCommand = {
830
954
  throw new Error('Element not in DOM');
831
955
  const r = await client.send('Runtime.callFunctionOn', {
832
956
  functionDeclaration: `function() { return this.getAttribute(${JSON.stringify(attrName)}); }`,
833
- objectId, returnByValue: true,
957
+ objectId,
958
+ returnByValue: true,
834
959
  }, sessionId);
835
960
  value = r.result?.value ?? null;
836
961
  break;
@@ -867,7 +992,8 @@ const getCommand = {
867
992
  throw new Error('Element not in DOM');
868
993
  const r = await client.send('Runtime.callFunctionOn', {
869
994
  functionDeclaration: 'function() { const s = window.getComputedStyle(this); return JSON.stringify(Object.fromEntries([...s].map(k => [k, s.getPropertyValue(k)]))); }',
870
- objectId, returnByValue: true,
995
+ objectId,
996
+ returnByValue: true,
871
997
  }, sessionId);
872
998
  try {
873
999
  value = JSON.parse(r.result?.value ?? '{}');
@@ -895,7 +1021,12 @@ const scrollCommand = {
895
1021
  options: [
896
1022
  { name: 'amount', short: 'a', type: 'number', description: 'Pixels to scroll', default: 300 },
897
1023
  { name: 'ref', type: 'string', description: 'Element ref to scroll within' },
898
- { name: 'selector', short: 's', type: 'string', description: 'CSS selector of element to scroll within' },
1024
+ {
1025
+ name: 'selector',
1026
+ short: 's',
1027
+ type: 'string',
1028
+ description: 'CSS selector of element to scroll within',
1029
+ },
899
1030
  ],
900
1031
  action: async (ctx) => {
901
1032
  const { client, sessionId } = await ensureConnected(_port);
@@ -905,12 +1036,14 @@ const scrollCommand = {
905
1036
  throw new Error('Usage: monomind browse scroll up|down|left|right [amount]');
906
1037
  // Support positional amount: scroll down 300
907
1038
  const positionalAmount = ctx.args[1] !== undefined ? parseInt(ctx.args[1], 10) : undefined;
908
- const amount = (positionalAmount && Number.isFinite(positionalAmount)) ? positionalAmount : ctx.flags.amount ?? 300;
1039
+ const amount = positionalAmount && Number.isFinite(positionalAmount)
1040
+ ? positionalAmount
1041
+ : (ctx.flags.amount ?? 300);
909
1042
  if (ctx.flags.selector) {
910
1043
  const sel = ctx.flags.selector;
911
1044
  const dx = direction === 'right' ? amount : direction === 'left' ? -amount : 0;
912
1045
  const dy = direction === 'down' ? amount : direction === 'up' ? -amount : 0;
913
- const posJson = await browser.evaluateJs(client, sessionId, `(function(){var el=document.querySelector(${JSON.stringify(sel)});if(!el)return null;var r=el.getBoundingClientRect();return JSON.stringify({x:r.left+r.width/2,y:r.top+r.height/2});})()`);
1046
+ const posJson = (await browser.evaluateJs(client, sessionId, `(function(){var el=document.querySelector(${JSON.stringify(sel)});if(!el)return null;var r=el.getBoundingClientRect();return JSON.stringify({x:r.left+r.width/2,y:r.top+r.height/2});})()`));
914
1047
  if (!posJson)
915
1048
  throw new Error(`Selector not found: ${sel}`);
916
1049
  const pos = JSON.parse(posJson);
@@ -958,7 +1091,9 @@ const navigateCommand = {
958
1091
  expression: direction === 'back' ? 'history.back()' : 'history.forward()',
959
1092
  }, sessionId);
960
1093
  let fallbackHandle;
961
- const fallbackPromise = new Promise((r) => { fallbackHandle = setTimeout(r, 2000); });
1094
+ const fallbackPromise = new Promise((r) => {
1095
+ fallbackHandle = setTimeout(r, 2000);
1096
+ });
962
1097
  await Promise.race([frameStartedPromise, fallbackPromise]);
963
1098
  if (fallbackHandle !== undefined)
964
1099
  clearTimeout(fallbackHandle);
@@ -996,10 +1131,13 @@ const setCommand = {
996
1131
  const width = parseInt(ctx.args[1], 10);
997
1132
  const height = parseInt(ctx.args[2], 10);
998
1133
  const dpr = parseFloat(ctx.args[3]) || undefined;
999
- if (isNaN(width) || isNaN(height))
1134
+ if (Number.isNaN(width) || Number.isNaN(height))
1000
1135
  throw new Error('Usage: set viewport <width> <height> [dpr]');
1001
1136
  await client.send('Emulation.setDeviceMetricsOverride', {
1002
- width, height, deviceScaleFactor: dpr ?? 1, mobile: false,
1137
+ width,
1138
+ height,
1139
+ deviceScaleFactor: dpr ?? 1,
1140
+ mobile: false,
1003
1141
  }, sessionId);
1004
1142
  output.printSuccess(`Viewport set to ${width}x${height}${dpr ? ` @${dpr}x` : ''}`);
1005
1143
  break;
@@ -1016,7 +1154,7 @@ const setCommand = {
1016
1154
  const lat = parseFloat(ctx.args[1]);
1017
1155
  const lon = parseFloat(ctx.args[2]);
1018
1156
  const acc = parseFloat(ctx.args[3]) || 100;
1019
- if (isNaN(lat) || isNaN(lon))
1157
+ if (Number.isNaN(lat) || Number.isNaN(lon))
1020
1158
  throw new Error('Usage: set geo <latitude> <longitude> [accuracy]');
1021
1159
  await browser.setGeolocation(client, sessionId, lat, lon, acc);
1022
1160
  output.printSuccess(`Geolocation set: ${lat}, ${lon}`);
@@ -1066,7 +1204,11 @@ const stateCommand = {
1066
1204
  name: 'state',
1067
1205
  description: 'Manage browser session state. Usage: monomind browse state save|load|list|rename|clean [name]',
1068
1206
  options: [
1069
- { name: 'older-than', type: 'number', description: 'For state clean: remove sessions older than N days' },
1207
+ {
1208
+ name: 'older-than',
1209
+ type: 'number',
1210
+ description: 'For state clean: remove sessions older than N days',
1211
+ },
1070
1212
  ],
1071
1213
  action: async (ctx) => {
1072
1214
  const browser = await getBrowser();
@@ -1123,7 +1265,13 @@ const stateCommand = {
1123
1265
  const title = await browser.getCurrentTitle(c, sid);
1124
1266
  const cookies = await browser.getCookies(c, sid);
1125
1267
  const ls = await browser.getAllLocalStorage(c, sid);
1126
- const info = { url, title, cookies: cookies.length, localStorage: Object.keys(ls).length, refs: _refs.size };
1268
+ const info = {
1269
+ url,
1270
+ title,
1271
+ cookies: cookies.length,
1272
+ localStorage: Object.keys(ls).length,
1273
+ refs: _refs.size,
1274
+ };
1127
1275
  print(JSON.stringify(info, null, 2));
1128
1276
  return { success: true, data: info };
1129
1277
  }
@@ -1146,14 +1294,14 @@ const stateCommand = {
1146
1294
  if (!sessions.includes(oldName))
1147
1295
  throw new Error(`Session not found: ${oldName}`);
1148
1296
  // W1: validate newName to prevent path traversal
1149
- const { basename: basenameFn } = await import('path');
1297
+ const { basename: basenameFn } = await import('node:path');
1150
1298
  const safeName = basenameFn(newName);
1151
1299
  if (safeName !== newName || safeName.startsWith('.') || safeName.includes('/')) {
1152
1300
  throw new Error('Invalid session name — must not contain path separators or start with "."');
1153
1301
  }
1154
- const { unlink: unlinkRename, readFile, writeFile, mkdir: mkdirRename } = await import('fs/promises');
1155
- const { join: joinR } = await import('path');
1156
- const { homedir } = await import('os');
1302
+ const { unlink: unlinkRename, readFile, writeFile, mkdir: mkdirRename, } = await import('node:fs/promises');
1303
+ const { join: joinR } = await import('node:path');
1304
+ const { homedir } = await import('node:os');
1157
1305
  const sessionDir = joinR(homedir(), '.monomind', 'browser-sessions');
1158
1306
  const oldPath = joinR(sessionDir, `${oldName}.json`);
1159
1307
  const newPath = joinR(sessionDir, `${newName}.json`);
@@ -1167,9 +1315,9 @@ const stateCommand = {
1167
1315
  }
1168
1316
  case 'clean': {
1169
1317
  const days = ctx.flags['older-than'] ?? 7;
1170
- const { unlink, stat } = await import('fs/promises');
1171
- const { join: joinC } = await import('path');
1172
- const { homedir: homedirC } = await import('os');
1318
+ const { unlink, stat } = await import('node:fs/promises');
1319
+ const { join: joinC } = await import('node:path');
1320
+ const { homedir: homedirC } = await import('node:os');
1173
1321
  const sessionDir = joinC(homedirC(), '.monomind', 'browser-sessions');
1174
1322
  const sessions = await browser.listSessions();
1175
1323
  const cutoff = Date.now() - days * 86400 * 1000;
@@ -1200,10 +1348,26 @@ const networkCommand = {
1200
1348
  { name: 'status', type: 'number', description: 'HTTP status for fulfill', default: 200 },
1201
1349
  { name: 'headers', type: 'string', description: 'JSON headers object' },
1202
1350
  { name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
1203
- { name: 'filter', type: 'string', description: 'Filter requests by URL substring (for network requests)' },
1204
- { name: 'method', type: 'string', description: 'Filter by HTTP method, e.g. GET, POST (for network requests)' },
1205
- { name: 'status-code', type: 'number', description: 'Filter by HTTP status code (for network requests)' },
1206
- { name: 'type', type: 'string', description: 'Filter by resource type: xhr|fetch|document|script|stylesheet|image (for network requests)' },
1351
+ {
1352
+ name: 'filter',
1353
+ type: 'string',
1354
+ description: 'Filter requests by URL substring (for network requests)',
1355
+ },
1356
+ {
1357
+ name: 'method',
1358
+ type: 'string',
1359
+ description: 'Filter by HTTP method, e.g. GET, POST (for network requests)',
1360
+ },
1361
+ {
1362
+ name: 'status-code',
1363
+ type: 'number',
1364
+ description: 'Filter by HTTP status code (for network requests)',
1365
+ },
1366
+ {
1367
+ name: 'type',
1368
+ type: 'string',
1369
+ description: 'Filter by resource type: xhr|fetch|document|script|stylesheet|image (for network requests)',
1370
+ },
1207
1371
  ],
1208
1372
  action: async (ctx) => {
1209
1373
  const { client, sessionId } = await ensureConnected(_port);
@@ -1216,15 +1380,19 @@ const networkCommand = {
1216
1380
  const pattern = ctx.flags.pattern;
1217
1381
  if (!pattern)
1218
1382
  throw new Error('--pattern required for network route');
1219
- const routes = [{
1383
+ const routes = [
1384
+ {
1220
1385
  pattern,
1221
1386
  action: ctx.flags.abort ? 'abort' : ctx.flags.fulfill ? 'fulfill' : 'continue',
1222
- response: ctx.flags.fulfill ? {
1223
- status: ctx.flags.status,
1224
- body: ctx.flags.fulfill,
1225
- headers: ctx.flags.headers ? JSON.parse(ctx.flags.headers) : {},
1226
- } : undefined,
1227
- }];
1387
+ response: ctx.flags.fulfill
1388
+ ? {
1389
+ status: ctx.flags.status,
1390
+ body: ctx.flags.fulfill,
1391
+ headers: ctx.flags.headers ? JSON.parse(ctx.flags.headers) : {},
1392
+ }
1393
+ : undefined,
1394
+ },
1395
+ ];
1228
1396
  await browser.setupRoutes(client, sessionId, routes);
1229
1397
  output.printSuccess(`Network route set: ${pattern}`);
1230
1398
  break;
@@ -1275,7 +1443,8 @@ const networkCommand = {
1275
1443
  if (filterStatus)
1276
1444
  reqs = reqs.filter((r) => r.status === filterStatus);
1277
1445
  if (filterType)
1278
- reqs = reqs.filter((r) => r.resourceType === filterType || r.type === filterType);
1446
+ reqs = reqs.filter((r) => r.resourceType === filterType ||
1447
+ r.type === filterType);
1279
1448
  if (ctx.flags.json)
1280
1449
  print(JSON.stringify({ data: reqs }));
1281
1450
  else {
@@ -1293,7 +1462,8 @@ const networkCommand = {
1293
1462
  if (!reqId)
1294
1463
  throw new Error('Usage: monomind browse network request <requestId>');
1295
1464
  const reqs = browser.getCapturedRequests(sessionId);
1296
- const req = reqs.find((r) => r.requestId === reqId || r.id === reqId);
1465
+ const req = reqs.find((r) => r.requestId === reqId ||
1466
+ r.id === reqId);
1297
1467
  if (!req) {
1298
1468
  output.printWarning(`Request not found: ${reqId}`);
1299
1469
  return { success: false };
@@ -1319,16 +1489,29 @@ const DEFAULT_EVAL_MAX_OUTPUT = 50_000;
1319
1489
  function truncateForOutput(text, maxOutput) {
1320
1490
  if (!(maxOutput > 0) || text.length <= maxOutput)
1321
1491
  return text;
1322
- return text.slice(0, maxOutput) + `\n[... truncated at ${maxOutput} chars]`;
1492
+ return `${text.slice(0, maxOutput)}\n[... truncated at ${maxOutput} chars]`;
1323
1493
  }
1324
1494
  const evalCommand = {
1325
1495
  name: 'eval',
1326
1496
  description: 'Evaluate JavaScript in page context. Usage: monomind browse eval "document.title"',
1327
1497
  options: [
1328
1498
  { name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
1329
- { name: 'stdin', type: 'boolean', description: 'Read JS expression from stdin (heredoc-friendly for multiline scripts)', default: false },
1330
- { name: 'max-output', type: 'number', description: `Truncate printed output to N characters (default ${DEFAULT_EVAL_MAX_OUTPUT}; 0 disables truncation)` },
1331
- { name: 'timeout', type: 'number', description: 'Max ms to wait for evaluation to settle (default 30000)' },
1499
+ {
1500
+ name: 'stdin',
1501
+ type: 'boolean',
1502
+ description: 'Read JS expression from stdin (heredoc-friendly for multiline scripts)',
1503
+ default: false,
1504
+ },
1505
+ {
1506
+ name: 'max-output',
1507
+ type: 'number',
1508
+ description: `Truncate printed output to N characters (default ${DEFAULT_EVAL_MAX_OUTPUT}; 0 disables truncation)`,
1509
+ },
1510
+ {
1511
+ name: 'timeout',
1512
+ type: 'number',
1513
+ description: 'Max ms to wait for evaluation to settle (default 30000)',
1514
+ },
1332
1515
  ],
1333
1516
  action: async (ctx) => {
1334
1517
  const { client, sessionId } = await ensureConnected(_port);
@@ -1354,6 +1537,254 @@ const evalCommand = {
1354
1537
  return { success: true, data: { result } };
1355
1538
  },
1356
1539
  };
1540
+ const reportCommand = {
1541
+ name: 'report',
1542
+ description: 'Test a page and write one self-contained HTML report + sibling JSON. Usage: monomind browse report <url>',
1543
+ options: [
1544
+ { name: 'out', short: 'o', type: 'string', description: 'Output .html path, or a directory' },
1545
+ {
1546
+ name: 'budget',
1547
+ short: 'b',
1548
+ type: 'string',
1549
+ description: 'Budget JSON file path, or inline JSON',
1550
+ },
1551
+ {
1552
+ name: 'devices',
1553
+ short: 'd',
1554
+ type: 'string',
1555
+ description: 'Comma-separated device names for the screenshot matrix',
1556
+ },
1557
+ {
1558
+ name: 'wait',
1559
+ short: 'w',
1560
+ type: 'string',
1561
+ description: 'Extra settle step: a CSS selector to wait for, or milliseconds',
1562
+ },
1563
+ { name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
1564
+ {
1565
+ name: 'full-page',
1566
+ type: 'boolean',
1567
+ description: 'Full-page screenshot (--no-full-page for viewport only)',
1568
+ default: true,
1569
+ },
1570
+ {
1571
+ name: 'vitals-wait',
1572
+ type: 'number',
1573
+ description: 'Milliseconds to let the web-vitals observers run',
1574
+ default: 2500,
1575
+ },
1576
+ {
1577
+ name: 'timeout',
1578
+ type: 'number',
1579
+ description: 'Milliseconds to wait for the page to go network-idle',
1580
+ default: 20000,
1581
+ },
1582
+ {
1583
+ name: 'keep-open',
1584
+ type: 'boolean',
1585
+ description: 'Leave the browser running after the report',
1586
+ default: false,
1587
+ },
1588
+ {
1589
+ name: 'repeat',
1590
+ type: 'number',
1591
+ description: 'Run the URL N times and report per-check flake rates (RIG-14)',
1592
+ },
1593
+ {
1594
+ name: 'record',
1595
+ type: 'boolean',
1596
+ description: 'Record frames for the evidence timeline even when the run passes',
1597
+ default: false,
1598
+ },
1599
+ {
1600
+ name: 'history',
1601
+ type: 'boolean',
1602
+ description: 'Print the stored run history for this URL instead of running it',
1603
+ default: false,
1604
+ },
1605
+ {
1606
+ name: 'save',
1607
+ type: 'boolean',
1608
+ description: 'Save this run to the history store (--no-save to skip)',
1609
+ default: true,
1610
+ },
1611
+ {
1612
+ name: 'history-max',
1613
+ type: 'number',
1614
+ description: 'Runs kept per URL before the oldest are pruned (default 20)',
1615
+ },
1616
+ {
1617
+ name: 'trend-window',
1618
+ type: 'number',
1619
+ description: 'Prior runs charted in the trend section (default 10)',
1620
+ },
1621
+ ],
1622
+ examples: [
1623
+ { command: 'monomind browse report https://example.com', description: 'Report with defaults' },
1624
+ {
1625
+ command: 'monomind browse report https://example.com --repeat 5',
1626
+ description: 'Run five times and report flake rates',
1627
+ },
1628
+ {
1629
+ command: 'monomind browse report https://example.com --history',
1630
+ description: 'Show the stored run history for a URL',
1631
+ },
1632
+ {
1633
+ command: 'monomind browse report https://example.com --record',
1634
+ description: 'Attach a frame-by-frame evidence timeline',
1635
+ },
1636
+ {
1637
+ command: 'monomind browse report https://example.com --out ./reports/home.html',
1638
+ description: 'Choose the output path',
1639
+ },
1640
+ {
1641
+ command: 'monomind browse report https://example.com --budget \'{"lcp":4000}\'',
1642
+ description: 'Relax a budget inline',
1643
+ },
1644
+ {
1645
+ command: 'monomind browse report https://example.com --devices "iPhone 14,iPad"',
1646
+ description: 'Add a device screenshot matrix',
1647
+ },
1648
+ ],
1649
+ action: async (ctx) => {
1650
+ const url = ctx.args[0];
1651
+ if (!url)
1652
+ throw new Error('URL required. Usage: monomind browse report <url>');
1653
+ // Imported lazily for the same reason getBrowser() is: this module loads
1654
+ // on every CLI invocation, and nothing but `report` needs the renderer.
1655
+ const { runReport, runReportRepeated, readHistory } = await import('../report/index.js');
1656
+ // --history is a read of the store, so it deliberately never launches a
1657
+ // browser — `report --history` must work offline and on a dead site.
1658
+ if (ctx.flags.history) {
1659
+ const { dir, runs } = await readHistory(url);
1660
+ if (ctx.flags.json) {
1661
+ print(JSON.stringify({ data: { dir, runs } }, null, 2));
1662
+ }
1663
+ else if (!runs.length) {
1664
+ output.printWarning(`No stored runs for ${url}`);
1665
+ print(`Looked in: ${dir}`);
1666
+ }
1667
+ else {
1668
+ print(`${runs.length} run(s) for ${url}`);
1669
+ print(`Stored in: ${dir}`);
1670
+ for (const run of runs) {
1671
+ const lcp = run.vitals.lcp === undefined ? '—' : `${Math.round(run.vitals.lcp)}ms`;
1672
+ print(` ${run.capturedAt} ${run.verdict.toUpperCase().padEnd(4)} ` +
1673
+ `LCP ${lcp.padStart(7)} ${run.counts.consoleErrors} console errors ` +
1674
+ `${run.counts.failedRequests} failed requests ${run.counts.a11yErrors} a11y errors`);
1675
+ }
1676
+ }
1677
+ return { success: true, data: { dir, runs } };
1678
+ }
1679
+ const repeatRaw = ctx.flags.repeat;
1680
+ const repeat = typeof repeatRaw === 'number' && Number.isFinite(repeatRaw) && repeatRaw > 1
1681
+ ? Math.floor(repeatRaw)
1682
+ : undefined;
1683
+ const { client, sessionId } = await ensureConnected(_port);
1684
+ const browser = await getBrowser();
1685
+ const rawDevices = ctx.flags.devices;
1686
+ const devices = typeof rawDevices === 'string'
1687
+ ? rawDevices
1688
+ .split(',')
1689
+ .map((d) => d.trim())
1690
+ .filter(Boolean)
1691
+ : undefined;
1692
+ const runOptions = {
1693
+ url,
1694
+ out: ctx.flags.out,
1695
+ budget: ctx.flags.budget,
1696
+ devices,
1697
+ wait: ctx.flags.wait,
1698
+ vitalsWaitMs: ctx.flags['vitals-wait'],
1699
+ loadTimeoutMs: ctx.flags.timeout,
1700
+ fullPage: ctx.flags['full-page'] !== false,
1701
+ cwd: ctx.cwd,
1702
+ // A failing run records its own evidence without being asked; --record
1703
+ // is for when you want the timeline from a run that passes.
1704
+ record: ctx.flags.record === true,
1705
+ history: ctx.flags.save !== false,
1706
+ historyMax: ctx.flags['history-max'],
1707
+ trendWindow: ctx.flags['trend-window'],
1708
+ };
1709
+ let result;
1710
+ try {
1711
+ result = repeat
1712
+ ? await runReportRepeated(client, sessionId, { ...runOptions, repeat })
1713
+ : await runReport(client, sessionId, runOptions);
1714
+ }
1715
+ finally {
1716
+ // A report is a one-shot command — CI should not be left with an
1717
+ // orphan Chrome. Only close a browser THIS process launched: an
1718
+ // attached one belongs to the user's own `open`/`connect` session.
1719
+ if (!ctx.flags['keep-open'] && browser.getLaunchedPid(_port) !== undefined) {
1720
+ browser.stopRequestCapture(sessionId);
1721
+ browser.teardownConsoleCapture(sessionId);
1722
+ try {
1723
+ await browser.closeBrowser(client, _port);
1724
+ }
1725
+ catch {
1726
+ /* best-effort */
1727
+ }
1728
+ _client = null;
1729
+ _sessionId = '';
1730
+ _targetId = '';
1731
+ _refs = new Map();
1732
+ await browser.clearActivePort();
1733
+ await browser.clearRefCache();
1734
+ }
1735
+ }
1736
+ const { report, htmlPath, jsonPath, summary, historyDir } = result;
1737
+ const { flake } = result;
1738
+ // With --repeat the flake verdict governs: a check that failed 2 of 5
1739
+ // runs must not exit 0 just because the last run happened to be green.
1740
+ const passed = flake ? flake.verdict === 'pass' : report.verdict === 'pass';
1741
+ if (ctx.flags.json) {
1742
+ const { toJsonReport } = await import('../report/index.js');
1743
+ print(JSON.stringify({ data: { ...toJsonReport(report), flake, htmlPath, jsonPath, historyDir } }, null, 2));
1744
+ }
1745
+ else {
1746
+ if (passed)
1747
+ output.printSuccess(summary);
1748
+ else
1749
+ output.printError(summary);
1750
+ if (flake) {
1751
+ print(` ${flake.confidenceNote}`);
1752
+ for (const check of flake.checks.filter((c) => !c.stable)) {
1753
+ print(` ${check.label}: failed ${check.failed} of ${check.runs} runs`);
1754
+ }
1755
+ for (const signal of flake.signals.slice(0, 5)) {
1756
+ print(` ${signal.kind} "${signal.signature}" in ${signal.runs} of ${signal.total} runs`);
1757
+ }
1758
+ }
1759
+ for (const failure of report.failures) {
1760
+ print(` ${failure.budget}: expected ${failure.expected}, got ${failure.actual}`);
1761
+ if (failure.detail)
1762
+ print(` ${failure.detail}`);
1763
+ }
1764
+ for (const headline of report.trend?.headlines ?? [])
1765
+ output.printWarning(headline);
1766
+ if (report.diff?.structure) {
1767
+ const d = report.diff.structure;
1768
+ print(` vs previous run: ${d.gained.length} gained, ${d.lost.length} lost, ${d.renamed.length} renamed`);
1769
+ }
1770
+ if (report.diff?.pixels?.comparable) {
1771
+ print(` vs previous run: ${report.diff.pixels.changedPercent.toFixed(2)}% of pixels changed`);
1772
+ }
1773
+ for (const note of report.notes)
1774
+ output.printWarning(note);
1775
+ print(`Report: ${htmlPath}`);
1776
+ print(`JSON: ${jsonPath}`);
1777
+ if (historyDir)
1778
+ print(`History: ${historyDir}`);
1779
+ }
1780
+ // Non-zero exit is the point of RIG-06 — CI and agents gate on it.
1781
+ return {
1782
+ success: passed,
1783
+ exitCode: passed ? 0 : 1,
1784
+ data: { htmlPath, jsonPath, report, flake },
1785
+ };
1786
+ },
1787
+ };
1357
1788
  const closeCommand = {
1358
1789
  name: 'close',
1359
1790
  description: 'Close the active browser session',
@@ -1367,19 +1798,25 @@ const closeCommand = {
1367
1798
  try {
1368
1799
  await browser.stopHarRecording(client, sid);
1369
1800
  }
1370
- catch { /* ignore */ }
1801
+ catch {
1802
+ /* ignore */
1803
+ }
1371
1804
  }
1372
1805
  if (browser.getTraceStatus(sid)) {
1373
1806
  try {
1374
1807
  await browser.stopTrace(client, sid);
1375
1808
  }
1376
- catch { /* ignore */ }
1809
+ catch {
1810
+ /* ignore */
1811
+ }
1377
1812
  }
1378
1813
  if (browser.isProfilingActive(sid)) {
1379
1814
  try {
1380
1815
  await browser.stopCpuProfile(client, sid);
1381
1816
  }
1382
- catch { /* ignore */ }
1817
+ catch {
1818
+ /* ignore */
1819
+ }
1383
1820
  }
1384
1821
  browser.teardownRouteInterception(sid);
1385
1822
  browser.stopRequestCapture(sid);
@@ -1418,7 +1855,9 @@ const closeCommand = {
1418
1855
  try {
1419
1856
  conn.client.close();
1420
1857
  }
1421
- catch { /* already gone */ }
1858
+ catch {
1859
+ /* already gone */
1860
+ }
1422
1861
  }
1423
1862
  // closeBrowser has no PID fallback in a fresh process (launchedPids
1424
1863
  // is per-process) — re-probe so we report what actually happened.
@@ -1428,8 +1867,10 @@ const closeCommand = {
1428
1867
  const probeDeadline = Date.now() + 3000;
1429
1868
  while (stillUp && Date.now() < probeDeadline) {
1430
1869
  try {
1431
- await fetch(`http://127.0.0.1:${persisted.port}/json/version`, { signal: AbortSignal.timeout(800) });
1432
- await new Promise(r => setTimeout(r, 300));
1870
+ await fetch(`http://127.0.0.1:${persisted.port}/json/version`, {
1871
+ signal: AbortSignal.timeout(800),
1872
+ });
1873
+ await new Promise((r) => setTimeout(r, 300));
1433
1874
  }
1434
1875
  catch {
1435
1876
  stillUp = false;
@@ -1733,7 +2174,7 @@ const tapCommand = {
1733
2174
  y = Math.round(box.y);
1734
2175
  }
1735
2176
  else {
1736
- const posJson = await browser.evaluateJs(client, sessionId, `(function(){var el=document.querySelector(${JSON.stringify(arg)});if(!el)return null;var r=el.getBoundingClientRect();return JSON.stringify({x:r.left+r.width/2,y:r.top+r.height/2});})()`);
2177
+ const posJson = (await browser.evaluateJs(client, sessionId, `(function(){var el=document.querySelector(${JSON.stringify(arg)});if(!el)return null;var r=el.getBoundingClientRect();return JSON.stringify({x:r.left+r.width/2,y:r.top+r.height/2});})()`));
1737
2178
  if (!posJson)
1738
2179
  throw new Error(`Selector not found: ${arg}`);
1739
2180
  const pos = JSON.parse(posJson);
@@ -1750,9 +2191,25 @@ const swipeCommand = {
1750
2191
  name: 'swipe',
1751
2192
  description: 'Swipe gesture (mobile). Usage: monomind browse swipe up|down|left|right [distance] [--x N] [--y N]',
1752
2193
  options: [
1753
- { name: 'x', type: 'number', description: 'Start X coordinate (default: center)', default: 200 },
1754
- { name: 'y', type: 'number', description: 'Start Y coordinate (default: center)', default: 400 },
1755
- { name: 'distance', short: 'd', type: 'number', description: 'Swipe distance in pixels', default: 300 },
2194
+ {
2195
+ name: 'x',
2196
+ type: 'number',
2197
+ description: 'Start X coordinate (default: center)',
2198
+ default: 200,
2199
+ },
2200
+ {
2201
+ name: 'y',
2202
+ type: 'number',
2203
+ description: 'Start Y coordinate (default: center)',
2204
+ default: 400,
2205
+ },
2206
+ {
2207
+ name: 'distance',
2208
+ short: 'd',
2209
+ type: 'number',
2210
+ description: 'Swipe distance in pixels',
2211
+ default: 300,
2212
+ },
1756
2213
  ],
1757
2214
  action: async (ctx) => {
1758
2215
  const { client, sessionId } = await ensureConnected(_port);
@@ -1763,14 +2220,16 @@ const swipeCommand = {
1763
2220
  const startX = ctx.flags.x ?? 200;
1764
2221
  const startY = ctx.flags.y ?? 400;
1765
2222
  const positionalDistance = ctx.args[1] !== undefined ? parseInt(ctx.args[1], 10) : undefined;
1766
- const distance = (positionalDistance && Number.isFinite(positionalDistance)) ? positionalDistance : ctx.flags.distance ?? 300;
2223
+ const distance = positionalDistance && Number.isFinite(positionalDistance)
2224
+ ? positionalDistance
2225
+ : (ctx.flags.distance ?? 300);
1767
2226
  const dx = direction === 'right' ? distance : direction === 'left' ? -distance : 0;
1768
2227
  const dy = direction === 'down' ? distance : direction === 'up' ? -distance : 0;
1769
2228
  await client.send('Input.dispatchTouchEvent', { type: 'touchStart', touchPoints: [{ x: startX, y: startY }] }, sessionId);
1770
2229
  const steps = 10;
1771
2230
  for (let i = 1; i <= steps; i++) {
1772
- const x = Math.round(startX + dx * i / steps);
1773
- const y = Math.round(startY + dy * i / steps);
2231
+ const x = Math.round(startX + (dx * i) / steps);
2232
+ const y = Math.round(startY + (dy * i) / steps);
1774
2233
  await client.send('Input.dispatchTouchEvent', { type: 'touchMove', touchPoints: [{ x, y }] }, sessionId);
1775
2234
  await new Promise((r) => setTimeout(r, 16));
1776
2235
  }
@@ -1835,33 +2294,43 @@ const downloadCommand = {
1835
2294
  name: 'download',
1836
2295
  description: 'Click an element and capture the triggered file download. Usage: monomind browse download @e1 ./output.pdf',
1837
2296
  options: [
1838
- { name: 'timeout', short: 't', type: 'number', description: 'Max wait for download in ms', default: 30000 },
2297
+ {
2298
+ name: 'timeout',
2299
+ short: 't',
2300
+ type: 'number',
2301
+ description: 'Max wait for download in ms',
2302
+ default: 30000,
2303
+ },
1839
2304
  { name: 'json', type: 'boolean', description: 'Output result as JSON', default: false },
1840
2305
  ],
1841
2306
  action: async (ctx) => {
1842
2307
  const { client, sessionId } = await ensureConnected(_port);
1843
- const browser = await getBrowser();
2308
+ const _browser = await getBrowser();
1844
2309
  const refOrSel = ctx.args[0];
1845
2310
  const savePath = ctx.args[1];
1846
2311
  if (!refOrSel || !savePath)
1847
2312
  throw new Error('Usage: monomind browse download @e1|selector <save-path>');
1848
- const { mkdir } = await import('fs/promises');
1849
- const { dirname } = await import('path');
1850
- const { tmpdir } = await import('os');
1851
- const { join } = await import('path');
2313
+ const { mkdir } = await import('node:fs/promises');
2314
+ const { dirname } = await import('node:path');
2315
+ const { tmpdir } = await import('node:os');
2316
+ const { join } = await import('node:path');
1852
2317
  const downloadDir = join(tmpdir(), `mm-download-${Date.now()}`);
1853
2318
  await mkdir(downloadDir, { recursive: true });
1854
2319
  // Enable Page.downloadWillBegin / Page.downloadProgress events
1855
- await client.send('Browser.setDownloadBehavior', {
2320
+ await client
2321
+ .send('Browser.setDownloadBehavior', {
1856
2322
  behavior: 'allow',
1857
2323
  downloadPath: downloadDir,
1858
2324
  eventsEnabled: true,
1859
- }, undefined).catch(() => {
2325
+ }, undefined)
2326
+ .catch(() => {
1860
2327
  // Fallback: older Chrome API (session-scoped)
1861
- return client.send('Page.setDownloadBehavior', {
2328
+ return client
2329
+ .send('Page.setDownloadBehavior', {
1862
2330
  behavior: 'allow',
1863
2331
  downloadPath: downloadDir,
1864
- }, sessionId).catch(() => { });
2332
+ }, sessionId)
2333
+ .catch(() => { });
1865
2334
  });
1866
2335
  // Track when download completes
1867
2336
  const downloadPromise = new Promise((resolve, reject) => {
@@ -1873,13 +2342,20 @@ const downloadCommand = {
1873
2342
  let offProgress;
1874
2343
  // cleanup defined before setTimeout so the timeout callback can call it
1875
2344
  let timeout;
1876
- const cleanup = () => { clearTimeout(timeout); offBegin?.(); offProgress?.(); };
1877
- timeout = setTimeout(() => { cleanup(); reject(new Error('Download timed out')); }, ctx.flags.timeout);
2345
+ const cleanup = () => {
2346
+ clearTimeout(timeout);
2347
+ offBegin?.();
2348
+ offProgress?.();
2349
+ };
2350
+ timeout = setTimeout(() => {
2351
+ cleanup();
2352
+ reject(new Error('Download timed out'));
2353
+ }, ctx.flags.timeout);
1878
2354
  offProgress = client.on('Browser.downloadProgress', async (params) => {
1879
2355
  if (params.guid === guid && params.state === 'completed') {
1880
2356
  cleanup();
1881
2357
  // Find the downloaded file in downloadDir
1882
- const { readdir, rename, rmdir } = await import('fs/promises');
2358
+ const { readdir, rename, rmdir } = await import('node:fs/promises');
1883
2359
  const files = await readdir(downloadDir);
1884
2360
  if (files.length > 0) {
1885
2361
  const src = join(downloadDir, files[0]);
@@ -2071,9 +2547,7 @@ const frameCommand = {
2071
2547
  const tabCommand = {
2072
2548
  name: 'tab',
2073
2549
  description: 'Tab management. Usage: monomind browse tab list|new|close [url]',
2074
- options: [
2075
- { name: 'label', type: 'string', description: 'Label for new tab' },
2076
- ],
2550
+ options: [{ name: 'label', type: 'string', description: 'Label for new tab' }],
2077
2551
  action: async (ctx) => {
2078
2552
  const { client, sessionId } = await ensureConnected(_port);
2079
2553
  const browser = await getBrowser();
@@ -2102,19 +2576,25 @@ const tabCommand = {
2102
2576
  try {
2103
2577
  await browser.stopHarRecording(client, sid);
2104
2578
  }
2105
- catch { /* ignore */ }
2579
+ catch {
2580
+ /* ignore */
2581
+ }
2106
2582
  }
2107
2583
  if (browser.getTraceStatus(sid)) {
2108
2584
  try {
2109
2585
  await browser.stopTrace(client, sid);
2110
2586
  }
2111
- catch { /* ignore */ }
2587
+ catch {
2588
+ /* ignore */
2589
+ }
2112
2590
  }
2113
2591
  if (browser.isProfilingActive(sid)) {
2114
2592
  try {
2115
2593
  await browser.stopCpuProfile(client, sid);
2116
2594
  }
2117
- catch { /* ignore */ }
2595
+ catch {
2596
+ /* ignore */
2597
+ }
2118
2598
  }
2119
2599
  browser.teardownRouteInterception(sid);
2120
2600
  browser.stopRequestCapture(sid);
@@ -2142,19 +2622,25 @@ const tabCommand = {
2142
2622
  try {
2143
2623
  await browser.stopHarRecording(client, oldSid);
2144
2624
  }
2145
- catch { /* ignore */ }
2625
+ catch {
2626
+ /* ignore */
2627
+ }
2146
2628
  }
2147
2629
  if (browser.getTraceStatus(oldSid)) {
2148
2630
  try {
2149
2631
  await browser.stopTrace(client, oldSid);
2150
2632
  }
2151
- catch { /* ignore */ }
2633
+ catch {
2634
+ /* ignore */
2635
+ }
2152
2636
  }
2153
2637
  if (browser.isProfilingActive(oldSid)) {
2154
2638
  try {
2155
2639
  await browser.stopCpuProfile(client, oldSid);
2156
2640
  }
2157
- catch { /* ignore */ }
2641
+ catch {
2642
+ /* ignore */
2643
+ }
2158
2644
  }
2159
2645
  await browser.disableInterception(client, oldSid).catch(() => { });
2160
2646
  browser.stopRequestCapture(oldSid);
@@ -2192,19 +2678,25 @@ const windowCommand = {
2192
2678
  try {
2193
2679
  await browser.stopHarRecording(client, oldSid);
2194
2680
  }
2195
- catch { /* ignore */ }
2681
+ catch {
2682
+ /* ignore */
2683
+ }
2196
2684
  }
2197
2685
  if (browser.getTraceStatus(oldSid)) {
2198
2686
  try {
2199
2687
  await browser.stopTrace(client, oldSid);
2200
2688
  }
2201
- catch { /* ignore */ }
2689
+ catch {
2690
+ /* ignore */
2691
+ }
2202
2692
  }
2203
2693
  if (browser.isProfilingActive(oldSid)) {
2204
2694
  try {
2205
2695
  await browser.stopCpuProfile(client, oldSid);
2206
2696
  }
2207
- catch { /* ignore */ }
2697
+ catch {
2698
+ /* ignore */
2699
+ }
2208
2700
  }
2209
2701
  browser.teardownRouteInterception(oldSid);
2210
2702
  browser.stopRequestCapture(oldSid);
@@ -2366,11 +2858,13 @@ const cookiesCommand = {
2366
2858
  if (!name || value === undefined) {
2367
2859
  throw new Error('Usage: monomind browse cookies set <name> <value> [--domain <d>]');
2368
2860
  }
2369
- await browser.setCookies(client, sessionId, [{
2861
+ await browser.setCookies(client, sessionId, [
2862
+ {
2370
2863
  name,
2371
2864
  value,
2372
2865
  domain: ctx.flags.domain,
2373
- }]);
2866
+ },
2867
+ ]);
2374
2868
  output.printSuccess(`Cookie set: ${name}`);
2375
2869
  break;
2376
2870
  }
@@ -2406,9 +2900,7 @@ const pdfCommand = {
2406
2900
  const isCommand = {
2407
2901
  name: 'is',
2408
2902
  description: 'Check element state. Usage: monomind browse is visible|enabled|checked @e1',
2409
- options: [
2410
- { name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
2411
- ],
2903
+ options: [{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false }],
2412
2904
  action: async (ctx) => {
2413
2905
  const { client, sessionId } = await ensureConnected(_port);
2414
2906
  const browser = await getBrowser();
@@ -2429,7 +2921,8 @@ const isCommand = {
2429
2921
  case 'checked':
2430
2922
  result = await browser.isChecked(client, sessionId, ref);
2431
2923
  break;
2432
- default: throw new Error(`Unknown check: ${check}. Use: visible|enabled|checked`);
2924
+ default:
2925
+ throw new Error(`Unknown check: ${check}. Use: visible|enabled|checked`);
2433
2926
  }
2434
2927
  if (ctx.flags.json) {
2435
2928
  print(JSON.stringify({ data: { [check]: result } }));
@@ -2467,7 +2960,7 @@ const findCommand = {
2467
2960
  if (locator === 'alttext') {
2468
2961
  // I2: use JS attribute comparison to avoid broken CSS selectors for values with spaces/quotes
2469
2962
  const valJson = JSON.stringify(value);
2470
- const found = await browser.evaluateJs(client, sessionId, `(function(v){var el=document.querySelector('img[alt]')||null;var all=document.querySelectorAll('[alt]');for(var i=0;i<all.length;i++){if(all[i].getAttribute('alt')===v){all[i].setAttribute('data-mm-located','true');return true;}}return false;})(${valJson})`);
2963
+ const found = (await browser.evaluateJs(client, sessionId, `(function(v){var el=document.querySelector('img[alt]')||null;var all=document.querySelectorAll('[alt]');for(var i=0;i<all.length;i++){if(all[i].getAttribute('alt')===v){all[i].setAttribute('data-mm-located','true');return true;}}return false;})(${valJson})`));
2471
2964
  if (!found) {
2472
2965
  output.printWarning(`alttext not found: ${value}`);
2473
2966
  return { success: false };
@@ -2479,7 +2972,7 @@ const findCommand = {
2479
2972
  if (locator === 'title') {
2480
2973
  // I2: use JS attribute comparison to avoid broken CSS selectors for values with spaces/quotes
2481
2974
  const valJson = JSON.stringify(value);
2482
- const found = await browser.evaluateJs(client, sessionId, `(function(v){var all=document.querySelectorAll('[title]');for(var i=0;i<all.length;i++){if(all[i].getAttribute('title')===v){all[i].setAttribute('data-mm-located','true');return true;}}return false;})(${valJson})`);
2975
+ const found = (await browser.evaluateJs(client, sessionId, `(function(v){var all=document.querySelectorAll('[title]');for(var i=0;i<all.length;i++){if(all[i].getAttribute('title')===v){all[i].setAttribute('data-mm-located','true');return true;}}return false;})(${valJson})`));
2483
2976
  if (!found) {
2484
2977
  output.printWarning(`title not found: ${value}`);
2485
2978
  return { success: false };
@@ -2585,7 +3078,13 @@ const diffCommand = {
2585
3078
  name: 'diff',
2586
3079
  description: 'Compare two URLs or snapshots. Usage: monomind browse diff url <url1> <url2> [--interactive] [--json]',
2587
3080
  options: [
2588
- { name: 'interactive', short: 'i', type: 'boolean', description: 'Snapshot interactive elements only', default: false },
3081
+ {
3082
+ name: 'interactive',
3083
+ short: 'i',
3084
+ type: 'boolean',
3085
+ description: 'Snapshot interactive elements only',
3086
+ default: false,
3087
+ },
2589
3088
  { name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
2590
3089
  ],
2591
3090
  action: async (ctx) => {
@@ -2600,11 +3099,15 @@ const diffCommand = {
2600
3099
  // Capture snapshot at url1
2601
3100
  await browser.openUrl(client, sessionId, url1);
2602
3101
  await browser.waitFor(client, sessionId, { load: 'load', timeout: 15000 });
2603
- const snap1 = await browser.captureSnapshot(client, sessionId, { interactiveOnly: ctx.flags.interactive });
3102
+ const snap1 = await browser.captureSnapshot(client, sessionId, {
3103
+ interactiveOnly: ctx.flags.interactive,
3104
+ });
2604
3105
  // Capture snapshot at url2
2605
3106
  await browser.openUrl(client, sessionId, url2);
2606
3107
  await browser.waitFor(client, sessionId, { load: 'load', timeout: 15000 });
2607
- const snap2 = await browser.captureSnapshot(client, sessionId, { interactiveOnly: ctx.flags.interactive });
3108
+ const snap2 = await browser.captureSnapshot(client, sessionId, {
3109
+ interactiveOnly: ctx.flags.interactive,
3110
+ });
2608
3111
  _refs = snap2.refs;
2609
3112
  await browser.saveRefCache(_targetId, snap2.url, _refs);
2610
3113
  // Text diff
@@ -2616,7 +3119,15 @@ const diffCommand = {
2616
3119
  const onlyIn2 = lines2.filter((l) => !set1.has(l));
2617
3120
  const changed = onlyIn1.length > 0 || onlyIn2.length > 0;
2618
3121
  if (ctx.flags.json) {
2619
- print(JSON.stringify({ changed, url1, url2, onlyIn1, onlyIn2, additions: onlyIn2.length, removals: onlyIn1.length }));
3122
+ print(JSON.stringify({
3123
+ changed,
3124
+ url1,
3125
+ url2,
3126
+ onlyIn1,
3127
+ onlyIn2,
3128
+ additions: onlyIn2.length,
3129
+ removals: onlyIn1.length,
3130
+ }));
2620
3131
  }
2621
3132
  else {
2622
3133
  if (!changed) {
@@ -2630,7 +3141,10 @@ const diffCommand = {
2630
3141
  print(`\x1b[32m+ ${l}\x1b[0m`);
2631
3142
  }
2632
3143
  }
2633
- return { success: true, data: { changed, url1, url2, additions: onlyIn2.length, removals: onlyIn1.length } };
3144
+ return {
3145
+ success: true,
3146
+ data: { changed, url1, url2, additions: onlyIn2.length, removals: onlyIn1.length },
3147
+ };
2634
3148
  }
2635
3149
  throw new Error('Usage: monomind browse diff url <url1> <url2>');
2636
3150
  },
@@ -2744,7 +3258,6 @@ export function parseBatchCommandLine(cmdStr) {
2744
3258
  flags[boolFlag[1]] = true;
2745
3259
  rest = rest.slice(boolFlag[0].length);
2746
3260
  consumedAnother = true;
2747
- continue;
2748
3261
  }
2749
3262
  }
2750
3263
  return { subName: 'eval', subArgs: [rest], flags };
@@ -2803,7 +3316,9 @@ const batchCommand = {
2803
3316
  parsedFlags[key] = true;
2804
3317
  }
2805
3318
  }
2806
- else if (subArgs[i].startsWith('-') && subArgs[i].length === 2 && /[a-zA-Z]/.test(subArgs[i][1])) {
3319
+ else if (subArgs[i].startsWith('-') &&
3320
+ subArgs[i].length === 2 &&
3321
+ /[a-zA-Z]/.test(subArgs[i][1])) {
2807
3322
  consumedIndices.add(i);
2808
3323
  const shortKey = subArgs[i][1];
2809
3324
  const optDef = subCmd.options?.find((o) => o.short === shortKey);
@@ -2835,7 +3350,11 @@ const batchCommand = {
2835
3350
  };
2836
3351
  const cmdResult = await subCmd.action(fakeCtx);
2837
3352
  const succeeded = cmdResult?.success !== false;
2838
- results.push({ command: cmdStr, success: succeeded, error: succeeded ? undefined : 'Command returned failure' });
3353
+ results.push({
3354
+ command: cmdStr,
3355
+ success: succeeded,
3356
+ error: succeeded ? undefined : 'Command returned failure',
3357
+ });
2839
3358
  if (!succeeded && ctx.flags.bail)
2840
3359
  break;
2841
3360
  }
@@ -2886,7 +3405,12 @@ const connectCommand = {
2886
3405
  options: [
2887
3406
  { name: 'port', short: 'p', type: 'number', description: 'CDP port', default: 9222 },
2888
3407
  { name: 'target', type: 'string', description: 'Target ID to attach to' },
2889
- { name: 'auto-connect', type: 'boolean', description: 'Auto-discover running Chrome on ports 9222 and 9229', default: false },
3408
+ {
3409
+ name: 'auto-connect',
3410
+ type: 'boolean',
3411
+ description: 'Auto-discover running Chrome on ports 9222 and 9229',
3412
+ default: false,
3413
+ },
2890
3414
  ],
2891
3415
  action: async (ctx) => {
2892
3416
  let port = ctx.flags.port ?? 9222;
@@ -2902,7 +3426,9 @@ const connectCommand = {
2902
3426
  break;
2903
3427
  }
2904
3428
  }
2905
- catch { /* port not open */ }
3429
+ catch {
3430
+ /* port not open */
3431
+ }
2906
3432
  }
2907
3433
  if (!found)
2908
3434
  throw new Error('No running Chrome instance found. Launch Chrome with --remote-debugging-port or use --port.');
@@ -2915,19 +3441,25 @@ const connectCommand = {
2915
3441
  try {
2916
3442
  await browser.stopHarRecording(prevClient, prevSid);
2917
3443
  }
2918
- catch { /* ignore */ }
3444
+ catch {
3445
+ /* ignore */
3446
+ }
2919
3447
  }
2920
3448
  if (browser.getTraceStatus(prevSid)) {
2921
3449
  try {
2922
3450
  await browser.stopTrace(prevClient, prevSid);
2923
3451
  }
2924
- catch { /* ignore */ }
3452
+ catch {
3453
+ /* ignore */
3454
+ }
2925
3455
  }
2926
3456
  if (browser.isProfilingActive(prevSid)) {
2927
3457
  try {
2928
3458
  await browser.stopCpuProfile(prevClient, prevSid);
2929
3459
  }
2930
- catch { /* ignore */ }
3460
+ catch {
3461
+ /* ignore */
3462
+ }
2931
3463
  }
2932
3464
  browser.teardownRouteInterception(prevSid);
2933
3465
  browser.stopRequestCapture(prevSid);
@@ -3030,7 +3562,9 @@ const traceCommand = {
3030
3562
  throw new Error('Usage: monomind browse trace start|stop [path]');
3031
3563
  switch (action) {
3032
3564
  case 'start':
3033
- await browser.startTrace(client, sessionId, { screenshots: ctx.flags.screenshots });
3565
+ await browser.startTrace(client, sessionId, {
3566
+ screenshots: ctx.flags.screenshots,
3567
+ });
3034
3568
  output.printSuccess('Trace started');
3035
3569
  return { success: true };
3036
3570
  case 'stop': {
@@ -3064,7 +3598,9 @@ const profilerCommand = {
3064
3598
  throw new Error('Usage: monomind browse profiler start|stop|heap [path]');
3065
3599
  switch (action) {
3066
3600
  case 'start':
3067
- await browser.startCpuProfile(client, sessionId, { samplingInterval: ctx.flags.interval });
3601
+ await browser.startCpuProfile(client, sessionId, {
3602
+ samplingInterval: ctx.flags.interval,
3603
+ });
3068
3604
  output.printSuccess('CPU profiler started');
3069
3605
  return { success: true };
3070
3606
  case 'stop': {
@@ -3155,7 +3691,7 @@ const resizeCommand = {
3155
3691
  const browser = await getBrowser();
3156
3692
  const width = parseInt(ctx.args[0], 10);
3157
3693
  const height = parseInt(ctx.args[1], 10);
3158
- if (isNaN(width) || isNaN(height))
3694
+ if (Number.isNaN(width) || Number.isNaN(height))
3159
3695
  throw new Error('Usage: monomind browse resize <width> <height>');
3160
3696
  await browser.setViewport(client, sessionId, width, height);
3161
3697
  output.printSuccess(`Resized to ${width}x${height}`);
@@ -3291,6 +3827,7 @@ const browseCommand = {
3291
3827
  traceCommand,
3292
3828
  profilerCommand,
3293
3829
  vitalsCommand,
3830
+ reportCommand,
3294
3831
  harCommand,
3295
3832
  resizeCommand,
3296
3833
  closeCommand,
@@ -3303,7 +3840,10 @@ const browseCommand = {
3303
3840
  ],
3304
3841
  examples: [
3305
3842
  { command: 'monomind browse open https://example.com', description: 'Open a URL' },
3306
- { command: 'monomind browse snapshot -i', description: 'Interactive-only snapshot (93% token reduction)' },
3843
+ {
3844
+ command: 'monomind browse snapshot -i',
3845
+ description: 'Interactive-only snapshot (93% token reduction)',
3846
+ },
3307
3847
  { command: 'monomind browse click @e3', description: 'Click element by ref' },
3308
3848
  { command: 'monomind browse fill @e1 "user@example.com"', description: 'Fill an input' },
3309
3849
  { command: 'monomind browse press Enter', description: 'Press Enter key' },
@@ -3317,7 +3857,14 @@ const browseCommand = {
3317
3857
  { command: 'monomind browse state save my-session', description: 'Save session state' },
3318
3858
  { command: 'monomind browse navigate back', description: 'Navigate back' },
3319
3859
  { command: 'monomind browse eval "document.title"', description: 'Evaluate JavaScript' },
3320
- { command: 'monomind browse network route --pattern "https://api.*" --abort', description: 'Abort API calls' },
3860
+ {
3861
+ command: 'monomind browse network route --pattern "https://api.*" --abort',
3862
+ description: 'Abort API calls',
3863
+ },
3864
+ {
3865
+ command: 'monomind browse report https://example.com',
3866
+ description: 'One-command page report with a pass/fail verdict',
3867
+ },
3321
3868
  { command: 'monomind browse close', description: 'Close browser session' },
3322
3869
  ],
3323
3870
  action: async (_ctx) => {
@@ -3374,6 +3921,7 @@ const browseCommand = {
3374
3921
  output.printInfo(' batch Execute multiple commands');
3375
3922
  output.printInfo(' addinitscript Add script to run before page navigation');
3376
3923
  output.printInfo(' removeinitscript Remove a previously added init script');
3924
+ output.printInfo(' report Test a page and write an HTML report');
3377
3925
  output.printInfo(' close Close the browser session');
3378
3926
  return { success: true };
3379
3927
  },