@weavetab/mcp 2.5.0-beta.2 → 2.5.0-beta.4

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 (113) hide show
  1. package/AGENTS.md +1 -1
  2. package/ARCHITECTURE.md +6 -6
  3. package/LICENSE +1 -1
  4. package/README.md +158 -961
  5. package/SECURITY.md +33 -22
  6. package/TROUBLESHOOTING.md +2 -2
  7. package/dist/cdp/bridge.js +2 -1
  8. package/dist/cdp/connector.d.ts +2 -0
  9. package/dist/cdp/connector.js +8 -4
  10. package/dist/cdp/helpers.d.ts +4 -0
  11. package/dist/cdp/helpers.js +181 -0
  12. package/dist/cdp/omni_broker.d.ts +1 -1
  13. package/dist/cdp/omni_broker.js +1 -1
  14. package/dist/cdp/walker.d.ts +25 -1
  15. package/dist/cdp/walker.js +304 -7
  16. package/dist/cli/plugin.js +24 -17
  17. package/dist/cli/updater.d.ts +1 -1
  18. package/dist/cli/updater.js +27 -7
  19. package/dist/config/loader.d.ts +8 -0
  20. package/dist/config/loader.js +41 -2
  21. package/dist/desktop/discovery.d.ts +44 -0
  22. package/dist/desktop/discovery.js +364 -0
  23. package/dist/index.js +95 -15
  24. package/dist/intelligence/marks.d.ts +36 -0
  25. package/dist/intelligence/marks.js +30 -0
  26. package/dist/intelligence/patternCompiler.d.ts +118 -0
  27. package/dist/intelligence/patternCompiler.js +259 -0
  28. package/dist/intelligence/sessionMemory.d.ts +2 -0
  29. package/dist/intelligence/sessionMemory.js +18 -8
  30. package/dist/overlay/injector.js +2 -2
  31. package/dist/plugin/interface.d.ts +76 -2
  32. package/dist/plugin/loader.d.ts +15 -1
  33. package/dist/plugin/loader.js +217 -70
  34. package/dist/plugin/pipeline.d.ts +75 -0
  35. package/dist/plugin/pipeline.js +183 -0
  36. package/dist/plugin/registry.js +3 -2
  37. package/dist/security/secrets.d.ts +1 -0
  38. package/dist/security/secrets.js +13 -0
  39. package/dist/sensors/actionability.js +1 -2
  40. package/dist/sensors/guard.js +4 -10
  41. package/dist/sensors/mutations.js +12 -0
  42. package/dist/sensors/operation_monitor.js +1 -0
  43. package/dist/server.d.ts +1 -1
  44. package/dist/server.js +428 -156
  45. package/dist/state/agent.d.ts +1 -0
  46. package/dist/state/agent.js +3 -0
  47. package/dist/state/delta.d.ts +12 -4
  48. package/dist/state/delta.js +67 -14
  49. package/dist/state/session.d.ts +6 -0
  50. package/dist/state/session.js +27 -0
  51. package/dist/tools/browser_detect.d.ts +16 -3
  52. package/dist/tools/browser_detect.js +50 -5
  53. package/dist/tools/burst.d.ts +17 -8
  54. package/dist/tools/burst.js +236 -77
  55. package/dist/tools/checkpoint.d.ts +38 -0
  56. package/dist/tools/checkpoint.js +271 -0
  57. package/dist/tools/click.d.ts +1 -0
  58. package/dist/tools/click.js +74 -70
  59. package/dist/tools/design_extract.js +1 -1
  60. package/dist/tools/eval.js +1 -1
  61. package/dist/tools/fill.d.ts +20 -1
  62. package/dist/tools/fill.js +199 -40
  63. package/dist/tools/find.d.ts +16 -0
  64. package/dist/tools/find.js +67 -44
  65. package/dist/tools/github/index.d.ts +19 -4
  66. package/dist/tools/github/index.js +40 -4
  67. package/dist/tools/highlight.d.ts +6 -2
  68. package/dist/tools/highlight.js +158 -72
  69. package/dist/tools/inspect.js +1 -1
  70. package/dist/tools/key.js +65 -56
  71. package/dist/tools/navigate.js +29 -34
  72. package/dist/tools/pdf.js +21 -13
  73. package/dist/tools/pointer.d.ts +18 -3
  74. package/dist/tools/pointer.js +89 -1
  75. package/dist/tools/read.d.ts +28 -8
  76. package/dist/tools/read.js +230 -56
  77. package/dist/tools/scrape.js +67 -13
  78. package/dist/tools/screenshot.js +21 -16
  79. package/dist/tools/scroll.js +6 -11
  80. package/dist/tools/select.js +8 -2
  81. package/dist/tools/snapshot.js +3 -34
  82. package/dist/tools/tabs.js +14 -6
  83. package/dist/tools/thought.d.ts +2 -0
  84. package/dist/tools/thought.js +13 -14
  85. package/dist/tools/type.d.ts +2 -0
  86. package/dist/tools/type.js +107 -58
  87. package/dist/tools/type_secret.js +42 -76
  88. package/dist/tools/upload.js +1 -1
  89. package/dist/tools/video_compiler.d.ts +1 -1
  90. package/dist/tools/video_compiler.js +11 -12
  91. package/dist/version.d.ts +5 -0
  92. package/dist/version.js +5 -0
  93. package/docs/DOCUMENTATION.md +21 -32
  94. package/docs/PLUGINS.md +121 -0
  95. package/docs/TOOLS.md +56 -0
  96. package/docs/architecture/blind-injection.md +83 -20
  97. package/docs/architecture/overview.md +49 -49
  98. package/docs/configuration/folders.md +55 -54
  99. package/docs/desktop-automation/electron-apps.md +217 -162
  100. package/docs/get-started/introduction.md +60 -60
  101. package/docs/intelligence/site-profiles.md +151 -0
  102. package/docs/tools/tools.md +631 -293
  103. package/docs/troubleshooting/trouble-loop.md +31 -31
  104. package/ext-dist/background.js +10 -62
  105. package/ext-dist/content.js +333 -920
  106. package/ext-dist/hud/hud.js +9 -9
  107. package/ext-dist/manifest.json +2 -6
  108. package/ext-dist/styles.css +6 -1
  109. package/mcp.json +101 -67
  110. package/package.json +5 -6
  111. package/server.json +3 -3
  112. package/ext-dist/sidepanel/sidepanel.js +0 -25
  113. package/ext-dist/sidepanel.html +0 -547
