@weavetab/mcp 2.5.0-beta.1 → 2.5.0-beta.4

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 (156) hide show
  1. package/AGENTS.md +2 -1
  2. package/ARCHITECTURE.md +6 -6
  3. package/LICENSE +1 -1
  4. package/README.md +164 -802
  5. package/SECURITY.md +33 -22
  6. package/TROUBLESHOOTING.md +3 -3
  7. package/dist/cdp/bridge.d.ts +10 -0
  8. package/dist/cdp/bridge.js +285 -78
  9. package/dist/cdp/connector.d.ts +7 -3
  10. package/dist/cdp/connector.js +196 -75
  11. package/dist/cdp/helpers.d.ts +4 -0
  12. package/dist/cdp/helpers.js +181 -0
  13. package/dist/cdp/lock.d.ts +9 -6
  14. package/dist/cdp/lock.js +72 -59
  15. package/dist/cdp/omni_broker.d.ts +2 -1
  16. package/dist/cdp/omni_broker.js +13 -7
  17. package/dist/cdp/profiles.d.ts +10 -0
  18. package/dist/cdp/profiles.js +65 -0
  19. package/dist/cdp/walker.d.ts +25 -1
  20. package/dist/cdp/walker.js +354 -7
  21. package/dist/cli/plugin.d.ts +0 -2
  22. package/dist/cli/plugin.js +51 -696
  23. package/dist/cli/updater.d.ts +1 -1
  24. package/dist/cli/updater.js +27 -7
  25. package/dist/config/loader.d.ts +33 -6
  26. package/dist/config/loader.js +174 -22
  27. package/dist/desktop/discovery.d.ts +44 -0
  28. package/dist/desktop/discovery.js +364 -0
  29. package/dist/index.js +140 -64
  30. package/dist/intelligence/marks.d.ts +36 -0
  31. package/dist/intelligence/marks.js +30 -0
  32. package/dist/intelligence/patternCompiler.d.ts +118 -0
  33. package/dist/intelligence/patternCompiler.js +259 -0
  34. package/dist/intelligence/sessionMemory.d.ts +2 -0
  35. package/dist/intelligence/sessionMemory.js +18 -8
  36. package/dist/overlay/ghost.js +2 -2
  37. package/dist/overlay/injector.js +3 -5
  38. package/dist/plugin/extension_bridge.d.ts +40 -0
  39. package/dist/plugin/extension_bridge.js +158 -0
  40. package/dist/plugin/interface.d.ts +151 -15
  41. package/dist/plugin/loader.d.ts +19 -1
  42. package/dist/plugin/loader.js +331 -63
  43. package/dist/plugin/pipeline.d.ts +75 -0
  44. package/dist/plugin/pipeline.js +183 -0
  45. package/dist/plugin/registry.js +12 -1
  46. package/dist/plugin/storage.d.ts +51 -0
  47. package/dist/plugin/storage.js +164 -0
  48. package/dist/security/secrets.d.ts +1 -0
  49. package/dist/security/secrets.js +13 -0
  50. package/dist/sensors/actionability.js +1 -2
  51. package/dist/sensors/guard.js +4 -10
  52. package/dist/sensors/mutations.js +12 -0
  53. package/dist/sensors/operation_monitor.js +1 -0
  54. package/dist/server.d.ts +1 -1
  55. package/dist/server.js +600 -236
  56. package/dist/state/agent.d.ts +1 -0
  57. package/dist/state/agent.js +3 -0
  58. package/dist/state/delta.d.ts +12 -4
  59. package/dist/state/delta.js +67 -14
  60. package/dist/state/session.d.ts +8 -2
  61. package/dist/state/session.js +60 -13
  62. package/dist/tools/browser_detect.d.ts +16 -2
  63. package/dist/tools/browser_detect.js +54 -6
  64. package/dist/tools/burst.d.ts +17 -7
  65. package/dist/tools/burst.js +240 -83
  66. package/dist/tools/checkpoint.d.ts +38 -0
  67. package/dist/tools/checkpoint.js +271 -0
  68. package/dist/tools/click.d.ts +1 -0
  69. package/dist/tools/click.js +97 -98
  70. package/dist/tools/clipboard.js +31 -24
  71. package/dist/tools/design_extract.js +1 -1
  72. package/dist/tools/eval.js +1 -1
  73. package/dist/tools/fill.d.ts +20 -1
  74. package/dist/tools/fill.js +199 -40
  75. package/dist/tools/find.d.ts +16 -0
  76. package/dist/tools/find.js +67 -44
  77. package/dist/tools/github/index.d.ts +19 -4
  78. package/dist/tools/github/index.js +40 -4
  79. package/dist/tools/highlight.d.ts +6 -2
  80. package/dist/tools/highlight.js +158 -72
  81. package/dist/tools/inspect.js +1 -1
  82. package/dist/tools/key.js +65 -56
  83. package/dist/tools/navigate.d.ts +1 -1
  84. package/dist/tools/navigate.js +57 -44
  85. package/dist/tools/pdf.js +21 -13
  86. package/dist/tools/pointer.d.ts +18 -3
  87. package/dist/tools/pointer.js +89 -1
  88. package/dist/tools/read.d.ts +28 -8
  89. package/dist/tools/read.js +230 -56
  90. package/dist/tools/scrape.js +67 -13
  91. package/dist/tools/screenshot.js +21 -16
  92. package/dist/tools/scroll.js +6 -11
  93. package/dist/tools/select.js +8 -2
  94. package/dist/tools/snapshot.js +3 -34
  95. package/dist/tools/tabs.d.ts +10 -10
  96. package/dist/tools/tabs.js +55 -39
  97. package/dist/tools/thought.d.ts +2 -0
  98. package/dist/tools/thought.js +13 -14
  99. package/dist/tools/type.d.ts +2 -0
  100. package/dist/tools/type.js +109 -60
  101. package/dist/tools/type_secret.js +42 -76
  102. package/dist/tools/upload.js +1 -1
  103. package/dist/tools/video_compiler.d.ts +1 -1
  104. package/dist/tools/video_compiler.js +11 -12
  105. package/dist/version.d.ts +5 -0
  106. package/dist/version.js +5 -0
  107. package/docs/DOCUMENTATION.md +82 -36
  108. package/docs/PLUGINS.md +82 -499
  109. package/docs/TOOLS.md +51 -99
  110. package/docs/architecture/blind-injection.md +83 -0
  111. package/docs/architecture/dual-injection.md +32 -0
  112. package/docs/architecture/overview.md +49 -0
  113. package/docs/architecture/security.md +35 -0
  114. package/docs/architecture/transport.md +37 -0
  115. package/docs/captcha-stealth/captcha-solver.md +75 -0
  116. package/docs/captcha-stealth/stealth-physics.md +60 -0
  117. package/docs/cli/cli-commands.md +44 -0
  118. package/docs/cli/cli-flags.md +20 -0
  119. package/docs/cli/cli-introduction.md +20 -0
  120. package/docs/cli/cli-scripting.md +32 -0
  121. package/docs/cli/g-ide/ide-antigravity.md +24 -0
  122. package/docs/cli/g-ide/ide-claude-code.md +24 -0
  123. package/docs/cli/g-ide/ide-claude-desktop.md +32 -0
  124. package/docs/cli/g-ide/ide-cursor.md +31 -0
  125. package/docs/cli/g-ide/ide-generic.md +34 -0
  126. package/docs/cli/g-ide/ide-opencode.md +24 -0
  127. package/docs/cli/g-ide/ide-vscode-copilot.md +24 -0
  128. package/docs/cli/g-ide/ide-windsurf.md +24 -0
  129. package/docs/configuration/config-locations.md +24 -0
  130. package/docs/configuration/configuration.md +73 -0
  131. package/docs/configuration/env-variables.md +19 -0
  132. package/docs/configuration/folders.md +55 -0
  133. package/docs/desktop-automation/electron-apps.md +217 -0
  134. package/docs/get-started/installation.md +47 -0
  135. package/docs/get-started/introduction.md +60 -0
  136. package/docs/get-started/quickstart.md +54 -0
  137. package/docs/get-started/setup-wizard.md +26 -0
  138. package/docs/intelligence/site-profiles.md +151 -0
  139. package/docs/skills-ecosystem/skills-overview.md +35 -0
  140. package/docs/skills-ecosystem/skills-recipes.md +36 -0
  141. package/docs/tools/tools.md +631 -0
  142. package/docs/troubleshooting/trouble-browser.md +27 -0
  143. package/docs/troubleshooting/trouble-config.md +20 -0
  144. package/docs/troubleshooting/trouble-loop.md +31 -0
  145. package/ext-dist/background.js +10 -62
  146. package/ext-dist/content.js +333 -921
  147. package/ext-dist/hud/hud.js +9 -9
  148. package/ext-dist/manifest.json +2 -6
  149. package/ext-dist/styles.css +6 -1
  150. package/mcp.json +108 -72
  151. package/package.json +14 -9
  152. package/server.json +3 -3
  153. package/docs/DEV_MODE.md +0 -68
  154. package/docs/dev_plugin.md +0 -310
  155. package/ext-dist/sidepanel/sidepanel.js +0 -25
  156. package/ext-dist/sidepanel.html +0 -547
