@weavetab/mcp 2.5.0-beta.1 → 2.5.0-beta.2
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 +1 -0
- package/LICENSE +2 -2
- package/README.md +208 -43
- package/SECURITY.md +3 -3
- package/TROUBLESHOOTING.md +1 -1
- package/dist/cdp/bridge.d.ts +10 -0
- package/dist/cdp/bridge.js +284 -78
- package/dist/cdp/connector.d.ts +5 -3
- package/dist/cdp/connector.js +188 -71
- package/dist/cdp/lock.d.ts +9 -6
- package/dist/cdp/lock.js +72 -59
- package/dist/cdp/omni_broker.d.ts +1 -0
- package/dist/cdp/omni_broker.js +12 -6
- package/dist/cdp/profiles.d.ts +10 -0
- package/dist/cdp/profiles.js +65 -0
- package/dist/cdp/walker.js +50 -0
- package/dist/cli/plugin.d.ts +0 -2
- package/dist/cli/plugin.js +31 -683
- package/dist/cli/updater.js +2 -2
- package/dist/config/loader.d.ts +25 -6
- package/dist/config/loader.js +133 -20
- package/dist/index.js +48 -52
- package/dist/overlay/ghost.js +2 -2
- package/dist/overlay/injector.js +1 -3
- package/dist/plugin/extension_bridge.d.ts +40 -0
- package/dist/plugin/extension_bridge.js +158 -0
- package/dist/plugin/interface.d.ts +78 -16
- package/dist/plugin/loader.d.ts +4 -0
- package/dist/plugin/loader.js +178 -57
- package/dist/plugin/registry.js +11 -1
- package/dist/plugin/storage.d.ts +51 -0
- package/dist/plugin/storage.js +164 -0
- package/dist/server.js +213 -121
- package/dist/state/session.d.ts +2 -2
- package/dist/state/session.js +33 -13
- package/dist/tools/browser_detect.d.ts +2 -1
- package/dist/tools/browser_detect.js +6 -3
- package/dist/tools/burst.d.ts +1 -0
- package/dist/tools/burst.js +4 -6
- package/dist/tools/click.js +23 -28
- package/dist/tools/clipboard.js +31 -24
- package/dist/tools/navigate.d.ts +1 -1
- package/dist/tools/navigate.js +28 -10
- package/dist/tools/tabs.d.ts +10 -10
- package/dist/tools/tabs.js +47 -39
- package/dist/tools/type.js +2 -2
- package/docs/DOCUMENTATION.md +63 -6
- package/docs/architecture/blind-injection.md +20 -0
- package/docs/architecture/dual-injection.md +32 -0
- package/docs/architecture/overview.md +49 -0
- package/docs/architecture/security.md +35 -0
- package/docs/architecture/transport.md +37 -0
- package/docs/captcha-stealth/captcha-solver.md +75 -0
- package/docs/captcha-stealth/stealth-physics.md +60 -0
- package/docs/cli/cli-commands.md +44 -0
- package/docs/cli/cli-flags.md +20 -0
- package/docs/cli/cli-introduction.md +20 -0
- package/docs/cli/cli-scripting.md +32 -0
- package/docs/cli/g-ide/ide-antigravity.md +24 -0
- package/docs/cli/g-ide/ide-claude-code.md +24 -0
- package/docs/cli/g-ide/ide-claude-desktop.md +32 -0
- package/docs/cli/g-ide/ide-cursor.md +31 -0
- package/docs/cli/g-ide/ide-generic.md +34 -0
- package/docs/cli/g-ide/ide-opencode.md +24 -0
- package/docs/cli/g-ide/ide-vscode-copilot.md +24 -0
- package/docs/cli/g-ide/ide-windsurf.md +24 -0
- package/docs/configuration/config-locations.md +24 -0
- package/docs/configuration/configuration.md +73 -0
- package/docs/configuration/env-variables.md +19 -0
- package/docs/configuration/folders.md +54 -0
- package/docs/desktop-automation/electron-apps.md +162 -0
- package/docs/get-started/installation.md +47 -0
- package/docs/get-started/introduction.md +60 -0
- package/docs/get-started/quickstart.md +54 -0
- package/docs/get-started/setup-wizard.md +26 -0
- package/docs/skills-ecosystem/skills-overview.md +35 -0
- package/docs/skills-ecosystem/skills-recipes.md +36 -0
- package/docs/tools/tools.md +293 -0
- package/docs/troubleshooting/trouble-browser.md +27 -0
- package/docs/troubleshooting/trouble-config.md +20 -0
- package/docs/troubleshooting/trouble-loop.md +31 -0
- package/ext-dist/content.js +0 -1
- package/mcp.json +8 -6
- package/package.json +11 -5
- package/server.json +3 -3
- package/docs/DEV_MODE.md +0 -68
- package/docs/PLUGINS.md +0 -538
- package/docs/TOOLS.md +0 -104
- package/docs/dev_plugin.md +0 -310
package/dist/cdp/connector.js
CHANGED
|
@@ -7,9 +7,9 @@ import { logDebug } from "../audit/logger.js";
|
|
|
7
7
|
*/
|
|
8
8
|
import CDP from "chrome-remote-interface";
|
|
9
9
|
import { blockStorageAccess } from "../security/guard.js";
|
|
10
|
-
import { launchWithBridge, injectStealthScript } from "./bridge.js";
|
|
10
|
+
import { launchWithBridge, launchElectronApp, injectStealthScript } from "./bridge.js";
|
|
11
11
|
import { setupOverlay, ensureVisualLayer } from "../overlay/injector.js";
|
|
12
|
-
import { onConfigChange, saveConfig, loadConfigSync, getCachedConfig, getConfigAgentId, getConfigCookies, getConfigProfileDir, getConfigMode } from "../config/loader.js";
|
|
12
|
+
import { onConfigChange, saveConfig, loadConfigSync, getCachedConfig, getConfigAgentId, getConfigCookies, getConfigProfileDir, getConfigMode, getElectronAppConfig, getConfigExtension } from "../config/loader.js";
|
|
13
13
|
import { sessionRegistry } from "../state/session.js";
|
|
14
14
|
import { initConsoleBuffering } from "../tools/console.js";
|
|
15
15
|
import { registerMutationListener, clearTabCache } from "../state/cache.js";
|
|
@@ -95,12 +95,21 @@ async function waitForPageReady(session) {
|
|
|
95
95
|
});
|
|
96
96
|
}
|
|
97
97
|
// ─── Session init (overlay + re-injection) ───────────────────────────────────
|
|
98
|
-
async function initSession(session, targetId, port, config) {
|
|
98
|
+
async function initSession(session, targetId, port, config, isElectron) {
|
|
99
99
|
initDialogHandler(session);
|
|
100
100
|
// Initialize Omni-Context Broker for iframes
|
|
101
101
|
const { initOmniBroker } = await import("./omni_broker.js");
|
|
102
102
|
await initOmniBroker(session, targetId, port);
|
|
103
|
-
if (
|
|
103
|
+
if (isElectron) {
|
|
104
|
+
// Electron desktop app initialization
|
|
105
|
+
try {
|
|
106
|
+
await session.Page.addScriptToEvaluateOnNewDocument({ source: WT_REGISTRY_INIT_EXPR });
|
|
107
|
+
}
|
|
108
|
+
catch { /* non-fatal */ }
|
|
109
|
+
// Direct DOM minimal overlay injection without Chrome Extension
|
|
110
|
+
await ensureVisualLayer(session, config);
|
|
111
|
+
}
|
|
112
|
+
else if (activeEngine === "chromium") {
|
|
104
113
|
// Only block cookies/storage when cookies mode is disabled
|
|
105
114
|
if (!getConfigCookies(config)) {
|
|
106
115
|
await blockStorageAccess(session);
|
|
@@ -142,7 +151,7 @@ async function initSession(session, targetId, port, config) {
|
|
|
142
151
|
clearTabCache(targetId);
|
|
143
152
|
// Invalidate URC on navigation — elements from the old page are stale
|
|
144
153
|
urcInvalidate(targetId);
|
|
145
|
-
if (activeEngine === "chromium") {
|
|
154
|
+
if (isElectron || activeEngine === "chromium") {
|
|
146
155
|
try {
|
|
147
156
|
await waitForPageReady(session);
|
|
148
157
|
const cfg = getCachedConfig();
|
|
@@ -154,8 +163,8 @@ async function initSession(session, targetId, port, config) {
|
|
|
154
163
|
catch { /* ignore */ }
|
|
155
164
|
}
|
|
156
165
|
});
|
|
157
|
-
// Diagnostic: verify extension content script loaded
|
|
158
|
-
if (activeEngine === "chromium") {
|
|
166
|
+
// Diagnostic: verify extension content script loaded (Standard browser only)
|
|
167
|
+
if (!isElectron && activeEngine === "chromium" && getConfigExtension(config) !== false) {
|
|
159
168
|
try {
|
|
160
169
|
// Check for multiple indicators that extension is loaded
|
|
161
170
|
const { result: extCheck } = await session.Runtime.evaluate({
|
|
@@ -174,29 +183,12 @@ async function initSession(session, targetId, port, config) {
|
|
|
174
183
|
else {
|
|
175
184
|
logDebug(`[Weavetab] [!] Extension content script NOT detected — HUD/overlay may not work`);
|
|
176
185
|
logDebug(`[Weavetab] [!] Try reloading page or check if extension loaded in browser://extensions`);
|
|
177
|
-
// Try to inject a basic error indicator into the page
|
|
178
|
-
try {
|
|
179
|
-
await session.Runtime.evaluate({
|
|
180
|
-
expression: `
|
|
181
|
-
(function() {
|
|
182
|
-
const div = document.createElement('div');
|
|
183
|
-
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;';
|
|
184
|
-
div.textContent = 'WeaveTab Extension Not Loaded';
|
|
185
|
-
document.body.appendChild(div);
|
|
186
|
-
setTimeout(() => div.remove(), 5000);
|
|
187
|
-
})()
|
|
188
|
-
`
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
catch { /* ignore */ }
|
|
192
186
|
}
|
|
193
187
|
}
|
|
194
188
|
catch (checkErr) {
|
|
195
189
|
logDebug(`[Weavetab] [!] Extension health check failed: ${checkErr}`);
|
|
196
190
|
}
|
|
197
|
-
|
|
198
|
-
// Push config + initial idle state to extension right away (Chrome only)
|
|
199
|
-
if (activeEngine === "chromium") {
|
|
191
|
+
// Push config + initial idle state to extension right away (Chrome only)
|
|
200
192
|
const { sendToExtension } = await import("../cdp/helpers.js");
|
|
201
193
|
sendToExtension(session, {
|
|
202
194
|
type: 'Weavetab_CONFIG',
|
|
@@ -207,8 +199,7 @@ async function initSession(session, targetId, port, config) {
|
|
|
207
199
|
state: 'idle',
|
|
208
200
|
}).catch(() => { });
|
|
209
201
|
}
|
|
210
|
-
// Analytics ping every
|
|
211
|
-
// independently cancelled when the tab closes or cleanupAll() is called.
|
|
202
|
+
// Analytics ping every 30s
|
|
212
203
|
const sessionStartTime = Date.now();
|
|
213
204
|
const analyticsInterval = setInterval(async () => {
|
|
214
205
|
try {
|
|
@@ -225,34 +216,135 @@ async function initSession(session, targetId, port, config) {
|
|
|
225
216
|
}).catch(() => { });
|
|
226
217
|
}
|
|
227
218
|
catch { /* ignore */ }
|
|
228
|
-
}, 30_000);
|
|
229
|
-
// Register in the per-tab map so cleanupAll() and the session-close handler can
|
|
230
|
-
// cancel it without relying on a single overwriteable global slot.
|
|
219
|
+
}, 30_000);
|
|
231
220
|
analyticsIntervals.set(targetId, analyticsInterval);
|
|
232
|
-
// unref() so this polling interval does not pin the Node.js event loop.
|
|
233
|
-
// An orphaned MCP process (IDE disconnected, browser closed) would otherwise
|
|
234
|
-
// stay alive indefinitely due to this 30s interval firing forever.
|
|
235
221
|
analyticsInterval.unref();
|
|
236
222
|
}
|
|
237
223
|
// ─── Main connection entry point ──────────────────────────────────────────────
|
|
238
|
-
export async function getOrConnect(config) {
|
|
239
|
-
if (getConfigMode(config) === "attach") {
|
|
224
|
+
export async function getOrConnect(config, targetApp) {
|
|
225
|
+
if (getConfigMode(config) === "attach" && !targetApp) {
|
|
240
226
|
throw new Error("[!] Attach mode (connect to your real browser) coming soon. Use session mode for now.");
|
|
241
227
|
}
|
|
242
|
-
|
|
228
|
+
// 1. Check existing active session for this agent & app
|
|
229
|
+
const activeClient = sessionRegistry.getActive(getConfigAgentId(config), targetApp);
|
|
243
230
|
if (activeClient) {
|
|
244
231
|
try {
|
|
245
232
|
await activeClient.session.Browser.getVersion();
|
|
246
233
|
return activeClient;
|
|
247
234
|
}
|
|
248
235
|
catch {
|
|
249
|
-
logDebug(
|
|
236
|
+
logDebug(`[Weavetab CDP] Connection lost for ${targetApp || "browser"}. Reconnecting...`);
|
|
250
237
|
clearProfileVerificationCache(activeClient.targetId);
|
|
251
238
|
sessionRegistry.unregister(activeClient.targetId);
|
|
252
239
|
}
|
|
253
240
|
}
|
|
254
|
-
const releaseMutex = await acquireMutex();
|
|
241
|
+
const releaseMutex = await acquireMutex(targetApp);
|
|
255
242
|
try {
|
|
243
|
+
if (targetApp) {
|
|
244
|
+
// ──────────────── ELECTRON APP CONNECTION FLOW ────────────────
|
|
245
|
+
const appCfg = getElectronAppConfig(targetApp, config);
|
|
246
|
+
if (!appCfg) {
|
|
247
|
+
throw new Error(`[Weavetab] ✗ Electron app "${targetApp}" is not configured in config.json.\n` +
|
|
248
|
+
` Please add an entry under "electron": { "${targetApp}": "C:\\\\path\\\\to\\\\app.exe" } in your config.`);
|
|
249
|
+
}
|
|
250
|
+
let port = null;
|
|
251
|
+
const sessionLock = readLock(targetApp);
|
|
252
|
+
if (sessionLock && sessionLock.mode === "session") {
|
|
253
|
+
if (isLockPending(sessionLock)) {
|
|
254
|
+
logDebug(`[Weavetab] Launch in progress for ${targetApp} by PID ${sessionLock.pid}. Waiting up to 5s...`);
|
|
255
|
+
for (let i = 0; i < 5; i++) {
|
|
256
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
257
|
+
const updatedLock = readLock(targetApp);
|
|
258
|
+
if (!updatedLock || !isLockPending(updatedLock))
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const freshLock = readLock(targetApp);
|
|
263
|
+
if (freshLock && (await isLockValid(freshLock))) {
|
|
264
|
+
port = freshLock.port;
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
clearLock(targetApp);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
let targets = [];
|
|
271
|
+
if (port) {
|
|
272
|
+
try {
|
|
273
|
+
targets = await withCDPTimeout(CDP.List({ host: "127.0.0.1", port }), 3000, "CDP.List existing Electron");
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
port = null;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (!port || targets.length === 0) {
|
|
280
|
+
logDebug(`[Weavetab CDP] Launching Electron app ${targetApp}...`);
|
|
281
|
+
const launchResult = await launchElectronApp(targetApp, config);
|
|
282
|
+
port = launchResult.port;
|
|
283
|
+
// Poll for target window availability (Electron may take up to 2-3s to render initial window)
|
|
284
|
+
for (let retry = 0; retry < 10; retry++) {
|
|
285
|
+
try {
|
|
286
|
+
targets = await withCDPTimeout(CDP.List({ host: "127.0.0.1", port }), 2000, "CDP.List Electron retry");
|
|
287
|
+
if (targets.length > 0)
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
catch { /* wait */ }
|
|
291
|
+
await new Promise(r => setTimeout(r, 500));
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
let pageTarget = targets.find(t => t.type === "page" || t.type === "webview" || t.type === "app") ?? targets[0];
|
|
295
|
+
if (!pageTarget) {
|
|
296
|
+
throw new Error(`[Weavetab] ✗ No window or webview target found in Electron app "${targetApp}".`);
|
|
297
|
+
}
|
|
298
|
+
const targetId = pageTarget.id || pageTarget.targetId;
|
|
299
|
+
const session = await withCDPTimeout(CDP({ host: "127.0.0.1", port, target: targetId }), 5000, "CDP connect Electron target");
|
|
300
|
+
session.on("close", () => {
|
|
301
|
+
logDebug(`[Weavetab CDP] Electron session WebSocket closed (${targetApp}) — unregistering.`);
|
|
302
|
+
const iv = analyticsIntervals.get(targetId);
|
|
303
|
+
if (iv) {
|
|
304
|
+
clearInterval(iv);
|
|
305
|
+
analyticsIntervals.delete(targetId);
|
|
306
|
+
}
|
|
307
|
+
clearProfileVerificationCache(targetId);
|
|
308
|
+
import("./omni_broker.js").then(m => m.cleanupOmniBroker(targetId)).catch(() => { });
|
|
309
|
+
import("../tools/network_intercept.js").then(m => m.cleanupNetworkIntercept(session)).catch(() => { });
|
|
310
|
+
sessionRegistry.unregister(targetId);
|
|
311
|
+
});
|
|
312
|
+
session.on("error", (err) => {
|
|
313
|
+
logDebug(`[Weavetab CDP] Electron session WebSocket error (${targetApp}):`, err?.message ?? err);
|
|
314
|
+
});
|
|
315
|
+
// Validate connection health
|
|
316
|
+
try {
|
|
317
|
+
await session.Browser.getVersion();
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
throw new Error("Failed health check: CDP session connected to Electron app but getVersion failed.");
|
|
321
|
+
}
|
|
322
|
+
await Promise.all([
|
|
323
|
+
session.Accessibility.enable(),
|
|
324
|
+
session.DOM.enable(),
|
|
325
|
+
session.Page.enable(),
|
|
326
|
+
session.Runtime.enable(),
|
|
327
|
+
session.Network.enable().catch(() => { }),
|
|
328
|
+
session.Performance.enable().catch(() => { })
|
|
329
|
+
]);
|
|
330
|
+
await wireUiConfigBinding(session);
|
|
331
|
+
await initSession(session, targetId, port, config, true);
|
|
332
|
+
await session.Target.setDiscoverTargets({ discover: true }).catch(() => { });
|
|
333
|
+
logDebug(`[Weavetab CDP] [OK] Active Electron target: ${pageTarget.title || pageTarget.url || targetApp}`);
|
|
334
|
+
const client = {
|
|
335
|
+
session,
|
|
336
|
+
targetId,
|
|
337
|
+
url: pageTarget.url || "",
|
|
338
|
+
title: pageTarget.title || targetApp,
|
|
339
|
+
overlayInstalled: false,
|
|
340
|
+
agentId: getConfigAgentId(config),
|
|
341
|
+
appName: targetApp,
|
|
342
|
+
isElectron: true,
|
|
343
|
+
};
|
|
344
|
+
sessionRegistry.register(targetId, client);
|
|
345
|
+
return client;
|
|
346
|
+
}
|
|
347
|
+
// ──────────────── STANDARD WEB BROWSER FLOW ────────────────
|
|
256
348
|
const sessionLock = readLock();
|
|
257
349
|
if (sessionLock && sessionLock.mode === "session") {
|
|
258
350
|
if (isLockPending(sessionLock)) {
|
|
@@ -437,7 +529,7 @@ export async function getOrConnect(config) {
|
|
|
437
529
|
if (activatedId) {
|
|
438
530
|
const client = sessionRegistry.getAll().find(c => c.targetId === activatedId);
|
|
439
531
|
if (client) {
|
|
440
|
-
sessionRegistry.setActive(activatedId, client.agentId);
|
|
532
|
+
sessionRegistry.setActive(activatedId, client.agentId, client.appName);
|
|
441
533
|
}
|
|
442
534
|
}
|
|
443
535
|
});
|
|
@@ -491,28 +583,34 @@ export async function getOrConnect(config) {
|
|
|
491
583
|
await releaseMutex();
|
|
492
584
|
}
|
|
493
585
|
}
|
|
494
|
-
export async function getTabList(agentId) {
|
|
586
|
+
export async function getTabList(agentId, targetApp) {
|
|
495
587
|
const config = getCachedConfig();
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
588
|
+
let port = null;
|
|
589
|
+
if (targetApp) {
|
|
590
|
+
const lock = readLock(targetApp);
|
|
591
|
+
if (lock && (await isLockValid(lock))) {
|
|
592
|
+
port = lock.port;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
port = actualPort;
|
|
597
|
+
if (!port) {
|
|
598
|
+
const lock = readLock();
|
|
599
|
+
if (lock && lock.mode === "session") {
|
|
600
|
+
const valid = await isLockValid(lock);
|
|
601
|
+
if (valid) {
|
|
602
|
+
port = lock.port;
|
|
603
|
+
}
|
|
604
|
+
else {
|
|
605
|
+
logDebug("[Weavetab CDP] getTabList: stale lock detected — clearing.");
|
|
606
|
+
clearLock();
|
|
607
|
+
port = null;
|
|
608
|
+
}
|
|
511
609
|
}
|
|
512
610
|
}
|
|
513
611
|
}
|
|
514
612
|
if (!port)
|
|
515
|
-
throw new Error(
|
|
613
|
+
throw new Error(`[!] No active ${targetApp ? targetApp : "browser"} session. Run browser_go or launch app first.`);
|
|
516
614
|
try {
|
|
517
615
|
const metaSession = await withCDPTimeout(CDP({ host: "127.0.0.1", port }), 5000, "getTabList CDP");
|
|
518
616
|
const { targetInfos } = await withCDPTimeout(metaSession.Target.getTargets(), 5000, "getTabList getTargets");
|
|
@@ -520,21 +618,21 @@ export async function getTabList(agentId) {
|
|
|
520
618
|
const aid = agentId || getConfigAgentId(config);
|
|
521
619
|
const bctxId = sessionRegistry.getBrowserContextIdForAgent(aid);
|
|
522
620
|
return targetInfos
|
|
523
|
-
.filter(t => t.type === "page" && (!bctxId || t.browserContextId === bctxId))
|
|
621
|
+
.filter(t => (t.type === "page" || t.type === "webview" || t.type === "app" || t.type === "other" || t.type === "iframe") && (!bctxId || t.browserContextId === bctxId))
|
|
524
622
|
.map(t => ({
|
|
525
623
|
id: t.targetId,
|
|
526
|
-
title: t.title,
|
|
624
|
+
title: t.title || t.url || t.type,
|
|
527
625
|
url: t.url,
|
|
528
626
|
type: t.type,
|
|
529
627
|
}));
|
|
530
628
|
}
|
|
531
629
|
catch {
|
|
532
|
-
throw new Error(`[!] Weavetab cannot reach browser on port ${port}.`);
|
|
630
|
+
throw new Error(`[!] Weavetab cannot reach ${targetApp ? targetApp : "browser"} on port ${port}.`);
|
|
533
631
|
}
|
|
534
632
|
}
|
|
535
633
|
// Only allow the UI to update cosmetic/dev settings via the binding.
|
|
536
634
|
// Never allow overwriting security-critical fields (role, browserPath, block, allow, profileDir, etc.).
|
|
537
|
-
const UI_ALLOWED_FIELDS = new Set(["devMode", "
|
|
635
|
+
const UI_ALLOWED_FIELDS = new Set(["devMode", "extension"]);
|
|
538
636
|
async function wireUiConfigBinding(session) {
|
|
539
637
|
try {
|
|
540
638
|
await session.Runtime.addBinding({ name: "__wt_save_ui_config" });
|
|
@@ -562,21 +660,38 @@ async function wireUiConfigBinding(session) {
|
|
|
562
660
|
}
|
|
563
661
|
catch { /* already bound */ }
|
|
564
662
|
}
|
|
565
|
-
export async function connectToTab(targetId, port, agentId, browserContextId) {
|
|
663
|
+
export async function connectToTab(targetId, port, agentId, browserContextId, appName) {
|
|
566
664
|
const existing = sessionRegistry.getAll().find(c => c.targetId === targetId);
|
|
567
665
|
if (existing)
|
|
568
666
|
return existing;
|
|
569
|
-
const lock = readLock();
|
|
570
|
-
const p = port || actualPort || lock?.port;
|
|
667
|
+
const lock = readLock(appName);
|
|
668
|
+
const p = port || (appName ? lock?.port : (actualPort || lock?.port));
|
|
571
669
|
if (!p)
|
|
572
|
-
throw new Error(
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
670
|
+
throw new Error(`[!] No active ${appName || "browser"} session. Run browser_go first.`);
|
|
671
|
+
let t = null;
|
|
672
|
+
try {
|
|
673
|
+
const targets = await withCDPTimeout(CDP.List({ host: "127.0.0.1", port: p }), 3000, "connectToTab CDP.List");
|
|
674
|
+
t = targets.find((item) => (item.id || item.targetId) === targetId);
|
|
675
|
+
}
|
|
676
|
+
catch { }
|
|
677
|
+
if (!t) {
|
|
678
|
+
try {
|
|
679
|
+
const metaSession = await withCDPTimeout(CDP({ host: "127.0.0.1", port: p }), 3000, "connectToTab meta CDP");
|
|
680
|
+
const { targetInfos } = await withCDPTimeout(metaSession.Target.getTargets(), 3000, "connectToTab meta getTargets");
|
|
681
|
+
await metaSession.close();
|
|
682
|
+
const ti = targetInfos.find(item => item.targetId === targetId);
|
|
683
|
+
if (ti) {
|
|
684
|
+
t = { id: ti.targetId, targetId: ti.targetId, url: ti.url, title: ti.title, type: ti.type };
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
catch { }
|
|
688
|
+
}
|
|
689
|
+
if (!t) {
|
|
690
|
+
t = { id: targetId, targetId, url: "", title: appName || "Tab", type: "page" };
|
|
691
|
+
}
|
|
577
692
|
const session = await withCDPTimeout(CDP({ host: "127.0.0.1", port: p, target: targetId }), 5000, "connectToTab CDP");
|
|
578
693
|
session.on("close", () => {
|
|
579
|
-
logDebug(
|
|
694
|
+
logDebug(`[Weavetab CDP] connectToTab session closed (${appName || "browser"}) — unregistering.`);
|
|
580
695
|
// Cancel this tab's analytics interval so it doesn't outlive the session
|
|
581
696
|
const iv = analyticsIntervals.get(targetId);
|
|
582
697
|
if (iv) {
|
|
@@ -589,7 +704,7 @@ export async function connectToTab(targetId, port, agentId, browserContextId) {
|
|
|
589
704
|
sessionRegistry.unregister(targetId);
|
|
590
705
|
});
|
|
591
706
|
session.on("error", (err) => {
|
|
592
|
-
logDebug(
|
|
707
|
+
logDebug(`[Weavetab CDP] connectToTab session error (${appName || "browser"}):`, err?.message ?? err);
|
|
593
708
|
});
|
|
594
709
|
await Promise.all([
|
|
595
710
|
session.Accessibility.enable(),
|
|
@@ -601,7 +716,7 @@ export async function connectToTab(targetId, port, agentId, browserContextId) {
|
|
|
601
716
|
]);
|
|
602
717
|
await wireUiConfigBinding(session);
|
|
603
718
|
const currentConfig = getCachedConfig();
|
|
604
|
-
await initSession(session, targetId, p, currentConfig);
|
|
719
|
+
await initSession(session, targetId, p, currentConfig, !!appName);
|
|
605
720
|
// NOTE: initSession() already installs a frameNavigated handler that handles
|
|
606
721
|
// clearSnapshot, clearViewportCache, and ensureVisualLayer. Do NOT add a second
|
|
607
722
|
// one here — it would double the CDP work on every navigation.
|
|
@@ -614,6 +729,8 @@ export async function connectToTab(targetId, port, agentId, browserContextId) {
|
|
|
614
729
|
overlayInstalled: false,
|
|
615
730
|
agentId: actualAgentId,
|
|
616
731
|
browserContextId,
|
|
732
|
+
appName,
|
|
733
|
+
isElectron: !!appName,
|
|
617
734
|
};
|
|
618
735
|
sessionRegistry.register(targetId, client);
|
|
619
736
|
return client;
|
package/dist/cdp/lock.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export interface LockData {
|
|
|
4
4
|
profileDir: string;
|
|
5
5
|
browserPath: string;
|
|
6
6
|
mode: "session" | "attach";
|
|
7
|
+
appName?: string;
|
|
7
8
|
pending?: boolean;
|
|
8
9
|
timestamp?: number;
|
|
9
10
|
fingerprint?: {
|
|
@@ -12,17 +13,19 @@ export interface LockData {
|
|
|
12
13
|
cdpVersion?: string;
|
|
13
14
|
};
|
|
14
15
|
}
|
|
15
|
-
declare function
|
|
16
|
-
export declare function
|
|
17
|
-
|
|
18
|
-
export declare function
|
|
19
|
-
export declare function
|
|
16
|
+
export declare function getLockFilePath(appName?: string): string;
|
|
17
|
+
export declare function getMutexFilePath(appName?: string): string;
|
|
18
|
+
declare function acquireMutex(appName?: string): Promise<() => Promise<void>>;
|
|
19
|
+
export declare function writeLockSafe(data: LockData, appName?: string): Promise<void>;
|
|
20
|
+
export declare function writeLock(data: LockData, appName?: string): Promise<void>;
|
|
21
|
+
export declare function readLock(appName?: string): LockData | null;
|
|
22
|
+
export declare function clearLock(appName?: string): void;
|
|
20
23
|
/**
|
|
21
24
|
* Safe variant: only clears the lock if it was written by THIS process (process.pid).
|
|
22
25
|
* Prevents a second MCP server instance from wiping the first instance's browser lock
|
|
23
26
|
* during its retry loop, which would allow a race condition where two browsers spawn.
|
|
24
27
|
*/
|
|
25
|
-
export declare function clearLockIfOwned(): void;
|
|
28
|
+
export declare function clearLockIfOwned(appName?: string): void;
|
|
26
29
|
export declare function isProcessRunning(pid: number): boolean;
|
|
27
30
|
/**
|
|
28
31
|
* Two-factor lock validation: PID alive AND port accepting TCP connections.
|
package/dist/cdp/lock.js
CHANGED
|
@@ -10,49 +10,56 @@ import * as path from "node:path";
|
|
|
10
10
|
import * as os from "node:os";
|
|
11
11
|
import * as net from "node:net";
|
|
12
12
|
import { lock } from "proper-lockfile";
|
|
13
|
+
import { sanitizeAppName } from "./profiles.js";
|
|
13
14
|
const LOCK_DIR = path.join(os.homedir(), ".weavetab-system", "locks");
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
15
|
+
export function getLockFilePath(appName) {
|
|
16
|
+
if (appName && appName.trim()) {
|
|
17
|
+
return path.join(LOCK_DIR, `.weavetab.electron.${sanitizeAppName(appName)}.lock`);
|
|
18
|
+
}
|
|
19
|
+
return path.join(LOCK_DIR, ".weavetab.lock");
|
|
20
|
+
}
|
|
21
|
+
export function getMutexFilePath(appName) {
|
|
22
|
+
if (appName && appName.trim()) {
|
|
23
|
+
return path.join(LOCK_DIR, `.weavetab.electron.${sanitizeAppName(appName)}.lock.mutex`);
|
|
24
|
+
}
|
|
25
|
+
return path.join(LOCK_DIR, ".weavetab.lock.mutex");
|
|
26
|
+
}
|
|
27
|
+
// ─── In-process serialization queues per app/browser ──────────────────────────
|
|
28
|
+
const _inProcessQueues = new Map();
|
|
29
|
+
const _heldDepths = new Map();
|
|
30
|
+
const _heldFileReleases = new Map();
|
|
31
|
+
const _heldSlotResolves = new Map();
|
|
32
|
+
function getTargetKey(appName) {
|
|
33
|
+
return appName && appName.trim() ? `electron:${sanitizeAppName(appName)}` : "browser";
|
|
34
|
+
}
|
|
35
|
+
async function acquireMutex(appName) {
|
|
36
|
+
const key = getTargetKey(appName);
|
|
37
|
+
const currentDepth = _heldDepths.get(key) ?? 0;
|
|
38
|
+
// Re-entrant fast path: this process already holds the mutex for this target.
|
|
39
|
+
if (currentDepth > 0) {
|
|
40
|
+
_heldDepths.set(key, currentDepth + 1);
|
|
36
41
|
return async () => {
|
|
37
|
-
|
|
42
|
+
const d = (_heldDepths.get(key) ?? 1) - 1;
|
|
43
|
+
_heldDepths.set(key, Math.max(0, d));
|
|
38
44
|
};
|
|
39
45
|
}
|
|
40
|
-
// Enqueue this caller
|
|
46
|
+
// Enqueue this caller for this specific key
|
|
41
47
|
let resolveSlot;
|
|
42
48
|
const slot = new Promise(resolve => { resolveSlot = resolve; });
|
|
43
|
-
const prevQueue =
|
|
44
|
-
|
|
45
|
-
await prevQueue; // Wait for all prior callers to fully release
|
|
49
|
+
const prevQueue = _inProcessQueues.get(key) ?? Promise.resolve();
|
|
50
|
+
_inProcessQueues.set(key, prevQueue.then(() => slot));
|
|
51
|
+
await prevQueue; // Wait for all prior callers for this key to fully release
|
|
46
52
|
fs.mkdirSync(LOCK_DIR, { recursive: true });
|
|
47
|
-
|
|
53
|
+
const mutexFile = getMutexFilePath(appName);
|
|
54
|
+
if (!fs.existsSync(mutexFile)) {
|
|
48
55
|
try {
|
|
49
|
-
fs.writeFileSync(
|
|
56
|
+
fs.writeFileSync(mutexFile, String(process.pid), "utf-8");
|
|
50
57
|
}
|
|
51
58
|
catch { /* race/non-fatal */ }
|
|
52
59
|
}
|
|
53
60
|
let fileRelease = null;
|
|
54
61
|
try {
|
|
55
|
-
fileRelease = await lock(
|
|
62
|
+
fileRelease = await lock(mutexFile, {
|
|
56
63
|
retries: {
|
|
57
64
|
retries: 25,
|
|
58
65
|
minTimeout: 200,
|
|
@@ -62,28 +69,29 @@ async function acquireMutex() {
|
|
|
62
69
|
});
|
|
63
70
|
// Write current PID to mutex file for diagnostics/HUD
|
|
64
71
|
try {
|
|
65
|
-
fs.writeFileSync(
|
|
72
|
+
fs.writeFileSync(mutexFile, String(process.pid), "utf-8");
|
|
66
73
|
}
|
|
67
74
|
catch { /* non-fatal */ }
|
|
68
75
|
}
|
|
69
76
|
catch (err) {
|
|
70
77
|
resolveSlot(); // Unblock the queue even on failure
|
|
71
|
-
throw new Error(`[Weavetab] Could not acquire launch mutex after retries: ${err?.message || err}`);
|
|
78
|
+
throw new Error(`[Weavetab] Could not acquire launch mutex for ${key} after retries: ${err?.message || err}`);
|
|
72
79
|
}
|
|
73
80
|
// Mark this process as the current holder so nested calls skip the queue.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
81
|
+
_heldDepths.set(key, 1);
|
|
82
|
+
_heldFileReleases.set(key, fileRelease);
|
|
83
|
+
_heldSlotResolves.set(key, resolveSlot);
|
|
77
84
|
// Return a release function that releases both the file lock and the in-process slot
|
|
78
85
|
return async () => {
|
|
79
|
-
|
|
80
|
-
|
|
86
|
+
const d = (_heldDepths.get(key) ?? 1) - 1;
|
|
87
|
+
_heldDepths.set(key, Math.max(0, d));
|
|
88
|
+
if (d > 0)
|
|
81
89
|
return; // nested regions still active
|
|
82
|
-
|
|
83
|
-
const rel =
|
|
84
|
-
const slotResolve =
|
|
85
|
-
|
|
86
|
-
|
|
90
|
+
_heldDepths.delete(key);
|
|
91
|
+
const rel = _heldFileReleases.get(key);
|
|
92
|
+
const slotResolve = _heldSlotResolves.get(key);
|
|
93
|
+
_heldFileReleases.delete(key);
|
|
94
|
+
_heldSlotResolves.delete(key);
|
|
87
95
|
try {
|
|
88
96
|
if (rel)
|
|
89
97
|
await rel();
|
|
@@ -94,44 +102,49 @@ async function acquireMutex() {
|
|
|
94
102
|
};
|
|
95
103
|
}
|
|
96
104
|
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
97
|
-
export async function writeLockSafe(data) {
|
|
98
|
-
const
|
|
105
|
+
export async function writeLockSafe(data, appName) {
|
|
106
|
+
const targetApp = appName || data.appName;
|
|
107
|
+
const release = await acquireMutex(targetApp);
|
|
108
|
+
const lockFile = getLockFilePath(targetApp);
|
|
99
109
|
try {
|
|
100
110
|
fs.mkdirSync(LOCK_DIR, { recursive: true });
|
|
101
|
-
fs.writeFileSync(
|
|
111
|
+
fs.writeFileSync(lockFile, JSON.stringify(data, null, 2), "utf-8");
|
|
102
112
|
}
|
|
103
113
|
finally {
|
|
104
114
|
await release();
|
|
105
115
|
}
|
|
106
116
|
}
|
|
107
|
-
export async function writeLock(data) {
|
|
108
|
-
await writeLockSafe(data);
|
|
117
|
+
export async function writeLock(data, appName) {
|
|
118
|
+
await writeLockSafe(data, appName);
|
|
109
119
|
}
|
|
110
|
-
export function readLock() {
|
|
111
|
-
|
|
120
|
+
export function readLock(appName) {
|
|
121
|
+
const lockFile = getLockFilePath(appName);
|
|
122
|
+
if (!fs.existsSync(lockFile))
|
|
112
123
|
return null;
|
|
113
124
|
try {
|
|
114
|
-
return JSON.parse(fs.readFileSync(
|
|
125
|
+
return JSON.parse(fs.readFileSync(lockFile, "utf-8"));
|
|
115
126
|
}
|
|
116
127
|
catch {
|
|
117
128
|
return null;
|
|
118
129
|
}
|
|
119
130
|
}
|
|
120
|
-
export function clearLock() {
|
|
131
|
+
export function clearLock(appName) {
|
|
132
|
+
const lockFile = getLockFilePath(appName);
|
|
133
|
+
const mutexFile = getMutexFilePath(appName);
|
|
121
134
|
try {
|
|
122
|
-
if (fs.existsSync(
|
|
123
|
-
fs.unlinkSync(
|
|
135
|
+
if (fs.existsSync(lockFile))
|
|
136
|
+
fs.unlinkSync(lockFile);
|
|
124
137
|
}
|
|
125
138
|
catch (e) {
|
|
126
139
|
logDebug(`[Weavetab] [!] Failed to clear lock file: ${e}`);
|
|
127
140
|
}
|
|
128
141
|
try {
|
|
129
|
-
if (fs.existsSync(
|
|
130
|
-
fs.unlinkSync(
|
|
142
|
+
if (fs.existsSync(mutexFile))
|
|
143
|
+
fs.unlinkSync(mutexFile);
|
|
131
144
|
}
|
|
132
145
|
catch { /* ignore */ }
|
|
133
146
|
try {
|
|
134
|
-
const lockDir =
|
|
147
|
+
const lockDir = mutexFile + ".lock";
|
|
135
148
|
if (fs.existsSync(lockDir))
|
|
136
149
|
fs.rmSync(lockDir, { recursive: true, force: true });
|
|
137
150
|
}
|
|
@@ -142,14 +155,14 @@ export function clearLock() {
|
|
|
142
155
|
* Prevents a second MCP server instance from wiping the first instance's browser lock
|
|
143
156
|
* during its retry loop, which would allow a race condition where two browsers spawn.
|
|
144
157
|
*/
|
|
145
|
-
export function clearLockIfOwned() {
|
|
158
|
+
export function clearLockIfOwned(appName) {
|
|
146
159
|
try {
|
|
147
|
-
const lock = readLock();
|
|
160
|
+
const lock = readLock(appName);
|
|
148
161
|
if (!lock || lock.pid !== process.pid) {
|
|
149
162
|
logDebug(`[Weavetab] clearLockIfOwned: skipping — lock owned by PID ${lock?.pid ?? 'none'}, this PID is ${process.pid}`);
|
|
150
163
|
return;
|
|
151
164
|
}
|
|
152
|
-
clearLock();
|
|
165
|
+
clearLock(appName);
|
|
153
166
|
}
|
|
154
167
|
catch (e) {
|
|
155
168
|
logDebug(`[Weavetab] clearLockIfOwned error: ${e}`);
|
|
@@ -14,4 +14,5 @@ export declare function initOmniBroker(parentSession: CDP.Client, parentTargetId
|
|
|
14
14
|
* Executes a CDP command across all known iframe contexts if it fails in the top-level context.
|
|
15
15
|
*/
|
|
16
16
|
export declare function omniEvaluate(parentSession: CDP.Client, parentTargetId: string, expression: string, returnByValue?: boolean): Promise<any>;
|
|
17
|
+
export declare function getOmniSessions(parentTargetId: string): Map<string, CDP.Client> | undefined;
|
|
17
18
|
export declare function cleanupOmniBroker(parentTargetId: string): void;
|