@monoes/monobrowse 1.0.5 → 1.0.7

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 (250) hide show
  1. package/README.md +68 -0
  2. package/dist/src/__tests__/adapters.test.d.ts +2 -0
  3. package/dist/src/__tests__/adapters.test.d.ts.map +1 -0
  4. package/dist/src/__tests__/adapters.test.js +104 -0
  5. package/dist/src/__tests__/adapters.test.js.map +1 -0
  6. package/dist/src/__tests__/batch-eval-parsing.test.d.ts +0 -0
  7. package/dist/src/__tests__/batch-eval-parsing.test.d.ts.map +0 -0
  8. package/dist/src/__tests__/batch-eval-parsing.test.js +0 -0
  9. package/dist/src/__tests__/batch-eval-parsing.test.js.map +0 -0
  10. package/dist/src/__tests__/box-model.test.d.ts +2 -0
  11. package/dist/src/__tests__/box-model.test.d.ts.map +1 -0
  12. package/dist/src/__tests__/box-model.test.js +36 -0
  13. package/dist/src/__tests__/box-model.test.js.map +1 -0
  14. package/dist/src/__tests__/browser-launch.test.d.ts +0 -0
  15. package/dist/src/__tests__/browser-launch.test.d.ts.map +0 -0
  16. package/dist/src/__tests__/browser-launch.test.js +0 -0
  17. package/dist/src/__tests__/browser-launch.test.js.map +0 -0
  18. package/dist/src/__tests__/cdp-client.test.d.ts +2 -0
  19. package/dist/src/__tests__/cdp-client.test.d.ts.map +1 -0
  20. package/dist/src/__tests__/cdp-client.test.js +326 -0
  21. package/dist/src/__tests__/cdp-client.test.js.map +1 -0
  22. package/dist/src/__tests__/close-browser.test.d.ts +2 -0
  23. package/dist/src/__tests__/close-browser.test.d.ts.map +1 -0
  24. package/dist/src/__tests__/close-browser.test.js +128 -0
  25. package/dist/src/__tests__/close-browser.test.js.map +1 -0
  26. package/dist/src/__tests__/console-log.test.d.ts +2 -0
  27. package/dist/src/__tests__/console-log.test.d.ts.map +1 -0
  28. package/dist/src/__tests__/console-log.test.js +195 -0
  29. package/dist/src/__tests__/console-log.test.js.map +1 -0
  30. package/dist/src/__tests__/find.test.d.ts +2 -0
  31. package/dist/src/__tests__/find.test.d.ts.map +1 -0
  32. package/dist/src/__tests__/find.test.js +299 -0
  33. package/dist/src/__tests__/find.test.js.map +1 -0
  34. package/dist/src/__tests__/frame-command.test.d.ts +2 -0
  35. package/dist/src/__tests__/frame-command.test.d.ts.map +1 -0
  36. package/dist/src/__tests__/frame-command.test.js +85 -0
  37. package/dist/src/__tests__/frame-command.test.js.map +1 -0
  38. package/dist/src/__tests__/output.test.d.ts +0 -0
  39. package/dist/src/__tests__/output.test.d.ts.map +0 -0
  40. package/dist/src/__tests__/output.test.js +0 -0
  41. package/dist/src/__tests__/output.test.js.map +0 -0
  42. package/dist/src/__tests__/ref-cache.test.d.ts +0 -0
  43. package/dist/src/__tests__/ref-cache.test.d.ts.map +0 -0
  44. package/dist/src/__tests__/ref-cache.test.js +43 -2
  45. package/dist/src/__tests__/ref-cache.test.js.map +1 -1
  46. package/dist/src/__tests__/session-state.test.d.ts +2 -0
  47. package/dist/src/__tests__/session-state.test.d.ts.map +1 -0
  48. package/dist/src/__tests__/session-state.test.js +203 -0
  49. package/dist/src/__tests__/session-state.test.js.map +1 -0
  50. package/dist/src/__tests__/snapshot.test.d.ts +2 -0
  51. package/dist/src/__tests__/snapshot.test.d.ts.map +1 -0
  52. package/dist/src/__tests__/snapshot.test.js +262 -0
  53. package/dist/src/__tests__/snapshot.test.js.map +1 -0
  54. package/dist/src/__tests__/storage-eval.test.d.ts +2 -0
  55. package/dist/src/__tests__/storage-eval.test.d.ts.map +1 -0
  56. package/dist/src/__tests__/storage-eval.test.js +133 -0
  57. package/dist/src/__tests__/storage-eval.test.js.map +1 -0
  58. package/dist/src/__tests__/tabs.test.d.ts +2 -0
  59. package/dist/src/__tests__/tabs.test.d.ts.map +1 -0
  60. package/dist/src/__tests__/tabs.test.js +161 -0
  61. package/dist/src/__tests__/tabs.test.js.map +1 -0
  62. package/dist/src/browser/action-builder/analyzer.d.ts +0 -0
  63. package/dist/src/browser/action-builder/analyzer.d.ts.map +0 -0
  64. package/dist/src/browser/action-builder/analyzer.js +0 -0
  65. package/dist/src/browser/action-builder/analyzer.js.map +0 -0
  66. package/dist/src/browser/action-builder/types.d.ts +0 -0
  67. package/dist/src/browser/action-builder/types.d.ts.map +0 -0
  68. package/dist/src/browser/action-builder/types.js +0 -0
  69. package/dist/src/browser/action-builder/types.js.map +0 -0
  70. package/dist/src/browser/actions.d.ts +0 -0
  71. package/dist/src/browser/actions.d.ts.map +0 -0
  72. package/dist/src/browser/actions.js +0 -0
  73. package/dist/src/browser/actions.js.map +0 -0
  74. package/dist/src/browser/adapters/gemini.d.ts +0 -0
  75. package/dist/src/browser/adapters/gemini.d.ts.map +0 -0
  76. package/dist/src/browser/adapters/gemini.js +0 -0
  77. package/dist/src/browser/adapters/gemini.js.map +0 -0
  78. package/dist/src/browser/adapters/google.d.ts +0 -0
  79. package/dist/src/browser/adapters/google.d.ts.map +0 -0
  80. package/dist/src/browser/adapters/google.js +0 -0
  81. package/dist/src/browser/adapters/google.js.map +0 -0
  82. package/dist/src/browser/adapters/index.d.ts +0 -0
  83. package/dist/src/browser/adapters/index.d.ts.map +0 -0
  84. package/dist/src/browser/adapters/index.js +0 -0
  85. package/dist/src/browser/adapters/index.js.map +0 -0
  86. package/dist/src/browser/adapters/instagram.d.ts +0 -0
  87. package/dist/src/browser/adapters/instagram.d.ts.map +0 -0
  88. package/dist/src/browser/adapters/instagram.js +0 -0
  89. package/dist/src/browser/adapters/instagram.js.map +0 -0
  90. package/dist/src/browser/adapters/linkedin.d.ts +0 -0
  91. package/dist/src/browser/adapters/linkedin.d.ts.map +0 -0
  92. package/dist/src/browser/adapters/linkedin.js +0 -0
  93. package/dist/src/browser/adapters/linkedin.js.map +0 -0
  94. package/dist/src/browser/adapters/microsoft.d.ts +0 -0
  95. package/dist/src/browser/adapters/microsoft.d.ts.map +0 -0
  96. package/dist/src/browser/adapters/microsoft.js +0 -0
  97. package/dist/src/browser/adapters/microsoft.js.map +0 -0
  98. package/dist/src/browser/adapters/x.d.ts +0 -0
  99. package/dist/src/browser/adapters/x.d.ts.map +0 -0
  100. package/dist/src/browser/adapters/x.js +0 -0
  101. package/dist/src/browser/adapters/x.js.map +0 -0
  102. package/dist/src/browser/batch.d.ts +0 -0
  103. package/dist/src/browser/batch.d.ts.map +0 -0
  104. package/dist/src/browser/batch.js +0 -0
  105. package/dist/src/browser/batch.js.map +0 -0
  106. package/dist/src/browser/browser.d.ts +4 -0
  107. package/dist/src/browser/browser.d.ts.map +1 -1
  108. package/dist/src/browser/browser.js +55 -7
  109. package/dist/src/browser/browser.js.map +1 -1
  110. package/dist/src/browser/cdp.d.ts +4 -1
  111. package/dist/src/browser/cdp.d.ts.map +1 -1
  112. package/dist/src/browser/cdp.js +45 -16
  113. package/dist/src/browser/cdp.js.map +1 -1
  114. package/dist/src/browser/console-log.d.ts +0 -0
  115. package/dist/src/browser/console-log.d.ts.map +0 -0
  116. package/dist/src/browser/console-log.js +0 -0
  117. package/dist/src/browser/console-log.js.map +0 -0
  118. package/dist/src/browser/dashboard/api-types.d.ts +0 -0
  119. package/dist/src/browser/dashboard/api-types.d.ts.map +0 -0
  120. package/dist/src/browser/dashboard/api-types.js +0 -0
  121. package/dist/src/browser/dashboard/api-types.js.map +0 -0
  122. package/dist/src/browser/dashboard/server.d.ts +0 -0
  123. package/dist/src/browser/dashboard/server.d.ts.map +0 -0
  124. package/dist/src/browser/dashboard/server.js +0 -0
  125. package/dist/src/browser/dashboard/server.js.map +0 -0
  126. package/dist/src/browser/dialog.d.ts +0 -0
  127. package/dist/src/browser/dialog.d.ts.map +0 -0
  128. package/dist/src/browser/dialog.js +0 -0
  129. package/dist/src/browser/dialog.js.map +0 -0
  130. package/dist/src/browser/emulation.d.ts +0 -0
  131. package/dist/src/browser/emulation.d.ts.map +0 -0
  132. package/dist/src/browser/emulation.js +0 -0
  133. package/dist/src/browser/emulation.js.map +0 -0
  134. package/dist/src/browser/find.d.ts +0 -0
  135. package/dist/src/browser/find.d.ts.map +0 -0
  136. package/dist/src/browser/find.js +0 -0
  137. package/dist/src/browser/find.js.map +0 -0
  138. package/dist/src/browser/har.d.ts +0 -0
  139. package/dist/src/browser/har.d.ts.map +0 -0
  140. package/dist/src/browser/har.js +0 -0
  141. package/dist/src/browser/har.js.map +0 -0
  142. package/dist/src/browser/index.d.ts +0 -0
  143. package/dist/src/browser/index.d.ts.map +0 -0
  144. package/dist/src/browser/index.js +0 -0
  145. package/dist/src/browser/index.js.map +0 -0
  146. package/dist/src/browser/network.d.ts +0 -0
  147. package/dist/src/browser/network.d.ts.map +0 -0
  148. package/dist/src/browser/network.js +0 -0
  149. package/dist/src/browser/network.js.map +0 -0
  150. package/dist/src/browser/pdf.d.ts +0 -0
  151. package/dist/src/browser/pdf.d.ts.map +0 -0
  152. package/dist/src/browser/pdf.js +0 -0
  153. package/dist/src/browser/pdf.js.map +0 -0
  154. package/dist/src/browser/profiler.d.ts +0 -0
  155. package/dist/src/browser/profiler.d.ts.map +0 -0
  156. package/dist/src/browser/profiler.js +0 -0
  157. package/dist/src/browser/profiler.js.map +0 -0
  158. package/dist/src/browser/record.d.ts +0 -0
  159. package/dist/src/browser/record.d.ts.map +0 -0
  160. package/dist/src/browser/record.js +0 -0
  161. package/dist/src/browser/record.js.map +0 -0
  162. package/dist/src/browser/ref-cache.d.ts +6 -1
  163. package/dist/src/browser/ref-cache.d.ts.map +1 -1
  164. package/dist/src/browser/ref-cache.js +21 -3
  165. package/dist/src/browser/ref-cache.js.map +1 -1
  166. package/dist/src/browser/screenshot.d.ts +0 -0
  167. package/dist/src/browser/screenshot.d.ts.map +0 -0
  168. package/dist/src/browser/screenshot.js +0 -0
  169. package/dist/src/browser/screenshot.js.map +0 -0
  170. package/dist/src/browser/session.d.ts +0 -0
  171. package/dist/src/browser/session.d.ts.map +0 -0
  172. package/dist/src/browser/session.js +0 -0
  173. package/dist/src/browser/session.js.map +0 -0
  174. package/dist/src/browser/snapshot.d.ts +11 -0
  175. package/dist/src/browser/snapshot.d.ts.map +1 -1
  176. package/dist/src/browser/snapshot.js +11 -0
  177. package/dist/src/browser/snapshot.js.map +1 -1
  178. package/dist/src/browser/storage.d.ts +0 -0
  179. package/dist/src/browser/storage.d.ts.map +0 -0
  180. package/dist/src/browser/storage.js +0 -0
  181. package/dist/src/browser/storage.js.map +0 -0
  182. package/dist/src/browser/tabs.d.ts +5 -1
  183. package/dist/src/browser/tabs.d.ts.map +1 -1
  184. package/dist/src/browser/tabs.js +4 -3
  185. package/dist/src/browser/tabs.js.map +1 -1
  186. package/dist/src/browser/trace.d.ts +0 -0
  187. package/dist/src/browser/trace.d.ts.map +0 -0
  188. package/dist/src/browser/trace.js +0 -0
  189. package/dist/src/browser/trace.js.map +0 -0
  190. package/dist/src/browser/types.d.ts +4 -0
  191. package/dist/src/browser/types.d.ts.map +1 -1
  192. package/dist/src/browser/types.js +0 -0
  193. package/dist/src/browser/types.js.map +1 -1
  194. package/dist/src/browser/vitals.d.ts +0 -0
  195. package/dist/src/browser/vitals.d.ts.map +0 -0
  196. package/dist/src/browser/vitals.js +0 -0
  197. package/dist/src/browser/vitals.js.map +0 -0
  198. package/dist/src/browser/wait.d.ts +0 -0
  199. package/dist/src/browser/wait.d.ts.map +0 -0
  200. package/dist/src/browser/wait.js +0 -0
  201. package/dist/src/browser/wait.js.map +0 -0
  202. package/dist/src/cli/action.d.ts +0 -0
  203. package/dist/src/cli/action.d.ts.map +0 -0
  204. package/dist/src/cli/action.js +0 -0
  205. package/dist/src/cli/action.js.map +0 -0
  206. package/dist/src/cli/commands.d.ts +21 -0
  207. package/dist/src/cli/commands.d.ts.map +1 -1
  208. package/dist/src/cli/commands.js +123 -7
  209. package/dist/src/cli/commands.js.map +1 -1
  210. package/dist/src/cli/output.d.ts +0 -0
  211. package/dist/src/cli/output.d.ts.map +0 -0
  212. package/dist/src/cli/output.js +0 -0
  213. package/dist/src/cli/output.js.map +0 -0
  214. package/dist/src/cli/platform.d.ts +0 -0
  215. package/dist/src/cli/platform.d.ts.map +0 -0
  216. package/dist/src/cli/platform.js +0 -0
  217. package/dist/src/cli/platform.js.map +0 -0
  218. package/dist/src/cli/types.d.ts +0 -0
  219. package/dist/src/cli/types.d.ts.map +0 -0
  220. package/dist/src/cli/types.js +0 -0
  221. package/dist/src/cli/types.js.map +0 -0
  222. package/dist/src/cli.d.ts +0 -0
  223. package/dist/src/cli.d.ts.map +0 -0
  224. package/dist/src/cli.js +0 -0
  225. package/dist/src/cli.js.map +0 -0
  226. package/dist/src/index.d.ts +0 -0
  227. package/dist/src/index.d.ts.map +0 -0
  228. package/dist/src/index.js +0 -0
  229. package/dist/src/index.js.map +0 -0
  230. package/dist/tsconfig.tsbuildinfo +1 -1
  231. package/package.json +9 -5
  232. package/src/__tests__/adapters.test.ts +125 -0
  233. package/src/__tests__/box-model.test.ts +39 -0
  234. package/src/__tests__/cdp-client.test.ts +375 -0
  235. package/src/__tests__/close-browser.test.ts +141 -0
  236. package/src/__tests__/console-log.test.ts +250 -0
  237. package/src/__tests__/find.test.ts +354 -0
  238. package/src/__tests__/frame-command.test.ts +98 -0
  239. package/src/__tests__/ref-cache.test.ts +47 -2
  240. package/src/__tests__/session-state.test.ts +251 -0
  241. package/src/__tests__/snapshot.test.ts +331 -0
  242. package/src/__tests__/storage-eval.test.ts +171 -0
  243. package/src/__tests__/tabs.test.ts +183 -0
  244. package/src/browser/browser.ts +59 -7
  245. package/src/browser/cdp.ts +52 -15
  246. package/src/browser/ref-cache.ts +29 -6
  247. package/src/browser/snapshot.ts +11 -0
  248. package/src/browser/tabs.ts +12 -4
  249. package/src/browser/types.ts +4 -0
  250. package/src/cli/commands.ts +119 -7
