@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.
- package/AGENTS.md +48 -0
- package/ARCHITECTURE.md +590 -0
- package/LICENSE +631 -0
- package/README.md +909 -0
- package/SECURITY.md +196 -0
- package/TROUBLESHOOTING.md +365 -0
- package/dist/anticaptcha/detector.d.ts +49 -0
- package/dist/anticaptcha/detector.js +465 -0
- package/dist/anticaptcha/integration.d.ts +36 -0
- package/dist/anticaptcha/integration.js +123 -0
- package/dist/anticaptcha/solver.d.ts +27 -0
- package/dist/anticaptcha/solver.js +552 -0
- package/dist/audit/logger.d.ts +13 -0
- package/dist/audit/logger.js +177 -0
- package/dist/audit/telemetry.d.ts +36 -0
- package/dist/audit/telemetry.js +162 -0
- package/dist/cdp/bridge.d.ts +16 -0
- package/dist/cdp/bridge.js +826 -0
- package/dist/cdp/confirm.d.ts +26 -0
- package/dist/cdp/confirm.js +79 -0
- package/dist/cdp/connector.d.ts +37 -0
- package/dist/cdp/connector.js +661 -0
- package/dist/cdp/extension.d.ts +36 -0
- package/dist/cdp/extension.js +118 -0
- package/dist/cdp/finder.d.ts +1 -0
- package/dist/cdp/finder.js +2 -0
- package/dist/cdp/helpers.d.ts +11 -0
- package/dist/cdp/helpers.js +195 -0
- package/dist/cdp/label_cache.d.ts +12 -0
- package/dist/cdp/label_cache.js +51 -0
- package/dist/cdp/lock.d.ts +33 -0
- package/dist/cdp/lock.js +206 -0
- package/dist/cdp/omni_broker.d.ts +17 -0
- package/dist/cdp/omni_broker.js +119 -0
- package/dist/cdp/profiles.d.ts +1 -0
- package/dist/cdp/profiles.js +87 -0
- package/dist/cdp/pruner.d.ts +41 -0
- package/dist/cdp/pruner.js +263 -0
- package/dist/cdp/ref_cache.d.ts +101 -0
- package/dist/cdp/ref_cache.js +234 -0
- package/dist/cdp/retry.d.ts +5 -0
- package/dist/cdp/retry.js +39 -0
- package/dist/cdp/retry_enhanced.d.ts +44 -0
- package/dist/cdp/retry_enhanced.js +102 -0
- package/dist/cdp/security.d.ts +50 -0
- package/dist/cdp/security.js +237 -0
- package/dist/cdp/verify.d.ts +39 -0
- package/dist/cdp/verify.js +143 -0
- package/dist/cdp/walker.d.ts +91 -0
- package/dist/cdp/walker.js +896 -0
- package/dist/cdp/walker.test.d.ts +2 -0
- package/dist/cdp/walker.test.js +172 -0
- package/dist/cdp/websocket_transport.d.ts +15 -0
- package/dist/cdp/websocket_transport.js +118 -0
- package/dist/cli/plugin.d.ts +9 -0
- package/dist/cli/plugin.js +1134 -0
- package/dist/cli/updater.d.ts +1 -0
- package/dist/cli/updater.js +72 -0
- package/dist/config/loader.d.ts +120 -0
- package/dist/config/loader.js +704 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1248 -0
- package/dist/intelligence/hints.d.ts +11 -0
- package/dist/intelligence/hints.js +39 -0
- package/dist/intelligence/intent.d.ts +33 -0
- package/dist/intelligence/intent.js +171 -0
- package/dist/intelligence/sessionMemory.d.ts +66 -0
- package/dist/intelligence/sessionMemory.js +284 -0
- package/dist/intelligence/sessionMemory.test.d.ts +2 -0
- package/dist/intelligence/sessionMemory.test.js +62 -0
- package/dist/intelligence/trail.d.ts +64 -0
- package/dist/intelligence/trail.js +233 -0
- package/dist/intelligence/trail.test.d.ts +2 -0
- package/dist/intelligence/trail.test.js +110 -0
- package/dist/intelligence/vision.d.ts +24 -0
- package/dist/intelligence/vision.js +151 -0
- package/dist/lifecycle/mission_manager.d.ts +16 -0
- package/dist/lifecycle/mission_manager.js +44 -0
- package/dist/overlay/bubble.d.ts +16 -0
- package/dist/overlay/bubble.js +37 -0
- package/dist/overlay/ghost.d.ts +35 -0
- package/dist/overlay/ghost.js +508 -0
- package/dist/overlay/injector.d.ts +22 -0
- package/dist/overlay/injector.js +198 -0
- package/dist/plugin/interface.d.ts +134 -0
- package/dist/plugin/interface.js +1 -0
- package/dist/plugin/loader.d.ts +18 -0
- package/dist/plugin/loader.js +155 -0
- package/dist/plugin/registry.d.ts +50 -0
- package/dist/plugin/registry.js +234 -0
- package/dist/security/blacklist.d.ts +24 -0
- package/dist/security/blacklist.js +80 -0
- package/dist/security/filter.d.ts +2 -0
- package/dist/security/filter.js +20 -0
- package/dist/security/guard.d.ts +8 -0
- package/dist/security/guard.js +35 -0
- package/dist/security/ratelimit.d.ts +11 -0
- package/dist/security/ratelimit.js +57 -0
- package/dist/security/secrets.d.ts +75 -0
- package/dist/security/secrets.js +365 -0
- package/dist/sensors/actionability.d.ts +81 -0
- package/dist/sensors/actionability.js +303 -0
- package/dist/sensors/guard.d.ts +25 -0
- package/dist/sensors/guard.js +168 -0
- package/dist/sensors/mutations.d.ts +23 -0
- package/dist/sensors/mutations.js +93 -0
- package/dist/sensors/network.d.ts +67 -0
- package/dist/sensors/network.js +193 -0
- package/dist/sensors/operation_monitor.d.ts +80 -0
- package/dist/sensors/operation_monitor.js +203 -0
- package/dist/sensors/pacing.d.ts +44 -0
- package/dist/sensors/pacing.js +111 -0
- package/dist/sensors/thoughts.d.ts +2 -0
- package/dist/sensors/thoughts.js +59 -0
- package/dist/sensors/wait.d.ts +12 -0
- package/dist/sensors/wait.js +44 -0
- package/dist/server.d.ts +7 -0
- package/dist/server.js +1687 -0
- package/dist/state/agent.d.ts +36 -0
- package/dist/state/agent.js +101 -0
- package/dist/state/cache.d.ts +16 -0
- package/dist/state/cache.js +92 -0
- package/dist/state/cache.test.d.ts +2 -0
- package/dist/state/cache.test.js +76 -0
- package/dist/state/delta.d.ts +71 -0
- package/dist/state/delta.js +234 -0
- package/dist/state/delta.test.d.ts +2 -0
- package/dist/state/delta.test.js +279 -0
- package/dist/state/groups.d.ts +15 -0
- package/dist/state/groups.js +80 -0
- package/dist/state/session.d.ts +30 -0
- package/dist/state/session.js +155 -0
- package/dist/tools/accessibility.d.ts +14 -0
- package/dist/tools/accessibility.js +32 -0
- package/dist/tools/ask.d.ts +14 -0
- package/dist/tools/ask.js +49 -0
- package/dist/tools/automation.d.ts +36 -0
- package/dist/tools/automation.js +218 -0
- package/dist/tools/browser_detect.d.ts +13 -0
- package/dist/tools/browser_detect.js +73 -0
- package/dist/tools/burst.d.ts +35 -0
- package/dist/tools/burst.js +344 -0
- package/dist/tools/canvas.d.ts +25 -0
- package/dist/tools/canvas.js +132 -0
- package/dist/tools/captcha.d.ts +38 -0
- package/dist/tools/captcha.js +209 -0
- package/dist/tools/click.d.ts +36 -0
- package/dist/tools/click.js +561 -0
- package/dist/tools/click_and_wait.d.ts +33 -0
- package/dist/tools/click_and_wait.js +149 -0
- package/dist/tools/clipboard.d.ts +29 -0
- package/dist/tools/clipboard.js +279 -0
- package/dist/tools/console.d.ts +21 -0
- package/dist/tools/console.js +107 -0
- package/dist/tools/cookies.d.ts +67 -0
- package/dist/tools/cookies.js +121 -0
- package/dist/tools/design_extract.d.ts +109 -0
- package/dist/tools/design_extract.js +641 -0
- package/dist/tools/dialog.d.ts +24 -0
- package/dist/tools/dialog.js +109 -0
- package/dist/tools/drag.d.ts +15 -0
- package/dist/tools/drag.js +29 -0
- package/dist/tools/emulation.d.ts +33 -0
- package/dist/tools/emulation.js +69 -0
- package/dist/tools/eval.d.ts +20 -0
- package/dist/tools/eval.js +92 -0
- package/dist/tools/fill.d.ts +41 -0
- package/dist/tools/fill.js +667 -0
- package/dist/tools/find.d.ts +30 -0
- package/dist/tools/find.js +60 -0
- package/dist/tools/geo.d.ts +19 -0
- package/dist/tools/geo.js +29 -0
- package/dist/tools/github/analyze.d.ts +4 -0
- package/dist/tools/github/analyze.js +113 -0
- package/dist/tools/github/cache.d.ts +21 -0
- package/dist/tools/github/cache.js +28 -0
- package/dist/tools/github/index.d.ts +4 -0
- package/dist/tools/github/index.js +4 -0
- package/dist/tools/github/issues.d.ts +9 -0
- package/dist/tools/github/issues.js +88 -0
- package/dist/tools/github/pr.d.ts +5 -0
- package/dist/tools/github/pr.js +88 -0
- package/dist/tools/github/rateLimiter.d.ts +4 -0
- package/dist/tools/github/rateLimiter.js +46 -0
- package/dist/tools/github/read.d.ts +34 -0
- package/dist/tools/github/read.js +74 -0
- package/dist/tools/github/repoContext.d.ts +20 -0
- package/dist/tools/github/repoContext.js +129 -0
- package/dist/tools/highlight.d.ts +20 -0
- package/dist/tools/highlight.js +140 -0
- package/dist/tools/hover.d.ts +19 -0
- package/dist/tools/hover.js +57 -0
- package/dist/tools/inspect.d.ts +17 -0
- package/dist/tools/inspect.js +125 -0
- package/dist/tools/integration.test.d.ts +2 -0
- package/dist/tools/integration.test.js +96 -0
- package/dist/tools/key.d.ts +25 -0
- package/dist/tools/key.js +173 -0
- package/dist/tools/macro_compiler.d.ts +7 -0
- package/dist/tools/macro_compiler.js +82 -0
- package/dist/tools/mouse.d.ts +21 -0
- package/dist/tools/mouse.js +62 -0
- package/dist/tools/navigate.d.ts +16 -0
- package/dist/tools/navigate.js +188 -0
- package/dist/tools/network_intercept.d.ts +17 -0
- package/dist/tools/network_intercept.js +151 -0
- package/dist/tools/office/index.d.ts +9 -0
- package/dist/tools/office/index.js +7 -0
- package/dist/tools/office/list.d.ts +15 -0
- package/dist/tools/office/list.js +41 -0
- package/dist/tools/office/parse.d.ts +20 -0
- package/dist/tools/office/parse.js +172 -0
- package/dist/tools/pdf.d.ts +22 -0
- package/dist/tools/pdf.js +42 -0
- package/dist/tools/performance.d.ts +12 -0
- package/dist/tools/performance.js +47 -0
- package/dist/tools/performance_test.d.ts +70 -0
- package/dist/tools/performance_test.js +128 -0
- package/dist/tools/plan.d.ts +39 -0
- package/dist/tools/plan.js +161 -0
- package/dist/tools/pointer.d.ts +35 -0
- package/dist/tools/pointer.js +522 -0
- package/dist/tools/read.d.ts +30 -0
- package/dist/tools/read.js +488 -0
- package/dist/tools/recording.d.ts +45 -0
- package/dist/tools/recording.js +124 -0
- package/dist/tools/response.d.ts +129 -0
- package/dist/tools/response.js +5 -0
- package/dist/tools/scrape.d.ts +24 -0
- package/dist/tools/scrape.js +140 -0
- package/dist/tools/screenshot.d.ts +40 -0
- package/dist/tools/screenshot.js +79 -0
- package/dist/tools/scroll.d.ts +23 -0
- package/dist/tools/scroll.js +196 -0
- package/dist/tools/select.d.ts +13 -0
- package/dist/tools/select.js +217 -0
- package/dist/tools/snapshot.d.ts +27 -0
- package/dist/tools/snapshot.js +99 -0
- package/dist/tools/storage.d.ts +18 -0
- package/dist/tools/storage.js +29 -0
- package/dist/tools/tabs.d.ts +77 -0
- package/dist/tools/tabs.js +342 -0
- package/dist/tools/thought.d.ts +16 -0
- package/dist/tools/thought.js +79 -0
- package/dist/tools/type.d.ts +65 -0
- package/dist/tools/type.js +1061 -0
- package/dist/tools/type_secret.d.ts +23 -0
- package/dist/tools/type_secret.js +229 -0
- package/dist/tools/upload.d.ts +19 -0
- package/dist/tools/upload.js +103 -0
- package/dist/tools/video_compiler.d.ts +24 -0
- package/dist/tools/video_compiler.js +199 -0
- package/dist/tools/viewport.d.ts +44 -0
- package/dist/tools/viewport.js +230 -0
- package/dist/tools/wait.d.ts +19 -0
- package/dist/tools/wait.js +202 -0
- package/dist/tools/wait_for.d.ts +23 -0
- package/dist/tools/wait_for.js +30 -0
- package/dist/tools/windows.d.ts +22 -0
- package/dist/tools/windows.js +57 -0
- package/dist/ui/index.d.ts +1 -0
- package/dist/ui/index.js +12 -0
- package/docs/DEV_MODE.md +68 -0
- package/docs/DOCUMENTATION.md +532 -0
- package/docs/PLUGINS.md +538 -0
- package/docs/TOOLS.md +104 -0
- package/docs/dev_plugin.md +310 -0
- package/ext-dist/background.js +467 -0
- package/ext-dist/content.js +3089 -0
- package/ext-dist/dashboard.html +583 -0
- package/ext-dist/hud/hud.js +73 -0
- package/ext-dist/hud/index.html +20 -0
- package/ext-dist/manifest.json +77 -0
- package/ext-dist/sidepanel/sidepanel.js +25 -0
- package/ext-dist/sidepanel.html +547 -0
- package/ext-dist/styles.css +218 -0
- package/icon.png +0 -0
- package/mcp.json +796 -0
- package/package.json +113 -0
- package/server.json +23 -0
- 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
|
+
}
|