@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.
Files changed (98) hide show
  1. package/AGENTS.md +1 -0
  2. package/LICENSE +2 -2
  3. package/README.md +208 -43
  4. package/SECURITY.md +3 -3
  5. package/TROUBLESHOOTING.md +1 -1
  6. package/dist/cdp/bridge.d.ts +10 -0
  7. package/dist/cdp/bridge.js +288 -68
  8. package/dist/cdp/connector.d.ts +5 -3
  9. package/dist/cdp/connector.js +188 -74
  10. package/dist/cdp/extension.js +0 -1
  11. package/dist/cdp/helpers.js +0 -2
  12. package/dist/cdp/lock.d.ts +9 -6
  13. package/dist/cdp/lock.js +72 -59
  14. package/dist/cdp/omni_broker.d.ts +1 -0
  15. package/dist/cdp/omni_broker.js +12 -6
  16. package/dist/cdp/profiles.d.ts +10 -0
  17. package/dist/cdp/profiles.js +65 -0
  18. package/dist/cdp/walker.js +50 -0
  19. package/dist/cli/plugin.d.ts +0 -2
  20. package/dist/cli/plugin.js +31 -683
  21. package/dist/cli/updater.js +2 -2
  22. package/dist/config/loader.d.ts +25 -6
  23. package/dist/config/loader.js +133 -20
  24. package/dist/index.js +48 -52
  25. package/dist/lifecycle/mission_manager.d.ts +0 -1
  26. package/dist/lifecycle/mission_manager.js +0 -3
  27. package/dist/overlay/ghost.d.ts +4 -0
  28. package/dist/overlay/ghost.js +118 -94
  29. package/dist/overlay/injector.d.ts +1 -1
  30. package/dist/overlay/injector.js +1 -15
  31. package/dist/plugin/extension_bridge.d.ts +40 -0
  32. package/dist/plugin/extension_bridge.js +158 -0
  33. package/dist/plugin/interface.d.ts +78 -16
  34. package/dist/plugin/loader.d.ts +4 -0
  35. package/dist/plugin/loader.js +178 -57
  36. package/dist/plugin/registry.js +11 -1
  37. package/dist/plugin/storage.d.ts +51 -0
  38. package/dist/plugin/storage.js +164 -0
  39. package/dist/server.js +217 -151
  40. package/dist/state/session.d.ts +2 -2
  41. package/dist/state/session.js +33 -13
  42. package/dist/tools/browser_detect.d.ts +2 -1
  43. package/dist/tools/browser_detect.js +6 -5
  44. package/dist/tools/burst.d.ts +1 -0
  45. package/dist/tools/burst.js +4 -6
  46. package/dist/tools/click.js +23 -28
  47. package/dist/tools/clipboard.js +31 -24
  48. package/dist/tools/navigate.d.ts +1 -1
  49. package/dist/tools/navigate.js +28 -10
  50. package/dist/tools/read.js +1 -1
  51. package/dist/tools/tabs.d.ts +10 -10
  52. package/dist/tools/tabs.js +47 -39
  53. package/dist/tools/type.js +2 -2
  54. package/docs/DOCUMENTATION.md +63 -6
  55. package/docs/architecture/blind-injection.md +20 -0
  56. package/docs/architecture/dual-injection.md +32 -0
  57. package/docs/architecture/overview.md +49 -0
  58. package/docs/architecture/security.md +35 -0
  59. package/docs/architecture/transport.md +37 -0
  60. package/docs/captcha-stealth/captcha-solver.md +75 -0
  61. package/docs/captcha-stealth/stealth-physics.md +60 -0
  62. package/docs/cli/cli-commands.md +44 -0
  63. package/docs/cli/cli-flags.md +20 -0
  64. package/docs/cli/cli-introduction.md +20 -0
  65. package/docs/cli/cli-scripting.md +32 -0
  66. package/docs/cli/g-ide/ide-antigravity.md +24 -0
  67. package/docs/cli/g-ide/ide-claude-code.md +24 -0
  68. package/docs/cli/g-ide/ide-claude-desktop.md +32 -0
  69. package/docs/cli/g-ide/ide-cursor.md +31 -0
  70. package/docs/cli/g-ide/ide-generic.md +34 -0
  71. package/docs/cli/g-ide/ide-opencode.md +24 -0
  72. package/docs/cli/g-ide/ide-vscode-copilot.md +24 -0
  73. package/docs/cli/g-ide/ide-windsurf.md +24 -0
  74. package/docs/configuration/config-locations.md +24 -0
  75. package/docs/configuration/configuration.md +73 -0
  76. package/docs/configuration/env-variables.md +19 -0
  77. package/docs/configuration/folders.md +54 -0
  78. package/docs/desktop-automation/electron-apps.md +162 -0
  79. package/docs/get-started/installation.md +47 -0
  80. package/docs/get-started/introduction.md +60 -0
  81. package/docs/get-started/quickstart.md +54 -0
  82. package/docs/get-started/setup-wizard.md +26 -0
  83. package/docs/skills-ecosystem/skills-overview.md +35 -0
  84. package/docs/skills-ecosystem/skills-recipes.md +36 -0
  85. package/docs/tools/tools.md +293 -0
  86. package/docs/troubleshooting/trouble-browser.md +27 -0
  87. package/docs/troubleshooting/trouble-config.md +20 -0
  88. package/docs/troubleshooting/trouble-loop.md +31 -0
  89. package/ext-dist/content.js +126 -101
  90. package/mcp.json +8 -15
  91. package/package.json +11 -5
  92. package/server.json +3 -3
  93. package/dist/tools/plan.d.ts +0 -39
  94. package/dist/tools/plan.js +0 -161
  95. package/docs/DEV_MODE.md +0 -68
  96. package/docs/PLUGINS.md +0 -538
  97. package/docs/TOOLS.md +0 -104
  98. package/docs/dev_plugin.md +0 -310