@@ -24,11 +24,16 @@ export async function activateTab(client: CdpClient, oldSessionId: string, targe
24
24
  return result.sessionId;
25
25
  }
26
26
 
27
+ export interface FrameSwitchResult {
28
+ url: string | null;
29
+ sessionId: string | null;
30
+ }
31
+
27
32
  export async function switchToFrame(
28
33
  client: CdpClient,
29
34
  sessionId: string,
30
35
  frameSelector: string
31
- ): Promise<string | null> {
36
+ ): Promise<FrameSwitchResult> {
32
37
  const { result } = await client.send<{ result: { result: { value: string | null } } }>(
33
38
  'Runtime.evaluate',
34
39
  {
@@ -50,7 +55,7 @@ export async function switchToFrame(
50
55
  const match = frameTree.childFrames?.find(
51
56
  (cf) => cf.frame.url === frameSrc || cf.frame.id === frameSelector
52
57
  );
53
- if (!match) return frameSrc;
58
+ if (!match) return { url: frameSrc, sessionId: null };
54
59
 
55
60
  const targets = await client.send<{ targetInfos: Array<{ targetId: string; type: string; url: string }> }>(
56
61
  'Target.getTargets', {}
@@ -59,8 +64,11 @@ export async function switchToFrame(
59
64
  (t) => t.type === 'iframe' && t.url === match.frame.url
60
65
  );
61
66
  if (oopif) {
62
- await client.send('Target.attachToTarget', { targetId: oopif.targetId, flatten: true });
67
+ const attached = await client.send<{ sessionId: string }>(
68
+ 'Target.attachToTarget', { targetId: oopif.targetId, flatten: true }
69
+ );
70
+ return { url: frameSrc, sessionId: attached.sessionId };
63
71
  }
64
72
 
65
- return frameSrc;
73
+ return { url: frameSrc, sessionId: null };
66
74
  }
@@ -9,6 +9,10 @@ export interface BrowserConfig {
9
9
  executablePath?: string;
10
10
  userDataDir?: string;
11
11
  args?: string[];
12
+ /** Milliseconds to wait for the launched Chrome to open its CDP port
13
+ * before giving up. Default 10000 — raise this for environments where
14
+ * process spawn is consistently slower (e.g. constrained CI runners). */
15
+ launchTimeoutMs?: number;
12
16
  }
13
17
 
14
18
  export interface SessionState {
@@ -16,11 +16,52 @@ let _sessionId = '';
16
16
  let _targetId = '';
17
17
  let _port = DEFAULT_PORT;
18
18
  let _refs: Map<string, ElementRef> = new Map();
19
+ // Saved parent sessionId when inside an iframe — restored by `frame main`.
20
+ let _parentSessionId = '';
19
21
 
20
22
  async function getBrowser() {
21
23
  return import('../index.js');
22
24
  }
23
25
 
26
+ // Best-effort cleanup on Ctrl-C / kill so a launched Chrome doesn't linger as
27
+ // an orphan process when a command is interrupted mid-flight (e.g. during a
28
+ // long-running wait/eval that CdpClient.send()'s own timeout hasn't tripped
29
+ // yet).
30
+ //
31
+ // This module is imported for EVERY `monomind` CLI invocation (commands/
32
+ // index.ts statically imports browse.ts, which imports this file) — not just
33
+ // browse commands — so the handler is only *registered* once a browser
34
+ // session is actually launched here (see ensureSignalCleanupHandlers(),
35
+ // called from the `open` action below), not at module load. Registering
36
+ // unconditionally at import time would call process.exit() on Ctrl-C for
37
+ // every unrelated monomind command (e.g. a long-running `org run` daemon),
38
+ // pre-empting whatever other SIGINT/SIGTERM handling that process needs.
39
+ const SIGNAL_EXIT_CODES: Record<'SIGINT' | 'SIGTERM', number> = { SIGINT: 130, SIGTERM: 143 };
40
+ let _signalHandlersRegistered = false;
41
+ function ensureSignalCleanupHandlers(): void {
42
+ if (_signalHandlersRegistered) return;
43
+ _signalHandlersRegistered = true;
44
+ for (const sig of ['SIGINT', 'SIGTERM'] as const) {
45
+ process.on(sig, () => {
46
+ void (async () => {
47
+ try {
48
+ const browser = await getBrowser();
49
+ if (_client) {
50
+ await browser.closeBrowser(_client, _port);
51
+ } else {
52
+ const pid = browser.getLaunchedPid(_port);
53
+ if (pid !== undefined) {
54
+ try { process.kill(pid, 'SIGKILL'); } catch { /* already exited */ }
55
+ }
56
+ }
57
+ } catch {
58
+ // best-effort — never block process exit on cleanup failure
59
+ }
60
+ })().finally(() => process.exit(SIGNAL_EXIT_CODES[sig]));
61
+ });
62
+ }
63
+ }
64
+
24
65
  // Each CLI invocation is a fresh process, so this module's `_port` variable
25
66
  // is always freshly initialized to DEFAULT_PORT — a `--port 9333` passed to
26
67
  // an earlier `open` command has no effect on this process's own default.
@@ -62,6 +103,7 @@ async function ensureConnected(port: number, targetId?: string) {
62
103
  _client = conn.client;
63
104
  _sessionId = conn.sessionId;
64
105
  _targetId = conn.target.id;
106
+ _parentSessionId = '';
65
107
  _refs = new Map();
66
108
  await hydrateRefsFromCache(browser, _targetId, conn.target.url);
67
109
  }
@@ -157,6 +199,7 @@ async function switchToHeaded(url: string, port: number): Promise<void> {
157
199
  _client.close();
158
200
  _client = null;
159
201
  _sessionId = '';
202
+ _parentSessionId = '';
160
203
  _targetId = '';
161
204
  }
162
205
 
@@ -203,6 +246,7 @@ async function switchToHeaded(url: string, port: number): Promise<void> {
203
246
  _client.close();
204
247
  _client = null;
205
248
  _sessionId = '';
249
+ _parentSessionId = '';
206
250
  _targetId = '';
207
251
 
208
252
  // Relaunch headless and restore session
@@ -277,14 +321,40 @@ const openCommand: Command = {
277
321
  prevClient.close();
278
322
  _client = null;
279
323
  _sessionId = '';
324
+ _parentSessionId = '';
280
325
  _targetId = '';
281
326
  _refs = new Map();
282
327
  }
283
328
 
284
329
  _port = await browser.launchBrowser({ port, headless: !forceHeaded });
285
330
  // Persist the active port so subsequent CLI invocations (each a fresh
286
- // process) default to attaching here instead of hardcoded 9222.
287
- await browser.saveActivePort(_port);
331
+ // process) default to attaching here instead of hardcoded 9222. Also
332
+ // persist the launched PID/userDataDir so a later process's closeBrowser
333
+ // can still kill this Chrome even though launchedPids (browser.ts) is
334
+ // per-process and empty there.
335
+ //
336
+ // launchBrowser() can either LAUNCH a fresh Chrome or ATTACH to one
337
+ // already listening on the requested port (see its own "attach if
338
+ // already Chrome" comment) — it returns only a port number, with no
339
+ // signal telling this caller which happened. getLaunchedPid(_port) is
340
+ // undefined on the attach path (this process never spawned anything).
341
+ // Bug fixed here: unconditionally saving {pid: undefined, ...} on
342
+ // attach used to CLOBBER a real PID a previous `open` had already
343
+ // persisted for this exact port, destroying the only way a later
344
+ // process's closeBrowser() PID-kill fallback could ever find it.
345
+ const freshPid = browser.getLaunchedPid(_port);
346
+ const freshUserDataDir = browser.getLaunchedUserDataDir(_port);
347
+ if (freshPid !== undefined) {
348
+ await browser.saveActivePort(_port, { pid: freshPid, userDataDir: freshUserDataDir });
349
+ } else {
350
+ // Attach path: preserve whatever PID/userDataDir was already on file
351
+ // for this port (most likely from the process that originally
352
+ // launched it) instead of overwriting with undefined.
353
+ const existing = await browser.loadActivePortInfo();
354
+ const preserved = existing && existing.port === _port ? existing : undefined;
355
+ await browser.saveActivePort(_port, { pid: preserved?.pid, userDataDir: preserved?.userDataDir });
356
+ }
357
+ ensureSignalCleanupHandlers();
288
358
  const conn = await browser.connectToTarget(_port);
289
359
  _client = conn.client;
290
360
  _sessionId = conn.sessionId;
@@ -777,7 +847,8 @@ const getCommand: Command = {
777
847
  const refKey = refArg.startsWith('@') ? refArg.slice(1) : refArg;
778
848
  const ref = _refs.get(refKey);
779
849
  if (!ref) throw new Error(`Ref @${refKey} not found`);
780
- value = await browser.getElementBox(client, sessionId, ref);
850
+ const center = await browser.getElementBox(client, sessionId, ref);
851
+ value = deriveBoxOutput(center);
781
852
  break;
782
853
  }
783
854
  case 'styles': {
@@ -1274,6 +1345,7 @@ const closeCommand: Command = {
1274
1345
  client.close();
1275
1346
  _client = null;
1276
1347
  _sessionId = '';
1348
+ _parentSessionId = '';
1277
1349
  _targetId = '';
1278
1350
  _refs = new Map();
1279
1351
  // Session is gone — forget the persisted port and stale refs so later
@@ -1591,8 +1663,11 @@ const tapCommand: Command = {
1591
1663
  const ref = await browser.resolveRef(client, sessionId, _refs, key);
1592
1664
  const box = await browser.getElementBox(client, sessionId, ref);
1593
1665
  if (!box) throw new Error(`Cannot get bounds for @${key}`);
1594
- x = Math.round(box.x + box.width / 2);
1595
- y = Math.round(box.y + box.height / 2);
1666
+ // box.x/box.y from getElementBox() ARE already the center point (see
1667
+ // its own doc comment) — adding width/2 here double-offset the tap
1668
+ // target away from the element (issue #15).
1669
+ x = Math.round(box.x);
1670
+ y = Math.round(box.y);
1596
1671
  } else {
1597
1672
  const posJson = await browser.evaluateJs(client, sessionId,
1598
1673
  `(function(){var el=document.querySelector(${JSON.stringify(arg)});if(!el)return null;var r=el.getBoundingClientRect();return JSON.stringify({x:r.left+r.width/2,y:r.top+r.height/2});})()`) as string | null;
@@ -1915,10 +1990,23 @@ const frameCommand: Command = {
1915
1990
  if (!target) throw new Error('Usage: monomind browse frame <selector>|main');
1916
1991
 
1917
1992
  if (target === 'main') {
1993
+ if (_parentSessionId) {
1994
+ // Detach from the OOPIF session (best-effort — it may already be gone)
1995
+ await client.send('Target.detachFromTarget', { sessionId: _sessionId }).catch(() => {});
1996
+ _sessionId = _parentSessionId;
1997
+ _parentSessionId = '';
1998
+ }
1918
1999
  output.printSuccess('Switched to main frame');
1919
2000
  } else {
1920
- const frameSrc = await browser.switchToFrame(client, sessionId, target);
1921
- output.printSuccess(`Switched to frame: ${frameSrc ?? target}`);
2001
+ const frameResult = await browser.switchToFrame(client, sessionId, target);
2002
+ if (frameResult.sessionId) {
2003
+ // Save the parent session so `frame main` can restore it
2004
+ _parentSessionId = _sessionId;
2005
+ _sessionId = frameResult.sessionId;
2006
+ // Enable CDP domains on the iframe's session so subsequent commands work
2007
+ await browser.enableSessionDomains(client, _sessionId);
2008
+ }
2009
+ output.printSuccess(`Switched to frame: ${frameResult.url ?? target}`);
1922
2010
  }
1923
2011
  return { success: true };
1924
2012
  },
@@ -1970,6 +2058,7 @@ const tabCommand: Command = {
1970
2058
  client.close();
1971
2059
  _client = null;
1972
2060
  _sessionId = '';
2061
+ _parentSessionId = '';
1973
2062
  _targetId = '';
1974
2063
  _refs = new Map();
1975
2064
  } else {
@@ -2497,8 +2586,30 @@ function tokenizeBatchCommand(input: string): string[] {
2497
2586
  * only its own known --flags are consumed from the front; everything after
2498
2587
  * them is taken verbatim as one argument, untouched by tokenization.
2499
2588
  *
2589
+ /**
2590
+ * `get box`'s output shape: browser.getElementBox() returns the element's
2591
+ * CENTER point under x/y (correct for internal click-target callers), but
2592
+ * `get box` is a bounding-box accessor where x/y conventionally means the
2593
+ * top-left origin — a caller computing its own center as `box.x + width/2`
2594
+ * would otherwise double-offset away from the element (issue #15). Expose
2595
+ * both conventions, explicitly labeled, so neither is ambiguous.
2596
+ *
2500
2597
  * Exported for direct unit testing — not part of the CLI's public API.
2501
2598
  */
2599
+ export function deriveBoxOutput(
2600
+ center: { x: number; y: number; width: number; height: number } | null
2601
+ ): { x: number; y: number; width: number; height: number; centerX: number; centerY: number } | null {
2602
+ if (!center) return null;
2603
+ return {
2604
+ x: center.x - center.width / 2,
2605
+ y: center.y - center.height / 2,
2606
+ width: center.width,
2607
+ height: center.height,
2608
+ centerX: center.x,
2609
+ centerY: center.y,
2610
+ };
2611
+ }
2612
+
2502
2613
  export function parseBatchCommandLine(cmdStr: string): { subName: string; subArgs: string[]; flags: Record<string, unknown> } {
2503
2614
  const trimmed = cmdStr.trim();
2504
2615
  const evalMatch = trimmed.match(/^eval\b\s*/);
@@ -2700,6 +2811,7 @@ const connectCommand: Command = {
2700
2811
  prevClient.close();
2701
2812
  _client = null;
2702
2813
  _sessionId = '';
2814
+ _parentSessionId = '';
2703
2815
  _targetId = '';
2704
2816
  _refs = new Map();
2705
2817
  }