@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,303 @@
1
+ /**
2
+ * src/sensors/actionability.ts
3
+ *
4
+ * Playwright-Strategy Actionability Engine for WeaveTab
5
+ * ─────────────────────────────────────────────────────
6
+ * Implements every correctness principle from playwright-strategy.md:
7
+ *
8
+ * 1. RAF-stability gate — element must not be animating before we touch it
9
+ * 2. Strict hit-target — elementsFromPoint at the computed click point must
10
+ * confirm the target is on top (or the target's child).
11
+ * 3. WeaveTab transparency — WeaveTab's own UI (data-wt-ignore nodes) is excluded
12
+ * from the hit stack so the tool never blocks itself.
13
+ * 4. 4-way scroll rotation — escape sticky headers / overlays by cycling four
14
+ * scrollIntoView alignments before giving up.
15
+ * 5. Per-action backoff — [0, 20, 100, 100, 500] ms retry on transient failures.
16
+ * 6. Rich blocker report — on permanent occlusion, return the blocking element's
17
+ * description so the model can reason about it.
18
+ *
19
+ * Usage:
20
+ * import { verifyActionability } from "../sensors/actionability.js";
21
+ * const result = await verifyActionability(session, selector);
22
+ * if (result.status !== "ACTIONABLE") { ... }
23
+ */
24
+ // ── Constants ─────────────────────────────────────────────────────────────────
25
+ /** Playwright's per-action retry backoff (ms). */
26
+ const BACKOFF = [0, 20, 100, 100, 500];
27
+ /**
28
+ * 4-way scroll alignment rotation — escape sticky headers.
29
+ * undefined = browser default (nearest), then progressively more aggressive.
30
+ */
31
+ const SCROLL_ALIGNMENTS = [
32
+ undefined,
33
+ { block: "end", inline: "end" },
34
+ { block: "center", inline: "center" },
35
+ { block: "start", inline: "start" },
36
+ ];
37
+ /**
38
+ * Injected expression that does the full Playwright-style actionability check
39
+ * in the page context. Returns a serialisable result object.
40
+ *
41
+ * Parameters injected at build-time via template literals:
42
+ * selector — CSS selector string
43
+ * scrollOptionsJSON — JSON string of ScrollIntoViewOptions or "null"
44
+ * fingerprintJSON — JSON string of {tag,role} or "null"
45
+ * stabilityMs — ms to wait for stable rect
46
+ */
47
+ function buildPageExpression(selector, scrollOptions, fingerprint, stabilityMs) {
48
+ const selectorLiteral = JSON.stringify(selector);
49
+ const scrollLiteral = scrollOptions ? JSON.stringify(scrollOptions) : "null";
50
+ const fingerprintLiteral = fingerprint ? JSON.stringify(fingerprint) : "null";
51
+ return `(async function() {
52
+ // ── Helper: is this node owned by WeaveTab? ────────────────────────────
53
+ function isWtOwned(el) {
54
+ if (!el) return false;
55
+ // Attribute-based markers: data-wt-ignore and data-wt-internal both signal
56
+ // WeaveTab-owned UI that must not block page interactions.
57
+ if (el.getAttribute && (
58
+ el.getAttribute('data-wt-ignore') !== null ||
59
+ el.getAttribute('data-wt-internal') !== null ||
60
+ el.getAttribute('data-wt-overlay') !== null
61
+ )) return true;
62
+ const wtIds = ['Weavetab-hud', '__Weavetab_controlled_indicator', 'wt-main-container', 'wt-system-icon', 'wt-f3-host', 'Weavetab-root'];
63
+ if (el.id && wtIds.includes(el.id)) return true;
64
+ if (el.closest) {
65
+ if (el.closest('[data-wt-ignore],[data-wt-internal],[data-wt-overlay]')) return true;
66
+ if (wtIds.some(id => el.closest('#' + id))) return true;
67
+ }
68
+ return false;
69
+ }
70
+
71
+ // ── Step 1: Resolve element ────────────────────────────────────────────
72
+ let el = document.querySelector(${selectorLiteral});
73
+ if (!el && window.__Weavetab_shadowQuery) {
74
+ el = window.__Weavetab_shadowQuery(${selectorLiteral});
75
+ }
76
+ if (!el) return { status: 'NOT_FOUND' };
77
+
78
+ // ── Step 2: Fingerprint check ──────────────────────────────────────────
79
+ const fp = ${fingerprintLiteral};
80
+ if (fp) {
81
+ if (fp.tag && el.tagName.toLowerCase() !== fp.tag.toLowerCase())
82
+ return { status: 'FINGERPRINT_MISMATCH' };
83
+ if (fp.role && el.getAttribute('role') !== fp.role)
84
+ return { status: 'FINGERPRINT_MISMATCH' };
85
+ }
86
+
87
+ // ── Step 3: Basic visibility ───────────────────────────────────────────
88
+ // Use checkVisibility() (Chrome 105+) as the most reliable single check.
89
+ // Falls back to manual computed-style + rect checks for older runtimes.
90
+ if (typeof el.checkVisibility === 'function') {
91
+ if (!el.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true }))
92
+ return { status: 'INVISIBLE' };
93
+ } else {
94
+ const cs = window.getComputedStyle(el);
95
+ if (cs.display === 'none' || cs.visibility === 'hidden' || cs.opacity === '0')
96
+ return { status: 'INVISIBLE' };
97
+ }
98
+
99
+ let rect = el.getBoundingClientRect();
100
+ if (rect.width === 0 || rect.height === 0) return { status: 'INVISIBLE' };
101
+ // Off-screen by more than 2× viewport dimension in any direction
102
+ if (rect.right < -window.innerWidth || rect.bottom < -window.innerHeight ||
103
+ rect.left > 2 * window.innerWidth || rect.top > 2 * window.innerHeight)
104
+ return { status: 'INVISIBLE' };
105
+
106
+ // ── Step 4: Scroll into view (using provided alignment) ────────────────
107
+ const scrollOpts = ${scrollLiteral};
108
+ if (scrollOpts) {
109
+ el.scrollIntoView(scrollOpts);
110
+ await new Promise(r => setTimeout(r, 120));
111
+ } else if (rect.top < 0 || rect.bottom > window.innerHeight ||
112
+ rect.left < 0 || rect.right > window.innerWidth) {
113
+ el.scrollIntoView({ block: 'nearest', inline: 'nearest' });
114
+ await new Promise(r => setTimeout(r, 120));
115
+ }
116
+ rect = el.getBoundingClientRect();
117
+
118
+ // ── Step 5: RAF-stability gate ─────────────────────────────────────────
119
+ const stabilityDeadline = Date.now() + ${stabilityMs};
120
+ let prevRect = el.getBoundingClientRect();
121
+ let stable = false;
122
+ while (Date.now() < stabilityDeadline) {
123
+ await new Promise(r => requestAnimationFrame(r));
124
+ await new Promise(r => requestAnimationFrame(r)); // two ticks like Playwright
125
+ const cur = el.getBoundingClientRect();
126
+ if (
127
+ Math.abs(cur.x - prevRect.x) < 0.5 &&
128
+ Math.abs(cur.y - prevRect.y) < 0.5 &&
129
+ Math.abs(cur.width - prevRect.width) < 0.5 &&
130
+ Math.abs(cur.height - prevRect.height) < 0.5
131
+ ) {
132
+ stable = true;
133
+ break;
134
+ }
135
+ prevRect = cur;
136
+ }
137
+ if (!stable) return { status: 'UNSTABLE' };
138
+ rect = el.getBoundingClientRect();
139
+
140
+ // ── Step 6: Compute click centre (clamped to viewport) ─────────────────
141
+ const cx = Math.round(Math.min(Math.max(rect.left + rect.width / 2, 0), window.innerWidth - 1));
142
+ const cy = Math.round(Math.min(Math.max(rect.top + rect.height / 2, 0), window.innerHeight - 1));
143
+
144
+ // ── Step 7: Strict hit-target check ───────────────────────────────────
145
+ // elementsFromPoint returns the full composited stack, topmost first.
146
+ const stack = document.elementsFromPoint(cx, cy);
147
+ if (!stack || stack.length === 0) return { status: 'ACTIONABLE', x: cx, y: cy };
148
+
149
+ // Walk from the topmost element. Anything that is WeaveTab-owned is
150
+ // transparent — skip it. The first non-WT element must be el or its descendant.
151
+ let blocker = null;
152
+ for (const hit of stack) {
153
+ if (isWtOwned(hit)) continue; // WeaveTab transparency
154
+ blocker = hit;
155
+ break;
156
+ }
157
+
158
+ if (!blocker || blocker === el || el.contains(blocker)) {
159
+ return { status: 'ACTIONABLE', x: cx, y: cy };
160
+ }
161
+
162
+ // Check composed tree (shadow DOM + slots)
163
+ let ancestor = blocker;
164
+ while (ancestor) {
165
+ if (ancestor === el) return { status: 'ACTIONABLE', x: cx, y: cy };
166
+ ancestor = ancestor.assignedSlot || ancestor.parentElement ||
167
+ (ancestor.getRootNode && ancestor.getRootNode({ composed: true }) !== ancestor
168
+ ? ancestor.getRootNode({ composed: true }).host : null);
169
+ }
170
+
171
+ return {
172
+ status: 'OCCLUDED',
173
+ x: cx, y: cy,
174
+ occludedBy: {
175
+ tag: blocker.tagName.toLowerCase(),
176
+ id: blocker.id || '',
177
+ className: (blocker.className && typeof blocker.className === 'string')
178
+ ? blocker.className.slice(0, 60) : '',
179
+ ariaLabel: blocker.getAttribute('aria-label') || blocker.getAttribute('title') || null,
180
+ isWeavetabOwned: isWtOwned(blocker)
181
+ }
182
+ };
183
+ })()`;
184
+ }
185
+ // ── Main export ───────────────────────────────────────────────────────────────
186
+ /**
187
+ * Full Playwright-strategy actionability verification.
188
+ *
189
+ * Runs up to `BACKOFF.length` attempts with 4-way scroll rotation,
190
+ * RAF stability gating, WeaveTab-transparent hit-target checking,
191
+ * and per-attempt retry backoff.
192
+ *
193
+ * Returns `status: "ACTIONABLE"` with computed (x, y) when safe to dispatch,
194
+ * or a failure status with an optional `occludedBy` description.
195
+ */
196
+ export async function verifyActionability(session, selector, opts = {}) {
197
+ const stabilityMs = opts.stabilityTimeoutMs ?? 300;
198
+ const totalDeadline = Date.now() + (opts.totalTimeoutMs ?? 2000);
199
+ let lastResult = { status: "NOT_FOUND" };
200
+ let scrollIdx = 0;
201
+ for (let attempt = 0; attempt < BACKOFF.length; attempt++) {
202
+ // Honour total time budget
203
+ if (Date.now() >= totalDeadline)
204
+ break;
205
+ // Backoff delay (first attempt is instant)
206
+ if (BACKOFF[attempt] > 0) {
207
+ await new Promise((r) => setTimeout(r, BACKOFF[attempt]));
208
+ }
209
+ const scrollAlignment = SCROLL_ALIGNMENTS[scrollIdx % SCROLL_ALIGNMENTS.length];
210
+ const expr = buildPageExpression(selector, scrollAlignment, opts.fingerprint, stabilityMs);
211
+ let raw;
212
+ try {
213
+ const { result } = await session.Runtime.evaluate({
214
+ expression: expr,
215
+ awaitPromise: true,
216
+ returnByValue: true,
217
+ });
218
+ raw = result?.value;
219
+ }
220
+ catch {
221
+ // CDP error — element might have been detached; retry
222
+ continue;
223
+ }
224
+ if (!raw)
225
+ continue;
226
+ lastResult = raw;
227
+ switch (lastResult.status) {
228
+ case "ACTIONABLE":
229
+ return lastResult;
230
+ case "NOT_FOUND":
231
+ case "FINGERPRINT_MISMATCH":
232
+ case "INVISIBLE":
233
+ // These are likely permanent; still retry in case of SPA re-render
234
+ break;
235
+ case "UNSTABLE":
236
+ // Give the animation more time on next attempt
237
+ break;
238
+ case "OCCLUDED": {
239
+ const occ = lastResult.occludedBy;
240
+ // If the only blocker is WeaveTab's own UI, treat as transparent and retry
241
+ if (occ?.isWeavetabOwned) {
242
+ lastResult = { status: "ACTIONABLE", x: lastResult.x, y: lastResult.y };
243
+ return lastResult;
244
+ }
245
+ // Advance scroll alignment to try to escape the blocker
246
+ scrollIdx++;
247
+ break;
248
+ }
249
+ }
250
+ }
251
+ return lastResult;
252
+ }
253
+ // ── Convenience wrapper ───────────────────────────────────────────────────────
254
+ /**
255
+ * Quick check: is an element at these coords actually blocked by a non-WT overlay?
256
+ * Used for lightweight pre-dispatch validation inside click.ts.
257
+ */
258
+ export async function quickHitCheck(session, x, y, selectorOfTarget) {
259
+ const expr = `(function() {
260
+ function isWtOwned(el) {
261
+ if (!el) return false;
262
+ if (el.getAttribute && (
263
+ el.getAttribute('data-wt-ignore') !== null ||
264
+ el.getAttribute('data-wt-internal') !== null ||
265
+ el.getAttribute('data-wt-overlay') !== null
266
+ )) return true;
267
+ const ids = ['Weavetab-hud','__Weavetab_controlled_indicator','wt-main-container','wt-system-icon','wt-f3-host','Weavetab-root'];
268
+ if (el.id && ids.includes(el.id)) return true;
269
+ return !!(el.closest && (el.closest('[data-wt-ignore],[data-wt-internal],[data-wt-overlay]') || ids.some(id => el.closest('#' + id))));
270
+ }
271
+ const target = document.querySelector(${JSON.stringify(selectorOfTarget)})
272
+ || (window.__Weavetab_shadowQuery ? window.__Weavetab_shadowQuery(${JSON.stringify(selectorOfTarget)}) : null);
273
+ const stack = document.elementsFromPoint(${x}, ${y});
274
+ if (!stack || !stack.length) return { clear: true };
275
+ for (const hit of stack) {
276
+ if (isWtOwned(hit)) continue;
277
+ if (!target || hit === target || target.contains(hit)) return { clear: true };
278
+ // Walk composed tree for shadow DOM
279
+ let ancestor = hit;
280
+ while (ancestor) {
281
+ if (ancestor === target) return { clear: true };
282
+ ancestor = ancestor.assignedSlot || ancestor.parentElement ||
283
+ (ancestor.getRootNode && ancestor.getRootNode({composed:true}) !== ancestor
284
+ ? ancestor.getRootNode({composed:true}).host : null);
285
+ }
286
+ return {
287
+ clear: false,
288
+ blockerDescription: hit.tagName.toLowerCase() +
289
+ (hit.id ? '#' + hit.id : '') +
290
+ (hit.className && typeof hit.className === 'string' ? '.' + hit.className.split(' ')[0] : '') +
291
+ ' intercepts pointer events'
292
+ };
293
+ }
294
+ return { clear: true };
295
+ })()`;
296
+ try {
297
+ const { result } = await session.Runtime.evaluate({ expression: expr, returnByValue: true });
298
+ return result?.value ?? { clear: true };
299
+ }
300
+ catch {
301
+ return { clear: true }; // non-fatal; proceed optimistically
302
+ }
303
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Weavetab — sensors/guard.ts
3
+ * guardedAction pipeline: element readiness, DOM stability wait, and navigation await.
4
+ * Weavetab by fy2ne
5
+ */
6
+ import CDP from "chrome-remote-interface";
7
+ export interface PollOptions {
8
+ timeout: number;
9
+ interval: number;
10
+ }
11
+ export declare function pollUntil(predicate: () => Promise<boolean>, opts: PollOptions): Promise<void>;
12
+ export declare function waitForDOMStable(session: CDP.Client, quietPeriod?: number, // Reduced from 100ms
13
+ timeout?: number, // Reduced from 1500ms
14
+ fast?: boolean): Promise<void>;
15
+ export declare function guardedAction(session: CDP.Client, backendNodeId: number, action: () => Promise<void>, label?: string, postActionPipeline?: () => Promise<void>, fast?: boolean): Promise<void>;
16
+ export interface PageWeight {
17
+ level: "light" | "medium" | "heavy";
18
+ elementCount: number;
19
+ imageCount: number;
20
+ scriptCount: number;
21
+ iframeCount: number;
22
+ suggestedTimeout: number;
23
+ }
24
+ export declare function detectPageWeight(session: CDP.Client): Promise<PageWeight>;
25
+ export declare function awaitNavigation(session: CDP.Client, timeout?: number): Promise<void>;
@@ -0,0 +1,168 @@
1
+ export async function pollUntil(predicate, opts) {
2
+ const deadline = Date.now() + opts.timeout;
3
+ while (Date.now() < deadline) {
4
+ if (await predicate())
5
+ return;
6
+ await sleep(opts.interval);
7
+ }
8
+ throw new Error(`pollUntil: timed out after ${opts.timeout}ms`);
9
+ }
10
+ // sleep is ONLY permitted inside the auto-wait pipeline — never in tool handlers
11
+ function sleep(ms) {
12
+ return new Promise((r) => setTimeout(r, ms));
13
+ }
14
+ // ─── DOM checks (all run inside CDP Runtime.callFunction for speed) ───────────
15
+ import { waitForNetworkIdle } from "./wait.js";
16
+ async function isElementReady(session, backendNodeId) {
17
+ try {
18
+ const { nodeIds } = await session.DOM.pushNodesByBackendIdsToFrontend({
19
+ backendNodeIds: [backendNodeId],
20
+ });
21
+ if (!nodeIds[0])
22
+ return false;
23
+ const { object } = await session.DOM.resolveNode({ nodeId: nodeIds[0] });
24
+ if (!object?.objectId)
25
+ return false;
26
+ const { result } = await session.Runtime.callFunctionOn({
27
+ objectId: object.objectId,
28
+ functionDeclaration: `function() {
29
+ if (this.disabled || this.getAttribute('aria-disabled') === 'true') return false;
30
+ const r = this.getBoundingClientRect();
31
+ const s = window.getComputedStyle(this);
32
+ if (r.width === 0 || r.height === 0 || s.visibility === 'hidden' || s.display === 'none' || s.opacity === '0') return false;
33
+ const topEl = document.elementFromPoint(r.left + r.width / 2, r.top + r.height / 2);
34
+ return topEl !== null && (topEl === this || this.contains(topEl));
35
+ }`,
36
+ returnByValue: true,
37
+ });
38
+ return result?.value === true;
39
+ }
40
+ catch {
41
+ return false;
42
+ }
43
+ }
44
+ // ─── DOM stability (waits for no mutations for quietPeriod ms) ────────────────
45
+ export async function waitForDOMStable(session, quietPeriod = 50, // Reduced from 100ms
46
+ timeout = 800, // Reduced from 1500ms
47
+ fast = false) {
48
+ // Fast path: skip stability check entirely when fast=true
49
+ if (fast)
50
+ return Promise.resolve();
51
+ const result = await session.Runtime.evaluate({
52
+ expression: `(function(quiet, timeout) {
53
+ return new Promise((resolve) => {
54
+ let timer;
55
+ const obs = new MutationObserver(() => {
56
+ clearTimeout(timer);
57
+ timer = setTimeout(() => { obs.disconnect(); resolve(true); }, quiet);
58
+ });
59
+ obs.observe(document.body, { childList: true, subtree: true, attributes: true });
60
+ timer = setTimeout(() => { obs.disconnect(); resolve(true); }, quiet);
61
+ setTimeout(() => { obs.disconnect(); resolve(true); }, timeout);
62
+ });
63
+ })(${quietPeriod}, ${timeout})`,
64
+ awaitPromise: true,
65
+ returnByValue: true,
66
+ }).catch(() => ({ result: { value: true } }));
67
+ return Promise.resolve();
68
+ }
69
+ // ─── guardedAction — the mandatory wrapper for every CDP interaction ──────────
70
+ //
71
+ // Rule 9: Every CDP interaction goes through this pipeline:
72
+ // 1. DOM existence check
73
+ // 2. Visibility check
74
+ // 3. Interactability check
75
+ // 4. DOM stability wait (150ms quiet period for SPA re-renders)
76
+ // Then fires the action.
77
+ //
78
+ // Manual sleeps in tool handlers are BANNED. This is the only place they live.
79
+ export async function guardedAction(session, backendNodeId, action, label = "unknown", postActionPipeline, fast = false) {
80
+ // Fast path: minimal checks when fast=true
81
+ if (fast) {
82
+ // Quick single check (no polling)
83
+ const ready = await isElementReady(session, backendNodeId);
84
+ if (!ready) {
85
+ throw new Error(`guardedAction: element "${label}" not ready (fast mode)`);
86
+ }
87
+ await action();
88
+ if (postActionPipeline)
89
+ await postActionPipeline();
90
+ return;
91
+ }
92
+ // 1. Single-pass check: existence, visibility, and interactability
93
+ await pollUntil(() => isElementReady(session, backendNodeId), {
94
+ timeout: 800, // Reduced from 1500ms
95
+ interval: 30, // Reduced from 50ms
96
+ }).catch(() => {
97
+ throw new Error(`guardedAction: element "${label}" not ready (missing, hidden, or blocked) after 0.8s`);
98
+ });
99
+ // 2. DOM stability pre-action
100
+ await waitForDOMStable(session, 50, 600); // Reduced from 100/1000
101
+ // 3. Fire
102
+ await action();
103
+ // 4. Post-action — DOM stability + network idle (parallel, shorter)
104
+ await Promise.all([
105
+ waitForDOMStable(session, 50, 800), // Reduced from 100/1500
106
+ waitForNetworkIdle(session, 200, 800).catch(() => { }), // Reduced from 300/1500
107
+ postActionPipeline ? postActionPipeline() : Promise.resolve(),
108
+ ]);
109
+ }
110
+ export async function detectPageWeight(session) {
111
+ try {
112
+ const { result } = await session.Runtime.evaluate({
113
+ expression: `(function() {
114
+ const all = document.querySelectorAll('*');
115
+ const images = document.querySelectorAll('img, picture, svg');
116
+ const scripts = document.querySelectorAll('script');
117
+ const iframes = document.querySelectorAll('iframe, frame');
118
+ return JSON.stringify({
119
+ elementCount: all.length,
120
+ imageCount: images.length,
121
+ scriptCount: scripts.length,
122
+ iframeCount: iframes.length
123
+ });
124
+ })()`,
125
+ returnByValue: true,
126
+ });
127
+ const data = JSON.parse(result.value);
128
+ const { elementCount, imageCount, scriptCount, iframeCount } = data;
129
+ // Weight scoring heuristic
130
+ let score = 0;
131
+ score += Math.min(elementCount / 500, 10); // 0-10: 0 to 5000+ elements
132
+ score += Math.min(imageCount / 10, 5); // 0-5: 0 to 50+ images
133
+ score += Math.min(scriptCount / 5, 3); // 0-3: 0 to 15+ scripts
134
+ score += Math.min(iframeCount, 2); // 0-2: 0+ iframes
135
+ const level = score < 5 ? "light" : score < 12 ? "medium" : "heavy";
136
+ const baseTimeout = 8000;
137
+ const suggestedTimeout = Math.min(Math.round(baseTimeout + score * 1500), 60000);
138
+ return { level, elementCount, imageCount, scriptCount, iframeCount, suggestedTimeout };
139
+ }
140
+ catch {
141
+ return {
142
+ level: "light",
143
+ elementCount: 0,
144
+ imageCount: 0,
145
+ scriptCount: 0,
146
+ iframeCount: 0,
147
+ suggestedTimeout: 10000,
148
+ };
149
+ }
150
+ }
151
+ // ─── Navigation wait — used after clicks that trigger navigation ──────────────
152
+ export async function awaitNavigation(session, timeout = 6000 // Reduced from 10000ms
153
+ ) {
154
+ // Poll readyState instead of consuming one-shot event
155
+ await pollUntil(async () => {
156
+ const { result } = await session.Runtime.evaluate({
157
+ expression: `document.readyState`,
158
+ returnByValue: true,
159
+ }).catch(() => ({ result: { value: 'loading' } }));
160
+ return result?.value === 'complete' || result?.value === 'interactive';
161
+ }, { timeout, interval: 50 }).catch(() => {
162
+ throw new Error(`Navigation timeout after ${timeout}ms`);
163
+ });
164
+ // After DOM content fires, detect page weight for post-load stability wait
165
+ const weight = await detectPageWeight(session);
166
+ const stabilityTimeout = weight.level === "heavy" ? 3000 : weight.level === "medium" ? 2000 : 1000; // Reduced from 5000/3000/2000
167
+ await waitForDOMStable(session, 50, stabilityTimeout); // Reduced quiet period from 100ms
168
+ }
@@ -0,0 +1,23 @@
1
+ import CDP from "chrome-remote-interface";
2
+ export type MutationSummary = {
3
+ newElements: {
4
+ backendNodeId: number;
5
+ tag: string;
6
+ text: string;
7
+ role?: string;
8
+ }[];
9
+ removedElements: {
10
+ backendNodeId: number;
11
+ tag: string;
12
+ text: string;
13
+ }[];
14
+ attributeChanges: {
15
+ backendNodeId: number;
16
+ element: string;
17
+ attribute: string;
18
+ oldValue: string | null;
19
+ newValue: string;
20
+ }[];
21
+ significantChange: boolean;
22
+ };
23
+ export declare function watchMutations(session: CDP.Client, durationMs: number): Promise<MutationSummary>;
@@ -0,0 +1,93 @@
1
+ // Heuristics for "significant" changes
2
+ const SIGNIFICANT_TAGS = new Set(['button', 'a', 'input', 'textarea', 'select', 'h1', 'h2', 'h3', '[role="button"]', '[role="link"]']);
3
+ const MAX_TEXT_LEN = 150;
4
+ export async function watchMutations(session, durationMs) {
5
+ const { DOM } = session;
6
+ const summary = {
7
+ newElements: [],
8
+ removedElements: [],
9
+ attributeChanges: [],
10
+ significantChange: false,
11
+ };
12
+ let timeoutId = null;
13
+ let documentUpdated = false;
14
+ const onDocumentUpdated = () => {
15
+ documentUpdated = true;
16
+ summary.significantChange = true; // Full page load is always significant
17
+ };
18
+ const onAttributeModified = (params) => {
19
+ const { nodeId, name, value } = params;
20
+ // For simplicity, we can't get the old value here without more complex tracking.
21
+ // Let's just record the change.
22
+ summary.attributeChanges.push({
23
+ backendNodeId: nodeId,
24
+ element: `nodeId: ${nodeId}`,
25
+ attribute: name,
26
+ oldValue: "not_tracked", // The protocol doesn't provide the old value directly
27
+ newValue: value,
28
+ });
29
+ summary.significantChange = true; // Assume attribute changes can be significant
30
+ };
31
+ const onChildNodeInserted = async (params) => {
32
+ const { parentNodeId, previousNodeId, node } = params;
33
+ // We only care about element nodes
34
+ if (node.nodeType !== 1)
35
+ return; // 1 = Element Node
36
+ // nodeValue is always null for element nodes (nodeType === 1).
37
+ // For elements, approximate text from nodeValue of first text child.
38
+ // CDP Node type doesn't have textContent, so we fall back to nodeValue.
39
+ const textContent = node.nodeValue || '';
40
+ // CDP DOM attributes are a flattened string array: [name1, value1, name2, value2, ...]
41
+ // Find 'role' by iterating in pairs.
42
+ let role = '';
43
+ const attrs = node.attributes || [];
44
+ for (let ai = 0; ai < attrs.length - 1; ai += 2) {
45
+ if (attrs[ai] === 'role') {
46
+ role = attrs[ai + 1] || '';
47
+ break;
48
+ }
49
+ }
50
+ summary.newElements.push({
51
+ backendNodeId: node.backendNodeId,
52
+ tag: node.localName,
53
+ text: textContent.substring(0, MAX_TEXT_LEN),
54
+ role,
55
+ });
56
+ if (SIGNIFICANT_TAGS.has(node.localName) || (role && SIGNIFICANT_TAGS.has(`[role="${role}"]`))) {
57
+ summary.significantChange = true;
58
+ }
59
+ };
60
+ const onChildNodeRemoved = (params) => {
61
+ const { parentNodeId, nodeId } = params;
62
+ // We don't have info about the removed node other than its ID.
63
+ // A more complex implementation would cache node info.
64
+ summary.removedElements.push({
65
+ backendNodeId: nodeId,
66
+ tag: 'unknown',
67
+ text: 'unknown'
68
+ });
69
+ };
70
+ // --- Main Logic ---
71
+ const promise = new Promise(resolve => {
72
+ timeoutId = setTimeout(resolve, durationMs);
73
+ });
74
+ try {
75
+ // Enable DOM notifications
76
+ await DOM.enable();
77
+ const listeners = [
78
+ DOM.on("documentUpdated", onDocumentUpdated),
79
+ DOM.on("attributeModified", onAttributeModified),
80
+ DOM.on("childNodeInserted", onChildNodeInserted),
81
+ DOM.on("childNodeRemoved", onChildNodeRemoved)
82
+ ];
83
+ await DOM.getDocument({ depth: -1, pierce: true });
84
+ await promise;
85
+ }
86
+ finally {
87
+ if (timeoutId)
88
+ clearTimeout(timeoutId);
89
+ // Disabling the DOM agent automatically removes all listeners.
90
+ await DOM.disable();
91
+ }
92
+ return summary;
93
+ }
@@ -0,0 +1,67 @@
1
+ import CDP from "chrome-remote-interface";
2
+ export type NetworkSecurityDetails = {
3
+ protocol?: string;
4
+ issuer?: string;
5
+ subjectName?: string;
6
+ validFrom?: number;
7
+ validTo?: number;
8
+ cipher?: string;
9
+ keyExchange?: string;
10
+ sanList?: string[];
11
+ certificateId?: number;
12
+ };
13
+ export type NetworkTimingBreakdown = {
14
+ dnsMs?: number;
15
+ connectMs?: number;
16
+ sslMs?: number;
17
+ sendMs?: number;
18
+ waitMs?: number;
19
+ downloadMs?: number;
20
+ totalMs?: number;
21
+ };
22
+ export type NetworkFailureDetails = {
23
+ errorText: string;
24
+ canceled?: boolean;
25
+ blockedReason?: string;
26
+ };
27
+ export type CapturedRequest = {
28
+ url: string;
29
+ method: string;
30
+ status?: number;
31
+ isApi: boolean;
32
+ timestamp?: number;
33
+ securityDetails?: NetworkSecurityDetails;
34
+ timing?: NetworkTimingBreakdown;
35
+ failure?: NetworkFailureDetails;
36
+ _startTimeMonotonic?: number;
37
+ _responseHeadersMonotonic?: number;
38
+ };
39
+ export type TelemetryReport = {
40
+ networkRequests: CapturedRequest[];
41
+ domMutations: string[];
42
+ navigationOccurred: boolean;
43
+ newUrl?: string;
44
+ toastMessages: string[];
45
+ pageTitle?: string;
46
+ errors: string[];
47
+ };
48
+ export declare class TelemetryCapture {
49
+ private session;
50
+ private requests;
51
+ private toastMessages;
52
+ private errors;
53
+ private navigationOccurred;
54
+ private newUrl?;
55
+ private active;
56
+ private _onRequestWillBeSent;
57
+ private _onResponseReceived;
58
+ private _onLoadingFinished;
59
+ private _onLoadingFailed;
60
+ private _onFrameNavigated;
61
+ private _onChildNodeInserted;
62
+ private _onConsoleAPICalled;
63
+ constructor(session: CDP.Client);
64
+ private setupListeners;
65
+ start(): Promise<void>;
66
+ stop(): Promise<TelemetryReport>;
67
+ }