@@ -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 { getOutputDir, getDownloadDir, getConfigHeadless, getConfigProxy, getConfigDisableExtensions, getConfigDevMode, getConfigBlock, getConfigAllow, getConfigBrowserType, getConfigBrowserPath, getConfigBrowserFlags, getConfigCookies, getConfigProfileDir } from "../config/loader.js";
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: wt config browserPath \"/path/to/browser\"");
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 extPath = path.resolve(__dirname, "../../ext-dist");
528
- // Verify extension directory exists
529
- if (!fs.existsSync(extPath)) {
530
- throw new Error(`[Weavetab] Extension directory does not exist: ${extPath}. Build the extension first.`);
531
- }
532
- const manifestPath = path.join(extPath, "manifest.json");
533
- const contentPath = path.join(extPath, "content.js");
534
- const backgroundPath = path.join(extPath, "background.js");
535
- if (!fs.existsSync(manifestPath)) {
536
- throw new Error(`[Weavetab] Extension manifest missing: ${manifestPath}`);
537
- }
538
- if (!fs.existsSync(contentPath)) {
539
- throw new Error(`[Weavetab] Extension content script missing: ${contentPath}`);
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
- if (!fs.existsSync(backgroundPath)) {
542
- throw new Error(`[Weavetab] Extension background script missing: ${backgroundPath}`);
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
- // Wipe the MV3 service worker cache before every launch so Chrome always
574
- // executes the latest background.js from disk rather than a stale cached
575
- // script stored in the profile. Without this, edits to ext-dist/background.js
576
- // have no effect until Chrome manually reloads the extension — the SW cache
577
- // (Default/Service Worker/ScriptCache + Default/Service Worker/Database) is
578
- // keyed by URL hash and persists across sessions in the user-data-dir.
579
- // This ensures any future background.js change takes effect immediately
580
- // on all existing profiles without manual cache clearing.
581
- try {
582
- const swBase = path.join(profileDir, "Default", "Service Worker");
583
- const swScriptCache = path.join(swBase, "ScriptCache");
584
- const swDatabase = path.join(swBase, "Database");
585
- if (fs.existsSync(swScriptCache)) {
586
- fs.rmSync(swScriptCache, { recursive: true, force: true });
587
- logDebug("[Weavetab] Wiped SW ScriptCache to force background.js reload.");
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
- if (fs.existsSync(swDatabase)) {
590
- fs.rmSync(swDatabase, { recursive: true, force: true });
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
- try {
679
- await new Promise(r => setTimeout(r, 1500));
680
- const testSession = await withCDPTimeout(CDP({ host: "127.0.0.1", port: actualPort }), 4000, "CDP connect");
681
- const { targetInfos } = await withCDPTimeout(testSession.Target.getTargets(), 4000, "getTargets");
682
- const extensionTargets = targetInfos.filter(t => t.type === "service_worker" || (t.type === "page" && t.url.startsWith("chrome-extension://")));
683
- await testSession.close();
684
- if (extensionTargets.length > 0) {
685
- logDebug(`[Weavetab] [OK] Extension loaded - found ${extensionTargets.length} extension target(s)`);
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
- else {
688
- logDebug(`[Weavetab] [!] Extension may not have loaded - no extension targets found. Check chrome://extensions in the browser.`);
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
+ }
@@ -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