@monoes/monobrowse 1.0.2 → 1.0.4

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 (277) hide show
  1. package/dist/src/__tests__/batch-eval-parsing.test.d.ts +2 -0
  2. package/dist/src/__tests__/batch-eval-parsing.test.d.ts.map +1 -0
  3. package/dist/src/__tests__/batch-eval-parsing.test.js +47 -0
  4. package/dist/src/__tests__/batch-eval-parsing.test.js.map +1 -0
  5. package/dist/src/__tests__/output.test.d.ts +2 -0
  6. package/dist/src/__tests__/output.test.d.ts.map +1 -0
  7. package/dist/src/__tests__/output.test.js +54 -0
  8. package/dist/src/__tests__/output.test.js.map +1 -0
  9. package/dist/src/__tests__/ref-cache.test.d.ts +2 -0
  10. package/dist/src/__tests__/ref-cache.test.d.ts.map +1 -0
  11. package/dist/src/__tests__/ref-cache.test.js +145 -0
  12. package/dist/src/__tests__/ref-cache.test.js.map +1 -0
  13. package/dist/src/browser/action-builder/analyzer.d.ts +0 -0
  14. package/dist/src/browser/action-builder/analyzer.d.ts.map +0 -0
  15. package/dist/src/browser/action-builder/analyzer.js +0 -0
  16. package/dist/src/browser/action-builder/analyzer.js.map +1 -1
  17. package/dist/src/browser/action-builder/types.d.ts +0 -0
  18. package/dist/src/browser/action-builder/types.d.ts.map +0 -0
  19. package/dist/src/browser/action-builder/types.js +0 -0
  20. package/dist/src/browser/action-builder/types.js.map +0 -0
  21. package/dist/src/browser/actions.d.ts +1 -1
  22. package/dist/src/browser/actions.d.ts.map +1 -1
  23. package/dist/src/browser/actions.js +19 -2
  24. package/dist/src/browser/actions.js.map +1 -1
  25. package/dist/src/browser/adapters/gemini.d.ts +0 -0
  26. package/dist/src/browser/adapters/gemini.d.ts.map +0 -0
  27. package/dist/src/browser/adapters/gemini.js +0 -0
  28. package/dist/src/browser/adapters/gemini.js.map +0 -0
  29. package/dist/src/browser/adapters/google.d.ts +0 -0
  30. package/dist/src/browser/adapters/google.d.ts.map +0 -0
  31. package/dist/src/browser/adapters/google.js +0 -0
  32. package/dist/src/browser/adapters/google.js.map +0 -0
  33. package/dist/src/browser/adapters/index.d.ts +0 -0
  34. package/dist/src/browser/adapters/index.d.ts.map +0 -0
  35. package/dist/src/browser/adapters/index.js +0 -0
  36. package/dist/src/browser/adapters/index.js.map +0 -0
  37. package/dist/src/browser/adapters/instagram.d.ts +0 -0
  38. package/dist/src/browser/adapters/instagram.d.ts.map +0 -0
  39. package/dist/src/browser/adapters/instagram.js +0 -0
  40. package/dist/src/browser/adapters/instagram.js.map +0 -0
  41. package/dist/src/browser/adapters/linkedin.d.ts +0 -0
  42. package/dist/src/browser/adapters/linkedin.d.ts.map +0 -0
  43. package/dist/src/browser/adapters/linkedin.js +0 -0
  44. package/dist/src/browser/adapters/linkedin.js.map +0 -0
  45. package/dist/src/browser/adapters/microsoft.d.ts +0 -0
  46. package/dist/src/browser/adapters/microsoft.d.ts.map +0 -0
  47. package/dist/src/browser/adapters/microsoft.js +0 -0
  48. package/dist/src/browser/adapters/microsoft.js.map +0 -0
  49. package/dist/src/browser/adapters/x.d.ts +0 -0
  50. package/dist/src/browser/adapters/x.d.ts.map +0 -0
  51. package/dist/src/browser/adapters/x.js +0 -0
  52. package/dist/src/browser/adapters/x.js.map +0 -0
  53. package/dist/src/browser/batch.d.ts +0 -0
  54. package/dist/src/browser/batch.d.ts.map +0 -0
  55. package/dist/src/browser/batch.js +0 -0
  56. package/dist/src/browser/batch.js.map +0 -0
  57. package/dist/src/browser/browser.d.ts +10 -0
  58. package/dist/src/browser/browser.d.ts.map +1 -1
  59. package/dist/src/browser/browser.js +111 -3
  60. package/dist/src/browser/browser.js.map +1 -1
  61. package/dist/src/browser/cdp.d.ts +0 -0
  62. package/dist/src/browser/cdp.d.ts.map +1 -1
  63. package/dist/src/browser/cdp.js +1 -1
  64. package/dist/src/browser/cdp.js.map +1 -1
  65. package/dist/src/browser/console-log.d.ts +0 -0
  66. package/dist/src/browser/console-log.d.ts.map +0 -0
  67. package/dist/src/browser/console-log.js +0 -0
  68. package/dist/src/browser/console-log.js.map +0 -0
  69. package/dist/src/browser/dashboard/api-types.d.ts +0 -0
  70. package/dist/src/browser/dashboard/api-types.d.ts.map +0 -0
  71. package/dist/src/browser/dashboard/api-types.js +0 -0
  72. package/dist/src/browser/dashboard/api-types.js.map +0 -0
  73. package/dist/src/browser/dashboard/server.d.ts +0 -0
  74. package/dist/src/browser/dashboard/server.d.ts.map +1 -1
  75. package/dist/src/browser/dashboard/server.js +37 -2
  76. package/dist/src/browser/dashboard/server.js.map +1 -1
  77. package/dist/src/browser/dashboard/ui.html +1 -1
  78. package/dist/src/browser/dialog.d.ts +0 -0
  79. package/dist/src/browser/dialog.d.ts.map +0 -0
  80. package/dist/src/browser/dialog.js +0 -0
  81. package/dist/src/browser/dialog.js.map +0 -0
  82. package/dist/src/browser/emulation.d.ts +0 -0
  83. package/dist/src/browser/emulation.d.ts.map +0 -0
  84. package/dist/src/browser/emulation.js +0 -0
  85. package/dist/src/browser/emulation.js.map +0 -0
  86. package/dist/src/browser/find.d.ts +0 -0
  87. package/dist/src/browser/find.d.ts.map +0 -0
  88. package/dist/src/browser/find.js +0 -0
  89. package/dist/src/browser/find.js.map +1 -1
  90. package/dist/src/browser/har.d.ts +0 -0
  91. package/dist/src/browser/har.d.ts.map +0 -0
  92. package/dist/src/browser/har.js +0 -0
  93. package/dist/src/browser/har.js.map +0 -0
  94. package/dist/src/browser/index.d.ts +1 -0
  95. package/dist/src/browser/index.d.ts.map +1 -1
  96. package/dist/src/browser/index.js +1 -0
  97. package/dist/src/browser/index.js.map +1 -1
  98. package/dist/src/browser/network.d.ts +7 -0
  99. package/dist/src/browser/network.d.ts.map +1 -1
  100. package/dist/src/browser/network.js +10 -0
  101. package/dist/src/browser/network.js.map +1 -1
  102. package/dist/src/browser/pdf.d.ts +0 -0
  103. package/dist/src/browser/pdf.d.ts.map +0 -0
  104. package/dist/src/browser/pdf.js +0 -0
  105. package/dist/src/browser/pdf.js.map +1 -1
  106. package/dist/src/browser/profiler.d.ts +0 -0
  107. package/dist/src/browser/profiler.d.ts.map +0 -0
  108. package/dist/src/browser/profiler.js +0 -0
  109. package/dist/src/browser/profiler.js.map +1 -1
  110. package/dist/src/browser/record.d.ts +3 -0
  111. package/dist/src/browser/record.d.ts.map +1 -1
  112. package/dist/src/browser/record.js +37 -5
  113. package/dist/src/browser/record.js.map +1 -1
  114. package/dist/src/browser/ref-cache.d.ts +41 -0
  115. package/dist/src/browser/ref-cache.d.ts.map +1 -0
  116. package/dist/src/browser/ref-cache.js +120 -0
  117. package/dist/src/browser/ref-cache.js.map +1 -0
  118. package/dist/src/browser/screenshot.d.ts +0 -0
  119. package/dist/src/browser/screenshot.d.ts.map +0 -0
  120. package/dist/src/browser/screenshot.js +0 -0
  121. package/dist/src/browser/screenshot.js.map +1 -1
  122. package/dist/src/browser/session.d.ts +0 -0
  123. package/dist/src/browser/session.d.ts.map +1 -1
  124. package/dist/src/browser/session.js +12 -7
  125. package/dist/src/browser/session.js.map +1 -1
  126. package/dist/src/browser/snapshot.d.ts +0 -0
  127. package/dist/src/browser/snapshot.d.ts.map +0 -0
  128. package/dist/src/browser/snapshot.js +0 -0
  129. package/dist/src/browser/snapshot.js.map +1 -1
  130. package/dist/src/browser/storage.d.ts +0 -0
  131. package/dist/src/browser/storage.d.ts.map +0 -0
  132. package/dist/src/browser/storage.js +0 -0
  133. package/dist/src/browser/storage.js.map +0 -0
  134. package/dist/src/browser/tabs.d.ts +1 -1
  135. package/dist/src/browser/tabs.d.ts.map +1 -1
  136. package/dist/src/browser/tabs.js +20 -4
  137. package/dist/src/browser/tabs.js.map +1 -1
  138. package/dist/src/browser/trace.d.ts +0 -0
  139. package/dist/src/browser/trace.d.ts.map +0 -0
  140. package/dist/src/browser/trace.js +0 -0
  141. package/dist/src/browser/trace.js.map +1 -1
  142. package/dist/src/browser/types.d.ts +0 -0
  143. package/dist/src/browser/types.d.ts.map +0 -0
  144. package/dist/src/browser/types.js +0 -0
  145. package/dist/src/browser/types.js.map +0 -0
  146. package/dist/src/browser/vitals.d.ts +0 -0
  147. package/dist/src/browser/vitals.d.ts.map +0 -0
  148. package/dist/src/browser/vitals.js +0 -0
  149. package/dist/src/browser/vitals.js.map +0 -0
  150. package/dist/src/browser/wait.d.ts +0 -0
  151. package/dist/src/browser/wait.d.ts.map +0 -0
  152. package/dist/src/browser/wait.js +0 -0
  153. package/dist/src/browser/wait.js.map +0 -0
  154. package/dist/src/cli/action.d.ts +0 -0
  155. package/dist/src/cli/action.d.ts.map +1 -1
  156. package/dist/src/cli/action.js +87 -2
  157. package/dist/src/cli/action.js.map +1 -1
  158. package/dist/src/cli/commands.d.ts +22 -0
  159. package/dist/src/cli/commands.d.ts.map +1 -1
  160. package/dist/src/cli/commands.js +276 -28
  161. package/dist/src/cli/commands.js.map +1 -1
  162. package/dist/src/cli/output.d.ts +0 -0
  163. package/dist/src/cli/output.d.ts.map +1 -1
  164. package/dist/src/cli/output.js +0 -0
  165. package/dist/src/cli/output.js.map +0 -0
  166. package/dist/src/cli/platform.d.ts +0 -0
  167. package/dist/src/cli/platform.d.ts.map +0 -0
  168. package/dist/src/cli/platform.js +0 -0
  169. package/dist/src/cli/platform.js.map +0 -0
  170. package/dist/src/cli/types.d.ts +0 -0
  171. package/dist/src/cli/types.d.ts.map +0 -0
  172. package/dist/src/cli/types.js +0 -0
  173. package/dist/src/cli/types.js.map +0 -0
  174. package/dist/src/cli.d.ts +0 -0
  175. package/dist/src/cli.d.ts.map +0 -0
  176. package/dist/src/cli.js.map +1 -1
  177. package/dist/src/index.d.ts +0 -0
  178. package/dist/src/index.d.ts.map +0 -0
  179. package/dist/src/index.js +0 -0
  180. package/dist/src/index.js.map +0 -0
  181. package/dist/tsconfig.tsbuildinfo +1 -1
  182. package/package.json +2 -2
  183. package/src/__tests__/batch-eval-parsing.test.ts +52 -0
  184. package/src/__tests__/output.test.ts +62 -0
  185. package/src/__tests__/ref-cache.test.ts +175 -0
  186. package/src/browser/action-builder/analyzer.ts +0 -0
  187. package/src/browser/action-builder/types.ts +0 -0
  188. package/src/browser/actions.ts +22 -2
  189. package/src/browser/adapters/gemini.ts +0 -0
  190. package/src/browser/adapters/google.ts +0 -0
  191. package/src/browser/adapters/index.ts +0 -0
  192. package/src/browser/adapters/instagram.ts +0 -0
  193. package/src/browser/adapters/linkedin.ts +0 -0
  194. package/src/browser/adapters/microsoft.ts +0 -0
  195. package/src/browser/adapters/x.ts +0 -0
  196. package/src/browser/batch.ts +0 -0
  197. package/src/browser/browser.ts +115 -2
  198. package/src/browser/cdp.ts +1 -1
  199. package/src/browser/console-log.ts +0 -0
  200. package/src/browser/dashboard/api-types.ts +0 -0
  201. package/src/browser/dashboard/server.ts +37 -2
  202. package/src/browser/dashboard/ui.html +1 -1
  203. package/src/browser/dialog.ts +0 -0
  204. package/src/browser/emulation.ts +0 -0
  205. package/src/browser/find.ts +0 -0
  206. package/src/browser/har.ts +0 -0
  207. package/src/browser/index.ts +1 -0
  208. package/src/browser/network.ts +11 -0
  209. package/src/browser/pdf.ts +0 -0
  210. package/src/browser/profiler.ts +0 -0
  211. package/src/browser/record.ts +43 -6
  212. package/src/browser/ref-cache.ts +141 -0
  213. package/src/browser/screenshot.ts +0 -0
  214. package/src/browser/session.ts +13 -7
  215. package/src/browser/snapshot.ts +0 -0
  216. package/src/browser/storage.ts +0 -0
  217. package/src/browser/tabs.ts +36 -7
  218. package/src/browser/trace.ts +0 -0
  219. package/src/browser/types.ts +0 -0
  220. package/src/browser/vitals.ts +0 -0
  221. package/src/browser/wait.ts +0 -0
  222. package/src/cli/action.ts +85 -3
  223. package/src/cli/commands.ts +272 -28
  224. package/src/cli/output.ts +0 -0
  225. package/src/cli/platform.ts +0 -0
  226. package/src/cli/types.ts +0 -0
  227. package/src/cli.ts +0 -0
  228. package/src/index.ts +0 -0
  229. package/dist/package.json +0 -71
  230. package/dist/src/browser/playbook/browser-handlers.d.ts +0 -3
  231. package/dist/src/browser/playbook/browser-handlers.d.ts.map +0 -1
  232. package/dist/src/browser/playbook/browser-handlers.js +0 -377
  233. package/dist/src/browser/playbook/browser-handlers.js.map +0 -1
  234. package/dist/src/browser/playbook/builtin-handlers.d.ts +0 -3
  235. package/dist/src/browser/playbook/builtin-handlers.d.ts.map +0 -1
  236. package/dist/src/browser/playbook/builtin-handlers.js +0 -364
  237. package/dist/src/browser/playbook/builtin-handlers.js.map +0 -1
  238. package/dist/src/browser/playbook/index.d.ts +0 -3
  239. package/dist/src/browser/playbook/index.d.ts.map +0 -1
  240. package/dist/src/browser/playbook/index.js +0 -3
  241. package/dist/src/browser/playbook/index.js.map +0 -1
  242. package/dist/src/browser/playbook/store.d.ts +0 -8
  243. package/dist/src/browser/playbook/store.d.ts.map +0 -1
  244. package/dist/src/browser/playbook/store.js +0 -87
  245. package/dist/src/browser/playbook/store.js.map +0 -1
  246. package/dist/src/browser/workflow/builtin-handlers.d.ts +0 -3
  247. package/dist/src/browser/workflow/builtin-handlers.d.ts.map +0 -1
  248. package/dist/src/browser/workflow/builtin-handlers.js +0 -343
  249. package/dist/src/browser/workflow/builtin-handlers.js.map +0 -1
  250. package/dist/src/browser/workflow/engine.d.ts +0 -10
  251. package/dist/src/browser/workflow/engine.d.ts.map +0 -1
  252. package/dist/src/browser/workflow/engine.js +0 -174
  253. package/dist/src/browser/workflow/engine.js.map +0 -1
  254. package/dist/src/browser/workflow/expression.d.ts +0 -4
  255. package/dist/src/browser/workflow/expression.d.ts.map +0 -1
  256. package/dist/src/browser/workflow/expression.js +0 -89
  257. package/dist/src/browser/workflow/expression.js.map +0 -1
  258. package/dist/src/browser/workflow/index.d.ts +0 -6
  259. package/dist/src/browser/workflow/index.d.ts.map +0 -1
  260. package/dist/src/browser/workflow/index.js +0 -6
  261. package/dist/src/browser/workflow/index.js.map +0 -1
  262. package/dist/src/browser/workflow/store.d.ts +0 -8
  263. package/dist/src/browser/workflow/store.d.ts.map +0 -1
  264. package/dist/src/browser/workflow/store.js +0 -87
  265. package/dist/src/browser/workflow/store.js.map +0 -1
  266. package/dist/src/browser/workflow/types.d.ts +0 -62
  267. package/dist/src/browser/workflow/types.d.ts.map +0 -1
  268. package/dist/src/browser/workflow/types.js +0 -2
  269. package/dist/src/browser/workflow/types.js.map +0 -1
  270. package/dist/src/cli/playbook.d.ts +0 -3
  271. package/dist/src/cli/playbook.d.ts.map +0 -1
  272. package/dist/src/cli/playbook.js +0 -265
  273. package/dist/src/cli/playbook.js.map +0 -1
  274. package/dist/src/cli/workflow.d.ts +0 -2
  275. package/dist/src/cli/workflow.d.ts.map +0 -1
  276. package/dist/src/cli/workflow.js +0 -3
  277. package/dist/src/cli/workflow.js.map +0 -1
