@weavetab/mcp 2.5.0-beta.0 → 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 +288 -68
- package/dist/cdp/connector.d.ts +5 -3
- package/dist/cdp/connector.js +188 -74
- package/dist/cdp/extension.js +0 -1
- package/dist/cdp/helpers.js +0 -2
- 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/lifecycle/mission_manager.d.ts +0 -1
- package/dist/lifecycle/mission_manager.js +0 -3
- package/dist/overlay/ghost.d.ts +4 -0
- package/dist/overlay/ghost.js +118 -94
- package/dist/overlay/injector.d.ts +1 -1
- package/dist/overlay/injector.js +1 -15
- 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 +217 -151
- 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 -5
- 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/read.js +1 -1
- 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 +126 -101
- package/mcp.json +8 -15
- package/package.json +11 -5
- package/server.json +3 -3
- package/dist/tools/plan.d.ts +0 -39
- package/dist/tools/plan.js +0 -161
- 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/bridge.js
CHANGED
|
@@ -12,8 +12,9 @@ import path from "node:path";
|
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
13
|
import CDP from "chrome-remote-interface";
|
|
14
14
|
import { readLock, writeLock, clearLock, isLockValid, isLockPending } from "./lock.js";
|
|
15
|
-
import { getSessionProfile } from "./profiles.js";
|
|
16
|
-
import
|
|
15
|
+
import { getSessionProfile, getElectronProfile } from "./profiles.js";
|
|
16
|
+
import net from "node:net";
|
|
17
|
+
import { getOutputDir, getDownloadDir, getConfigHeadless, getConfigProxy, getConfigDevMode, getConfigBlock, getConfigAllow, getConfigBrowserType, getConfigBrowserPath, getConfigBrowserFlags, getConfigCookies, getConfigProfileDir, getSystemDir, getElectronAppConfig, getConfigExtension } from "../config/loader.js";
|
|
17
18
|
import { createBrowserFingerprint } from "./security.js";
|
|
18
19
|
function withCDPTimeout(promise, ms, label) {
|
|
19
20
|
return Promise.race([
|
|
@@ -97,6 +98,37 @@ function isValidBrowserPath(browserPath) {
|
|
|
97
98
|
}
|
|
98
99
|
return true;
|
|
99
100
|
}
|
|
101
|
+
export function isValidElectronExecutablePath(execPath) {
|
|
102
|
+
if (!execPath || typeof execPath !== "string")
|
|
103
|
+
return false;
|
|
104
|
+
const normalizedPath = execPath.toLowerCase().trim();
|
|
105
|
+
// Check for suspicious patterns that might indicate command injection or shell escapes
|
|
106
|
+
const suspiciousPatterns = [
|
|
107
|
+
'..', // path traversal
|
|
108
|
+
'%', // URL encoding
|
|
109
|
+
'cmd.exe', 'powershell.exe', 'bash', 'sh', 'wscript.exe', 'cscript.exe'
|
|
110
|
+
];
|
|
111
|
+
if (suspiciousPatterns.some(pattern => normalizedPath.includes(pattern))) {
|
|
112
|
+
logDebug(`[SECURITY] Electron path validation failed: suspicious pattern in path: ${execPath}`);
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
// Verify that the file actually exists
|
|
116
|
+
try {
|
|
117
|
+
if (!fs.existsSync(execPath)) {
|
|
118
|
+
logDebug(`[SECURITY] Electron path validation failed: file does not exist: ${execPath}`);
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
const stat = fs.statSync(execPath);
|
|
122
|
+
if (!stat.isFile()) {
|
|
123
|
+
logDebug(`[SECURITY] Electron path validation failed: target is not a file: ${execPath}`);
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
100
132
|
function pathsForPlatform(browserName) {
|
|
101
133
|
const plat = os.platform();
|
|
102
134
|
const winPaths = [];
|
|
@@ -424,14 +456,6 @@ function getLaunchFlags(config, extPath) {
|
|
|
424
456
|
flags.push("--headless=new");
|
|
425
457
|
if (getConfigProxy(config))
|
|
426
458
|
flags.push(`--proxy-server=${getConfigProxy(config)}`);
|
|
427
|
-
// disableExtensions: NOTE — --disable-extensions kills ALL extensions including ours.
|
|
428
|
-
// --load-extension does NOT reliably override --disable-extensions on Edge.
|
|
429
|
-
// Since we can't disable other extensions while keeping ours (unpacked extensions
|
|
430
|
-
// have unpredictable IDs), we skip the flag entirely and let other extensions coexist.
|
|
431
|
-
// The user can manually disable extensions in their browser profile if needed.
|
|
432
|
-
if (getConfigDisableExtensions(config)) {
|
|
433
|
-
logDebug("[Weavetab] [SAFETY] disableExtensions=true — cannot safely disable other extensions while keeping Weavetab extension. Loading extension alongside any existing ones.");
|
|
434
|
-
}
|
|
435
459
|
// devMode: enable verbose remote debugging
|
|
436
460
|
if (getConfigDevMode(config)) {
|
|
437
461
|
flags.push("--enable-logging", "--v=1");
|
|
@@ -477,7 +501,7 @@ export async function launchWithBridge(config) {
|
|
|
477
501
|
" - Google Chrome (https://www.google.com/chrome/)\n" +
|
|
478
502
|
" - Brave (https://brave.com/)\n" +
|
|
479
503
|
" - Microsoft Edge (https://www.microsoft.com/edge)\n" +
|
|
480
|
-
" If you have a browser installed in a custom location, set it via:
|
|
504
|
+
" If you have a browser installed in a custom location, set it via: weavetab config browserPath \"/path/to/browser\"");
|
|
481
505
|
}
|
|
482
506
|
const debugPort = 0;
|
|
483
507
|
const lock = readLock();
|
|
@@ -524,31 +548,53 @@ export async function launchWithBridge(config) {
|
|
|
524
548
|
await new Promise(r => setTimeout(r, 1500));
|
|
525
549
|
}
|
|
526
550
|
try {
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
551
|
+
const enableExtension = getConfigExtension(config) !== false;
|
|
552
|
+
let extPath = "";
|
|
553
|
+
let normalizedExtPath = "";
|
|
554
|
+
if (enableExtension) {
|
|
555
|
+
extPath = path.resolve(__dirname, "../../ext-dist");
|
|
556
|
+
const systemExtPath = getSystemDir("extension");
|
|
557
|
+
if (fs.existsSync(extPath)) {
|
|
558
|
+
try {
|
|
559
|
+
fs.cpSync(extPath, systemExtPath, { recursive: true, force: true });
|
|
560
|
+
}
|
|
561
|
+
catch (e) {
|
|
562
|
+
logDebug(`[Weavetab] Failed to sync extension to system directory: ${e}`);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
else {
|
|
566
|
+
if (fs.existsSync(systemExtPath)) {
|
|
567
|
+
logDebug(`[Weavetab] Local extension missing. Falling back to system extension directory.`);
|
|
568
|
+
extPath = systemExtPath;
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
throw new Error(`[Weavetab] Extension directory does not exist: ${extPath}. Build the extension first.`);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
const manifestPath = path.join(extPath, "manifest.json");
|
|
575
|
+
const contentPath = path.join(extPath, "content.js");
|
|
576
|
+
const backgroundPath = path.join(extPath, "background.js");
|
|
577
|
+
if (!fs.existsSync(manifestPath)) {
|
|
578
|
+
throw new Error(`[Weavetab] Extension manifest missing: ${manifestPath}`);
|
|
579
|
+
}
|
|
580
|
+
if (!fs.existsSync(contentPath)) {
|
|
581
|
+
throw new Error(`[Weavetab] Extension content script missing: ${contentPath}`);
|
|
582
|
+
}
|
|
583
|
+
if (!fs.existsSync(backgroundPath)) {
|
|
584
|
+
throw new Error(`[Weavetab] Extension background script missing: ${backgroundPath}`);
|
|
585
|
+
}
|
|
586
|
+
normalizedExtPath = os.platform() === 'win32'
|
|
587
|
+
? extPath.replace(/\//g, '\\')
|
|
588
|
+
: extPath;
|
|
540
589
|
}
|
|
541
|
-
|
|
542
|
-
|
|
590
|
+
else {
|
|
591
|
+
logDebug(`[Weavetab] Browser extension loading disabled via config (extension: false).`);
|
|
543
592
|
}
|
|
544
593
|
const resolvedDownloadDir = getDownloadDir(config) && getDownloadDir(config).trim()
|
|
545
594
|
? getDownloadDir(config).trim()
|
|
546
595
|
: getOutputDir("downloads", config);
|
|
547
596
|
// Normalize paths for cross-platform compatibility
|
|
548
597
|
const platform = os.platform();
|
|
549
|
-
const normalizedExtPath = platform === 'win32'
|
|
550
|
-
? extPath.replace(/\//g, '\\')
|
|
551
|
-
: extPath;
|
|
552
598
|
const normalizedProfileDir = platform === 'win32'
|
|
553
599
|
? profileDir.replace(/\//g, '\\')
|
|
554
600
|
: profileDir;
|
|
@@ -570,40 +616,34 @@ export async function launchWithBridge(config) {
|
|
|
570
616
|
fs.unlinkSync(singletonCookie);
|
|
571
617
|
}
|
|
572
618
|
catch { /* non-fatal */ }
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
619
|
+
if (enableExtension) {
|
|
620
|
+
// Wipe the MV3 service worker cache before every launch so Chrome always
|
|
621
|
+
// executes the latest background.js from disk rather than a stale cached
|
|
622
|
+
// script stored in the profile.
|
|
623
|
+
try {
|
|
624
|
+
const swBase = path.join(profileDir, "Default", "Service Worker");
|
|
625
|
+
const swScriptCache = path.join(swBase, "ScriptCache");
|
|
626
|
+
const swDatabase = path.join(swBase, "Database");
|
|
627
|
+
if (fs.existsSync(swScriptCache)) {
|
|
628
|
+
fs.rmSync(swScriptCache, { recursive: true, force: true });
|
|
629
|
+
logDebug("[Weavetab] Wiped SW ScriptCache to force background.js reload.");
|
|
630
|
+
}
|
|
631
|
+
if (fs.existsSync(swDatabase)) {
|
|
632
|
+
fs.rmSync(swDatabase, { recursive: true, force: true });
|
|
633
|
+
logDebug("[Weavetab] Wiped SW Database to force background.js reload.");
|
|
634
|
+
}
|
|
588
635
|
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
logDebug("[Weavetab] Wiped SW Database to force background.js reload.");
|
|
636
|
+
catch (swErr) {
|
|
637
|
+
logDebug(`[Weavetab] SW cache wipe failed (non-fatal): ${swErr}`);
|
|
592
638
|
}
|
|
593
639
|
}
|
|
594
|
-
catch (swErr) {
|
|
595
|
-
logDebug(`[Weavetab] SW cache wipe failed (non-fatal): ${swErr}`);
|
|
596
|
-
}
|
|
597
640
|
const args = [
|
|
598
641
|
`--remote-debugging-port=${debugPort}`,
|
|
599
642
|
`--user-data-dir=${normalizedProfileDir}`,
|
|
600
643
|
`--default-download-dir=${normalizedDownloadDir}`,
|
|
601
644
|
...getLaunchFlags(config, extPath),
|
|
602
645
|
...filterBrowserFlags(getConfigBrowserFlags(config) || []),
|
|
603
|
-
`--load-extension=${normalizedExtPath}
|
|
604
|
-
// NOTE: --disable-extensions-except was removed. It was killing any
|
|
605
|
-
// extensions the user manually loaded into the Weavetab profile.
|
|
606
|
-
// --load-extension alone is sufficient to auto-load the Weavetab extension.
|
|
646
|
+
...(enableExtension ? [`--load-extension=${normalizedExtPath}`] : []),
|
|
607
647
|
"about:blank",
|
|
608
648
|
];
|
|
609
649
|
let pid;
|
|
@@ -672,25 +712,27 @@ export async function launchWithBridge(config) {
|
|
|
672
712
|
}
|
|
673
713
|
else {
|
|
674
714
|
logDebug(`[Weavetab] [OK] ${browser.name} ready on :${actualPort} (attempt ${attempt})`);
|
|
675
|
-
// Verify extension loaded by checking for extension pages.
|
|
715
|
+
// Verify extension loaded by checking for extension pages if extension is enabled.
|
|
676
716
|
// MV3 service workers can take 1-3s to initialize after Chrome starts,
|
|
677
717
|
// so we wait briefly before querying targets.
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
718
|
+
if (enableExtension) {
|
|
719
|
+
try {
|
|
720
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
721
|
+
const testSession = await withCDPTimeout(CDP({ host: "127.0.0.1", port: actualPort }), 4000, "CDP connect");
|
|
722
|
+
const { targetInfos } = await withCDPTimeout(testSession.Target.getTargets(), 4000, "getTargets");
|
|
723
|
+
const extensionTargets = targetInfos.filter(t => t.type === "service_worker" || (t.type === "page" && t.url.startsWith("chrome-extension://")));
|
|
724
|
+
await testSession.close();
|
|
725
|
+
if (extensionTargets.length > 0) {
|
|
726
|
+
logDebug(`[Weavetab] [OK] Extension loaded - found ${extensionTargets.length} extension target(s)`);
|
|
727
|
+
}
|
|
728
|
+
else {
|
|
729
|
+
logDebug(`[Weavetab] [!] Extension may not have loaded - no extension targets found. Check chrome://extensions in the browser.`);
|
|
730
|
+
}
|
|
686
731
|
}
|
|
687
|
-
|
|
688
|
-
logDebug(`[Weavetab] [!]
|
|
732
|
+
catch (extCheckErr) {
|
|
733
|
+
logDebug(`[Weavetab] [!] Could not verify extension load: ${extCheckErr}`);
|
|
689
734
|
}
|
|
690
735
|
}
|
|
691
|
-
catch (extCheckErr) {
|
|
692
|
-
logDebug(`[Weavetab] [!] Could not verify extension load: ${extCheckErr}`);
|
|
693
|
-
}
|
|
694
736
|
}
|
|
695
737
|
await writeLock({ pid, port: actualPort, profileDir, browserPath: browser.path, mode: "session", pending: false });
|
|
696
738
|
recordLaunchSuccess(profileDir);
|
|
@@ -824,3 +866,181 @@ async function resolvePortFromStderr(proc, pid, _engine) {
|
|
|
824
866
|
});
|
|
825
867
|
});
|
|
826
868
|
}
|
|
869
|
+
export async function getFreePort(preferredPort = 9222) {
|
|
870
|
+
const isAvailable = (p) => {
|
|
871
|
+
return new Promise((resolve) => {
|
|
872
|
+
const s = net.createServer();
|
|
873
|
+
s.once("error", () => resolve(false));
|
|
874
|
+
s.once("listening", () => {
|
|
875
|
+
s.close(() => resolve(true));
|
|
876
|
+
});
|
|
877
|
+
s.listen(p, "127.0.0.1");
|
|
878
|
+
});
|
|
879
|
+
};
|
|
880
|
+
if (preferredPort > 0 && (await isAvailable(preferredPort))) {
|
|
881
|
+
return preferredPort;
|
|
882
|
+
}
|
|
883
|
+
return new Promise((resolve, reject) => {
|
|
884
|
+
const s = net.createServer();
|
|
885
|
+
s.once("error", reject);
|
|
886
|
+
s.once("listening", () => {
|
|
887
|
+
const addr = s.address();
|
|
888
|
+
const p = typeof addr === "object" && addr ? addr.port : 9222;
|
|
889
|
+
s.close(() => resolve(p));
|
|
890
|
+
});
|
|
891
|
+
s.listen(0, "127.0.0.1");
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
export function testPortListening(port) {
|
|
895
|
+
return new Promise((resolve) => {
|
|
896
|
+
const socket = new net.Socket();
|
|
897
|
+
const timeout = setTimeout(() => {
|
|
898
|
+
socket.destroy();
|
|
899
|
+
resolve(false);
|
|
900
|
+
}, 1000);
|
|
901
|
+
socket.connect(port, "127.0.0.1", () => {
|
|
902
|
+
clearTimeout(timeout);
|
|
903
|
+
socket.destroy();
|
|
904
|
+
resolve(true);
|
|
905
|
+
});
|
|
906
|
+
socket.on("error", () => {
|
|
907
|
+
clearTimeout(timeout);
|
|
908
|
+
resolve(false);
|
|
909
|
+
});
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
export async function launchElectronApp(appName, config) {
|
|
913
|
+
const appCfg = getElectronAppConfig(appName, config);
|
|
914
|
+
if (!appCfg) {
|
|
915
|
+
throw new Error(`[Weavetab] ✗ Electron app "${appName}" is not configured in config.json.\n` +
|
|
916
|
+
` Please add an entry under "electron": { "${appName}": "C:\\\\path\\\\to\\\\app.exe" } in your config.`);
|
|
917
|
+
}
|
|
918
|
+
// 1. Direct port dev attach mode (e.g., dev server or npm run dev)
|
|
919
|
+
if (appCfg.port && !appCfg.path) {
|
|
920
|
+
const isAlive = await testPortListening(appCfg.port);
|
|
921
|
+
if (isAlive) {
|
|
922
|
+
logDebug(`[Weavetab] Attached to live Electron dev server on port ${appCfg.port}`);
|
|
923
|
+
return { path: "dev-server", name: appName, port: appCfg.port, engine: "chromium", isElectron: true };
|
|
924
|
+
}
|
|
925
|
+
throw new Error(`[Weavetab] ✗ Electron dev app "${appName}" on port ${appCfg.port} is not responding. Ensure your dev server is running.`);
|
|
926
|
+
}
|
|
927
|
+
// 2. Direct URL dev attach mode
|
|
928
|
+
if (appCfg.url && !appCfg.path) {
|
|
929
|
+
try {
|
|
930
|
+
const parsed = new URL(appCfg.url);
|
|
931
|
+
const port = parseInt(parsed.port || (parsed.protocol === "https:" ? "443" : "80"), 10);
|
|
932
|
+
const isAlive = await testPortListening(port);
|
|
933
|
+
if (isAlive) {
|
|
934
|
+
logDebug(`[Weavetab] Attached to live Electron app at ${appCfg.url} (port ${port})`);
|
|
935
|
+
return { path: appCfg.url, name: appName, port, engine: "chromium", isElectron: true };
|
|
936
|
+
}
|
|
937
|
+
throw new Error(`[Weavetab] ✗ Electron dev app "${appName}" at ${appCfg.url} is not responding.`);
|
|
938
|
+
}
|
|
939
|
+
catch (e) {
|
|
940
|
+
throw new Error(`[Weavetab] ✗ Invalid or unreachable dev URL for "${appName}": ${e?.message || e}`);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
const execPath = appCfg.path;
|
|
944
|
+
if (!execPath || !isValidElectronExecutablePath(execPath)) {
|
|
945
|
+
throw new Error(`[Weavetab] ✗ Invalid or non-existent Electron executable path for "${appName}": ${execPath}`);
|
|
946
|
+
}
|
|
947
|
+
// Check app-scoped lock
|
|
948
|
+
const lock = readLock(appName);
|
|
949
|
+
if (lock && lock.mode === "session") {
|
|
950
|
+
if (isLockPending(lock)) {
|
|
951
|
+
logDebug(`[Weavetab] Launch already in progress for ${appName} (PID: ${lock.pid}). Waiting...`);
|
|
952
|
+
throw new Error(`Launch already in progress for ${appName}. Wait for it to complete.`);
|
|
953
|
+
}
|
|
954
|
+
const valid = await isLockValid(lock);
|
|
955
|
+
if (valid) {
|
|
956
|
+
logDebug(`[Weavetab] [OK] Reusing valid Electron lock for ${appName} (PID: ${lock.pid}, Port: ${lock.port})`);
|
|
957
|
+
return { path: execPath, name: appName, port: lock.port, engine: "chromium", isElectron: true };
|
|
958
|
+
}
|
|
959
|
+
logDebug(`[Weavetab] [!] Stale lock detected for ${appName}. Clearing.`);
|
|
960
|
+
clearLock(appName);
|
|
961
|
+
}
|
|
962
|
+
else if (lock) {
|
|
963
|
+
clearLock(appName);
|
|
964
|
+
}
|
|
965
|
+
const profileDir = getElectronProfile(appName, appCfg.profileDir);
|
|
966
|
+
const actualPort = appCfg.port && appCfg.port > 0 ? appCfg.port : await getFreePort(9222);
|
|
967
|
+
// Clean up SingletonLock
|
|
968
|
+
const singletonLock = path.join(profileDir, "SingletonLock");
|
|
969
|
+
const singletonCookie = path.join(profileDir, "SingletonCookie");
|
|
970
|
+
try {
|
|
971
|
+
if (fs.existsSync(singletonLock))
|
|
972
|
+
fs.unlinkSync(singletonLock);
|
|
973
|
+
}
|
|
974
|
+
catch { /* non-fatal */ }
|
|
975
|
+
try {
|
|
976
|
+
if (fs.existsSync(singletonCookie))
|
|
977
|
+
fs.unlinkSync(singletonCookie);
|
|
978
|
+
}
|
|
979
|
+
catch { /* non-fatal */ }
|
|
980
|
+
const platform = os.platform();
|
|
981
|
+
const normalizedProfileDir = platform === 'win32'
|
|
982
|
+
? profileDir.replace(/\//g, '\\')
|
|
983
|
+
: profileDir;
|
|
984
|
+
const resolvedDownloadDir = getDownloadDir(config) && getDownloadDir(config).trim()
|
|
985
|
+
? getDownloadDir(config).trim()
|
|
986
|
+
: getOutputDir("downloads", config);
|
|
987
|
+
const normalizedDownloadDir = platform === 'win32'
|
|
988
|
+
? resolvedDownloadDir.replace(/\//g, '\\')
|
|
989
|
+
: resolvedDownloadDir;
|
|
990
|
+
// Notice: Clean Electron launch flags: only isolated user-data-dir, remote-debugging-port, and optional user args.
|
|
991
|
+
// We NEVER pass Chromium browser flags or extension flags to Electron binaries.
|
|
992
|
+
const args = [
|
|
993
|
+
`--remote-debugging-port=${actualPort}`,
|
|
994
|
+
`--user-data-dir=${normalizedProfileDir}`,
|
|
995
|
+
`--default-download-dir=${normalizedDownloadDir}`,
|
|
996
|
+
"--no-first-run",
|
|
997
|
+
"--no-default-browser-check",
|
|
998
|
+
...(appCfg.args || []),
|
|
999
|
+
];
|
|
1000
|
+
const spawnOptions = {
|
|
1001
|
+
detached: true,
|
|
1002
|
+
stdio: "ignore",
|
|
1003
|
+
};
|
|
1004
|
+
if (platform === 'darwin' && execPath.includes('.app')) {
|
|
1005
|
+
spawnOptions.shell = true;
|
|
1006
|
+
}
|
|
1007
|
+
const proc = spawn(execPath, args, spawnOptions);
|
|
1008
|
+
proc.unref();
|
|
1009
|
+
const pid = proc.pid;
|
|
1010
|
+
logDebug(`[Weavetab] Launching Electron app ${appName} (PID: ${pid}, port: ${actualPort}).`);
|
|
1011
|
+
// Poll CDP endpoint on actualPort
|
|
1012
|
+
let confirmed = false;
|
|
1013
|
+
for (let i = 0; i < 20; i++) {
|
|
1014
|
+
try {
|
|
1015
|
+
await withCDPTimeout(CDP.List({ host: "127.0.0.1", port: actualPort }), 1500, "CDP.List Electron");
|
|
1016
|
+
confirmed = true;
|
|
1017
|
+
break;
|
|
1018
|
+
}
|
|
1019
|
+
catch {
|
|
1020
|
+
await new Promise(r => setTimeout(r, 600));
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
if (!confirmed) {
|
|
1024
|
+
const isListening = await testPortListening(actualPort);
|
|
1025
|
+
if (!isListening) {
|
|
1026
|
+
try {
|
|
1027
|
+
if (pid)
|
|
1028
|
+
process.kill(pid);
|
|
1029
|
+
}
|
|
1030
|
+
catch { /* ignore */ }
|
|
1031
|
+
clearLock(appName);
|
|
1032
|
+
throw new Error(`[Weavetab] Electron app ${appName} failed to start CDP listener on port ${actualPort} after 12s.`);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
await writeLock({
|
|
1036
|
+
pid,
|
|
1037
|
+
port: actualPort,
|
|
1038
|
+
profileDir,
|
|
1039
|
+
browserPath: execPath,
|
|
1040
|
+
mode: "session",
|
|
1041
|
+
appName,
|
|
1042
|
+
pending: false,
|
|
1043
|
+
timestamp: Date.now(),
|
|
1044
|
+
}, appName);
|
|
1045
|
+
return { path: execPath, name: appName, port: actualPort, engine: "chromium", isElectron: true };
|
|
1046
|
+
}
|
package/dist/cdp/connector.d.ts
CHANGED
|
@@ -17,12 +17,14 @@ export interface BrowserClient {
|
|
|
17
17
|
overlayInstalled: boolean;
|
|
18
18
|
agentId: string;
|
|
19
19
|
browserContextId?: string;
|
|
20
|
+
appName?: string;
|
|
21
|
+
isElectron?: boolean;
|
|
20
22
|
/** Sticky pruning state: if true, all subsequent DOM reads by action tools (click, type, etc.) will also apply pruning. */
|
|
21
23
|
pruneActive?: boolean;
|
|
22
24
|
}
|
|
23
|
-
export declare function getOrConnect(config: Config): Promise<BrowserClient>;
|
|
24
|
-
export declare function getTabList(agentId?: string): Promise<any[]>;
|
|
25
|
-
export declare function connectToTab(targetId: string, port?: number, agentId?: string, browserContextId?: string): Promise<BrowserClient>;
|
|
25
|
+
export declare function getOrConnect(config: Config, targetApp?: string): Promise<BrowserClient>;
|
|
26
|
+
export declare function getTabList(agentId?: string, targetApp?: string): Promise<any[]>;
|
|
27
|
+
export declare function connectToTab(targetId: string, port?: number, agentId?: string, browserContextId?: string, appName?: string): Promise<BrowserClient>;
|
|
26
28
|
/**
|
|
27
29
|
* Cancel analytics intervals and close the CDP WebSocket sessions without
|
|
28
30
|
* touching the browser process. Safe to call in server mode — the browser
|