@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
@@ -7,9 +7,9 @@ import { logDebug } from "../audit/logger.js";
7
7
  */
8
8
  import CDP from "chrome-remote-interface";
9
9
  import { blockStorageAccess } from "../security/guard.js";
10
- import { launchWithBridge, injectStealthScript } from "./bridge.js";
10
+ import { launchWithBridge, launchElectronApp, injectStealthScript } from "./bridge.js";
11
11
  import { setupOverlay, ensureVisualLayer } from "../overlay/injector.js";
12
- import { onConfigChange, saveConfig, loadConfigSync, getCachedConfig, getConfigAgentId, getConfigCookies, getConfigProfileDir, getConfigMode } from "../config/loader.js";
12
+ import { onConfigChange, saveConfig, loadConfigSync, getCachedConfig, getConfigAgentId, getConfigCookies, getConfigProfileDir, getConfigMode, getElectronAppConfig, getConfigExtension } from "../config/loader.js";
13
13
  import { sessionRegistry } from "../state/session.js";
14
14
  import { initConsoleBuffering } from "../tools/console.js";
15
15
  import { registerMutationListener, clearTabCache } from "../state/cache.js";
@@ -17,6 +17,7 @@ import { recordTabActivity } from "../tools/tabs.js";
17
17
  import { initDialogHandler } from "../tools/dialog.js";
18
18
  import { WT_REGISTRY_INIT_EXPR } from "./walker.js";
19
19
  import { urcInvalidate } from "./ref_cache.js";
20
+ import { marksInvalidate } from "../intelligence/marks.js";
20
21
  import { readLock, clearLock, isLockValid, isLockPending, acquireMutex } from "./lock.js";
21
22
  import { getSessionProfile } from "./profiles.js";
22
23
  import { safelyKillWeavetabProcesses, verifyProfileIsolation, verifyBrowserFingerprint, clearProfileVerificationCache } from "./security.js";
@@ -95,12 +96,20 @@ async function waitForPageReady(session) {
95
96
  });
96
97
  }
97
98
  // ─── Session init (overlay + re-injection) ───────────────────────────────────