package/SECURITY.md CHANGED
@@ -42,28 +42,43 @@ Weavetab interacts directly with the local Chromium browser instance via the Chr
42
42
 
43
43
  ---
44
44
 
45
- ## Blind Injection Threat Model
45
+ ## Blind Injection Threat Model & Hardening
46
46
 
47
- The **Blind Injection** architecture specifically ensures that credentials do not leak back into the LLM context or Weavetab logs.
47
+ The **Blind Injection** architecture specifically ensures that credentials do not leak back into the LLM context, Weavetab logs, DOM snapshots, visual captures, or scrape results.
48
48
 
49
49
  ### Threats Mitigated
50
50
 
51
- - **Accidental context inclusion** — LLM reading the DOM and echoing passwords
52
- - **Log scraping** — Passwords appearing in plaintext audit logs
53
- - **DOM snapshot leakage** — Secret values masked as `[REDACTED]` in all subsequent maps, deltas, and AX tree dumps
54
- - **Prompt injection domain theft** — Domain check runs before the secret value is ever resolved
51
+ - **Accidental Context Inclusion** — LLM reading the DOM, AX tree, or input discovery and echoing passwords
52
+ - **DOM & Scrape Scraping** — Passwords extracted via direct DOM access, `browser_scrape`, or `document.querySelector().value`
53
+ - **Visual Capture Leaks** — Passwords visible in screenshots, MHTML snapshots, or PDF printouts
54
+ - **Log Scraping** — Passwords appearing in plaintext action logs or audit records
55
+ - **DOM Snapshot Leakage** — Secret values masked as `[REDACTED]` in all subsequent maps, deltas, and AX tree dumps
56
+ - **Prompt Injection Domain Theft** — Domain check runs before the secret value is ever resolved from disk
57
+ - **Coercion & Serialization Leaks** — `JSON.stringify(el)`, `String(el)`, or array spreads leaking underlying element values
58
+
59
+ ### Defense-in-Depth Implementation
60
+
61
+ 1. **Storage at Rest**: Secrets in `~/.weavetab/secrets.json` are encrypted using AES-256-GCM with a machine-isolated key in `~/.weavetab-system/secrets-key`.
62
+ 2. **In-Process Resolution**: The AI model only receives and passes the key name (`envKey: "MY_KEY"`). The secret value is resolved in memory and injected directly via CDP `Input.insertText`.
63
+ 3. **DOM Property Hardening**: Injected elements are tagged `data-wt-secret="true"`, instance `.value` getters are overridden with `[REDACTED]`, and `Symbol.iterator`, `Symbol.toPrimitive`, and `toJSON` are locked.
64
+ 4. **In-Memory SessionSecretTracker**: Tracks node IDs and secret substrings per session to redact any coincidental echoes across ActionMaps, delta diffs, or scrape outputs.
65
+ 5. **Universal Tool Containment**:
66
+ - `browser_type`: Broad secret regex (`type`, `name`, `id`, `autocomplete`, `aria-label`, PINs, tokens, multi-language variants) auto-hardens target fields and suppresses raw value readbacks.
67
+ - `browser_fill`: Registers secrets in both single and `blast_mode`, redacting return values and scan discovery mode (`scan: true`).
68
+ - `browser_scrape`: Evaluates in-page extraction substituting all attributes of sensitive nodes with `[REDACTED]`.
69
+ - `browser_screenshot`, `browser_snapshot`, `browser_pdf`: Dynamically blanks/masks sensitive fields during capture and restores them in `finally` blocks.
55
70
 
