@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,11 @@
1
+ /**
2
+ * Read a domain-specific knowledge hint.
3
+ * Returns the first non-empty line from the file, trimmed to 20 tokens (≈120 chars).
4
+ * Returns undefined if no file exists for this domain.
5
+ */
6
+ export declare function readKnowledgeHint(hostname: string): Promise<string | undefined>;
7
+ /**
8
+ * Append a discovery note to the domain knowledge file.
9
+ * Called automatically by healing.ts after recovering a lost element (Rule 16).
10
+ */
11
+ export declare function appendKnowledgeHint(hostname: string, note: string): Promise<void>;
@@ -0,0 +1,39 @@
1
+ // src/intelligence/hints.ts
2
+ // JIT knowledge hints (Rule 16).
3
+ // Reads ~/.weavetab/knowledge/[domain].md and returns max-20-token hint.
4
+ // Call appendKnowledgeHint() from any tool when you discover a site-specific quirk.
5
+ import { readFile } from "fs/promises";
6
+ import { join } from "path";
7
+ import { GLOBAL_CONFIG_DIR } from "../config/loader.js";
8
+ const KNOWLEDGE_DIR = join(GLOBAL_CONFIG_DIR, "knowledge");
9
+ /**
10
+ * Read a domain-specific knowledge hint.
11
+ * Returns the first non-empty line from the file, trimmed to 20 tokens (≈120 chars).
12
+ * Returns undefined if no file exists for this domain.
13
+ */
14
+ export async function readKnowledgeHint(hostname) {
15
+ const safe = hostname.replace(/[^a-zA-Z0-9.-]/g, "_");
16
+ const filePath = join(KNOWLEDGE_DIR, `${safe}.md`);
17
+ try {
18
+ const content = await readFile(filePath, "utf-8");
19
+ const firstLine = content.split("\n").find((l) => l.trim().length > 0);
20
+ if (!firstLine)
21
+ return undefined;
22
+ // Trim to ~120 chars (~20 tokens)
23
+ return firstLine.trim().slice(0, 120);
24
+ }
25
+ catch {
26
+ return undefined;
27
+ }
28
+ }
29
+ /**
30
+ * Append a discovery note to the domain knowledge file.
31
+ * Called automatically by healing.ts after recovering a lost element (Rule 16).
32
+ */
33
+ export async function appendKnowledgeHint(hostname, note) {
34
+ const { appendFile } = await import("fs/promises");
35
+ const safe = hostname.replace(/[^a-zA-Z0-9.-]/g, "_");
36
+ const filePath = join(KNOWLEDGE_DIR, `${safe}.md`);
37
+ const timestamp = new Date().toISOString().slice(0, 10);
38
+ await appendFile(filePath, `\n[${timestamp}] ${note.trim()}\n`, "utf-8");
39
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Intent resolution system for interaction tools
3
+ * Classifies targets and determines optimal interaction strategies before dispatch
4
+ */
5
+ import CDP from "chrome-remote-interface";
6
+ export interface TargetClassification {
7
+ /** Element type (button, input, link, etc.) */
8
+ type: string;
9
+ /** What interaction is semantically expected */
10
+ interactionExpected: 'click' | 'type' | 'select' | 'check' | 'upload' | 'scroll';
11
+ /** Likely success condition */
12
+ successCondition: string;
13
+ /** Fallback strategies if primary fails */
14
+ fallbackStrategies: string[];
15
+ /** Whether this is a React/Vue controlled component */
16
+ isControlled: boolean;
17
+ /** Estimated complexity (simple/complex) */
18
+ complexity: 'simple' | 'complex';
19
+ }
20
+ /**
21
+ * Classify a target element to determine optimal interaction strategy
22
+ * Cheap (1 CDP call) classification that caches results per element
23
+ */
24
+ export declare function classifyTarget(session: CDP.Client, selector: string): Promise<TargetClassification>;
25
+ /**
26
+ * Clear classification cache for a specific target or all targets
27
+ */
28
+ export declare function clearClassificationCache(selector?: string): void;
29
+ /**
30
+ * Get optimal strategy based on classification
31
+ * Returns the best strategy to try first
32
+ */
33
+ export declare function getOptimalStrategy(classification: TargetClassification): string;
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Intent resolution system for interaction tools
3
+ * Classifies targets and determines optimal interaction strategies before dispatch
4
+ */
5
+ // Simple in-memory cache for classification results
6
+ const classificationCache = new Map();
7
+ const CACHE_TTL_MS = 30000; // 30 seconds
8
+ const CACHE_MAX_SIZE = 500; // hard cap to prevent unbounded growth
9
+ // Stale entries are checked on read, but a background sweep removes expired
10
+ // entries every 60 s and a max-size cap (evict oldest first) keeps the cache
11
+ // from growing without bound even for keys that are never looked up again.
12
+ setInterval(() => {
13
+ const now = Date.now();
14
+ for (const [k, v] of classificationCache.entries()) {
15
+ if (now - v.timestamp >= CACHE_TTL_MS)
16
+ classificationCache.delete(k);
17
+ }
18
+ }, 60_000).unref(); // .unref() so this interval never prevents process exit
19
+ /**
20
+ * Classify a target element to determine optimal interaction strategy
21
+ * Cheap (1 CDP call) classification that caches results per element
22
+ */
23
+ export async function classifyTarget(session, selector) {
24
+ // Check cache first
25
+ const cacheKey = (session.target?.targetId || 'default') + ":" + selector;
26
+ const cached = classificationCache.get(cacheKey);
27
+ if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
28
+ return cached.classification;
29
+ }
30
+ // Enforce max-size cap before inserting a new entry (evict oldest)
31
+ if (classificationCache.size >= CACHE_MAX_SIZE) {
32
+ const firstKey = classificationCache.keys().next().value;
33
+ if (firstKey !== undefined)
34
+ classificationCache.delete(firstKey);
35
+ }
36
+ try {
37
+ const { result } = await session.Runtime.evaluate({
38
+ expression: `(function(sel) {
39
+ try {
40
+ var el = document.querySelector(sel);
41
+ if (!el) return JSON.stringify({ error: 'not_found' });
42
+
43
+ var tag = el.tagName.toLowerCase();
44
+ var typeAttr = (el.getAttribute('type') || 'text').toLowerCase();
45
+ var role = el.getAttribute('role') || '';
46
+ var isControlled = false;
47
+
48
+ // Detect React/Vue controlled components
49
+ if ((el as any)._reactInternalFiber || (el as any)._reactRootContainer ||
50
+ (el as any).__vueApp__ || (el as any).__VUE__ ||
51
+ el.hasAttribute('data-reactroot')) {
52
+ isControlled = true;
53
+ }
54
+
55
+ var classification: any = {
56
+ type: tag,
57
+ interactionExpected: 'click',
58
+ successCondition: 'element_clicked',
59
+ fallbackStrategies: [],
60
+ isControlled: isControlled,
61
+ complexity: 'simple'
62
+ };
63
+
64
+ // Determine interaction type based on element
65
+ if (tag === 'input') {
66
+ if (typeAttr === 'checkbox' || typeAttr === 'radio') {
67
+ classification.interactionExpected = 'check';
68
+ classification.successCondition = 'checked_state_changed';
69
+ classification.fallbackStrategies = ['click', 'property_set'];
70
+ } else if (typeAttr === 'file') {
71
+ classification.interactionExpected = 'upload';
72
+ classification.successCondition = 'file_selected';
73
+ classification.fallbackStrategies = ['click', 'direct_upload'];
74
+ } else if (typeAttr === 'submit' || typeAttr === 'button') {
75
+ classification.interactionExpected = 'click';
76
+ classification.successCondition = 'form_submitted_or_clicked';
77
+ classification.fallbackStrategies = ['click', 'form_submit'];
78
+ } else {
79
+ classification.interactionExpected = 'type';
80
+ classification.successCondition = 'value_changed';
81
+ classification.fallbackStrategies = ['native_setter', 'react_setter', 'dispatch_events'];
82
+ classification.complexity = isControlled ? 'complex' : 'simple';
83
+ }
84
+ } else if (tag === 'textarea') {
85
+ classification.interactionExpected = 'type';
86
+ classification.successCondition = 'value_changed';
87
+ classification.fallbackStrategies = ['native_setter', 'react_setter', 'dispatch_events'];
88
+ classification.complexity = isControlled ? 'complex' : 'simple';
89
+ } else if (tag === 'select') {
90
+ classification.interactionExpected = 'select';
91
+ classification.successCondition = 'selection_changed';
92
+ classification.fallbackStrategies = ['property_set', 'click_option'];
93
+ classification.complexity = isControlled ? 'complex' : 'simple';
94
+ } else if (tag === 'button' || role === 'button') {
95
+ classification.interactionExpected = 'click';
96
+ classification.successCondition = 'clicked';
97
+ classification.fallbackStrategies = ['click', 'keyboard_enter'];
98
+ } else if (tag === 'a') {
99
+ classification.interactionExpected = 'click';
100
+ classification.successCondition = 'navigated';
101
+ classification.fallbackStrategies = ['click', 'direct_navigate'];
102
+ } else if (tag === 'div' || tag === 'span') {
103
+ if (role === 'textbox' || role === 'combobox' || (el as any).isContentEditable) {
104
+ classification.interactionExpected = 'type';
105
+ classification.successCondition = 'text_changed';
106
+ classification.fallbackStrategies = ['text_content_set', 'dispatch_events'];
107
+ classification.complexity = 'complex';
108
+ } else {
109
+ classification.interactionExpected = 'click';
110
+ classification.successCondition = 'clicked';
111
+ classification.fallbackStrategies = ['click'];
112
+ }
113
+ }
114
+
115
+ return JSON.stringify(classification);
116
+ } catch(e) {
117
+ return JSON.stringify({ error: (e as Error).message });
118
+ }
119
+ })(${JSON.stringify(selector)})`,
120
+ returnByValue: true,
121
+ });
122
+ const classification = result?.value;
123
+ // Cache the result
124
+ if (classification && !classification.error) {
125
+ classificationCache.set(cacheKey, { classification, timestamp: Date.now() });
126
+ }
127
+ return classification;
128
+ }
129
+ catch (error) {
130
+ // Return safe default on error
131
+ return {
132
+ type: 'unknown',
133
+ interactionExpected: 'click',
134
+ successCondition: 'clicked',
135
+ fallbackStrategies: ['click'],
136
+ isControlled: false,
137
+ complexity: 'simple'
138
+ };
139
+ }
140
+ }
141
+ /**
142
+ * Clear classification cache for a specific target or all targets
143
+ */
144
+ export function clearClassificationCache(selector) {
145
+ if (selector) {
146
+ // Clear specific selector entries
147
+ for (const [key] of classificationCache) {
148
+ if (key.endsWith(":" + selector)) {
149
+ classificationCache.delete(key);
150
+ }
151
+ }
152
+ }
153
+ else {
154
+ // Clear all cache
155
+ classificationCache.clear();
156
+ }
157
+ }
158
+ /**
159
+ * Get optimal strategy based on classification
160
+ * Returns the best strategy to try first
161
+ */
162
+ export function getOptimalStrategy(classification) {
163
+ // For controlled components, prefer React-specific strategies
164
+ if (classification.isControlled) {
165
+ if (classification.interactionExpected === 'type') {
166
+ return 'react_setter';
167
+ }
168
+ }
169
+ // Default to first fallback strategy
170
+ return classification.fallbackStrategies[0] || classification.interactionExpected;
171
+ }
@@ -0,0 +1,66 @@
1
+ import CDP from "chrome-remote-interface";
2
+ export type FailureType = "DEAD_SELECTOR" | "ACTION_FAILED";
3
+ export interface SelectorFingerprint {
4
+ tag?: string;
5
+ classes?: string[];
6
+ role?: string;
7
+ relativeTo?: string;
8
+ structuralIdiom?: string;
9
+ }
10
+ export interface MemoryEntry {
11
+ intent: string;
12
+ selector: string;
13
+ urlPath?: string;
14
+ fingerprint?: SelectorFingerprint;
15
+ successCount: number;
16
+ failureCount: number;
17
+ consecutiveFailures: number;
18
+ lastSuccessVisit: number;
19
+ state: "active" | "drift_detected" | "deprecated";
20
+ }
21
+ export interface NegativeMemoryEntry {
22
+ selector: string;
23
+ urlPath?: string;
24
+ failureType: FailureType;
25
+ expiredAtVisit: number;
26
+ }
27
+ export interface DomainProfile {
28
+ domainFamily?: string;
29
+ strategies: string[];
30
+ memories: MemoryEntry[];
31
+ failedSelectors: NegativeMemoryEntry[];
32
+ shadowDom: boolean;
33
+ framework?: string;
34
+ updatedAt: string;
35
+ visitCount: number;
36
+ }
37
+ export declare function loadDomainProfile(hostname: string): Promise<DomainProfile | null>;
38
+ export declare function saveDomainProfile(hostname: string, updates: Partial<DomainProfile>): Promise<DomainProfile>;
39
+ /**
40
+ * Increment the visit counter for a domain. Call this exactly once per page navigation,
41
+ * NOT on every logMemorySuccess/Failure call.
42
+ */
43
+ export declare function bumpVisitCount(hostname: string): Promise<void>;
44
+ export declare function logMemorySuccess(hostname: string, intent: string, selector: string, fingerprint?: SelectorFingerprint, urlPath?: string): Promise<void>;
45
+ export declare function logMemoryFailure(hostname: string, intent: string, selector: string, failureType: FailureType, urlPath?: string): Promise<void>;
46
+ export interface MemoryHit {
47
+ selector: string;
48
+ fingerprint?: SelectorFingerprint;
49
+ score: number;
50
+ }
51
+ /**
52
+ * Evaluates memory entries and returns the highest confidence memory hit.
53
+ */
54
+ export declare function getBestSelectorForIntent(hostname: string, intent: string, urlPath?: string): Promise<MemoryHit | undefined>;
55
+ /**
56
+ * Detect the frontend framework used by the current page.
57
+ */
58
+ export declare function detectFramework(session: CDP.Client): Promise<string | undefined>;
59
+ /**
60
+ * Check if the page uses Shadow DOM.
61
+ */
62
+ export declare function detectShadowDom(session: CDP.Client): Promise<boolean>;
63
+ /**
64
+ * Format a domain profile as a system hint string.
65
+ */
66
+ export declare function formatDomainHint(hostname: string, profile: DomainProfile): string | undefined;
@@ -0,0 +1,284 @@
1
+ import { readFile, writeFile } from "fs/promises";
2
+ import { join } from "path";
3
+ import { GLOBAL_CONFIG_DIR } from "../config/loader.js";
4
+ import { logDebug } from "../audit/logger.js";
5
+ import fs from "fs";
6
+ function domainFile(hostname) {
7
+ const safe = hostname.replace(/[^a-zA-Z0-9.-]/g, "_");
8
+ const memoryDir = join(GLOBAL_CONFIG_DIR, "memory");
9
+ if (!fs.existsSync(memoryDir))
10
+ fs.mkdirSync(memoryDir, { recursive: true });
11
+ return join(memoryDir, `${safe}.json`);
12
+ }
13
+ // memoryCache is capped at 200 DomainProfile entries (one per unique hostname).
14
+ // When the cap is hit, the oldest entry is evicted (insertion-order LRU) so the
15
+ // cache can't grow without bound over a long session.
16
+ const memoryCache = new Map();
17
+ const MEMORY_CACHE_MAX = 200;
18
+ function memoryCacheSet(hostname, profile) {
19
+ if (memoryCache.size >= MEMORY_CACHE_MAX && !memoryCache.has(hostname)) {
20
+ // Evict the oldest (first) entry
21
+ const firstKey = memoryCache.keys().next().value;
22
+ if (firstKey !== undefined)
23
+ memoryCache.delete(firstKey);
24
+ }
25
+ memoryCache.set(hostname, profile);
26
+ }
27
+ export async function loadDomainProfile(hostname) {
28
+ if (memoryCache.has(hostname)) {
29
+ return memoryCache.get(hostname);
30
+ }
31
+ try {
32
+ const content = await readFile(domainFile(hostname), "utf-8");
33
+ const profile = JSON.parse(content);
34
+ // Auto-expire negative memory
35
+ profile.failedSelectors = profile.failedSelectors.filter((neg) => profile.visitCount < neg.expiredAtVisit);
36
+ // Decay and drift detection
37
+ for (const mem of profile.memories) {
38
+ const visitsSinceSuccess = profile.visitCount - mem.lastSuccessVisit;
39
+ if (mem.consecutiveFailures >= 3 && mem.successCount > 5) {
40
+ mem.state = "drift_detected";
41
+ }
42
+ else if (visitsSinceSuccess > 20) {
43
+ // Simple heuristic: if we visited the domain 20 times and didn't use this successfully, decay
44
+ // For now just keep it active, confidence scoring will handle the numeric decay
45
+ if (mem.state !== "deprecated") {
46
+ mem.state = "active";
47
+ }
48
+ }
49
+ }
50
+ memoryCacheSet(hostname, profile);
51
+ return profile;
52
+ }
53
+ catch {
54
+ return null;
55
+ }
56
+ }
57
+ export async function saveDomainProfile(hostname, updates) {
58
+ const existing = await loadDomainProfile(hostname);
59
+ // NOTE: visitCount is NOT bumped here — call bumpVisitCount() only on page navigation.
60
+ const visitCount = existing?.visitCount ?? 0;
61
+ const profile = {
62
+ strategies: [],
63
+ memories: [],
64
+ failedSelectors: [],
65
+ shadowDom: false,
66
+ ...existing,
67
+ ...updates,
68
+ updatedAt: new Date().toISOString(),
69
+ visitCount,
70
+ };
71
+ // Dedup strategies
72
+ if (updates.strategies) {
73
+ profile.strategies = Array.from(new Set([...(existing?.strategies || []), ...updates.strategies]));
74
+ }
75
+ memoryCacheSet(hostname, profile);
76
+ // Gate profile writing on 3+ data points
77
+ let dataPoints = profile.failedSelectors.length;
78
+ for (const mem of profile.memories) {
79
+ dataPoints += (mem.successCount + mem.failureCount);
80
+ }
81
+ if (dataPoints >= 3) {
82
+ await writeFile(domainFile(hostname), JSON.stringify(profile, null, 2), "utf-8");
83
+ }
84
+ return profile;
85
+ }
86
+ /**
87
+ * Increment the visit counter for a domain. Call this exactly once per page navigation,
88
+ * NOT on every logMemorySuccess/Failure call.
89
+ */
90
+ export async function bumpVisitCount(hostname) {
91
+ const existing = await loadDomainProfile(hostname);
92
+ const visitCount = (existing?.visitCount ?? 0) + 1;
93
+ await saveDomainProfile(hostname, { visitCount });
94
+ }
95
+ export async function logMemorySuccess(hostname, intent, selector, fingerprint, urlPath) {
96
+ const profile = (await loadDomainProfile(hostname)) || {
97
+ strategies: [],
98
+ memories: [],
99
+ failedSelectors: [],
100
+ shadowDom: false,
101
+ updatedAt: new Date().toISOString(),
102
+ visitCount: 0,
103
+ };
104
+ let mem = profile.memories.find(m => m.intent === intent && m.selector === selector);
105
+ if (!mem) {
106
+ mem = {
107
+ intent,
108
+ selector,
109
+ urlPath,
110
+ fingerprint,
111
+ successCount: 0,
112
+ failureCount: 0,
113
+ consecutiveFailures: 0,
114
+ lastSuccessVisit: profile.visitCount,
115
+ state: "active"
116
+ };
117
+ profile.memories.push(mem);
118
+ }
119
+ mem.successCount++;
120
+ mem.consecutiveFailures = 0;
121
+ mem.lastSuccessVisit = profile.visitCount;
122
+ if (fingerprint) {
123
+ mem.fingerprint = { ...mem.fingerprint, ...fingerprint };
124
+ }
125
+ mem.state = "active";
126
+ logDebug(`[Memory] Success logged for selector '${selector}' on intent '${intent}'.`);
127
+ // If it was in failedSelectors, remove it
128
+ profile.failedSelectors = profile.failedSelectors.filter(f => f.selector !== selector);
129
+ await saveDomainProfile(hostname, {
130
+ memories: profile.memories,
131
+ failedSelectors: profile.failedSelectors
132
+ });
133
+ }
134
+ export async function logMemoryFailure(hostname, intent, selector, failureType, urlPath) {
135
+ const profile = (await loadDomainProfile(hostname)) || {
136
+ strategies: [],
137
+ memories: [],
138
+ failedSelectors: [],
139
+ shadowDom: false,
140
+ updatedAt: new Date().toISOString(),
141
+ visitCount: 0,
142
+ };
143
+ const mem = profile.memories.find(m => m.intent === intent && m.selector === selector);
144
+ if (mem) {
145
+ mem.failureCount++;
146
+ mem.consecutiveFailures++;
147
+ logDebug(`[Memory] Failure logged for selector '${selector}'. Consecutive failures: ${mem.consecutiveFailures}`);
148
+ if (mem.consecutiveFailures >= 5) {
149
+ logDebug(`[Memory] Selector '${selector}' deprecated due to 5 consecutive failures.`);
150
+ mem.state = "deprecated";
151
+ }
152
+ }
153
+ // Add to negative memory, expires after 50 visits
154
+ const existingNeg = profile.failedSelectors.find(f => f.selector === selector);
155
+ if (existingNeg) {
156
+ existingNeg.expiredAtVisit = profile.visitCount + 50;
157
+ existingNeg.failureType = failureType;
158
+ }
159
+ else {
160
+ profile.failedSelectors.push({
161
+ selector,
162
+ urlPath,
163
+ failureType,
164
+ expiredAtVisit: profile.visitCount + 50
165
+ });
166
+ }
167
+ await saveDomainProfile(hostname, {
168
+ memories: profile.memories,
169
+ failedSelectors: profile.failedSelectors
170
+ });
171
+ }
172
+ /**
173
+ * Evaluates memory entries and returns the highest confidence memory hit.
174
+ */
175
+ export async function getBestSelectorForIntent(hostname, intent, urlPath) {
176
+ const profile = await loadDomainProfile(hostname);
177
+ if (!profile)
178
+ return undefined;
179
+ let validMemories = profile.memories.filter(m => m.intent === intent && m.state !== "deprecated");
180
+ if (urlPath && validMemories.some(m => m.urlPath === urlPath)) {
181
+ // Exact path match takes precedence
182
+ validMemories = validMemories.filter(m => m.urlPath === urlPath);
183
+ }
184
+ else if (urlPath) {
185
+ // If no exact match, try broad match (e.g. same base path) or fallback to global
186
+ const base = urlPath.split('/')[1];
187
+ const baseMatches = validMemories.filter(m => m.urlPath && m.urlPath.startsWith(`/${base}`));
188
+ if (baseMatches.length > 0) {
189
+ validMemories = baseMatches;
190
+ }
191
+ }
192
+ if (validMemories.length === 0)
193
+ return undefined;
194
+ // Score = successRate * decayFactor - penalty
195
+ const scored = validMemories.map(mem => {
196
+ const totalAttempts = mem.successCount + mem.failureCount;
197
+ const confidence = totalAttempts > 0 ? mem.successCount / totalAttempts : 0;
198
+ const visitsSinceSuccess = Math.max(0, profile.visitCount - mem.lastSuccessVisit);
199
+ // Decay factor (staleness): drops 5% per visit without success, floor at 0.1
200
+ const decayFactor = Math.max(0.1, 1 - (visitsSinceSuccess * 0.05));
201
+ // Penalty for recent consecutive failures (reliability cliff)
202
+ const penalty = mem.consecutiveFailures * 0.2;
203
+ const finalScore = (confidence * decayFactor) - penalty;
204
+ logDebug(`[Memory] Evaluated intent '${intent}' on selector '${mem.selector}': Confidence=${confidence.toFixed(2)}, DecayFactor=${decayFactor.toFixed(2)}, Penalty=${penalty.toFixed(2)} => FinalScore=${finalScore.toFixed(2)}`);
205
+ return { ...mem, score: finalScore };
206
+ });
207
+ scored.sort((a, b) => b.score - a.score);
208
+ const best = scored[0];
209
+ if (best.score > 0.5) {
210
+ logDebug(`[Memory] Hit! Skipping LLM. High-confidence selector for '${intent}': ${best.selector} (Score: ${best.score.toFixed(2)})`);
211
+ return { selector: best.selector, fingerprint: best.fingerprint, score: best.score };
212
+ }
213
+ logDebug(`[Memory] Miss. No selector met the 0.5 score threshold for intent '${intent}'. Falling back to DOM heuristic / LLM.`);
214
+ return undefined;
215
+ }
216
+ /**
217
+ * Detect the frontend framework used by the current page.
218
+ */
219
+ export async function detectFramework(session) {
220
+ const checks = [
221
+ { expr: `!!window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && !!document.querySelector('[data-reactroot], [data-reactid]')`, name: 'react' },
222
+ { expr: `!!window.__VUE__ || !!document.querySelector('[data-v-]')`, name: 'vue' },
223
+ { expr: `!!window.angular || !!document.querySelector('[ng-]')`, name: 'angular' },
224
+ { expr: `!!window.LitElement || !!document.querySelector('[lit-element]')`, name: 'lit' },
225
+ { expr: `!!window.SvelteComponent || !!document.querySelector('[svelte]')`, name: 'svelte' },
226
+ { expr: `!!window.NextRouter`, name: 'next' },
227
+ { expr: `!!window.__NUXT__`, name: 'nuxt' },
228
+ { expr: `!!window.Gatsby`, name: 'gatsby' },
229
+ ];
230
+ for (const check of checks) {
231
+ try {
232
+ const resp = await session.Runtime.evaluate({
233
+ expression: `(function() { return ${check.expr}; })()`,
234
+ returnByValue: true,
235
+ });
236
+ if (resp?.result?.value)
237
+ return check.name;
238
+ }
239
+ catch { /* ignore */ }
240
+ }
241
+ return undefined;
242
+ }
243
+ /**
244
+ * Check if the page uses Shadow DOM.
245
+ */
246
+ export async function detectShadowDom(session) {
247
+ try {
248
+ const resp = await session.Runtime.evaluate({
249
+ expression: `(function() {
250
+ return document.querySelectorAll('*').some(el => el.shadowRoot !== null);
251
+ })()`,
252
+ returnByValue: true,
253
+ });
254
+ return !!resp?.result?.value;
255
+ }
256
+ catch {
257
+ return false;
258
+ }
259
+ }
260
+ /**
261
+ * Format a domain profile as a system hint string.
262
+ */
263
+ export function formatDomainHint(hostname, profile) {
264
+ const parts = [];
265
+ if (profile.framework)
266
+ parts.push(`framework: ${profile.framework}`);
267
+ if (profile.shadowDom)
268
+ parts.push('uses Shadow DOM');
269
+ if (profile.strategies.length > 0)
270
+ parts.push(`known strategies: ${profile.strategies.join(', ')}`);
271
+ const activeMemories = profile.memories.filter(m => m.state !== "deprecated");
272
+ if (activeMemories.length > 0) {
273
+ const inputs = activeMemories.map(k => {
274
+ let flag = '';
275
+ if (k.state === 'drift_detected')
276
+ flag = ' [DRIFT_DETECTED: site may have changed]';
277
+ return `${k.intent} (${k.selector})${flag}`;
278
+ }).join('; ');
279
+ parts.push(`known intents: ${inputs}`);
280
+ }
281
+ if (parts.length === 0)
282
+ return undefined;
283
+ return `[Session Memory] ${hostname}: ${parts.join(' | ')}`;
284
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=sessionMemory.test.d.ts.map
@@ -0,0 +1,62 @@
1
+ /**
2
+ * sessionMemory.test.ts — Phase 1.1
3
+ * Integrated into node:test runner (npm test / npm run test:unit).
4
+ * Previously a manual runTests() script; ported to node:test format.
5
+ */
6
+ import { test, before, after } from "node:test";
7
+ import assert from "node:assert/strict";
8
+ import fs from "fs";
9
+ import path from "path";
10
+ import { getBestSelectorForIntent, logMemorySuccess, logMemoryFailure, loadDomainProfile, } from "./sessionMemory.js";
11
+ import { getSystemDir } from "../config/loader.js";
12
+ const hostname = "test.weavetab.dev";
13
+ let profilePath;
14
+ before(() => {
15
+ const memoryDir = getSystemDir("memory");
16
+ profilePath = path.join(memoryDir, `${hostname}.json`);
17
+ if (fs.existsSync(profilePath))
18
+ fs.unlinkSync(profilePath);
19
+ });
20
+ after(() => {
21
+ if (fs.existsSync(profilePath))
22
+ fs.unlinkSync(profilePath);
23
+ });
24
+ test("1: Initial success log — selector is immediately retrievable", async () => {
25
+ await logMemorySuccess(hostname, "Click Login", ".login-btn", { role: "button", tag: "button" });
26
+ const sel = await getBestSelectorForIntent(hostname, "Click Login");
27
+ assert.equal(sel?.selector, ".login-btn", `Expected .login-btn, got ${sel?.selector}`);
28
+ });
29
+ test("2: Dead selector (first strike) — score drops below threshold", async () => {
30
+ // Simulate visit count increment
31
+ const profile = await loadDomainProfile(hostname);
32
+ profile.visitCount++;
33
+ fs.writeFileSync(profilePath, JSON.stringify(profile, null, 2));
34
+ await logMemoryFailure(hostname, "Click Login", ".login-btn", "DEAD_SELECTOR");
35
+ const sel = await getBestSelectorForIntent(hostname, "Click Login");
36
+ assert.equal(sel, undefined, "Expected undefined — score dropped below 0.5 threshold after one failure");
37
+ });
38
+ test("3: Dead selector recovery — new successful selector wins", async () => {
39
+ await logMemorySuccess(hostname, "Click Login", "#login-new", { role: "button", tag: "button" });
40
+ const sel = await getBestSelectorForIntent(hostname, "Click Login");
41
+ assert.equal(sel?.selector, "#login-new", `Expected #login-new, got ${sel?.selector}`);
42
+ });
43
+ test("4: Decay check — both old and new memory entries exist in profile", async () => {
44
+ const profile = await loadDomainProfile(hostname);
45
+ const oldMem = profile.memories.find(m => m.selector === ".login-btn");
46
+ const newMem = profile.memories.find(m => m.selector === "#login-new");
47
+ assert.ok(oldMem !== undefined, "Expected old memory (.login-btn) to still exist");
48
+ assert.ok(newMem !== undefined, "Expected new memory (#login-new) to exist");
49
+ });
50
+ test("5: Negative memory expiration — both selectors decay below threshold after many visits", async () => {
51
+ const profile = await loadDomainProfile(hostname);
52
+ const inFailed = profile.failedSelectors.find(f => f.selector === ".login-btn");
53
+ assert.ok(inFailed !== undefined, "Expected .login-btn in failedSelectors after DEAD_SELECTOR strike");
54
+ // Advance visits far past expiration window (expiration = visitCount + 10).
55
+ // After +15 visits, DecayFactor drops to ~0.25, pulling #login-new below 0.5 threshold too.
56
+ profile.visitCount += 15;
57
+ fs.writeFileSync(profilePath, JSON.stringify(profile, null, 2));
58
+ // Both selectors are now expired / decayed — correct behavior is a miss (force DOM re-resolution)
59
+ const sel = await getBestSelectorForIntent(hostname, "Click Login");
60
+ assert.equal(sel, undefined, "Expected undefined after extreme visit-count decay — selectors should re-confirm via DOM");
61
+ });
62
+ //# sourceMappingURL=sessionMemory.test.js.map