98
- async function initSession(session, targetId, port, config) {
99
+ async function initSession(session, targetId, port, config, isElectron) {
99
100
  initDialogHandler(session);
100
- // Initialize Omni-Context Broker for iframes
101
101
  const { initOmniBroker } = await import("./omni_broker.js");
102
102
  await initOmniBroker(session, targetId, port);
103
- if (activeEngine === "chromium") {
103
+ if (isElectron) {
104
+ // Electron desktop app initialization
105
+ try {
106
+ await session.Page.addScriptToEvaluateOnNewDocument({ source: WT_REGISTRY_INIT_EXPR });
107
+ }
108
+ catch { /* non-fatal */ }
109
+ // Direct DOM minimal overlay injection without Chrome Extension
110
+ await ensureVisualLayer(session, config);
111
+ }
112
+ else if (activeEngine === "chromium") {
104
113
  // Only block cookies/storage when cookies mode is disabled
105
114
  if (!getConfigCookies(config)) {
106
115
  await blockStorageAccess(session);
@@ -142,7 +151,8 @@ async function initSession(session, targetId, port, config) {
142
151
  clearTabCache(targetId);
143
152
  // Invalidate URC on navigation — elements from the old page are stale
144
153
  urcInvalidate(targetId);
145
- if (activeEngine === "chromium") {
154
+ marksInvalidate(targetId);
155
+ if (isElectron || activeEngine === "chromium") {
146
156
  try {
147
157
  await waitForPageReady(session);
148
158
  const cfg = getCachedConfig();
@@ -154,8 +164,8 @@ async function initSession(session, targetId, port, config) {
154
164
  catch { /* ignore */ }
155
165
  }
156
166
  });
157
- // Diagnostic: verify extension content script loaded
158
- if (activeEngine === "chromium") {
167
+ // Diagnostic: verify extension content script loaded (Standard browser only)
168
+ if (!isElectron && activeEngine === "chromium" && getConfigExtension(config) !== false) {
159
169
  try {
160
170
  // Check for multiple indicators that extension is loaded
161
171
  const { result: extCheck } = await session.Runtime.evaluate({
@@ -174,29 +184,12 @@ async function initSession(session, targetId, port, config) {
174
184
  else {
175
185
  logDebug(`[Weavetab] [!] Extension content script NOT detected — HUD/overlay may not work`);
176
186
  logDebug(`[Weavetab] [!] Try reloading page or check if extension loaded in browser://extensions`);
177
- // Try to inject a basic error indicator into the page
178
- try {
179
- await session.Runtime.evaluate({
180
- expression: `
181
- (function() {
182
- const div = document.createElement('div');
183
- div.style.cssText = 'position:fixed;top:10px;right:10px;background:red;color:white;padding:10px;z-index:999999;font-family:sans-serif;font-size:14px;';
184
- div.textContent = 'WeaveTab Extension Not Loaded';
185
- document.body.appendChild(div);
186
- setTimeout(() => div.remove(), 5000);
187
- })()
188
- `
189
- });
190
- }
191
- catch { /* ignore */ }
192
187
  }
193
188
  }
194
189
  catch (checkErr) {
195
190
  logDebug(`[Weavetab] [!] Extension health check failed: ${checkErr}`);
196
191
  }
197
- }
198
- // Push config + initial idle state to extension right away (Chrome only)
199
- if (activeEngine === "chromium") {
192
+ // Push config + initial idle state to extension right away (Chrome only)
200
193
  const { sendToExtension } = await import("../cdp/helpers.js");
201
194
  sendToExtension(session, {
202
195
  type: 'Weavetab_CONFIG',
@@ -207,8 +200,7 @@ async function initSession(session, targetId, port, config) {
207
200
  state: 'idle',
208
201
  }).catch(() => { });
209
202
  }
210
- // Analytics ping every 5s — stored per-tab so each interval can be
211
- // independently cancelled when the tab closes or cleanupAll() is called.
203
+ // Analytics ping every 30s
212
204
  const sessionStartTime = Date.now();
213
205
  const analyticsInterval = setInterval(async () => {
214
206
  try {
@@ -225,34 +217,138 @@ async function initSession(session, targetId, port, config) {
225
217
  }).catch(() => { });
226
218
  }
227
219
  catch { /* ignore */ }
228
- }, 30_000); // 30s interval — reduced from 5s to cut idle CDP overhead (6 round-trips/min → 1)
229
- // Register in the per-tab map so cleanupAll() and the session-close handler can
230
- // cancel it without relying on a single overwriteable global slot.
220
+ }, 30_000);
231
221
  analyticsIntervals.set(targetId, analyticsInterval);
232
- // unref() so this polling interval does not pin the Node.js event loop.
233
- // An orphaned MCP process (IDE disconnected, browser closed) would otherwise
234
- // stay alive indefinitely due to this 30s interval firing forever.
235
222
  analyticsInterval.unref();
236
223
  }
237
224
  // ─── Main connection entry point ──────────────────────────────────────────────
238
- export async function getOrConnect(config) {
239
- if (getConfigMode(config) === "attach") {
240
- throw new Error("[!] Attach mode (connect to your real browser) coming soon. Use session mode for now.");
225
+ export async function getOrConnect(config, targetApp) {
226
+ if (getConfigMode(config) === "attach" && !targetApp) {
227
+ // track as limitation - use session mode
228
+ const err = new Error("[!] Attach mode is not yet supported. Use session mode (default) instead.");
229
+ err.code = "NOT_IMPLEMENTED";
230
+ throw err;
241
231
  }
242
- const activeClient = sessionRegistry.getActive(getConfigAgentId(config));
232
+ // 1. Check existing active session for this agent & app
233
+ const activeClient = sessionRegistry.getActive(getConfigAgentId(config), targetApp);
243
234
  if (activeClient) {
244
235
  try {
245
236
  await activeClient.session.Browser.getVersion();
246
237
  return activeClient;
247
238
  }
248
239
  catch {
249
- logDebug("[Weavetab CDP] Connection lost. Reconnecting...");
240
+ logDebug(`[Weavetab CDP] Connection lost for ${targetApp || "browser"}. Reconnecting...`);
250
241
  clearProfileVerificationCache(activeClient.targetId);
251
242
  sessionRegistry.unregister(activeClient.targetId);
252
243
  }
253
244
  }
254
- const releaseMutex = await acquireMutex();
245
+ const releaseMutex = await acquireMutex(targetApp);
255
246
  try {
247
+ if (targetApp) {
248
+ // ──────────────── ELECTRON APP CONNECTION FLOW ────────────────
249
+ const appCfg = getElectronAppConfig(targetApp, config);
250
+ if (!appCfg) {
251
+ throw new Error(`[Weavetab] ✗ Electron app "${targetApp}" is not configured in config.json.\n` +
252
+ ` Please add an entry under "electron": { "${targetApp}": "C:\\\\path\\\\to\\\\app.exe" } in your config.`);
253
+ }
254
+ let port = null;
255
+ const sessionLock = readLock(targetApp);
256
+ if (sessionLock && sessionLock.mode === "session") {
257
+ if (isLockPending(sessionLock)) {
258
+ logDebug(`[Weavetab] Launch in progress for ${targetApp} by PID ${sessionLock.pid}. Waiting up to 5s...`);
259
+ for (let i = 0; i < 5; i++) {
260
+ await new Promise(r => setTimeout(r, 1000));
261
+ const updatedLock = readLock(targetApp);
262
+ if (!updatedLock || !isLockPending(updatedLock))
263
+ break;
264
+ }
265
+ }
266
+ const freshLock = readLock(targetApp);
267
+ if (freshLock && (await isLockValid(freshLock))) {
268
+ port = freshLock.port;
269
+ }
270
+ else {
271
+ clearLock(targetApp);
272
+ }
273
+ }
274
+ let targets = [];
275
+ if (port) {
276
+ try {
277
+ targets = await withCDPTimeout(CDP.List({ host: "127.0.0.1", port }), 3000, "CDP.List existing Electron");
278
+ }
279
+ catch {
280
+ port = null;
281
+ }
282
+ }
283
+ if (!port || targets.length === 0) {
284
+ logDebug(`[Weavetab CDP] Launching Electron app ${targetApp}...`);
285
+ const launchResult = await launchElectronApp(targetApp, config);
286
+ port = launchResult.port;
287
+ // Poll for target window availability (Electron may take up to 2-3s to render initial window)
288
+ for (let retry = 0; retry < 10; retry++) {
289
+ try {
290
+ targets = await withCDPTimeout(CDP.List({ host: "127.0.0.1", port }), 2000, "CDP.List Electron retry");
291
+ if (targets.length > 0)
292
+ break;
293
+ }
294
+ catch { /* wait */ }
295
+ await new Promise(r => setTimeout(r, 500));
296
+ }
297
+ }
298
+ let pageTarget = targets.find(t => t.type === "page" || t.type === "webview" || t.type === "app") ?? targets[0];
299
+ if (!pageTarget) {
300
+ throw new Error(`[Weavetab] ✗ No window or webview target found in Electron app "${targetApp}".`);
301
+ }
302
+ const targetId = pageTarget.id || pageTarget.targetId;
303
+ const session = await withCDPTimeout(CDP({ host: "127.0.0.1", port, target: targetId }), 5000, "CDP connect Electron target");
304
+ session.on("close", () => {
305
+ logDebug(`[Weavetab CDP] Electron session WebSocket closed (${targetApp}) — unregistering.`);
306
+ const iv = analyticsIntervals.get(targetId);
307
+ if (iv) {
308
+ clearInterval(iv);
309
+ analyticsIntervals.delete(targetId);
310
+ }
311
+ clearProfileVerificationCache(targetId);
312
+ import("./omni_broker.js").then(m => m.cleanupOmniBroker(targetId)).catch(() => { });
313
+ import("../tools/network_intercept.js").then(m => m.cleanupNetworkIntercept(session)).catch(() => { });
314
+ sessionRegistry.unregister(targetId);
315
+ });
316
+ session.on("error", (err) => {
317
+ logDebug(`[Weavetab CDP] Electron session WebSocket error (${targetApp}):`, err?.message ?? err);
318
+ });
319
+ // Validate connection health
320
+ try {
321
+ await session.Browser.getVersion();
322
+ }
323
+ catch {
324
+ throw new Error("Failed health check: CDP session connected to Electron app but getVersion failed.");
325
+ }
326
+ await Promise.all([
327
+ session.Accessibility.enable(),
328
+ session.DOM.enable(),
329
+ session.Page.enable(),
330
+ session.Runtime.enable(),
331
+ session.Network.enable().catch(() => { }),
332
+ session.Performance.enable().catch(() => { })
333
+ ]);
334
+ await wireUiConfigBinding(session);
335
+ await initSession(session, targetId, port, config, true);
336
+ await session.Target.setDiscoverTargets({ discover: true }).catch(() => { });
337
+ logDebug(`[Weavetab CDP] [OK] Active Electron target: ${pageTarget.title || pageTarget.url || targetApp}`);
338
+ const client = {
339
+ session,
340
+ targetId,
341
+ url: pageTarget.url || "",
342
+ title: pageTarget.title || targetApp,
343
+ overlayInstalled: false,
344
+ agentId: getConfigAgentId(config),
345
+ appName: targetApp,
346
+ isElectron: true,
347
+ };
348
+ sessionRegistry.register(targetId, client);
349
+ return client;
350
+ }
351
+ // ──────────────── STANDARD WEB BROWSER FLOW ────────────────
256
352
  const sessionLock = readLock();
257
353
  if (sessionLock && sessionLock.mode === "session") {
258
354
  if (isLockPending(sessionLock)) {
@@ -413,7 +509,7 @@ export async function getOrConnect(config) {
413
509
  // Continue anyway for now - in production this should be a hard failure
414
510
  }
415
511
  else {
416
- logDebug(`[Weavetab Security] Browser fingerprint verified successfully`);
512
+ logDebug(`[Weavetab Security] Browser fingerprint verified`);
417
513
  }
418
514
  }
419
515
  // NOTE: initSession() already installs the frameNavigated handler (overlay
@@ -429,7 +525,7 @@ export async function getOrConnect(config) {
429
525
  // Continue anyway for now - in production this should be a hard failure
430
526
  }
431
527
  else {
432
- logDebug(`[Weavetab Security] Profile isolation verified successfully`);
528
+ logDebug(`[Weavetab Security] Profile isolation`);
433
529
  }
434
530
  await session.Target.setDiscoverTargets({ discover: true });
435
531
  session.on("Target.targetActivated", (event) => {
@@ -437,7 +533,7 @@ export async function getOrConnect(config) {
437
533
  if (activatedId) {
438
534
  const client = sessionRegistry.getAll().find(c => c.targetId === activatedId);
439
535
  if (client) {
440
- sessionRegistry.setActive(activatedId, client.agentId);
536
+ sessionRegistry.setActive(activatedId, client.agentId, client.appName);
441
537
  }
442
538
  }
443
539
  });
@@ -491,28 +587,34 @@ export async function getOrConnect(config) {
491
587
  await releaseMutex();
492
588
  }
493
589
  }
494
- export async function getTabList(agentId) {
590
+ export async function getTabList(agentId, targetApp) {
495
591
  const config = getCachedConfig();
496
- // Validate the lock before blindly connecting (same two-factor check that
497
- // getOrConnect() performs). A stale lock after a server restart would otherwise
498
- // produce raw ECONNREFUSED.
499
- let port = actualPort;
500
- if (!port) {
501
- const lock = readLock();
502
- if (lock && lock.mode === "session") {
503
- const valid = await isLockValid(lock);
504
- if (valid) {
505
- port = lock.port;
506
- }
507
- else {
508
- logDebug("[Weavetab CDP] getTabList: stale lock detected — clearing.");
509
- clearLock();
510
- port = null;
592
+ let port = null;
593
+ if (targetApp) {
594
+ const lock = readLock(targetApp);
595
+ if (lock && (await isLockValid(lock))) {
596
+ port = lock.port;
597
+ }
598
+ }
599
+ else {
600
+ port = actualPort;
601
+ if (!port) {
602
+ const lock = readLock();
603
+ if (lock && lock.mode === "session") {
604
+ const valid = await isLockValid(lock);
605
+ if (valid) {
606
+ port = lock.port;
607
+ }
608
+ else {
609
+ logDebug("[Weavetab CDP] getTabList: stale lock detected — clearing.");
610
+ clearLock();
611
+ port = null;
612
+ }
511
613
  }
512
614
  }
513
615
  }
514
616
  if (!port)
515
- throw new Error("[!] No active browser session. Run browser_go first.");
617
+ throw new Error(`[!] No active ${targetApp ? targetApp : "browser"} session. Run browser_go or launch app first.`);
516
618
  try {
517
619
  const metaSession = await withCDPTimeout(CDP({ host: "127.0.0.1", port }), 5000, "getTabList CDP");
518
620
  const { targetInfos } = await withCDPTimeout(metaSession.Target.getTargets(), 5000, "getTabList getTargets");
@@ -520,21 +622,21 @@ export async function getTabList(agentId) {
520
622
  const aid = agentId || getConfigAgentId(config);
521
623
  const bctxId = sessionRegistry.getBrowserContextIdForAgent(aid);
522
624
  return targetInfos
523
- .filter(t => t.type === "page" && (!bctxId || t.browserContextId === bctxId))
625
+ .filter(t => (t.type === "page" || t.type === "webview" || t.type === "app" || t.type === "other" || t.type === "iframe") && (!bctxId || t.browserContextId === bctxId))
524
626
  .map(t => ({
525
627
  id: t.targetId,
526
- title: t.title,
628
+ title: t.title || t.url || t.type,
527
629
  url: t.url,
528
630
  type: t.type,
529
631
  }));
530
632
  }
531
633
  catch {
532
- throw new Error(`[!] Weavetab cannot reach browser on port ${port}.`);
634
+ throw new Error(`[!] Weavetab cannot reach ${targetApp ? targetApp : "browser"} on port ${port}.`);
533
635
  }
534
636
  }
535
637
  // Only allow the UI to update cosmetic/dev settings via the binding.
536
638
  // Never allow overwriting security-critical fields (role, browserPath, block, allow, profileDir, etc.).
537
- const UI_ALLOWED_FIELDS = new Set(["devMode", "visuals", "darkMode"]);
639
+ const UI_ALLOWED_FIELDS = new Set(["devMode", "extension"]);
538
640
  async function wireUiConfigBinding(session) {
539
641
  try {
540
642
  await session.Runtime.addBinding({ name: "__wt_save_ui_config" });
@@ -562,21 +664,38 @@ async function wireUiConfigBinding(session) {
562
664
  }
563
665
  catch { /* already bound */ }
564
666
  }
565
- export async function connectToTab(targetId, port, agentId, browserContextId) {
667
+ export async function connectToTab(targetId, port, agentId, browserContextId, appName) {
566
668
  const existing = sessionRegistry.getAll().find(c => c.targetId === targetId);
567
669
  if (existing)
568
670
  return existing;
569
- const lock = readLock();
570
- const p = port || actualPort || lock?.port;
671
+ const lock = readLock(appName);
672
+ const p = port || (appName ? lock?.port : (actualPort || lock?.port));
571
673
  if (!p)
572
- throw new Error("[!] No active browser session. Run browser_go first.");
573
- const targets = await withCDPTimeout(CDP.List({ host: "127.0.0.1", port: p }), 5000, "connectToTab CDP.List");
574
- const t = targets.find(t => t.id === targetId);
575
- if (!t)
576
- throw new Error("Tab not found");
674
+ throw new Error(`[!] No active ${appName || "browser"} session. Run browser_go first.`);
675
+ let t = null;
676
+ try {
677
+ const targets = await withCDPTimeout(CDP.List({ host: "127.0.0.1", port: p }), 3000, "connectToTab CDP.List");
678
+ t = targets.find((item) => (item.id || item.targetId) === targetId);
679
+ }
680
+ catch { }
681
+ if (!t) {
682
+ try {
683
+ const metaSession = await withCDPTimeout(CDP({ host: "127.0.0.1", port: p }), 3000, "connectToTab meta CDP");
684
+ const { targetInfos } = await withCDPTimeout(metaSession.Target.getTargets(), 3000, "connectToTab meta getTargets");
685
+ await metaSession.close();
686
+ const ti = targetInfos.find(item => item.targetId === targetId);
687
+ if (ti) {
688
+ t = { id: ti.targetId, targetId: ti.targetId, url: ti.url, title: ti.title, type: ti.type };
689
+ }
690
+ }
691
+ catch { }
692
+ }
693
+ if (!t) {
694
+ t = { id: targetId, targetId, url: "", title: appName || "Tab", type: "page" };
695
+ }
577
696
  const session = await withCDPTimeout(CDP({ host: "127.0.0.1", port: p, target: targetId }), 5000, "connectToTab CDP");
578
697
  session.on("close", () => {
579
- logDebug("[Weavetab CDP] connectToTab session closed — unregistering.");
698
+ logDebug(`[Weavetab CDP] connectToTab session closed (${appName || "browser"}) — unregistering.`);
580
699
  // Cancel this tab's analytics interval so it doesn't outlive the session
581
700
  const iv = analyticsIntervals.get(targetId);
582
701
  if (iv) {
@@ -589,7 +708,7 @@ export async function connectToTab(targetId, port, agentId, browserContextId) {
589
708
  sessionRegistry.unregister(targetId);
590
709
  });
591
710
  session.on("error", (err) => {
592
- logDebug("[Weavetab CDP] connectToTab session error:", err?.message ?? err);
711
+ logDebug(`[Weavetab CDP] connectToTab session error (${appName || "browser"}):`, err?.message ?? err);
593
712
  });
594
713
  await Promise.all([
595
714
  session.Accessibility.enable(),
@@ -601,7 +720,7 @@ export async function connectToTab(targetId, port, agentId, browserContextId) {
601
720
  ]);
602
721
  await wireUiConfigBinding(session);
603
722
  const currentConfig = getCachedConfig();
604
- await initSession(session, targetId, p, currentConfig);
723
+ await initSession(session, targetId, p, currentConfig, !!appName);
605
724
  // NOTE: initSession() already installs a frameNavigated handler that handles
606
725
  // clearSnapshot, clearViewportCache, and ensureVisualLayer. Do NOT add a second
607
726
  // one here — it would double the CDP work on every navigation.
@@ -614,6 +733,8 @@ export async function connectToTab(targetId, port, agentId, browserContextId) {
614
733
  overlayInstalled: false,
615
734
  agentId: actualAgentId,
616
735
  browserContextId,
736
+ appName,
737
+ isElectron: !!appName,
617
738
  };
618
739
  sessionRegistry.register(targetId, client);
619
740
  return client;
@@ -9,3 +9,7 @@ export declare function sendToExtension(session: CDP.Client, message: object): P
9
9
  export declare function getFromExtension<T = any>(session: CDP.Client, message: object, retries?: number): Promise<T | null>;
10
10
  export declare function hideWeaveTabOverlay(session: CDP.Client): Promise<void>;
11
11
  export declare function restoreWeaveTabOverlay(session: CDP.Client): Promise<void>;
12
+ export declare function applySecretHardening(session: CDP.Client, objectId?: string, selector?: string): Promise<void>;
13
+ export declare function registerAndHardenSecretField(session: CDP.Client, targetId: string, selector: string, secretValue?: string, objectId?: string): Promise<void>;
14
+ export declare function maskSecretFieldsForCapture(session: CDP.Client): Promise<void>;
15
+ export declare function restoreSecretFieldsAfterCapture(session: CDP.Client): Promise<void>;
@@ -1,4 +1,5 @@
1
1
  import { logDebug } from "../audit/logger.js";
2
+ import { SessionSecretTracker } from "../security/secrets.js";
2
3
  const MAX_QUEUE_SIZE = 50;
3
4
  const BATCH_INTERVAL_MS = 10; // Reduced from 20ms for faster message delivery
4
5
  const messageQueues = new Map();
@@ -191,3 +192,183 @@ export async function restoreWeaveTabOverlay(session) {
191
192
  returnByValue: false,
192
193
  }).catch(() => { });
193
194
  }
195
+ // ─── Secret Hardening & Containment Helpers ──────────────────────────────────
196
+ export async function applySecretHardening(session, objectId, selector) {
197
+ const hardeningScript = `function() {
198
+ try {
199
+ const el = this;
200
+ if (!el) return;
201
+ // Mark as secret
202
+ try { el.setAttribute('data-wt-secret', 'true'); } catch (_) {}
203
+
204
+ // 1. Redact direct .value access via instance-level property override
205
+ const proto = Object.getPrototypeOf(el);
206
+ const origDesc = proto ? Object.getOwnPropertyDescriptor(proto, 'value') : null;
207
+ if (origDesc) {
208
+ Object.defineProperty(el, 'value', {
209
+ get: function() { return '[REDACTED]'; },
210
+ set: origDesc.set,
211
+ configurable: true,
212
+ enumerable: true
213
+ });
214
+ }
215
+
216
+ // 2. Defeat array spread, iteration, and coercion
217
+ try {
218
+ Object.defineProperty(el, Symbol.iterator, {
219
+ value: function* () { yield '[REDACTED]'; },
220
+ configurable: true
221
+ });
222
+ Object.defineProperty(el, Symbol.toPrimitive, {
223
+ value: function() { return '[REDACTED]'; },
224
+ configurable: true
225
+ });
226
+ // Defeat JSON.stringify / structuredClone leaks
227
+ Object.defineProperty(el, 'toJSON', {
228
+ value: function() { return '[REDACTED]'; },
229
+ configurable: true
230
+ });
231
+ } catch(_) {}
232
+ } catch (e) {
233
+ // fail silently so as not to disrupt the page execution
234
+ }
235
+ }`;
236
+ if (objectId) {
237
+ await session.Runtime.callFunctionOn({
238
+ functionDeclaration: hardeningScript,
239
+ objectId,
240
+ silent: true
241
+ }).catch(() => { });
242
+ }
243
+ else if (selector) {
244
+ await session.Runtime.evaluate({
245
+ expression: `(function(sel) {
246
+ const el = document.querySelector(sel) || (window.__Weavetab_shadowQuery ? window.__Weavetab_shadowQuery(sel) : null);
247
+ if (el) {
248
+ (${hardeningScript}).call(el);
249
+ }
250
+ })(${JSON.stringify(selector)})`,
251
+ returnByValue: false
252
+ }).catch(() => { });
253
+ }
254
+ }
255
+ export async function registerAndHardenSecretField(session, targetId, selector, secretValue, objectId) {
256
+ try {
257
+ if (secretValue) {
258
+ SessionSecretTracker.registerSecretValue(targetId, secretValue);
259
+ }
260
+ let resolvedObjectId = objectId;
261
+ if (!resolvedObjectId && selector) {
262
+ const nodeInfo = await session.Runtime.evaluate({
263
+ expression: `(function(sel) {
264
+ return document.querySelector(sel) || (window.__Weavetab_shadowQuery ? window.__Weavetab_shadowQuery(sel) : null);
265
+ })(${JSON.stringify(selector)})`,
266
+ returnByValue: false
267
+ }).catch(() => null);
268
+ resolvedObjectId = nodeInfo?.result?.objectId;
269
+ }
270
+ if (resolvedObjectId) {
271
+ try {
272
+ const nodeDetails = await session.DOM.requestNode({ objectId: resolvedObjectId });
273
+ if (nodeDetails?.nodeId) {
274
+ const desc = await session.DOM.describeNode({ nodeId: nodeDetails.nodeId });
275
+ if (desc?.node?.backendNodeId) {
276
+ SessionSecretTracker.markNodeAsSecret(targetId, desc.node.backendNodeId);
277
+ }
278
+ }
279
+ }
280
+ catch { /* DOM lookup best-effort */ }
281
+ }
282
+ await applySecretHardening(session, resolvedObjectId, selector);
283
+ }
284
+ catch (err) {
285
+ logDebug(`[Weavetab] registerAndHardenSecretField failed: ${err}`);
286
+ }
287
+ }
288
+ export async function maskSecretFieldsForCapture(session) {
289
+ await session.Runtime.evaluate({
290
+ expression: `(function() {
291
+ var secretRegex = /password|passcode|pin|passphrase|secret|token|auth.?key|access.?code|mot.?de.?passe|contrase[nñ]a|kennwort|senha/i;
292
+
293
+ function queryAllDeep(root, matchFn) {
294
+ var matched = [];
295
+ var elements = root.querySelectorAll('*');
296
+ for (var i = 0; i < elements.length; i++) {
297
+ var el = elements[i];
298
+ if (matchFn(el)) matched.push(el);
299
+ if (el.shadowRoot) {
300
+ matched = matched.concat(queryAllDeep(el.shadowRoot, matchFn));
301
+ }
302
+ }
303
+ return matched;
304
+ }
305
+
306
+ function isSecretNode(el) {
307
+ if (!el || !el.tagName) return false;
308
+ if (el.hasAttribute && el.hasAttribute('data-wt-secret')) return true;
309
+ var tag = el.tagName.toLowerCase();
310
+ if (tag === 'input' || tag === 'textarea') {
311
+ var t = (el.getAttribute('type') || '').toLowerCase();
312
+ var ac = (el.getAttribute('autocomplete') || '').toLowerCase();
313
+ var name = (el.getAttribute('name') || '').toLowerCase();
314
+ var id = (el.getAttribute('id') || '').toLowerCase();
315
+ var aria = (el.getAttribute('aria-label') || '').toLowerCase();
316
+ return t === 'password' || ac.includes('password') || secretRegex.test(name) || secretRegex.test(id) || secretRegex.test(aria);
317
+ }
318
+ return false;
319
+ }
320
+
321
+ var els = queryAllDeep(document, isSecretNode);
322
+ for (var i = 0; i < els.length; i++) {
323
+ var el = els[i];
324
+ el.__wt_cap_val_backup = el.value;
325
+ try { el.value = ''; } catch(e) {}
326
+ if (el.isContentEditable) {
327
+ el.__wt_cap_txt_backup = el.textContent;
328
+ try { el.textContent = ''; } catch(e) {}
329
+ }
330
+ el.__wt_cap_sec_backup = el.style.webkitTextSecurity;
331
+ try { el.style.webkitTextSecurity = 'disc'; } catch(e) {}
332
+ }
333
+ })()`,
334
+ returnByValue: false,
335
+ }).catch(() => { });
336
+ }
337
+ export async function restoreSecretFieldsAfterCapture(session) {
338
+ await session.Runtime.evaluate({
339
+ expression: `(function() {
340
+ function queryAllDeep(root) {
341
+ var matched = [];
342
+ var elements = root.querySelectorAll('*');
343
+ for (var i = 0; i < elements.length; i++) {
344
+ var el = elements[i];
345
+ if (el.__wt_cap_val_backup !== undefined || el.__wt_cap_txt_backup !== undefined || el.__wt_cap_sec_backup !== undefined) {
346
+ matched.push(el);
347
+ }
348
+ if (el.shadowRoot) {
349
+ matched = matched.concat(queryAllDeep(el.shadowRoot));
350
+ }
351
+ }
352
+ return matched;
353
+ }
354
+
355
+ var els = queryAllDeep(document);
356
+ for (var i = 0; i < els.length; i++) {
357
+ var el = els[i];
358
+ if (el.__wt_cap_val_backup !== undefined) {
359
+ try { el.value = el.__wt_cap_val_backup; } catch(e) {}
360
+ delete el.__wt_cap_val_backup;
361
+ }
362
+ if (el.__wt_cap_txt_backup !== undefined) {
363
+ try { el.textContent = el.__wt_cap_txt_backup; } catch(e) {}
364
+ delete el.__wt_cap_txt_backup;
365
+ }
366
+ if (el.__wt_cap_sec_backup !== undefined) {
367
+ try { el.style.webkitTextSecurity = el.__wt_cap_sec_backup; } catch(e) {}
368
+ delete el.__wt_cap_sec_backup;
369
+ }
370
+ }
371
+ })()`,
372
+ returnByValue: false,
373
+ }).catch(() => { });
374
+ }
@@ -4,6 +4,7 @@ export interface LockData {
4
4
  profileDir: string;
5
5
  browserPath: string;
6
6
  mode: "session" | "attach";
7
+ appName?: string;
7
8
  pending?: boolean;
8
9
  timestamp?: number;
9
10
  fingerprint?: {
@@ -12,17 +13,19 @@ export interface LockData {
12
13
  cdpVersion?: string;
13
14
  };
14
15
  }
15
- declare function acquireMutex(): Promise<() => Promise<void>>;
16
- export declare function writeLockSafe(data: LockData): Promise<void>;
17
- export declare function writeLock(data: LockData): Promise<void>;
18
- export declare function readLock(): LockData | null;
19
- export declare function clearLock(): void;
16
+ export declare function getLockFilePath(appName?: string): string;
17
+ export declare function getMutexFilePath(appName?: string): string;
18
+ declare function acquireMutex(appName?: string): Promise<() => Promise<void>>;
19
+ export declare function writeLockSafe(data: LockData, appName?: string): Promise<void>;
20
+ export declare function writeLock(data: LockData, appName?: string): Promise<void>;
21
+ export declare function readLock(appName?: string): LockData | null;
22
+ export declare function clearLock(appName?: string): void;
20
23
  /**
21
24
  * Safe variant: only clears the lock if it was written by THIS process (process.pid).
22
25
  * Prevents a second MCP server instance from wiping the first instance's browser lock
23
26
  * during its retry loop, which would allow a race condition where two browsers spawn.
24
27
  */
25
- export declare function clearLockIfOwned(): void;
28
+ export declare function clearLockIfOwned(appName?: string): void;
26
29
  export declare function isProcessRunning(pid: number): boolean;
27
30
  /**
28
31
  * Two-factor lock validation: PID alive AND port accepting TCP connections.