@@ -6,14 +6,41 @@ import { output } from './output.js';
6
6
  import { createActionCommand } from './action.js';
7
7
  import { createPlatformCommand } from './platform.js';
8
8
  // Runtime state (single session per CLI process)
9
+ const DEFAULT_PORT = 9222;
9
10
  let _client = null;
10
11
  let _sessionId = '';
11
12
  let _targetId = '';
12
- let _port = 9222;
13
+ let _port = DEFAULT_PORT;
13
14
  let _refs = new Map();
14
15
  async function getBrowser() {
15
16
  return import('../index.js');
16
17
  }
18
+ // Each CLI invocation is a fresh process, so this module's `_port` variable
19
+ // is always freshly initialized to DEFAULT_PORT — a `--port 9333` passed to
20
+ // an earlier `open` command has no effect on this process's own default.
21
+ // Resolve the persisted "active port" (written by `open --port N`, read via
22
+ // ref-cache.ts's saveActivePort/loadActivePort) so subsequent commands
23
+ // attach to the browser the user actually opened instead of silently
24
+ // launching/attaching to a second, unrelated Chrome instance on 9222.
25
+ async function resolveDefaultPort(browser) {
26
+ const persisted = await browser.loadActivePortInfo();
27
+ if (!persisted)
28
+ return DEFAULT_PORT;
29
+ if (!persisted.launched) {
30
+ // connect-origin port: the browser belongs to someone else. If it died,
31
+ // relaunching our own headless Chrome on that port would squat the
32
+ // user's debug port and silently swap which browser commands act on —
33
+ // fail loudly instead.
34
+ try {
35
+ await fetch(`http://127.0.0.1:${persisted.port}/json/version`, { signal: AbortSignal.timeout(1500) });
36
+ }
37
+ catch {
38
+ await browser.clearActivePort();
39
+ throw new Error(`Connected browser on port ${persisted.port} is gone — re-run \`connect\` (or \`open\` to launch a fresh one).`);
40
+ }
41
+ }
42
+ return persisted.port;
43
+ }
17
44
  async function ensureConnected(port, targetId) {
18
45
  const browser = await getBrowser();
19
46
  if (!_client || !_client.isConnected()) {
@@ -24,15 +51,45 @@ async function ensureConnected(port, targetId) {
24
51
  browser.teardownConsoleCapture(_sessionId);
25
52
  _client.close();
26
53
  }
27
- _port = await browser.launchBrowser({ port, headless: true });
54
+ const effectivePort = port === DEFAULT_PORT ? await resolveDefaultPort(browser) : port;
55
+ _port = await browser.launchBrowser({ port: effectivePort, headless: true });
28
56
  const conn = await browser.connectToTarget(_port, targetId);
29
57
  _client = conn.client;
30
58
  _sessionId = conn.sessionId;
31
59
  _targetId = conn.target.id;
32
60
  _refs = new Map();
61
+ await hydrateRefsFromCache(browser, _targetId, conn.target.url);
33
62
  }
34
63
  return { client: _client, sessionId: _sessionId, targetId: _targetId };
35
64
  }