@@ -1,5 +1,7 @@
1
1
  import { logDebug } from "../audit/logger.js";
2
2
  import { SessionSecretTracker } from "../security/secrets.js";
3
+ import { getStoredRef, getStoredProps } from "../state/delta.js";
4
+ import { getRawCachedActionMap } from "../state/cache.js";
3
5
  // ─── Public types ─────────────────────────────────────────────────────────────
4
6
  export const WT_REGISTRY_INIT_EXPR = `
5
7
  window.__weavetab = window.__weavetab || {};
@@ -162,7 +164,7 @@ export async function buildActionMap(session, options = {}, targetId = "default"
162
164
  try {
163
165
  const parsed = JSON.parse(liveIndexResult.value);
164
166
  if (parsed.source === 'live-index' && parsed.elements && Array.isArray(parsed.elements)) {
165
- logDebug(`[Walker] Successfully read ${parsed.elements.length} elements from extension live index`);
167
+ logDebug(`[Walker] read ${parsed.elements.length} elements from extension live index`);
166
168
  extensionMap = parsed.elements;
167
169
  }
168
170
  }
@@ -309,8 +311,16 @@ export async function buildActionMap(session, options = {}, targetId = "default"
309
311
  else if (role === 'input' && (el.type === 'text' || el.type === 'search')) role = 'textbox';
310
312
  var frameUrl = '';
311
313
  try { if (el.ownerDocument !== document) frameUrl = el.ownerDocument.defaultView.location.href; } catch(e) { frameUrl = 'cross-origin-' + (_coIdx++); }
312
- var isSecret = el.hasAttribute('data-wt-secret') || (el.tagName === 'INPUT' && el.type === 'password');
313
- var labelStr = el.getAttribute('aria-label') || el.innerText || el.value || el.placeholder || '';
314
+ var secretRegex = /password|passcode|pin|passphrase|secret|token|auth.?key|access.?code|mot.?de.?passe|contrase[nñ]a|kennwort|senha/i;
315
+ var isSecret = el.hasAttribute('data-wt-secret') ||
316
+ (el.tagName === 'INPUT' && (
317
+ el.type === 'password' ||
318
+ secretRegex.test(el.getAttribute('autocomplete') || '') ||
319
+ secretRegex.test(el.getAttribute('name') || '') ||
320
+ secretRegex.test(el.getAttribute('id') || '') ||
321
+ secretRegex.test(el.getAttribute('aria-label') || '')
322
+ ));
323
+ var labelStr = el.getAttribute('aria-label') || el.innerText || (isSecret ? '' : el.value) || el.placeholder || '';
314
324
  // For radio/checkbox, serialize checked state as 'true'/'false' so the agent
315
325
  // can see which option is selected without having to scrape the DOM separately.
316
326
  var elType = (el.type || '').toLowerCase();
@@ -345,10 +355,59 @@ export async function buildActionMap(session, options = {}, targetId = "default"
345
355
  break;
346
356
  }
347
357
  }
358
+
359
+ // ── Container context binding ─────────────────────────────────────
360
+ // Walk up the DOM to find a meaningful card/row/list-item heading that
361
+ // describes this element's entity context, e.g. "Pro Plan ($29/mo)".
362
+ var containerCtx = '';
363
+ try {
364
+ var CARD_SEL = '[class*="card"],[class*="row"],[class*="item"],[class*="product"],[class*="plan"],[class*="entry"],[class*="result"],[class*="tile"],[class*="option"],[data-entity]';
365
+ var ancestor = el.parentElement;
366
+ var depth3 = 0;
367
+ while (ancestor && depth3 < 8) {
368
+ if (ancestor.matches && ancestor.matches(CARD_SEL)) {
369
+ var heading = ancestor.querySelector('h1,h2,h3,h4,h5,h6,[class*="title"],[class*="name"],[class*="heading"],[class*="label"]');
370
+ if (heading) {
371
+ var hText = (heading.innerText || heading.textContent || '').trim().slice(0, 60);
372
+ if (hText && hText !== labelStr) { containerCtx = hText; break; }
373
+ }
374
+ var allText = (ancestor.innerText || ancestor.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 60);
375
+ if (allText && allText !== labelStr) { containerCtx = allText; break; }
376
+ }
377
+ ancestor = ancestor.parentElement;
378
+ depth3++;
379
+ }
380
+ } catch(e) {}
381
+
382
+ // ── Modal detection ───────────────────────────────────────────────
383
+ var inModal = false;
384
+ var modalTitleStr = '';
385
+ try {
386
+ var MODAL_SEL = 'dialog[open],[role="dialog"],[aria-modal="true"],[class*="modal"],[class*="overlay"],[class*="drawer"]';
387
+ var modalAnc = el.parentElement;
388
+ var md = 0;
389
+ while (modalAnc && md < 15) {
390
+ if (modalAnc.matches && modalAnc.matches(MODAL_SEL)) {
391
+ inModal = true;
392
+ var mh = modalAnc.querySelector('h1,h2,h3,h4,[class*="title"]');
393
+ if (mh) {
394
+ var lbId = modalAnc.getAttribute('aria-labelledby');
395
+ var lbEl = lbId ? document.getElementById(lbId) : null;
396
+ modalTitleStr = ((lbEl || mh).innerText || (lbEl || mh).textContent || '').trim().slice(0, 60);
397
+ }
398
+ break;
399
+ }
400
+ modalAnc = modalAnc.parentElement;
401
+ md++;
402
+ }
403
+ } catch(e) {}
404
+
348
405
  results.push({
349
406
  id: String(id),
350
407
  role: el.getAttribute('role') || role,
408
+ type: elType || undefined,
351
409
  label: labelStr,
410
+ placeholder: el.placeholder || undefined,
352
411
  value: valStr,
353
412
  x: Math.round(rect.left + rect.width / 2),
354
413
  y: Math.round(rect.top + rect.height / 2),
@@ -357,7 +416,12 @@ export async function buildActionMap(session, options = {}, targetId = "default"
357
416
  textContent: txtContent,
358
417
  className: el.className || '',
359
418
  frameUrl: frameUrl,
360
- structuralIdiom: idiom || undefined
419
+ structuralIdiom: idiom || undefined,
420
+ containerContext: containerCtx || undefined,
421
+ isModal: inModal || undefined,
422
+ modalTitle: modalTitleStr || undefined,
423
+ disabled: (el.disabled || el.getAttribute('aria-disabled') === 'true') || undefined,
424
+ checked: isCheckable ? el.checked : undefined,
361
425
  });
362
426
  } catch (e) {
363
427
  // Silently skip elements that cause errors (cross-origin SVG, detached nodes, etc.)
@@ -446,6 +510,8 @@ export async function buildActionMap(session, options = {}, targetId = "default"
446
510
  role: el.role || el.tag || 'generic',
447
511
  label: el.label || el.text || el.placeholder || '',
448
512
  value: el.value,
513
+ placeholder: el.placeholder || undefined,
514
+ type: el.type || undefined,
449
515
  group: 'main_content',
450
516
  fingerprint: `${el.role || el.tag || 'generic'}:${el.label || el.text || ''}:${el.id}`,
451
517
  depth: 0,
@@ -459,6 +525,10 @@ export async function buildActionMap(session, options = {}, targetId = "default"
459
525
  className: el.className || el.attributes?.class || '',
460
526
  structuralIdiom: el.structuralIdiom || undefined,
461
527
  frameUrl: el.frameUrl || undefined,
528
+ containerContext: el.containerContext || undefined,
529
+ isModal: el.isModal || undefined,
530
+ modalTitle: el.modalTitle || undefined,
531
+ disabled: el.disabled || undefined,
462
532
  }));
463
533
  // Post-mapping sanitizer — catch any w:undefined/w:null and reassign.
464
534
  // Uses a stable hash derived from the element's fingerprint, coordinates, and
@@ -590,7 +660,15 @@ export async function buildActionMap(session, options = {}, targetId = "default"
590
660
  if (options.limit && filtered.length > options.limit) {
591
661
  filtered = filtered.slice(0, options.limit);
592
662
  }
593
- return { url, title, elements: filtered, targetId };
663
+ // Apply session secret redaction over all element labels, values, placeholders
664
+ const redactedElements = filtered.map(el => ({
665
+ ...el,
666
+ label: targetId ? SessionSecretTracker.redact(targetId, el.label) : el.label,
667
+ value: el.value !== undefined ? (targetId ? SessionSecretTracker.redact(targetId, el.value) : el.value) : undefined,
668
+ placeholder: el.placeholder ? (targetId ? SessionSecretTracker.redact(targetId, el.placeholder) : el.placeholder) : undefined,
669
+ textContent: el.textContent ? (targetId ? SessionSecretTracker.redact(targetId, el.textContent) : el.textContent) : undefined,
670
+ }));
671
+ return { url, title, elements: redactedElements, targetId };
594
672
  }
595
673
  // ─── toRefEntries — converts ActionElements to RefEntry for delta tracking ───
596
674
  export function toRefEntries(elements) {
@@ -600,7 +678,7 @@ export function toRefEntries(elements) {
600
678
  label: el.label,
601
679
  type: el.type,
602
680
  href: el.href,
603
- disabled: el.disabled,
681
+ disabled: el.disabled ?? false,
604
682
  value: el.value,
605
683
  fingerprint: el.fingerprint,
606
684
  }));
@@ -888,11 +966,17 @@ export async function buildAXTreeMap(session, _options = {}, targetId = "default
888
966
  else {
889
967
  value = node.value?.value !== undefined ? String(node.value.value) : undefined;
890
968
  }
891
- const isSecretRole = role === 'password' || node.name?.value?.toLowerCase().includes('password');
969
+ const SECRET_TERMS = /password|passcode|pin|passphrase|secret|token|auth.?key|access.?code|mot.?de.?passe|contrase[nñ]a|kennwort|senha|пароль|密碼|密码/i;
970
+ const isSecretRole = role === 'password' || SECRET_TERMS.test(node.name?.value || '') || SECRET_TERMS.test(node.description?.value || '');
892
971
  if (isSecretRole || (node.backendDOMNodeId !== undefined && SessionSecretTracker.isNodeSecret(targetId, node.backendDOMNodeId))) {
893
972
  label = '[REDACTED]';
894
973
  value = '[REDACTED]';
895
974
  }
975
+ else {
976
+ label = targetId ? SessionSecretTracker.redact(targetId, label) : label;
977
+ if (value !== undefined)
978
+ value = targetId ? SessionSecretTracker.redact(targetId, value) : value;
979
+ }
896
980
  // Use globally-unique sequential ID (Bug #5 fix: was local idCounter = 1).
897
981
  const id = dedupeRefId(`w:${axIdCursor++}`);
898
982
  elements.push({
@@ -944,3 +1028,216 @@ export async function buildAXTreeMap(session, _options = {}, targetId = "default
944
1028
  axTreeCacheSet(url, map);
945
1029
  return map;
946
1030
  }
1031
+ /**
1032
+ * Sanitizes element IDs in an ActionElement list, replacing empty or malformed
1033
+ * IDs with sequential fallback IDs.
1034
+ */
1035
+ export function sanitizeElementIds(els, counter) {
1036
+ let fixed = 0;
1037
+ for (const el of els) {
1038
+ if (!el.id || el.id === "w:undefined" || el.id === "w:null" || el.id === "w:") {
1039
+ el.id = `w:${counter.value++}`;
1040
+ fixed++;
1041
+ }
1042
+ }
1043
+ return fixed;
1044
+ }
1045
+ /**
1046
+ * Recovers a detached or invalidated w:NN ref on reactive SPAs.
1047
+ * When hydration or DOM re-renders detach an element, this helper executes a sub-5ms
1048
+ * multi-tier recovery heuristic (connected node verification, stored CSS selector,
1049
+ * exact label/aria, placeholder/name match, and fuzzy substring) and re-stamps
1050
+ * data-wt-id="NN" onto the live DOM node so subsequent interaction succeeds.
1051
+ */
1052
+ export async function recoverDetachedRef(session, targetId, ref, preferredCandidate) {
1053
+ const refNumericId = ref.replace(/^w:/, '');
1054
+ // Extract known metadata for this ref
1055
+ const storedRef = getStoredRef(targetId, ref);
1056
+ const storedProps = getStoredProps(targetId, ref);
1057
+ const rawMap = getRawCachedActionMap(targetId);
1058
+ const cachedEl = rawMap?.elements?.find(e => e.id === ref || e.id === `w:${refNumericId}`);
1059
+ const candidateLabel = preferredCandidate?.label || storedProps?.label || storedRef?.label || cachedEl?.label || '';
1060
+ const candidatePlaceholder = preferredCandidate?.placeholder || storedProps?.placeholder || cachedEl?.placeholder || '';
1061
+ const candidateRole = preferredCandidate?.role || storedProps?.role || storedRef?.role || cachedEl?.role || '';
1062
+ const candidateSelector = preferredCandidate?.selector || cachedEl?.selector || (storedProps?.className ? `.${storedProps.className.split(' ')[0]}` : undefined);
1063
+ const candidateText = preferredCandidate?.textContent || storedProps?.textContent || cachedEl?.textContent || '';
1064
+ const meta = {
1065
+ label: candidateLabel === '[REDACTED]' ? '' : candidateLabel,
1066
+ placeholder: candidatePlaceholder === '[REDACTED]' ? '' : candidatePlaceholder,
1067
+ role: candidateRole,
1068
+ selector: candidateSelector,
1069
+ textContent: candidateText === '[REDACTED]' ? '' : candidateText,
1070
+ };
1071
+ try {
1072
+ const { result } = await session.Runtime.evaluate({
1073
+ expression: `(function(refId, meta) {
1074
+ // 1. Check if node with data-wt-id=refId is already in DOM, connected, and visible
1075
+ const existing = document.querySelector('[data-wt-id="' + refId + '"]');
1076
+ if (existing && existing.isConnected) {
1077
+ const r = existing.getBoundingClientRect();
1078
+ if (r.width > 0 && r.height > 0) {
1079
+ return {
1080
+ success: true,
1081
+ strategy: 'connected_existing',
1082
+ x: Math.round(r.left + r.width / 2),
1083
+ y: Math.round(r.top + r.height / 2),
1084
+ label: existing.getAttribute('aria-label') || (existing.innerText || existing.textContent || '').trim() || existing.placeholder || '',
1085
+ role: existing.getAttribute('role') || existing.tagName.toLowerCase()
1086
+ };
1087
+ }
1088
+ }
1089
+
1090
+ function isValidCandidate(el) {
1091
+ if (!el || !el.isConnected) return false;
1092
+ const r = el.getBoundingClientRect();
1093
+ if (r.width <= 0 || r.height <= 0) return false;
1094
+ try {
1095
+ const style = window.getComputedStyle(el);
1096
+ if (style.visibility === 'hidden' || style.display === 'none') return false;
1097
+ } catch(e) {}
1098
+ return true;
1099
+ }
1100
+
1101
+ // 2. Strategy: Stored Selector (if stable CSS selector recorded)
1102
+ if (meta.selector && !meta.selector.startsWith('[data-wt-id')) {
1103
+ try {
1104
+ const el = document.querySelector(meta.selector);
1105
+ if (isValidCandidate(el)) {
1106
+ el.setAttribute('data-wt-id', refId);
1107
+ const r = el.getBoundingClientRect();
1108
+ return {
1109
+ success: true,
1110
+ strategy: 'stored_selector',
1111
+ x: Math.round(r.left + r.width / 2),
1112
+ y: Math.round(r.top + r.height / 2),
1113
+ label: el.getAttribute('aria-label') || (el.innerText || el.textContent || '').trim() || el.placeholder || '',
1114
+ role: el.getAttribute('role') || el.tagName.toLowerCase()
1115
+ };
1116
+ }
1117
+ } catch(e) {}
1118
+ }
1119
+
1120
+ const normLabel = (meta.label || '').toLowerCase().trim();
1121
+ const normPlaceholder = (meta.placeholder || '').toLowerCase().trim();
1122
+
1123
+ const candidates = Array.from(document.querySelectorAll(
1124
+ 'button, a, input, select, textarea, [role="button"], [role="link"], [role="textbox"], [role="checkbox"], [role="tab"], [tabindex="0"]'
1125
+ ));
1126
+
1127
+ // 3. Strategy: Exact Label / Text / Aria match
1128
+ if (normLabel) {
1129
+ for (const c of candidates) {
1130
+ if (!isValidCandidate(c)) continue;
1131
+ const aria = (c.getAttribute('aria-label') || '').toLowerCase().trim();
1132
+ const text = ((c.innerText || c.textContent || '')).toLowerCase().trim();
1133
+ const val = (c.value || '').toLowerCase().trim();
1134
+ const placeholder = (c.placeholder || '').toLowerCase().trim();
1135
+ const name = (c.getAttribute('name') || '').toLowerCase().trim();
1136
+
1137
+ if (aria === normLabel || text === normLabel || placeholder === normLabel || name === normLabel || (val && val === normLabel)) {
1138
+ c.setAttribute('data-wt-id', refId);
1139
+ const r = c.getBoundingClientRect();
1140
+ return {
1141
+ success: true,
1142
+ strategy: 'exact_label',
1143
+ x: Math.round(r.left + r.width / 2),
1144
+ y: Math.round(r.top + r.height / 2),
1145
+ label: c.getAttribute('aria-label') || (c.innerText || c.textContent || '').trim() || c.placeholder || '',
1146
+ role: c.getAttribute('role') || c.tagName.toLowerCase()
1147
+ };
1148
+ }
1149
+ }
1150
+ }
1151
+
1152
+ // 4. Strategy: Placeholder or Name match
1153
+ if (normPlaceholder) {
1154
+ for (const c of candidates) {
1155
+ if (!isValidCandidate(c)) continue;
1156
+ const placeholder = (c.placeholder || '').toLowerCase().trim();
1157
+ const name = (c.getAttribute('name') || '').toLowerCase().trim();
1158
+ if (placeholder === normPlaceholder || name === normPlaceholder) {
1159
+ c.setAttribute('data-wt-id', refId);
1160
+ const r = c.getBoundingClientRect();
1161
+ return {
1162
+ success: true,
1163
+ strategy: 'placeholder_or_name',
1164
+ x: Math.round(r.left + r.width / 2),
1165
+ y: Math.round(r.top + r.height / 2),
1166
+ label: c.placeholder || c.getAttribute('name') || '',
1167
+ role: c.getAttribute('role') || c.tagName.toLowerCase()
1168
+ };
1169
+ }
1170
+ }
1171
+ }
1172
+
1173
+ // 5. Strategy: Fuzzy Substring Match (if label length >= 3)
1174
+ if (normLabel && normLabel.length >= 3) {
1175
+ let bestMatch = null;
1176
+ let minDiff = Infinity;
1177
+ for (const c of candidates) {
1178
+ if (!isValidCandidate(c)) continue;
1179
+ const text = ((c.innerText || c.textContent || '')).toLowerCase().trim();
1180
+ const aria = (c.getAttribute('aria-label') || '').toLowerCase().trim();
1181
+ const candStr = aria || text;
1182
+ if (candStr && candStr.includes(normLabel)) {
1183
+ const diff = candStr.length - normLabel.length;
1184
+ if (diff >= 0 && diff < minDiff) {
1185
+ minDiff = diff;
1186
+ bestMatch = c;
1187
+ }
1188
+ }
1189
+ }
1190
+ if (bestMatch) {
1191
+ bestMatch.setAttribute('data-wt-id', refId);
1192
+ const r = bestMatch.getBoundingClientRect();
1193
+ return {
1194
+ success: true,
1195
+ strategy: 'fuzzy_label',
1196
+ x: Math.round(r.left + r.width / 2),
1197
+ y: Math.round(r.top + r.height / 2),
1198
+ label: bestMatch.getAttribute('aria-label') || (bestMatch.innerText || bestMatch.textContent || '').trim() || bestMatch.placeholder || '',
1199
+ role: bestMatch.getAttribute('role') || bestMatch.tagName.toLowerCase()
1200
+ };
1201
+ }
1202
+ }
1203
+
1204
+ return { success: false };
1205
+ })(${JSON.stringify(refNumericId)}, ${JSON.stringify(meta)})`,
1206
+ returnByValue: true,
1207
+ });
1208
+ const res = result?.value;
1209
+ if (res && res.success) {
1210
+ const recoveredElement = {
1211
+ id: ref.startsWith('w:') ? ref : `w:${ref}`,
1212
+ role: res.role || candidateRole || 'button',
1213
+ label: res.label || candidateLabel || 'recovered-element',
1214
+ x: res.x,
1215
+ y: res.y,
1216
+ selector: `[data-wt-id="${refNumericId}"]`,
1217
+ group: "utility",
1218
+ depth: 0,
1219
+ fingerprint: `${res.role || candidateRole}:${res.label || candidateLabel}`,
1220
+ };
1221
+ // Patch the raw cache if present so subsequent lookups hit
1222
+ if (rawMap && rawMap.elements) {
1223
+ const existingIdx = rawMap.elements.findIndex(e => e.id === recoveredElement.id);
1224
+ if (existingIdx !== -1) {
1225
+ rawMap.elements[existingIdx] = recoveredElement;
1226
+ }
1227
+ else {
1228
+ rawMap.elements.push(recoveredElement);
1229
+ }
1230
+ }
1231
+ return {
1232
+ recovered: true,
1233
+ element: recoveredElement,
1234
+ strategy: res.strategy,
1235
+ warning: `(Auto-recovered detached ref '${ref}' via ${res.strategy}: '${recoveredElement.label}')`,
1236
+ };
1237
+ }
1238
+ }
1239
+ catch (err) {
1240
+ logDebug(`[Walker] Detached ref recovery failed for ${ref}: ${err}`);
1241
+ }
1242
+ return { recovered: false };
1243
+ }
@@ -5,17 +5,18 @@ import ora from "ora";
5
5
  import path from "node:path";
6
6
  import { spawn } from "node:child_process";
7
7
  import { createRequire } from "node:module";
8
+ import { WEAVETAB_VERSION } from "../version.js";
8
9
  const _require = createRequire(import.meta.url);
9
10
  function getVersion() {
10
11
  try {
11
- return _require("../../package.json").version || "2.5.0-beta.2";
12
+ return _require("../../package.json").version || WEAVETAB_VERSION;
12
13
  }
13
14
  catch {
14
- return "2.5.0-beta.2";
15
+ return WEAVETAB_VERSION;
15
16
  }
16
17
  }
17
- import { getVerifiedPackages, resolvePluginTrust, resolvePluginCandidates, REGISTRY_URL, } from "../plugin/registry.js";
18
- import { listInstalledPlugins, inspectInstalledPackage } from "../plugin/loader.js";
18
+ import { getVerifiedPackages, resolvePluginTrust, resolvePluginCandidates, invalidateRegistryCache, REGISTRY_URL, } from "../plugin/registry.js";
19
+ import { listInstalledPlugins, inspectInstalledPackage, normalizePermissions } from "../plugin/loader.js";
19
20
  // Helper to run child process safely
20
21
  function runCommand(cmd, cmdArgs, cwd = process.cwd()) {
21
22
  return new Promise((resolve) => {
@@ -23,7 +24,7 @@ function runCommand(cmd, cmdArgs, cwd = process.cwd()) {
23
24
  const executable = isWin && !cmd.endsWith(".cmd") && !cmd.endsWith(".exe") ? `${cmd}.cmd` : cmd;
24
25
  const child = spawn(executable, cmdArgs, {
25
26
  cwd,
26
- shell: isWin,
27
+ shell: false,
27
28
  stdio: ["ignore", "pipe", "pipe"],
28
29
  });
29
30
  let stdout = "";
@@ -167,14 +168,14 @@ export async function runPluginAdd(inputName, args = []) {
167
168
  }
168
169
  return;
169
170
  }
170
- spinner.succeed(pc.green(`Successfully installed ${pc.bold(selectedPkg)}`));
171
+ spinner.succeed(pc.green(`installed ${pc.bold(selectedPkg)}`));
171
172
  // Inspect installed package for weavetab.json
172
173
  const targetDir = path.join(process.cwd(), "node_modules", selectedPkg);
173
174
  const installedInfo = inspectInstalledPackage(targetDir);
174
175
  if (installedInfo && installedInfo.manifest) {
175
176
  const manifest = installedInfo.manifest;
176
177
  const tools = manifest.tools || [];
177
- const permissions = manifest.permissions || {};
178
+ const permissions = normalizePermissions(manifest.permissions);
178
179
  const infoLines = [
179
180
  `${pc.bold("Plugin Details:")}`,
180
181
  ` ${pc.dim("Name:")} ${manifest.name || selectedPkg}`,
@@ -387,7 +388,7 @@ export async function runPluginRemove(pkgName, args = []) {
387
388
  console.error(pc.dim(result.stderr));
388
389
  return;
389
390
  }
390
- spinner.succeed(pc.green(`Successfully removed ${pc.bold(pkgName)}`));
391
+ spinner.succeed(pc.green(`removed ${pc.bold(pkgName)}`));
391
392
  console.log(pc.dim("The plugin and its tools have been uninstalled.\n"));
392
393
  }
393
394
  // ---------------------------------------------------------------------------
@@ -419,7 +420,12 @@ export async function runPluginVerify(pkgName) {
419
420
  // 7. MAIN CLI ROUTER FOR PLUGINS (User / Operations Mode)
420
421
  // ---------------------------------------------------------------------------
421
422
  export async function runPluginCli(args) {
422
- const subcmd = args[0]?.toLowerCase();
423
+ if (args.includes("--refresh")) {
424
+ invalidateRegistryCache();
425
+ console.log(pc.green("[OK] Plugin registry cache refreshed.\n"));
426
+ }
427
+ const cleanArgs = args.filter((a) => a !== "--refresh");
428
+ const subcmd = cleanArgs[0]?.toLowerCase();
423
429
  if (!subcmd || subcmd === "--help" || subcmd === "-h" || subcmd === "help") {
424
430
  console.log();
425
431
  console.log(boxen(pc.bold(pc.cyan(" WEAVETAB PLUGIN MANAGER ")), { padding: 0, borderStyle: "round", borderColor: "cyan" }));
@@ -428,7 +434,7 @@ export async function runPluginCli(args) {
428
434
  console.log(` weavetab plugin <command> [options]`);
429
435
  console.log();
430
436
  console.log(pc.bold("COMMANDS"));
431
- console.log(` ${pc.cyan("add <pkg>")} Install a plugin (resolves official @weavetab/ & community)`);
437
+ console.log(` ${pc.cyan("add <pkg>")} Install any plugin by npm package name (e.g. @fy2ne/wt-youtube, my-plugin)`);
432
438
  console.log(` ${pc.cyan("browse")} Interactive terminal explorer to browse & 1-click install`);
433
439
  console.log(` ${pc.cyan("search [query]")} Search plugins across official registry & npm`);
434
440
  console.log(` ${pc.cyan("list")} List all installed plugins and their registered tools`);
@@ -436,10 +442,11 @@ export async function runPluginCli(args) {
436
442
  console.log(` ${pc.cyan("verify <pkg>")} Check trust tier and registry verification status`);
437
443
  console.log();
438
444
  console.log(pc.bold("DEVELOPER SDK (@weavetab/sdk)"));
439
- console.log(` ${pc.dim("Scaffold a plugin:")} ${pc.cyan("npx -y @weavetab/sdk init <name>")}`);
445
+ console.log(` ${pc.dim("Scaffold a plugin:")} ${pc.cyan("npx @weavetab/sdk init <name>")}`);
440
446
  console.log(` ${pc.dim("Build & validate:")} ${pc.cyan("npx @weavetab/sdk build")}`);
441
447
  console.log();
442
- console.log(pc.bold("OPTIONS (for 'add')"));
448
+ console.log(pc.bold("OPTIONS (for 'add' / general)"));
449
+ console.log(` ${pc.cyan("--refresh")} Invalidate and refresh the verified plugin cache`);
443
450
  console.log(` ${pc.cyan("--official, -O")} Prefer official @weavetab/plugin-<name>`);
444
451
  console.log(` ${pc.cyan("--community, -C")} Prefer community package`);
445
452
  console.log(` ${pc.cyan("--yes, -y")} Bypass interactive warning prompt for unverified plugins`);
@@ -448,13 +455,13 @@ export async function runPluginCli(args) {
448
455
  }
449
456
  if (subcmd === "init" || subcmd === "create" || subcmd === "new" || subcmd === "build" || subcmd === "manifest") {
450
457
  console.log(pc.yellow(`\n[Weavetab] Plugin authoring commands belong to the developer SDK (@weavetab/sdk).`));
451
- console.log(pc.dim("To scaffold a new plugin: ") + pc.cyan("npx -y @weavetab/sdk init <name>"));
458
+ console.log(pc.dim("To scaffold a new plugin: ") + pc.cyan("npx @weavetab/sdk init <name>"));
452
459
  console.log(pc.dim("To build/validate a plugin: ") + pc.cyan("npx @weavetab/sdk build"));
453
460
  console.log(pc.dim("SDK documentation: ") + pc.cyan("https://github.com/weavetab/sdk\n"));
454
461
  return;
455
462
  }
456
463
  if (subcmd === "add" || subcmd === "install" || subcmd === "i") {
457
- await runPluginAdd(args[1], args.slice(2));
464
+ await runPluginAdd(cleanArgs[1], cleanArgs.slice(2));
458
465
  return;
459
466
  }
460
467
  if (subcmd === "browse" || subcmd === "explore") {
@@ -462,7 +469,7 @@ export async function runPluginCli(args) {
462
469
  return;
463
470
  }
464
471
  if (subcmd === "search" || subcmd === "find") {
465
- await runPluginSearch(args[1]);
472
+ await runPluginSearch(cleanArgs[1]);
466
473
  return;
467
474
  }
468
475
  if (subcmd === "list" || subcmd === "ls") {
@@ -470,11 +477,11 @@ export async function runPluginCli(args) {
470
477
  return;
471
478
  }
472
479
  if (subcmd === "remove" || subcmd === "rm" || subcmd === "uninstall") {
473
- await runPluginRemove(args[1], args.slice(2));
480
+ await runPluginRemove(cleanArgs[1], cleanArgs.slice(2));
474
481
  return;
475
482
  }
476
483
  if (subcmd === "verify" || subcmd === "check") {
477
- await runPluginVerify(args[1]);
484
+ await runPluginVerify(cleanArgs[1]);
478
485
  return;
479
486
  }
480
487
  console.log(pc.red(`[ERROR] Unknown plugin subcommand: "${subcmd}"`));
@@ -1 +1 @@
1
- export declare function checkForUpdates(): Promise<void>;
1
+ export declare function checkForUpdates(force?: boolean): Promise<void>;
@@ -1,19 +1,33 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import os from "node:os";
3
4
  import { fileURLToPath } from "node:url";
4
5
  const __filename = fileURLToPath(import.meta.url);
5
6
  const __dirname = path.dirname(__filename);
6
- export async function checkForUpdates() {
7
+ const UPDATE_CACHE_FILE = path.join(os.homedir(), ".weavetab-system", "cache", "update_check.json");
8
+ const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
9
+ export async function checkForUpdates(force = false) {
7
10
  try {
11
+ const now = Date.now();
12
+ // Check cache unless forced
13
+ if (!force && fs.existsSync(UPDATE_CACHE_FILE)) {
14
+ try {
15
+ const cached = JSON.parse(fs.readFileSync(UPDATE_CACHE_FILE, "utf-8"));
16
+ if (cached.lastChecked && now - cached.lastChecked < CHECK_INTERVAL_MS) {
17
+ return;
18
+ }
19
+ }
20
+ catch { /* malformed cache */ }
21
+ }
8
22
  // Read local package.json to get current version
9
23
  const packageJsonPath = path.resolve(__dirname, "../../package.json");
10
24
  if (!fs.existsSync(packageJsonPath))
11
25
  return;
12
26
  const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
13
27
  const currentVersion = pkg.version;
14
- // Fast, non-blocking fetch with 2-second timeout
28
+ // Fast, non-blocking fetch with 1.5-second timeout
15
29
  const controller = new AbortController();
16
- const timeoutId = setTimeout(() => controller.abort(), 2000);
30
+ const timeoutId = setTimeout(() => controller.abort(), 1500);
17
31
  const res = await fetch("https://registry.npmjs.org/@weavetab/mcp/latest", {
18
32
  signal: controller.signal,
19
33
  });
@@ -22,12 +36,18 @@ export async function checkForUpdates() {
22
36
  return;
23
37
  const data = await res.json();
24
38
  const latestVersion = data.version;
25
- // Simple version comparison (assumes semver format like x.y.z)
39
+ // Save timestamp to cache
40
+ try {
41
+ fs.mkdirSync(path.dirname(UPDATE_CACHE_FILE), { recursive: true });
42
+ fs.writeFileSync(UPDATE_CACHE_FILE, JSON.stringify({ lastChecked: now, latestVersion }), "utf-8");
43
+ }
44
+ catch { /* non-fatal */ }
45
+ // Simple version comparison
26
46
  if (isNewerVersion(currentVersion, latestVersion)) {
27
47
  await printUpdateBox(currentVersion, latestVersion);
28
48
  }
29
49
  }
30
- catch (err) {
50
+ catch {
31
51
  // Silently fail if offline or npm is down
32
52
  }
33
53
  }
@@ -52,7 +72,7 @@ async function printUpdateBox(current, latest) {
52
72
  chalk.bold(`Update available! `) + chalk.dim(current) + chalk.reset(` → `) + chalk.green(latest),
53
73
  `Run ${chalk.cyan("npm i -g @weavetab/mcp")} to update.`,
54
74
  ``,
55
- `Changelog: ${chalk.blue(`https://github.com/weavetab/mcp/changelog/v${latest}`)}`
75
+ `Changelog: ${chalk.blue(`https://github.com/weavetab/mcp/releases/tag/v${latest}`)}`
56
76
  ].join("\n");
57
77
  const box = boxen(msg, {
58
78
  padding: 1,
@@ -67,6 +87,6 @@ async function printUpdateBox(current, latest) {
67
87
  // Fallback if formatting fails
68
88
  console.error(`\n[Weavetab MCP] Update available: ${current} -> ${latest}`);
69
89
  console.error(`Run "npm i -g @weavetab/mcp" to update.`);
70
- console.error(`Changelog: https://github.com/weavetab/mcp/changelog/v${latest}\n`);
90
+ console.error(`Changelog: https://github.com/weavetab/mcp/releases/tag/v${latest}\n`);
71
91
  }
72
92
  }
@@ -136,4 +136,12 @@ export declare function getSystemAppDir(appName: string): string;
136
136
  * Alias for consistency — identical to getSystemDir.
137
137
  */
138
138
  export declare function getGlobalSystemDir(subdir: string): string;
139
+ /**
140
+ * Returns the canonical directory for persistent site profiles and learned workflow patterns.
141
+ * Lives in ~/.weavetab-system/memory/sites/ — strictly machine-managed, never user-authored.
142
+ * Separated from the legacy selector-memory dir (~/.weavetab-system/memory/) so existing
143
+ * per-domain selector caches are not disrupted.
144
+ */
145
+ export declare function getSystemMemorySitesDir(): string;
139
146
  export declare function saveConfig(config: Config): void;
147
+ export declare function stopConfigWatcher(): void;