@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,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Weavetab — cdp/extension.ts
|
|
3
|
+
* Extension detection and health-check layer.
|
|
4
|
+
* All extension-dependent tools should call requireExtension() before proceeding.
|
|
5
|
+
* Weavetab by fy2ne
|
|
6
|
+
*/
|
|
7
|
+
import CDP from "chrome-remote-interface";
|
|
8
|
+
/**
|
|
9
|
+
* Check whether the Weavetab extension is loaded in the current page.
|
|
10
|
+
* Uses DOM markers instead of JS globals because content scripts run in
|
|
11
|
+
* an ISOLATED world (MV3) — window.__Weavetab is invisible from MAIN world.
|
|
12
|
+
*/
|
|
13
|
+
export declare function isExtensionLoaded(session: CDP.Client): Promise<boolean>;
|
|
14
|
+
/**
|
|
15
|
+
* Throws a clear, actionable error if the extension is not loaded.
|
|
16
|
+
*/
|
|
17
|
+
export declare function requireExtension(session: CDP.Client): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Full health check — which extension features are available.
|
|
20
|
+
*/
|
|
21
|
+
export declare function extensionHealth(session: CDP.Client): Promise<{
|
|
22
|
+
loaded: boolean;
|
|
23
|
+
version?: string;
|
|
24
|
+
features: string[];
|
|
25
|
+
}>;
|
|
26
|
+
/**
|
|
27
|
+
* Suppress all visual overlays/borders the extension draws on interactive elements.
|
|
28
|
+
* Call this before taking screenshots or when clean rendering is needed.
|
|
29
|
+
* Default behavior for browser_map — overlays should only appear via browser_highlight.
|
|
30
|
+
*/
|
|
31
|
+
export declare function suppressOverlays(session: CDP.Client): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Re-enable visual overlays for debugging purposes.
|
|
34
|
+
* Only call this if user explicitly passes show_overlays: true to browser_map.
|
|
35
|
+
*/
|
|
36
|
+
export declare function restoreOverlays(session: CDP.Client): Promise<void>;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check whether the Weavetab extension is loaded in the current page.
|
|
3
|
+
* Uses DOM markers instead of JS globals because content scripts run in
|
|
4
|
+
* an ISOLATED world (MV3) — window.__Weavetab is invisible from MAIN world.
|
|
5
|
+
*/
|
|
6
|
+
export async function isExtensionLoaded(session) {
|
|
7
|
+
try {
|
|
8
|
+
const { result } = await session.Runtime.evaluate({
|
|
9
|
+
expression: `!!(
|
|
10
|
+
document.getElementById('wt-extension-marker') ||
|
|
11
|
+
document.querySelector('weavetab-host') ||
|
|
12
|
+
document.querySelector('[data-wt-internal]') ||
|
|
13
|
+
document.getElementById('__Weavetab_cursor_host') ||
|
|
14
|
+
document.getElementById('wt-ui-styles')
|
|
15
|
+
)`,
|
|
16
|
+
returnByValue: true,
|
|
17
|
+
});
|
|
18
|
+
return result?.value === true;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Throws a clear, actionable error if the extension is not loaded.
|
|
26
|
+
*/
|
|
27
|
+
export async function requireExtension(session) {
|
|
28
|
+
let loaded = await isExtensionLoaded(session);
|
|
29
|
+
if (!loaded) {
|
|
30
|
+
try {
|
|
31
|
+
// Auto-recovery: if marker is missing, the page might be in a broken state.
|
|
32
|
+
// Attempt a single reload before failing the tool call.
|
|
33
|
+
await session.Page.reload({ ignoreCache: false });
|
|
34
|
+
await new Promise(r => setTimeout(r, 2000)); // wait for content script injection
|
|
35
|
+
loaded = await isExtensionLoaded(session);
|
|
36
|
+
}
|
|
37
|
+
catch { /* ignore reload errors */ }
|
|
38
|
+
}
|
|
39
|
+
if (!loaded) {
|
|
40
|
+
throw new Error("[!] Weavetab Extension not detected on this page. " +
|
|
41
|
+
"Make sure the extension is installed (from ext-dist/) and the page has been loaded/refreshed. " +
|
|
42
|
+
"Some tools require the extension for full functionality.");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Full health check — which extension features are available.
|
|
47
|
+
*/
|
|
48
|
+
export async function extensionHealth(session) {
|
|
49
|
+
try {
|
|
50
|
+
// Use DOM markers + fallback JS checks via postMessage bridge
|
|
51
|
+
const loaded = await isExtensionLoaded(session);
|
|
52
|
+
const { result } = await session.Runtime.evaluate({
|
|
53
|
+
expression: `(function() {
|
|
54
|
+
const features = [];
|
|
55
|
+
if (document.querySelector('weavetab-host') || document.getElementById('__Weavetab_cursor_host')) features.push('cursor');
|
|
56
|
+
if (document.querySelector('[data-wt-internal]') || document.getElementById('wt-ui-styles')) features.push('styles');
|
|
57
|
+
if (document.querySelector('.wt-bubble')) features.push('bubble');
|
|
58
|
+
if (document.getElementById('wt-plan-panel')) features.push('plan');
|
|
59
|
+
return JSON.stringify({
|
|
60
|
+
loaded: ${loaded},
|
|
61
|
+
features
|
|
62
|
+
});
|
|
63
|
+
})()`,
|
|
64
|
+
returnByValue: true,
|
|
65
|
+
});
|
|
66
|
+
if (result?.value) {
|
|
67
|
+
return JSON.parse(result.value);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch { /* ignore */ }
|
|
71
|
+
return { loaded: false, features: [] };
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Suppress all visual overlays/borders the extension draws on interactive elements.
|
|
75
|
+
* Call this before taking screenshots or when clean rendering is needed.
|
|
76
|
+
* Default behavior for browser_map — overlays should only appear via browser_highlight.
|
|
77
|
+
*/
|
|
78
|
+
export async function suppressOverlays(session) {
|
|
79
|
+
try {
|
|
80
|
+
await session.Runtime.evaluate({
|
|
81
|
+
expression: `(function() {
|
|
82
|
+
// Remove any existing Weavetab overlay elements
|
|
83
|
+
document.querySelectorAll('[data-Weavetab-overlay]').forEach(el => el.remove());
|
|
84
|
+
// Hide fallback visual elements
|
|
85
|
+
var sr = document.getElementById('Weavetab-root');
|
|
86
|
+
if (sr && sr.shadowRoot) {
|
|
87
|
+
var els = sr.shadowRoot.querySelectorAll('#wt-fallback-cursor, #wt-fallback-hud, #wt-fallback-thought');
|
|
88
|
+
els.forEach(function(el){ el.style.display = 'none'; });
|
|
89
|
+
}
|
|
90
|
+
// Post a message for the content script (isolated world)
|
|
91
|
+
window.postMessage({ type: 'Weavetab_SUPPRESS_VISUALS', suppress: true }, '*');
|
|
92
|
+
})()`,
|
|
93
|
+
returnByValue: false,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
catch { /* non-fatal */ }
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Re-enable visual overlays for debugging purposes.
|
|
100
|
+
* Only call this if user explicitly passes show_overlays: true to browser_map.
|
|
101
|
+
*/
|
|
102
|
+
export async function restoreOverlays(session) {
|
|
103
|
+
try {
|
|
104
|
+
await session.Runtime.evaluate({
|
|
105
|
+
expression: `(function() {
|
|
106
|
+
// Restore fallback visual elements
|
|
107
|
+
var sr = document.getElementById('Weavetab-root');
|
|
108
|
+
if (sr && sr.shadowRoot) {
|
|
109
|
+
var els = sr.shadowRoot.querySelectorAll('#wt-fallback-cursor, #wt-fallback-hud, #wt-fallback-thought');
|
|
110
|
+
els.forEach(function(el){ el.style.display = ''; });
|
|
111
|
+
}
|
|
112
|
+
window.postMessage({ type: 'Weavetab_SUPPRESS_VISUALS', suppress: false }, '*');
|
|
113
|
+
})()`,
|
|
114
|
+
returnByValue: false,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
catch { /* non-fatal */ }
|
|
118
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import CDP from "chrome-remote-interface";
|
|
2
|
+
/**
|
|
3
|
+
* sendToExtension is the only bridge to communicate from Node (CLI)
|
|
4
|
+
* to the Weavetab Chrome Extension via CDP executeScript.
|
|
5
|
+
* Messages are queued and batched with backpressure.
|
|
6
|
+
* Returns true if the message was queued, false if dropped.
|
|
7
|
+
*/
|
|
8
|
+
export declare function sendToExtension(session: CDP.Client, message: object): Promise<boolean>;
|
|
9
|
+
export declare function getFromExtension<T = any>(session: CDP.Client, message: object, retries?: number): Promise<T | null>;
|
|
10
|
+
export declare function hideWeaveTabOverlay(session: CDP.Client): Promise<void>;
|
|
11
|
+
export declare function restoreWeaveTabOverlay(session: CDP.Client): Promise<void>;
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { logDebug } from "../audit/logger.js";
|
|
2
|
+
const MAX_QUEUE_SIZE = 50;
|
|
3
|
+
const BATCH_INTERVAL_MS = 10; // Reduced from 20ms for faster message delivery
|
|
4
|
+
const messageQueues = new Map();
|
|
5
|
+
const queueTimers = new Map();
|
|
6
|
+
async function drainQueue(session, targetId) {
|
|
7
|
+
const queue = messageQueues.get(targetId);
|
|
8
|
+
if (!queue || queue.length === 0)
|
|
9
|
+
return;
|
|
10
|
+
const batch = queue.splice(0, Math.min(queue.length, 10));
|
|
11
|
+
const messages = batch.map((q, i) => {
|
|
12
|
+
const msg = q.message;
|
|
13
|
+
// Add dedup _id using queue entry's id (unique) so content.js __wt_processedMsgIds works
|
|
14
|
+
if (typeof msg === 'object' && msg !== null) {
|
|
15
|
+
msg._id = q.id + (i > 0 ? '-' + i : '');
|
|
16
|
+
}
|
|
17
|
+
return msg;
|
|
18
|
+
});
|
|
19
|
+
try {
|
|
20
|
+
const msgData = JSON.stringify(messages.length === 1 ? messages[0] : messages);
|
|
21
|
+
await session.Runtime.evaluate({
|
|
22
|
+
expression: `
|
|
23
|
+
try {
|
|
24
|
+
window.postMessage({ type: 'Weavetab-msg-in', data: ${msgData} }, '*');
|
|
25
|
+
} catch(e) {}
|
|
26
|
+
`
|
|
27
|
+
});
|
|
28
|
+
batch.forEach(q => q.resolve(true));
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
logDebug(`[Weavetab] Failed to send batch (${batch.length} msgs)`, e);
|
|
32
|
+
batch.forEach(q => {
|
|
33
|
+
q.attempts++;
|
|
34
|
+
if (q.attempts < 3) {
|
|
35
|
+
queue.push(q); // re-queue for retry
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
q.resolve(false);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
// Schedule next drain if messages remain
|
|
43
|
+
if (queue.length > 0) {
|
|
44
|
+
const timer = setTimeout(() => drainQueue(session, targetId), BATCH_INTERVAL_MS);
|
|
45
|
+
queueTimers.set(targetId, timer);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
const t = queueTimers.get(targetId);
|
|
49
|
+
if (t) {
|
|
50
|
+
clearTimeout(t);
|
|
51
|
+
queueTimers.delete(targetId);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function enqueueMessage(session, targetId, message) {
|
|
56
|
+
return new Promise((resolve) => {
|
|
57
|
+
let queue = messageQueues.get(targetId);
|
|
58
|
+
if (!queue) {
|
|
59
|
+
queue = [];
|
|
60
|
+
messageQueues.set(targetId, queue);
|
|
61
|
+
}
|
|
62
|
+
// Backpressure: if queue is full, reject oldest message to make room
|
|
63
|
+
if (queue.length >= MAX_QUEUE_SIZE) {
|
|
64
|
+
const dropped = queue.shift();
|
|
65
|
+
if (dropped) {
|
|
66
|
+
logDebug(`[Weavetab] Backpressure: dropping oldest message (${dropped.id})`);
|
|
67
|
+
dropped.resolve(false);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const qMsg = {
|
|
71
|
+
id: Math.random().toString(36).slice(2, 10),
|
|
72
|
+
message,
|
|
73
|
+
resolve,
|
|
74
|
+
timestamp: Date.now(),
|
|
75
|
+
attempts: 0,
|
|
76
|
+
};
|
|
77
|
+
queue.push(qMsg);
|
|
78
|
+
// Start drain timer if not already running
|
|
79
|
+
if (!queueTimers.has(targetId)) {
|
|
80
|
+
const timer = setTimeout(() => drainQueue(session, targetId), BATCH_INTERVAL_MS);
|
|
81
|
+
queueTimers.set(targetId, timer);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
86
|
+
function getTargetId(session) {
|
|
87
|
+
return session._targetId || session.targetId || "default";
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* sendToExtension is the only bridge to communicate from Node (CLI)
|
|
91
|
+
* to the Weavetab Chrome Extension via CDP executeScript.
|
|
92
|
+
* Messages are queued and batched with backpressure.
|
|
93
|
+
* Returns true if the message was queued, false if dropped.
|
|
94
|
+
*/
|
|
95
|
+
export async function sendToExtension(session, message) {
|
|
96
|
+
try {
|
|
97
|
+
const targetId = getTargetId(session);
|
|
98
|
+
return await enqueueMessage(session, targetId, message);
|
|
99
|
+
}
|
|
100
|
+
catch (e) {
|
|
101
|
+
logDebug("[Weavetab] Failed to sendToExtension", e);
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
export async function getFromExtension(session, message, retries = 0) {
|
|
106
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
107
|
+
try {
|
|
108
|
+
const { result } = await session.Runtime.evaluate({
|
|
109
|
+
expression: `new Promise((resolve) => {
|
|
110
|
+
try {
|
|
111
|
+
const id = Math.random().toString();
|
|
112
|
+
const listener = (e) => {
|
|
113
|
+
if (e.detail && e.detail.id === id) {
|
|
114
|
+
window.removeEventListener('Weavetab-msg-out', listener);
|
|
115
|
+
resolve(JSON.stringify(e.detail.data));
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
window.addEventListener('Weavetab-msg-out', listener);
|
|
119
|
+
window.dispatchEvent(new CustomEvent('Weavetab-msg-in', { detail: Object.assign({}, ${JSON.stringify(message)}, { id }) }));
|
|
120
|
+
|
|
121
|
+
// Fallback timeout — 100ms fail-fast (reduced from 150ms)
|
|
122
|
+
setTimeout(() => {
|
|
123
|
+
window.removeEventListener('Weavetab-msg-out', listener);
|
|
124
|
+
resolve("null");
|
|
125
|
+
}, 100);
|
|
126
|
+
} catch (e) {
|
|
127
|
+
resolve("null");
|
|
128
|
+
}
|
|
129
|
+
})`,
|
|
130
|
+
awaitPromise: true,
|
|
131
|
+
returnByValue: true
|
|
132
|
+
});
|
|
133
|
+
if (result && result.value && result.value !== "null") {
|
|
134
|
+
return JSON.parse(result.value);
|
|
135
|
+
}
|
|
136
|
+
// Got null — if we have retries left, try again
|
|
137
|
+
if (attempt < retries) {
|
|
138
|
+
logDebug(`[Weavetab] Extension did not respond (attempt ${attempt + 1}/${retries + 1}), retrying...`);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch (e) {
|
|
143
|
+
if (attempt < retries) {
|
|
144
|
+
logDebug(`[Weavetab] getFromExtension error (attempt ${attempt + 1}/${retries + 1}): ${e instanceof Error ? e.message : e}`);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
logDebug("[Weavetab] Failed to getFromExtension after retries", e);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
export async function hideWeaveTabOverlay(session) {
|
|
153
|
+
await session.Runtime.evaluate({
|
|
154
|
+
expression: `(function(){
|
|
155
|
+
var existing = document.getElementById('__wt_hide_overlay_styles');
|
|
156
|
+
if (existing) return; // already hidden
|
|
157
|
+
var style = document.createElement('style');
|
|
158
|
+
style.id = '__wt_hide_overlay_styles';
|
|
159
|
+
// Cover ALL Weavetab overlay elements so they never appear in screenshots/PDFs/snapshots:
|
|
160
|
+
// - [id^="Weavetab-"] : Weavetab-root shadow host, any legacy Weavetab- elements
|
|
161
|
+
// - [id^="__Weavetab_"] : __Weavetab_cursor_host (ghost cursor)
|
|
162
|
+
// - [id^="wt-"] : wt-* legacy element IDs
|
|
163
|
+
// - [data-wt-internal] : HUD iframe host (closed shadow DOM mount)
|
|
164
|
+
// - [data-wt-ignore] : any element flagged as agent-invisible
|
|
165
|
+
// - .wt-bubble : thought bubble / action text popups
|
|
166
|
+
// - #__wt_ask_bridge : question input bridge element
|
|
167
|
+
// - #wt-plan-dashboard : mission plan dashboard
|
|
168
|
+
// - #wt-debug-overlay : debug overlay panel
|
|
169
|
+
// - #wt-main-container : WeaveTab OS panel (todos + chat)
|
|
170
|
+
style.textContent = [
|
|
171
|
+
'[id^="Weavetab-"]',
|
|
172
|
+
'[id^="__Weavetab_"]',
|
|
173
|
+
'[id^="wt-"]',
|
|
174
|
+
'[data-wt-internal]',
|
|
175
|
+
'[data-wt-ignore]',
|
|
176
|
+
'.wt-bubble',
|
|
177
|
+
'#__wt_ask_bridge',
|
|
178
|
+
'#wt-plan-dashboard',
|
|
179
|
+
'#wt-debug-overlay',
|
|
180
|
+
'#wt-main-container',
|
|
181
|
+
].join(', ') + ' { display: none !important; visibility: hidden !important; opacity: 0 !important; }';
|
|
182
|
+
document.head.appendChild(style);
|
|
183
|
+
})()`,
|
|
184
|
+
returnByValue: false,
|
|
185
|
+
}).catch(() => { });
|
|
186
|
+
}
|
|
187
|
+
export async function restoreWeaveTabOverlay(session) {
|
|
188
|
+
await session.Runtime.evaluate({
|
|
189
|
+
expression: `(function(){
|
|
190
|
+
var style = document.getElementById('__wt_hide_overlay_styles');
|
|
191
|
+
if (style) style.remove();
|
|
192
|
+
})()`,
|
|
193
|
+
returnByValue: false,
|
|
194
|
+
}).catch(() => { });
|
|
195
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
declare class LabelCache {
|
|
2
|
+
private maxEntries;
|
|
3
|
+
private cache;
|
|
4
|
+
constructor(maxEntries?: number);
|
|
5
|
+
private getKey;
|
|
6
|
+
get(url: string, label: string): string | null;
|
|
7
|
+
set(url: string, label: string, ref: string): void;
|
|
8
|
+
invalidate(url: string): void;
|
|
9
|
+
private evict;
|
|
10
|
+
}
|
|
11
|
+
export declare const labelCache: LabelCache;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// src/cdp/label_cache.ts
|
|
2
|
+
// In-memory LRU cache to map labels to Ref IDs to speed up repeated browser_click calls
|
|
3
|
+
class LabelCache {
|
|
4
|
+
maxEntries;
|
|
5
|
+
cache;
|
|
6
|
+
constructor(maxEntries = 200) {
|
|
7
|
+
this.maxEntries = maxEntries;
|
|
8
|
+
this.cache = new Map();
|
|
9
|
+
}
|
|
10
|
+
getKey(url, label) {
|
|
11
|
+
return `${url}:::${label}`;
|
|
12
|
+
}
|
|
13
|
+
get(url, label) {
|
|
14
|
+
const key = this.getKey(url, label);
|
|
15
|
+
const entry = this.cache.get(key);
|
|
16
|
+
if (!entry)
|
|
17
|
+
return null;
|
|
18
|
+
// Update timestamp for LRU
|
|
19
|
+
entry.timestamp = Date.now();
|
|
20
|
+
return entry.ref;
|
|
21
|
+
}
|
|
22
|
+
set(url, label, ref) {
|
|
23
|
+
const key = this.getKey(url, label);
|
|
24
|
+
if (this.cache.size >= this.maxEntries) {
|
|
25
|
+
this.evict();
|
|
26
|
+
}
|
|
27
|
+
this.cache.set(key, { ref, timestamp: Date.now() });
|
|
28
|
+
}
|
|
29
|
+
invalidate(url) {
|
|
30
|
+
const prefix = `${url}:::`;
|
|
31
|
+
for (const key of this.cache.keys()) {
|
|
32
|
+
if (key.startsWith(prefix)) {
|
|
33
|
+
this.cache.delete(key);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
evict() {
|
|
38
|
+
let oldestKey = null;
|
|
39
|
+
let oldestTime = Infinity;
|
|
40
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
41
|
+
if (entry.timestamp < oldestTime) {
|
|
42
|
+
oldestTime = entry.timestamp;
|
|
43
|
+
oldestKey = key;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (oldestKey) {
|
|
47
|
+
this.cache.delete(oldestKey);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export const labelCache = new LabelCache();
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface LockData {
|
|
2
|
+
pid?: number;
|
|
3
|
+
port: number;
|
|
4
|
+
profileDir: string;
|
|
5
|
+
browserPath: string;
|
|
6
|
+
mode: "session" | "attach";
|
|
7
|
+
pending?: boolean;
|
|
8
|
+
timestamp?: number;
|
|
9
|
+
fingerprint?: {
|
|
10
|
+
launchTimestamp: number;
|
|
11
|
+
userAgent?: string;
|
|
12
|
+
cdpVersion?: string;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
declare function acquireMutex(): Promise<() => Promise<void>>;
|
|
16
|
+
export declare function writeLockSafe(data: LockData): Promise<void>;
|
|
17
|
+
export declare function writeLock(data: LockData): Promise<void>;
|
|
18
|
+
export declare function readLock(): LockData | null;
|
|
19
|
+
export declare function clearLock(): void;
|
|
20
|
+
/**
|
|
21
|
+
* Safe variant: only clears the lock if it was written by THIS process (process.pid).
|
|
22
|
+
* Prevents a second MCP server instance from wiping the first instance's browser lock
|
|
23
|
+
* during its retry loop, which would allow a race condition where two browsers spawn.
|
|
24
|
+
*/
|
|
25
|
+
export declare function clearLockIfOwned(): void;
|
|
26
|
+
export declare function isProcessRunning(pid: number): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Two-factor lock validation: PID alive AND port accepting TCP connections.
|
|
29
|
+
* Prevents PID-reuse false-positives after a crash.
|
|
30
|
+
*/
|
|
31
|
+
export declare function isLockValid(lock: LockData): Promise<boolean>;
|
|
32
|
+
export declare function isLockPending(lock: LockData): boolean;
|
|
33
|
+
export { acquireMutex };
|
package/dist/cdp/lock.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { logDebug } from "../audit/logger.js";
|
|
2
|
+
/**
|
|
3
|
+
* Weavetab — cdp/lock.ts
|
|
4
|
+
* Crash-safe, mutex-protected lock file for browser process tracking.
|
|
5
|
+
* Uses proper-lockfile for OS-level advisory locking to prevent race conditions.
|
|
6
|
+
* Weavetab by fy2ne
|
|
7
|
+
*/
|
|
8
|
+
import * as fs from "node:fs";
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
import * as os from "node:os";
|
|
11
|
+
import * as net from "node:net";
|
|
12
|
+
import { lock } from "proper-lockfile";
|
|
13
|
+
const LOCK_DIR = path.join(os.homedir(), ".weavetab-system", "locks");
|
|
14
|
+
const LOCK_FILE = path.join(LOCK_DIR, ".weavetab.lock");
|
|
15
|
+
const MUTEX_FILE = path.join(LOCK_DIR, ".weavetab.lock.mutex");
|
|
16
|
+
// ─── In-process serialization queue ──────────────────────────────────────────
|
|
17
|
+
// When multiple MCP tool calls run in parallel inside the same Node process they
|
|
18
|
+
// all race to acquireMutex() simultaneously. The OS-level proper-lockfile handles
|
|
19
|
+
// cross-process contention, but within the same process concurrent callers share
|
|
20
|
+
// the same lockfile handle and can trigger "Lock is already released" errors.
|
|
21
|
+
// This queue serializes callers at the JS level before they touch the filesystem.
|
|
22
|
+
let _inProcessMutexQueue = Promise.resolve();
|
|
23
|
+
// Re-entrancy tracking. getOrConnect() acquires the mutex and then calls
|
|
24
|
+
// launchWithBridge(), which internally calls writeLock() → acquireMutex()
|
|
25
|
+
// again (nested). The serialization queue would block the nested call on the
|
|
26
|
+
// outer slot, which only releases after the launch finishes — a self-deadlock.
|
|
27
|
+
// Track how many regions this process currently holds and only touch the OS
|
|
28
|
+
// file lock + queue slot on the outermost acquisition / release.
|
|
29
|
+
let _heldDepth = 0;
|
|
30
|
+
let _heldFileRelease = null;
|
|
31
|
+
let _heldSlotResolve = null;
|
|
32
|
+
async function acquireMutex() {
|
|
33
|
+
// Re-entrant fast path: this process already holds the mutex.
|
|
34
|
+
if (_heldDepth > 0) {
|
|
35
|
+
_heldDepth++;
|
|
36
|
+
return async () => {
|
|
37
|
+
_heldDepth--;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
// Enqueue this caller — it will only proceed once all prior callers finish.
|
|
41
|
+
let resolveSlot;
|
|
42
|
+
const slot = new Promise(resolve => { resolveSlot = resolve; });
|
|
43
|
+
const prevQueue = _inProcessMutexQueue;
|
|
44
|
+
_inProcessMutexQueue = _inProcessMutexQueue.then(() => slot);
|
|
45
|
+
await prevQueue; // Wait for all prior callers to fully release
|
|
46
|
+
fs.mkdirSync(LOCK_DIR, { recursive: true });
|
|
47
|
+
if (!fs.existsSync(MUTEX_FILE)) {
|
|
48
|
+
try {
|
|
49
|
+
fs.writeFileSync(MUTEX_FILE, String(process.pid), "utf-8");
|
|
50
|
+
}
|
|
51
|
+
catch { /* race/non-fatal */ }
|
|
52
|
+
}
|
|
53
|
+
let fileRelease = null;
|
|
54
|
+
try {
|
|
55
|
+
fileRelease = await lock(MUTEX_FILE, {
|
|
56
|
+
retries: {
|
|
57
|
+
retries: 25,
|
|
58
|
+
minTimeout: 200,
|
|
59
|
+
maxTimeout: 1000,
|
|
60
|
+
factor: 1.1,
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
// Write current PID to mutex file for diagnostics/HUD
|
|
64
|
+
try {
|
|
65
|
+
fs.writeFileSync(MUTEX_FILE, String(process.pid), "utf-8");
|
|
66
|
+
}
|
|
67
|
+
catch { /* non-fatal */ }
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
resolveSlot(); // Unblock the queue even on failure
|
|
71
|
+
throw new Error(`[Weavetab] Could not acquire launch mutex after retries: ${err?.message || err}`);
|
|
72
|
+
}
|
|
73
|
+
// Mark this process as the current holder so nested calls skip the queue.
|
|
74
|
+
_heldDepth = 1;
|
|
75
|
+
_heldFileRelease = fileRelease;
|
|
76
|
+
_heldSlotResolve = resolveSlot;
|
|
77
|
+
// Return a release function that releases both the file lock and the in-process slot
|
|
78
|
+
return async () => {
|
|
79
|
+
_heldDepth--;
|
|
80
|
+
if (_heldDepth > 0)
|
|
81
|
+
return; // nested regions still active
|
|
82
|
+
_heldDepth = 0;
|
|
83
|
+
const rel = _heldFileRelease;
|
|
84
|
+
const slotResolve = _heldSlotResolve;
|
|
85
|
+
_heldFileRelease = null;
|
|
86
|
+
_heldSlotResolve = null;
|
|
87
|
+
try {
|
|
88
|
+
if (rel)
|
|
89
|
+
await rel();
|
|
90
|
+
}
|
|
91
|
+
catch { /* already released — safe to ignore */ }
|
|
92
|
+
if (slotResolve)
|
|
93
|
+
slotResolve(); // Let the next queued caller proceed
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
97
|
+
export async function writeLockSafe(data) {
|
|
98
|
+
const release = await acquireMutex();
|
|
99
|
+
try {
|
|
100
|
+
fs.mkdirSync(LOCK_DIR, { recursive: true });
|
|
101
|
+
fs.writeFileSync(LOCK_FILE, JSON.stringify(data, null, 2), "utf-8");
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
await release();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
export async function writeLock(data) {
|
|
108
|
+
await writeLockSafe(data);
|
|
109
|
+
}
|
|
110
|
+
export function readLock() {
|
|
111
|
+
if (!fs.existsSync(LOCK_FILE))
|
|
112
|
+
return null;
|
|
113
|
+
try {
|
|
114
|
+
return JSON.parse(fs.readFileSync(LOCK_FILE, "utf-8"));
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
export function clearLock() {
|
|
121
|
+
try {
|
|
122
|
+
if (fs.existsSync(LOCK_FILE))
|
|
123
|
+
fs.unlinkSync(LOCK_FILE);
|
|
124
|
+
}
|
|
125
|
+
catch (e) {
|
|
126
|
+
logDebug(`[Weavetab] [!] Failed to clear lock file: ${e}`);
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
if (fs.existsSync(MUTEX_FILE))
|
|
130
|
+
fs.unlinkSync(MUTEX_FILE);
|
|
131
|
+
}
|
|
132
|
+
catch { /* ignore */ }
|
|
133
|
+
try {
|
|
134
|
+
const lockDir = MUTEX_FILE + ".lock";
|
|
135
|
+
if (fs.existsSync(lockDir))
|
|
136
|
+
fs.rmSync(lockDir, { recursive: true, force: true });
|
|
137
|
+
}
|
|
138
|
+
catch { /* ignore */ }
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Safe variant: only clears the lock if it was written by THIS process (process.pid).
|
|
142
|
+
* Prevents a second MCP server instance from wiping the first instance's browser lock
|
|
143
|
+
* during its retry loop, which would allow a race condition where two browsers spawn.
|
|
144
|
+
*/
|
|
145
|
+
export function clearLockIfOwned() {
|
|
146
|
+
try {
|
|
147
|
+
const lock = readLock();
|
|
148
|
+
if (!lock || lock.pid !== process.pid) {
|
|
149
|
+
logDebug(`[Weavetab] clearLockIfOwned: skipping — lock owned by PID ${lock?.pid ?? 'none'}, this PID is ${process.pid}`);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
clearLock();
|
|
153
|
+
}
|
|
154
|
+
catch (e) {
|
|
155
|
+
logDebug(`[Weavetab] clearLockIfOwned error: ${e}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
export function isProcessRunning(pid) {
|
|
159
|
+
try {
|
|
160
|
+
process.kill(pid, 0);
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Two-factor lock validation: PID alive AND port accepting TCP connections.
|
|
169
|
+
* Prevents PID-reuse false-positives after a crash.
|
|
170
|
+
*/
|
|
171
|
+
export async function isLockValid(lock) {
|
|
172
|
+
// Factor 1: PID alive
|
|
173
|
+
if (lock.pid && !isProcessRunning(lock.pid)) {
|
|
174
|
+
logDebug(`[Weavetab CDP] Lock invalid: Process ${lock.pid} is not running.`);
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
// Factor 2: TCP port accepting connections
|
|
178
|
+
return new Promise((resolve) => {
|
|
179
|
+
const socket = new net.Socket();
|
|
180
|
+
const timeout = setTimeout(() => {
|
|
181
|
+
logDebug(`[Weavetab CDP] Lock invalid: Timeout connecting to port ${lock.port}.`);
|
|
182
|
+
socket.destroy();
|
|
183
|
+
resolve(false);
|
|
184
|
+
}, 1000); // Reduced timeout to 1s for faster validation
|
|
185
|
+
socket.connect(lock.port, "127.0.0.1", () => {
|
|
186
|
+
clearTimeout(timeout);
|
|
187
|
+
socket.destroy();
|
|
188
|
+
resolve(true);
|
|
189
|
+
});
|
|
190
|
+
socket.on("error", (err) => {
|
|
191
|
+
logDebug(`[Weavetab CDP] Lock invalid: Error connecting to port ${lock.port} - ${err.message}`);
|
|
192
|
+
clearTimeout(timeout);
|
|
193
|
+
resolve(false);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
export function isLockPending(lock) {
|
|
198
|
+
if (!lock.pending)
|
|
199
|
+
return false;
|
|
200
|
+
// If it's been pending for more than 15 seconds, consider it stale/failed
|
|
201
|
+
if (lock.timestamp && Date.now() - lock.timestamp > 15000) {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
export { acquireMutex };
|