@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,36 @@
1
+ import CDP from "chrome-remote-interface";
2
+ export type GroupState = "idle" | "weaving" | "stuck";
3
+ interface AgentStateCallbacks {
4
+ onStateChange: (session: CDP.Client | null, state: GroupState, word?: string) => void;
5
+ onUpdateTabTitle: (session: CDP.Client, state: GroupState) => void;
6
+ }
7
+ declare class AgentStateManager {
8
+ private state;
9
+ private word;
10
+ private idleTimeoutMs;
11
+ private idleTimer;
12
+ private callbacks;
13
+ private session;
14
+ private errorStreak;
15
+ constructor(idleTimeoutMs?: number);
16
+ setCallbacks(cb: AgentStateCallbacks): void;
17
+ setSession(session: CDP.Client): void;
18
+ setIdleTimeout(ms: number): void;
19
+ reportToolCall(): void;
20
+ reportToolSuccess(): void;
21
+ reportToolError(): void;
22
+ reportStuck(word: string): void;
23
+ reportDone(): void;
24
+ reportTransportClosed(): void;
25
+ getState(): {
26
+ state: GroupState;
27
+ word: string;
28
+ };
29
+ isStuck(): boolean;
30
+ resetErrorStreak(): void;
31
+ private transition;
32
+ private startIdleTimer;
33
+ private clearIdleTimer;
34
+ }
35
+ export declare const agentState: AgentStateManager;
36
+ export {};
@@ -0,0 +1,101 @@
1
+ class AgentStateManager {
2
+ state = "idle";
3
+ word = "";
4
+ idleTimeoutMs;
5
+ idleTimer = null;
6
+ callbacks = null;
7
+ session = null;
8
+ errorStreak = 0;
9
+ constructor(idleTimeoutMs = 60000) {
10
+ this.idleTimeoutMs = idleTimeoutMs;
11
+ }
12
+ setCallbacks(cb) {
13
+ this.callbacks = cb;
14
+ }
15
+ setSession(session) {
16
+ this.session = session;
17
+ }
18
+ setIdleTimeout(ms) {
19
+ this.idleTimeoutMs = ms;
20
+ }
21
+ reportToolCall() {
22
+ this.clearIdleTimer();
23
+ if (this.state !== "weaving") {
24
+ this.transition("weaving");
25
+ }
26
+ else {
27
+ // Always fire onStateChange so activeSessionTabId stays in sync
28
+ this.callbacks?.onStateChange(this.session, this.state, this.word);
29
+ }
30
+ this.startIdleTimer();
31
+ }
32
+ reportToolSuccess() {
33
+ this.errorStreak = 0;
34
+ }
35
+ reportToolError() {
36
+ this.errorStreak++;
37
+ if (this.errorStreak >= 5) {
38
+ this.reportStuck("error_streak");
39
+ }
40
+ }
41
+ reportStuck(word) {
42
+ this.clearIdleTimer();
43
+ this.word = word;
44
+ this.transition("stuck", word);
45
+ }
46
+ reportDone() {
47
+ this.clearIdleTimer();
48
+ this.word = "";
49
+ this.state = "idle";
50
+ // Always broadcast — bypasses guard to ensure favicon/tab cleanup
51
+ this.callbacks?.onStateChange(this.session, "idle");
52
+ if (this.session)
53
+ this.callbacks?.onUpdateTabTitle(this.session, "idle");
54
+ }
55
+ reportTransportClosed() {
56
+ this.clearIdleTimer();
57
+ this.word = "";
58
+ this.state = "idle";
59
+ this.callbacks?.onStateChange(this.session, "idle");
60
+ }
61
+ getState() {
62
+ return { state: this.state, word: this.word };
63
+ }
64
+ isStuck() {
65
+ return this.state === "stuck";
66
+ }
67
+ resetErrorStreak() {
68
+ this.errorStreak = 0;
69
+ }
70
+ transition(newState, word) {
71
+ // Always fire the callback for 'idle' so favicon dots get cleared on every tab
72
+ const isRedundant = this.state === newState && (!word || word === this.word);
73
+ if (isRedundant && newState !== "idle")
74
+ return;
75
+ this.state = newState;
76
+ if (word !== undefined)
77
+ this.word = word;
78
+ this.callbacks?.onStateChange(this.session, this.state, this.word);
79
+ if (this.session) {
80
+ this.callbacks?.onUpdateTabTitle(this.session, this.state);
81
+ }
82
+ }
83
+ startIdleTimer() {
84
+ this.clearIdleTimer();
85
+ this.idleTimer = setTimeout(() => {
86
+ this.word = "";
87
+ this.transition("idle");
88
+ }, this.idleTimeoutMs);
89
+ // unref() so this timer does not pin the Node.js event loop. Without this,
90
+ // the process stays alive indefinitely between tool calls even when the IDE
91
+ // is idle and nothing else is running.
92
+ this.idleTimer.unref();
93
+ }
94
+ clearIdleTimer() {
95
+ if (this.idleTimer) {
96
+ clearTimeout(this.idleTimer);
97
+ this.idleTimer = null;
98
+ }
99
+ }
100
+ }
101
+ export const agentState = new AgentStateManager();
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Weavetab — state/cache.ts
3
+ * Per-tab action map and box model cache with mutation-based TTL invalidation.
4
+ * Weavetab by fy2ne
5
+ */
6
+ import CDP from "chrome-remote-interface";
7
+ import type { ActionMap } from "../cdp/walker.js";
8
+ export declare function getCachedActionMap(targetId: string, currentUrl?: string): ActionMap | null;
9
+ export declare function getRawCachedActionMap(targetId: string, currentUrl?: string): ActionMap | null;
10
+ export declare function cacheActionMap(targetId: string, map: ActionMap): void;
11
+ export declare function cacheBoxModel(targetId: string, backendNodeId: number, boxModel: any): void;
12
+ export declare function getCachedBoxModel(targetId: string, backendNodeId: number): any | null;
13
+ export declare function cacheNode(targetId: string, backendNodeId: number, node: any): void;
14
+ export declare function getCachedNode(targetId: string, backendNodeId: number): any | null;
15
+ export declare function clearTabCache(targetId: string): void;
16
+ export declare function registerMutationListener(session: CDP.Client, targetId: string): void;
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Weavetab — state/cache.ts
3
+ * Per-tab action map and box model cache with mutation-based TTL invalidation.
4
+ * Weavetab by fy2ne
5
+ */
6
+ const tabCache = new Map();
7
+ export function getCachedActionMap(targetId, currentUrl) {
8
+ const state = tabCache.get(targetId);
9
+ if (!state)
10
+ return null;
11
+ // Ref ID Caching Layer: validity TTL based on DOM mutations
12
+ if (state.lastMutationTime > state.lastReadTime) {
13
+ return null; // Mutated since last read, cache invalid
14
+ }
15
+ // URL validation to prevent stale cache across page loads/navigations
16
+ if (currentUrl && state.map && state.map.url !== currentUrl) {
17
+ return null;
18
+ }
19
+ return state.map;
20
+ }
21
+ export function getRawCachedActionMap(targetId, currentUrl) {
22
+ const state = tabCache.get(targetId);
23
+ if (!state)
24
+ return null;
25
+ if (currentUrl && state.map && state.map.url !== currentUrl) {
26
+ return null;
27
+ }
28
+ return state.map;
29
+ }
30
+ export function cacheActionMap(targetId, map) {
31
+ const state = tabCache.get(targetId) || { lastMutationTime: 0, lastReadTime: 0, map: null, url: null, boxModels: new Map(), domNodes: new Map() };
32
+ state.map = map;
33
+ state.url = map.url;
34
+ state.lastReadTime = Date.now();
35
+ tabCache.set(targetId, state);
36
+ }
37
+ export function cacheBoxModel(targetId, backendNodeId, boxModel) {
38
+ const state = tabCache.get(targetId);
39
+ if (state)
40
+ state.boxModels.set(backendNodeId, boxModel);
41
+ }
42
+ export function getCachedBoxModel(targetId, backendNodeId) {
43
+ const state = tabCache.get(targetId);
44
+ if (!state || state.lastMutationTime > state.lastReadTime)
45
+ return null;
46
+ return state.boxModels.get(backendNodeId) || null;
47
+ }
48
+ export function cacheNode(targetId, backendNodeId, node) {
49
+ const state = tabCache.get(targetId);
50
+ if (state)
51
+ state.domNodes.set(backendNodeId, node);
52
+ }
53
+ export function getCachedNode(targetId, backendNodeId) {
54
+ const state = tabCache.get(targetId);
55
+ if (!state || state.lastMutationTime > state.lastReadTime)
56
+ return null;
57
+ return state.domNodes.get(backendNodeId) || null;
58
+ }
59
+ export function clearTabCache(targetId) {
60
+ tabCache.delete(targetId);
61
+ }
62
+ // Smarter Idle Detection: Event-driven MutationObserver
63
+ // Guard: only register listeners once per CDP session object to prevent
64
+ // accumulation when the same session is passed through multiple init paths.
65
+ const registeredSessions = new WeakSet();
66
+ export function registerMutationListener(session, targetId) {
67
+ // Bail out early if this session already has listeners attached.
68
+ // Without this guard, connectToTab + initSession could both call this function
69
+ // for the same session, stacking duplicate listeners that are never removed.
70
+ if (registeredSessions.has(session))
71
+ return;
72
+ registeredSessions.add(session);
73
+ let debounceTimer;
74
+ const markMutated = () => {
75
+ // Debounce to avoid flooding the cache invalidation
76
+ if (debounceTimer)
77
+ clearTimeout(debounceTimer);
78
+ debounceTimer = setTimeout(() => {
79
+ debounceTimer = undefined;
80
+ const state = tabCache.get(targetId);
81
+ if (state) {
82
+ state.lastMutationTime = Date.now();
83
+ // Clear box models and dom nodes as they are no longer reliable
84
+ state.boxModels.clear();
85
+ state.domNodes.clear();
86
+ }
87
+ }, 50); // 50ms debounced quiet window
88
+ };
89
+ session.DOM.on("childNodeCountUpdated", markMutated);
90
+ session.DOM.on("attributeModified", markMutated);
91
+ session.DOM.on("characterDataModified", markMutated);
92
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=cache.test.d.ts.map
@@ -0,0 +1,76 @@
1
+ /**
2
+ * cache.test.ts — Phase 1.1
3
+ * Unit tests for action map and box model caching with mutation-based TTL invalidation.
4
+ */
5
+ import { test } from "node:test";
6
+ import assert from "node:assert/strict";
7
+ import { getCachedActionMap, cacheActionMap, cacheBoxModel, getCachedBoxModel, cacheNode, getCachedNode, clearTabCache, } from "./cache.js";
8
+ test("cache: basic action map storage and retrieval", () => {
9
+ const targetId = "tab-1";
10
+ const mockMap = {
11
+ url: "https://example.com",
12
+ elements: [],
13
+ };
14
+ clearTabCache(targetId);
15
+ assert.equal(getCachedActionMap(targetId), null, "Initial cache miss");
16
+ cacheActionMap(targetId, mockMap);
17
+ const retrieved = getCachedActionMap(targetId);
18
+ assert.ok(retrieved !== null, "Cache hit after storage");
19
+ assert.equal(retrieved?.url, "https://example.com");
20
+ clearTabCache(targetId);
21
+ });
22
+ test("cache: URL validation prevents stale cache", () => {
23
+ const targetId = "tab-2";
24
+ const mockMap = {
25
+ url: "https://example.com",
26
+ elements: [],
27
+ };
28
+ cacheActionMap(targetId, mockMap);
29
+ // Same URL → cache hit
30
+ assert.ok(getCachedActionMap(targetId, "https://example.com") !== null);
31
+ // Different URL → cache miss
32
+ assert.equal(getCachedActionMap(targetId, "https://other.com"), null);
33
+ clearTabCache(targetId);
34
+ });
35
+ test("cache: box model storage and retrieval", () => {
36
+ const targetId = "tab-3";
37
+ const backendNodeId = 12345;
38
+ const mockBoxModel = { model: { content: [0, 0, 100, 100] } };
39
+ // First cache an action map to initialize the state
40
+ cacheActionMap(targetId, { url: "https://example.com", elements: [] });
41
+ cacheBoxModel(targetId, backendNodeId, mockBoxModel);
42
+ const retrieved = getCachedBoxModel(targetId, backendNodeId);
43
+ assert.ok(retrieved !== null, "Box model cached");
44
+ assert.deepEqual(retrieved, mockBoxModel);
45
+ clearTabCache(targetId);
46
+ });
47
+ test("cache: node storage and retrieval", () => {
48
+ const targetId = "tab-4";
49
+ const backendNodeId = 67890;
50
+ const mockNode = { nodeId: 67890, nodeName: "DIV" };
51
+ // First cache an action map to initialize the state
52
+ cacheActionMap(targetId, { url: "https://example.com", elements: [] });
53
+ cacheNode(targetId, backendNodeId, mockNode);
54
+ const retrieved = getCachedNode(targetId, backendNodeId);
55
+ assert.ok(retrieved !== null, "Node cached");
56
+ assert.deepEqual(retrieved, mockNode);
57
+ clearTabCache(targetId);
58
+ });
59
+ test("cache: clearTabCache removes all cached data", () => {
60
+ const targetId = "tab-5";
61
+ const mockMap = { url: "https://example.com", elements: [] };
62
+ const mockBoxModel = { model: { content: [0, 0, 100, 100] } };
63
+ cacheActionMap(targetId, mockMap);
64
+ cacheBoxModel(targetId, 12345, mockBoxModel);
65
+ assert.ok(getCachedActionMap(targetId) !== null);
66
+ assert.ok(getCachedBoxModel(targetId, 12345) !== null);
67
+ clearTabCache(targetId);
68
+ assert.equal(getCachedActionMap(targetId), null);
69
+ assert.equal(getCachedBoxModel(targetId, 12345), null);
70
+ });
71
+ test("cache: missing cache returns null without error", () => {
72
+ assert.equal(getCachedActionMap("nonexistent-tab"), null);
73
+ assert.equal(getCachedBoxModel("nonexistent-tab", 999), null);
74
+ assert.equal(getCachedNode("nonexistent-tab", 999), null);
75
+ });
76
+ //# sourceMappingURL=cache.test.js.map
@@ -0,0 +1,71 @@
1
+ import type { ActionElement as _ActionElement } from "../cdp/walker.js";
2
+ export type ActionElement = _ActionElement;
3
+ export interface RefEntry {
4
+ id: string;
5
+ role: string;
6
+ label: string;
7
+ type?: string;
8
+ value?: string;
9
+ href?: string;
10
+ disabled?: boolean;
11
+ fingerprint: string;
12
+ }
13
+ export interface SnapshotDiff {
14
+ added: RefEntry[];
15
+ removed: string[];
16
+ changed: RefEntry[];
17
+ stable_count: number;
18
+ _refs_truncated?: number;
19
+ }
20
+ export interface PropertyDiff {
21
+ [propName: string]: {
22
+ from: string;
23
+ to: string;
24
+ };
25
+ }
26
+ export interface RichChangedRef {
27
+ ref: string;
28
+ role: string;
29
+ label: string;
30
+ changes: PropertyDiff;
31
+ actionable?: boolean;
32
+ recommended_action?: string;
33
+ }
34
+ export interface IframeChange {
35
+ frameUrl: string;
36
+ status: "added" | "removed" | "changed";
37
+ elementCount: number;
38
+ }
39
+ export interface RichDelta {
40
+ added: RefEntry[];
41
+ removed: string[];
42
+ changed: RichChangedRef[];
43
+ stable_count: number;
44
+ iframes?: IframeChange[];
45
+ }
46
+ /**
47
+ * Store a full snapshot for a tab target.
48
+ */
49
+ export declare function storeSnapshot(targetId: string, url: string, refs: RefEntry[]): void;
50
+ /**
51
+ * Eagerly initialize the property snapshot cache for a tab.
52
+ */
53
+ export declare function seedRichSnapshot(targetId: string, url: string, elements: ActionElement[]): void;
54
+ /**
55
+ * Compute basic delta.
56
+ */
57
+ export declare function computeDelta(targetId: string, url: string, newRefs: RefEntry[]): SnapshotDiff | null;
58
+ /**
59
+ * Rich delta: wraps computeDelta with property-level change details.
60
+ */
61
+ export declare function computeRichDelta(targetId: string, url: string, newElements: ActionElement[]): Promise<RichDelta | {
62
+ delta_timeout: true;
63
+ } | null>;
64
+ /**
65
+ * Clear snapshot for a tab (called on hard navigation).
66
+ */
67
+ export declare function clearSnapshot(targetId: string): void;
68
+ /**
69
+ * Clear all snapshots (called on CDP disconnect).
70
+ */
71
+ export declare function clearAllSnapshots(): void;
@@ -0,0 +1,234 @@
1
+ // src/state/delta.ts
2
+ // Per-tab snapshot diffing for delta reads (Rule 11).
3
+ // weave_read first visit → full snapshot stored here.
4
+ // Subsequent calls → diff against stored snapshot → return only changed refs.
5
+ import { SessionSecretTracker } from "../security/secrets.js";
6
+ const snapshots = new Map();
7
+ const propSnapshots = new Map();
8
+ const frameSnapshots = new Map();
9
+ const INTERACTIVE_ROLES = new Set([
10
+ 'button', 'link', 'textbox', 'searchbox', 'combobox',
11
+ 'checkbox', 'radio', 'menuitem', 'tab', 'select',
12
+ ]);
13
+ function serializeProps(el, targetId) {
14
+ const isSecret = (el.backendNodeId !== undefined && targetId &&
15
+ SessionSecretTracker.isNodeSecret(targetId, el.backendNodeId)) ||
16
+ el.label === '[REDACTED]';
17
+ return {
18
+ label: isSecret ? '[REDACTED]' : (el.label ?? ''),
19
+ value: isSecret ? '[REDACTED]' : (el.value ?? ''),
20
+ disabled: String(!!el.disabled),
21
+ role: el.role,
22
+ href: el.href ?? '',
23
+ placeholder: el.placeholder ?? '',
24
+ textContent: isSecret ? '[REDACTED]' : (el.textContent ?? ''),
25
+ className: (el.className ?? '').split(' ').filter(Boolean).sort().join(' '),
26
+ };
27
+ }
28
+ /**
29
+ * Store a full snapshot for a tab target.
30
+ */
31
+ export function storeSnapshot(targetId, url, refs) {
32
+ const map = new Map();
33
+ for (const r of refs)
34
+ map.set(r.id, r);
35
+ snapshots.set(targetId, { url, refs: map });
36
+ }
37
+ /**
38
+ * Eagerly initialize the property snapshot cache for a tab.
39
+ */
40
+ export function seedRichSnapshot(targetId, url, elements) {
41
+ const propMap = new Map();
42
+ const snapMap = new Map();
43
+ const frameMap = new Map();
44
+ for (const el of elements) {
45
+ propMap.set(el.id, serializeProps(el, targetId));
46
+ snapMap.set(el.id, {
47
+ id: el.id,
48
+ role: el.role,
49
+ label: el.label,
50
+ type: el.type,
51
+ href: el.href,
52
+ disabled: el.disabled,
53
+ value: el.value,
54
+ fingerprint: el.fingerprint,
55
+ });
56
+ const fk = el.frameUrl || '__top__';
57
+ frameMap.set(fk, (frameMap.get(fk) || 0) + 1);
58
+ }
59
+ propSnapshots.set(targetId, { url, props: propMap });
60
+ snapshots.set(targetId, { url, refs: snapMap });
61
+ frameSnapshots.set(targetId, frameMap);
62
+ }
63
+ /**
64
+ * Compute basic delta.
65
+ */
66
+ export function computeDelta(targetId, url, newRefs) {
67
+ const prevWrapper = snapshots.get(targetId);
68
+ if (!prevWrapper || prevWrapper.url !== url)
69
+ return null;
70
+ const prev = prevWrapper.refs;
71
+ const next = new Map();
72
+ for (const r of newRefs)
73
+ next.set(r.id, r);
74
+ // Short-circuit: for maps with 500+ refs, return count-only diff
75
+ // to avoid O(n) comparison hanging on large pages.
76
+ if (prev.size > 500) {
77
+ const addedCount = next.size - prev.size;
78
+ const removedCount = Array.from(prev.keys()).filter(id => !next.has(id)).length;
79
+ snapshots.set(targetId, { url, refs: next });
80
+ return { added: [], removed: [], changed: [], stable_count: Math.min(prev.size, next.size), _refs_truncated: prev.size };
81
+ }
82
+ const added = [];
83
+ const removed = [];
84
+ const changed = [];
85
+ let stable_count = 0;
86
+ for (const [id, ref] of next) {
87
+ const old = prev.get(id);
88
+ if (!old) {
89
+ added.push(ref);
90
+ }
91
+ else if (old.label !== ref.label ||
92
+ old.disabled !== ref.disabled ||
93
+ old.type !== ref.type ||
94
+ (ref.value !== undefined && old.value !== ref.value)) {
95
+ changed.push(ref);
96
+ }
97
+ else {
98
+ stable_count++;
99
+ }
100
+ }
101
+ for (const id of prev.keys()) {
102
+ if (!next.has(id))
103
+ removed.push(id);
104
+ }
105
+ snapshots.set(targetId, { url, refs: next });
106
+ return { added, removed, changed, stable_count };
107
+ }
108
+ /**
109
+ * Rich delta: wraps computeDelta with property-level change details.
110
+ */
111
+ export async function computeRichDelta(targetId, url, newElements) {
112
+ // Wrap computation in a 5-second race against a timeout to avoid O(n^2) hangs
113
+ const computePromise = new Promise((resolve) => {
114
+ if (!propSnapshots.has(targetId) || propSnapshots.get(targetId).url !== url) {
115
+ seedRichSnapshot(targetId, url, newElements);
116
+ resolve(null);
117
+ return;
118
+ }
119
+ const oldPropMap = propSnapshots.get(targetId).props;
120
+ const newRefs = newElements
121
+ .filter(el => !el.selector?.includes('data-wt-internal'))
122
+ .map(el => ({
123
+ id: el.id,
124
+ role: el.role,
125
+ label: el.label,
126
+ type: el.type,
127
+ href: el.href,
128
+ disabled: el.disabled,
129
+ value: el.value,
130
+ fingerprint: el.fingerprint,
131
+ }));
132
+ const basicDiff = computeDelta(targetId, url, newRefs);
133
+ if (!basicDiff) {
134
+ resolve(null);
135
+ return;
136
+ }
137
+ // Compute rich changes with property diffs
138
+ const richChanged = [];
139
+ for (const changedRef of basicDiff.changed) {
140
+ const oldProps = oldPropMap.get(changedRef.id);
141
+ const newEl = newElements.find(e => e.id === changedRef.id);
142
+ if (!oldProps || !newEl) {
143
+ richChanged.push({
144
+ ref: changedRef.id,
145
+ role: changedRef.role,
146
+ label: changedRef.label,
147
+ changes: {},
148
+ });
149
+ continue;
150
+ }
151
+ const newProps = serializeProps(newEl, targetId);
152
+ const changes = {};
153
+ for (const key of [...new Set([...Object.keys(oldProps), ...Object.keys(newProps)])]) {
154
+ if (oldProps[key] !== newProps[key]) {
155
+ changes[key] = { from: oldProps[key] ?? '', to: newProps[key] ?? '' };
156
+ }
157
+ }
158
+ const isActionable = INTERACTIVE_ROLES.has(newEl.role);
159
+ let recommendedAction;
160
+ if (isActionable) {
161
+ recommendedAction = newEl.role === 'link' ? 'navigate' : 'click';
162
+ }
163
+ richChanged.push({
164
+ ref: changedRef.id,
165
+ role: changedRef.role,
166
+ label: changedRef.label,
167
+ changes,
168
+ actionable: isActionable,
169
+ recommended_action: recommendedAction,
170
+ });
171
+ }
172
+ // ── Phase 5d: Iframe-aware content tracking ──────────────────────────────
173
+ const newFrameCounts = new Map();
174
+ for (const el of newElements) {
175
+ const fk = el.frameUrl || '__top__';
176
+ newFrameCounts.set(fk, (newFrameCounts.get(fk) || 0) + 1);
177
+ }
178
+ const oldFrames = frameSnapshots.get(targetId) || new Map();
179
+ const iframeChanges = [];
180
+ // Detect added/changed frames
181
+ for (const [fk, count] of newFrameCounts) {
182
+ if (fk === '__top__')
183
+ continue;
184
+ const oldCount = oldFrames.get(fk);
185
+ if (oldCount === undefined) {
186
+ iframeChanges.push({ frameUrl: fk, status: "added", elementCount: count });
187
+ }
188
+ else if (oldCount !== count) {
189
+ iframeChanges.push({ frameUrl: fk, status: "changed", elementCount: count });
190
+ }
191
+ }
192
+ // Detect removed frames
193
+ for (const [fk] of oldFrames) {
194
+ if (fk === '__top__')
195
+ continue;
196
+ if (!newFrameCounts.has(fk)) {
197
+ iframeChanges.push({ frameUrl: fk, status: "removed", elementCount: 0 });
198
+ }
199
+ }
200
+ frameSnapshots.set(targetId, newFrameCounts);
201
+ // Update property snapshot
202
+ const newPropMap = new Map();
203
+ for (const el of newElements) {
204
+ newPropMap.set(el.id, serializeProps(el, targetId));
205
+ }
206
+ propSnapshots.set(targetId, { url, props: newPropMap });
207
+ resolve({
208
+ added: basicDiff.added,
209
+ removed: basicDiff.removed,
210
+ changed: richChanged,
211
+ stable_count: basicDiff.stable_count,
212
+ ...(iframeChanges.length > 0 ? { iframes: iframeChanges } : {}),
213
+ });
214
+ });
215
+ const timeoutPromise = new Promise((resolve) => {
216
+ setTimeout(() => resolve({ delta_timeout: true }), 5000);
217
+ });
218
+ return Promise.race([computePromise, timeoutPromise]);
219
+ }
220
+ /**
221
+ * Clear snapshot for a tab (called on hard navigation).
222
+ */
223
+ export function clearSnapshot(targetId) {
224
+ snapshots.delete(targetId);
225
+ frameSnapshots.delete(targetId);
226
+ }
227
+ /**
228
+ * Clear all snapshots (called on CDP disconnect).
229
+ */
230
+ export function clearAllSnapshots() {
231
+ snapshots.clear();
232
+ propSnapshots.clear();
233
+ frameSnapshots.clear();
234
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=delta.test.d.ts.map