65
+ // Each CLI invocation is a fresh process, so the in-memory _refs Map built by
66
+ // a prior `snapshot` command is gone by the time a later `find`/`click`/etc.
67
+ // command runs. Rehydrate it from the on-disk ref cache (written by
68
+ // captureSnapshot call sites below) so refs resolved by a previous process
69
+ // remain usable. Falls back silently to an empty Map if no cache exists.
70
+ //
71
+ // The cached `url` field (captured at snapshot time) is compared against the
72
+ // browser's CURRENT url (`currentUrl`, from the just-fetched target info).
73
+ // A stale `backendDOMNodeId` can still successfully resolve via
74
+ // DOM.getBoxModel even after a same-tab SPA navigation or DOM mutation
75
+ // changed what's actually at those coordinates — so a mismatch here means
76
+ // EVERY ref in the cache is potentially pointing at the wrong element. We
77
+ // hard-invalidate (skip hydration entirely, so any @eN lookup fails loudly
78
+ // via resolveRef's "not found" error) rather than the weaker 30s time-based
79
+ // staleness check below, which only warns.
80
+ async function hydrateRefsFromCache(browser, targetId, currentUrl) {
81
+ const cached = await browser.loadRefCache(targetId);
82
+ if (!cached)
83
+ return;
84
+ if (currentUrl && cached.url && currentUrl !== cached.url) {
85
+ output.printError(`Stale references — page has navigated (cache: ${cached.url} → current: ${currentUrl}). Re-run snapshot before using @eN refs.`);
86
+ return; // leave _refs empty — do not attempt to resolve refs against a different page
87
+ }
88
+ _refs = cached.refs;
89
+ if (cached.stale) {
90
+ output.printWarning(`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`);
91
+ }
92
+ }
36
93
  // URL patterns that signal an unambiguous login/auth wall.
