@monoes/monobrowse 1.0.6 → 1.0.8

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 (290) hide show
  1. package/LICENSE +191 -0
  2. package/README.md +68 -0
  3. package/dist/src/__tests__/adapters.test.d.ts +2 -0
  4. package/dist/src/__tests__/adapters.test.d.ts.map +1 -0
  5. package/dist/src/__tests__/adapters.test.js +104 -0
  6. package/dist/src/__tests__/adapters.test.js.map +1 -0
  7. package/dist/src/__tests__/batch-eval-parsing.test.d.ts +0 -0
  8. package/dist/src/__tests__/batch-eval-parsing.test.d.ts.map +0 -0
  9. package/dist/src/__tests__/batch-eval-parsing.test.js +0 -0
  10. package/dist/src/__tests__/batch-eval-parsing.test.js.map +0 -0
  11. package/dist/src/__tests__/box-model.test.d.ts +0 -0
  12. package/dist/src/__tests__/box-model.test.d.ts.map +0 -0
  13. package/dist/src/__tests__/box-model.test.js +0 -0
  14. package/dist/src/__tests__/box-model.test.js.map +0 -0
  15. package/dist/src/__tests__/browser-launch.test.d.ts +0 -0
  16. package/dist/src/__tests__/browser-launch.test.d.ts.map +0 -0
  17. package/dist/src/__tests__/browser-launch.test.js +0 -0
  18. package/dist/src/__tests__/browser-launch.test.js.map +0 -0
  19. package/dist/src/__tests__/cdp-client.test.d.ts +2 -0
  20. package/dist/src/__tests__/cdp-client.test.d.ts.map +1 -0
  21. package/dist/src/__tests__/cdp-client.test.js +326 -0
  22. package/dist/src/__tests__/cdp-client.test.js.map +1 -0
  23. package/dist/src/__tests__/close-browser.test.d.ts +2 -0
  24. package/dist/src/__tests__/close-browser.test.d.ts.map +1 -0
  25. package/dist/src/__tests__/close-browser.test.js +188 -0
  26. package/dist/src/__tests__/close-browser.test.js.map +1 -0
  27. package/dist/src/__tests__/console-log.test.d.ts +2 -0
  28. package/dist/src/__tests__/console-log.test.d.ts.map +1 -0
  29. package/dist/src/__tests__/console-log.test.js +195 -0
  30. package/dist/src/__tests__/console-log.test.js.map +1 -0
  31. package/dist/src/__tests__/find.test.d.ts +2 -0
  32. package/dist/src/__tests__/find.test.d.ts.map +1 -0
  33. package/dist/src/__tests__/find.test.js +299 -0
  34. package/dist/src/__tests__/find.test.js.map +1 -0
  35. package/dist/src/__tests__/frame-command.test.d.ts +2 -0
  36. package/dist/src/__tests__/frame-command.test.d.ts.map +1 -0
  37. package/dist/src/__tests__/frame-command.test.js +85 -0
  38. package/dist/src/__tests__/frame-command.test.js.map +1 -0
  39. package/dist/src/__tests__/output.test.d.ts +0 -0
  40. package/dist/src/__tests__/output.test.d.ts.map +0 -0
  41. package/dist/src/__tests__/output.test.js +0 -0
  42. package/dist/src/__tests__/output.test.js.map +0 -0
  43. package/dist/src/__tests__/ref-cache.test.d.ts +0 -0
  44. package/dist/src/__tests__/ref-cache.test.d.ts.map +0 -0
  45. package/dist/src/__tests__/ref-cache.test.js +43 -2
  46. package/dist/src/__tests__/ref-cache.test.js.map +1 -1
  47. package/dist/src/__tests__/session-state.test.d.ts +2 -0
  48. package/dist/src/__tests__/session-state.test.d.ts.map +1 -0
  49. package/dist/src/__tests__/session-state.test.js +203 -0
  50. package/dist/src/__tests__/session-state.test.js.map +1 -0
  51. package/dist/src/__tests__/snapshot.test.d.ts +2 -0
  52. package/dist/src/__tests__/snapshot.test.d.ts.map +1 -0
  53. package/dist/src/__tests__/snapshot.test.js +262 -0
  54. package/dist/src/__tests__/snapshot.test.js.map +1 -0
  55. package/dist/src/__tests__/storage-eval.test.d.ts +2 -0
  56. package/dist/src/__tests__/storage-eval.test.d.ts.map +1 -0
  57. package/dist/src/__tests__/storage-eval.test.js +133 -0
  58. package/dist/src/__tests__/storage-eval.test.js.map +1 -0
  59. package/dist/src/__tests__/tabs.test.d.ts +2 -0
  60. package/dist/src/__tests__/tabs.test.d.ts.map +1 -0
  61. package/dist/src/__tests__/tabs.test.js +161 -0
  62. package/dist/src/__tests__/tabs.test.js.map +1 -0
  63. package/dist/src/browser/action-builder/analyzer.d.ts +0 -0
  64. package/dist/src/browser/action-builder/analyzer.d.ts.map +0 -0
  65. package/dist/src/browser/action-builder/analyzer.js +0 -0
  66. package/dist/src/browser/action-builder/analyzer.js.map +0 -0
  67. package/dist/src/browser/action-builder/types.d.ts +0 -0
  68. package/dist/src/browser/action-builder/types.d.ts.map +0 -0
  69. package/dist/src/browser/action-builder/types.js +0 -0
  70. package/dist/src/browser/action-builder/types.js.map +0 -0
  71. package/dist/src/browser/actions.d.ts +0 -0
  72. package/dist/src/browser/actions.d.ts.map +0 -0
  73. package/dist/src/browser/actions.js +0 -0
  74. package/dist/src/browser/actions.js.map +0 -0
  75. package/dist/src/browser/adapters/gemini.d.ts +0 -0
  76. package/dist/src/browser/adapters/gemini.d.ts.map +0 -0
  77. package/dist/src/browser/adapters/gemini.js +0 -0
  78. package/dist/src/browser/adapters/gemini.js.map +0 -0
  79. package/dist/src/browser/adapters/google.d.ts +0 -0
  80. package/dist/src/browser/adapters/google.d.ts.map +0 -0
  81. package/dist/src/browser/adapters/google.js +0 -0
  82. package/dist/src/browser/adapters/google.js.map +0 -0
  83. package/dist/src/browser/adapters/index.d.ts +0 -0
  84. package/dist/src/browser/adapters/index.d.ts.map +0 -0
  85. package/dist/src/browser/adapters/index.js +0 -0
  86. package/dist/src/browser/adapters/index.js.map +0 -0
  87. package/dist/src/browser/adapters/instagram.d.ts +0 -0
  88. package/dist/src/browser/adapters/instagram.d.ts.map +0 -0
  89. package/dist/src/browser/adapters/instagram.js +0 -0
  90. package/dist/src/browser/adapters/instagram.js.map +0 -0
  91. package/dist/src/browser/adapters/linkedin.d.ts +0 -0
  92. package/dist/src/browser/adapters/linkedin.d.ts.map +0 -0
  93. package/dist/src/browser/adapters/linkedin.js +0 -0
  94. package/dist/src/browser/adapters/linkedin.js.map +0 -0
  95. package/dist/src/browser/adapters/microsoft.d.ts +0 -0
  96. package/dist/src/browser/adapters/microsoft.d.ts.map +0 -0
  97. package/dist/src/browser/adapters/microsoft.js +0 -0
  98. package/dist/src/browser/adapters/microsoft.js.map +0 -0
  99. package/dist/src/browser/adapters/x.d.ts +0 -0
  100. package/dist/src/browser/adapters/x.d.ts.map +0 -0
  101. package/dist/src/browser/adapters/x.js +0 -0
  102. package/dist/src/browser/adapters/x.js.map +0 -0
  103. package/dist/src/browser/batch.d.ts +0 -0
  104. package/dist/src/browser/batch.d.ts.map +0 -0
  105. package/dist/src/browser/batch.js +0 -0
  106. package/dist/src/browser/batch.js.map +0 -0
  107. package/dist/src/browser/browser.d.ts +13 -0
  108. package/dist/src/browser/browser.d.ts.map +1 -1
  109. package/dist/src/browser/browser.js +99 -16
  110. package/dist/src/browser/browser.js.map +1 -1
  111. package/dist/src/browser/cdp.d.ts +4 -1
  112. package/dist/src/browser/cdp.d.ts.map +1 -1
  113. package/dist/src/browser/cdp.js +45 -16
  114. package/dist/src/browser/cdp.js.map +1 -1
  115. package/dist/src/browser/console-log.d.ts +0 -0
  116. package/dist/src/browser/console-log.d.ts.map +0 -0
  117. package/dist/src/browser/console-log.js +0 -0
  118. package/dist/src/browser/console-log.js.map +0 -0
  119. package/dist/src/browser/dashboard/api-types.d.ts +0 -0
  120. package/dist/src/browser/dashboard/api-types.d.ts.map +0 -0
  121. package/dist/src/browser/dashboard/api-types.js +0 -0
  122. package/dist/src/browser/dashboard/api-types.js.map +0 -0
  123. package/dist/src/browser/dashboard/server.d.ts +0 -0
  124. package/dist/src/browser/dashboard/server.d.ts.map +0 -0
  125. package/dist/src/browser/dashboard/server.js +0 -0
  126. package/dist/src/browser/dashboard/server.js.map +0 -0
  127. package/dist/src/browser/dashboard/ui.html +0 -0
  128. package/dist/src/browser/dialog.d.ts +0 -0
  129. package/dist/src/browser/dialog.d.ts.map +0 -0
  130. package/dist/src/browser/dialog.js +0 -0
  131. package/dist/src/browser/dialog.js.map +0 -0
  132. package/dist/src/browser/emulation.d.ts +0 -0
  133. package/dist/src/browser/emulation.d.ts.map +0 -0
  134. package/dist/src/browser/emulation.js +0 -0
  135. package/dist/src/browser/emulation.js.map +0 -0
  136. package/dist/src/browser/find.d.ts +0 -0
  137. package/dist/src/browser/find.d.ts.map +0 -0
  138. package/dist/src/browser/find.js +0 -0
  139. package/dist/src/browser/find.js.map +0 -0
  140. package/dist/src/browser/har.d.ts +0 -0
  141. package/dist/src/browser/har.d.ts.map +0 -0
  142. package/dist/src/browser/har.js +0 -0
  143. package/dist/src/browser/har.js.map +0 -0
  144. package/dist/src/browser/index.d.ts +0 -0
  145. package/dist/src/browser/index.d.ts.map +0 -0
  146. package/dist/src/browser/index.js +0 -0
  147. package/dist/src/browser/index.js.map +0 -0
  148. package/dist/src/browser/network.d.ts +0 -0
  149. package/dist/src/browser/network.d.ts.map +0 -0
  150. package/dist/src/browser/network.js +0 -0
  151. package/dist/src/browser/network.js.map +0 -0
  152. package/dist/src/browser/pdf.d.ts +0 -0
  153. package/dist/src/browser/pdf.d.ts.map +0 -0
  154. package/dist/src/browser/pdf.js +0 -0
  155. package/dist/src/browser/pdf.js.map +0 -0
  156. package/dist/src/browser/profiler.d.ts +0 -0
  157. package/dist/src/browser/profiler.d.ts.map +0 -0
  158. package/dist/src/browser/profiler.js +0 -0
  159. package/dist/src/browser/profiler.js.map +0 -0
  160. package/dist/src/browser/record.d.ts +0 -0
  161. package/dist/src/browser/record.d.ts.map +0 -0
  162. package/dist/src/browser/record.js +0 -0
  163. package/dist/src/browser/record.js.map +0 -0
  164. package/dist/src/browser/ref-cache.d.ts +6 -1
  165. package/dist/src/browser/ref-cache.d.ts.map +1 -1
  166. package/dist/src/browser/ref-cache.js +21 -3
  167. package/dist/src/browser/ref-cache.js.map +1 -1
  168. package/dist/src/browser/screenshot.d.ts +0 -0
  169. package/dist/src/browser/screenshot.d.ts.map +0 -0
  170. package/dist/src/browser/screenshot.js +0 -0
  171. package/dist/src/browser/screenshot.js.map +0 -0
  172. package/dist/src/browser/session.d.ts +0 -0
  173. package/dist/src/browser/session.d.ts.map +0 -0
  174. package/dist/src/browser/session.js +0 -0
  175. package/dist/src/browser/session.js.map +0 -0
  176. package/dist/src/browser/snapshot.d.ts +0 -0
  177. package/dist/src/browser/snapshot.d.ts.map +0 -0
  178. package/dist/src/browser/snapshot.js +0 -0
  179. package/dist/src/browser/snapshot.js.map +0 -0
  180. package/dist/src/browser/storage.d.ts +0 -0
  181. package/dist/src/browser/storage.d.ts.map +0 -0
  182. package/dist/src/browser/storage.js +0 -0
  183. package/dist/src/browser/storage.js.map +0 -0
  184. package/dist/src/browser/tabs.d.ts +5 -1
  185. package/dist/src/browser/tabs.d.ts.map +1 -1
  186. package/dist/src/browser/tabs.js +4 -3
  187. package/dist/src/browser/tabs.js.map +1 -1
  188. package/dist/src/browser/trace.d.ts +0 -0
  189. package/dist/src/browser/trace.d.ts.map +0 -0
  190. package/dist/src/browser/trace.js +0 -0
  191. package/dist/src/browser/trace.js.map +0 -0
  192. package/dist/src/browser/types.d.ts +4 -0
  193. package/dist/src/browser/types.d.ts.map +1 -1
  194. package/dist/src/browser/types.js +0 -0
  195. package/dist/src/browser/types.js.map +1 -1
  196. package/dist/src/browser/vitals.d.ts +0 -0
  197. package/dist/src/browser/vitals.d.ts.map +0 -0
  198. package/dist/src/browser/vitals.js +0 -0
  199. package/dist/src/browser/vitals.js.map +0 -0
  200. package/dist/src/browser/wait.d.ts +0 -0
  201. package/dist/src/browser/wait.d.ts.map +0 -0
  202. package/dist/src/browser/wait.js +0 -0
  203. package/dist/src/browser/wait.js.map +0 -0
  204. package/dist/src/cli/action.d.ts +0 -0
  205. package/dist/src/cli/action.d.ts.map +0 -0
  206. package/dist/src/cli/action.js +0 -0
  207. package/dist/src/cli/action.js.map +0 -0
  208. package/dist/src/cli/commands.d.ts +0 -0
  209. package/dist/src/cli/commands.d.ts.map +1 -1
  210. package/dist/src/cli/commands.js +96 -4
  211. package/dist/src/cli/commands.js.map +1 -1
  212. package/dist/src/cli/output.d.ts +0 -0
  213. package/dist/src/cli/output.d.ts.map +0 -0
  214. package/dist/src/cli/output.js +0 -0
  215. package/dist/src/cli/output.js.map +0 -0
  216. package/dist/src/cli/platform.d.ts +0 -0
  217. package/dist/src/cli/platform.d.ts.map +0 -0
  218. package/dist/src/cli/platform.js +0 -0
  219. package/dist/src/cli/platform.js.map +0 -0
  220. package/dist/src/cli/types.d.ts +0 -0
  221. package/dist/src/cli/types.d.ts.map +0 -0
  222. package/dist/src/cli/types.js +0 -0
  223. package/dist/src/cli/types.js.map +0 -0
  224. package/dist/src/cli.d.ts +0 -0
  225. package/dist/src/cli.d.ts.map +0 -0
  226. package/dist/src/cli.js.map +0 -0
  227. package/dist/src/index.d.ts +0 -0
  228. package/dist/src/index.d.ts.map +0 -0
  229. package/dist/src/index.js +0 -0
  230. package/dist/src/index.js.map +0 -0
  231. package/dist/tsconfig.tsbuildinfo +1 -1
  232. package/package.json +11 -8
  233. package/src/__tests__/adapters.test.ts +125 -0
  234. package/src/__tests__/batch-eval-parsing.test.ts +0 -0
  235. package/src/__tests__/box-model.test.ts +0 -0
  236. package/src/__tests__/browser-launch.test.ts +0 -0
  237. package/src/__tests__/cdp-client.test.ts +375 -0
  238. package/src/__tests__/close-browser.test.ts +205 -0
  239. package/src/__tests__/console-log.test.ts +250 -0
  240. package/src/__tests__/find.test.ts +354 -0
  241. package/src/__tests__/frame-command.test.ts +98 -0
  242. package/src/__tests__/output.test.ts +0 -0
  243. package/src/__tests__/ref-cache.test.ts +47 -2
  244. package/src/__tests__/session-state.test.ts +251 -0
  245. package/src/__tests__/snapshot.test.ts +331 -0
  246. package/src/__tests__/storage-eval.test.ts +171 -0
  247. package/src/__tests__/tabs.test.ts +183 -0
  248. package/src/browser/action-builder/analyzer.ts +0 -0
  249. package/src/browser/action-builder/types.ts +0 -0
  250. package/src/browser/actions.ts +0 -0
  251. package/src/browser/adapters/gemini.ts +0 -0
  252. package/src/browser/adapters/google.ts +0 -0
  253. package/src/browser/adapters/index.ts +0 -0
  254. package/src/browser/adapters/instagram.ts +0 -0
  255. package/src/browser/adapters/linkedin.ts +0 -0
  256. package/src/browser/adapters/microsoft.ts +0 -0
  257. package/src/browser/adapters/x.ts +0 -0
  258. package/src/browser/batch.ts +0 -0
  259. package/src/browser/browser.ts +101 -16
  260. package/src/browser/cdp.ts +52 -15
  261. package/src/browser/console-log.ts +0 -0
  262. package/src/browser/dashboard/api-types.ts +0 -0
  263. package/src/browser/dashboard/server.ts +0 -0
  264. package/src/browser/dashboard/ui.html +0 -0
  265. package/src/browser/dialog.ts +0 -0
  266. package/src/browser/emulation.ts +0 -0
  267. package/src/browser/find.ts +0 -0
  268. package/src/browser/har.ts +0 -0
  269. package/src/browser/index.ts +0 -0
  270. package/src/browser/network.ts +0 -0
  271. package/src/browser/pdf.ts +0 -0
  272. package/src/browser/profiler.ts +0 -0
  273. package/src/browser/record.ts +0 -0
  274. package/src/browser/ref-cache.ts +29 -6
  275. package/src/browser/screenshot.ts +0 -0
  276. package/src/browser/session.ts +0 -0
  277. package/src/browser/snapshot.ts +0 -0
  278. package/src/browser/storage.ts +0 -0
  279. package/src/browser/tabs.ts +12 -4
  280. package/src/browser/trace.ts +0 -0
  281. package/src/browser/types.ts +4 -0
  282. package/src/browser/vitals.ts +0 -0
  283. package/src/browser/wait.ts +0 -0
  284. package/src/cli/action.ts +0 -0
  285. package/src/cli/commands.ts +90 -4
  286. package/src/cli/output.ts +0 -0
  287. package/src/cli/platform.ts +0 -0
  288. package/src/cli/types.ts +0 -0
  289. package/src/cli.ts +0 -0
  290. package/src/index.ts +0 -0
@@ -1,13 +1,41 @@
1
1
  import { WebSocket } from 'ws';
2
2
  import type { CdpCommand, CdpResponse, CdpTarget } from './types.js';
3
3
 
4
+ // send() had no timeout of its own — a command whose response never arrives
5
+ // (wedged/crashed renderer, dropped socket that never fires 'close') left a
6
+ // pendingCommands entry forever, hanging whatever awaited it (and, since
7
+ // most CLI actions await a single send(), the whole CLI process). Ad-hoc
8
+ // timeouts were previously patched in at a handful of call sites (e.g.
9
+ // actions.ts's evaluateJs) instead of here, so most commands had no
10
+ // protection at all. This is the default for every send() unless a caller
11
+ // passes 0 (no timeout) or a different value.
12
+ export const DEFAULT_CDP_SEND_TIMEOUT_MS = 30_000;
13
+
4
14
  export class CdpClient {
5
15
  private ws: WebSocket | null = null;
6
- private pendingCommands = new Map<number, { resolve: (r: CdpResponse) => void; reject: (e: Error) => void }>();
16
+ private pendingCommands = new Map<number, { resolve: (r: CdpResponse) => void; reject: (e: Error) => void; timer?: ReturnType<typeof setTimeout> }>();
7
17
  private eventListeners = new Map<string, Set<(params: Record<string, unknown>, sessionId?: string) => void>>();
8
18
  private nextId = 1;
9
19
  private connected = false;
10
20
 
21
+ constructor() {
22
+ // Target.targetCrashed/targetDestroyed fire over the same WS connection
23
+ // as command responses, but a crashed renderer's connection can otherwise
24
+ // sit open with no 'close'/'error' event ever firing — flush pending
25
+ // commands here so send() doesn't have to wait out its own timeout.
26
+ this.on('Target.targetCrashed', () => this.flushPending(new Error('CDP target crashed')));
27
+ this.on('Target.targetDestroyed', () => this.flushPending(new Error('CDP target destroyed')));
28
+ }
29
+
30
+ private flushPending(err: Error): void {
31
+ this.connected = false;
32
+ for (const { reject: r, timer } of this.pendingCommands.values()) {
33
+ if (timer) clearTimeout(timer);
34
+ r(err);
35
+ }
36
+ this.pendingCommands.clear();
37
+ }
38
+
11
39
  async connect(wsUrl: string): Promise<void> {
12
40
  return new Promise((resolve, reject) => {
13
41
  this.ws = new WebSocket(wsUrl);
@@ -22,18 +50,12 @@ export class CdpClient {
22
50
  reject(err);
23
51
  } else {
24
52
  // Post-connect: flush all pending commands — 'close' may not fire on all platforms
25
- this.connected = false;
26
- for (const { reject: r } of this.pendingCommands.values()) r(err);
27
- this.pendingCommands.clear();
53
+ this.flushPending(err instanceof Error ? err : new Error(String(err)));
28
54
  }
29
55
  });
30
56
 
31
57
  this.ws.on('close', () => {
32
- this.connected = false;
33
- for (const { reject: r } of this.pendingCommands.values()) {
34
- r(new Error('CDP connection closed'));
35
- }
36
- this.pendingCommands.clear();
58
+ this.flushPending(new Error('CDP connection closed'));
37
59
  });
38
60
 
39
61
  this.ws.on('message', (data) => {
@@ -62,7 +84,12 @@ export class CdpClient {
62
84
  });
63
85
  }
64
86
 
65
- send<T = Record<string, unknown>>(method: string, params?: Record<string, unknown>, sessionId?: string): Promise<T> {
87
+ send<T = Record<string, unknown>>(
88
+ method: string,
89
+ params?: Record<string, unknown>,
90
+ sessionId?: string,
91
+ timeoutMs: number = DEFAULT_CDP_SEND_TIMEOUT_MS
92
+ ): Promise<T> {
66
93
  return new Promise((resolve, reject) => {
67
94
  if (!this.ws || !this.connected) {
68
95
  reject(new Error('CDP not connected'));
@@ -76,12 +103,24 @@ export class CdpClient {
76
103
  const id = this.nextId++;
77
104
  const cmd: CdpCommand = { id, method, params };
78
105
  if (sessionId) cmd.sessionId = sessionId;
106
+
107
+ let timer: ReturnType<typeof setTimeout> | undefined;
108
+ if (timeoutMs > 0) {
109
+ timer = setTimeout(() => {
110
+ this.pendingCommands.delete(id);
111
+ reject(new Error(`CDP command "${method}" timed out after ${timeoutMs}ms`));
112
+ }, timeoutMs);
113
+ timer.unref?.();
114
+ }
115
+
79
116
  this.pendingCommands.set(id, {
80
- resolve: (r) => resolve((r.result ?? {}) as T),
81
- reject,
117
+ resolve: (r) => { if (timer) clearTimeout(timer); resolve((r.result ?? {}) as T); },
118
+ reject: (e) => { if (timer) clearTimeout(timer); reject(e); },
119
+ timer,
82
120
  });
83
121
  this.ws.send(JSON.stringify(cmd), (err) => {
84
122
  if (err) {
123
+ if (timer) clearTimeout(timer);
85
124
  this.pendingCommands.delete(id);
86
125
  reject(err);
87
126
  }
@@ -120,9 +159,7 @@ export class CdpClient {
120
159
  }
121
160
 
122
161
  close(): void {
123
- this.connected = false;
124
- for (const { reject: r } of this.pendingCommands.values()) r(new Error('CDP connection closed'));
125
- this.pendingCommands.clear();
162
+ this.flushPending(new Error('CDP connection closed'));
126
163
  this.ws?.close();
127
164
  this.ws = null;
128
165
  this.eventListeners.clear();
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -97,14 +97,29 @@ export async function clearRefCache(): Promise<void> {
97
97
  * falling back to the hardcoded default port and launching/attaching to a
98
98
  * second, unrelated Chrome instance.
99
99
  */
100
- export async function saveActivePort(port: number, opts?: { launched?: boolean }): Promise<void> {
100
+ export async function saveActivePort(
101
+ port: number,
102
+ opts?: { launched?: boolean; pid?: number; userDataDir?: string }
103
+ ): Promise<void> {
101
104
  try {
102
105
  await mkdir(CACHE_DIR, { recursive: true });
103
106
  // `launched` records provenance: true = monobrowse spawned this Chrome
104
107
  // (safe to Browser.close later); false = attached to a browser someone
105
108
  // else owns (must never be killed). Absent (old files) reads as launched
106
109
  // — matches pre-flag behavior where every persisted port came from open.
107
- await writeFile(PORT_FILE, JSON.stringify({ port, launched: opts?.launched !== false, savedAt: Date.now() }));
110
+ //
111
+ // `pid`/`userDataDir` are persisted so a LATER, fresh CLI process (each
112
+ // command is its own node process — see module header) can still find
113
+ // and kill the Chrome this one launched. Without this, closeBrowser()'s
114
+ // PID-kill fallback can never fire outside the launching process, since
115
+ // the in-memory launchedPids Map (browser.ts) is empty there.
116
+ await writeFile(PORT_FILE, JSON.stringify({
117
+ port,
118
+ launched: opts?.launched !== false,
119
+ pid: opts?.pid,
120
+ userDataDir: opts?.userDataDir,
121
+ savedAt: Date.now(),
122
+ }));
108
123
  } catch {
109
124
  // Best-effort — persistence failure just means the next process falls
110
125
  // back to the hardcoded default port, matching prior behavior.
@@ -126,13 +141,21 @@ export async function loadActivePort(): Promise<number | null> {
126
141
  return (await loadActivePortInfo())?.port ?? null;
127
142
  }
128
143
 
129
- /** Load the persisted active port with its provenance flag. */
130
- export async function loadActivePortInfo(): Promise<{ port: number; launched: boolean } | null> {
144
+ /** Load the persisted active port with its provenance flag, PID, user-data-dir, and save timestamp. */
145
+ export async function loadActivePortInfo(): Promise<
146
+ { port: number; launched: boolean; pid?: number; userDataDir?: string; savedAt?: number } | null
147
+ > {
131
148
  try {
132
149
  const raw = await readFile(PORT_FILE, 'utf8');
133
- const data = JSON.parse(raw) as { port?: unknown; launched?: unknown };
150
+ const data = JSON.parse(raw) as { port?: unknown; launched?: unknown; pid?: unknown; userDataDir?: unknown; savedAt?: unknown };
134
151
  if (typeof data.port === 'number' && Number.isInteger(data.port) && data.port >= 1024 && data.port <= 65535) {
135
- return { port: data.port, launched: data.launched !== false };
152
+ return {
153
+ port: data.port,
154
+ launched: data.launched !== false,
155
+ pid: typeof data.pid === 'number' && Number.isInteger(data.pid) && data.pid > 0 ? data.pid : undefined,
156
+ userDataDir: typeof data.userDataDir === 'string' ? data.userDataDir : undefined,
157
+ savedAt: typeof data.savedAt === 'number' ? data.savedAt : undefined,
158
+ };
136
159
  }
137
160
  return null;
138
161
  } catch {
File without changes
File without changes
File without changes
File without changes
@@ -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
  }
File without changes
@@ -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 {
File without changes
File without changes
package/src/cli/action.ts CHANGED
File without changes
@@ -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;
@@ -1275,6 +1345,7 @@ const closeCommand: Command = {
1275
1345
  client.close();
1276
1346
  _client = null;
1277
1347
  _sessionId = '';
1348
+ _parentSessionId = '';
1278
1349
  _targetId = '';
1279
1350
  _refs = new Map();
1280
1351
  // Session is gone — forget the persisted port and stale refs so later
@@ -1919,10 +1990,23 @@ const frameCommand: Command = {
1919
1990
  if (!target) throw new Error('Usage: monomind browse frame <selector>|main');
1920
1991
 
1921
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
+ }
1922
1999
  output.printSuccess('Switched to main frame');
1923
2000
  } else {
1924
- const frameSrc = await browser.switchToFrame(client, sessionId, target);
1925
- 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}`);
1926
2010
  }
1927
2011
  return { success: true };
1928
2012
  },
@@ -1974,6 +2058,7 @@ const tabCommand: Command = {
1974
2058
  client.close();
1975
2059
  _client = null;
1976
2060
  _sessionId = '';
2061
+ _parentSessionId = '';
1977
2062
  _targetId = '';
1978
2063
  _refs = new Map();
1979
2064
  } else {
@@ -2726,6 +2811,7 @@ const connectCommand: Command = {
2726
2811
  prevClient.close();
2727
2812
  _client = null;
2728
2813
  _sessionId = '';
2814
+ _parentSessionId = '';
2729
2815
  _targetId = '';
2730
2816
  _refs = new Map();
2731
2817
  }
package/src/cli/output.ts CHANGED
File without changes
File without changes
package/src/cli/types.ts CHANGED
File without changes
package/src/cli.ts CHANGED
File without changes
package/src/index.ts CHANGED
File without changes