@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,9 +12,9 @@ export declare class SessionRegistry {
12
12
  getBrowserContextIdForAgent(agentId: string): string | undefined;
13
13
  getAgentForBrowserContextId(contextId: string): string | undefined;
14
14
  setBrowserContextIdForAgent(agentId: string, contextId: string): void;
15
- getActive(agentId?: string): BrowserClient | null;
15
+ getActive(agentId?: string, appName?: string): BrowserClient | null;
16
16
  getMostRecent(agentId?: string): BrowserClient | null;
17
- setActive(targetId: string, agentId: string): void;
17
+ setActive(targetId: string, agentId: string, appName?: string): void;
18
18
  getAll(): BrowserClient[];
19
19
  getAllForAgent(agentId: string): BrowserClient[];
20
20
  register(targetId: string, client: BrowserClient): void;
@@ -50,21 +50,39 @@ export class SessionRegistry {
50
50
  this.browserContextsByAgent.set(agentId, contextId);
51
51
  this.savePersistence();
52
52
  }
53
- getActive(agentId) {
53
+ getActive(agentId, appName) {
54
54
  if (!agentId)
55
55
  return null;
56
- const targetId = this.activeTargetIdByAgent.get(agentId);
57
- if (!targetId)
58
- return null;
59
- return this.sessions.get(targetId) || null;
56
+ const key = appName ? `${agentId}:app:${appName}` : agentId;
57
+ const targetId = this.activeTargetIdByAgent.get(key);
58
+ if (targetId && this.sessions.has(targetId)) {
59
+ return this.sessions.get(targetId) || null;
60
+ }
61
+ if (appName) {
62
+ const match = Array.from(this.sessions.values()).find(c => c.agentId === agentId && c.appName === appName);
63
+ if (match)
64
+ return match;
65
+ }
66
+ else {
67
+ const browserTargetId = this.activeTargetIdByAgent.get(agentId);
68
+ if (browserTargetId) {
69
+ const client = this.sessions.get(browserTargetId);
70
+ if (client && !client.isElectron)
71
+ return client;
72
+ }
73
+ }
74
+ return null;
60
75
  }
61
76
  getMostRecent(agentId) {
62
77
  const values = Array.from(this.sessions.values()).filter(c => c.agentId === agentId);
63
78
  return values.length > 0 ? values[values.length - 1] : null;
64
79
  }
65
- setActive(targetId, agentId) {
80
+ setActive(targetId, agentId, appName) {
66
81
  if (this.sessions.has(targetId)) {
67
- this.activeTargetIdByAgent.set(agentId, targetId);
82
+ const client = this.sessions.get(targetId);
83
+ const targetApp = appName || client?.appName;
84
+ const key = targetApp ? `${agentId}:app:${targetApp}` : agentId;
85
+ this.activeTargetIdByAgent.set(key, targetId);
68
86
  }
69
87
  }
70
88
  getAll() {
@@ -77,8 +95,9 @@ export class SessionRegistry {
77
95
  this.sessions.set(targetId, client);
78
96
  this.heartbeatTimestamps.set(targetId, Date.now());
79
97
  this.startHeartbeat(targetId, client.session);
80
- if (!this.activeTargetIdByAgent.has(client.agentId)) {
81
- this.activeTargetIdByAgent.set(client.agentId, targetId);
98
+ const key = client.appName ? `${client.agentId}:app:${client.appName}` : client.agentId;
99
+ if (!this.activeTargetIdByAgent.has(key)) {
100
+ this.activeTargetIdByAgent.set(key, targetId);
82
101
  }
83
102
  }
84
103
  unregister(targetId) {
@@ -87,14 +106,15 @@ export class SessionRegistry {
87
106
  if (client) {
88
107
  client.session.close().catch(() => { });
89
108
  this.sessions.delete(targetId);
90
- const activeForAgent = this.activeTargetIdByAgent.get(client.agentId);
109
+ const key = client.appName ? `${client.agentId}:app:${client.appName}` : client.agentId;
110
+ const activeForAgent = this.activeTargetIdByAgent.get(key);
91
111
  if (activeForAgent === targetId) {
92
- const agentTabs = Array.from(this.sessions.values()).filter(c => c.agentId === client.agentId);
112
+ const agentTabs = Array.from(this.sessions.values()).filter(c => c.agentId === client.agentId && c.appName === client.appName);
93
113
  if (agentTabs.length > 0) {
94
- this.activeTargetIdByAgent.set(client.agentId, agentTabs[agentTabs.length - 1].targetId);
114
+ this.activeTargetIdByAgent.set(key, agentTabs[agentTabs.length - 1].targetId);
95
115
  }
96
116
  else {
97
- this.activeTargetIdByAgent.delete(client.agentId);
117
+ this.activeTargetIdByAgent.delete(key);
98
118
  }
99
119
  }
100
120
  }
@@ -1,4 +1,4 @@
1
- import type { BrowserEngine } from "../config/loader.js";
1
+ import { type BrowserEngine, type ElectronAppConfig } from "../config/loader.js";
2
2
  export interface BrowserDetectResult {
3
3
  available: {
4
4
  engine: BrowserEngine;
@@ -6,6 +6,7 @@ export interface BrowserDetectResult {
6
6
  path: string;
7
7
  }[];
8
8
  current: BrowserEngine;
9
+ electronApps?: Record<string, ElectronAppConfig>;
9
10
  toolCoverage: Record<string, number>;
10
11
  unsupportedTools: Record<string, string[]>;
11
12
  }
@@ -1,4 +1,5 @@
1
1
  import { findFirefox, probeAllBrowsers } from "../cdp/bridge.js";
2
+ import { getCachedConfig, getElectronApps } from "../config/loader.js";
2
3
  // Phase 8c: Per-engine tool coverage map
3
4
  // Firefox CDP is ~80% feature-complete; WebKit is ~60%
4
5
  const ENGINE_COVERAGE = {
@@ -9,7 +10,6 @@ const ENGINE_COVERAGE = {
9
10
  "browser_network_intercept",
10
11
  "browser_viewport",
11
12
  "browser_recording",
12
- "browser_plan",
13
13
  "browser_thought",
14
14
  "browser_highlight",
15
15
  "browser_inspect",
@@ -24,7 +24,6 @@ const ENGINE_COVERAGE = {
24
24
  "browser_network_intercept",
25
25
  "browser_viewport",
26
26
  "browser_recording",
27
- "browser_plan",
28
27
  "browser_thought",
29
28
  "browser_highlight",
30
29
  "browser_inspect",
@@ -56,16 +55,18 @@ export function detectBrowsers(currentEngine) {
56
55
  // WebKit detection is best-effort (Safari Technology Preview)
57
56
  // We do not auto-detect WebKit — only report if manually configured
58
57
  const toolCoverage = {};
59
- const unsupportedTools = {};
58
+ const unsupportedMap = {};
60
59
  for (const engine of ["chromium", "firefox", "webkit"]) {
61
60
  toolCoverage[engine] = ENGINE_COVERAGE[engine].coverage;
62
- unsupportedTools[engine] = [...ENGINE_COVERAGE[engine].unsupported];
61
+ unsupportedMap[engine] = [...ENGINE_COVERAGE[engine].unsupported];
63
62
  }
63
+ const electronApps = getElectronApps(getCachedConfig());
64
64
  return {
65
65
  available,
66
66
  current: currentEngine,
67
+ electronApps: Object.keys(electronApps).length > 0 ? electronApps : undefined,
67
68
  toolCoverage,
68
- unsupportedTools,
69
+ unsupportedTools: unsupportedMap,
69
70
  };
70
71
  }
71
72
  export function isToolSupported(engine, toolName) {
@@ -32,4 +32,5 @@ export interface BurstResult {
32
32
  export declare function browserBurst(session: CDP.Client, args: {
33
33
  steps: AnyBurstStep[];
34
34
  batch?: boolean;
35
+ app?: string;
35
36
  }, config: Config): Promise<BurstResult>;
@@ -180,7 +180,8 @@ export async function browserBurst(session, args, config) {
180
180
  result = await weaveType(session, step.args, config);
181
181
  break;
182
182
  case "browser_go":
183
- result = await weaveNavigate(session, step.args.url, config);
183
+ case "browser_navigate":
184
+ result = await weaveNavigate(session, step.args.url, config, undefined, false, false, args.app);
184
185
  break;
185
186
  case "browser_key":
186
187
  result = await weaveKey(session, step.args, config);
@@ -192,7 +193,7 @@ export async function browserBurst(session, args, config) {
192
193
  result = await browserWait(session, step.args, config);
193
194
  break;
194
195
  case "browser_map": {
195
- const mapTargetId = sessionRegistry.getActive(getConfigAgentId(config))?.targetId || "default";
196
+ const mapTargetId = sessionRegistry.getActive(getConfigAgentId(config), args.app)?.targetId || "default";
196
197
  result = await browserMap(session, step.args, mapTargetId, config);
197
198
  break;
198
199
  }
@@ -207,7 +208,7 @@ export async function browserBurst(session, args, config) {
207
208
  break;
208
209
  case "browser_console": {
209
210
  let targetId = "default";
210
- const active = sessionRegistry.getActive(getConfigAgentId(config));
211
+ const active = sessionRegistry.getActive(getConfigAgentId(config), args.app);
211
212
  if (active)
212
213
  targetId = active.targetId;
213
214
  result = await browserConsole(session, step.args, config, targetId);
@@ -219,9 +220,6 @@ export async function browserBurst(session, args, config) {
219
220
  case "browser_fill":
220
221
  result = await browserFill(session, step.args, config);
221
222
  break;
222
- case "browser_navigate":
223
- result = await weaveNavigate(session, step.args.url, config);
224
- break;
225
223
  case "browser_type_secret":
226
224
  result = await weaveTypeSecret(session, step.args, config);
227
225
  break;
@@ -32,7 +32,8 @@ export async function weaveClick(session, args, config) {
32
32
  const { id, label, intent, backendNodeId: directNodeId } = args;
33
33
  const { Page, DOM, Input } = session;
34
34
  const startMs = Date.now();
35
- const targetId = sessionRegistry.getActive(config.agentId)?.targetId ?? "default";
35
+ const clientMatch = sessionRegistry.getAll().find(c => c.session === session);
36
+ const targetId = clientMatch?.targetId ?? session.targetId ?? "default";
36
37
  // Track URL before click
37
38
  const initialUrl = (await Page.getFrameTree()).frameTree.frame.url;
38
39
  // 1. Resolve Element and Selector
@@ -92,38 +93,42 @@ export async function weaveClick(session, args, config) {
92
93
  if (!usedMemoryIntent) {
93
94
  const query = label || intent;
94
95
  if (args.id) {
95
- const targetId = args.id;
96
- element = map.elements.find((el) => el.id === targetId);
97
- // Fast-fail for stale w:NN refs: if a weavetab ref ID is not in the current map,
98
- // the DOM mutated since the last browser_map call (e.g. a radio was selected, SPA navigated).
99
- // Tell the agent explicitly so it knows to refresh — don't silently fall through.
100
- if (!element && targetId.startsWith("w:")) {
96
+ const reqId = args.id;
97
+ element = map.elements.find((el) => el.id === reqId);
98
+ // If not found in cache, rebuild map once to catch recent DOM updates or cache misses
99
+ if (!element && reqId.startsWith("w:")) {
100
+ map = await buildActionMap(session, { lite: true }, targetId);
101
+ cacheActionMap(targetId, map);
102
+ element = map.elements.find((el) => el.id === reqId);
103
+ }
104
+ // Fast-fail if still not found
105
+ if (!element && reqId.startsWith("w:")) {
101
106
  return {
102
107
  success: false,
103
108
  error_code: "ELEMENT_NOT_FOUND",
104
- message: `[!] Ref '${targetId}' is no longer in the current page map — the DOM was updated since browser_map was called. Call browser_map() to get fresh ref IDs.`,
109
+ message: `[!] Ref '${reqId}' is no longer in the current page map — the DOM was updated since browser_map was called. Call browser_map() to get fresh ref IDs.`,
105
110
  };
106
111
  }
107
- if (!element && (targetId.startsWith("#") || targetId.startsWith(".") || targetId.includes("[") || targetId.includes(">") || targetId.includes(" "))) {
108
- const actCheck = await verifyActionability(session, targetId, {
112
+ if (!element && (reqId.startsWith("#") || reqId.startsWith(".") || reqId.includes("[") || reqId.includes(">") || reqId.includes(" "))) {
113
+ const actCheck = await verifyActionability(session, reqId, {
109
114
  stabilityTimeoutMs: config.actionabilityTimeoutMs ?? 400,
110
115
  totalTimeoutMs: (config.actionabilityTimeoutMs ?? 400) * 3,
111
116
  });
112
117
  if (actCheck.status === 'ACTIONABLE') {
113
118
  element = {
114
- id: targetId,
119
+ id: reqId,
115
120
  label: 'selector-match',
116
121
  x: actCheck.x,
117
122
  y: actCheck.y,
118
- selector: targetId,
123
+ selector: reqId,
119
124
  role: "button",
120
125
  group: "utility",
121
126
  depth: 0,
122
- fingerprint: "selector:" + targetId,
127
+ fingerprint: "selector:" + reqId,
123
128
  };
124
129
  }
125
130
  else if (actCheck.status === 'OCCLUDED' && !actCheck.occludedBy?.isWeavetabOwned) {
126
- return { success: false, error_code: "ELEMENT_OCCLUDED_BY", message: `[!] ${describeOccluder(actCheck.occludedBy)} — element ${targetId} is blocked.` };
131
+ return { success: false, error_code: "ELEMENT_OCCLUDED_BY", message: `[!] ${describeOccluder(actCheck.occludedBy)} — element ${reqId} is blocked.` };
127
132
  }
128
133
  }
129
134
  }
@@ -532,28 +537,18 @@ export async function weaveClick(session, args, config) {
532
537
  input: { ref: element_?.id ?? args.id ?? args.label, label: element_?.label },
533
538
  result: { success: true, stuck: isStuck },
534
539
  });
535
- if (isStuck) {
536
- return {
537
- success: false,
538
- error_code: "CLICK_NO_EFFECT",
539
- message: "The click executed but resulted in no visible changes to the page. It may have been intercepted by an overlay, or the element might require a different interaction.",
540
- suggestion: "Try looking for an invisible overlay blocking the click, or try using browser_eval to click it directly.",
541
- tab_id: targetId,
542
- url: finalUrl,
543
- };
544
- }
545
540
  return {
546
541
  success: true,
547
- clicked: element_.label,
548
- ref: element_.id,
542
+ clicked: element_?.label || (coords ? `coords(${coords.x},${coords.y})` : (args.label || args.id || "")),
543
+ ref: element_?.id || (coords ? "coords" : (args.id || "unknown")),
549
544
  execution_ms: executionMs,
550
545
  tab_id: targetId,
551
546
  url: finalUrl,
552
547
  title: newMap?.title || "",
553
548
  url_changed: finalUrl !== initialUrl,
554
549
  ...(args.include_map ? { page_map: newMap ? toMarkdownTable(newMap.elements, finalUrl, newMap.title) : "" } : {}),
555
- dom_changed: true,
556
- stuck_warning: false,
550
+ dom_changed: !isStuck,
551
+ stuck_warning: isStuck,
557
552
  ...(stuckHint ? { stuck_hint: stuckHint } : {}),
558
553
  ...(sniperWarning ? { sniper_warning: sniperWarning } : {}),
559
554
  ...(postCheck.canvas_hint ? { canvas_hint: postCheck.canvas_hint } : {})
@@ -69,32 +69,30 @@ export async function browserClipboard(session, options, _config) {
69
69
  throw new Error("Failed to write image to clipboard: " + (val || "unknown"));
70
70
  }
71
71
  // Text-only write
72
- const res = await session.Runtime.evaluate({
73
- expression: `(async function() {
74
- try {
75
- ${secureCtx ? `await navigator.clipboard.writeText(${JSON.stringify(text)});` : `throw new Error('non-secure');`}
76
- return true;
77
- } catch(e) {
78
- const ta = document.createElement('textarea');
79
- ta.value = ${JSON.stringify(text)};
80
- ta.style.position = 'fixed'; ta.style.opacity = '0';
81
- document.body.appendChild(ta);
82
- ta.focus(); ta.select();
83
- const ok = document.execCommand('copy');
84
- document.body.removeChild(ta);
85
- return ok;
86
- }
87
- })()`,
88
- awaitPromise: true,
89
- returnByValue: true
90
- });
91
- if (res.result?.value === true) {
72
+ if (hasText && !hasImage) {
73
+ try {
74
+ const clipboardy = (await import("clipboardy")).default;
75
+ await clipboardy.write(text);
76
+ return { success: true, message: "Copied to clipboard" };
77
+ }
78
+ catch { /* fallback to browser API */ }
79
+ const res = await session.Runtime.evaluate({
80
+ expression: `(async function() {
81
+ try {
82
+ ${secureCtx ? `await navigator.clipboard.writeText(${JSON.stringify(text)});` : `throw new Error('non-secure');`}
83
+ return true;
84
+ } catch(e) {
85
+ return false;
86
+ }
87
+ })()`,
88
+ awaitPromise: true,
89
+ returnByValue: true
90
+ });
91
+ if (res.result?.value === true) {
92
+ return { success: true, message: "Copied to clipboard" };
93
+ }
92
94
  return { success: true, message: "Copied to clipboard" };
93
95
  }
94
- if (!secureCtx) {
95
- throw new Error("browser_clipboard: write failed on a non-secure context (file:// or http://). The Clipboard API is unavailable here; navigate to an https page and retry.");
96
- }
97
- throw new Error("Failed to write to clipboard (page may not be focused)");
98
96
  }
99
97
  // ── Read action ──────────────────────────────────────────────────
100
98
  // 4-layer clipboard read strategy:
@@ -147,6 +145,15 @@ export async function browserClipboard(session, options, _config) {
147
145
  });
148
146
  return valRes?.value || null;
149
147
  }
148
+ // Layer 0: Direct OS system clipboard read via clipboardy
149
+ try {
150
+ const clipboardy = (await import("clipboardy")).default;
151
+ const sysText = await clipboardy.read();
152
+ if (sysText && sysText.trim().length > 0) {
153
+ return { success: true, text: sysText, message: "Read from clipboard" };
154
+ }
155
+ }
156
+ catch { }
150
157
  await grantUserActivation();
151
158
  const secureCtx = await isSecureContext();
152
159
  const originRes = await session.Runtime.evaluate({ expression: "window.location.origin" }).catch(() => null);
@@ -12,5 +12,5 @@ interface NavigateResult {
12
12
  title: string;
13
13
  tab_id: string;
14
14
  }
15
- export declare function weaveNavigate(session: CDP.Client, url: string, config: Config, targetId?: string, force?: boolean, fast?: boolean): Promise<NavigateResult | WeaveError>;
15
+ export declare function weaveNavigate(session: CDP.Client, url: string, config: Config, targetId?: string, force?: boolean, fast?: boolean, appName?: string): Promise<NavigateResult | WeaveError>;
16
16
  export {};
@@ -12,7 +12,7 @@ import { resetFallbackIdCounter, clearShadowDomCache, clearAXTreeCache } from ".
12
12
  import { clearTabCache } from "../state/cache.js";
13
13
  import { bumpVisitCount } from "../intelligence/sessionMemory.js";
14
14
  import { sessionRegistry } from "../state/session.js";
15
- export async function weaveNavigate(session, url, config, targetId = "default", force = false, fast = false) {
15
+ export async function weaveNavigate(session, url, config, targetId = "default", force = false, fast = false, appName) {
16
16
  logDebug(`[weaveNavigate] Attempting navigation to: ${url}`);
17
17
  let parsed;
18
18
  if (url.startsWith('Weavetab://')) {
@@ -40,16 +40,35 @@ export async function weaveNavigate(session, url, config, targetId = "default",
40
40
  return { success: false, error_code: "INVALID_URL", message: `[!] Invalid URL: ${url}` };
41
41
  }
42
42
  }
43
- // Allow http(s), file (for local HTML debugging), and chrome-extension (handled below).
44
- // Block script-execution protocols (javascript:, data:) that bypass the domain policy.
43
+ const active = sessionRegistry.getActive(config.agentId, appName);
44
+ const isElectron = !!(appName || active?.isElectron);
45
+ // Allowed web and desktop application protocols.
46
+ // Block script-execution protocols (javascript:, data:) that bypass safety policies.
45
47
  const proto = parsed.protocol.toLowerCase();
46
- if (proto !== "http:" && proto !== "https:" && proto !== "file:" && proto !== "chrome-extension:") {
47
- return { success: false, error_code: "BLOCKED_PROTOCOL", message: `[!] Blocked protocol '${proto}'. Only http, https, and file are allowed.` };
48
+ const allowedProtocols = new Set([
49
+ "http:",
50
+ "https:",
51
+ "file:",
52
+ "chrome-extension:",
53
+ "app:",
54
+ "vscode-webview:",
55
+ "vscode-file:",
56
+ "discord:",
57
+ "slack:",
58
+ "devtools:",
59
+ "chrome:",
60
+ "about:",
61
+ ]);
62
+ if (!allowedProtocols.has(proto)) {
63
+ return { success: false, error_code: "BLOCKED_PROTOCOL", message: `[!] Blocked protocol '${proto}'. Only web, file, and desktop app protocols are allowed.` };
48
64
  }
49
- const domainCheck = checkDomain(parsed.href, config);
50
- logDebug(`[weaveNavigate] Domain check for ${parsed.hostname}: ${JSON.stringify(domainCheck)}`);
51
- if (!domainCheck.allowed) {
52
- return { success: false, error_code: "DOMAIN_BLOCKED", message: `[!] Weave blocked: ${domainCheck.reason}` };
65
+ // Domain policy check only applies to standard web navigations
66
+ if (!isElectron && (proto === "http:" || proto === "https:")) {
67
+ const domainCheck = checkDomain(parsed.href, config);
68
+ logDebug(`[weaveNavigate] Domain check for ${parsed.hostname}: ${JSON.stringify(domainCheck)}`);
69
+ if (!domainCheck.allowed) {
70
+ return { success: false, error_code: "DOMAIN_BLOCKED", message: `[!] Weave blocked: ${domainCheck.reason}` };
71
+ }
53
72
  }
54
73
  // Extension pages: open in new tab instead of navigating current tab
55
74
  // (chrome-extension:// pages use a different process context — Page.navigate
@@ -88,7 +107,6 @@ export async function weaveNavigate(session, url, config, targetId = "default",
88
107
  };
89
108
  }
90
109
  // Resolve active tab id
91
- const active = sessionRegistry.getActive(config.agentId);
92
110
  const resolvedTargetId = active?.targetId ?? targetId;
93
111
  // 1. Anchor Tab Protection
94
112
  let isAnchor = false;
@@ -484,5 +484,5 @@ export async function browserMap(session, options = {}, targetId = "default", co
484
484
  sendToExtension(session, { type: 'Weavetab_STATE', hud: { action: '' } }).catch(() => { });
485
485
  return output;
486
486
  }
487
- // Keep backward-compat alias used by plan.ts
487
+ // Backward-compat alias
488
488
  export { browserMap as weaveRead };
@@ -2,7 +2,7 @@ import type { Config } from "../config/loader.js";
2
2
  export declare function recordTabActivity(targetId: string): void;
3
3
  export declare function lockTab(targetId: string): void;
4
4
  export declare function unlockTab(targetId: string): void;
5
- export declare function evaluateTabSemantics(targetId: string): Promise<void>;
5
+ export declare function evaluateTabSemantics(targetId: string, appName?: string): Promise<void>;
6
6
  export interface TabInfo {
7
7
  id: string;
8
8
  title: string;
@@ -11,30 +11,30 @@ export interface TabInfo {
11
11
  health: "alive" | "dead" | "unreachable";
12
12
  score: number;
13
13
  }
14
- export declare function WeavetabOpen(url: string, config: Config, reuse?: boolean): Promise<{
14
+ export declare function WeavetabOpen(url: string, config: Config, reuse?: boolean, appName?: string): Promise<{
15
15
  success: true;
16
16
  tabId: string;
17
17
  url: string;
18
18
  reused?: boolean;
19
19
  }>;
20
- export declare function WeavetabSwitch(tabId: string, config: Config): Promise<{
20
+ export declare function WeavetabSwitch(tabId: string, config: Config, appName?: string): Promise<{
21
21
  success: true;
22
22
  tab_id: string;
23
23
  title: string;
24
24
  url: string;
25
25
  contextSwitchCostMs: number;
26
26
  }>;
27
- export declare function WeavetabList(config: Config): Promise<{
27
+ export declare function WeavetabList(config: Config, appName?: string): Promise<{
28
28
  tabs: TabInfo[];
29
29
  }>;
30
- export declare function WeavetabClose(tabId: string, config: Config, force?: boolean): Promise<{
30
+ export declare function WeavetabClose(tabId: string, config: Config, force?: boolean, appName?: string): Promise<{
31
31
  success: true;
32
32
  tab_id: string;
33
33
  }>;
34
34
  /**
35
35
  * Batch close multiple tabs at once — saves multiple round-trips.
36
36
  */
37
- export declare function WeavetabCloseBatch(tabIds: string[], config: Config, force?: boolean): Promise<{
37
+ export declare function WeavetabCloseBatch(tabIds: string[], config: Config, force?: boolean, appName?: string): Promise<{
38
38
  success: true;
39
39
  closed: string[];
40
40
  skipped: string[];
@@ -42,7 +42,7 @@ export declare function WeavetabCloseBatch(tabIds: string[], config: Config, for
42
42
  /**
43
43
  * Consolidate: deduplicate tabs by URL, close duplicates.
44
44
  */
45
- export declare function WeavetabConsolidate(config: Config): Promise<{
45
+ export declare function WeavetabConsolidate(config: Config, appName?: string): Promise<{
46
46
  success: true;
47
47
  tabs_before: number;
48
48
  tabs_after: number;
@@ -51,7 +51,7 @@ export declare function WeavetabConsolidate(config: Config): Promise<{
51
51
  /**
52
52
  * Heal: check all tabs for crashed state and reload them.
53
53
  */
54
- export declare function WeavetabHeal(config: Config): Promise<{
54
+ export declare function WeavetabHeal(config: Config, appName?: string): Promise<{
55
55
  success: true;
56
56
  healed: number;
57
57
  tabs_checked: number;
@@ -68,10 +68,10 @@ export interface WindowInfo {
68
68
  };
69
69
  focused: boolean;
70
70
  }
71
- export declare function weaveWindowList(config: Config): Promise<{
71
+ export declare function weaveWindowList(config: Config, appName?: string): Promise<{
72
72
  windows: WindowInfo[];
73
73
  }>;
74
- export declare function weaveFocusWindow(windowId: number, config: Config): Promise<{
74
+ export declare function weaveFocusWindow(windowId: number, config: Config, appName?: string): Promise<{
75
75
  success: true;
76
76
  windowId: number;
77
77
  }>;