@weavetab/mcp 2.5.0-beta.0

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 (281) hide show
  1. package/AGENTS.md +48 -0
  2. package/ARCHITECTURE.md +590 -0
  3. package/LICENSE +631 -0
  4. package/README.md +909 -0
  5. package/SECURITY.md +196 -0
  6. package/TROUBLESHOOTING.md +365 -0
  7. package/dist/anticaptcha/detector.d.ts +49 -0
  8. package/dist/anticaptcha/detector.js +465 -0
  9. package/dist/anticaptcha/integration.d.ts +36 -0
  10. package/dist/anticaptcha/integration.js +123 -0
  11. package/dist/anticaptcha/solver.d.ts +27 -0
  12. package/dist/anticaptcha/solver.js +552 -0
  13. package/dist/audit/logger.d.ts +13 -0
  14. package/dist/audit/logger.js +177 -0
  15. package/dist/audit/telemetry.d.ts +36 -0
  16. package/dist/audit/telemetry.js +162 -0
  17. package/dist/cdp/bridge.d.ts +16 -0
  18. package/dist/cdp/bridge.js +826 -0
  19. package/dist/cdp/confirm.d.ts +26 -0
  20. package/dist/cdp/confirm.js +79 -0
  21. package/dist/cdp/connector.d.ts +37 -0
  22. package/dist/cdp/connector.js +661 -0
  23. package/dist/cdp/extension.d.ts +36 -0
  24. package/dist/cdp/extension.js +118 -0
  25. package/dist/cdp/finder.d.ts +1 -0
  26. package/dist/cdp/finder.js +2 -0
  27. package/dist/cdp/helpers.d.ts +11 -0
  28. package/dist/cdp/helpers.js +195 -0
  29. package/dist/cdp/label_cache.d.ts +12 -0
  30. package/dist/cdp/label_cache.js +51 -0
  31. package/dist/cdp/lock.d.ts +33 -0
  32. package/dist/cdp/lock.js +206 -0
  33. package/dist/cdp/omni_broker.d.ts +17 -0
  34. package/dist/cdp/omni_broker.js +119 -0
  35. package/dist/cdp/profiles.d.ts +1 -0
  36. package/dist/cdp/profiles.js +87 -0
  37. package/dist/cdp/pruner.d.ts +41 -0
  38. package/dist/cdp/pruner.js +263 -0
  39. package/dist/cdp/ref_cache.d.ts +101 -0
  40. package/dist/cdp/ref_cache.js +234 -0
  41. package/dist/cdp/retry.d.ts +5 -0
  42. package/dist/cdp/retry.js +39 -0
  43. package/dist/cdp/retry_enhanced.d.ts +44 -0
  44. package/dist/cdp/retry_enhanced.js +102 -0
  45. package/dist/cdp/security.d.ts +50 -0
  46. package/dist/cdp/security.js +237 -0
  47. package/dist/cdp/verify.d.ts +39 -0
  48. package/dist/cdp/verify.js +143 -0
  49. package/dist/cdp/walker.d.ts +91 -0
  50. package/dist/cdp/walker.js +896 -0
  51. package/dist/cdp/walker.test.d.ts +2 -0
  52. package/dist/cdp/walker.test.js +172 -0
  53. package/dist/cdp/websocket_transport.d.ts +15 -0
  54. package/dist/cdp/websocket_transport.js +118 -0
  55. package/dist/cli/plugin.d.ts +9 -0
  56. package/dist/cli/plugin.js +1134 -0
  57. package/dist/cli/updater.d.ts +1 -0
  58. package/dist/cli/updater.js +72 -0
  59. package/dist/config/loader.d.ts +120 -0
  60. package/dist/config/loader.js +704 -0
  61. package/dist/index.d.ts +2 -0
  62. package/dist/index.js +1248 -0
  63. package/dist/intelligence/hints.d.ts +11 -0
  64. package/dist/intelligence/hints.js +39 -0
  65. package/dist/intelligence/intent.d.ts +33 -0
  66. package/dist/intelligence/intent.js +171 -0
  67. package/dist/intelligence/sessionMemory.d.ts +66 -0
  68. package/dist/intelligence/sessionMemory.js +284 -0
  69. package/dist/intelligence/sessionMemory.test.d.ts +2 -0
  70. package/dist/intelligence/sessionMemory.test.js +62 -0
  71. package/dist/intelligence/trail.d.ts +64 -0
  72. package/dist/intelligence/trail.js +233 -0
  73. package/dist/intelligence/trail.test.d.ts +2 -0
  74. package/dist/intelligence/trail.test.js +110 -0
  75. package/dist/intelligence/vision.d.ts +24 -0
  76. package/dist/intelligence/vision.js +151 -0
  77. package/dist/lifecycle/mission_manager.d.ts +16 -0
  78. package/dist/lifecycle/mission_manager.js +44 -0
  79. package/dist/overlay/bubble.d.ts +16 -0
  80. package/dist/overlay/bubble.js +37 -0
  81. package/dist/overlay/ghost.d.ts +35 -0
  82. package/dist/overlay/ghost.js +508 -0
  83. package/dist/overlay/injector.d.ts +22 -0
  84. package/dist/overlay/injector.js +198 -0
  85. package/dist/plugin/interface.d.ts +134 -0
  86. package/dist/plugin/interface.js +1 -0
  87. package/dist/plugin/loader.d.ts +18 -0
  88. package/dist/plugin/loader.js +155 -0
  89. package/dist/plugin/registry.d.ts +50 -0
  90. package/dist/plugin/registry.js +234 -0
  91. package/dist/security/blacklist.d.ts +24 -0
  92. package/dist/security/blacklist.js +80 -0
  93. package/dist/security/filter.d.ts +2 -0
  94. package/dist/security/filter.js +20 -0
  95. package/dist/security/guard.d.ts +8 -0
  96. package/dist/security/guard.js +35 -0
  97. package/dist/security/ratelimit.d.ts +11 -0
  98. package/dist/security/ratelimit.js +57 -0
  99. package/dist/security/secrets.d.ts +75 -0
  100. package/dist/security/secrets.js +365 -0
  101. package/dist/sensors/actionability.d.ts +81 -0
  102. package/dist/sensors/actionability.js +303 -0
  103. package/dist/sensors/guard.d.ts +25 -0
  104. package/dist/sensors/guard.js +168 -0
  105. package/dist/sensors/mutations.d.ts +23 -0
  106. package/dist/sensors/mutations.js +93 -0
  107. package/dist/sensors/network.d.ts +67 -0
  108. package/dist/sensors/network.js +193 -0
  109. package/dist/sensors/operation_monitor.d.ts +80 -0
  110. package/dist/sensors/operation_monitor.js +203 -0
  111. package/dist/sensors/pacing.d.ts +44 -0
  112. package/dist/sensors/pacing.js +111 -0
  113. package/dist/sensors/thoughts.d.ts +2 -0
  114. package/dist/sensors/thoughts.js +59 -0
  115. package/dist/sensors/wait.d.ts +12 -0
  116. package/dist/sensors/wait.js +44 -0
  117. package/dist/server.d.ts +7 -0
  118. package/dist/server.js +1687 -0
  119. package/dist/state/agent.d.ts +36 -0
  120. package/dist/state/agent.js +101 -0
  121. package/dist/state/cache.d.ts +16 -0
  122. package/dist/state/cache.js +92 -0
  123. package/dist/state/cache.test.d.ts +2 -0
  124. package/dist/state/cache.test.js +76 -0
  125. package/dist/state/delta.d.ts +71 -0
  126. package/dist/state/delta.js +234 -0
  127. package/dist/state/delta.test.d.ts +2 -0
  128. package/dist/state/delta.test.js +279 -0
  129. package/dist/state/groups.d.ts +15 -0
  130. package/dist/state/groups.js +80 -0
  131. package/dist/state/session.d.ts +30 -0
  132. package/dist/state/session.js +155 -0
  133. package/dist/tools/accessibility.d.ts +14 -0
  134. package/dist/tools/accessibility.js +32 -0
  135. package/dist/tools/ask.d.ts +14 -0
  136. package/dist/tools/ask.js +49 -0
  137. package/dist/tools/automation.d.ts +36 -0
  138. package/dist/tools/automation.js +218 -0
  139. package/dist/tools/browser_detect.d.ts +13 -0
  140. package/dist/tools/browser_detect.js +73 -0
  141. package/dist/tools/burst.d.ts +35 -0
  142. package/dist/tools/burst.js +344 -0
  143. package/dist/tools/canvas.d.ts +25 -0
  144. package/dist/tools/canvas.js +132 -0
  145. package/dist/tools/captcha.d.ts +38 -0
  146. package/dist/tools/captcha.js +209 -0
  147. package/dist/tools/click.d.ts +36 -0
  148. package/dist/tools/click.js +561 -0
  149. package/dist/tools/click_and_wait.d.ts +33 -0
  150. package/dist/tools/click_and_wait.js +149 -0
  151. package/dist/tools/clipboard.d.ts +29 -0
  152. package/dist/tools/clipboard.js +279 -0
  153. package/dist/tools/console.d.ts +21 -0
  154. package/dist/tools/console.js +107 -0
  155. package/dist/tools/cookies.d.ts +67 -0
  156. package/dist/tools/cookies.js +121 -0
  157. package/dist/tools/design_extract.d.ts +109 -0
  158. package/dist/tools/design_extract.js +641 -0
  159. package/dist/tools/dialog.d.ts +24 -0
  160. package/dist/tools/dialog.js +109 -0
  161. package/dist/tools/drag.d.ts +15 -0
  162. package/dist/tools/drag.js +29 -0
  163. package/dist/tools/emulation.d.ts +33 -0
  164. package/dist/tools/emulation.js +69 -0
  165. package/dist/tools/eval.d.ts +20 -0
  166. package/dist/tools/eval.js +92 -0
  167. package/dist/tools/fill.d.ts +41 -0
  168. package/dist/tools/fill.js +667 -0
  169. package/dist/tools/find.d.ts +30 -0
  170. package/dist/tools/find.js +60 -0
  171. package/dist/tools/geo.d.ts +19 -0
  172. package/dist/tools/geo.js +29 -0
  173. package/dist/tools/github/analyze.d.ts +4 -0
  174. package/dist/tools/github/analyze.js +113 -0
  175. package/dist/tools/github/cache.d.ts +21 -0
  176. package/dist/tools/github/cache.js +28 -0
  177. package/dist/tools/github/index.d.ts +4 -0
  178. package/dist/tools/github/index.js +4 -0
  179. package/dist/tools/github/issues.d.ts +9 -0
  180. package/dist/tools/github/issues.js +88 -0
  181. package/dist/tools/github/pr.d.ts +5 -0
  182. package/dist/tools/github/pr.js +88 -0
  183. package/dist/tools/github/rateLimiter.d.ts +4 -0
  184. package/dist/tools/github/rateLimiter.js +46 -0
  185. package/dist/tools/github/read.d.ts +34 -0
  186. package/dist/tools/github/read.js +74 -0
  187. package/dist/tools/github/repoContext.d.ts +20 -0
  188. package/dist/tools/github/repoContext.js +129 -0
  189. package/dist/tools/highlight.d.ts +20 -0
  190. package/dist/tools/highlight.js +140 -0
  191. package/dist/tools/hover.d.ts +19 -0
  192. package/dist/tools/hover.js +57 -0
  193. package/dist/tools/inspect.d.ts +17 -0
  194. package/dist/tools/inspect.js +125 -0
  195. package/dist/tools/integration.test.d.ts +2 -0
  196. package/dist/tools/integration.test.js +96 -0
  197. package/dist/tools/key.d.ts +25 -0
  198. package/dist/tools/key.js +173 -0
  199. package/dist/tools/macro_compiler.d.ts +7 -0
  200. package/dist/tools/macro_compiler.js +82 -0
  201. package/dist/tools/mouse.d.ts +21 -0
  202. package/dist/tools/mouse.js +62 -0
  203. package/dist/tools/navigate.d.ts +16 -0
  204. package/dist/tools/navigate.js +188 -0
  205. package/dist/tools/network_intercept.d.ts +17 -0
  206. package/dist/tools/network_intercept.js +151 -0
  207. package/dist/tools/office/index.d.ts +9 -0
  208. package/dist/tools/office/index.js +7 -0
  209. package/dist/tools/office/list.d.ts +15 -0
  210. package/dist/tools/office/list.js +41 -0
  211. package/dist/tools/office/parse.d.ts +20 -0
  212. package/dist/tools/office/parse.js +172 -0
  213. package/dist/tools/pdf.d.ts +22 -0
  214. package/dist/tools/pdf.js +42 -0
  215. package/dist/tools/performance.d.ts +12 -0
  216. package/dist/tools/performance.js +47 -0
  217. package/dist/tools/performance_test.d.ts +70 -0
  218. package/dist/tools/performance_test.js +128 -0
  219. package/dist/tools/plan.d.ts +39 -0
  220. package/dist/tools/plan.js +161 -0
  221. package/dist/tools/pointer.d.ts +35 -0
  222. package/dist/tools/pointer.js +522 -0
  223. package/dist/tools/read.d.ts +30 -0
  224. package/dist/tools/read.js +488 -0
  225. package/dist/tools/recording.d.ts +45 -0
  226. package/dist/tools/recording.js +124 -0
  227. package/dist/tools/response.d.ts +129 -0
  228. package/dist/tools/response.js +5 -0
  229. package/dist/tools/scrape.d.ts +24 -0
  230. package/dist/tools/scrape.js +140 -0
  231. package/dist/tools/screenshot.d.ts +40 -0
  232. package/dist/tools/screenshot.js +79 -0
  233. package/dist/tools/scroll.d.ts +23 -0
  234. package/dist/tools/scroll.js +196 -0
  235. package/dist/tools/select.d.ts +13 -0
  236. package/dist/tools/select.js +217 -0
  237. package/dist/tools/snapshot.d.ts +27 -0
  238. package/dist/tools/snapshot.js +99 -0
  239. package/dist/tools/storage.d.ts +18 -0
  240. package/dist/tools/storage.js +29 -0
  241. package/dist/tools/tabs.d.ts +77 -0
  242. package/dist/tools/tabs.js +342 -0
  243. package/dist/tools/thought.d.ts +16 -0
  244. package/dist/tools/thought.js +79 -0
  245. package/dist/tools/type.d.ts +65 -0
  246. package/dist/tools/type.js +1061 -0
  247. package/dist/tools/type_secret.d.ts +23 -0
  248. package/dist/tools/type_secret.js +229 -0
  249. package/dist/tools/upload.d.ts +19 -0
  250. package/dist/tools/upload.js +103 -0
  251. package/dist/tools/video_compiler.d.ts +24 -0
  252. package/dist/tools/video_compiler.js +199 -0
  253. package/dist/tools/viewport.d.ts +44 -0
  254. package/dist/tools/viewport.js +230 -0
  255. package/dist/tools/wait.d.ts +19 -0
  256. package/dist/tools/wait.js +202 -0
  257. package/dist/tools/wait_for.d.ts +23 -0
  258. package/dist/tools/wait_for.js +30 -0
  259. package/dist/tools/windows.d.ts +22 -0
  260. package/dist/tools/windows.js +57 -0
  261. package/dist/ui/index.d.ts +1 -0
  262. package/dist/ui/index.js +12 -0
  263. package/docs/DEV_MODE.md +68 -0
  264. package/docs/DOCUMENTATION.md +532 -0
  265. package/docs/PLUGINS.md +538 -0
  266. package/docs/TOOLS.md +104 -0
  267. package/docs/dev_plugin.md +310 -0
  268. package/ext-dist/background.js +467 -0
  269. package/ext-dist/content.js +3089 -0
  270. package/ext-dist/dashboard.html +583 -0
  271. package/ext-dist/hud/hud.js +73 -0
  272. package/ext-dist/hud/index.html +20 -0
  273. package/ext-dist/manifest.json +77 -0
  274. package/ext-dist/sidepanel/sidepanel.js +25 -0
  275. package/ext-dist/sidepanel.html +547 -0
  276. package/ext-dist/styles.css +218 -0
  277. package/icon.png +0 -0
  278. package/mcp.json +796 -0
  279. package/package.json +113 -0
  280. package/server.json +23 -0
  281. package/smithery.yaml +11 -0
