@weavetab/mcp 2.5.0 → 2.5.1

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 (168) hide show
  1. package/AGENTS.md +49 -49
  2. package/ARCHITECTURE.md +590 -590
  3. package/LICENSE +216 -216
  4. package/README.md +269 -269
  5. package/SECURITY.md +207 -207
  6. package/TROUBLESHOOTING.md +373 -365
  7. package/dist/anticaptcha/detector.js +395 -395
  8. package/dist/anticaptcha/solver.js +130 -130
  9. package/dist/cdp/bridge.js +73 -73
  10. package/dist/cdp/connector.js +46 -22
  11. package/dist/cdp/extension.js +34 -34
  12. package/dist/cdp/helpers.js +174 -174
  13. package/dist/cdp/pruner.js +195 -195
  14. package/dist/cdp/ref_cache.js +66 -66
  15. package/dist/cdp/security.js +10 -10
  16. package/dist/cdp/verify.js +57 -57
  17. package/dist/cdp/walker.js +563 -525
  18. package/dist/cli/plugin.js +26 -3
  19. package/dist/config/loader.d.ts +0 -3
  20. package/dist/config/loader.js +0 -9
  21. package/dist/index.js +0 -4
  22. package/dist/intelligence/intent.js +81 -81
  23. package/dist/intelligence/sessionMemory.js +2 -2
  24. package/dist/overlay/ghost.js +22 -22
  25. package/dist/overlay/injector.js +89 -89
  26. package/dist/plugin/extension_bridge.js +54 -54
  27. package/dist/plugin/interface.d.ts +7 -0
  28. package/dist/plugin/loader.js +3 -4
  29. package/dist/plugin/schema.d.ts +11 -0
  30. package/dist/plugin/schema.js +59 -0
  31. package/dist/security/guard.js +18 -18
  32. package/dist/sensors/actionability.js +167 -167
  33. package/dist/sensors/guard.js +29 -29
  34. package/dist/sensors/network.js +15 -17
  35. package/dist/server.d.ts +1 -1
  36. package/dist/server.js +173 -144
  37. package/dist/state/delta.js +6 -7
  38. package/dist/tools/automation.js +85 -85
  39. package/dist/tools/canvas.js +48 -25
  40. package/dist/tools/checkpoint.js +94 -35
  41. package/dist/tools/click.js +52 -41
  42. package/dist/tools/clipboard.js +80 -74
  43. package/dist/tools/design_extract.js +441 -441
  44. package/dist/tools/dialog.js +13 -13
  45. package/dist/tools/emulation.d.ts +2 -0
  46. package/dist/tools/emulation.js +7 -0
  47. package/dist/tools/fill.js +344 -347
  48. package/dist/tools/find.js +78 -52
  49. package/dist/tools/highlight.js +67 -67
  50. package/dist/tools/inspect.js +54 -25
  51. package/dist/tools/key.js +42 -35
  52. package/dist/tools/navigate.d.ts +1 -1
  53. package/dist/tools/navigate.js +35 -35
  54. package/dist/tools/office/parse.js +62 -18
  55. package/dist/tools/pdf.d.ts +9 -0
  56. package/dist/tools/pdf.js +7 -4
  57. package/dist/tools/performance.d.ts +11 -4
  58. package/dist/tools/performance.js +94 -42
  59. package/dist/tools/performance_test.d.ts +2 -2
  60. package/dist/tools/performance_test.js +69 -69
  61. package/dist/tools/pointer.js +116 -116
  62. package/dist/tools/read.js +99 -67
  63. package/dist/tools/recording.js +9 -0
  64. package/dist/tools/scrape.js +146 -146
  65. package/dist/tools/screenshot.d.ts +2 -0
  66. package/dist/tools/screenshot.js +46 -4
  67. package/dist/tools/scroll.d.ts +3 -1
  68. package/dist/tools/scroll.js +13 -15
  69. package/dist/tools/select.d.ts +3 -0
  70. package/dist/tools/select.js +131 -124
  71. package/dist/tools/snapshot.js +8 -6
  72. package/dist/tools/storage.d.ts +5 -1
  73. package/dist/tools/storage.js +16 -0
  74. package/dist/tools/tabs.d.ts +7 -5
  75. package/dist/tools/tabs.js +79 -26
  76. package/dist/tools/thought.js +8 -8
  77. package/dist/tools/type.js +274 -274
  78. package/dist/tools/type_secret.js +23 -23
  79. package/dist/tools/upload.js +3 -3
  80. package/dist/tools/video_compiler.js +9 -0
  81. package/dist/tools/wait.js +35 -35
  82. package/dist/version.d.ts +1 -1
  83. package/dist/version.js +1 -1
  84. package/docs/DOCUMENTATION.md +583 -585
  85. package/docs/PLUGINS.md +139 -121
  86. package/docs/TOOLS.md +3 -4
  87. package/docs/architecture/blind-injection.md +83 -83
  88. package/docs/architecture/overview.md +49 -49
  89. package/docs/architecture/security.md +36 -36
  90. package/docs/architecture/transport.md +37 -37
  91. package/docs/cli/cli-commands.md +50 -50
  92. package/docs/cli/cli-flags.md +28 -28
  93. package/docs/cli/cli-scripting.md +35 -35
  94. package/docs/configuration/config-locations.md +23 -23
  95. package/docs/configuration/configuration.md +39 -27
  96. package/docs/configuration/env-variables.md +19 -19
  97. package/docs/configuration/folders.md +62 -55
  98. package/docs/desktop-automation/electron-apps.md +225 -214
  99. package/docs/get-started/installation.md +1 -1
  100. package/docs/get-started/introduction.md +60 -60
  101. package/docs/get-started/quickstart.md +2 -2
  102. package/docs/get-started/setup-wizard.md +27 -27
  103. package/docs/intelligence/site-profiles.md +151 -151
  104. package/docs/tools/tools.md +582 -631
  105. package/docs/troubleshooting/trouble-browser.md +27 -27
  106. package/docs/troubleshooting/trouble-loop.md +31 -31
  107. package/ext-dist/background.js +415 -415
  108. package/ext-dist/content.js +2527 -2527
  109. package/ext-dist/dashboard.html +583 -583
  110. package/ext-dist/hud/hud.js +3 -3
  111. package/ext-dist/hud/index.html +20 -20
  112. package/ext-dist/manifest.json +73 -73
  113. package/ext-dist/styles.css +223 -223
  114. package/mcp.json +796 -805
  115. package/package.json +118 -118
  116. package/server.json +23 -23
  117. package/dist/cdp/walker.test.d.ts +0 -2
  118. package/dist/cdp/walker.test.js +0 -172
  119. package/dist/intelligence/sessionMemory.test.d.ts +0 -2
  120. package/dist/intelligence/sessionMemory.test.js +0 -62
  121. package/dist/intelligence/trail.test.d.ts +0 -2
  122. package/dist/intelligence/trail.test.js +0 -110
  123. package/dist/state/cache.test.d.ts +0 -2
  124. package/dist/state/cache.test.js +0 -76
  125. package/dist/state/delta.test.d.ts +0 -2
  126. package/dist/state/delta.test.js +0 -279
  127. package/dist/state/groups.d.ts +0 -15
  128. package/dist/state/groups.js +0 -80
  129. package/dist/tools/accessibility.d.ts +0 -14
  130. package/dist/tools/accessibility.js +0 -32
  131. package/dist/tools/ask.d.ts +0 -14
  132. package/dist/tools/ask.js +0 -49
  133. package/dist/tools/click_and_wait.d.ts +0 -33
  134. package/dist/tools/click_and_wait.js +0 -149
  135. package/dist/tools/drag.d.ts +0 -15
  136. package/dist/tools/drag.js +0 -29
  137. package/dist/tools/geo.d.ts +0 -19
  138. package/dist/tools/geo.js +0 -29
  139. package/dist/tools/github/analyze.d.ts +0 -4
  140. package/dist/tools/github/analyze.js +0 -113
  141. package/dist/tools/github/cache.d.ts +0 -21
  142. package/dist/tools/github/cache.js +0 -28
  143. package/dist/tools/github/index.d.ts +0 -19
  144. package/dist/tools/github/index.js +0 -40
  145. package/dist/tools/github/issues.d.ts +0 -9
  146. package/dist/tools/github/issues.js +0 -88
  147. package/dist/tools/github/pr.d.ts +0 -5
  148. package/dist/tools/github/pr.js +0 -88
  149. package/dist/tools/github/rateLimiter.d.ts +0 -4
  150. package/dist/tools/github/rateLimiter.js +0 -46
  151. package/dist/tools/github/read.d.ts +0 -34
  152. package/dist/tools/github/read.js +0 -74
  153. package/dist/tools/github/repoContext.d.ts +0 -20
  154. package/dist/tools/github/repoContext.js +0 -129
  155. package/dist/tools/hover.d.ts +0 -19
  156. package/dist/tools/hover.js +0 -57
  157. package/dist/tools/integration.test.d.ts +0 -2
  158. package/dist/tools/integration.test.js +0 -96
  159. package/dist/tools/macro_compiler.d.ts +0 -7
  160. package/dist/tools/macro_compiler.js +0 -82
  161. package/dist/tools/mouse.d.ts +0 -21
  162. package/dist/tools/mouse.js +0 -62
  163. package/dist/tools/response.d.ts +0 -129
  164. package/dist/tools/response.js +0 -5
  165. package/dist/tools/wait_for.d.ts +0 -23
  166. package/dist/tools/wait_for.js +0 -30
  167. package/dist/tools/windows.d.ts +0 -22
  168. package/dist/tools/windows.js +0 -57
