@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,661 @@
1
+ import { logDebug } from "../audit/logger.js";
2
+ /**
3
+ * Weavetab — cdp/connector.ts
4
+ * CDP connection pool, browser launch, per-tab session wiring.
5
+ * V3: Two-factor lock validation, networkIdle with 5s fallback, SIGTERM cleanup.
6
+ * Weavetab by fy2ne
7
+ */
8
+ import CDP from "chrome-remote-interface";
9
+ import { blockStorageAccess } from "../security/guard.js";
10
+ import { launchWithBridge, injectStealthScript } from "./bridge.js";
11
+ import { setupOverlay, ensureVisualLayer } from "../overlay/injector.js";
12
+ import { onConfigChange, saveConfig, loadConfigSync, getCachedConfig, getConfigAgentId, getConfigCookies, getConfigProfileDir, getConfigMode } from "../config/loader.js";
13
+ import { sessionRegistry } from "../state/session.js";
14
+ import { initConsoleBuffering } from "../tools/console.js";
15
+ import { registerMutationListener, clearTabCache } from "../state/cache.js";
16
+ import { recordTabActivity } from "../tools/tabs.js";
17
+ import { initDialogHandler } from "../tools/dialog.js";
18
+ import { WT_REGISTRY_INIT_EXPR } from "./walker.js";
19
+ import { urcInvalidate } from "./ref_cache.js";
20
+ import { readLock, clearLock, isLockValid, isLockPending, acquireMutex } from "./lock.js";
21
+ import { getSessionProfile } from "./profiles.js";
22
+ import { safelyKillWeavetabProcesses, verifyProfileIsolation, verifyBrowserFingerprint, clearProfileVerificationCache } from "./security.js";
23
+ function withCDPTimeout(promise, ms, label) {
24
+ return Promise.race([
25
+ promise,
26
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms)),
27
+ ]);
28
+ }
29
+ onConfigChange((newConfig) => {
30
+ const activeClient = sessionRegistry.getActive(getConfigAgentId(newConfig));
31
+ if (activeClient) {
32
+ activeClient.session.Runtime.evaluate({
33
+ expression: `if(window.__wt_setConfig) window.__wt_setConfig(${JSON.stringify(newConfig)}); else window.__wt_config = ${JSON.stringify(newConfig)};`
34
+ }).catch(() => { });
35
+ }
36
+ });
37
+ let actualPort = null;
38
+ let activeEngine = "chromium";
39
+ /** Per-tab analytics interval handles — keyed by targetId.
40
+ * Replacing the old single-slot global that caused interval leaks when multiple tabs were open. */
41
+ const analyticsIntervals = new Map();
42
+ export function getPort() {
43
+ return actualPort;
44
+ }
45
+ export function getActiveEngine() {
46
+ return activeEngine;
47
+ }
48
+ // ─── Navigation readiness: loadEventFired + networkIdle with 5s cap ───────────
49
+ async function waitForPageReady(session) {
50
+ const NETWORK_IDLE_TIMEOUT_MS = 5000;
51
+ const NETWORK_IDLE_QUIET_MS = 500;
52
+ const readyCheck = await session.Runtime.evaluate({
53
+ expression: "document.readyState === 'complete'",
54
+ returnByValue: true
55
+ }).catch(() => ({ result: { value: false } }));
56
+ if (readyCheck?.result?.value !== true) {
57
+ await Promise.race([
58
+ session.Page.loadEventFired(),
59
+ new Promise(r => setTimeout(r, 10000)), // max 10s for load
60
+ ]);
61
+ }
62
+ // networkIdle: no new requests for NETWORK_IDLE_QUIET_MS, capped at NETWORK_IDLE_TIMEOUT_MS
63
+ await new Promise((resolve) => {
64
+ let done = false;
65
+ let quietTimer = null;
66
+ const hardTimeout = setTimeout(() => { done = true; resolve(); }, NETWORK_IDLE_TIMEOUT_MS);
67
+ const cleanup = () => {
68
+ if (done)
69
+ return;
70
+ done = true;
71
+ if (quietTimer)
72
+ clearTimeout(quietTimer);
73
+ clearTimeout(hardTimeout);
74
+ try {
75
+ session.removeListener("Network.requestWillBeSent", resetQuiet);
76
+ session.removeListener("Network.loadingFinished", resetQuiet);
77
+ session.removeListener("Network.loadingFailed", resetQuiet);
78
+ }
79
+ catch { /* ignore */ }
80
+ resolve();
81
+ };
82
+ const resetQuiet = () => {
83
+ if (done)
84
+ return;
85
+ if (quietTimer)
86
+ clearTimeout(quietTimer);
87
+ quietTimer = setTimeout(cleanup, NETWORK_IDLE_QUIET_MS);
88
+ };
89
+ // Listen for network activity
90
+ session.Network.enable().catch(() => { });
91
+ session.Network.on("requestWillBeSent", resetQuiet);
92
+ session.Network.on("loadingFinished", resetQuiet);
93
+ session.Network.on("loadingFailed", resetQuiet);
94
+ resetQuiet(); // start the quiet timer immediately
95
+ });
96
+ }
97
+ // ─── Session init (overlay + re-injection) ───────────────────────────────────
98
+ async function initSession(session, targetId, port, config) {
99
+ initDialogHandler(session);
100
+ // Initialize Omni-Context Broker for iframes
101
+ const { initOmniBroker } = await import("./omni_broker.js");
102
+ await initOmniBroker(session, targetId, port);
103
+ if (activeEngine === "chromium") {
104
+ // Only block cookies/storage when cookies mode is disabled
105
+ if (!getConfigCookies(config)) {
106
+ await blockStorageAccess(session);
107
+ }
108
+ // Inject stealth script before page runs anything
109
+ await injectStealthScript(session);
110
+ // Inject WeakMap registry so atomicResolve can re-hydrate elements after SPA reconciliation
111
+ try {
112
+ await session.Page.addScriptToEvaluateOnNewDocument({ source: WT_REGISTRY_INIT_EXPR });
113
+ }
114
+ catch { /* non-fatal — older CDP targets may not support this */ }
115
+ // Inject overlay (requires Chrome extension)
116
+ await setupOverlay(session, config);
117
+ // Force silent downloads — suppress the Windows "Save As" OS popup entirely.
118
+ // Weavetab routes all downloads to the active .weavetab/downloads directory.
119
+ try {
120
+ const { getOutputDir, getDownloadDir } = await import("../config/loader.js");
121
+ const fs = await import("node:fs");
122
+ const downloadDir = getDownloadDir(config)?.trim()
123
+ ? getDownloadDir(config).trim()
124
+ : getOutputDir("downloads", config);
125
+ if (!fs.default.existsSync(downloadDir)) {
126
+ fs.default.mkdirSync(downloadDir, { recursive: true });
127
+ }
128
+ await session.Browser.setDownloadBehavior({
129
+ behavior: "allow",
130
+ downloadPath: downloadDir,
131
+ eventsEnabled: true,
132
+ }).catch(() => { });
133
+ }
134
+ catch { /* non-fatal: older Chrome versions may not support this API */ }
135
+ }
136
+ registerMutationListener(session, targetId);
137
+ initConsoleBuffering(session, targetId);
138
+ // Re-inject on navigation — use loadEventFired instead of raw setTimeout
139
+ session.Page.on("frameNavigated", async (frame) => {
140
+ if (frame.frame.parentId)
141
+ return;
142
+ clearTabCache(targetId);
143
+ // Invalidate URC on navigation — elements from the old page are stale
144
+ urcInvalidate(targetId);
145
+ if (activeEngine === "chromium") {
146
+ try {
147
+ await waitForPageReady(session);
148
+ const cfg = getCachedConfig();
149
+ const clientRef = sessionRegistry.getAll().find(c => c.targetId === targetId);
150
+ if (clientRef)
151
+ clientRef.overlayInstalled = false;
152
+ await ensureVisualLayer(session, cfg, clientRef ?? undefined);
153
+ // Re-inject active plan into the new page
154
+ const { restoreActivePlanIntoPage } = await import("../tools/plan.js");
155
+ await restoreActivePlanIntoPage(session);
156
+ }
157
+ catch { /* ignore */ }
158
+ }
159
+ });
160
+ // Diagnostic: verify extension content script loaded
161
+ if (activeEngine === "chromium") {
162
+ try {
163
+ // Check for multiple indicators that extension is loaded
164
+ const { result: extCheck } = await session.Runtime.evaluate({
165
+ expression: `!!(
166
+ document.querySelector('weavetab-host') ||
167
+ document.getElementById('__Weavetab_cursor_host') ||
168
+ document.getElementById('wt-ui-styles') ||
169
+ document.querySelector('[data-wt-internal]') ||
170
+ window.__wt_chat_queue
171
+ )`,
172
+ returnByValue: true,
173
+ });
174
+ if (extCheck?.value) {
175
+ logDebug(`[Weavetab] [OK] Extension content script detected`);
176
+ }
177
+ else {
178
+ logDebug(`[Weavetab] [!] Extension content script NOT detected — HUD/overlay may not work`);
179
+ logDebug(`[Weavetab] [!] Try reloading page or check if extension loaded in browser://extensions`);
180
+ // Try to inject a basic error indicator into the page
181
+ try {
182
+ await session.Runtime.evaluate({
183
+ expression: `
184
+ (function() {
185
+ const div = document.createElement('div');
186
+ div.style.cssText = 'position:fixed;top:10px;right:10px;background:red;color:white;padding:10px;z-index:999999;font-family:sans-serif;font-size:14px;';
187
+ div.textContent = 'WeaveTab Extension Not Loaded';
188
+ document.body.appendChild(div);
189
+ setTimeout(() => div.remove(), 5000);
190
+ })()
191
+ `
192
+ });
193
+ }
194
+ catch { /* ignore */ }
195
+ }
196
+ }
197
+ catch (checkErr) {
198
+ logDebug(`[Weavetab] [!] Extension health check failed: ${checkErr}`);
199
+ }
200
+ }
201
+ // Push config + initial idle state to extension right away (Chrome only)
202
+ if (activeEngine === "chromium") {
203
+ const { sendToExtension } = await import("../cdp/helpers.js");
204
+ sendToExtension(session, {
205
+ type: 'Weavetab_CONFIG',
206
+ devMode: config.devMode,
207
+ }).catch(() => { });
208
+ sendToExtension(session, {
209
+ type: 'Weavetab_GROUP_STATE',
210
+ state: 'idle',
211
+ }).catch(() => { });
212
+ }
213
+ // Analytics ping every 5s — stored per-tab so each interval can be
214
+ // independently cancelled when the tab closes or cleanupAll() is called.
215
+ const sessionStartTime = Date.now();
216
+ const analyticsInterval = setInterval(async () => {
217
+ try {
218
+ const t = Date.now();
219
+ const healthObj = await session.Runtime.evaluate({
220
+ expression: `document.readyState === 'complete' ? '100% Loaded' : 'Loading...'`,
221
+ returnByValue: true
222
+ });
223
+ const latency = Date.now() - t;
224
+ const health = healthObj?.result?.value || "Unknown";
225
+ const sessionSeconds = Math.floor((Date.now() - sessionStartTime) / 1000);
226
+ await session.Runtime.evaluate({
227
+ expression: `if(window.__wt_push_analytics) window.__wt_push_analytics("${health}", ${latency}, ${sessionSeconds})`
228
+ }).catch(() => { });
229
+ }
230
+ catch { /* ignore */ }
231
+ }, 30_000); // 30s interval — reduced from 5s to cut idle CDP overhead (6 round-trips/min → 1)
232
+ // Register in the per-tab map so cleanupAll() and the session-close handler can
233
+ // cancel it without relying on a single overwriteable global slot.
234
+ analyticsIntervals.set(targetId, analyticsInterval);
235
+ // unref() so this polling interval does not pin the Node.js event loop.
236
+ // An orphaned MCP process (IDE disconnected, browser closed) would otherwise
237
+ // stay alive indefinitely due to this 30s interval firing forever.
238
+ analyticsInterval.unref();
239
+ }
240
+ // ─── Main connection entry point ──────────────────────────────────────────────
241
+ export async function getOrConnect(config) {
242
+ if (getConfigMode(config) === "attach") {
243
+ throw new Error("[!] Attach mode (connect to your real browser) coming soon. Use session mode for now.");
244
+ }
245
+ const activeClient = sessionRegistry.getActive(getConfigAgentId(config));
246
+ if (activeClient) {
247
+ try {
248
+ await activeClient.session.Browser.getVersion();
249
+ return activeClient;
250
+ }
251
+ catch {
252
+ logDebug("[Weavetab CDP] Connection lost. Reconnecting...");
253
+ clearProfileVerificationCache(activeClient.targetId);
254
+ sessionRegistry.unregister(activeClient.targetId);
255
+ }
256
+ }
257
+ const releaseMutex = await acquireMutex();
258
+ try {
259
+ const sessionLock = readLock();
260
+ if (sessionLock && sessionLock.mode === "session") {
261
+ if (isLockPending(sessionLock)) {
262
+ logDebug(`[Weavetab] Launch in progress by PID ${sessionLock.pid}. Waiting up to 5s...`);
263
+ for (let i = 0; i < 5; i++) {
264
+ await new Promise(r => setTimeout(r, 1000));
265
+ const updatedLock = readLock();
266
+ if (!updatedLock || !isLockPending(updatedLock)) {
267
+ break;
268
+ }
269
+ }
270
+ }
271
+ const freshLock = readLock();
272
+ if (freshLock) {
273
+ const valid = await isLockValid(freshLock);
274
+ if (valid) {
275
+ actualPort = freshLock.port;
276
+ }
277
+ else {
278
+ logDebug("[Weavetab CDP] Stale lock detected — clearing.");
279
+ clearLock();
280
+ actualPort = null;
281
+ }
282
+ }
283
+ else {
284
+ actualPort = null;
285
+ }
286
+ }
287
+ else if (sessionLock) {
288
+ actualPort = null;
289
+ }
290
+ let port = actualPort ?? null;
291
+ let targets;
292
+ try {
293
+ if (!port)
294
+ throw new Error("no port yet");
295
+ targets = await withCDPTimeout(CDP.List({ host: "127.0.0.1", port }), 5000, "CDP.List existing");
296
+ actualPort = port;
297
+ }
298
+ catch {
299
+ logDebug(`[Weavetab CDP] No browser on port ${port ?? "auto"}. Launching new instance...`);
300
+ // Only kill stale Weavetab processes — never kill the PID belonging to a
301
+ // still-valid lock. Two server processes sharing a profile would otherwise kill
302
+ // each other's browser on every reconnect.
303
+ const profileDir = getSessionProfile(getConfigCookies(config), getConfigProfileDir(config));
304
+ try {
305
+ const currentLock = readLock();
306
+ if (currentLock && await isLockValid(currentLock)) {
307
+ logDebug(`[Weavetab Security] Skipping safelyKill — current lock PID ${currentLock.pid} is still valid.`);
308
+ }
309
+ else {
310
+ safelyKillWeavetabProcesses(profileDir);
311
+ }
312
+ }
313
+ catch { /* ignore */ }
314
+ const launchResult = await launchWithBridge(config);
315
+ port = launchResult.port;
316
+ activeEngine = launchResult.engine ?? "chromium";
317
+ actualPort = port;
318
+ targets = await withCDPTimeout(CDP.List({ host: "127.0.0.1", port }), 5000, "CDP.List new");
319
+ }
320
+ // Pick best target
321
+ const metaSession = await withCDPTimeout(CDP({ host: "127.0.0.1", port }), 5000, "CDP metaSession");
322
+ let bctxId = sessionRegistry.getBrowserContextIdForAgent(getConfigAgentId(config));
323
+ if (!bctxId) {
324
+ // When cookies:true, use the default browser context so profile cookies,
325
+ // sessions, and extensions are accessible. Only create an isolated incognito
326
+ // context when cookies are disabled (privacy/sandbox mode).
327
+ const cookiesEnabled = getConfigCookies(config);
328
+ if (cookiesEnabled) {
329
+ // Use default context — cookies from profile are available
330
+ bctxId = undefined;
331
+ }
332
+ else {
333
+ try {
334
+ const res = await metaSession.Target.createBrowserContext({ disposeOnDetach: false });
335
+ bctxId = res.browserContextId;
336
+ sessionRegistry.setBrowserContextIdForAgent(getConfigAgentId(config), bctxId);
337
+ }
338
+ catch (e) {
339
+ logDebug("[Weavetab CDP] Context creation failed (might not be supported by this browser), falling back to default context.", e);
340
+ }
341
+ }
342
+ }
343
+ const { targetInfos } = await metaSession.Target.getTargets();
344
+ let agentTargets = targetInfos.filter(t => t.type === "page" && (!bctxId || t.browserContextId === bctxId));
345
+ let pageTarget = agentTargets.find(t => t.url !== "about:blank" && !t.url.startsWith("chrome://") && !t.url.startsWith("chrome-extension://")) ?? agentTargets.find(t => !t.url.startsWith("chrome-extension://")) ?? agentTargets[0];
346
+ if (!pageTarget || pageTarget.url === "about:blank") {
347
+ const { targetId } = await metaSession.Target.createTarget({ url: "chrome://newtab/", browserContextId: bctxId });
348
+ // Keep the stale about:blank ref so we can close it below
349
+ const staleId = pageTarget?.targetId;
350
+ pageTarget = { targetId, url: "chrome://newtab/", title: "New Tab", type: "page" };
351
+ // If we replaced an about:blank, close it now
352
+ if (staleId) {
353
+ await metaSession.Target.closeTarget({ targetId: staleId }).catch(() => { });
354
+ }
355
+ }
356
+ // Close stale about:blank tabs to avoid visual clutter on each launch
357
+ try {
358
+ const blankTabs = agentTargets.filter(t => t.url === "about:blank" && t.targetId !== pageTarget.targetId);
359
+ for (const bt of blankTabs) {
360
+ await metaSession.Target.closeTarget({ targetId: bt.targetId }).catch(() => { });
361
+ }
362
+ }
363
+ catch { /* non-fatal */ }
364
+ await metaSession.close();
365
+ if (!pageTarget)
366
+ throw new Error("No active page tab found even after launching.");
367
+ const session = await withCDPTimeout(CDP({ host: "127.0.0.1", port, target: pageTarget.targetId }), 5000, "CDP connect target");
368
+ // Auto-cleanup on CDP WebSocket close/error (browser crash, tab close, etc.)
369
+ session.on("close", () => {
370
+ logDebug("[Weavetab CDP] Session WebSocket closed — unregistering.");
371
+ // Cancel this tab's analytics interval so it doesn't run after disconnect
372
+ const tid = pageTarget.targetId;
373
+ const iv = analyticsIntervals.get(tid);
374
+ if (iv) {
375
+ clearInterval(iv);
376
+ analyticsIntervals.delete(tid);
377
+ }
378
+ clearProfileVerificationCache(tid);
379
+ import("./omni_broker.js").then(m => m.cleanupOmniBroker(tid)).catch(() => { });
380
+ import("../tools/network_intercept.js").then(m => m.cleanupNetworkIntercept(session)).catch(() => { });
381
+ sessionRegistry.unregister(tid);
382
+ });
383
+ session.on("error", (err) => {
384
+ logDebug("[Weavetab CDP] Session WebSocket error:", err?.message ?? err);
385
+ });
386
+ // Validate connection health
387
+ try {
388
+ await session.Browser.getVersion();
389
+ }
390
+ catch {
391
+ throw new Error("Failed health check: CDP session connected but getVersion failed.");
392
+ }
393
+ await Promise.all([
394
+ session.Accessibility.enable(),
395
+ session.DOM.enable(),
396
+ session.Page.enable(),
397
+ session.Runtime.enable(),
398
+ session.Network.enable().catch(() => { }),
399
+ session.Performance.enable().catch(() => { })
400
+ ]);
401
+ await wireUiConfigBinding(session);
402
+ // Verify browser fingerprint at connect time
403
+ const fingerprintLock = readLock();
404
+ if (fingerprintLock && fingerprintLock.fingerprint) {
405
+ const fingerprintCheck = await verifyBrowserFingerprint(session, {
406
+ pid: fingerprintLock.pid || 0,
407
+ port: fingerprintLock.port,
408
+ profileDir: fingerprintLock.profileDir,
409
+ browserPath: fingerprintLock.browserPath,
410
+ launchTimestamp: fingerprintLock.fingerprint.launchTimestamp,
411
+ userAgent: fingerprintLock.fingerprint.userAgent || "",
412
+ cdpVersion: fingerprintLock.fingerprint.cdpVersion || "",
413
+ });
414
+ if (!fingerprintCheck.valid) {
415
+ logDebug(`[Weavetab Security] Browser fingerprint verification failed: ${fingerprintCheck.reason}`);
416
+ // Continue anyway for now - in production this should be a hard failure
417
+ }
418
+ else {
419
+ logDebug(`[Weavetab Security] Browser fingerprint verified successfully`);
420
+ }
421
+ }
422
+ // NOTE: initSession() already installs the frameNavigated handler (overlay
423
+ // re-injection + clearSnapshot + clearViewportCache). Do NOT add a second
424
+ // one here — it would fire twice on every navigation, doubling CDP load.
425
+ await initSession(session, pageTarget.targetId, port, config);
426
+ // Verify profile isolation at session init time (cached, no per-call overhead)
427
+ const profileCheck = await verifyProfileIsolation(session, config, pageTarget.targetId);
428
+ if (!profileCheck.verified) {
429
+ logDebug(`[Weavetab Security] Profile isolation verification failed - using unexpected profile`);
430
+ logDebug(`[Weavetab Security] Expected: ${profileCheck.expectedProfile}`);
431
+ logDebug(`[Weavetab Security] Actual: ${profileCheck.actualProfile || 'unknown'}`);
432
+ // Continue anyway for now - in production this should be a hard failure
433
+ }
434
+ else {
435
+ logDebug(`[Weavetab Security] Profile isolation verified successfully`);
436
+ }
437
+ await session.Target.setDiscoverTargets({ discover: true });
438
+ session.on("Target.targetActivated", (event) => {
439
+ const activatedId = event?.targetInfo?.targetId ?? event?.targetId;
440
+ if (activatedId) {
441
+ const client = sessionRegistry.getAll().find(c => c.targetId === activatedId);
442
+ if (client) {
443
+ sessionRegistry.setActive(activatedId, client.agentId);
444
+ }
445
+ }
446
+ });
447
+ session.Target.on("targetCreated", async (event) => {
448
+ if (event.targetInfo.type === "page") {
449
+ sessionRegistry.notifyNewTab(event.targetInfo.targetId);
450
+ recordTabActivity(event.targetInfo.targetId);
451
+ try {
452
+ const bctxId = event.targetInfo.browserContextId;
453
+ const agentIdForTab = bctxId ? sessionRegistry.getAgentForBrowserContextId(bctxId) : undefined;
454
+ await connectToTab(event.targetInfo.targetId, port, agentIdForTab, bctxId);
455
+ // Auto-connect to new tab
456
+ }
457
+ catch (e) {
458
+ logDebug(`[Weavetab] Failed to auto-connect to new tab: ${e}`);
459
+ }
460
+ }
461
+ });
462
+ session.Target.on("targetDestroyed", (event) => {
463
+ clearProfileVerificationCache(event.targetId);
464
+ sessionRegistry.unregister(event.targetId);
465
+ });
466
+ session.Target.on("targetInfoChanged", (event) => {
467
+ const changedId = event.targetInfo.targetId;
468
+ recordTabActivity(changedId);
469
+ const match = sessionRegistry.getAll().find(c => c.targetId === changedId);
470
+ if (match) {
471
+ match.url = event.targetInfo.url;
472
+ match.title = event.targetInfo.title;
473
+ }
474
+ });
475
+ logDebug(`[Weavetab CDP] [OK] Active tab: ${pageTarget.title || pageTarget.url}`);
476
+ const client = {
477
+ session,
478
+ targetId: pageTarget.targetId,
479
+ url: pageTarget.url,
480
+ title: pageTarget.title,
481
+ overlayInstalled: false,
482
+ agentId: getConfigAgentId(config),
483
+ browserContextId: bctxId,
484
+ };
485
+ sessionRegistry.register(pageTarget.targetId, client);
486
+ // Wire overlay into all other open tabs for this agent
487
+ const otherPages = agentTargets.filter(t => t.targetId !== pageTarget.targetId);
488
+ for (const t of otherPages) {
489
+ connectToTab(t.targetId, port, getConfigAgentId(config), bctxId).catch(() => { });
490
+ }
491
+ return client;
492
+ }
493
+ finally {
494
+ await releaseMutex();
495
+ }
496
+ }
497
+ export async function getTabList(agentId) {
498
+ const config = getCachedConfig();
499
+ // Validate the lock before blindly connecting (same two-factor check that
500
+ // getOrConnect() performs). A stale lock after a server restart would otherwise
501
+ // produce raw ECONNREFUSED.
502
+ let port = actualPort;
503
+ if (!port) {
504
+ const lock = readLock();
505
+ if (lock && lock.mode === "session") {
506
+ const valid = await isLockValid(lock);
507
+ if (valid) {
508
+ port = lock.port;
509
+ }
510
+ else {
511
+ logDebug("[Weavetab CDP] getTabList: stale lock detected — clearing.");
512
+ clearLock();
513
+ port = null;
514
+ }
515
+ }
516
+ }
517
+ if (!port)
518
+ throw new Error("[!] No active browser session. Run browser_go first.");
519
+ try {
520
+ const metaSession = await withCDPTimeout(CDP({ host: "127.0.0.1", port }), 5000, "getTabList CDP");
521
+ const { targetInfos } = await withCDPTimeout(metaSession.Target.getTargets(), 5000, "getTabList getTargets");
522
+ await metaSession.close();
523
+ const aid = agentId || getConfigAgentId(config);
524
+ const bctxId = sessionRegistry.getBrowserContextIdForAgent(aid);
525
+ return targetInfos
526
+ .filter(t => t.type === "page" && (!bctxId || t.browserContextId === bctxId))
527
+ .map(t => ({
528
+ id: t.targetId,
529
+ title: t.title,
530
+ url: t.url,
531
+ type: t.type,
532
+ }));
533
+ }
534
+ catch {
535
+ throw new Error(`[!] Weavetab cannot reach browser on port ${port}.`);
536
+ }
537
+ }
538
+ // Only allow the UI to update cosmetic/dev settings via the binding.
539
+ // Never allow overwriting security-critical fields (role, browserPath, block, allow, profileDir, etc.).
540
+ const UI_ALLOWED_FIELDS = new Set(["devMode", "visuals", "darkMode"]);
541
+ async function wireUiConfigBinding(session) {
542
+ try {
543
+ await session.Runtime.addBinding({ name: "__wt_save_ui_config" });
544
+ session.Runtime.on("bindingCalled", (event) => {
545
+ if (event.name === "__wt_save_ui_config") {
546
+ try {
547
+ const payload = JSON.parse(event.payload);
548
+ if (typeof payload !== "object" || payload === null)
549
+ return;
550
+ const currentConfig = loadConfigSync();
551
+ const safe = {};
552
+ for (const k of Object.keys(payload)) {
553
+ if (UI_ALLOWED_FIELDS.has(k))
554
+ safe[k] = payload[k];
555
+ }
556
+ if (Object.keys(safe).length === 0)
557
+ return;
558
+ saveConfig({ ...currentConfig, ...safe });
559
+ }
560
+ catch (e) {
561
+ logDebug("[Weavetab] Error saving config from UI:", e);
562
+ }
563
+ }
564
+ });
565
+ }
566
+ catch { /* already bound */ }
567
+ }
568
+ export async function connectToTab(targetId, port, agentId, browserContextId) {
569
+ const existing = sessionRegistry.getAll().find(c => c.targetId === targetId);
570
+ if (existing)
571
+ return existing;
572
+ const lock = readLock();
573
+ const p = port || actualPort || lock?.port;
574
+ if (!p)
575
+ throw new Error("[!] No active browser session. Run browser_go first.");
576
+ const targets = await withCDPTimeout(CDP.List({ host: "127.0.0.1", port: p }), 5000, "connectToTab CDP.List");
577
+ const t = targets.find(t => t.id === targetId);
578
+ if (!t)
579
+ throw new Error("Tab not found");
580
+ const session = await withCDPTimeout(CDP({ host: "127.0.0.1", port: p, target: targetId }), 5000, "connectToTab CDP");
581
+ session.on("close", () => {
582
+ logDebug("[Weavetab CDP] connectToTab session closed — unregistering.");
583
+ // Cancel this tab's analytics interval so it doesn't outlive the session
584
+ const iv = analyticsIntervals.get(targetId);
585
+ if (iv) {
586
+ clearInterval(iv);
587
+ analyticsIntervals.delete(targetId);
588
+ }
589
+ clearProfileVerificationCache(targetId);
590
+ import("./omni_broker.js").then(m => m.cleanupOmniBroker(targetId)).catch(() => { });
591
+ import("../tools/network_intercept.js").then(m => m.cleanupNetworkIntercept(session)).catch(() => { });
592
+ sessionRegistry.unregister(targetId);
593
+ });
594
+ session.on("error", (err) => {
595
+ logDebug("[Weavetab CDP] connectToTab session error:", err?.message ?? err);
596
+ });
597
+ await Promise.all([
598
+ session.Accessibility.enable(),
599
+ session.DOM.enable(),
600
+ session.Page.enable(),
601
+ session.Runtime.enable(),
602
+ session.Network.enable().catch(() => { }),
603
+ session.Performance.enable().catch(() => { })
604
+ ]);
605
+ await wireUiConfigBinding(session);
606
+ const currentConfig = getCachedConfig();
607
+ await initSession(session, targetId, p, currentConfig);
608
+ // NOTE: initSession() already installs a frameNavigated handler that handles
609
+ // clearSnapshot, clearViewportCache, and ensureVisualLayer. Do NOT add a second
610
+ // one here — it would double the CDP work on every navigation.
611
+ const actualAgentId = agentId || getConfigAgentId(currentConfig);
612
+ const client = {
613
+ session,
614
+ targetId,
615
+ url: t.url,
616
+ title: t.title,
617
+ overlayInstalled: false,
618
+ agentId: actualAgentId,
619
+ browserContextId,
620
+ };
621
+ sessionRegistry.register(targetId, client);
622
+ return client;
623
+ }
624
+ // ─── Cleanup: called on process exit/signal ───────────────────────────────────
625
+ /**
626
+ * Cancel analytics intervals and close the CDP WebSocket sessions without
627
+ * touching the browser process. Safe to call in server mode — the browser
628
+ * remains alive for the next client.
629
+ */
630
+ export async function cleanupIntervals() {
631
+ // Cancel every per-tab analytics interval.
632
+ for (const iv of analyticsIntervals.values())
633
+ clearInterval(iv);
634
+ analyticsIntervals.clear();
635
+ // Close CDP WebSocket connections so the Node process can exit cleanly,
636
+ // but do NOT send Browser.close() — that would kill the browser process.
637
+ const clients = sessionRegistry.getAll();
638
+ for (const client of clients) {
639
+ try {
640
+ await client.session.close();
641
+ }
642
+ catch { /* ignore */ }
643
+ }
644
+ }
645
+ /**
646
+ * Full cleanup: intervals + WebSocket sessions + Browser.close().
647
+ * Only safe to call when the browser should also be terminated (non-server mode,
648
+ * SIGINT, SIGTERM, explicit user-requested shutdown).
649
+ */
650
+ export async function cleanupAll() {
651
+ const clients = sessionRegistry.getAll();
652
+ // Try to cleanly shut down the browser using the first available CDP session.
653
+ if (clients.length > 0) {
654
+ try {
655
+ await clients[0].session.Browser.close();
656
+ }
657
+ catch { /* ignore */ }
658
+ }
659
+ // Cancel intervals and close WebSocket sessions.
660
+ await cleanupIntervals();
661
+ }