@@ -0,0 +1,826 @@
1
+ import { logDebug } from "../audit/logger.js";
2
+ /**
3
+ * Weavetab — cdp/bridge.ts
4
+ * Browser process launcher: sentinel lock, profile resolution, stderr port parsing.
5
+ * V3 — includes 10s stderr timeout, stealth injection.
6
+ * Weavetab by fy2ne
7
+ */
8
+ import { spawn, execSync, execFile } from "node:child_process";
9
+ import * as fs from "node:fs";
10
+ import * as os from "node:os";
11
+ import path from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+ import CDP from "chrome-remote-interface";
14
+ import { readLock, writeLock, clearLock, isLockValid, isLockPending } from "./lock.js";
15
+ import { getSessionProfile } from "./profiles.js";
16
+ import { getOutputDir, getDownloadDir, getConfigHeadless, getConfigProxy, getConfigDisableExtensions, getConfigDevMode, getConfigBlock, getConfigAllow, getConfigBrowserType, getConfigBrowserPath, getConfigBrowserFlags, getConfigCookies, getConfigProfileDir } from "../config/loader.js";
17
+ import { createBrowserFingerprint } from "./security.js";
18
+ function withCDPTimeout(promise, ms, label) {
19
+ return Promise.race([
20
+ promise,
21
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms)),
22
+ ]);
23
+ }
24
+ const __filename = fileURLToPath(import.meta.url);
25
+ const __dirname = path.dirname(__filename);
26
+ const BROWSER_PRIORITY = [
27
+ "Google Chrome",
28
+ "Brave",
29
+ "Microsoft Edge",
30
+ "Chromium"
31
+ ];
32
+ // ─── Browser Path Validation ──────────────────────────────────────────────────────
33
+ // Validate that the browser path is a known safe browser executable
34
+ function isValidBrowserPath(browserPath) {
35
+ const platform = os.platform();
36
+ const normalizedPath = browserPath.toLowerCase();
37
+ // Known safe browser binary names
38
+ const safeBinaries = [
39
+ 'chrome.exe', 'chrome', 'google chrome',
40
+ 'brave.exe', 'brave', 'brave browser',
41
+ 'msedge.exe', 'msedge', 'microsoft edge',
42
+ 'chromium.exe', 'chromium',
43
+ 'firefox.exe', 'firefox',
44
+ 'firefox.app', 'google chrome.app', 'brave browser.app', 'microsoft edge.app'
45
+ ];
46
+ // Check if the path contains a known browser binary name
47
+ const hasSafeBinary = safeBinaries.some(binary => normalizedPath.includes(binary) ||
48
+ normalizedPath.endsWith(binary));
49
+ if (!hasSafeBinary) {
50
+ logDebug(`[SECURITY] Browser path validation failed: unknown binary in path`);
51
+ return false;
52
+ }
53
+ // Check for suspicious patterns that might indicate malicious paths
54
+ const suspiciousPatterns = [
55
+ '..', // path traversal
56
+ '%', // URL encoding (suspicious in paths)
57
+ 'cmd.exe', 'powershell.exe', 'bash', 'sh', // shell executables
58
+ 'temp', 'tmp', // temporary directories
59
+ 'download', // downloads directory
60
+ ];
61
+ const hasSuspiciousPattern = suspiciousPatterns.some(pattern => normalizedPath.includes(pattern));
62
+ if (hasSuspiciousPattern) {
63
+ logDebug(`[SECURITY] Browser path validation failed: suspicious pattern in path`);
64
+ return false;
65
+ }
66
+ // On Windows, check that the path is in a known safe location
67
+ if (platform === 'win32') {
68
+ const safeLocations = [
69
+ 'program files',
70
+ 'program files (x86)',
71
+ 'localappdata',
72
+ 'programdata',
73
+ 'c:\\users\\'
74
+ ];
75
+ const isInSafeLocation = safeLocations.some(location => normalizedPath.includes(location) ||
76
+ normalizedPath.startsWith('c:\\'));
77
+ if (!isInSafeLocation) {
78
+ logDebug(`[SECURITY] Browser path validation failed: not in safe location`);
79
+ return false;
80
+ }
81
+ }
82
+ // On macOS, check that the path is in /Applications
83
+ if (platform === 'darwin') {
84
+ if (!normalizedPath.startsWith('/applications/') && !normalizedPath.startsWith('/usr/local/bin/')) {
85
+ logDebug(`[SECURITY] Browser path validation failed: not in safe location`);
86
+ return false;
87
+ }
88
+ }
89
+ // On Linux, check that the path is in a safe location
90
+ if (platform === 'linux') {
91
+ const safeLocations = ['/usr/bin/', '/usr/local/bin/', '/snap/bin/', '/opt/'];
92
+ const isInSafeLocation = safeLocations.some(location => normalizedPath.startsWith(location));
93
+ if (!isInSafeLocation) {
94
+ logDebug(`[SECURITY] Browser path validation failed: not in safe location`);
95
+ return false;
96
+ }
97
+ }
98
+ return true;
99
+ }
100
+ function pathsForPlatform(browserName) {
101
+ const plat = os.platform();
102
+ const winPaths = [];
103
+ const macPaths = [];
104
+ const linuxPaths = [];
105
+ const localAppData = process.env.LOCALAPPDATA || "";
106
+ const programFiles = process.env.PROGRAMFILES || "";
107
+ const programFilesX86 = process.env["PROGRAMFILES(X86)"] || "";
108
+ if (browserName === "Google Chrome") {
109
+ winPaths.push(`${programFiles}\\Google\\Chrome\\Application\\chrome.exe`, `${programFilesX86}\\Google\\Chrome\\Application\\chrome.exe`, `${localAppData}\\Google\\Chrome\\Application\\chrome.exe`);
110
+ macPaths.push("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome");
111
+ linuxPaths.push("/usr/bin/google-chrome", "/usr/bin/google-chrome-stable", "/usr/local/bin/google-chrome");
112
+ }
113
+ else if (browserName === "Brave") {
114
+ winPaths.push(`${programFiles}\\BraveSoftware\\Brave-Browser\\Application\\brave.exe`, `${programFilesX86}\\BraveSoftware\\Brave-Browser\\Application\\brave.exe`, `${localAppData}\\BraveSoftware\\Brave-Browser\\Application\\brave.exe`);
115
+ macPaths.push("/Applications/Brave Browser.app/Contents/MacOS/Brave Browser");
116
+ linuxPaths.push("/usr/bin/brave-browser", "/usr/bin/brave");
117
+ }
118
+ else if (browserName === "Microsoft Edge") {
119
+ winPaths.push(`${programFilesX86}\\Microsoft\\Edge\\Application\\msedge.exe`, `${programFiles}\\Microsoft\\Edge\\Application\\msedge.exe`, `${localAppData}\\Microsoft\\Edge\\Application\\msedge.exe`);
120
+ macPaths.push("/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge");
121
+ linuxPaths.push("/usr/bin/microsoft-edge", "/usr/bin/microsoft-edge-stable");
122
+ }
123
+ else if (browserName === "Chromium") {
124
+ macPaths.push("/Applications/Chromium.app/Contents/MacOS/Chromium");
125
+ linuxPaths.push("/usr/bin/chromium", "/usr/bin/chromium-browser", "/snap/bin/chromium");
126
+ }
127
+ if (plat === "win32")
128
+ return winPaths.filter(Boolean);
129
+ if (plat === "darwin")
130
+ return macPaths;
131
+ return linuxPaths;
132
+ }
133
+ export function findBrowser(preferred) {
134
+ const platform = os.platform();
135
+ if (preferred && (preferred.startsWith("/") || preferred.includes("/") || preferred.includes("\\"))) {
136
+ const normalizedPath = platform === 'win32'
137
+ ? preferred.replace(/\\/g, '\\')
138
+ : preferred.replace(/\\\\/g, '/');
139
+ if (fs.existsSync(normalizedPath)) {
140
+ // Validate the configured browser path
141
+ if (!isValidBrowserPath(normalizedPath)) {
142
+ logDebug(`[SECURITY] Configured browser path failed validation: ${normalizedPath}`);
143
+ return null;
144
+ }
145
+ const lowerPath = normalizedPath.toLowerCase();
146
+ let browserName = "Custom";
147
+ if (lowerPath.includes("edge") || lowerPath.includes("msedge"))
148
+ browserName = "Microsoft Edge";
149
+ else if (lowerPath.includes("chrome"))
150
+ browserName = "Google Chrome";
151
+ else if (lowerPath.includes("brave"))
152
+ browserName = "Brave";
153
+ else if (lowerPath.includes("chromium"))
154
+ browserName = "Chromium";
155
+ logDebug(`[Weavetab] Using configured browser path: ${browserName} at ${normalizedPath}`);
156
+ return { name: browserName, path: normalizedPath };
157
+ }
158
+ else {
159
+ logDebug(`[Weavetab] Configured browser path does not exist: ${normalizedPath}`);
160
+ }
161
+ }
162
+ const list = preferred
163
+ ? [
164
+ ...BROWSER_PRIORITY.filter((name) => name.toLowerCase().includes(preferred.toLowerCase())),
165
+ ...BROWSER_PRIORITY.filter((name) => !name.toLowerCase().includes(preferred.toLowerCase())),
166
+ ]
167
+ : BROWSER_PRIORITY;
168
+ for (const name of list) {
169
+ for (const p of pathsForPlatform(name)) {
170
+ if (p && fs.existsSync(p)) {
171
+ logDebug(`[Weavetab] Auto-detected browser: ${name} at ${p}`);
172
+ return { name, path: p };
173
+ }
174
+ }
175
+ }
176
+ try {
177
+ let cmd = "";
178
+ if (platform === "win32") {
179
+ cmd = "where chrome.exe brave.exe msedge.exe";
180
+ }
181
+ else if (platform === "darwin") {
182
+ cmd = "which /Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome /Applications/Brave\\\\ Browser.app/Contents/MacOS/Brave\\\\ Browser /Applications/Microsoft\\\\ Edge.app/Contents/MacOS/Microsoft\\\\ Edge";
183
+ }
184
+ else {
185
+ cmd = "which google-chrome brave-browser microsoft-edge chromium";
186
+ }
187
+ const result = execSync(cmd, { stdio: ["ignore", "pipe", "ignore"] }).toString().trim().split("\n")[0];
188
+ if (result && fs.existsSync(result)) {
189
+ logDebug(`[Weavetab] Fallback browser found via system: ${result}`);
190
+ return { name: "System Browser", path: result };
191
+ }
192
+ }
193
+ catch { /* ignore */ }
194
+ return null;
195
+ }
196
+ export function findFirefox() {
197
+ const plat = os.platform();
198
+ const paths = [];
199
+ const programFiles = process.env.PROGRAMFILES || "";
200
+ const programFilesX86 = process.env["PROGRAMFILES(X86)"] || "";
201
+ const localAppData = process.env.LOCALAPPDATA || "";
202
+ if (plat === "win32") {
203
+ paths.push(`${programFiles}\\Mozilla Firefox\\firefox.exe`, `${programFilesX86}\\Mozilla Firefox\\firefox.exe`, `${localAppData}\\Mozilla Firefox\\firefox.exe`);
204
+ }
205
+ else if (plat === "darwin") {
206
+ paths.push("/Applications/Firefox.app/Contents/MacOS/firefox");
207
+ }
208
+ else {
209
+ paths.push("/usr/bin/firefox", "/usr/local/bin/firefox", "/snap/bin/firefox");
210
+ }
211
+ for (const p of paths) {
212
+ if (p && fs.existsSync(p)) {
213
+ return { name: "Firefox", path: p };
214
+ }
215
+ }
216
+ try {
217
+ const cmd = plat === "win32" ? "where firefox" : "which firefox";
218
+ const result = execSync(cmd, { stdio: ["ignore", "pipe", "ignore"] }).toString().trim().split("\n")[0];
219
+ if (result && fs.existsSync(result))
220
+ return { name: "Firefox", path: result };
221
+ }
222
+ catch { /* ignore */ }
223
+ return null;
224
+ }
225
+ export function probeAllBrowsers() {
226
+ const results = [];
227
+ for (const name of BROWSER_PRIORITY) {
228
+ for (const p of pathsForPlatform(name)) {
229
+ if (p && fs.existsSync(p)) {
230
+ results.push({ name, path: p });
231
+ break;
232
+ }
233
+ }
234
+ }
235
+ return results;
236
+ }
237
+ // ─── Process killing helper (Cross-platform) ─────────────────────────────
238
+ function killProcess(pid) {
239
+ const platform = os.platform();
240
+ try {
241
+ if (platform === 'win32') {
242
+ execFile('taskkill', ['/F', '/PID', String(pid)], () => { });
243
+ }
244
+ else {
245
+ execFile('kill', ['-9', String(pid)], () => { });
246
+ }
247
+ }
248
+ catch (e) {
249
+ logDebug(`[Weavetab] Exception killing process ${pid}:`, e.message);
250
+ }
251
+ }
252
+ // ─── Circuit breaker to prevent repeated failures ─────────────────────────────
253
+ const launchFailureCount = new Map();
254
+ const CIRCUIT_BREAKER_THRESHOLD = 3;
255
+ const CIRCUIT_BREAKER_RESET_MS = 60_000; // 1 minute
256
+ function checkCircuitBreaker(profileDir) {
257
+ const now = Date.now();
258
+ const failures = launchFailureCount.get(profileDir) || 0;
259
+ // Reset if enough time has passed
260
+ if (failures > 0 && now - (launchFailureCount.get(`${profileDir}_time`) || 0) > CIRCUIT_BREAKER_RESET_MS) {
261
+ launchFailureCount.delete(profileDir);
262
+ launchFailureCount.delete(`${profileDir}_time`);
263
+ return false;
264
+ }
265
+ // Check if threshold exceeded
266
+ if (failures >= CIRCUIT_BREAKER_THRESHOLD) {
267
+ logDebug(`[Weavetab] [!] Circuit breaker triggered: Too many recent launch failures for profile. Waiting ${CIRCUIT_BREAKER_RESET_MS / 1000}s before retry.`);
268
+ return true;
269
+ }
270
+ return false;
271
+ }
272
+ function recordLaunchFailure(profileDir) {
273
+ const failures = (launchFailureCount.get(profileDir) || 0) + 1;
274
+ launchFailureCount.set(profileDir, failures);
275
+ launchFailureCount.set(`${profileDir}_time`, Date.now());
276
+ logDebug(`[Weavetab] [!] Launch failure recorded (${failures}/${CIRCUIT_BREAKER_THRESHOLD}). Circuit breaker will trigger at ${CIRCUIT_BREAKER_THRESHOLD}.`);
277
+ }
278
+ function recordLaunchSuccess(profileDir) {
279
+ launchFailureCount.delete(profileDir);
280
+ launchFailureCount.delete(`${profileDir}_time`);
281
+ }
282
+ // ─── Stealth script: injected before any page JS runs ─────────────────────────
283
+ const STEALTH_SCRIPT = `
284
+ (function() {
285
+ 'use strict';
286
+
287
+ // 1. Spoof navigator.webdriver
288
+ Object.defineProperty(navigator, 'webdriver', {
289
+ get: () => undefined,
290
+ configurable: true,
291
+ });
292
+
293
+ // 2. Remove chrome automation props
294
+ if (window.navigator.plugins.length === 0) {
295
+ Object.defineProperty(navigator, 'plugins', {
296
+ get: () => [1, 2, 3, 4, 5],
297
+ configurable: true,
298
+ });
299
+ }
300
+
301
+ // 3. Spoof navigator.languages (defaults to en-US if unset)
302
+ if (!navigator.languages || navigator.languages.length === 0) {
303
+ Object.defineProperty(navigator, 'languages', {
304
+ get: () => ['en-US', 'en'],
305
+ configurable: true,
306
+ });
307
+ }
308
+
309
+ // 4. Spoof Chrome runtime
310
+ window.chrome = window.chrome || {};
311
+ window.chrome.runtime = window.chrome.runtime || {};
312
+
313
+ // 5. Randomize hardwareConcurrency (4–8 cores)
314
+ Object.defineProperty(navigator, 'hardwareConcurrency', {
315
+ get: () => 4 + Math.floor(Math.random() * 4),
316
+ configurable: true,
317
+ });
318
+
319
+ // 6. Randomize deviceMemory (4, 8, or 12 GB, weighted toward 8)
320
+ Object.defineProperty(navigator, 'deviceMemory', {
321
+ get: () => [4, 8, 8, 8, 12][Math.floor(Math.random() * 5)],
322
+ configurable: true,
323
+ });
324
+
325
+ // 7. WebGL vendor/renderer spoofing — only spoof the two info params,
326
+ // pass everything else through untouched. Wrapped in try/catch so a
327
+ // missing WebGL context never breaks the page.
328
+ try {
329
+ var _origGetParam = WebGLRenderingContext.prototype.getParameter;
330
+ WebGLRenderingContext.prototype.getParameter = function(p) {
331
+ if (p === 37445) return 'Google Inc. (Intel)';
332
+ if (p === 37446) {
333
+ return navigator.platform.indexOf('Mac') === 0
334
+ ? 'ANGLE (Intel, Intel(R) Iris(TM) Plus Graphics 640, OpenGL 4.1)'
335
+ : 'ANGLE (Intel, Intel(R) UHD Graphics 620 Direct3D11 vs_5_0 ps_5_0, D3D11)';
336
+ }
337
+ return _origGetParam.apply(this, arguments);
338
+ };
339
+ if (typeof WebGL2RenderingContext !== 'undefined') {
340
+ var _origGetParam2 = WebGL2RenderingContext.prototype.getParameter;
341
+ WebGL2RenderingContext.prototype.getParameter = function(p) {
342
+ if (p === 37445) return 'Google Inc. (Intel)';
343
+ if (p === 37446) {
344
+ return navigator.platform.indexOf('Mac') === 0
345
+ ? 'ANGLE (Intel, Intel(R) Iris(TM) Plus Graphics 640, OpenGL 4.1)'
346
+ : 'ANGLE (Intel, Intel(R) UHD Graphics 620 Direct3D11 vs_5_0 ps_5_0, D3D11)';
347
+ }
348
+ return _origGetParam2.apply(this, arguments);
349
+ };
350
+ }
351
+ } catch(e) {}
352
+
353
+ // NOTE: Function.prototype.toString proxy removed — it was wrapping
354
+ // every toString call globally and could break page rendering.
355
+ })();
356
+ `;
357
+ // ─── Mandatory security flags (always applied, never overridable) ──────────────
358
+ const MANDATORY_FLAGS = Object.freeze([
359
+ "--disable-sync",
360
+ ]);
361
+ /**
362
+ * Returns the "key" part of a Chrome flag (everything before the first =).
363
+ * Example: "--disable-sync=foo" → "--disable-sync"
364
+ */
365
+ function getFlagKey(flag) {
366
+ return flag.split("=")[0];
367
+ }
368
+ /**
369
+ * Filters custom browser flags, dropping any that conflict with mandatory
370
+ * security flags. Handles the --enable-foo / --disable-foo symmetry.
371
+ */
372
+ function filterBrowserFlags(customFlags) {
373
+ const mandatoryKeys = new Set(MANDATORY_FLAGS.map(getFlagKey));
374
+ // Add derived conflicting keys (--enable-* for a --disable-* mandatory flag and vice versa)
375
+ for (const key of mandatoryKeys) {
376
+ if (key.startsWith("--disable-")) {
377
+ mandatoryKeys.add("--enable-" + key.slice("--disable-".length));
378
+ }
379
+ if (key.startsWith("--enable-")) {
380
+ mandatoryKeys.add("--disable-" + key.slice("--enable-".length));
381
+ }
382
+ }
383
+ return customFlags.filter(flag => {
384
+ const key = getFlagKey(flag);
385
+ if (mandatoryKeys.has(key)) {
386
+ logDebug(`[Weavetab] ⚠ Custom browser flag "${flag}" conflicts with a mandatory security flag. Dropping it.`);
387
+ return false;
388
+ }
389
+ return true;
390
+ });
391
+ }
392
+ // ─── Launch flags ─────────────────────────────────────────────────────────────
393
+ function getLaunchFlags(config, extPath) {
394
+ const plat = os.platform();
395
+ const flags = [
396
+ "--disable-blink-features=AutomationControlled",
397
+ "--test-type",
398
+ "--no-first-run",
399
+ "--no-default-browser-check",
400
+ "--disable-default-apps",
401
+ "--no-service-autorun",
402
+ "--disable-background-mode",
403
+ "--disable-backgrounding-occluded-windows",
404
+ "--start-maximized",
405
+ "--disable-infobars",
406
+ ];
407
+ // Platform-specific flags
408
+ if (plat === "win32") {
409
+ // Windows: prevent browser from becoming default
410
+ flags.push("--disable-features=TranslateUI");
411
+ flags.push("--disable-features=DefaultBrowserGuidance");
412
+ }
413
+ else if (plat === "darwin") {
414
+ // macOS: use mock keychain to prevent password prompts
415
+ flags.push("--use-mock-keychain");
416
+ // Prevent macOS from showing permission prompts
417
+ flags.push("--disable-mac-overlays");
418
+ }
419
+ else if (plat === "linux") {
420
+ // Linux: use basic password store to prevent keyring issues
421
+ flags.push("--password-store=basic");
422
+ }
423
+ if (getConfigHeadless(config))
424
+ flags.push("--headless=new");
425
+ if (getConfigProxy(config))
426
+ flags.push(`--proxy-server=${getConfigProxy(config)}`);
427
+ // disableExtensions: NOTE — --disable-extensions kills ALL extensions including ours.
428
+ // --load-extension does NOT reliably override --disable-extensions on Edge.
429
+ // Since we can't disable other extensions while keeping ours (unpacked extensions
430
+ // have unpredictable IDs), we skip the flag entirely and let other extensions coexist.
431
+ // The user can manually disable extensions in their browser profile if needed.
432
+ if (getConfigDisableExtensions(config)) {
433
+ logDebug("[Weavetab] [SAFETY] disableExtensions=true — cannot safely disable other extensions while keeping Weavetab extension. Loading extension alongside any existing ones.");
434
+ }
435
+ // devMode: enable verbose remote debugging
436
+ if (getConfigDevMode(config)) {
437
+ flags.push("--enable-logging", "--v=1");
438
+ }
439
+ // Block list → Chrome --host-rules (blocks at DNS level)
440
+ const blockRules = [];
441
+ const blockList = getConfigBlock(config);
442
+ if (blockList && blockList.length > 0) {
443
+ for (const domain of blockList) {
444
+ // Strip wildcards for host-rules format
445
+ const cleaned = domain.replace(/^\*\./, "");
446
+ blockRules.push(`MAP ${cleaned} 0.0.0.0`, `MAP *.${cleaned} 0.0.0.0`);
447
+ }
448
+ }
449
+ // Allow list overrides block list
450
+ const allowList = getConfigAllow(config);
451
+ if (allowList && allowList.length > 0) {
452
+ for (const domain of allowList) {
453
+ blockRules.push(`EXCLUDE ${domain}`, `EXCLUDE *.${domain}`);
454
+ }
455
+ }
456
+ if (blockRules.length > 0) {
457
+ flags.push(`--host-resolver-rules=${blockRules.join(", ")}`);
458
+ }
459
+ // Mandatory security flags applied AFTER all configurable flags
460
+ // These cannot be overridden by any config setting
461
+ flags.push(...MANDATORY_FLAGS);
462
+ return flags;
463
+ }
464
+ // ─── Main launcher ────────────────────────────────────────────────────────────
465
+ export async function launchWithBridge(config) {
466
+ const engine = getConfigBrowserType(config) || "chromium";
467
+ if (engine !== "chromium") {
468
+ return launchNonChromium(config, engine);
469
+ }
470
+ const browserPath = getConfigBrowserPath(config);
471
+ const browser = browserPath && browserPath !== "auto"
472
+ ? findBrowser(browserPath)
473
+ : findBrowser();
474
+ if (!browser) {
475
+ throw new Error("[Weavetab] ✗ No Chromium browser found on this system.\n" +
476
+ " Please install one of the following supported browsers:\n" +
477
+ " - Google Chrome (https://www.google.com/chrome/)\n" +
478
+ " - Brave (https://brave.com/)\n" +
479
+ " - Microsoft Edge (https://www.microsoft.com/edge)\n" +
480
+ " If you have a browser installed in a custom location, set it via: wt config browserPath \"/path/to/browser\"");
481
+ }
482
+ const debugPort = 0;
483
+ const lock = readLock();
484
+ if (lock && lock.mode === "session") {
485
+ if (isLockPending(lock)) {
486
+ logDebug(`[Weavetab] Launch already in progress (PID: ${lock.pid}). Waiting...`);
487
+ throw new Error("Launch already in progress. Wait for it to complete.");
488
+ }
489
+ const valid = await isLockValid(lock);
490
+ if (valid) {
491
+ logDebug(`[Weavetab] [OK] Reusing valid lock (PID: ${lock.pid}, Port: ${lock.port})`);
492
+ return { path: browser.path, name: browser.name, port: lock.port, engine: "chromium" };
493
+ }
494
+ logDebug(`[Weavetab] [!] Stale lock detected. Clearing.`);
495
+ clearLock();
496
+ }
497
+ else if (lock) {
498
+ clearLock();
499
+ }
500
+ const MAX_ATTEMPTS = 3;
501
+ let lastError = null;
502
+ const profileDir = getSessionProfile(getConfigCookies(config), getConfigProfileDir(config), config.syncDataAcrossBrowsers, browser.path);
503
+ // Circuit breaker check before attempting launch
504
+ if (checkCircuitBreaker(profileDir)) {
505
+ throw new Error(`[Weavetab] Circuit breaker triggered: Too many recent launch failures. Please wait ${CIRCUIT_BREAKER_RESET_MS / 1000}s before retrying.`);
506
+ }
507
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
508
+ if (attempt > 1) {
509
+ logDebug(`[Weavetab] [!] Retrying browser launch (attempt ${attempt}/${MAX_ATTEMPTS}) after 1.5s...`);
510
+ // Auto-recovery for corrupted profiles (e.g., FrameWidgetHost mojo crashes after hard kill).
511
+ // If we reach the final attempt, nuke the isolated session profile as a last resort to recover.
512
+ if (attempt === MAX_ATTEMPTS && profileDir.includes(".weavetab-system") && profileDir.includes("session-profile")) {
513
+ logDebug(`[Weavetab] [!] Wiping potentially corrupted session profile as last resort: ${profileDir}`);
514
+ try {
515
+ fs.rmSync(profileDir, { recursive: true, force: true });
516
+ }
517
+ catch { /* ignore */ }
518
+ }
519
+ // Do NOT clear the lock between retries — that creates a ~1500ms window where
520
+ // another concurrent getOrConnect() sees no lock and races to start a second browser.
521
+ // Instead, keep the pending lock alive (or write a fresh one) so concurrent calls
522
+ // wait on it rather than spawning their own instance.
523
+ await writeLock({ pid: process.pid, port: 0, profileDir, browserPath: browser.path, mode: "session", pending: true, timestamp: Date.now() });
524
+ await new Promise(r => setTimeout(r, 1500));
525
+ }
526
+ try {
527
+ const extPath = path.resolve(__dirname, "../../ext-dist");
528
+ // Verify extension directory exists
529
+ if (!fs.existsSync(extPath)) {
530
+ throw new Error(`[Weavetab] Extension directory does not exist: ${extPath}. Build the extension first.`);
531
+ }
532
+ const manifestPath = path.join(extPath, "manifest.json");
533
+ const contentPath = path.join(extPath, "content.js");
534
+ const backgroundPath = path.join(extPath, "background.js");
535
+ if (!fs.existsSync(manifestPath)) {
536
+ throw new Error(`[Weavetab] Extension manifest missing: ${manifestPath}`);
537
+ }
538
+ if (!fs.existsSync(contentPath)) {
539
+ throw new Error(`[Weavetab] Extension content script missing: ${contentPath}`);
540
+ }
541
+ if (!fs.existsSync(backgroundPath)) {
542
+ throw new Error(`[Weavetab] Extension background script missing: ${backgroundPath}`);
543
+ }
544
+ const resolvedDownloadDir = getDownloadDir(config) && getDownloadDir(config).trim()
545
+ ? getDownloadDir(config).trim()
546
+ : getOutputDir("downloads", config);
547
+ // Normalize paths for cross-platform compatibility
548
+ const platform = os.platform();
549
+ const normalizedExtPath = platform === 'win32'
550
+ ? extPath.replace(/\//g, '\\')
551
+ : extPath;
552
+ const normalizedProfileDir = platform === 'win32'
553
+ ? profileDir.replace(/\//g, '\\')
554
+ : profileDir;
555
+ const normalizedDownloadDir = platform === 'win32'
556
+ ? resolvedDownloadDir.replace(/\//g, '\\')
557
+ : resolvedDownloadDir;
558
+ // Clean up stale Chrome SingletonLock from previous session.
559
+ // On Windows, Browser.close() sometimes doesn't fully release the lock
560
+ // before Node.js spawns a new Chrome, causing it to start without extensions.
561
+ const singletonLock = path.join(profileDir, "SingletonLock");
562
+ const singletonCookie = path.join(profileDir, "SingletonCookie");
563
+ try {
564
+ if (fs.existsSync(singletonLock))
565
+ fs.unlinkSync(singletonLock);
566
+ }
567
+ catch { /* non-fatal */ }
568
+ try {
569
+ if (fs.existsSync(singletonCookie))
570
+ fs.unlinkSync(singletonCookie);
571
+ }
572
+ catch { /* non-fatal */ }
573
+ // Wipe the MV3 service worker cache before every launch so Chrome always
574
+ // executes the latest background.js from disk rather than a stale cached
575
+ // script stored in the profile. Without this, edits to ext-dist/background.js
576
+ // have no effect until Chrome manually reloads the extension — the SW cache
577
+ // (Default/Service Worker/ScriptCache + Default/Service Worker/Database) is
578
+ // keyed by URL hash and persists across sessions in the user-data-dir.
579
+ // This ensures any future background.js change takes effect immediately
580
+ // on all existing profiles without manual cache clearing.
581
+ try {
582
+ const swBase = path.join(profileDir, "Default", "Service Worker");
583
+ const swScriptCache = path.join(swBase, "ScriptCache");
584
+ const swDatabase = path.join(swBase, "Database");
585
+ if (fs.existsSync(swScriptCache)) {
586
+ fs.rmSync(swScriptCache, { recursive: true, force: true });
587
+ logDebug("[Weavetab] Wiped SW ScriptCache to force background.js reload.");
588
+ }
589
+ if (fs.existsSync(swDatabase)) {
590
+ fs.rmSync(swDatabase, { recursive: true, force: true });
591
+ logDebug("[Weavetab] Wiped SW Database to force background.js reload.");
592
+ }
593
+ }
594
+ catch (swErr) {
595
+ logDebug(`[Weavetab] SW cache wipe failed (non-fatal): ${swErr}`);
596
+ }
597
+ const args = [
598
+ `--remote-debugging-port=${debugPort}`,
599
+ `--user-data-dir=${normalizedProfileDir}`,
600
+ `--default-download-dir=${normalizedDownloadDir}`,
601
+ ...getLaunchFlags(config, extPath),
602
+ ...filterBrowserFlags(getConfigBrowserFlags(config) || []),
603
+ `--load-extension=${normalizedExtPath}`,
604
+ // NOTE: --disable-extensions-except was removed. It was killing any
605
+ // extensions the user manually loaded into the Weavetab profile.
606
+ // --load-extension alone is sufficient to auto-load the Weavetab extension.
607
+ "about:blank",
608
+ ];
609
+ let pid;
610
+ // Cross-platform spawn options
611
+ const spawnOptions = {
612
+ detached: true,
613
+ stdio: ["ignore", "ignore", "pipe"],
614
+ };
615
+ // On macOS, sometimes we need to set the shell to true for .app paths
616
+ if (platform === 'darwin' && browser.path.includes('.app')) {
617
+ spawnOptions.shell = true;
618
+ }
619
+ // Validate browser path before spawning
620
+ if (!isValidBrowserPath(browser.path)) {
621
+ throw new Error(`Security violation: Invalid browser path: ${browser.path}`);
622
+ }
623
+ const proc = spawn(browser.path, args, spawnOptions);
624
+ proc.unref();
625
+ pid = proc.pid;
626
+ logDebug(`[Weavetab] Launching ${browser.name} (PID: ${pid}, port: auto, attempt: ${attempt}).`);
627
+ let actualPort;
628
+ try {
629
+ actualPort = await resolvePortFromStderr(proc, pid, engine);
630
+ }
631
+ catch (portErr) {
632
+ // Kill process just in case it's still alive
633
+ try {
634
+ if (pid)
635
+ killProcess(pid);
636
+ }
637
+ catch { /* ignore */ }
638
+ clearLock();
639
+ lastError = portErr instanceof Error ? portErr : new Error(String(portErr));
640
+ logDebug(`[Weavetab] [!] Attempt ${attempt} failed to resolve port: ${lastError.message}`);
641
+ continue;
642
+ }
643
+ // Create a browser fingerprint for process identity verification
644
+ const fingerprint = createBrowserFingerprint(pid || 0, actualPort, profileDir, browser.path);
645
+ await writeLock({
646
+ pid,
647
+ port: actualPort,
648
+ profileDir,
649
+ browserPath: browser.path,
650
+ mode: "session",
651
+ pending: true,
652
+ timestamp: Date.now(),
653
+ fingerprint: {
654
+ launchTimestamp: fingerprint.launchTimestamp,
655
+ }
656
+ });
657
+ const probePort = actualPort;
658
+ let confirmed = false;
659
+ for (let i = 0; i < 20; i++) {
660
+ try {
661
+ await withCDPTimeout(CDP.List({ host: "127.0.0.1", port: probePort }), 2000, "CDP.List");
662
+ confirmed = true;
663
+ break;
664
+ }
665
+ catch {
666
+ await new Promise(r => setTimeout(r, 800));
667
+ }
668
+ }
669
+ if (!confirmed) {
670
+ logDebug(`[Weavetab] [!] Browser not confirmed after 16s on attempt ${attempt}.`);
671
+ // Still proceed — browser may be loading slowly
672
+ }
673
+ else {
674
+ logDebug(`[Weavetab] [OK] ${browser.name} ready on :${actualPort} (attempt ${attempt})`);
675
+ // Verify extension loaded by checking for extension pages.
676
+ // MV3 service workers can take 1-3s to initialize after Chrome starts,
677
+ // so we wait briefly before querying targets.
678
+ try {
679
+ await new Promise(r => setTimeout(r, 1500));
680
+ const testSession = await withCDPTimeout(CDP({ host: "127.0.0.1", port: actualPort }), 4000, "CDP connect");
681
+ const { targetInfos } = await withCDPTimeout(testSession.Target.getTargets(), 4000, "getTargets");
682
+ const extensionTargets = targetInfos.filter(t => t.type === "service_worker" || (t.type === "page" && t.url.startsWith("chrome-extension://")));
683
+ await testSession.close();
684
+ if (extensionTargets.length > 0) {
685
+ logDebug(`[Weavetab] [OK] Extension loaded - found ${extensionTargets.length} extension target(s)`);
686
+ }
687
+ else {
688
+ logDebug(`[Weavetab] [!] Extension may not have loaded - no extension targets found. Check chrome://extensions in the browser.`);
689
+ }
690
+ }
691
+ catch (extCheckErr) {
692
+ logDebug(`[Weavetab] [!] Could not verify extension load: ${extCheckErr}`);
693
+ }
694
+ }
695
+ await writeLock({ pid, port: actualPort, profileDir, browserPath: browser.path, mode: "session", pending: false });
696
+ recordLaunchSuccess(profileDir);
697
+ return { ...browser, port: actualPort, engine: "chromium" };
698
+ }
699
+ catch (err) {
700
+ clearLock();
701
+ recordLaunchFailure(profileDir);
702
+ lastError = err instanceof Error ? err : new Error(String(err));
703
+ logDebug(`[Weavetab] [!] Launch attempt ${attempt} failed: ${lastError.message}`);
704
+ }
705
+ }
706
+ throw new Error(`[Weavetab] Browser failed to launch after ${MAX_ATTEMPTS} attempts. Last error: ${lastError?.message ?? "unknown"}`);
707
+ }
708
+ async function launchNonChromium(config, engine) {
709
+ let browser = null;
710
+ if (engine === "firefox") {
711
+ browser = findFirefox();
712
+ }
713
+ else if (engine === "webkit") {
714
+ // Safari Technology Preview — experimental
715
+ const safariPaths = os.platform() === "darwin"
716
+ ? ["/Applications/Safari Technology Preview.app/Contents/MacOS/safaridriver"]
717
+ : [];
718
+ for (const p of safariPaths) {
719
+ if (fs.existsSync(p)) {
720
+ browser = { name: "Safari TP", path: p };
721
+ break;
722
+ }
723
+ }
724
+ }
725
+ if (!browser) {
726
+ throw new Error(`[Weavetab] No ${engine} browser found.\n` +
727
+ ` Install ${engine === "firefox" ? "Firefox" : "Safari Technology Preview"} and try again.`);
728
+ }
729
+ const debugPort = 0;
730
+ const profileDir = getSessionProfile(getConfigCookies(config), getConfigProfileDir(config));
731
+ const args = [];
732
+ if (engine === "firefox") {
733
+ args.push(`--remote-debugging-port=${debugPort}`, `--profile`, profileDir, "--new-tab", "about:blank");
734
+ }
735
+ else {
736
+ // WebKit: Safari Technology Preview
737
+ args.push(`--remote-debugging-port=${debugPort}`);
738
+ }
739
+ const proc = spawn(browser.path, args, {
740
+ detached: true,
741
+ stdio: ["ignore", "ignore", "pipe"],
742
+ });
743
+ proc.unref();
744
+ const pid = proc.pid;
745
+ logDebug(`[Weavetab] Launching ${browser.name} (${engine}, PID: ${pid}, port: auto).`);
746
+ const actualPort = await resolvePortFromStderr(proc, pid, engine);
747
+ for (let i = 0; i < 15; i++) {
748
+ try {
749
+ await withCDPTimeout(CDP.List({ host: "127.0.0.1", port: actualPort }), 2000, "CDP.List nonChromium");
750
+ logDebug(`[Weavetab] [OK] ${browser.name} ready on :${actualPort}`);
751
+ return { path: browser.path, name: browser.name, port: actualPort, engine };
752
+ }
753
+ catch {
754
+ await new Promise(r => setTimeout(r, 1000));
755
+ }
756
+ }
757
+ logDebug(`[Weavetab] [!] ${browser.name} not confirmed after 15s — proceeding with port ${actualPort}.`);
758
+ return { path: browser.path, name: browser.name, port: actualPort, engine };
759
+ }
760
+ // ─── Stealth injection helper ─────────────────────────────────────────────────
761
+ export async function injectStealthScript(session) {
762
+ try {
763
+ await session.Page.addScriptToEvaluateOnNewDocument({ source: STEALTH_SCRIPT });
764
+ await session.Runtime.evaluate({ expression: STEALTH_SCRIPT }).catch(() => { });
765
+ }
766
+ catch { /* ignore */ }
767
+ }
768
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
769
+ async function resolvePortFromStderr(proc, pid, _engine) {
770
+ return new Promise((resolve, reject) => {
771
+ let resolved = false;
772
+ const TIMEOUT_MS = 10_000;
773
+ const timer = setTimeout(() => {
774
+ if (!resolved) {
775
+ resolved = true;
776
+ try {
777
+ proc.stderr?.destroy();
778
+ }
779
+ catch { /* ignore */ }
780
+ logDebug(`[Weavetab] [!] Browser did not emit DevTools port within 10s. Killing PID ${pid}.`);
781
+ try {
782
+ if (pid)
783
+ killProcess(pid);
784
+ }
785
+ catch { /* ignore */ }
786
+ clearLock();
787
+ reject(new Error("[Weavetab] Browser launch timed out — no DevTools port emitted on stderr."));
788
+ }
789
+ }, TIMEOUT_MS);
790
+ let stderrData = "";
791
+ proc.stderr?.on("data", (chunk) => {
792
+ stderrData += chunk.toString();
793
+ // Check for extension loading errors
794
+ if (stderrData.toLowerCase().includes("extension") &&
795
+ (stderrData.toLowerCase().includes("error") ||
796
+ stderrData.toLowerCase().includes("failed") ||
797
+ stderrData.toLowerCase().includes("could not load"))) {
798
+ logDebug(`[Weavetab] [!] Extension loading error detected in stderr: ${stderrData.slice(-500)}`);
799
+ }
800
+ const match = stderrData.match(/DevTools listening on ws:\/\/[\d.]+:(\d+)/);
801
+ if (match && !resolved) {
802
+ resolved = true;
803
+ clearTimeout(timer);
804
+ try {
805
+ proc.stderr?.destroy();
806
+ }
807
+ catch { /* ignore */ }
808
+ const port = parseInt(match[1], 10);
809
+ logDebug(`[Weavetab] [OK] Resolved dynamic port: ${port}`);
810
+ resolve(port);
811
+ }
812
+ });
813
+ proc.on("exit", (code) => {
814
+ if (!resolved) {
815
+ resolved = true;
816
+ clearTimeout(timer);
817
+ try {
818
+ proc.stderr?.destroy();
819
+ }
820
+ catch { /* ignore */ }
821
+ clearLock();
822
+ reject(new Error(`[Weavetab] Browser process exited (code ${code}) before emitting DevTools port.`));
823
+ }
824
+ });
825
+ });
826
+ }