@weavetab/mcp 2.5.0-beta.0

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 (281) hide show
  1. package/AGENTS.md +48 -0
  2. package/ARCHITECTURE.md +590 -0
  3. package/LICENSE +631 -0
  4. package/README.md +909 -0
  5. package/SECURITY.md +196 -0
  6. package/TROUBLESHOOTING.md +365 -0
  7. package/dist/anticaptcha/detector.d.ts +49 -0
  8. package/dist/anticaptcha/detector.js +465 -0
  9. package/dist/anticaptcha/integration.d.ts +36 -0
  10. package/dist/anticaptcha/integration.js +123 -0
  11. package/dist/anticaptcha/solver.d.ts +27 -0
  12. package/dist/anticaptcha/solver.js +552 -0
  13. package/dist/audit/logger.d.ts +13 -0
  14. package/dist/audit/logger.js +177 -0
  15. package/dist/audit/telemetry.d.ts +36 -0
  16. package/dist/audit/telemetry.js +162 -0
  17. package/dist/cdp/bridge.d.ts +16 -0
  18. package/dist/cdp/bridge.js +826 -0
  19. package/dist/cdp/confirm.d.ts +26 -0
  20. package/dist/cdp/confirm.js +79 -0
  21. package/dist/cdp/connector.d.ts +37 -0
  22. package/dist/cdp/connector.js +661 -0
  23. package/dist/cdp/extension.d.ts +36 -0
  24. package/dist/cdp/extension.js +118 -0
  25. package/dist/cdp/finder.d.ts +1 -0
  26. package/dist/cdp/finder.js +2 -0
  27. package/dist/cdp/helpers.d.ts +11 -0
  28. package/dist/cdp/helpers.js +195 -0
  29. package/dist/cdp/label_cache.d.ts +12 -0
  30. package/dist/cdp/label_cache.js +51 -0
  31. package/dist/cdp/lock.d.ts +33 -0
  32. package/dist/cdp/lock.js +206 -0
  33. package/dist/cdp/omni_broker.d.ts +17 -0
  34. package/dist/cdp/omni_broker.js +119 -0
  35. package/dist/cdp/profiles.d.ts +1 -0
  36. package/dist/cdp/profiles.js +87 -0
  37. package/dist/cdp/pruner.d.ts +41 -0
  38. package/dist/cdp/pruner.js +263 -0
  39. package/dist/cdp/ref_cache.d.ts +101 -0
  40. package/dist/cdp/ref_cache.js +234 -0
  41. package/dist/cdp/retry.d.ts +5 -0
  42. package/dist/cdp/retry.js +39 -0
  43. package/dist/cdp/retry_enhanced.d.ts +44 -0
  44. package/dist/cdp/retry_enhanced.js +102 -0
  45. package/dist/cdp/security.d.ts +50 -0
  46. package/dist/cdp/security.js +237 -0
  47. package/dist/cdp/verify.d.ts +39 -0
  48. package/dist/cdp/verify.js +143 -0
  49. package/dist/cdp/walker.d.ts +91 -0
  50. package/dist/cdp/walker.js +896 -0
  51. package/dist/cdp/walker.test.d.ts +2 -0
  52. package/dist/cdp/walker.test.js +172 -0
  53. package/dist/cdp/websocket_transport.d.ts +15 -0
  54. package/dist/cdp/websocket_transport.js +118 -0
  55. package/dist/cli/plugin.d.ts +9 -0
  56. package/dist/cli/plugin.js +1134 -0
  57. package/dist/cli/updater.d.ts +1 -0
  58. package/dist/cli/updater.js +72 -0
  59. package/dist/config/loader.d.ts +120 -0
  60. package/dist/config/loader.js +704 -0
  61. package/dist/index.d.ts +2 -0
  62. package/dist/index.js +1248 -0
  63. package/dist/intelligence/hints.d.ts +11 -0
  64. package/dist/intelligence/hints.js +39 -0
  65. package/dist/intelligence/intent.d.ts +33 -0
  66. package/dist/intelligence/intent.js +171 -0
  67. package/dist/intelligence/sessionMemory.d.ts +66 -0
  68. package/dist/intelligence/sessionMemory.js +284 -0
  69. package/dist/intelligence/sessionMemory.test.d.ts +2 -0
  70. package/dist/intelligence/sessionMemory.test.js +62 -0
  71. package/dist/intelligence/trail.d.ts +64 -0
  72. package/dist/intelligence/trail.js +233 -0
  73. package/dist/intelligence/trail.test.d.ts +2 -0
  74. package/dist/intelligence/trail.test.js +110 -0
  75. package/dist/intelligence/vision.d.ts +24 -0
  76. package/dist/intelligence/vision.js +151 -0
  77. package/dist/lifecycle/mission_manager.d.ts +16 -0
  78. package/dist/lifecycle/mission_manager.js +44 -0
  79. package/dist/overlay/bubble.d.ts +16 -0
  80. package/dist/overlay/bubble.js +37 -0
  81. package/dist/overlay/ghost.d.ts +35 -0
  82. package/dist/overlay/ghost.js +508 -0
  83. package/dist/overlay/injector.d.ts +22 -0
  84. package/dist/overlay/injector.js +198 -0
  85. package/dist/plugin/interface.d.ts +134 -0
  86. package/dist/plugin/interface.js +1 -0
  87. package/dist/plugin/loader.d.ts +18 -0
  88. package/dist/plugin/loader.js +155 -0
  89. package/dist/plugin/registry.d.ts +50 -0
  90. package/dist/plugin/registry.js +234 -0
  91. package/dist/security/blacklist.d.ts +24 -0
  92. package/dist/security/blacklist.js +80 -0
  93. package/dist/security/filter.d.ts +2 -0
  94. package/dist/security/filter.js +20 -0
  95. package/dist/security/guard.d.ts +8 -0
  96. package/dist/security/guard.js +35 -0
  97. package/dist/security/ratelimit.d.ts +11 -0
  98. package/dist/security/ratelimit.js +57 -0
  99. package/dist/security/secrets.d.ts +75 -0
  100. package/dist/security/secrets.js +365 -0
  101. package/dist/sensors/actionability.d.ts +81 -0
  102. package/dist/sensors/actionability.js +303 -0
  103. package/dist/sensors/guard.d.ts +25 -0
  104. package/dist/sensors/guard.js +168 -0
  105. package/dist/sensors/mutations.d.ts +23 -0
  106. package/dist/sensors/mutations.js +93 -0
  107. package/dist/sensors/network.d.ts +67 -0
  108. package/dist/sensors/network.js +193 -0
  109. package/dist/sensors/operation_monitor.d.ts +80 -0
  110. package/dist/sensors/operation_monitor.js +203 -0
  111. package/dist/sensors/pacing.d.ts +44 -0
  112. package/dist/sensors/pacing.js +111 -0
  113. package/dist/sensors/thoughts.d.ts +2 -0
  114. package/dist/sensors/thoughts.js +59 -0
  115. package/dist/sensors/wait.d.ts +12 -0
  116. package/dist/sensors/wait.js +44 -0
  117. package/dist/server.d.ts +7 -0
  118. package/dist/server.js +1687 -0
  119. package/dist/state/agent.d.ts +36 -0
  120. package/dist/state/agent.js +101 -0
  121. package/dist/state/cache.d.ts +16 -0
  122. package/dist/state/cache.js +92 -0
  123. package/dist/state/cache.test.d.ts +2 -0
  124. package/dist/state/cache.test.js +76 -0
  125. package/dist/state/delta.d.ts +71 -0
  126. package/dist/state/delta.js +234 -0
  127. package/dist/state/delta.test.d.ts +2 -0
  128. package/dist/state/delta.test.js +279 -0
  129. package/dist/state/groups.d.ts +15 -0
  130. package/dist/state/groups.js +80 -0
  131. package/dist/state/session.d.ts +30 -0
  132. package/dist/state/session.js +155 -0
  133. package/dist/tools/accessibility.d.ts +14 -0
  134. package/dist/tools/accessibility.js +32 -0
  135. package/dist/tools/ask.d.ts +14 -0
  136. package/dist/tools/ask.js +49 -0
  137. package/dist/tools/automation.d.ts +36 -0
  138. package/dist/tools/automation.js +218 -0
  139. package/dist/tools/browser_detect.d.ts +13 -0
  140. package/dist/tools/browser_detect.js +73 -0
  141. package/dist/tools/burst.d.ts +35 -0
  142. package/dist/tools/burst.js +344 -0
  143. package/dist/tools/canvas.d.ts +25 -0
  144. package/dist/tools/canvas.js +132 -0
  145. package/dist/tools/captcha.d.ts +38 -0
  146. package/dist/tools/captcha.js +209 -0
  147. package/dist/tools/click.d.ts +36 -0
  148. package/dist/tools/click.js +561 -0
  149. package/dist/tools/click_and_wait.d.ts +33 -0
  150. package/dist/tools/click_and_wait.js +149 -0
  151. package/dist/tools/clipboard.d.ts +29 -0
  152. package/dist/tools/clipboard.js +279 -0
  153. package/dist/tools/console.d.ts +21 -0
  154. package/dist/tools/console.js +107 -0
  155. package/dist/tools/cookies.d.ts +67 -0
  156. package/dist/tools/cookies.js +121 -0
  157. package/dist/tools/design_extract.d.ts +109 -0
  158. package/dist/tools/design_extract.js +641 -0
  159. package/dist/tools/dialog.d.ts +24 -0
  160. package/dist/tools/dialog.js +109 -0
  161. package/dist/tools/drag.d.ts +15 -0
  162. package/dist/tools/drag.js +29 -0
  163. package/dist/tools/emulation.d.ts +33 -0
  164. package/dist/tools/emulation.js +69 -0
  165. package/dist/tools/eval.d.ts +20 -0
  166. package/dist/tools/eval.js +92 -0
  167. package/dist/tools/fill.d.ts +41 -0
  168. package/dist/tools/fill.js +667 -0
  169. package/dist/tools/find.d.ts +30 -0
  170. package/dist/tools/find.js +60 -0
  171. package/dist/tools/geo.d.ts +19 -0
  172. package/dist/tools/geo.js +29 -0
  173. package/dist/tools/github/analyze.d.ts +4 -0
  174. package/dist/tools/github/analyze.js +113 -0
  175. package/dist/tools/github/cache.d.ts +21 -0
  176. package/dist/tools/github/cache.js +28 -0
  177. package/dist/tools/github/index.d.ts +4 -0
  178. package/dist/tools/github/index.js +4 -0
  179. package/dist/tools/github/issues.d.ts +9 -0
  180. package/dist/tools/github/issues.js +88 -0
  181. package/dist/tools/github/pr.d.ts +5 -0
  182. package/dist/tools/github/pr.js +88 -0
  183. package/dist/tools/github/rateLimiter.d.ts +4 -0
  184. package/dist/tools/github/rateLimiter.js +46 -0
  185. package/dist/tools/github/read.d.ts +34 -0
  186. package/dist/tools/github/read.js +74 -0
  187. package/dist/tools/github/repoContext.d.ts +20 -0
  188. package/dist/tools/github/repoContext.js +129 -0
  189. package/dist/tools/highlight.d.ts +20 -0
  190. package/dist/tools/highlight.js +140 -0
  191. package/dist/tools/hover.d.ts +19 -0
  192. package/dist/tools/hover.js +57 -0
  193. package/dist/tools/inspect.d.ts +17 -0
  194. package/dist/tools/inspect.js +125 -0
  195. package/dist/tools/integration.test.d.ts +2 -0
  196. package/dist/tools/integration.test.js +96 -0
  197. package/dist/tools/key.d.ts +25 -0
  198. package/dist/tools/key.js +173 -0
  199. package/dist/tools/macro_compiler.d.ts +7 -0
  200. package/dist/tools/macro_compiler.js +82 -0
  201. package/dist/tools/mouse.d.ts +21 -0
  202. package/dist/tools/mouse.js +62 -0
  203. package/dist/tools/navigate.d.ts +16 -0
  204. package/dist/tools/navigate.js +188 -0
  205. package/dist/tools/network_intercept.d.ts +17 -0
  206. package/dist/tools/network_intercept.js +151 -0
  207. package/dist/tools/office/index.d.ts +9 -0
  208. package/dist/tools/office/index.js +7 -0
  209. package/dist/tools/office/list.d.ts +15 -0
  210. package/dist/tools/office/list.js +41 -0
  211. package/dist/tools/office/parse.d.ts +20 -0
  212. package/dist/tools/office/parse.js +172 -0
  213. package/dist/tools/pdf.d.ts +22 -0
  214. package/dist/tools/pdf.js +42 -0
  215. package/dist/tools/performance.d.ts +12 -0
  216. package/dist/tools/performance.js +47 -0
  217. package/dist/tools/performance_test.d.ts +70 -0
  218. package/dist/tools/performance_test.js +128 -0
  219. package/dist/tools/plan.d.ts +39 -0
  220. package/dist/tools/plan.js +161 -0
  221. package/dist/tools/pointer.d.ts +35 -0
  222. package/dist/tools/pointer.js +522 -0
  223. package/dist/tools/read.d.ts +30 -0
  224. package/dist/tools/read.js +488 -0
  225. package/dist/tools/recording.d.ts +45 -0
  226. package/dist/tools/recording.js +124 -0
  227. package/dist/tools/response.d.ts +129 -0
  228. package/dist/tools/response.js +5 -0
  229. package/dist/tools/scrape.d.ts +24 -0
  230. package/dist/tools/scrape.js +140 -0
  231. package/dist/tools/screenshot.d.ts +40 -0
  232. package/dist/tools/screenshot.js +79 -0
  233. package/dist/tools/scroll.d.ts +23 -0
  234. package/dist/tools/scroll.js +196 -0
  235. package/dist/tools/select.d.ts +13 -0
  236. package/dist/tools/select.js +217 -0
  237. package/dist/tools/snapshot.d.ts +27 -0
  238. package/dist/tools/snapshot.js +99 -0
  239. package/dist/tools/storage.d.ts +18 -0
  240. package/dist/tools/storage.js +29 -0
  241. package/dist/tools/tabs.d.ts +77 -0
  242. package/dist/tools/tabs.js +342 -0
  243. package/dist/tools/thought.d.ts +16 -0
  244. package/dist/tools/thought.js +79 -0
  245. package/dist/tools/type.d.ts +65 -0
  246. package/dist/tools/type.js +1061 -0
  247. package/dist/tools/type_secret.d.ts +23 -0
  248. package/dist/tools/type_secret.js +229 -0
  249. package/dist/tools/upload.d.ts +19 -0
  250. package/dist/tools/upload.js +103 -0
  251. package/dist/tools/video_compiler.d.ts +24 -0
  252. package/dist/tools/video_compiler.js +199 -0
  253. package/dist/tools/viewport.d.ts +44 -0
  254. package/dist/tools/viewport.js +230 -0
  255. package/dist/tools/wait.d.ts +19 -0
  256. package/dist/tools/wait.js +202 -0
  257. package/dist/tools/wait_for.d.ts +23 -0
  258. package/dist/tools/wait_for.js +30 -0
  259. package/dist/tools/windows.d.ts +22 -0
  260. package/dist/tools/windows.js +57 -0
  261. package/dist/ui/index.d.ts +1 -0
  262. package/dist/ui/index.js +12 -0
  263. package/docs/DEV_MODE.md +68 -0
  264. package/docs/DOCUMENTATION.md +532 -0
  265. package/docs/PLUGINS.md +538 -0
  266. package/docs/TOOLS.md +104 -0
  267. package/docs/dev_plugin.md +310 -0
  268. package/ext-dist/background.js +467 -0
  269. package/ext-dist/content.js +3089 -0
  270. package/ext-dist/dashboard.html +583 -0
  271. package/ext-dist/hud/hud.js +73 -0
  272. package/ext-dist/hud/index.html +20 -0
  273. package/ext-dist/manifest.json +77 -0
  274. package/ext-dist/sidepanel/sidepanel.js +25 -0
  275. package/ext-dist/sidepanel.html +547 -0
  276. package/ext-dist/styles.css +218 -0
  277. package/icon.png +0 -0
  278. package/mcp.json +796 -0
  279. package/package.json +113 -0
  280. package/server.json +23 -0
  281. package/smithery.yaml +11 -0