@@ -7,8 +7,9 @@ import { buildActionMap, safeSelector } from "../cdp/walker.js";
7
7
  async function getSelector(session, options, config) {
8
8
  if (options.selector)
9
9
  return options.selector;
10
- if (options.ref || options.id) {
11
- const ref = (options.ref || options.id);
10
+ const targetRef = options.ref || options.id || options.target;
11
+ if (targetRef) {
12
+ const ref = targetRef;
12
13
  if (ref.startsWith("w:")) {
13
14
  return `[data-wt-id="${ref.slice(2)}"]`;
14
15
  }
@@ -43,60 +44,60 @@ async function getSelector(session, options, config) {
43
44
  throw new Error("Could not resolve node");
44
45
  const { result } = await session.Runtime.callFunctionOn({
45
46
  objectId: object.objectId,
46
- functionDeclaration: `function() {
47
- if (this.id) return '#' + CSS.escape(this.id);
48
- if (this.name) return 'select[name="' + CSS.escape(this.name) + '"]';
49
- var idx = 0, el = this;
50
- while (el.previousElementSibling) { idx++; el = el.previousElementSibling; }
51
- var parent = this.parentElement;
52
- if (!parent) return 'select:last-of-type';
53
- var tag = this.tagName.toLowerCase();
54
- return tag + ':nth-child(' + (idx + 1) + ')';
47
+ functionDeclaration: `function() {
48
+ if (this.id) return '#' + CSS.escape(this.id);
49
+ if (this.name) return 'select[name="' + CSS.escape(this.name) + '"]';
50
+ var idx = 0, el = this;
51
+ while (el.previousElementSibling) { idx++; el = el.previousElementSibling; }
52
+ var parent = this.parentElement;
53
+ if (!parent) return 'select:last-of-type';
54
+ var tag = this.tagName.toLowerCase();
55
+ return tag + ':nth-child(' + (idx + 1) + ')';
55
56
  }()`,
56
57
  returnByValue: true
57
58
  });
58
59
  return result.value;
59
60
  }
60
- throw new Error("browser_select requires either selector, nodeId, ref, or id");
61
+ throw new Error("browser_select requires either selector, nodeId, ref, target, or id");
61
62
  }
62
63
  function escapeSelector(sel) {
63
64
  return JSON.stringify(sel);
64
65
  }
65
66
  export async function browserSelect(session, options, _config) {
66
- if (!options.selector && !options.nodeId && !options.ref && !options.id) {
67
- throw new Error("browser_select requires either selector, nodeId, ref, or id");
67
+ if (!options.selector && !options.nodeId && !options.ref && !options.id && !options.target) {
68
+ throw new Error("browser_select requires either selector, nodeId, ref, target, or id");
68
69
  }
69
70
  const selector = await getSelector(session, options, _config);
70
71
  const value = options.value;
71
72
  const getSelectState = async () => {
72
73
  const { result } = await session.Runtime.evaluate({
73
- expression: `(function() {
74
- try {
75
- var el = document.querySelector(${escapeSelector(selector)});
76
- if (!el) return JSON.stringify({ exists: "false" });
77
- if (el.tagName === "SELECT") {
78
- var idx = el.selectedIndex;
79
- var opt = idx >= 0 ? el.options[idx] : null;
80
- return JSON.stringify({
81
- exists: "true",
82
- isNative: "true",
83
- selectedIndex: String(idx),
84
- value: el.value,
85
- selectedText: opt ? opt.text : ""
86
- });
87
- }
88
- // Custom UI Dropdown / Combobox / Popover
89
- var text = (el.innerText || el.textContent || "").trim();
90
- var val = el.getAttribute("data-value") || el.getAttribute("aria-valuenow") || text;
91
- return JSON.stringify({
92
- exists: "true",
93
- isNative: "false",
94
- value: val,
95
- selectedText: text
96
- });
97
- } catch(e) {
98
- return JSON.stringify({ exists: "false", error: e.message });
99
- }
74
+ expression: `(function() {
75
+ try {
76
+ var el = document.querySelector(${escapeSelector(selector)});
77
+ if (!el) return JSON.stringify({ exists: "false" });
78
+ if (el.tagName === "SELECT") {
79
+ var idx = el.selectedIndex;
80
+ var opt = idx >= 0 ? el.options[idx] : null;
81
+ return JSON.stringify({
82
+ exists: "true",
83
+ isNative: "true",
84
+ selectedIndex: String(idx),
85
+ value: el.value,
86
+ selectedText: opt ? opt.text : ""
87
+ });
88
+ }
89
+ // Custom UI Dropdown / Combobox / Popover
90
+ var text = (el.innerText || el.textContent || "").trim();
91
+ var val = el.getAttribute("data-value") || el.getAttribute("aria-valuenow") || text;
92
+ return JSON.stringify({
93
+ exists: "true",
94
+ isNative: "false",
95
+ value: val,
96
+ selectedText: text
97
+ });
98
+ } catch(e) {
99
+ return JSON.stringify({ exists: "false", error: e.message });
100
+ }
100
101
  })()`,
101
102
  returnByValue: true
102
103
  });
@@ -104,88 +105,88 @@ export async function browserSelect(session, options, _config) {
104
105
  };
105
106
  const action = async () => {
106
107
  const { result } = await session.Runtime.evaluate({
107
- expression: `(function(selector, value) {
108
- try {
109
- const el = document.querySelector(selector);
110
- if (!el) return 'Element not found';
111
-
112
- // ── NATIVE <select> ELEMENT ──────────────────────────────────────
113
- if (el.tagName === 'SELECT') {
114
- let foundIndex = -1;
115
- for (let i = 0; i < el.options.length; i++) {
116
- const opt = el.options[i];
117
- if (opt.value === value || opt.text === value || opt.text.trim() === value.trim()) {
118
- foundIndex = i;
119
- break;
120
- }
121
- }
122
- if (foundIndex === -1) return 'Option not found in <select>: ' + value;
123
-
124
- el.selectedIndex = foundIndex;
125
-
126
- const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLSelectElement.prototype, 'value')?.set;
127
- if (nativeSetter) nativeSetter.call(el, el.options[foundIndex].value);
128
-
129
- el.dispatchEvent(new Event('input', { bubbles: true }));
130
- el.dispatchEvent(new Event('change', { bubbles: true }));
131
- el.options[foundIndex].dispatchEvent(new MouseEvent('click', { bubbles: true }));
132
-
133
- return 'selected:' + el.selectedIndex;
134
- }
135
-
136
- // ── CUSTOM ARIA / REACT / RADIX / SHADCN DROPDOWN ────────────────
137
- // 1. Open popover if closed
138
- const isExpanded = el.getAttribute('aria-expanded') === 'true';
139
- if (!isExpanded) {
140
- el.click();
141
- el.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
142
- el.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
143
- }
144
-
145
- // 2. Scan options in open popover / listbox
146
- const targetText = value.toLowerCase().trim();
147
- const optionSelectors = [
148
- '[role="option"]',
149
- '[role="menuitem"]',
150
- '[role="treeitem"]',
151
- '[data-radix-collection-item]',
152
- 'li',
153
- '.option',
154
- '.select-option',
155
- '[data-value]'
156
- ];
157
-
158
- let candidates = Array.from(document.querySelectorAll(optionSelectors.join(',')));
159
- // Fallback to all visible clickable items inside active popovers/listboxes
160
- if (candidates.length === 0) {
161
- const containers = document.querySelectorAll('[role="listbox"], [role="menu"], [data-radix-popper-content-wrapper], .select-popover, .dropdown-menu');
162
- containers.forEach(c => {
163
- candidates.push(...Array.from(c.querySelectorAll('*')));
164
- });
165
- }
166
-
167
- let matchedEl = null;
168
- for (const cand of candidates) {
169
- const text = (cand.innerText || cand.textContent || '').toLowerCase().trim();
170
- const valAttr = (cand.getAttribute('data-value') || cand.getAttribute('value') || '').toLowerCase().trim();
171
- if (text === targetText || valAttr === targetText || text.includes(targetText)) {
172
- matchedEl = cand;
173
- break;
174
- }
175
- }
176
-
177
- if (!matchedEl) {
178
- return 'Custom option not found: ' + value;
179
- }
180
-
181
- matchedEl.click();
182
- matchedEl.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
183
- matchedEl.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
184
-
185
- return 'selected:custom';
186
- } catch (e) {
187
- return e.message || 'Unknown error';
188
- }
108
+ expression: `(function(selector, value) {
109
+ try {
110
+ const el = document.querySelector(selector);
111
+ if (!el) return 'Element not found';
112
+
113
+ // ── NATIVE <select> ELEMENT ──────────────────────────────────────
114
+ if (el.tagName === 'SELECT') {
115
+ let foundIndex = -1;
116
+ for (let i = 0; i < el.options.length; i++) {
117
+ const opt = el.options[i];
118
+ if (opt.value === value || opt.text === value || opt.text.trim() === value.trim()) {
119
+ foundIndex = i;
120
+ break;
121
+ }
122
+ }
123
+ if (foundIndex === -1) return 'Option not found in <select>: ' + value;
124
+
125
+ el.selectedIndex = foundIndex;
126
+
127
+ const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLSelectElement.prototype, 'value')?.set;
128
+ if (nativeSetter) nativeSetter.call(el, el.options[foundIndex].value);
129
+
130
+ el.dispatchEvent(new Event('input', { bubbles: true }));
131
+ el.dispatchEvent(new Event('change', { bubbles: true }));
132
+ el.options[foundIndex].dispatchEvent(new MouseEvent('click', { bubbles: true }));
133
+
134
+ return 'selected:' + el.selectedIndex;
135
+ }
136
+
137
+ // ── CUSTOM ARIA / REACT / RADIX / SHADCN DROPDOWN ────────────────
138
+ // 1. Open popover if closed
139
+ const isExpanded = el.getAttribute('aria-expanded') === 'true';
140
+ if (!isExpanded) {
141
+ el.click();
142
+ el.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
143
+ el.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
144
+ }
145
+
146
+ // 2. Scan options in open popover / listbox
147
+ const targetText = value.toLowerCase().trim();
148
+ const optionSelectors = [
149
+ '[role="option"]',
150
+ '[role="menuitem"]',
151
+ '[role="treeitem"]',
152
+ '[data-radix-collection-item]',
153
+ 'li',
154
+ '.option',
155
+ '.select-option',
156
+ '[data-value]'
157
+ ];
158
+
159
+ let candidates = Array.from(document.querySelectorAll(optionSelectors.join(',')));
160
+ // Fallback to all visible clickable items inside active popovers/listboxes
161
+ if (candidates.length === 0) {
162
+ const containers = document.querySelectorAll('[role="listbox"], [role="menu"], [data-radix-popper-content-wrapper], .select-popover, .dropdown-menu');
163
+ containers.forEach(c => {
164
+ candidates.push(...Array.from(c.querySelectorAll('*')));
165
+ });
166
+ }
167
+
168
+ let matchedEl = null;
169
+ for (const cand of candidates) {
170
+ const text = (cand.innerText || cand.textContent || '').toLowerCase().trim();
171
+ const valAttr = (cand.getAttribute('data-value') || cand.getAttribute('value') || '').toLowerCase().trim();
172
+ if (text === targetText || valAttr === targetText || text.includes(targetText)) {
173
+ matchedEl = cand;
174
+ break;
175
+ }
176
+ }
177
+
178
+ if (!matchedEl) {
179
+ return 'Custom option not found: ' + value;
180
+ }
181
+
182
+ matchedEl.click();
183
+ matchedEl.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
184
+ matchedEl.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
185
+
186
+ return 'selected:custom';
187
+ } catch (e) {
188
+ return e.message || 'Unknown error';
189
+ }
189
190
  })(${escapeSelector(selector)}, ${escapeSelector(value)})`,
190
191
  returnByValue: true
191
192
  });
@@ -208,7 +209,12 @@ export async function browserSelect(session, options, _config) {
208
209
  input: { ref: selector, value },
209
210
  result: { success: true, stuck: false },
210
211
  });
211
- return { success: true, message: `Selected value "${value}" on ${beforeState.isNative === "true" ? "native <select>" : "custom UI dropdown"} (${confirm.attempts} attempts, settled ${confirm.settleMs}ms)` };
212
+ return {
213
+ success: true,
214
+ value,
215
+ selected: value,
216
+ message: `Selected value "${value}" on ${beforeState.isNative === "true" ? "native <select>" : "custom UI dropdown"} (${confirm.attempts} attempts, settled ${confirm.settleMs}ms)`
217
+ };
212
218
  }
213
219
  recordAction({
214
220
  timestamp: new Date().toISOString(),
@@ -218,6 +224,7 @@ export async function browserSelect(session, options, _config) {
218
224
  });
219
225
  return {
220
226
  success: false,
227
+ value,
221
228
  message: `Selection did not take effect. Before: value="${beforeState.value}". After: value="${confirm.after.value}".`
222
229
  };
223
230
  }
@@ -14,12 +14,14 @@ export async function browserSnapshot(session, options, config) {
14
14
  if (!filename || !filename.trim()) {
15
15
  throw new Error("browser_snapshot requires a filename.");
16
16
  }
17
- // Strip any absolute path provided by the caller — always save in the managed snapshot dir
18
- filename = path.basename(filename);
19
- if (!filename.endsWith(".mhtml"))
20
- filename += ".mhtml";
21
- const snapshotDir = getOutputDir("snapshots", config);
22
- const filePath = path.join(snapshotDir, filename);
17
+ // If caller provided an absolute path, respect it; otherwise store in snapshots directory
18
+ let filePath = filename;
19
+ if (!path.isAbsolute(filePath)) {
20
+ const snapshotDir = getOutputDir("snapshots", config);
21
+ filePath = path.join(snapshotDir, path.basename(filename));
22
+ }
23
+ if (!filePath.endsWith(".mhtml"))
24
+ filePath += ".mhtml";
23
25
  if (action === "capture") {
24
26
  // Always hide overlay before capture (default true); set hideOverlay: false to skip.
25
27
  const shouldHide = options.hideOverlay !== false;
@@ -7,12 +7,16 @@ import CDP from "chrome-remote-interface";
7
7
  import type { Config } from "../config/loader.js";
8
8
  export interface StorageOptions {
9
9
  scope: "local" | "session";
10
- action: "get" | "set" | "clear";
10
+ action: "get" | "set" | "remove" | "list" | "clear";
11
11
  key?: string;
12
12
  value?: string;
13
13
  }
14
14
  export declare function browserStorage(session: CDP.Client, options: StorageOptions, _config: Config): Promise<{
15
15
  success: boolean;
16
16
  result?: string | null;
17
+ data?: {
18
+ key: string;
19
+ value: string | null;
20
+ }[];
17
21
  message?: string;
18
22
  }>;
@@ -19,6 +19,22 @@ export async function browserStorage(session, options, _config) {
19
19
  });
20
20
  return { success: true, message: `Set ${scope}Storage key ${key}` };
21
21
  }
22
+ else if (action === "remove") {
23
+ if (!key)
24
+ throw new Error("browser_storage remove requires a 'key'");
25
+ await session.Runtime.evaluate({
26
+ expression: `(function(k){ ${scope}Storage.removeItem(k); })(${JSON.stringify(key)})`
27
+ });
28
+ return { success: true, message: `Removed ${scope}Storage key ${key}` };
29
+ }
30
+ else if (action === "list") {
31
+ const { result } = await session.Runtime.evaluate({
32
+ expression: `(function(){ var out = []; var s = ${scope}Storage; for (var i = 0; i < s.length; i++) { var k = s.key(i); out.push({ key: k, value: s.getItem(k) }); } return out; })()`,
33
+ returnByValue: true
34
+ });
35
+ const data = Array.isArray(result.value) ? result.value : [];
36
+ return { success: true, data, message: `${scope}Storage has ${data.length} key(s)` };
37
+ }
22
38
  else if (action === "clear") {
23
39
  await session.Runtime.evaluate({
24
40
  expression: `${scope}Storage.clear()`
@@ -18,11 +18,13 @@ export declare function WeavetabOpen(url: string, config: Config, reuse?: boolea
18
18
  reused?: boolean;
19
19
  }>;
20
20
  export declare function WeavetabSwitch(tabId: string, config: Config, appName?: string): Promise<{
21
- success: true;
22
- tab_id: string;
23
- title: string;
24
- url: string;
25
- contextSwitchCostMs: number;
21
+ success: boolean;
22
+ tab_id?: string;
23
+ title?: string;
24
+ url?: string;
25
+ contextSwitchCostMs?: number;
26
+ error_code?: string;
27
+ message?: string;
26
28
  }>;
27
29
  export declare function WeavetabList(config: Config, appName?: string): Promise<{
28
30
  tabs: TabInfo[];
@@ -109,14 +109,31 @@ async function enforceMaxTabs(config) {
109
109
  lockedTabIds.delete(victim.id);
110
110
  logAction("browser_tabs", victim.id, `auto-closed idle tab: ${victim.title}`);
111
111
  }
112
+ function normalizeUrl(u) {
113
+ try {
114
+ const parsed = new URL(u);
115
+ let path = parsed.pathname;
116
+ if (path.length > 1 && path.endsWith('/')) {
117
+ path = path.slice(0, -1);
118
+ }
119
+ return `${parsed.protocol}//${parsed.host.toLowerCase()}${path}${parsed.search}${parsed.hash}`.replace(/\/$/, "");
120
+ }
121
+ catch {
122
+ return u.trim().replace(/\/+$/, "").toLowerCase();
123
+ }
124
+ }
112
125
  // ─── Tools ────────────────────────────────────────────────────────────────────
113
126
  export async function WeavetabOpen(url, config, reuse = true, appName) {
114
- // Infer appName from active session if omitted
115
127
  let resolvedAppName = appName;
116
128
  if (!resolvedAppName) {
117
- const lastApp = sessionRegistry.getLastActiveApp();
118
- if (lastApp) {
119
- resolvedAppName = lastApp;
129
+ // Only inherit the sticky Electron app when no web-browser session is active
130
+ // (mirrors inferTargetApp()). Otherwise a plain `open` must target the browser:
131
+ // Electron targets cannot create new tabs via CDP (Target.createTarget fails).
132
+ if (!sessionRegistry.isBrowserSessionActive()) {
133
+ const lastApp = sessionRegistry.getLastActiveApp();
134
+ if (lastApp) {
135
+ resolvedAppName = lastApp;
136
+ }
120
137
  }
121
138
  }
122
139
  const port = resolvePort(config, resolvedAppName);
@@ -124,7 +141,8 @@ export async function WeavetabOpen(url, config, reuse = true, appName) {
124
141
  if (reuse && url) {
125
142
  try {
126
143
  const targets = await getTabList(getConfigAgentId(config), resolvedAppName);
127
- const existing = targets.find((t) => t.url === url);
144
+ const targetNorm = normalizeUrl(url);
145
+ const existing = targets.find((t) => normalizeUrl(t.url) === targetNorm);
128
146
  if (existing) {
129
147
  const client = await connectToTab(existing.id, undefined, undefined, undefined, resolvedAppName);
130
148
  await client.session.Target.activateTarget({ targetId: existing.id });
@@ -141,39 +159,74 @@ export async function WeavetabOpen(url, config, reuse = true, appName) {
141
159
  }
142
160
  const bctxId = resolvedAppName ? undefined : sessionRegistry.getBrowserContextIdForAgent(getConfigAgentId(config));
143
161
  const metaSession = await CDP({ host: "127.0.0.1", port });
144
- const { targetId } = await metaSession.Target.createTarget({ url, browserContextId: bctxId });
162
+ let targetId;
163
+ try {
164
+ ({ targetId } = await metaSession.Target.createTarget({ url, browserContextId: bctxId }));
165
+ }
166
+ catch (err) {
167
+ await metaSession.close().catch(() => { });
168
+ if (resolvedAppName) {
169
+ throw new Error(`[!] Electron app "${resolvedAppName}" does not support opening new tabs or windows via CDP ` +
170
+ `(Target.createTarget: ${err?.message || err}). Use browser_navigate with ` +
171
+ `{ app: "${resolvedAppName}", url: "${url}" } to load this URL in the app's existing window instead.`);
172
+ }
173
+ throw err;
174
+ }
145
175
  await metaSession.close();
146
176
  recordTabActivity(targetId);
147
- await connectToTab(targetId, port, getConfigAgentId(config), bctxId, appName);
148
- await evaluateTabSemantics(targetId, appName);
149
- logAction("browser_tabs", url, `opened ${targetId}`);
177
+ const client = await connectToTab(targetId, port, getConfigAgentId(config), bctxId, resolvedAppName);
178
+ try {
179
+ await client.session.Target.activateTarget({ targetId });
180
+ }
181
+ catch { /* non-fatal */ }
182
+ sessionRegistry.setActive(targetId, getConfigAgentId(config), resolvedAppName);
183
+ await evaluateTabSemantics(targetId, resolvedAppName);
184
+ logAction("browser_tabs", url, `opened and activated ${targetId}`);
150
185
  return { success: true, tabId: targetId, url };
151
186
  }
152
187
  export async function WeavetabSwitch(tabId, config, appName) {
153
- const client = await connectToTab(tabId, undefined, undefined, undefined, appName);
154
- const startSwitch = Date.now();
155
- await client.session.Target.activateTarget({ targetId: tabId });
156
- sessionRegistry.setActive(tabId, getConfigAgentId(config), appName);
157
- recordTabActivity(tabId);
158
- const { frameTree } = await client.session.Page.getFrameTree();
159
- let title = tabId;
160
188
  try {
161
- const titleRes = await client.session.Runtime.evaluate({ expression: 'document.title', returnByValue: true });
162
- if (titleRes?.result?.value) {
163
- title = titleRes.result.value;
189
+ const client = await connectToTab(tabId, undefined, undefined, undefined, appName);
190
+ const startSwitch = Date.now();
191
+ await client.session.Target.activateTarget({ targetId: tabId });
192
+ sessionRegistry.setActive(tabId, getConfigAgentId(config), appName);
193
+ recordTabActivity(tabId);
194
+ const { frameTree } = await client.session.Page.getFrameTree();
195
+ let title = client.title || "";
196
+ try {
197
+ const titleRes = await client.session.Runtime.evaluate({ expression: 'document.title', returnByValue: true });
198
+ if (titleRes?.result?.value) {
199
+ title = titleRes.result.value;
200
+ }
201
+ }
202
+ catch { }
203
+ const url = frameTree?.frame?.url || client.url || "";
204
+ if (!title) {
205
+ title = frameTree?.frame?.name || url || tabId;
164
206
  }
207
+ const cost = Date.now() - startSwitch;
208
+ logAction("browser_tabs", tabId, `switched to ${title} (cost: ${cost}ms)`);
209
+ resetLoopCounter("tab_switch");
210
+ return { success: true, tab_id: tabId, title, url, contextSwitchCostMs: cost };
211
+ }
212
+ catch (err) {
213
+ return {
214
+ success: false,
215
+ error_code: "TAB_NOT_FOUND",
216
+ message: `Failed to switch to tab ${tabId}: ${err?.message || err}`,
217
+ };
165
218
  }
166
- catch { }
167
- const url = frameTree.frame.url;
168
- const cost = Date.now() - startSwitch;
169
- logAction("browser_tabs", tabId, `switched to ${title} (cost: ${cost}ms)`);
170
- resetLoopCounter("tab_switch");
171
- return { success: true, tab_id: tabId, title, url, contextSwitchCostMs: cost };
172
219
  }
173
220
  export async function WeavetabList(config, appName) {
174
221
  const targets = await getTabList(getConfigAgentId(config), appName);
175
222
  const activeTargetId = sessionRegistry.getActive(getConfigAgentId(config), appName)?.targetId;
176
223
  const tabs = targets
224
+ .filter((t) => {
225
+ if (t.url && (t.url.startsWith("chrome-extension://") || t.url.includes("/hud/index.html"))) {
226
+ return false;
227
+ }
228
+ return true;
229
+ })
177
230
  .map((t) => ({
178
231
  id: t.id,
179
232
  title: t.title,
@@ -186,7 +239,7 @@ export async function WeavetabList(config, appName) {
186
239
  ? "locked"
187
240
  : isIdleTab(t.id, activeTargetId)
188
241
  ? "idle"
189
- : "loading",
242
+ : "alive",
190
243
  }));
191
244
  return { tabs };
192
245
  }
@@ -1,6 +1,6 @@
1
1
  // src/tools/thought.ts
2
2
  // browser_thought — agent-to-user communication via green thought bubbles
3
- // Merged: browser_feedback (one-way) + browser_ask (two-way with input)
3
+ // Merged: browser_feedback (one-way) + legacy browser_ask (two-way with input) into browser_thoughts
4
4
  import { logAction } from "../audit/logger.js";
5
5
  import { sendToExtension } from "../cdp/helpers.js";
6
6
  function sleep(ms) {
@@ -38,10 +38,10 @@ export async function browserThought(session, args) {
38
38
  await sleep(500);
39
39
  try {
40
40
  const { result } = await session.Runtime.evaluate({
41
- expression: `(() => { try {
42
- var el = document.getElementById('__wt_ask_bridge');
43
- if (!el || el.dataset.status === 'pending') return null;
44
- return JSON.stringify({ status: el.dataset.status, result: el.dataset.result });
41
+ expression: `(() => { try {
42
+ var el = document.getElementById('__wt_ask_bridge');
43
+ if (!el || el.dataset.status === 'pending') return null;
44
+ return JSON.stringify({ status: el.dataset.status, result: el.dataset.result });
45
45
  } catch(e) { return null; } })()`,
46
46
  returnByValue: true,
47
47
  });
@@ -50,9 +50,9 @@ export async function browserThought(session, args) {
50
50
  if (data.status === 'submitted') {
51
51
  logAction("browser_thought", "answered", data.result);
52
52
  await session.Runtime.evaluate({
53
- expression: `(function() {
54
- var el = document.getElementById('__wt_ask_bridge');
55
- if (el) el.remove();
53
+ expression: `(function() {
54
+ var el = document.getElementById('__wt_ask_bridge');
55
+ if (el) el.remove();
56
56
  })()`
57
57
  }).catch(() => { });
58
58
  return { success: true, response: data.result, status: "answered" };