@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/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,45 +548,53 @@ export async function launchWithBridge(config) {
|
|
|
524
548
|
await new Promise(r => setTimeout(r, 1500));
|
|
525
549
|
}
|
|
526
550
|
try {
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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
|
+
}
|
|
532
564
|
}
|
|
533
|
-
|
|
534
|
-
|
|
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
|
+
}
|
|
535
573
|
}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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}`);
|
|
541
579
|
}
|
|
542
|
-
|
|
543
|
-
throw new Error(`[Weavetab] Extension
|
|
580
|
+
if (!fs.existsSync(contentPath)) {
|
|
581
|
+
throw new Error(`[Weavetab] Extension content script missing: ${contentPath}`);
|
|
544
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;
|
|
545
589
|
}
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
const backgroundPath = path.join(extPath, "background.js");
|
|
549
|
-
if (!fs.existsSync(manifestPath)) {
|
|
550
|
-
throw new Error(`[Weavetab] Extension manifest missing: ${manifestPath}`);
|
|
551
|
-
}
|
|
552
|
-
if (!fs.existsSync(contentPath)) {
|
|
553
|
-
throw new Error(`[Weavetab] Extension content script missing: ${contentPath}`);
|
|
554
|
-
}
|
|
555
|
-
if (!fs.existsSync(backgroundPath)) {
|
|
556
|
-
throw new Error(`[Weavetab] Extension background script missing: ${backgroundPath}`);
|
|
590
|
+
else {
|
|
591
|
+
logDebug(`[Weavetab] Browser extension loading disabled via config (extension: false).`);
|
|
557
592
|
}
|
|
558
593
|
const resolvedDownloadDir = getDownloadDir(config) && getDownloadDir(config).trim()
|
|
559
594
|
? getDownloadDir(config).trim()
|
|
560
595
|
: getOutputDir("downloads", config);
|
|
561
596
|
// Normalize paths for cross-platform compatibility
|
|
562
597
|
const platform = os.platform();
|
|
563
|
-
const normalizedExtPath = platform === 'win32'
|
|
564
|
-
? extPath.replace(/\//g, '\\')
|
|
565
|
-
: extPath;
|
|
566
598
|
const normalizedProfileDir = platform === 'win32'
|
|
567
599
|
? profileDir.replace(/\//g, '\\')
|
|
568
600
|
: profileDir;
|
|
@@ -584,40 +616,34 @@ export async function launchWithBridge(config) {
|
|
|
584
616
|
fs.unlinkSync(singletonCookie);
|
|
585
617
|
}
|
|
586
618
|
catch { /* non-fatal */ }
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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
|
+
}
|
|
602
635
|
}
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
logDebug("[Weavetab] Wiped SW Database to force background.js reload.");
|
|
636
|
+
catch (swErr) {
|
|
637
|
+
logDebug(`[Weavetab] SW cache wipe failed (non-fatal): ${swErr}`);
|
|
606
638
|
}
|
|
607
639
|
}
|
|
608
|
-
catch (swErr) {
|
|
609
|
-
logDebug(`[Weavetab] SW cache wipe failed (non-fatal): ${swErr}`);
|
|
610
|
-
}
|
|
611
640
|
const args = [
|
|
612
641
|
`--remote-debugging-port=${debugPort}`,
|
|
613
642
|
`--user-data-dir=${normalizedProfileDir}`,
|
|
614
643
|
`--default-download-dir=${normalizedDownloadDir}`,
|
|
615
644
|
...getLaunchFlags(config, extPath),
|
|
616
645
|
...filterBrowserFlags(getConfigBrowserFlags(config) || []),
|
|
617
|
-
`--load-extension=${normalizedExtPath}
|
|
618
|
-
// NOTE: --disable-extensions-except was removed. It was killing any
|
|
619
|
-
// extensions the user manually loaded into the Weavetab profile.
|
|
620
|
-
// --load-extension alone is sufficient to auto-load the Weavetab extension.
|
|
646
|
+
...(enableExtension ? [`--load-extension=${normalizedExtPath}`] : []),
|
|
621
647
|
"about:blank",
|
|
622
648
|
];
|
|
623
649
|
let pid;
|
|
@@ -686,25 +712,27 @@ export async function launchWithBridge(config) {
|
|
|
686
712
|
}
|
|
687
713
|
else {
|
|
688
714
|
logDebug(`[Weavetab] [OK] ${browser.name} ready on :${actualPort} (attempt ${attempt})`);
|
|
689
|
-
// Verify extension loaded by checking for extension pages.
|
|
715
|
+
// Verify extension loaded by checking for extension pages if extension is enabled.
|
|
690
716
|
// MV3 service workers can take 1-3s to initialize after Chrome starts,
|
|
691
717
|
// so we wait briefly before querying targets.
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
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
|
+
}
|
|
700
731
|
}
|
|
701
|
-
|
|
702
|
-
logDebug(`[Weavetab] [!]
|
|
732
|
+
catch (extCheckErr) {
|
|
733
|
+
logDebug(`[Weavetab] [!] Could not verify extension load: ${extCheckErr}`);
|
|
703
734
|
}
|
|
704
735
|
}
|
|
705
|
-
catch (extCheckErr) {
|
|
706
|
-
logDebug(`[Weavetab] [!] Could not verify extension load: ${extCheckErr}`);
|
|
707
|
-
}
|
|
708
736
|
}
|
|
709
737
|
await writeLock({ pid, port: actualPort, profileDir, browserPath: browser.path, mode: "session", pending: false });
|
|
710
738
|
recordLaunchSuccess(profileDir);
|
|
@@ -838,3 +866,181 @@ async function resolvePortFromStderr(proc, pid, _engine) {
|
|
|
838
866
|
});
|
|
839
867
|
});
|
|
840
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
|