@@ -0,0 +1,488 @@
1
+ import { logDebug } from "../audit/logger.js";
2
+ import { buildActionMap, toRefEntries, setShadowDomCache, buildAXTreeMap } from "../cdp/walker.js";
3
+ import { logAction } from "../audit/logger.js";
4
+ import { storeSnapshot, computeDelta, computeRichDelta, seedRichSnapshot } from "../state/delta.js";
5
+ import { readKnowledgeHint } from "../intelligence/hints.js";
6
+ import { saveDomainProfile, detectFramework, detectShadowDom, formatDomainHint } from "../intelligence/sessionMemory.js";
7
+ import { sessionRegistry } from "../state/session.js";
8
+ import { cacheActionMap, getCachedActionMap, getRawCachedActionMap } from "../state/cache.js";
9
+ import { startReadingSimulation } from "../overlay/ghost.js";
10
+ import { suppressOverlays, restoreOverlays } from "../cdp/extension.js";
11
+ import { sendToExtension } from "../cdp/helpers.js";
12
+ import { analyzeVisualFallback } from "../intelligence/vision.js";
13
+ import { getConfigAgentId } from "../config/loader.js";
14
+ // Cap at 80 elements; form/modal groups are always kept, utility trimmed last.
15
+ const MAX_ELEMENTS = 80;
16
+ function withTimeout(p, ms) {
17
+ return Promise.race([
18
+ p,
19
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`browserMap timed out after ${ms}ms`)), ms)),
20
+ ]);
21
+ }
22
+ function formatLite(el, activeElementId, changedIds) {
23
+ const parts = [];
24
+ // ── State badges ────────────────────────────────────────────────────────────
25
+ if (el.disabled)
26
+ parts.push("disabled");
27
+ if (activeElementId && el.id === activeElementId)
28
+ parts.push("FOCUSED");
29
+ if (el.viewport && el.viewport !== "visible")
30
+ parts.push(`offscreen:${el.viewport}`);
31
+ // ── Value / checked / placeholder ──────────────────────────────────────────
32
+ const role = (el.role || '').toLowerCase();
33
+ const type = (el.type || '').toLowerCase();
34
+ // Radio / checkbox checked state — critical for quiz forms
35
+ if ((role === 'radio' || role === 'checkbox' || type === 'radio' || type === 'checkbox') && el.value !== undefined) {
36
+ parts.push(el.value === 'true' || el.value === 'checked' ? '✓ checked' : '○ unchecked');
37
+ }
38
+ else if (el.value) {
39
+ // Truncate long values to avoid token waste
40
+ const v = el.value.length > 60 ? el.value.slice(0, 57) + '…' : el.value;
41
+ parts.push(`"${v}"`);
42
+ }
43
+ else if (el.placeholder) {
44
+ parts.push(`placeholder: "${el.placeholder}"`);
45
+ }
46
+ // ── Type suffix for inputs (e.g. input[email], input[date]) ────────────────
47
+ const typeLabel = (role === 'textbox' || role === 'searchbox' || role === 'combobox') && type
48
+ ? `/${type}` : '';
49
+ // ── Link hint ───────────────────────────────────────────────────────────────
50
+ if (role === 'link' && el.hrefType && el.hrefType !== 'same-origin') {
51
+ parts.push(el.hrefType);
52
+ }
53
+ const state = parts.length > 0 ? ` (${parts.join(', ')})` : '';
54
+ const changed = changedIds?.has(el.id) ? ' ★' : '';
55
+ return `[${el.id}] ${el.role}${typeLabel} "${el.label}"${state}${changed}`;
56
+ }
57
+ /** Full markdown table mode — only used when full:true is explicitly passed */
58
+ function formatFull(elements, url, title, hint) {
59
+ const lines = [];
60
+ if (hint)
61
+ lines.push(`> ${hint}\n`);
62
+ lines.push(`**${title || url}** — ${url}\n`);
63
+ for (const el of elements) {
64
+ const role = el.role || '';
65
+ const typeStr = el.type ? `/${el.type}` : '';
66
+ const roleTag = `${role}${typeStr}`;
67
+ const ref = el.id ? ` [${el.id}]` : '';
68
+ const rLow = role.toLowerCase();
69
+ const tLow = (el.type || '').toLowerCase();
70
+ let state = '';
71
+ if (rLow === 'radio' || rLow === 'checkbox' || tLow === 'radio' || tLow === 'checkbox') {
72
+ state = el.value === 'true' || el.value === 'checked' ? ' ✓' : ' ○';
73
+ }
74
+ else if (el.value) {
75
+ state = ` value="${el.value}"`;
76
+ }
77
+ else if (el.placeholder) {
78
+ state = ` placeholder="${el.placeholder}"`;
79
+ }
80
+ if (!el.id && !el.label.trim())
81
+ continue;
82
+ lines.push(`(${roleTag}) "${el.label}"${state}${ref}`);
83
+ }
84
+ return lines.join('\n');
85
+ }
86
+ export async function browserMap(session, options = {}, targetId = "default", config) {
87
+ logDebug(`[browserMap] Initiating read for targetId: ${targetId}, options: ${JSON.stringify(options)}`);
88
+ // ── Wait for DOM and Visual Stability ──────────────────────────────────────
89
+ try {
90
+ await session.Runtime.evaluate({
91
+ expression: `
92
+ new Promise(resolve => {
93
+ if (document.readyState === 'complete') {
94
+ setTimeout(resolve, 300); // 300ms visual stability delay
95
+ } else {
96
+ window.addEventListener('load', () => setTimeout(resolve, 300));
97
+ }
98
+ })
99
+ `,
100
+ awaitPromise: true
101
+ });
102
+ }
103
+ catch (e) {
104
+ logDebug(`[browserMap] Stability wait failed: ${e}`);
105
+ }
106
+ // Signal reading state via HUD only
107
+ sendToExtension(session, { type: 'Weavetab_STATE', hud: { action: 'reading the page...' } }).catch(() => { });
108
+ // Set sticky pruning state if provided, otherwise preserve existing or default to false
109
+ const client = sessionRegistry.getAll().find(c => c.targetId === targetId);
110
+ if (client && options.prune !== undefined) {
111
+ client.pruneActive = options.prune;
112
+ }
113
+ // ── Overlays: default to visible so the user can see the agent's focus ──
114
+ if (options.show_overlays === false) {
115
+ await suppressOverlays(session);
116
+ }
117
+ else {
118
+ await restoreOverlays(session);
119
+ }
120
+ // ── Build element map: DOM walker or AX tree ─────────────────────────────
121
+ let map = null;
122
+ let usedAXTree = false;
123
+ // Check cache first if NOT forcing an AX Tree read explicitly
124
+ if (!options.ax_tree) {
125
+ let currentUrl;
126
+ try {
127
+ const { frameTree } = await session.Page.getFrameTree();
128
+ currentUrl = frameTree.frame.url;
129
+ }
130
+ catch { }
131
+ // ── Compute fast DOM hash ──────────────────────────────────────────────────
132
+ let domHash = 0;
133
+ try {
134
+ const { result } = await session.Runtime.evaluate({
135
+ expression: 'document.body ? document.body.innerHTML.length : 0',
136
+ returnByValue: true
137
+ });
138
+ domHash = result.value ?? 0;
139
+ }
140
+ catch (e) {
141
+ logDebug(`[browserMap] Failed to compute DOM hash: ${e}`);
142
+ }
143
+ // Try strictly validated cache first
144
+ const cachedMap = getCachedActionMap(targetId, currentUrl);
145
+ if (cachedMap) {
146
+ logDebug(`[browserMap] Cache Hit (strict)! Returning cached action map with ${cachedMap.elements.length} elements.`);
147
+ map = cachedMap;
148
+ }
149
+ else {
150
+ // Fallback: check raw cache with domHash
151
+ const rawMap = getRawCachedActionMap(targetId, currentUrl);
152
+ if (rawMap && rawMap.domHash === domHash) {
153
+ logDebug(`[browserMap] Cache Hit (domHash=${domHash})! MutationObserver invalidated but DOM content length unchanged.`);
154
+ map = rawMap;
155
+ }
156
+ }
157
+ }
158
+ if (!map) {
159
+ if (options.ax_tree) {
160
+ // Explicit AX tree mode — skips DOM walker entirely
161
+ map = await buildAXTreeMap(session, {
162
+ scope: options.scope,
163
+ query: options.query,
164
+ limit: options.limit,
165
+ }, targetId);
166
+ usedAXTree = map.elements.length > 0;
167
+ logDebug(`[browserMap] AX tree mode: ${map.elements.length} elements`);
168
+ }
169
+ else {
170
+ const _emptyMap = { url: '', title: '', elements: [], targetId };
171
+ // DOM-based walker (standard path) — 8s timeout prevents hanging on heavy SPAs
172
+ map = await withTimeout(buildActionMap(session, {
173
+ scope: options.scope,
174
+ query: options.query,
175
+ limit: options.limit,
176
+ deep: options.deep,
177
+ prune: options.prune,
178
+ }, targetId), 8000).catch(() => _emptyMap);
179
+ logDebug(`[browserMap] buildActionMap completed. Elements found: ${map.elements.length}`);
180
+ // Auto-fallback to AX tree when DOM walker returns sparse or unlabeled-heavy results
181
+ if (map.elements.length > 0 && map.elements.length < 3) {
182
+ const axMap = await withTimeout(buildAXTreeMap(session, {}, targetId), 6000).catch(() => _emptyMap);
183
+ if (axMap.elements.length > map.elements.length) {
184
+ map = axMap;
185
+ usedAXTree = true;
186
+ logDebug(`[browserMap] AX tree fallback (sparse DOM): ${map.elements.length} elements`);
187
+ }
188
+ }
189
+ else if (map.elements.length > 0) {
190
+ const total = map.elements.length;
191
+ const unlabeled = map.elements.filter(e => !e.label && !e.value && !e.placeholder).length;
192
+ if (unlabeled / total > 0.5) {
193
+ const axMap = await withTimeout(buildAXTreeMap(session, {}, targetId), 6000).catch(() => _emptyMap);
194
+ if (axMap.elements.length > 0) {
195
+ map = axMap;
196
+ usedAXTree = true;
197
+ logDebug(`[browserMap] AX tree fallback (>50% unlabeled): ${map.elements.length} elements`);
198
+ }
199
+ }
200
+ }
201
+ // Record AX tree strategy in domain profile for future sessions
202
+ if (usedAXTree) {
203
+ try {
204
+ const hostname = new URL(map.url).hostname;
205
+ await saveDomainProfile(hostname, { strategies: ["axtree"] }).catch(() => { });
206
+ }
207
+ catch { /* best-effort */ }
208
+ }
209
+ }
210
+ try {
211
+ // Request document first to prevent CDP error
212
+ await session.DOM.getDocument({ depth: -1 }).catch(() => { });
213
+ // Calculate domHash if not already calculated (e.g. from AX tree fallback)
214
+ try {
215
+ const { result } = await session.Runtime.evaluate({
216
+ expression: 'document.body ? document.body.innerHTML.length : 0',
217
+ returnByValue: true
218
+ });
219
+ map.domHash = result.value ?? 0;
220
+ }
221
+ catch (e) { }
222
+ // Never write an AX-tree result into the primary DOM map cache.
223
+ // AX tree uses sequential w:1…w:N IDs; DOM walker uses stable content-addressable hashes.
224
+ // Caching the AX result would make the next plain browser_map() call return AX-tree refs,
225
+ // causing different tools to see different ref numbering for the same element.
226
+ if (!usedAXTree) {
227
+ cacheActionMap(targetId, map);
228
+ }
229
+ logAction("browser_map", map.url, `${map.elements.length} elements`);
230
+ // Auto-learn domain profile (non-blocking)
231
+ try {
232
+ const hostname = new URL(map.url).hostname;
233
+ const [framework, hasShadowDom] = await Promise.all([
234
+ detectFramework(session).catch(() => undefined),
235
+ detectShadowDom(session).catch(() => false),
236
+ ]);
237
+ if (framework || hasShadowDom) {
238
+ await saveDomainProfile(hostname, {
239
+ ...(framework ? { framework } : {}),
240
+ ...(hasShadowDom ? { shadowDom: true } : {}),
241
+ }).catch(() => { });
242
+ }
243
+ // Proactively seed shadow DOM cache for next browser_map call
244
+ if (hasShadowDom) {
245
+ setShadowDomCache(targetId, true);
246
+ }
247
+ }
248
+ catch { /* domain profile is best-effort */ }
249
+ // Filter out unresolvable nodes
250
+ const backendIds = map.elements.map(e => e.backendNodeId).filter((id) => id !== undefined);
251
+ logDebug(`[browserMap] Resolving ${backendIds.length} backend node IDs.`);
252
+ const resolved = await session.DOM.pushNodesByBackendIdsToFrontend({
253
+ backendNodeIds: backendIds
254
+ });
255
+ if (resolved.nodeIds) {
256
+ logDebug(`[browserMap] Resolved ${resolved.nodeIds.length} node IDs.`);
257
+ const validIds = new Set(resolved.nodeIds.filter(id => id !== 0));
258
+ map.elements = map.elements.filter(el => {
259
+ if (el.backendNodeId === undefined)
260
+ return true;
261
+ const nodeId = resolved.nodeIds[map.elements.indexOf(el)];
262
+ return nodeId !== undefined && validIds.has(nodeId);
263
+ });
264
+ logDebug(`[browserMap] Elements after resolution filter: ${map.elements.length}`);
265
+ }
266
+ else {
267
+ logDebug(`[browserMap] No node IDs resolved.`);
268
+ }
269
+ }
270
+ catch (e) {
271
+ logDebug(`[browserMap] Error during read: ${e}`);
272
+ throw e;
273
+ }
274
+ }
275
+ const newRefs = toRefEntries(map.elements);
276
+ const tabs = sessionRegistry.getAll();
277
+ const activeTabId = sessionRegistry.getActive(getConfigAgentId(config))?.targetId ?? targetId;
278
+ // ── Visual fallback when AX tree is empty ──────────────────────────────────
279
+ let visualFallback = null;
280
+ if (map.elements.length === 0) {
281
+ try {
282
+ // Canvas detection — check before full screenshot analysis
283
+ const { result: canvasCheck } = await session.Runtime.evaluate({
284
+ expression: `(function() {
285
+ const canvases = [...document.querySelectorAll('canvas')];
286
+ return JSON.stringify(canvases.map(c => {
287
+ const rect = c.getBoundingClientRect();
288
+ return {
289
+ id: c.id || '', width: Math.round(rect.width), height: Math.round(rect.height),
290
+ x: Math.round(rect.left + rect.width / 2),
291
+ y: Math.round(rect.top + rect.height / 2),
292
+ hasWebGL: !!(c.getContext('webgl') || c.getContext('webgl2'))
293
+ };
294
+ }));
295
+ })()`,
296
+ returnByValue: true,
297
+ }).catch(() => ({ result: { value: '[]' } }));
298
+ const canvases = JSON.parse(canvasCheck.value ?? '[]');
299
+ if (canvases.length > 0) {
300
+ for (let i = 0; i < canvases.length; i++) {
301
+ const c = canvases[i];
302
+ if (c.width > 0 && c.height > 0) {
303
+ map.elements.push({
304
+ id: `w:canvas${i}`,
305
+ role: "canvas",
306
+ label: `${c.hasWebGL ? 'WebGL ' : ''}Canvas ${c.width}x${c.height}${c.id ? ` #${c.id}` : ''}`,
307
+ group: "main_content",
308
+ fingerprint: `canvas:${i}:${c.id}`,
309
+ depth: 0,
310
+ x: c.x,
311
+ y: c.y,
312
+ selector: c.id ? `#${c.id}` : `canvas:nth-of-type(${i + 1})`
313
+ });
314
+ }
315
+ }
316
+ }
317
+ if (map.elements.length === 0) {
318
+ const vf = await analyzeVisualFallback(session);
319
+ const zones = vf.clickable_zones
320
+ .map((z) => ` coords(${z.x},${z.y})`)
321
+ .join("\n");
322
+ visualFallback =
323
+ `\nVISUAL FALLBACK (AX tree empty)\n` +
324
+ ` page_state: ${vf.page_state}\n` +
325
+ ` text_density: ${vf.text_density}%\n` +
326
+ ` heatmap: ${vf.content_heatmap}\n` +
327
+ (zones ? ` clickable_zones:\n${zones}\n` : " no clickable zones detected\n");
328
+ }
329
+ }
330
+ catch {
331
+ visualFallback = "\nVISUAL FALLBACK: unavailable\n";
332
+ }
333
+ }
334
+ // If we just added canvases, we need to update newRefs
335
+ if (map.elements.length > 0 && newRefs.length === 0) {
336
+ newRefs.push(...toRefEntries(map.elements));
337
+ }
338
+ // ── Delta mode ─────────────────────────────────────────────────────────────
339
+ if (options.delta) {
340
+ const richDiff = await computeRichDelta(targetId, map.url, map.elements);
341
+ if (richDiff) {
342
+ if ("delta_timeout" in richDiff) {
343
+ // Delta mode timed out — fall through to full read
344
+ }
345
+ else {
346
+ const lines = [];
347
+ const header = `Page: ${map.title || map.url} | ${map.elements.length} elements`;
348
+ lines.push(header);
349
+ if (richDiff.added.length === 0 && richDiff.changed.length === 0 && richDiff.removed.length === 0 && (!richDiff.iframes || richDiff.iframes.length === 0)) {
350
+ lines.push("stable — no changes detected");
351
+ }
352
+ else {
353
+ for (const r of richDiff.added)
354
+ lines.push(`+ [${r.id}] ${r.role} "${r.label}"`);
355
+ for (const r of richDiff.changed) {
356
+ const changes = Object.keys(r.changes);
357
+ const changeHints = changes.map(c => `${c}: "${r.changes[c].from}" → "${r.changes[c].to}"`).join(', ');
358
+ const actionable = r.actionable ? ` ⚡${r.recommended_action ? ` ${r.recommended_action}` : ''}` : '';
359
+ lines.push(`~ [${r.ref}] ${r.role} "${r.label}"${changeHints ? ` (${changeHints})` : ''}${actionable}`);
360
+ }
361
+ for (const id of richDiff.removed)
362
+ lines.push(`- ${id}`);
363
+ if (richDiff.iframes) {
364
+ for (const f of richDiff.iframes) {
365
+ const icon = f.status === "added" ? "+" : f.status === "removed" ? "-" : "~";
366
+ lines.push(`${icon} [iframe] ${f.frameUrl} (${f.status}, ${f.elementCount} elements)`);
367
+ }
368
+ }
369
+ }
370
+ if (visualFallback)
371
+ lines.push(visualFallback);
372
+ return lines.join("\n");
373
+ }
374
+ }
375
+ // No prior snapshot → fall through to full read
376
+ }
377
+ storeSnapshot(targetId, map.url, newRefs);
378
+ seedRichSnapshot(targetId, map.url, map.elements);
379
+ // JIT knowledge hint + session memory
380
+ let hint;
381
+ try {
382
+ const hostname = new URL(map.url).hostname;
383
+ const [knowledge, domainProfile] = await Promise.all([
384
+ readKnowledgeHint(hostname),
385
+ import("../intelligence/sessionMemory.js").then(m => m.loadDomainProfile(hostname)),
386
+ ]);
387
+ const memoryHint = domainProfile ? formatDomainHint(hostname, domainProfile) : undefined;
388
+ hint = [knowledge, memoryHint].filter(Boolean).join('\n') || undefined;
389
+ }
390
+ catch { /* ignore */ }
391
+ // ── Full table mode (explicit full:true only) ───────────────────────────────
392
+ if (options.full) {
393
+ const markdown = formatFull(map.elements, map.url, map.title, hint);
394
+ const wordCount = markdown.split(/\s+/).length;
395
+ startReadingSimulation(session, wordCount, config);
396
+ const tabLine = `⬡ ${tabs.length} tab(s) | active: ${activeTabId}`;
397
+ return tabLine + "\n\n" + markdown + (visualFallback ?? "");
398
+ }
399
+ // ── Prioritised element capping ──────────────────────────────────────────────
400
+ // Keep form/modal elements at all costs (they're the most actionable).
401
+ // Trim utility/nav from the end if we're over the cap.
402
+ let capped;
403
+ if (map.elements.length > MAX_ELEMENTS) {
404
+ const priority = map.elements.filter(e => e.group === 'form' || e.group === 'modal');
405
+ const secondary = map.elements.filter(e => e.group === 'main_content' || e.group === 'sidebar');
406
+ const tertiary = map.elements.filter(e => e.group === 'navigation' || e.group === 'utility');
407
+ capped = [...priority, ...secondary, ...tertiary].slice(0, MAX_ELEMENTS);
408
+ }
409
+ else {
410
+ capped = map.elements;
411
+ }
412
+ const overflowCount = map.elements.length - capped.length;
413
+ const lines = [];
414
+ if (hint)
415
+ lines.push(`> ${hint}`);
416
+ // Cure optional-login blindness by explicitly pointing out dismiss buttons
417
+ const dismissButton = map.elements.find(el => /(not now|skip|close|dismiss|no thanks|^x$)/i.test(el.label) &&
418
+ /(button|link)/i.test(el.role));
419
+ if (dismissButton) {
420
+ lines.push(`\n🚨 [POPUP DETECTED]: If an optional modal/login is blocking the page, try clicking [${dismissButton.id}] ("${dismissButton.label}") to dismiss it.\n`);
421
+ }
422
+ const changed = computeDelta(targetId, map.url, newRefs);
423
+ const changedIds = new Set(changed ? [...changed.added.map((r) => r.id), ...changed.changed.map((r) => r.id)] : []);
424
+ const beforePruneCount = map.elements.length;
425
+ lines.push(`Page: ${map.title || map.url} | ${map.elements.length} interactive elements` +
426
+ (changedIds.size > 0 ? ` | ${changedIds.size} changed` : "") +
427
+ (options.prune && beforePruneCount !== map.elements.length
428
+ ? ` (pruned ${beforePruneCount - map.elements.length})`
429
+ : ""));
430
+ lines.push("");
431
+ // ── Semantic section grouping ────────────────────────────────────────────────
432
+ const SECTION_LABEL = {
433
+ navigation: "── NAV ──────────────────",
434
+ main_content: "── MAIN ─────────────────",
435
+ form: "── FORM ─────────────────",
436
+ sidebar: "── SIDEBAR ──────────────",
437
+ modal: "── MODAL ────────────────",
438
+ utility: "── OTHER ────────────────",
439
+ };
440
+ let activeElementId;
441
+ try {
442
+ const { result } = await session.Runtime.evaluate({
443
+ expression: `(function() {
444
+ const ae = document.activeElement;
445
+ if (!ae || ae === document.body) return null;
446
+ return ae.getAttribute('data-wt-id') || null;
447
+ })()`,
448
+ returnByValue: true
449
+ });
450
+ if (result && result.value) {
451
+ activeElementId = 'w:' + result.value;
452
+ }
453
+ }
454
+ catch { }
455
+ let currentSection = null;
456
+ for (const el of capped) {
457
+ if (el.group !== currentSection) {
458
+ currentSection = el.group;
459
+ lines.push(SECTION_LABEL[currentSection]);
460
+ }
461
+ lines.push(formatLite(el, activeElementId, changedIds));
462
+ }
463
+ if (overflowCount > 0) {
464
+ lines.push(`(+${overflowCount} more below fold — call browser_scroll down to reveal)`);
465
+ }
466
+ if (visualFallback)
467
+ lines.push(visualFallback);
468
+ // Context-scoped tool hint — collapse 34-tool surface to ~12 relevant tools.
469
+ // Detected from map elements already computed above — zero extra latency.
470
+ const hasForm = map.elements.some(e => e.group === "form" || e.role === "input" || e.role === "combobox" || e.role === "checkbox" || e.role === "radio");
471
+ const hasSelect = map.elements.some(e => e.role === "combobox" || e.role === "listbox");
472
+ const hasCanvas = map.url?.includes("canvas") || false;
473
+ const isGithub = map.url?.includes("github.com") ?? false;
474
+ const baseTools = ["browser_click", "browser_type", "browser_key", "browser_scroll", "browser_wait", "browser_navigate", "browser_tabs"];
475
+ const formTools = hasForm ? ["browser_select", "browser_type_secret", "browser_find"] : [];
476
+ const githubTools = isGithub ? ["github_analyze", "github_read", "github_issues", "github_get_pr"] : [];
477
+ const canvasTools = hasCanvas ? ["browser_canvas", "browser_screenshot"] : ["browser_screenshot"];
478
+ const diagTools = ["browser_inspect", "browser_highlight", "browser_scrape", "browser_burst"];
479
+ const contextTools = [...new Set([...baseTools, ...formTools, ...githubTools, ...canvasTools, ...diagTools])];
480
+ lines.push(`\n_tools [${contextTools.join(", ")}]`);
481
+ const output = lines.join("\n");
482
+ const wordCount = output.split(/\s+/).length;
483
+ startReadingSimulation(session, wordCount, config);
484
+ sendToExtension(session, { type: 'Weavetab_STATE', hud: { action: '' } }).catch(() => { });
485
+ return output;
486
+ }
487
+ // Keep backward-compat alias used by plan.ts
488
+ export { browserMap as weaveRead };
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Weavetab — tools/recording.ts
3
+ * browser_recording: Session recording (screencast) for compliance review.
4
+ * Uses CDP Page.startScreencast at 2-5 FPS and compiles frames into video (.webp, .gif, .webm, .mp4).
5
+ * Weavetab by fy2ne
6
+ */
7
+ import CDP from "chrome-remote-interface";
8
+ import type { Config } from "../config/loader.js";
9
+ export interface RecordingOptions {
10
+ action: "start" | "stop" | "list";
11
+ sessionId?: string;
12
+ fps?: number;
13
+ maxFrames?: number;
14
+ /** Output video/animation format when stopping recording.
15
+ * Options: 'webp' (default pure Node animation), 'gif', 'webm', 'mp4', or 'frames' (raw images). */
16
+ format?: "webp" | "gif" | "webm" | "mp4" | "frames";
17
+ /** If false (default), cleans up raw JPEG image frames after video compilation. */
18
+ keepFrames?: boolean;
19
+ /** If false, keeps the WeaveTab UI overlay visible in the recording.
20
+ * Default: true — overlay is hidden from screencasts so agent never records itself. */
21
+ hideOverlay?: boolean;
22
+ }
23
+ interface RecordingState {
24
+ sessionId: string;
25
+ targetId: string;
26
+ startedAt: string;
27
+ fps: number;
28
+ maxFrames: number;
29
+ frameCount: number;
30
+ dir: string;
31
+ active: boolean;
32
+ }
33
+ export declare function browserRecording(session: CDP.Client, options: RecordingOptions, config: Config): Promise<{
34
+ success: boolean;
35
+ recordings?: RecordingState[];
36
+ error?: string;
37
+ dir?: string;
38
+ frameCount?: number;
39
+ videoPath?: string;
40
+ videoFormat?: string;
41
+ durationSeconds?: number;
42
+ fileSizeBytes?: number;
43
+ compilerUsed?: string;
44
+ }>;
45
+ export {};
@@ -0,0 +1,124 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { getOutputDir } from "../config/loader.js";
4
+ import { logAction } from "../audit/logger.js";
5
+ import { hideWeaveTabOverlay, restoreWeaveTabOverlay } from "../cdp/helpers.js";
6
+ import { sessionRegistry } from "../state/session.js";
7
+ import { compileVideo } from "./video_compiler.js";
8
+ const activeRecordings = new Map();
9
+ let screencastListener = null;
10
+ let currentSession = null;
11
+ function recordingDir(sessionId, config) {
12
+ return path.join(getOutputDir("recordings", config), sessionId);
13
+ }
14
+ export async function browserRecording(session, options, config) {
15
+ const { action, sessionId, fps = 3, maxFrames = 500 } = options;
16
+ if (action === "start") {
17
+ if (!sessionId)
18
+ return { success: false, error: "browser_recording start requires a 'sessionId'" };
19
+ if (activeRecordings.has(sessionId)) {
20
+ return { success: false, error: `Recording '${sessionId}' is already active` };
21
+ }
22
+ const dir = recordingDir(sessionId, config);
23
+ fs.mkdirSync(dir, { recursive: true });
24
+ const active = sessionRegistry.getActive(config.agentId);
25
+ const targetId = active ? active.targetId : "default";
26
+ const state = {
27
+ sessionId,
28
+ targetId,
29
+ startedAt: new Date().toISOString(),
30
+ fps: Math.max(1, Math.min(10, fps)),
31
+ maxFrames,
32
+ frameCount: 0,
33
+ dir,
34
+ active: true,
35
+ };
36
+ activeRecordings.set(sessionId, state);
37
+ currentSession = session;
38
+ const page = session.Page;
39
+ // Register screencast frame handler
40
+ if (screencastListener && currentSession) {
41
+ currentSession.removeListener("Page.screencastFrame", screencastListener);
42
+ }
43
+ screencastListener = async (frame) => {
44
+ const s = activeRecordings.get(sessionId);
45
+ if (!s || !s.active)
46
+ return;
47
+ s.frameCount++;
48
+ const fname = `frame-${String(s.frameCount).padStart(6, "0")}.jpg`;
49
+ const fpath = path.join(s.dir, fname);
50
+ try {
51
+ fs.writeFileSync(fpath, Buffer.from(frame.data, "base64"));
52
+ }
53
+ catch { /* disk full etc — skip frame */ }
54
+ await page.screencastFrameAck({ sessionId: frame.sessionId }).catch(() => { });
55
+ if (s.frameCount >= s.maxFrames) {
56
+ await page.stopScreencast().catch(() => { });
57
+ s.active = false;
58
+ }
59
+ };
60
+ session.on("Page.screencastFrame", screencastListener);
61
+ await page.startScreencast({
62
+ format: "jpeg",
63
+ quality: 60,
64
+ maxWidth: 1280,
65
+ maxHeight: 720,
66
+ everyNthFrame: 1,
67
+ }).catch((e) => {
68
+ activeRecordings.delete(sessionId);
69
+ throw new Error(`Failed to start screencast: ${e.message}`);
70
+ });
71
+ // Always hide overlay during recording (default true; set hideOverlay: false to skip)
72
+ if (options.hideOverlay !== false) {
73
+ await hideWeaveTabOverlay(session);
74
+ }
75
+ logAction("browser_recording", sessionId, `started (${fps}fps, max ${maxFrames} frames)`);
76
+ return { success: true, recordings: [state], dir };
77
+ }
78
+ if (action === "stop") {
79
+ if (!sessionId)
80
+ return { success: false, error: "browser_recording stop requires a 'sessionId'" };
81
+ const state = activeRecordings.get(sessionId);
82
+ if (!state)
83
+ return { success: false, error: `Recording '${sessionId}' not found` };
84
+ state.active = false;
85
+ try {
86
+ await session.Page.stopScreencast().catch(() => { });
87
+ }
88
+ catch { /* ignore */ }
89
+ await restoreWeaveTabOverlay(session);
90
+ if (screencastListener && currentSession) {
91
+ currentSession.removeListener("Page.screencastFrame", screencastListener);
92
+ screencastListener = null;
93
+ currentSession = null;
94
+ }
95
+ const count = state.frameCount;
96
+ activeRecordings.delete(sessionId);
97
+ // Compile frames into video file
98
+ const compileRes = await compileVideo({
99
+ recordingDir: state.dir,
100
+ fps: state.fps,
101
+ format: options.format || "webp",
102
+ keepFrames: options.keepFrames ?? false,
103
+ });
104
+ logAction("browser_recording", sessionId, `stopped (${count} frames, compiled: ${compileRes.videoPath || "none"})`);
105
+ return {
106
+ success: true,
107
+ recordings: Array.from(activeRecordings.values()),
108
+ frameCount: count,
109
+ dir: state.dir,
110
+ videoPath: compileRes.videoPath,
111
+ videoFormat: compileRes.format,
112
+ durationSeconds: compileRes.durationSeconds,
113
+ fileSizeBytes: compileRes.fileSizeBytes,
114
+ compilerUsed: compileRes.compilerUsed,
115
+ };
116
+ }
117
+ if (action === "list") {
118
+ return {
119
+ success: true,
120
+ recordings: Array.from(activeRecordings.values()),
121
+ };
122
+ }
123
+ return { success: false, error: `Unknown action: ${action}` };
124
+ }