37
94
  // Excludes 'auth', 'oauth', 'sso', 'saml' — their callback/ACS/token paths
38
95
  // (/auth/callback, /sso/callback, /saml/acs) are completion endpoints, not walls.
@@ -122,11 +179,14 @@ async function switchToHeaded(url, port) {
122
179
  // Capture post-auth cookies
123
180
  const authCookies = await browser.getCookies(_client, _sessionId).catch(() => []);
124
181
  const authLocalStorage = await browser.getLocalStorage(_client, _sessionId).catch(() => ({}));
125
- // Close headed session
182
+ // Close headed session — actually terminate the underlying Chrome process
183
+ // (Browser.close CDP command, PID-kill fallback), not just our CDP client
184
+ // connection, so the visible authenticated window doesn't linger forever.
126
185
  browser.teardownRouteInterception(_sessionId);
127
186
  browser.stopRequestCapture(_sessionId);
128
187
  browser.teardownDialogHandling(_sessionId);
129
188
  browser.teardownConsoleCapture(_sessionId);
189
+ await browser.closeBrowser(_client, headedPort);
130
190
  _client.close();
131
191
  _client = null;
132
192
  _sessionId = '';
@@ -151,6 +211,17 @@ async function switchToHeaded(url, port) {
151
211
  catch { /* ignore */ }
152
212
  }
153
213
  }