56
71
  ### Threats NOT Mitigated
57
72
 
58
- - This does **not** protect against a compromised host or a malicious process reading the secrets file directly
59
- - It is not "zero-knowledge"; Weavetab must read the secret from `secrets.json` to inject it
73
+ - This does **not** protect against a compromised host or a malicious OS process with elevated privileges reading the secrets file directly.
74
+ - It is not "zero-knowledge"; Weavetab must read and decrypt the secret from `secrets.json` to inject it into Chromium.
60
75
 
61
76
  ### Secrets Resolution Order
62
77
 
63
78
  When `browser_type_secret({ envKey: 'MY_SECRET' })` is called, the `JsonSecretStore` resolves from:
64
79
 
65
- 1. `./.Weavetab/secrets.json` (project-scoped, opt-in via `Weavetab secrets init --project`)
66
- 2. `~/.Weavetab/secrets.json` (global default)
80
+ 1. `./.weavetab/secrets.json` (project-scoped, opt-in via `weavetab secrets init --project`)
81
+ 2. `~/.weavetab/secrets.json` (global default)
67
82
 
68
83
  The old `EnvSecretProvider` (`.env`-based) is **deprecated and no longer supported**. All secrets must be stored in `secrets.json`. See the [README Secrets Management section](README.md#-secrets-management) for the full CLI workflow.
69
84
 
@@ -72,14 +87,10 @@ The old `EnvSecretProvider` (`.env`-based) is **deprecated and no longer support
72
87
  ```json
73
88
  {
74
89
  "ACME_CORP_PASSWORD": {
75
- "value": "hunter2",
90
+ "value": "encrypted_aes_gcm_string",
76
91
  "domains": ["login.example.com"],
77
- "description": "Account password"
78
- },
79
- "GITHUB_2FA": {
80
- "value": "123456",
81
- "domains": ["github.com"],
82
- "description": "2FA Code"
92
+ "createdAt": "2026-08-29T10:00:00.000Z",
93
+ "updatedAt": "2026-08-29T10:00:00.000Z"
83
94
  }
84
95
  }
85
96
  ```
@@ -91,9 +102,9 @@ The old `EnvSecretProvider` (`.env`-based) is **deprecated and no longer support
91
102
  | Tool Category | admin | viewer | system |
92
103
  |---------------|-------|--------|--------|
93
104
  | All mutation tools | ✅ | ❌ | ❌ |
94
- | Read-only (map, find, scrape, console, eval, inspect, snapshot, screenshot, pdf, cookies, tabs, wait, github_*) | ✅ | ✅ | ❌ |
105
+ | Read-only (map, find, scrape, console, eval, inspect, snapshot, screenshot, pdf, cookies, tabs, wait, browser_github) | ✅ | ✅ | ❌ |
95
106
  | burst, plan, wait, map | ✅ | ❌ | ✅ |
96
- | System (logs, detect, performance, reset_loop_counter) | ✅ | ❌ | ❌ |
107
+ | System (logs, detect, performance) | ✅ | ❌ | ❌ |
97
108
 
98
109
  ### Role Definitions
99
110
 
@@ -131,7 +142,7 @@ If you discover a security vulnerability in Weavetab, please **do not** open a p
131
142
 
132
143
  Instead, report via one of the following channels:
133
144
 
134
- - **Email**: security@Weavetab.dev
145
+ - **Email**: security@weavetab.dev
135
146
  - **GitHub**: Use the private vulnerability reporting feature on the repository
136
147
 
137
148
  ### Disclosure Timeline
@@ -185,8 +196,8 @@ To receive security notifications:
185
196
 
186
197
  For security-related inquiries:
187
198
 
188
- - **Security Vulnerabilities**: security@Weavetab.dev
189
- - **Security Questions**: conduct@Weavetab.dev
199
+ - **Security Vulnerabilities**: security@weavetab.dev
200
+ - **Security Questions**: conduct@weavetab.dev
190
201
  - **GitHub Security**: https://github.com/weavetab/mcp/security
191
202
 
192
203
  ---
@@ -108,7 +108,7 @@ browser_click({ id: "w:42" })
108
108
 
109
109
  **Cause:** The warning previously relied on strict text matching.
110
110
 
111
- **Solution:** `stuck_warning` now respects the underlying `delta` computation. If the DOM changed, the warning will not fire. If you still see false positives, use `browser_reset_loop_counter` to manually reset.
111
+ **Solution:** `stuck_warning` now respects the underlying `delta` computation. If the DOM changed, the warning will not fire. If you still see false positives, use `browser_map({ force: true })` or `browser_wait({ reset_loop: true })` to manually reset.
112
112
 
113
113
  ---
114
114
 
@@ -199,7 +199,7 @@ browser_click({ id: "w:42" })
199
199
  **Cause:** Extension incompatibility or corrupted Chrome profile.
200
200
 
201
201
  **Solution:**
202
- - Try running without extension: Set `loadExtension: false` in config
202
+ - Try running without extension: Set `"extension": false` in config
203
203
  - Use a fresh Chrome profile: Set `profileDir` to a new directory
204
204
  - Check Chrome version compatibility (requires Chrome 90+)
205
205
  - Disable hardware acceleration in Chrome settings
@@ -360,6 +360,6 @@ If you continue to experience issues:
360
360
  ## Additional Resources
361
361
 
362
362
  - [Architecture Documentation](ARCHITECTURE.md)
363
- - [Tool Reference](docs/TOOLS.md)
363
+ - [Tool Reference](docs/tools/tools.md)
364
364
  - [Security Policy](SECURITY.md)
365
365
  - [Contributing Guidelines](CONTRIBUTING.md)
@@ -4,6 +4,7 @@ export interface BrowserInfo {
4
4
  name: string;
5
5
  path: string;
6
6
  }
7
+ export declare function isValidElectronExecutablePath(execPath: string): boolean;
7
8
  export declare function findBrowser(preferred?: string): BrowserInfo | null;
8
9
  export declare function findFirefox(): BrowserInfo | null;
9
10
  export declare function probeAllBrowsers(): BrowserInfo[];
@@ -14,3 +15,12 @@ export declare function launchWithBridge(config: Config): Promise<{
14
15
  engine?: BrowserEngine;
15
16
  }>;
16
17
  export declare function injectStealthScript(session: CDP.Client): Promise<void>;
18
+ export declare function getFreePort(preferredPort?: number): Promise<number>;
19
+ export declare function testPortListening(port: number): Promise<boolean>;
20
+ export declare function launchElectronApp(appName: string, config: Config): Promise<{
21
+ path: string;
22
+ name: string;
23
+ port: number;
24
+ engine: "chromium";
25
+ isElectron: true;
26
+ }>;
@@ -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, getSystemDir } 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,45 +548,53 @@ export async function launchWithBridge(config) {
524
548
  await new Promise(r => setTimeout(r, 1500));
525
549
  }
