@monoes/monobrowse 1.0.11 → 1.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. package/dist/src/__tests__/adapters.test.js +5 -2
  2. package/dist/src/__tests__/adapters.test.js.map +1 -1
  3. package/dist/src/__tests__/batch-eval-parsing.test.js +4 -2
  4. package/dist/src/__tests__/batch-eval-parsing.test.js.map +1 -1
  5. package/dist/src/__tests__/box-model.test.js +9 -5
  6. package/dist/src/__tests__/box-model.test.js.map +1 -1
  7. package/dist/src/__tests__/browser-launch.test.d.ts +10 -0
  8. package/dist/src/__tests__/browser-launch.test.d.ts.map +1 -1
  9. package/dist/src/__tests__/browser-launch.test.js +17 -10
  10. package/dist/src/__tests__/browser-launch.test.js.map +1 -1
  11. package/dist/src/__tests__/cdp-client.test.js +9 -3
  12. package/dist/src/__tests__/cdp-client.test.js.map +1 -1
  13. package/dist/src/__tests__/close-browser.test.d.ts +13 -0
  14. package/dist/src/__tests__/close-browser.test.d.ts.map +1 -1
  15. package/dist/src/__tests__/close-browser.test.js +5 -5
  16. package/dist/src/__tests__/close-browser.test.js.map +1 -1
  17. package/dist/src/__tests__/console-log.test.js +17 -6
  18. package/dist/src/__tests__/console-log.test.js.map +1 -1
  19. package/dist/src/__tests__/find.test.js +26 -6
  20. package/dist/src/__tests__/find.test.js.map +1 -1
  21. package/dist/src/__tests__/frame-command.test.js +9 -3
  22. package/dist/src/__tests__/frame-command.test.js.map +1 -1
  23. package/dist/src/__tests__/open-url-navigate-error.test.js +1 -3
  24. package/dist/src/__tests__/open-url-navigate-error.test.js.map +1 -1
  25. package/dist/src/__tests__/output.test.js +1 -1
  26. package/dist/src/__tests__/output.test.js.map +1 -1
  27. package/dist/src/__tests__/reap-idle-browser.test.d.ts +15 -0
  28. package/dist/src/__tests__/reap-idle-browser.test.d.ts.map +1 -0
  29. package/dist/src/__tests__/reap-idle-browser.test.js +238 -0
  30. package/dist/src/__tests__/reap-idle-browser.test.js.map +1 -0
  31. package/dist/src/__tests__/ref-cache.test.js +18 -10
  32. package/dist/src/__tests__/ref-cache.test.js.map +1 -1
  33. package/dist/src/__tests__/session-state.test.d.ts +8 -0
  34. package/dist/src/__tests__/session-state.test.d.ts.map +1 -1
  35. package/dist/src/__tests__/session-state.test.js +10 -10
  36. package/dist/src/__tests__/session-state.test.js.map +1 -1
  37. package/dist/src/__tests__/snapshot.test.js +44 -14
  38. package/dist/src/__tests__/snapshot.test.js.map +1 -1
  39. package/dist/src/__tests__/storage-eval.test.js +14 -6
  40. package/dist/src/__tests__/storage-eval.test.js.map +1 -1
  41. package/dist/src/__tests__/tabs.test.js +25 -9
  42. package/dist/src/__tests__/tabs.test.js.map +1 -1
  43. package/dist/src/browser/action-builder/analyzer.d.ts +1 -1
  44. package/dist/src/browser/action-builder/analyzer.d.ts.map +1 -1
  45. package/dist/src/browser/action-builder/analyzer.js +25 -9
  46. package/dist/src/browser/action-builder/analyzer.js.map +1 -1
  47. package/dist/src/browser/action-builder/types.d.ts.map +1 -1
  48. package/dist/src/browser/actions.d.ts +1 -1
  49. package/dist/src/browser/actions.d.ts.map +1 -1
  50. package/dist/src/browser/actions.js +16 -6
  51. package/dist/src/browser/actions.js.map +1 -1
  52. package/dist/src/browser/adapters/gemini.d.ts.map +1 -1
  53. package/dist/src/browser/adapters/gemini.js.map +1 -1
  54. package/dist/src/browser/adapters/google.d.ts.map +1 -1
  55. package/dist/src/browser/adapters/google.js.map +1 -1
  56. package/dist/src/browser/adapters/index.d.ts.map +1 -1
  57. package/dist/src/browser/adapters/index.js +3 -3
  58. package/dist/src/browser/adapters/index.js.map +1 -1
  59. package/dist/src/browser/adapters/instagram.d.ts.map +1 -1
  60. package/dist/src/browser/adapters/instagram.js.map +1 -1
  61. package/dist/src/browser/adapters/linkedin.d.ts.map +1 -1
  62. package/dist/src/browser/adapters/linkedin.js.map +1 -1
  63. package/dist/src/browser/adapters/microsoft.d.ts.map +1 -1
  64. package/dist/src/browser/adapters/microsoft.js.map +1 -1
  65. package/dist/src/browser/adapters/x.d.ts.map +1 -1
  66. package/dist/src/browser/adapters/x.js.map +1 -1
  67. package/dist/src/browser/browser.d.ts +30 -0
  68. package/dist/src/browser/browser.d.ts.map +1 -1
  69. package/dist/src/browser/browser.js +122 -15
  70. package/dist/src/browser/browser.js.map +1 -1
  71. package/dist/src/browser/cdp.d.ts.map +1 -1
  72. package/dist/src/browser/cdp.js +21 -8
  73. package/dist/src/browser/cdp.js.map +1 -1
  74. package/dist/src/browser/console-log.d.ts.map +1 -1
  75. package/dist/src/browser/console-log.js +2 -2
  76. package/dist/src/browser/console-log.js.map +1 -1
  77. package/dist/src/browser/dashboard/server.d.ts.map +1 -1
  78. package/dist/src/browser/dashboard/server.js +29 -19
  79. package/dist/src/browser/dashboard/server.js.map +1 -1
  80. package/dist/src/browser/dashboard/ui.html +153 -165
  81. package/dist/src/browser/dialog.d.ts.map +1 -1
  82. package/dist/src/browser/dialog.js +3 -1
  83. package/dist/src/browser/dialog.js.map +1 -1
  84. package/dist/src/browser/emulation.d.ts.map +1 -1
  85. package/dist/src/browser/emulation.js +21 -6
  86. package/dist/src/browser/emulation.js.map +1 -1
  87. package/dist/src/browser/find.d.ts +4 -4
  88. package/dist/src/browser/find.d.ts.map +1 -1
  89. package/dist/src/browser/find.js +14 -6
  90. package/dist/src/browser/find.js.map +1 -1
  91. package/dist/src/browser/har.d.ts.map +1 -1
  92. package/dist/src/browser/har.js +14 -7
  93. package/dist/src/browser/har.js.map +1 -1
  94. package/dist/src/browser/index.d.ts +16 -16
  95. package/dist/src/browser/index.d.ts.map +1 -1
  96. package/dist/src/browser/index.js +16 -16
  97. package/dist/src/browser/index.js.map +1 -1
  98. package/dist/src/browser/network.d.ts.map +1 -1
  99. package/dist/src/browser/network.js +14 -4
  100. package/dist/src/browser/network.js.map +1 -1
  101. package/dist/src/browser/pdf.d.ts.map +1 -1
  102. package/dist/src/browser/pdf.js +3 -3
  103. package/dist/src/browser/pdf.js.map +1 -1
  104. package/dist/src/browser/profiler.d.ts.map +1 -1
  105. package/dist/src/browser/profiler.js +10 -5
  106. package/dist/src/browser/profiler.js.map +1 -1
  107. package/dist/src/browser/record.d.ts +1 -1
  108. package/dist/src/browser/record.d.ts.map +1 -1
  109. package/dist/src/browser/record.js +18 -7
  110. package/dist/src/browser/record.js.map +1 -1
  111. package/dist/src/browser/ref-cache.d.ts.map +1 -1
  112. package/dist/src/browser/ref-cache.js +9 -4
  113. package/dist/src/browser/ref-cache.js.map +1 -1
  114. package/dist/src/browser/screenshot.d.ts.map +1 -1
  115. package/dist/src/browser/screenshot.js +11 -7
  116. package/dist/src/browser/screenshot.js.map +1 -1
  117. package/dist/src/browser/session.d.ts +1 -1
  118. package/dist/src/browser/session.d.ts.map +1 -1
  119. package/dist/src/browser/session.js +27 -8
  120. package/dist/src/browser/session.js.map +1 -1
  121. package/dist/src/browser/snapshot.d.ts +1 -1
  122. package/dist/src/browser/snapshot.d.ts.map +1 -1
  123. package/dist/src/browser/snapshot.js +8 -4
  124. package/dist/src/browser/snapshot.js.map +1 -1
  125. package/dist/src/browser/storage.d.ts.map +1 -1
  126. package/dist/src/browser/storage.js.map +1 -1
  127. package/dist/src/browser/tabs.d.ts.map +1 -1
  128. package/dist/src/browser/tabs.js +9 -3
  129. package/dist/src/browser/tabs.js.map +1 -1
  130. package/dist/src/browser/trace.d.ts.map +1 -1
  131. package/dist/src/browser/trace.js +3 -3
  132. package/dist/src/browser/trace.js.map +1 -1
  133. package/dist/src/browser/types.d.ts.map +1 -1
  134. package/dist/src/browser/types.js +23 -4
  135. package/dist/src/browser/types.js.map +1 -1
  136. package/dist/src/browser/vitals.d.ts.map +1 -1
  137. package/dist/src/browser/vitals.js +1 -1
  138. package/dist/src/browser/vitals.js.map +1 -1
  139. package/dist/src/browser/wait.d.ts.map +1 -1
  140. package/dist/src/browser/wait.js +11 -5
  141. package/dist/src/browser/wait.js.map +1 -1
  142. package/dist/src/cli/action.d.ts.map +1 -1
  143. package/dist/src/cli/action.js +14 -11
  144. package/dist/src/cli/action.js.map +1 -1
  145. package/dist/src/cli/commands.d.ts.map +1 -1
  146. package/dist/src/cli/commands.js +453 -159
  147. package/dist/src/cli/commands.js.map +1 -1
  148. package/dist/src/cli/output.js +5 -5
  149. package/dist/src/cli/output.js.map +1 -1
  150. package/dist/src/cli/platform.d.ts.map +1 -1
  151. package/dist/src/cli/platform.js +15 -11
  152. package/dist/src/cli/platform.js.map +1 -1
  153. package/dist/src/cli/types.d.ts +1 -1
  154. package/dist/src/cli/types.d.ts.map +1 -1
  155. package/dist/src/cli.js +21 -9
  156. package/dist/src/cli.js.map +1 -1
  157. package/dist/src/index.d.ts +2 -2
  158. package/dist/src/index.d.ts.map +1 -1
  159. package/dist/src/index.js +2 -2
  160. package/dist/src/index.js.map +1 -1
  161. package/dist/tsconfig.tsbuildinfo +1 -1
  162. package/package.json +1 -1
  163. package/src/__tests__/adapters.test.ts +15 -8
  164. package/src/__tests__/batch-eval-parsing.test.ts +4 -2
  165. package/src/__tests__/box-model.test.ts +9 -5
  166. package/src/__tests__/browser-launch.test.ts +26 -15
  167. package/src/__tests__/cdp-client.test.ts +25 -9
  168. package/src/__tests__/close-browser.test.ts +11 -6
  169. package/src/__tests__/console-log.test.ts +36 -13
  170. package/src/__tests__/find.test.ts +58 -20
  171. package/src/__tests__/frame-command.test.ts +12 -6
  172. package/src/__tests__/open-url-navigate-error.test.ts +2 -4
  173. package/src/__tests__/output.test.ts +1 -1
  174. package/src/__tests__/reap-idle-browser.test.ts +276 -0
  175. package/src/__tests__/ref-cache.test.ts +25 -12
  176. package/src/__tests__/session-state.test.ts +40 -28
  177. package/src/__tests__/snapshot.test.ts +60 -21
  178. package/src/__tests__/storage-eval.test.ts +30 -16
  179. package/src/__tests__/tabs.test.ts +26 -10
  180. package/src/browser/action-builder/analyzer.ts +39 -15
  181. package/src/browser/action-builder/types.ts +45 -8
  182. package/src/browser/actions.ts +288 -123
  183. package/src/browser/adapters/gemini.ts +7 -3
  184. package/src/browser/adapters/google.ts +5 -3
  185. package/src/browser/adapters/index.ts +5 -4
  186. package/src/browser/adapters/instagram.ts +4 -2
  187. package/src/browser/adapters/linkedin.ts +4 -2
  188. package/src/browser/adapters/microsoft.ts +9 -5
  189. package/src/browser/adapters/x.ts +7 -3
  190. package/src/browser/browser.ts +188 -42
  191. package/src/browser/cdp.ts +43 -11
  192. package/src/browser/console-log.ts +24 -7
  193. package/src/browser/dashboard/server.ts +44 -25
  194. package/src/browser/dashboard/ui.html +153 -165
  195. package/src/browser/dialog.ts +25 -8
  196. package/src/browser/emulation.ts +86 -36
  197. package/src/browser/find.ts +110 -47
  198. package/src/browser/har.ts +58 -26
  199. package/src/browser/index.ts +16 -16
  200. package/src/browser/network.ts +147 -46
  201. package/src/browser/pdf.ts +4 -4
  202. package/src/browser/profiler.ts +23 -10
  203. package/src/browser/record.ts +38 -19
  204. package/src/browser/ref-cache.ts +38 -17
  205. package/src/browser/screenshot.ts +58 -32
  206. package/src/browser/session.ts +51 -18
  207. package/src/browser/snapshot.ts +34 -20
  208. package/src/browser/storage.ts +73 -17
  209. package/src/browser/tabs.ts +29 -17
  210. package/src/browser/trace.ts +14 -10
  211. package/src/browser/types.ts +23 -4
  212. package/src/browser/vitals.ts +33 -16
  213. package/src/browser/wait.ts +47 -21
  214. package/src/cli/action.ts +52 -21
  215. package/src/cli/commands.ts +1093 -385
  216. package/src/cli/output.ts +5 -5
  217. package/src/cli/platform.ts +44 -23
  218. package/src/cli/types.ts +1 -1
  219. package/src/cli.ts +34 -18
  220. package/src/index.ts +2 -2