214
+ /**
215
+ * Sanitize an image-format flag. The host CLI (monomind) defines a GLOBAL
216
+ * --format flag for output shaping (text|json|table, default 'text') that
217
+ * leaks into subcommand flags — so ctx.flags.format arrives as 'text' even
218
+ * when the user never passed it, and Chrome rejects it with
219
+ * "CDP error -32602: Invalid image format". Only trust values that are real
220
+ * image formats; otherwise fall back to the subcommand's own default.
221
+ */
222
+ function imageFormat(value, allowed, fallback) {
223
+ return allowed.includes(value) ? value : fallback;
224
+ }
154
225
  function print(msg) {
155
226
  process.stdout.write(msg + '\n');
156
227
  }
@@ -205,11 +276,18 @@ const openCommand = {
205
276
  _refs = new Map();
206
277
  }
207
278
  _port = await browser.launchBrowser({ port, headless: !forceHeaded });
279
+ // Persist the active port so subsequent CLI invocations (each a fresh
280
+ // process) default to attaching here instead of hardcoded 9222.
281
+ await browser.saveActivePort(_port);
208
282
  const conn = await browser.connectToTarget(_port);
209
283
  _client = conn.client;
210
284
  _sessionId = conn.sessionId;
211
285
  _targetId = conn.target.id;
212
286
  _refs = new Map();
287
+ // A snapshot taken before this navigation is no longer valid for the new
288
+ // page — drop the persisted ref cache so a stale process's weak
289
+ // time-based check can't resurrect it before the next explicit snapshot.
290
+ await browser.clearRefCache();
213
291
  if (ctx.flags.state && ctx.flags.session) {
214
292
  output.printWarning('Both --state and --session provided; --state takes precedence');
215
293
  }
@@ -261,12 +339,10 @@ const snapshotCommand = {
261
339
  selector: ctx.flags.selector,
262
340
  });
263
341
  _refs = result.refs;