526
550
  try {
527
- let extPath = path.resolve(__dirname, "../../ext-dist");
528
- const systemExtPath = getSystemDir("extension");
529
- if (fs.existsSync(extPath)) {
530
- try {
531
- fs.cpSync(extPath, systemExtPath, { recursive: true, force: true });
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
- catch (e) {
534
- logDebug(`[Weavetab] Failed to sync extension to system directory: ${e}`);
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
- else {
538
- if (fs.existsSync(systemExtPath)) {
539
- logDebug(`[Weavetab] Local extension missing. Falling back to system extension directory.`);
540
- extPath = systemExtPath;
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
- else {
543
- throw new Error(`[Weavetab] Extension directory does not exist: ${extPath}. Build the extension first.`);
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
- const manifestPath = path.join(extPath, "manifest.json");
547
- const contentPath = path.join(extPath, "content.js");
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
- // Wipe the MV3 service worker cache before every launch so Chrome always
588
- // executes the latest background.js from disk rather than a stale cached
589
- // script stored in the profile. Without this, edits to ext-dist/background.js
590
- // have no effect until Chrome manually reloads the extension — the SW cache
591
- // (Default/Service Worker/ScriptCache + Default/Service Worker/Database) is
592
- // keyed by URL hash and persists across sessions in the user-data-dir.
593
- // This ensures any future background.js change takes effect immediately
594
- // on all existing profiles without manual cache clearing.
595
- try {
596
- const swBase = path.join(profileDir, "Default", "Service Worker");
597
- const swScriptCache = path.join(swBase, "ScriptCache");
598
- const swDatabase = path.join(swBase, "Database");
599
- if (fs.existsSync(swScriptCache)) {
600
- fs.rmSync(swScriptCache, { recursive: true, force: true });
601
- 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
+ }
602
635
  }
603
- if (fs.existsSync(swDatabase)) {
604
- fs.rmSync(swDatabase, { recursive: true, force: true });
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
- try {
693
- await new Promise(r => setTimeout(r, 1500));
694
- const testSession = await withCDPTimeout(CDP({ host: "127.0.0.1", port: actualPort }), 4000, "CDP connect");
695
- const { targetInfos } = await withCDPTimeout(testSession.Target.getTargets(), 4000, "getTargets");
696
- const extensionTargets = targetInfos.filter(t => t.type === "service_worker" || (t.type === "page" && t.url.startsWith("chrome-extension://")));
697
- await testSession.close();
698
- if (extensionTargets.length > 0) {
699
- 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
+ }
700
731
  }
701
- else {
702
- 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}`);
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,182 @@ 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 { resolveElectronExecutablePath } = await import("../desktop/discovery.js");
944
+ const execPath = resolveElectronExecutablePath(appName, appCfg.path) || appCfg.path;
945
+ if (!execPath || !isValidElectronExecutablePath(execPath)) {
946
+ throw new Error(`[Weavetab] ✗ Invalid or non-existent Electron executable path for "${appName}": ${execPath}`);
947
+ }
948
+ // Check app-scoped lock
949
+ const lock = readLock(appName);
950
+ if (lock && lock.mode === "session") {
951
+ if (isLockPending(lock)) {
952
+ logDebug(`[Weavetab] Launch already in progress for ${appName} (PID: ${lock.pid}). Waiting...`);
953
+ throw new Error(`Launch already in progress for ${appName}. Wait for it to complete.`);
954
+ }
955
+ const valid = await isLockValid(lock);
956
+ if (valid) {
957
+ logDebug(`[Weavetab] [OK] Reusing valid Electron lock for ${appName} (PID: ${lock.pid}, Port: ${lock.port})`);
958
+ return { path: execPath, name: appName, port: lock.port, engine: "chromium", isElectron: true };
959
+ }
960
+ logDebug(`[Weavetab] [!] Stale lock detected for ${appName}. Clearing.`);
961
+ clearLock(appName);
962
+ }
963
+ else if (lock) {
964
+ clearLock(appName);
965
+ }
966
+ const profileDir = getElectronProfile(appName, appCfg.profileDir);
967
+ const actualPort = appCfg.port && appCfg.port > 0 ? appCfg.port : await getFreePort(9222);
968
+ // Clean up SingletonLock
969
+ const singletonLock = path.join(profileDir, "SingletonLock");
970
+ const singletonCookie = path.join(profileDir, "SingletonCookie");
971
+ try {
972
+ if (fs.existsSync(singletonLock))
973
+ fs.unlinkSync(singletonLock);
974
+ }
975
+ catch { /* non-fatal */ }
976
+ try {
977
+ if (fs.existsSync(singletonCookie))
978
+ fs.unlinkSync(singletonCookie);
979
+ }
980
+ catch { /* non-fatal */ }
981
+ const platform = os.platform();
982
+ const normalizedProfileDir = platform === 'win32'
983
+ ? profileDir.replace(/\//g, '\\')
984
+ : profileDir;
985
+ const resolvedDownloadDir = getDownloadDir(config) && getDownloadDir(config).trim()
986
+ ? getDownloadDir(config).trim()
987
+ : getOutputDir("downloads", config);
988
+ const normalizedDownloadDir = platform === 'win32'
989
+ ? resolvedDownloadDir.replace(/\//g, '\\')
990
+ : resolvedDownloadDir;
991
+ // Notice: Clean Electron launch flags: only isolated user-data-dir, remote-debugging-port, and optional user args.
992
+ // We NEVER pass Chromium browser flags or extension flags to Electron binaries.
993
+ const args = [
994
+ `--remote-debugging-port=${actualPort}`,
995
+ `--user-data-dir=${normalizedProfileDir}`,
996
+ `--default-download-dir=${normalizedDownloadDir}`,
997
+ "--no-first-run",
998
+ "--no-default-browser-check",
999
+ ...(appCfg.args || []),
1000
+ ];
1001
+ const spawnOptions = {
1002
+ detached: true,
1003
+ stdio: "ignore",
1004
+ };
1005
+ if (platform === 'darwin' && execPath.includes('.app')) {
1006
+ spawnOptions.shell = true;
1007
+ }
1008
+ const proc = spawn(execPath, args, spawnOptions);
1009
+ proc.unref();
1010
+ const pid = proc.pid;
1011
+ logDebug(`[Weavetab] Launching Electron app ${appName} (PID: ${pid}, port: ${actualPort}).`);
1012
+ // Poll CDP endpoint on actualPort
1013
+ let confirmed = false;
1014
+ for (let i = 0; i < 20; i++) {
1015
+ try {
1016
+ await withCDPTimeout(CDP.List({ host: "127.0.0.1", port: actualPort }), 1500, "CDP.List Electron");
1017
+ confirmed = true;
1018
+ break;
1019
+ }
1020
+ catch {
1021
+ await new Promise(r => setTimeout(r, 600));
1022
+ }
1023
+ }
1024
+ if (!confirmed) {
1025
+ const isListening = await testPortListening(actualPort);
1026
+ if (!isListening) {
1027
+ try {
1028
+ if (pid)
1029
+ process.kill(pid);
1030
+ }
1031
+ catch { /* ignore */ }
1032
+ clearLock(appName);
1033
+ throw new Error(`[Weavetab] Electron app ${appName} failed to start CDP listener on port ${actualPort} after 12s.`);
1034
+ }
1035
+ }
1036
+ await writeLock({
1037
+ pid,
1038
+ port: actualPort,
1039
+ profileDir,
1040
+ browserPath: execPath,
1041
+ mode: "session",
1042
+ appName,
1043
+ pending: false,
1044
+ timestamp: Date.now(),
1045
+ }, appName);
1046
+ return { path: execPath, name: appName, port: actualPort, engine: "chromium", isElectron: true };
1047
+ }
@@ -17,12 +17,16 @@ 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;
24
+ /** Port the CDP session is connected to */
25
+ port?: number;
22
26
  }
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>;
27
+ export declare function getOrConnect(config: Config, targetApp?: string): Promise<BrowserClient>;
28
+ export declare function getTabList(agentId?: string, targetApp?: string): Promise<any[]>;
29
+ export declare function connectToTab(targetId: string, port?: number, agentId?: string, browserContextId?: string, appName?: string): Promise<BrowserClient>;
26
30
  /**
27
31
  * Cancel analytics intervals and close the CDP WebSocket sessions without
28
32
  * touching the browser process. Safe to call in server mode — the browser