@@ -1,5 +1,5 @@
1
1
  // src/browser/adapters/gemini.ts
2
- import type { PlatformAdapter, PageInterface } from './index.js';
2
+ import type { PageInterface, PlatformAdapter } from './index.js';
3
3
 
4
4
  export const geminiAdapter: PlatformAdapter = {
5
5
  platform: 'gemini',
@@ -7,9 +7,13 @@ export const geminiAdapter: PlatformAdapter = {
7
7
  reservedPaths: ['/app', '/chat'],
8
8
  loginURL: () => 'https://accounts.google.com/signin/v2/identifier',
9
9
  async isLoggedIn(page: PageInterface): Promise<boolean> {
10
- return page.evaluate<boolean>("!!(document.querySelector('bard-sidenav') || document.querySelector('.ql-editor'))");
10
+ return page.evaluate<boolean>(
11
+ "!!(document.querySelector('bard-sidenav') || document.querySelector('.ql-editor'))",
12
+ );
11
13
  },
12
14
  async extractUsername(page: PageInterface): Promise<string> {
13
- return page.evaluate<string>("document.querySelector('.gb_A.gb_Sa')?.textContent?.trim() ?? ''");
15
+ return page.evaluate<string>(
16
+ "document.querySelector('.gb_A.gb_Sa')?.textContent?.trim() ?? ''",
17
+ );
14
18
  },
15
19
  };
@@ -1,5 +1,5 @@
1
1
  // src/browser/adapters/google.ts
2
- import type { PlatformAdapter, PageInterface } from './index.js';
2
+ import type { PageInterface, PlatformAdapter } from './index.js';
3
3
 
4
4
  export const googleAdapter: PlatformAdapter = {
5
5
  platform: 'google',
@@ -10,11 +10,13 @@ export const googleAdapter: PlatformAdapter = {
10
10
  const url = await page.url();
11
11
  // Logged in if we're on a Google service page (not the sign-in page)
12
12
  if (url.includes('accounts.google.com/signin')) return false;
13
- return page.evaluate<boolean>("!!(document.querySelector('[aria-label*=\"Google Account\"]') || document.querySelector('.gb_A') || document.cookie.includes('SSID'))");
13
+ return page.evaluate<boolean>(
14
+ "!!(document.querySelector('[aria-label*=\"Google Account\"]') || document.querySelector('.gb_A') || document.cookie.includes('SSID'))",
15
+ );
14
16
  },
15
17
  async extractUsername(page: PageInterface): Promise<string> {
16
18
  return page.evaluate<string>(
17
- "document.querySelector('[data-email]')?.getAttribute('data-email') ?? document.querySelector('.gb_A.gb_Sa')?.textContent?.trim() ?? ''"
19
+ "document.querySelector('[data-email]')?.getAttribute('data-email') ?? document.querySelector('.gb_A.gb_Sa')?.textContent?.trim() ?? ''",
18
20
  );
19
21
  },
20
22
  };
@@ -14,12 +14,12 @@ export interface PlatformAdapter {
14
14
  extractUsername(page: PageInterface): Promise<string>;
15
15
  }
16
16
 
17
- import { linkedinAdapter } from './linkedin.js';
18
- import { instagramAdapter } from './instagram.js';
19
- import { xAdapter } from './x.js';
20
17
  import { geminiAdapter } from './gemini.js';
21
18
  import { googleAdapter } from './google.js';
19
+ import { instagramAdapter } from './instagram.js';
20
+ import { linkedinAdapter } from './linkedin.js';
22
21
  import { microsoftAdapter } from './microsoft.js';
22
+ import { xAdapter } from './x.js';
23
23
 
24
24
  export const adapters: Map<string, PlatformAdapter> = new Map([
25
25
  ['linkedin', linkedinAdapter],
@@ -32,6 +32,7 @@ export const adapters: Map<string, PlatformAdapter> = new Map([
32
32
 
33
33
  export function getAdapter(platform: string): PlatformAdapter {
34
34
  const adapter = adapters.get(platform);
35
- if (!adapter) throw new Error(`Unknown platform: ${platform}. Supported: ${[...adapters.keys()].join(', ')}`);
35
+ if (!adapter)
36
+ throw new Error(`Unknown platform: ${platform}. Supported: ${[...adapters.keys()].join(', ')}`);
36
37
  return adapter;
37
38
  }
@@ -1,5 +1,5 @@
1
1
  // src/browser/adapters/instagram.ts
2
- import type { PlatformAdapter, PageInterface } from './index.js';
2
+ import type { PageInterface, PlatformAdapter } from './index.js';
3
3
 
4
4
  export const instagramAdapter: PlatformAdapter = {
5
5
  platform: 'instagram',
@@ -11,6 +11,8 @@ export const instagramAdapter: PlatformAdapter = {
11
11
  },
12
12
  async extractUsername(page: PageInterface): Promise<string> {
13
13
  // Use getAttribute to get the raw path-relative href (not the resolved absolute URL)
14
- return page.evaluate<string>("document.querySelector('a[href^=\"/\"]')?.getAttribute('href')?.split('/').filter(Boolean)[0] ?? ''");
14
+ return page.evaluate<string>(
15
+ "document.querySelector('a[href^=\"/\"]')?.getAttribute('href')?.split('/').filter(Boolean)[0] ?? ''",
16
+ );
15
17
  },
16
18
  };
@@ -1,5 +1,5 @@
1
1
  // src/browser/adapters/linkedin.ts
2
- import type { PlatformAdapter, PageInterface } from './index.js';
2
+ import type { PageInterface, PlatformAdapter } from './index.js';
3
3
 
4
4
  export const linkedinAdapter: PlatformAdapter = {
5
5
  platform: 'linkedin',
@@ -10,6 +10,8 @@ export const linkedinAdapter: PlatformAdapter = {
10
10
  return page.evaluate<boolean>("!!document.querySelector('.global-nav__me-photo')");
11
11
  },
12
12
  async extractUsername(page: PageInterface): Promise<string> {
13
- return page.evaluate<string>("document.querySelector('.profile-rail-card__actor-link')?.textContent?.trim() ?? ''");
13
+ return page.evaluate<string>(
14
+ "document.querySelector('.profile-rail-card__actor-link')?.textContent?.trim() ?? ''",
15
+ );
14
16
  },
15
17
  };
@@ -1,19 +1,23 @@
1
1
  // src/browser/adapters/microsoft.ts
2
- import type { PlatformAdapter, PageInterface } from './index.js';
2
+ import type { PageInterface, PlatformAdapter } from './index.js';
3
3
 
4
4
  export const microsoftAdapter: PlatformAdapter = {
5
5
  platform: 'microsoft',
6
6
  baseURL: 'https://login.microsoftonline.com',
7
7
  reservedPaths: ['/oauth2', '/common/oauth2', '/kmsi'],
8
- loginURL: () => 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=common&response_type=code&scope=openid+profile+email',
8
+ loginURL: () =>
9
+ 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=common&response_type=code&scope=openid+profile+email',
9
10
  async isLoggedIn(page: PageInterface): Promise<boolean> {
10
11
  const url = await page.url();
11
- if (url.includes('login.microsoftonline.com') || url.includes('login.microsoft.com')) return false;
12
- return page.evaluate<boolean>("!!(document.querySelector('[data-tid=\"user-email\"]') || document.querySelector('.mectrl_currentAccount') || document.cookie.includes('ESTSAUTH'))");
12
+ if (url.includes('login.microsoftonline.com') || url.includes('login.microsoft.com'))
13
+ return false;
14
+ return page.evaluate<boolean>(
15
+ "!!(document.querySelector('[data-tid=\"user-email\"]') || document.querySelector('.mectrl_currentAccount') || document.cookie.includes('ESTSAUTH'))",
16
+ );
13
17
  },
14
18
  async extractUsername(page: PageInterface): Promise<string> {
15
19
  return page.evaluate<string>(
16
- "document.querySelector('[data-tid=\"user-email\"]')?.textContent?.trim() ?? document.querySelector('.mectrl_headerEmail')?.textContent?.trim() ?? ''"
20
+ "document.querySelector('[data-tid=\"user-email\"]')?.textContent?.trim() ?? document.querySelector('.mectrl_headerEmail')?.textContent?.trim() ?? ''",
17
21
  );
18
22
  },
19
23
  };
@@ -1,5 +1,5 @@
1
1
  // src/browser/adapters/x.ts
2
- import type { PlatformAdapter, PageInterface } from './index.js';
2
+ import type { PageInterface, PlatformAdapter } from './index.js';
3
3
 
4
4
  export const xAdapter: PlatformAdapter = {
5
5
  platform: 'x',
@@ -7,9 +7,13 @@ export const xAdapter: PlatformAdapter = {
7
7
  reservedPaths: ['/home', '/explore', '/notifications', '/messages'],
8
8
  loginURL: () => 'https://x.com/i/flow/login',
9
9
  async isLoggedIn(page: PageInterface): Promise<boolean> {
10
- return page.evaluate<boolean>('!!document.querySelector(\'[data-testid="SideNav_AccountSwitcher_Button"]\')');
10
+ return page.evaluate<boolean>(
11
+ '!!document.querySelector(\'[data-testid="SideNav_AccountSwitcher_Button"]\')',
12
+ );
11
13
  },
12
14
  async extractUsername(page: PageInterface): Promise<string> {
13
- return page.evaluate<string>("document.querySelector('[data-testid=\"SideNav_AccountSwitcher_Button\"] span')?.textContent?.trim() ?? ''");
15
+ return page.evaluate<string>(
16
+ "document.querySelector('[data-testid=\"SideNav_AccountSwitcher_Button\"] span')?.textContent?.trim() ?? ''",
17
+ );
14
18
  },
15
19
  };
@@ -1,14 +1,14 @@
1
- import { spawn, execSync } from 'child_process';
2
- import { existsSync } from 'fs';
3
- import { tmpdir } from 'os';
4
- import { join } from 'path';
5
- import { connect } from 'net';
6
- import { CdpClient, fetchTargets, fetchNewTarget } from './cdp.js';
7
- import type { BrowserConfig, CdpTarget } from './types.js';
8
- import { CHROME_EXECUTABLES } from './types.js';
1
+ import { execSync, spawn } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { connect } from 'node:net';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import { CdpClient, fetchNewTarget, fetchTargets } from './cdp.js';
9
7
  import { enableConsoleCapture, setupConsoleCapture } from './console-log.js';
10
8
  import { setupDialogAutoHandling } from './dialog.js';
11
- import { loadActivePortInfo } from './ref-cache.js';
9
+ import { clearActivePort, loadActivePortInfo } from './ref-cache.js';
10
+ import type { BrowserConfig, CdpTarget } from './types.js';
11
+ import { CHROME_EXECUTABLES } from './types.js';
12
12
 
13
13
  const DEFAULT_PORT = 9222;
14
14
  const LAUNCH_TIMEOUT = 10_000;
@@ -22,6 +22,10 @@ const PROCESS_EXIT_POLL_MS = 50;
22
22
  // A cross-process persisted PID older than this is not trusted for the
23
23
  // SIGKILL fallback in closeBrowser() — see the call site for why.
24
24
  const PERSISTED_PID_MAX_AGE_MS = 24 * 60 * 60 * 1000;
25
+ // A browser WE launched that has had no targets for longer than this is
26
+ // reaped at the next launch/attach — see reapIdleLaunchedBrowser().
27
+ const IDLE_REAP_AFTER_MS = 30 * 60 * 1000;
28
+ const REAP_CONNECT_TIMEOUT_MS = 3000;
25
29
 
26
30
  // Tracks the PID of Chrome instances *we* spawned, keyed by CDP port, so
27
31
  // closeBrowser() has a kill fallback when the graceful `Browser.close` CDP
@@ -53,14 +57,17 @@ function findChrome(executablePath?: string): string {
53
57
  }
54
58
  // Try PATH
55
59
  try {
56
- const result = execSync('which google-chrome chromium-browser chromium microsoft-edge microsoft-edge-stable 2>/dev/null', { encoding: 'utf8' }).trim();
60
+ const result = execSync(
61
+ 'which google-chrome chromium-browser chromium microsoft-edge microsoft-edge-stable 2>/dev/null',
62
+ { encoding: 'utf8' },
63
+ ).trim();
57
64
  const first = result.split('\n')[0];
58
65
  if (first) return first;
59
66
  } catch {
60
67
  // ignore
61
68
  }
62
69
  throw new Error(
63
- 'No supported browser found. Install Google Chrome, Microsoft Edge, or Chromium — or pass executablePath in BrowserConfig.'
70
+ 'No supported browser found. Install Google Chrome, Microsoft Edge, or Chromium — or pass executablePath in BrowserConfig.',
64
71
  );
65
72
  }
66
73
 
@@ -81,7 +88,9 @@ export async function isPortOpen(port: number): Promise<boolean> {
81
88
  */
82
89
  async function isChromeIdentity(port: number): Promise<boolean> {
83
90
  try {
84
- const res = await fetch(`http://127.0.0.1:${port}/json/version`, { signal: AbortSignal.timeout(1000) });
91
+ const res = await fetch(`http://127.0.0.1:${port}/json/version`, {
92
+ signal: AbortSignal.timeout(1000),
93
+ });
85
94
  if (!res.ok) return false;
86
95
  const info = (await res.json()) as { Browser?: string };
87
96
  // Edge's Browser field is "Edg/<version>" with no "chrome"/"chromium"
@@ -124,6 +133,12 @@ export async function launchBrowser(config: BrowserConfig = {}): Promise<number>
124
133
  throw new Error(`Invalid port: ${rawPort}. Must be an integer between 1024 and 65535.`);
125
134
  }
126
135
 
136
+ // Every launch/attach is this tool's only chance to notice that a previous
137
+ // session abandoned a browser on a CDP port and never gave it back — there
138
+ // is no daemon to do it on a timer. Bounded and non-throwing; a stale
139
+ // instance on the port we are about to use is freed before we probe it.
140
+ await reapIdleLaunchedBrowser();
141
+
127
142
  // strictPort: fail fast on the exact requested port, matching the old
128
143
  // behavior (Vite has the same escape hatch for the same reason) — for
129
144
  // callers that treat the error as a signal ("this port is taken by
@@ -133,14 +148,15 @@ export async function launchBrowser(config: BrowserConfig = {}): Promise<number>
133
148
  if (await isChromeIdentity(rawPort)) return rawPort;
134
149
  throw new Error(
135
150
  `Port ${rawPort} is occupied by a process that does not identify as Chrome/Chromium. ` +
136
- `Refusing to attach — pass a different port or free port ${rawPort}.`
151
+ `Refusing to attach — pass a different port or free port ${rawPort}.`,
137
152
  );
138
153
  }
139
154
  return launchOnFreePort(config, rawPort);
140
155
  }
141
156
 
142
157
  const candidates: number[] = [];
143
- for (let i = 0; i < LAUNCH_PORT_SCAN_TRIES && rawPort + i <= 65535; i++) candidates.push(rawPort + i);
158
+ for (let i = 0; i < LAUNCH_PORT_SCAN_TRIES && rawPort + i <= 65535; i++)
159
+ candidates.push(rawPort + i);
144
160
 
145
161
  // Attach-if-already-Chrome only applies to the EXACT requested port — the
146
162
  // original, deliberate, single-port risk ("don't silently take over an
@@ -170,7 +186,7 @@ export async function launchBrowser(config: BrowserConfig = {}): Promise<number>
170
186
  }
171
187
  throw new Error(
172
188
  `Ports ${candidates[0]}-${candidates[candidates.length - 1]} are all occupied and port ${candidates[0]} ` +
173
- `isn't a Chrome/Chromium instance to attach to. Pass a different --port.`
189
+ `isn't a Chrome/Chromium instance to attach to. Pass a different --port.`,
174
190
  );
175
191
  }
176
192
 
@@ -223,7 +239,7 @@ async function launchOnFreePort(config: BrowserConfig, port: number): Promise<nu
223
239
  if (await isChromeIdentity(port)) return port;
224
240
  throw new Error(
225
241
  `Port ${port} is occupied by a CDP-speaking process that does not identify as Chrome/Chromium. ` +
226
- `Refusing to attach — pass a different port or free port ${port}.`
242
+ `Refusing to attach — pass a different port or free port ${port}.`,
227
243
  );
228
244
  }
229
245
  }
@@ -234,7 +250,7 @@ async function launchOnFreePort(config: BrowserConfig, port: number): Promise<nu
234
250
  if (await isTcpPortOpen(port)) {
235
251
  throw new Error(
236
252
  `Port ${port} is occupied by a non-Chrome process (TCP connection succeeds but no CDP response within ${launchTimeout}ms). ` +
237
- `Free the port or pass a different one.`
253
+ `Free the port or pass a different one.`,
238
254
  );
239
255
  }
240
256
 
@@ -254,7 +270,10 @@ export async function enableSessionDomains(client: CdpClient, sessionId: string)
254
270
  setupDialogAutoHandling(client, sessionId);
255
271
  }
256
272
 
257
- export async function connectToTarget(port: number, targetId?: string): Promise<{ client: CdpClient; target: CdpTarget; sessionId: string }> {
273
+ export async function connectToTarget(
274
+ port: number,
275
+ targetId?: string,
276
+ ): Promise<{ client: CdpClient; target: CdpTarget; sessionId: string }> {
258
277
  const targets = await fetchTargets(port);
259
278
  const pageTargets = targets.filter((t) => t.type === 'page');
260
279
 
@@ -306,7 +325,10 @@ export async function closeBrowser(client: CdpClient, port: number): Promise<voi
306
325
  await Promise.race([
307
326
  client.send('Browser.close', {}),
308
327
  new Promise<never>((_, reject) => {
309
- const t = setTimeout(() => reject(new Error('Browser.close timed out')), BROWSER_CLOSE_TIMEOUT_MS);
328
+ const t = setTimeout(
329
+ () => reject(new Error('Browser.close timed out')),
330
+ BROWSER_CLOSE_TIMEOUT_MS,
331
+ );
310
332
  t.unref?.();
311
333
  }),
312
334
  ]);
@@ -351,7 +373,9 @@ export async function closeBrowser(client: CdpClient, port: number): Promise<voi
351
373
  try {
352
374
  process.kill(pid, 0);
353
375
  process.kill(pid, 'SIGKILL');
354
- } catch { /* already exited, or never was — nothing to do */ }
376
+ } catch {
377
+ /* already exited, or never was — nothing to do */
378
+ }
355
379
  return;
356
380
  }
357
381
 
@@ -384,6 +408,90 @@ async function waitForProcessExit(pid: number): Promise<boolean> {
384
408
  }
385
409
  }
386
410
 
411
+ /**
412
+ * Give back the CDP port of a browser we launched that nothing is using any
413
+ * more. A launched Chrome is deliberately detached and unref'd so it outlives
414
+ * the command that spawned it and later commands can reattach — but nothing
415
+ * reaped it afterwards, so an abandoned session kept listening on its port
416
+ * (9222 by convention, which other local tools want) indefinitely. A foreign
417
+ * CDP server *answers* there rather than refusing the connection, so those
418
+ * tools retry the wrong port and report misleading errors instead of moving on.
419
+ *
420
+ * Nothing long-lived exists to run this on a timer — every CLI command is its
421
+ * own short-lived process — so it runs at the next launch/attach instead
422
+ * (see launchBrowser). A browser is reaped only when ALL of these hold:
423
+ * - the persisted session says WE launched it — `connect` records
424
+ * launched:false for a browser that belongs to the user, never touched;
425
+ * - that session is older than IDLE_REAP_AFTER_MS;
426
+ * - the CDP endpoint still answers and reports ZERO targets. An open tab,
427
+ * or a client attached to one (Chrome drops webSocketDebuggerUrl from an
428
+ * attached target but still lists it), means the session is live and
429
+ * reattach-across-commands must keep working.
430
+ *
431
+ * Shutdown goes through closeBrowser() on the browser-level websocket — the
432
+ * only connection left once every target is gone — so a recycled PID can
433
+ * never be signaled by mistake: graceful `Browser.close` reaches whatever is
434
+ * actually listening on that port, at any age, and the persisted PID is only
435
+ * ever used for closeBrowser's own age-bounded force-kill.
436
+ *
437
+ * Never throws: a failed reap must not stop a launch. Returns the reaped
438
+ * port, or null when there was nothing to reap.
439
+ */
440
+ export async function reapIdleLaunchedBrowser(): Promise<number | null> {
441
+ try {
442
+ const session = await loadActivePortInfo();
443
+ if (!session?.launched || session.savedAt === undefined) return null;
444
+ if (Date.now() - session.savedAt < IDLE_REAP_AFTER_MS) return null;
445
+
446
+ const wsUrl = await fetchBrowserWebSocketUrl(session.port);
447
+ if (!wsUrl) return null;
448
+ if ((await fetchTargets(session.port)).length > 0) return null;
449
+
450
+ const client = new CdpClient();
451
+ try {
452
+ // CdpClient.connect() has no timeout of its own — a socket that never
453
+ // opens (and never errors) would hang the launch this reap is running
454
+ // inside of, which is strictly worse than leaving the port squatted.
455
+ await Promise.race([
456
+ client.connect(wsUrl),
457
+ new Promise<never>((_, reject) => {
458
+ const t = setTimeout(
459
+ () => reject(new Error('Reap: CDP connect timed out')),
460
+ REAP_CONNECT_TIMEOUT_MS,
461
+ );
462
+ t.unref?.();
463
+ }),
464
+ ]);
465
+ await closeBrowser(client, session.port);
466
+ } finally {
467
+ try {
468
+ client.close();
469
+ } catch {
470
+ /* already gone */
471
+ }
472
+ }
473
+ launchedPids.delete(session.port);
474
+ launchedUserDataDirs.delete(session.port);
475
+ await clearActivePort();
476
+ return session.port;
477
+ } catch {
478
+ return null;
479
+ }
480
+ }
481
+
482
+ /** Chrome's browser-level debugger endpoint, from /json/version — the
483
+ * connection `Browser.close` belongs on. Null if the port isn't answering as
484
+ * a Chrome/Chromium CDP endpoint (same identity check as isChromeIdentity). */
485
+ async function fetchBrowserWebSocketUrl(port: number): Promise<string | null> {
486
+ const res = await fetch(`http://127.0.0.1:${port}/json/version`, {
487
+ signal: AbortSignal.timeout(1000),
488
+ });
489
+ if (!res.ok) return null;
490
+ const info = (await res.json()) as { Browser?: string; webSocketDebuggerUrl?: string };
491
+ if (typeof info.Browser !== 'string' || !/chrom(e|ium)|edg/i.test(info.Browser)) return null;
492
+ return typeof info.webSocketDebuggerUrl === 'string' ? info.webSocketDebuggerUrl : null;
493
+ }
494
+
387
495
  export async function openUrl(client: CdpClient, sessionId: string, url: string): Promise<void> {
388
496
  // Cap to 2 MB to prevent OOM in CDP message serializer (e.g. data: URI attacks)
389
497
  if (url.length > 2_097_152) throw new Error('URL exceeds 2 MB limit');
@@ -400,14 +508,24 @@ export async function waitForLoad(
400
508
  client: CdpClient,
401
509
  sessionId: string,
402
510
  condition: 'load' | 'networkidle' | 'domcontentloaded' = 'load',
403
- timeout = 30_000
511
+ timeout = 30_000,
404
512
  ): Promise<void> {
405
513
  if (condition === 'load' || condition === 'domcontentloaded') {
406
514
  // Guard against race where the page loads before the listener is registered
407
- const readyExpr = condition === 'load' ? 'document.readyState === "complete"' : 'document.readyState !== "loading"';
408
- const readyCheck = await client.send<{ result: { value?: boolean } }>('Runtime.evaluate', {
409
- expression: readyExpr, returnByValue: true,
410
- }, sessionId).catch(() => ({ result: { value: false } }));
515
+ const readyExpr =
516
+ condition === 'load'
517
+ ? 'document.readyState === "complete"'
518
+ : 'document.readyState !== "loading"';
519
+ const readyCheck = await client
520
+ .send<{ result: { value?: boolean } }>(
521
+ 'Runtime.evaluate',
522
+ {
523
+ expression: readyExpr,
524
+ returnByValue: true,
525
+ },
526
+ sessionId,
527
+ )
528
+ .catch(() => ({ result: { value: false } }));
411
529
  if (readyCheck.result?.value) return;
412
530
 
413
531
  const event = condition === 'load' ? 'Page.loadEventFired' : 'Page.domContentEventFired';
@@ -415,7 +533,10 @@ export async function waitForLoad(
415
533
  let timedOut = false;
416
534
  let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
417
535
  const timeoutPromise = new Promise<void>((resolve) => {
418
- timeoutHandle = setTimeout(() => { timedOut = true; resolve(); }, timeout);
536
+ timeoutHandle = setTimeout(() => {
537
+ timedOut = true;
538
+ resolve();
539
+ }, timeout);
419
540
  });
420
541
  try {
421
542
  await Promise.race([eventPromise, timeoutPromise]);
@@ -435,7 +556,7 @@ async function waitForNetworkIdle(
435
556
  client: CdpClient,
436
557
  sessionId: string,
437
558
  idleMs: number,
438
- timeout: number
559
+ timeout: number,
439
560
  ): Promise<void> {
440
561
  return new Promise((resolve, reject) => {
441
562
  let pending = 0;
@@ -447,9 +568,16 @@ async function waitForNetworkIdle(
447
568
  }, timeout);
448
569
 
449
570
  const cleanup = () => {
450
- if (idleTimer) { clearTimeout(idleTimer); idleTimer = null; }
571
+ if (idleTimer) {
572
+ clearTimeout(idleTimer);
573
+ idleTimer = null;
574
+ }
451
575
  clearTimeout(killTimer);
452
- offReq(); offResp(); offFail(); offCache(); offResp2();
576
+ offReq();
577
+ offResp();
578
+ offFail();
579
+ offCache();
580
+ offResp2();
453
581
  };
454
582
 
455
583
  const settle = () => {
@@ -462,24 +590,34 @@ async function waitForNetworkIdle(
462
590
  if (idleTimer) clearTimeout(idleTimer);
463
591
  idleTimer = setTimeout(settle, idleMs);
464
592
  } else {
465
- if (idleTimer) { clearTimeout(idleTimer); idleTimer = null; }
593
+ if (idleTimer) {
594
+ clearTimeout(idleTimer);
595
+ idleTimer = null;
596
+ }
466
597
  }
467
598
  };
468
599
 
469
600
  const offReq = client.on('Network.requestWillBeSent', (params, sid) => {
470
601
  if (sid !== sessionId) return;
471
602
  const id = params.requestId as string;
472
- if (!inflight.has(id)) { inflight.add(id); pending++; check(); }
603
+ if (!inflight.has(id)) {
604
+ inflight.add(id);
605
+ pending++;
606
+ check();
607
+ }
473
608
  });
474
609
 
475
610
  const decrement = (params: Record<string, unknown>, sid?: string) => {
476
611
  if (sid !== sessionId) return;
477
612
  const id = params.requestId as string;
478
- if (inflight.delete(id)) { pending = Math.max(0, pending - 1); check(); }
613
+ if (inflight.delete(id)) {
614
+ pending = Math.max(0, pending - 1);
615
+ check();
616
+ }
479
617
  };
480
618
 
481
- const offResp = client.on('Network.loadingFinished', decrement);
482
- const offFail = client.on('Network.loadingFailed', decrement);
619
+ const offResp = client.on('Network.loadingFinished', decrement);
620
+ const offFail = client.on('Network.loadingFailed', decrement);
483
621
  const offCache = client.on('Network.requestServedFromCache', decrement);
484
622
  // Guard against requests that never fire loadingFinished/loadingFailed (e.g. data: URLs)
485
623
  // Skip 3xx redirect responses — the request continues under the same requestId
@@ -494,18 +632,26 @@ async function waitForNetworkIdle(
494
632
  }
495
633
 
496
634
  export async function getCurrentUrl(client: CdpClient, sessionId: string): Promise<string> {
497
- const result = await client.send<{ result: { value: string } }>('Runtime.evaluate', {
498
- expression: 'location.href',
499
- returnByValue: true,
500
- }, sessionId);
635
+ const result = await client.send<{ result: { value: string } }>(
636
+ 'Runtime.evaluate',
637
+ {
638
+ expression: 'location.href',
639
+ returnByValue: true,
640
+ },
641
+ sessionId,
642
+ );
501
643
  return result.result?.value ?? '';
502
644
  }
503
645
 
504
646
  export async function getCurrentTitle(client: CdpClient, sessionId: string): Promise<string> {
505
- const result = await client.send<{ result: { value: string } }>('Runtime.evaluate', {
506
- expression: 'document.title',
507
- returnByValue: true,
508
- }, sessionId);
647
+ const result = await client.send<{ result: { value: string } }>(
648
+ 'Runtime.evaluate',
649
+ {
650
+ expression: 'document.title',
651
+ returnByValue: true,
652
+ },
653
+ sessionId,
654
+ );
509
655
  return result.result?.value ?? '';
510
656
  }
511
657
 
@@ -13,8 +13,18 @@ export const DEFAULT_CDP_SEND_TIMEOUT_MS = 30_000;
13
13
 
14
14
  export class CdpClient {
15
15
  private ws: WebSocket | null = null;
16
- private pendingCommands = new Map<number, { resolve: (r: CdpResponse) => void; reject: (e: Error) => void; timer?: ReturnType<typeof setTimeout> }>();
17
- private eventListeners = new Map<string, Set<(params: Record<string, unknown>, sessionId?: string) => void>>();
16
+ private pendingCommands = new Map<
17
+ number,
18
+ {
19
+ resolve: (r: CdpResponse) => void;
20
+ reject: (e: Error) => void;
21
+ timer?: ReturnType<typeof setTimeout>;
22
+ }
23
+ >();
24
+ private eventListeners = new Map<
25
+ string,
26
+ Set<(params: Record<string, unknown>, sessionId?: string) => void>
27
+ >();
18
28
  private nextId = 1;
19
29
  private connected = false;
20
30
 
@@ -73,7 +83,11 @@ export class CdpClient {
73
83
  const listeners = this.eventListeners.get(msg.method);
74
84
  if (listeners) {
75
85
  for (const fn of listeners) {
76
- try { fn(msg.params ?? {}, msg.sessionId); } catch { /* isolate per-listener errors */ }
86
+ try {
87
+ fn(msg.params ?? {}, msg.sessionId);
88
+ } catch {
89
+ /* isolate per-listener errors */
90
+ }
77
91
  }
78
92
  }
79
93
  }
@@ -88,7 +102,7 @@ export class CdpClient {
88
102
  method: string,
89
103
  params?: Record<string, unknown>,
90
104
  sessionId?: string,
91
- timeoutMs: number = DEFAULT_CDP_SEND_TIMEOUT_MS
105
+ timeoutMs: number = DEFAULT_CDP_SEND_TIMEOUT_MS,
92
106
  ): Promise<T> {
93
107
  return new Promise((resolve, reject) => {
94
108
  if (!this.ws || !this.connected) {
@@ -114,8 +128,14 @@ export class CdpClient {
114
128
  }
115
129
 
116
130
  this.pendingCommands.set(id, {
117
- resolve: (r) => { if (timer) clearTimeout(timer); resolve((r.result ?? {}) as T); },
118
- reject: (e) => { if (timer) clearTimeout(timer); reject(e); },
131
+ resolve: (r) => {
132
+ if (timer) clearTimeout(timer);
133
+ resolve((r.result ?? {}) as T);
134
+ },
135
+ reject: (e) => {
136
+ if (timer) clearTimeout(timer);
137
+ reject(e);
138
+ },
119
139
  timer,
120
140
  });
121
141
  this.ws.send(JSON.stringify(cmd), (err) => {
@@ -188,7 +208,9 @@ export async function fetchTargets(port: number): Promise<CdpTarget[]> {
188
208
 
189
209
  export async function fetchNewTarget(port: number, url: string): Promise<CdpTarget> {
190
210
  // Chrome v92+ requires PUT for /json/new; GET returns 405. URL must be encoded.
191
- const res = await fetch(`http://127.0.0.1:${port}/json/new?${encodeURIComponent(url)}`, { method: 'PUT' });
211
+ const res = await fetch(`http://127.0.0.1:${port}/json/new?${encodeURIComponent(url)}`, {
212
+ method: 'PUT',
213
+ });
192
214
  if (!res.ok) throw new Error(`Failed to create target: ${res.statusText}`);
193
215
  return readCdpJson(res) as Promise<CdpTarget>;
194
216
  }
@@ -203,7 +225,10 @@ export async function createBrowserPage(url: string, port = 9222): Promise<Brows
203
225
  const target = await fetchNewTarget(port, url);
204
226
  const wsDebuggerUrl = target.webSocketDebuggerUrl;
205
227
  if (!wsDebuggerUrl) {
206
- throw new Error('Chrome did not return a WebSocket debugger URL. Ensure Chrome is running with --remote-debugging-port=' + port);
228
+ throw new Error(
229
+ 'Chrome did not return a WebSocket debugger URL. Ensure Chrome is running with --remote-debugging-port=' +
230
+ port,
231
+ );
207
232
  }
208
233
 
209
234
  const client = new CdpClient();
@@ -218,7 +243,9 @@ export async function createBrowserPage(url: string, port = 9222): Promise<Brows
218
243
  await Promise.race([
219
244
  loadPromise,
220
245
  new Promise<void>((_, reject) =>
221
- AbortSignal.timeout(30_000).addEventListener('abort', () => reject(new Error('Page load timeout after 30s')))
246
+ AbortSignal.timeout(30_000).addEventListener('abort', () =>
247
+ reject(new Error('Page load timeout after 30s')),
248
+ ),
222
249
  ),
223
250
  ]);
224
251
  } catch (err) {
@@ -228,7 +255,9 @@ export async function createBrowserPage(url: string, port = 9222): Promise<Brows
228
255
 
229
256
  const closeTab = async (): Promise<void> => {
230
257
  try {
231
- await fetch(`http://127.0.0.1:${port}/json/close/${encodeURIComponent(target.id)}`, { method: 'GET' });
258
+ await fetch(`http://127.0.0.1:${port}/json/close/${encodeURIComponent(target.id)}`, {
259
+ method: 'GET',
260
+ });
232
261
  } catch {
233
262
  // best-effort: tab may already be gone
234
263
  }
@@ -243,7 +272,10 @@ export async function createBrowserPage(url: string, port = 9222): Promise<Brows
243
272
  return result.result.value;
244
273
  },
245
274
  evaluate: async <T>(expression: string): Promise<T> => {
246
- const result = await client.send<{ result: { value: T; }; exceptionDetails?: { text: string } }>('Runtime.evaluate', {
275
+ const result = await client.send<{
276
+ result: { value: T };
277
+ exceptionDetails?: { text: string };
278
+ }>('Runtime.evaluate', {
247
279
  expression,
248
280
  returnByValue: true,
249
281
  });