342
+ await browser.saveRefCache(_targetId, result.url, _refs);
264
343
  const applyOutputLimits = (text) => {
265
- let out = text;
266
344
  const maxOutput = ctx.flags['max-output'];
267
- if (maxOutput && out.length > maxOutput) {
268
- out = out.slice(0, maxOutput) + `\n[... truncated at ${maxOutput} chars]`;
269
- }
345
+ let out = maxOutput ? truncateForOutput(text, maxOutput) : text;
270
346
  if (ctx.flags['content-boundaries']) {
271
347
  const nonce = Math.random().toString(36).slice(2, 10);
272
348
  out = `MONOMIND_PAGE_CONTENT nonce=${nonce} origin=${result.url}\n${out}\nEND_MONOMIND_PAGE_CONTENT nonce=${nonce}`;
@@ -464,16 +540,31 @@ const waitCommand = {
464
540
  const rawTimeout = Math.min(ctx.flags.timeout ?? 30000, MAX_DOWNLOAD_TIMEOUT);
465
541
  const finalPath = await new Promise((resolve, reject) => {
466
542
  let guid = '';
543
+ let settled = false;
467
544
  // C2: capture off() functions to avoid listener leaks
468
545
  const offBegin = client.on('Browser.downloadWillBegin', (params) => { guid = params.guid; });
469
546
  let offProgress;
470
547
  // cleanup defined before setTimeout so the timeout callback can call it
471
548
  let tid;
472
- const cleanup = () => { clearTimeout(tid); offBegin?.(); offProgress?.(); };
473
- tid = setTimeout(() => { cleanup(); reject(new Error('Download timed out')); }, rawTimeout);
549
+ let pollTid;
550
+ const cleanup = () => { clearTimeout(tid); clearInterval(pollTid); offBegin?.(); offProgress?.(); };
551
+ const finish = (path) => {
552
+ if (settled)
553
+ return;
554
+ settled = true;
555
+ cleanup();
556
+ resolve(path);
557
+ };
558
+ const fail = (err) => {
559
+ if (settled)
560
+ return;
561
+ settled = true;
562
+ cleanup();
563
+ reject(err);
564
+ };
565
+ tid = setTimeout(() => fail(new Error('Download timed out')), rawTimeout);
474
566
  offProgress = client.on('Browser.downloadProgress', async (params) => {
475
567
  if (params.guid === guid && params.state === 'completed') {
476
- cleanup();
477
568
  const { readdir, rename, rmdir } = await import('fs/promises');
478
569
  const files = await readdir(downloadDir);
479
570
  if (files.length > 0) {
@@ -481,18 +572,45 @@ const waitCommand = {
481
572
  await mkdir(dirname(savePath), { recursive: true });
482
573
  await rename(src, savePath);
483
574
  await rmdir(downloadDir).catch(() => { }); // I1: cleanup temp dir
484
- resolve(savePath);
575
+ finish(savePath);
485
576
  }
486
577
  else {
487
578
  await rmdir(downloadDir).catch(() => { }); // I1: cleanup temp dir
488
- reject(new Error('Download completed but no file found'));
579
+ fail(new Error('Download completed but no file found'));
489
580
  }
490
581
  }
491
582
  else if (params.guid === guid && params.state === 'canceled') {
492
- cleanup();
493
- reject(new Error('Download was canceled'));
583
+ fail(new Error('Download was canceled'));
494
584
  }
495
585
  });
586
+ // Fallback for the empty-guid race: this process's listener attaches
587
+ // AFTER a separate `click` process may have already started (and even
588
+ // finished) the download — so downloadWillBegin/downloadProgress for it
589
+ // were never observed here. While guid is still empty, poll the expected
590
+ // output path directly: if it already exists with a stable, non-zero
591
+ // size across two consecutive checks, treat the download as complete.
592
+ let lastSize = -1;
593
+ let stableReads = 0;
594
+ pollTid = setInterval(async () => {
595
+ if (settled || guid)
596
+ return; // a real CDP event has taken over
597
+ try {
598
+ const { stat } = await import('fs/promises');
599
+ const st = await stat(savePath);
600
+ if (st.isFile() && st.size > 0 && st.size === lastSize) {
601
+ stableReads++;
602
+ if (stableReads >= 2)
603
+ finish(savePath);
604
+ }
605
+ else {
606
+ stableReads = 0;
607
+ }
608
+ lastSize = st.size;
609
+ }
610
+ catch {
611
+ // savePath not present yet — keep polling until timeout
612
+ }
613
+ }, 300);
496
614
  });
497
615
  output.printSuccess(`Download saved: ${finalPath}`);
498
616
  return { success: true, data: { path: finalPath } };
@@ -536,7 +654,7 @@ const screenshotCommand = {
536
654
  result = await browser.captureScreenshot(client, sessionId, {
537
655
  path: ctx.args[0],
538
656
  fullPage: ctx.flags.full,
539
- format: ctx.flags.format,
657
+ format: imageFormat(ctx.flags.format, ['png', 'jpeg', 'webp'], 'png'),
540
658
  quality: ctx.flags.quality,
541
659
  annotate,
542
660
  refs: annotate ? _refs : undefined,
@@ -780,6 +898,9 @@ const navigateCommand = {
780
898
  else {
781
899
  throw new Error(`Unknown direction: ${direction}. Use: back|forward|reload`);
782
900
  }
901
+ // Refs captured before this navigation may now point at different content.
902
+ _refs = new Map();
903
+ await browser.clearRefCache();
783
904
  output.printSuccess(`Navigated: ${direction}`);
784
905
  return { success: true };
785
906
  },
@@ -935,6 +1056,7 @@ const stateCommand = {
935
1056
  await browser.clearLocalStorage(c, sid);
936
1057
  await browser.clearSessionStorage(c, sid);
937
1058
  _refs = new Map();
1059
+ await browser.clearRefCache();
938
1060
  output.printSuccess('Browser state cleared (cookies, localStorage, sessionStorage, refs)');
939
1061
  return { success: true };
940
1062
  }
@@ -1111,12 +1233,25 @@ const networkCommand = {
1111
1233
  return { success: true };
1112
1234
  },
1113
1235
  };
1236
+ // Default output cap for eval when --max-output isn't passed explicitly —
1237
+ // print(String(result)) had NO limit at all (unlike snapshot's --max-output),
1238
+ // so e.g. `eval "document.documentElement.outerHTML"` could dump megabytes
1239
+ // straight into an agent's context. An explicit --max-output still overrides
1240
+ // this default.
1241
+ const DEFAULT_EVAL_MAX_OUTPUT = 50_000;
1242
+ function truncateForOutput(text, maxOutput) {
1243
+ if (!(maxOutput > 0) || text.length <= maxOutput)
1244
+ return text;
1245
+ return text.slice(0, maxOutput) + `\n[... truncated at ${maxOutput} chars]`;
1246
+ }
1114
1247
  const evalCommand = {
1115
1248
  name: 'eval',
1116
1249
  description: 'Evaluate JavaScript in page context. Usage: monomind browse eval "document.title"',
1117
1250
  options: [
1118
1251
  { name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
1119
1252
  { name: 'stdin', type: 'boolean', description: 'Read JS expression from stdin (heredoc-friendly for multiline scripts)', default: false },
1253
+ { name: 'max-output', type: 'number', description: `Truncate printed output to N characters (default ${DEFAULT_EVAL_MAX_OUTPUT}; 0 disables truncation)` },
1254
+ { name: 'timeout', type: 'number', description: 'Max ms to wait for evaluation to settle (default 30000)' },
1120
1255
  ],
1121
1256
  action: async (ctx) => {
1122
1257
  const { client, sessionId } = await ensureConnected(_port);
@@ -1130,12 +1265,14 @@ const evalCommand = {
1130
1265
  }
1131
1266
  if (!expr)
1132
1267
  throw new Error('Usage: monomind browse eval "<expression>" (or pipe with --stdin)');
1133
- const result = await browser.evaluateJs(client, sessionId, expr);
1268
+ const timeoutMs = ctx.flags.timeout;
1269
+ const result = await browser.evaluateJs(client, sessionId, expr, timeoutMs);
1270
+ const maxOutput = ctx.flags['max-output'] ?? DEFAULT_EVAL_MAX_OUTPUT;
1134
1271
  if (ctx.flags.json) {
1135
- print(JSON.stringify({ data: result }));
1272
+ print(truncateForOutput(JSON.stringify({ data: result }), maxOutput));
1136
1273
  }
1137
1274
  else {
1138
- print(String(result ?? ''));
1275
+ print(truncateForOutput(String(result ?? ''), maxOutput));
1139
1276
  }
1140
1277
  return { success: true, data: { result } };
1141
1278
  },
@@ -1176,10 +1313,66 @@ const closeCommand = {
1176
1313
  _sessionId = '';
1177
1314
  _targetId = '';
1178
1315
  _refs = new Map();
1316
+ // Session is gone — forget the persisted port and stale refs so later
1317
+ // invocations don't chase a dead endpoint (or the wrong elements).
1318
+ await browser.clearActivePort();
1319
+ await browser.clearRefCache();
1179
1320
  output.printSuccess('Browser session closed');
1180
1321
  }
1181
1322
  else {
1182
- output.printInfo('No active browser session');
1323
+ // Each CLI invocation is a fresh process, so `close` almost always
1324
+ // lands here. The persisted port file is the real session handle:
1325
+ // if monobrowse LAUNCHED that browser (open), gracefully Browser.close
1326
+ // it — otherwise every open→close cycle leaks a headless Chrome. If we
1327
+ // merely ATTACHED to it (connect, launched:false), never kill it: it's
1328
+ // the user's own browser. Either way, forget the port and refs.
1329
+ const browser = await getBrowser();
1330
+ const persisted = await browser.loadActivePortInfo();
1331
+ if (persisted?.launched) {
1332
+ try {
1333
+ const conn = await browser.connectToTarget(persisted.port);
1334
+ try {
1335
+ await browser.closeBrowser(conn.client, persisted.port);
1336
+ }
1337
+ finally {
1338
+ // Always drop our websocket — a hung Browser.close must not keep
1339
+ // this CLI process's event loop alive.
1340
+ try {
1341
+ conn.client.close();
1342
+ }
1343
+ catch { /* already gone */ }
1344
+ }
1345
+ // closeBrowser has no PID fallback in a fresh process (launchedPids
1346
+ // is per-process) — re-probe so we report what actually happened.
1347
+ // Poll briefly: Browser.close is acknowledged before the process
1348
+ // actually exits, so a single immediate probe false-alarms.
1349
+ let stillUp = true;
1350
+ const probeDeadline = Date.now() + 3000;
1351
+ while (stillUp && Date.now() < probeDeadline) {
1352
+ try {
1353
+ await fetch(`http://127.0.0.1:${persisted.port}/json/version`, { signal: AbortSignal.timeout(800) });
1354
+ await new Promise(r => setTimeout(r, 300));
1355
+ }
1356
+ catch {
1357
+ stillUp = false;
1358
+ }
1359
+ }
1360
+ if (stillUp)
1361
+ output.printWarning(`Browser on port ${persisted.port} did not exit — kill it manually if needed`);
1362
+ else
1363
+ output.printSuccess(`Closed browser on port ${persisted.port}`);
1364
+ }
1365
+ catch {
1366
+ output.printInfo(`No browser answering on port ${persisted.port} — nothing to close`);
1367
+ }
1368
+ }
1369
+ else {
1370
+ output.printInfo(persisted
1371
+ ? `Detached from browser on port ${persisted.port} (attached via connect — left running)`
1372
+ : 'No active browser session');
1373
+ }
1374
+ await browser.clearActivePort();
1375
+ await browser.clearRefCache();
1183
1376
  }
1184
1377
  return { success: true };
1185
1378
  },
@@ -2318,6 +2511,7 @@ const diffCommand = {
2318
2511
  await browser.waitFor(client, sessionId, { load: 'load', timeout: 15000 });
2319
2512
  const snap2 = await browser.captureSnapshot(client, sessionId, { interactiveOnly: ctx.flags.interactive });
2320
2513
  _refs = snap2.refs;
2514
+ await browser.saveRefCache(_targetId, snap2.url, _refs);
2321
2515
  // Text diff
2322
2516
  const lines1 = snap1.text.split('\n');
2323
2517
  const lines2 = snap2.text.split('\n');
@@ -2356,6 +2550,10 @@ const pushstateCommand = {
2356
2550
  if (!url)
2357
2551
  throw new Error('Usage: monomind browse pushstate <url>');
2358
2552
  await browser.pushState(client, sessionId, url);
2553
+ // SPA navigation changes what's on the page without a full page load —
2554
+ // refs captured before this pushState call may now resolve to different content.
2555
+ _refs = new Map();
2556
+ await browser.clearRefCache();
2359
2557
  output.printSuccess(`pushState: ${url}`);
2360
2558
  return { success: true };
2361
2559
  },
@@ -2390,6 +2588,52 @@ function tokenizeBatchCommand(input) {
2390
2588
  tokens.push(current);
2391
2589
  return tokens;
2392
2590
  }
2591
+ /**
2592
+ * Splits one line of a `batch` command string into a subcommand name, its
2593
+ * args, and any recognized leading flags.
2594
+ *
2595
+ * `eval` is special-cased: its sole argument is a raw JS expression, which
2596
+ * legitimately contains its own string-literal quotes (e.g.
2597
+ * `eval document.querySelector('a')`). tokenizeBatchCommand's shell-style
2598
+ * word-splitting treats `'...'`/`"..."` as grouping delimiters and discards
2599
+ * them — correct for a value like `fill @e1 "some text"`, but for `eval` it
2600
+ * silently deletes the expression's own quotes, turning a string literal
2601
+ * into a bare (undefined) identifier reference and producing a
2602
+ * ReferenceError instead of evaluating the intended expression. For `eval`,
2603
+ * only its own known --flags are consumed from the front; everything after
2604
+ * them is taken verbatim as one argument, untouched by tokenization.
2605
+ *
2606
+ * Exported for direct unit testing — not part of the CLI's public API.
2607
+ */
2608
+ export function parseBatchCommandLine(cmdStr) {
2609
+ const trimmed = cmdStr.trim();
2610
+ const evalMatch = trimmed.match(/^eval\b\s*/);
2611
+ if (!evalMatch) {
2612
+ const parts = tokenizeBatchCommand(trimmed);
2613
+ return { subName: parts[0], subArgs: parts.slice(1), flags: {} };
2614
+ }
2615
+ let rest = trimmed.slice(evalMatch[0].length);
2616
+ const flags = {};
2617
+ let consumedAnother = true;
2618
+ while (consumedAnother) {
2619
+ consumedAnother = false;
2620
+ const withValue = rest.match(/^--(max-output|timeout)\s+(-?\d+)\s*/);
2621
+ if (withValue) {
2622
+ flags[withValue[1]] = Number(withValue[2]);
2623
+ rest = rest.slice(withValue[0].length);
2624
+ consumedAnother = true;
2625
+ continue;
2626
+ }
2627
+ const boolFlag = rest.match(/^--(json|stdin)\b\s*/);
2628
+ if (boolFlag) {
2629
+ flags[boolFlag[1]] = true;
2630
+ rest = rest.slice(boolFlag[0].length);
2631
+ consumedAnother = true;
2632
+ continue;
2633
+ }
2634
+ }
2635
+ return { subName: 'eval', subArgs: [rest], flags };
2636
+ }
2393
2637
  const batchCommand = {
2394
2638
  name: 'batch',
2395
2639
  description: 'Execute multiple commands. Usage: monomind browse batch "open url" "snapshot -i" "click @e1"',
@@ -2403,9 +2647,7 @@ const batchCommand = {
2403
2647
  throw new Error('Usage: monomind browse batch "cmd1" "cmd2" ...');
2404
2648
  const results = [];
2405
2649
  for (const cmdStr of commands) {
2406
- const parts = tokenizeBatchCommand(cmdStr);
2407
- const subName = parts[0];
2408
- const subArgs = parts.slice(1);
2650
+ const { subName, subArgs, flags: preParsedFlags } = parseBatchCommandLine(cmdStr);
2409
2651
  const subCmd = browseCommand.subcommands?.find((s) => s.name === subName);
2410
2652
  if (!subCmd?.action) {
2411
2653
  const err = `Unknown command: ${subName}`;
@@ -2415,7 +2657,7 @@ const batchCommand = {
2415
2657
  continue;
2416
2658
  }
2417
2659
  try {
2418
- const parsedFlags = { _: [] };
2660
+ const parsedFlags = { _: [], ...preParsedFlags };
2419
2661
  const consumedIndices = new Set();
2420
2662
  // Parse --flags from subArgs, tracking which indices are flag names/values
2421
2663
  for (let i = 0; i < subArgs.length; i++) {
@@ -2525,7 +2767,7 @@ const removeinitscriptCommand = {
2525
2767
  };
2526
2768
  const connectCommand = {
2527
2769
  name: 'connect',
2528
- description: 'Connect to existing Chrome instance. Usage: monomind browse connect [--port 9222] [--target <id>] [--auto-connect]',
2770
+ 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]',
2529
2771
  options: [
2530
2772
  { name: 'port', short: 'p', type: 'number', description: 'CDP port', default: 9222 },
2531
2773
  { name: 'target', type: 'string', description: 'Target ID to attach to' },
@@ -2588,6 +2830,12 @@ const connectCommand = {
2588
2830
  _targetId = conn.target.id;
2589
2831
  _port = port;
2590
2832
  _refs = new Map();
2833
+ // Persist the port like `open` does — without this, the NEXT CLI process
2834
+ // (each command is a fresh process) resolves the hardcoded default and
2835
+ // tries to launch its own Chrome on 9222 instead of reusing this session.
2836
+ // launched:false marks this browser as someone else's — close must never
2837
+ // kill it, and a dead endpoint must not be silently relaunched.
2838
+ await browser.saveActivePort(port, { launched: false });
2591
2839
  const url = await browser.getCurrentUrl(_client, _sessionId);
2592
2840
  const title = await browser.getCurrentTitle(_client, _sessionId);
2593
2841
  output.printSuccess(`Connected: ${title} (${url})`);
@@ -2611,7 +2859,7 @@ const recordCommand = {
2611
2859
  switch (action) {
2612
2860
  case 'start':
2613
2861
  await browser.startRecording(client, sessionId, {
2614
- format: ctx.flags.format,
2862
+ format: imageFormat(ctx.flags.format, ['jpeg', 'png'], 'jpeg'),
2615
2863
  quality: ctx.flags.quality,
2616
2864
  });
2617
2865
  output.printSuccess('Recording started');
@@ -2627,12 +2875,12 @@ const recordCommand = {
2627
2875
  case 'restart': {
2628
2876
  const prevStatus = browser.getRecordingStatus(sessionId);
2629
2877
  let prevPath;
2630
- if (prevStatus.recording) {
2878
+ if (prevStatus.recording || prevStatus.autoStopped) {
2631
2879
  prevPath = await browser.stopRecording(client, sessionId, ctx.args[1]);
2632
2880
  output.printInfo(`Previous recording saved: ${prevPath}`);
2633
2881
  }
2634
2882
  await browser.startRecording(client, sessionId, {
2635
- format: ctx.flags.format,
2883
+ format: imageFormat(ctx.flags.format, ['jpeg', 'png'], 'jpeg'),
2636
2884
  quality: ctx.flags.quality,
2637
2885
  });
2638
2886
  output.printSuccess('Recording restarted');
@@ -2643,7 +2891,7 @@ const recordCommand = {
2643
2891
  if (ctx.flags.json)
2644
2892
  print(JSON.stringify({ data: status }));
2645
2893
  else
2646
- print(`Recording: ${status.recording} | Frames: ${status.frames}`);
2894
+ print(`Recording: ${status.recording} | Frames: ${status.frames}${status.autoStopped ? ' (auto-stopped: buffer limit reached — run "record stop" to save)' : ''}`);
2647
2895
  return { success: true, data: status };
2648
2896
  }
2649
2897
  default: