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

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 (127) hide show
  1. package/AGENTS.md +1 -1
  2. package/ARCHITECTURE.md +6 -6
  3. package/LICENSE +216 -631
  4. package/README.md +153 -1074
  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 +10 -6
  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.d.ts +1 -0
  17. package/dist/cli/plugin.js +81 -18
  18. package/dist/cli/updater.d.ts +1 -1
  19. package/dist/cli/updater.js +27 -7
  20. package/dist/config/loader.d.ts +9 -1
  21. package/dist/config/loader.js +42 -3
  22. package/dist/desktop/discovery.d.ts +44 -0
  23. package/dist/desktop/discovery.js +364 -0
  24. package/dist/index.js +137 -21
  25. package/dist/intelligence/marks.d.ts +36 -0
  26. package/dist/intelligence/marks.js +30 -0
  27. package/dist/intelligence/patternCompiler.d.ts +118 -0
  28. package/dist/intelligence/patternCompiler.js +259 -0
  29. package/dist/intelligence/sessionMemory.d.ts +2 -0
  30. package/dist/intelligence/sessionMemory.js +18 -8
  31. package/dist/overlay/injector.js +2 -2
  32. package/dist/plugin/interface.d.ts +88 -5
  33. package/dist/plugin/loader.d.ts +12 -1
  34. package/dist/plugin/loader.js +280 -70
  35. package/dist/plugin/pipeline.d.ts +75 -0
  36. package/dist/plugin/pipeline.js +183 -0
  37. package/dist/plugin/registry.d.ts +2 -1
  38. package/dist/plugin/registry.js +24 -14
  39. package/dist/security/secrets.d.ts +7 -0
  40. package/dist/security/secrets.js +37 -0
  41. package/dist/sensors/actionability.js +1 -2
  42. package/dist/sensors/guard.js +4 -10
  43. package/dist/sensors/mutations.js +12 -0
  44. package/dist/sensors/operation_monitor.js +1 -0
  45. package/dist/server.d.ts +1 -1
  46. package/dist/server.js +453 -178
  47. package/dist/state/agent.d.ts +1 -0
  48. package/dist/state/agent.js +3 -0
  49. package/dist/state/delta.d.ts +12 -4
  50. package/dist/state/delta.js +67 -14
  51. package/dist/state/session.d.ts +6 -0
  52. package/dist/state/session.js +27 -0
  53. package/dist/tools/browser_detect.d.ts +16 -3
  54. package/dist/tools/browser_detect.js +52 -7
  55. package/dist/tools/burst.d.ts +17 -8
  56. package/dist/tools/burst.js +236 -77
  57. package/dist/tools/captcha.js +1 -1
  58. package/dist/tools/checkpoint.d.ts +38 -0
  59. package/dist/tools/checkpoint.js +271 -0
  60. package/dist/tools/click.d.ts +1 -0
  61. package/dist/tools/click.js +74 -70
  62. package/dist/tools/design_extract.js +1 -1
  63. package/dist/tools/eval.js +1 -1
  64. package/dist/tools/fill.d.ts +20 -1
  65. package/dist/tools/fill.js +199 -40
  66. package/dist/tools/find.d.ts +16 -0
  67. package/dist/tools/find.js +67 -44
  68. package/dist/tools/github/index.d.ts +19 -4
  69. package/dist/tools/github/index.js +40 -4
  70. package/dist/tools/highlight.d.ts +6 -2
  71. package/dist/tools/highlight.js +158 -72
  72. package/dist/tools/inspect.js +1 -1
  73. package/dist/tools/key.js +65 -56
  74. package/dist/tools/navigate.d.ts +1 -1
  75. package/dist/tools/navigate.js +34 -39
  76. package/dist/tools/pdf.js +21 -13
  77. package/dist/tools/pointer.d.ts +18 -3
  78. package/dist/tools/pointer.js +89 -1
  79. package/dist/tools/read.d.ts +28 -8
  80. package/dist/tools/read.js +230 -56
  81. package/dist/tools/scrape.js +67 -13
  82. package/dist/tools/screenshot.js +21 -16
  83. package/dist/tools/scroll.js +6 -11
  84. package/dist/tools/select.js +8 -2
  85. package/dist/tools/snapshot.js +3 -34
  86. package/dist/tools/tabs.js +15 -7
  87. package/dist/tools/thought.d.ts +2 -0
  88. package/dist/tools/thought.js +13 -14
  89. package/dist/tools/type.d.ts +2 -0
  90. package/dist/tools/type.js +107 -58
  91. package/dist/tools/type_secret.js +42 -76
  92. package/dist/tools/upload.js +1 -1
  93. package/dist/tools/video_compiler.d.ts +1 -1
  94. package/dist/tools/video_compiler.js +11 -12
  95. package/dist/version.d.ts +5 -0
  96. package/dist/version.js +5 -0
  97. package/docs/DOCUMENTATION.md +37 -41
  98. package/docs/PLUGINS.md +121 -0
  99. package/docs/TOOLS.md +54 -0
  100. package/docs/architecture/blind-injection.md +83 -20
  101. package/docs/architecture/overview.md +49 -49
  102. package/docs/architecture/security.md +8 -7
  103. package/docs/architecture/transport.md +2 -2
  104. package/docs/cli/cli-commands.md +7 -1
  105. package/docs/cli/cli-flags.md +15 -7
  106. package/docs/cli/cli-scripting.md +6 -3
  107. package/docs/configuration/config-locations.md +3 -4
  108. package/docs/configuration/env-variables.md +4 -4
  109. package/docs/configuration/folders.md +55 -54
  110. package/docs/desktop-automation/electron-apps.md +214 -162
  111. package/docs/get-started/introduction.md +60 -60
  112. package/docs/get-started/setup-wizard.md +7 -6
  113. package/docs/intelligence/site-profiles.md +151 -0
  114. package/docs/tools/tools.md +631 -293
  115. package/docs/troubleshooting/trouble-browser.md +1 -1
  116. package/docs/troubleshooting/trouble-loop.md +31 -31
  117. package/ext-dist/background.js +10 -62
  118. package/ext-dist/content.js +333 -920
  119. package/ext-dist/hud/hud.js +9 -9
  120. package/ext-dist/manifest.json +2 -6
  121. package/ext-dist/styles.css +6 -1
  122. package/icon.png +0 -0
  123. package/mcp.json +87 -71
  124. package/package.json +118 -119
  125. package/server.json +5 -5
  126. package/ext-dist/sidepanel/sidepanel.js +0 -25
  127. 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
+ }
@@ -1,4 +1,5 @@
1
1
  export declare function runPluginAdd(inputName: string, args?: string[]): Promise<void>;
2
+ export declare function runPluginAddLocal(inputPath: string): Promise<void>;
2
3
  export declare function runPluginList(): Promise<void>;
3
4
  export declare function runPluginSearch(query?: string): Promise<void>;
4
5
  export declare function runPluginBrowse(): Promise<void>;
@@ -2,20 +2,22 @@ import * as p from "@clack/prompts";
2
2
  import pc from "picocolors";
3
3
  import boxen from "boxen";
4
4
  import ora from "ora";
5
+ import fs from "node:fs";
5
6
  import path from "node:path";
6
7
  import { spawn } from "node:child_process";
7
8
  import { createRequire } from "node:module";
9
+ import { WEAVETAB_VERSION } from "../version.js";
8
10
  const _require = createRequire(import.meta.url);
9
11
  function getVersion() {
10
12
  try {
11
- return _require("../../package.json").version || "2.5.0-beta.2";
13
+ return _require("../../package.json").version || WEAVETAB_VERSION;
12
14
  }
13
15
  catch {
14
- return "2.5.0-beta.2";
16
+ return WEAVETAB_VERSION;
15
17
  }
16
18
  }
17
- import { getVerifiedPackages, resolvePluginTrust, resolvePluginCandidates, REGISTRY_URL, } from "../plugin/registry.js";
18
- import { listInstalledPlugins, inspectInstalledPackage } from "../plugin/loader.js";
19
+ import { getVerifiedPackages, resolvePluginTrust, resolvePluginCandidates, invalidateRegistryCache, REGISTRY_URL, } from "../plugin/registry.js";
20
+ import { listInstalledPlugins, inspectInstalledPackage, normalizePermissions } from "../plugin/loader.js";
19
21
  // Helper to run child process safely
20
22
  function runCommand(cmd, cmdArgs, cwd = process.cwd()) {
21
23
  return new Promise((resolve) => {
@@ -46,8 +48,20 @@ function runNpmCommand(cmdArgs, cwd = process.cwd()) {
46
48
  // ---------------------------------------------------------------------------
47
49
  export async function runPluginAdd(inputName, args = []) {
48
50
  if (!inputName || inputName.startsWith("-")) {
49
- console.log(pc.red("[ERROR] Please provide a package name to install."));
50
- console.log(pc.dim("Usage: weavetab plugin add <packageName> [--official | --community] [--yes]"));
51
+ console.log(pc.red("[ERROR] Please provide a package name or local path to install."));
52
+ console.log(pc.dim("Usage: weavetab plugin add <packageName|./local-path> [--official | --community] [--yes]"));
53
+ return;
54
+ }
55
+ // Local path install: `weavetab plugin add ./my-plugin` (npm installs the directory).
56
+ if (inputName === "." ||
57
+ inputName.startsWith("./") ||
58
+ inputName.startsWith("../") ||
59
+ inputName.startsWith("file:") ||
60
+ inputName.startsWith("/") ||
61
+ inputName.startsWith("\\") ||
62
+ /^[a-zA-Z]:[\\/]/.test(inputName) ||
63
+ inputName.startsWith("~/")) {
64
+ await runPluginAddLocal(inputName);
51
65
  return;
52
66
  }
53
67
  const isForce = args.includes("--yes") || args.includes("-y") || args.includes("--force");
@@ -167,14 +181,14 @@ export async function runPluginAdd(inputName, args = []) {
167
181
  }
168
182
  return;
169
183
  }
170
- spinner.succeed(pc.green(`Successfully installed ${pc.bold(selectedPkg)}`));
184
+ spinner.succeed(pc.green(`installed ${pc.bold(selectedPkg)}`));
171
185
  // Inspect installed package for weavetab.json
172
186
  const targetDir = path.join(process.cwd(), "node_modules", selectedPkg);
173
187
  const installedInfo = inspectInstalledPackage(targetDir);
174
188
  if (installedInfo && installedInfo.manifest) {
175
189
  const manifest = installedInfo.manifest;
176
190
  const tools = manifest.tools || [];
177
- const permissions = manifest.permissions || {};
191
+ const permissions = normalizePermissions(manifest.permissions);
178
192
  const infoLines = [
179
193
  `${pc.bold("Plugin Details:")}`,
180
194
  ` ${pc.dim("Name:")} ${manifest.name || selectedPkg}`,
@@ -222,6 +236,49 @@ export async function runPluginAdd(inputName, args = []) {
222
236
  }
223
237
  }
224
238
  // ---------------------------------------------------------------------------
239
+ // 1b. ADD FROM LOCAL PATH (`weavetab plugin add ./my-plugin`)
240
+ // ---------------------------------------------------------------------------
241
+ export async function runPluginAddLocal(inputPath) {
242
+ let dir = inputPath;
243
+ if (dir.startsWith("file:"))
244
+ dir = dir.slice("file:".length);
245
+ if (dir.startsWith("~/"))
246
+ dir = path.join(process.env.HOME || process.env.USERPROFILE || "", dir.slice(2));
247
+ const absDir = path.resolve(process.cwd(), dir);
248
+ if (!fs.existsSync(absDir) || !fs.statSync(absDir).isDirectory()) {
249
+ console.log(pc.red(`[ERROR] Local path not found: ${inputPath}`));
250
+ return;
251
+ }
252
+ const local = inspectInstalledPackage(absDir);
253
+ if (!local) {
254
+ console.log(pc.red(`[ERROR] No valid weavetab.json manifest in ${absDir}`));
255
+ console.log(pc.dim("Scaffold one with: npx @weavetab/sdk init <name>"));
256
+ return;
257
+ }
258
+ const spinner = ora(`Installing local plugin ${pc.cyan(local.packageName)} from ${pc.dim(absDir)} via npm...`).start();
259
+ const installResult = await runNpmCommand(["install", absDir]);
260
+ if (installResult.code !== 0) {
261
+ spinner.fail(pc.red(`Failed to install ${local.packageName}`));
262
+ if (installResult.stderr)
263
+ console.error(pc.dim(installResult.stderr));
264
+ return;
265
+ }
266
+ spinner.succeed(pc.green(`installed ${pc.bold(local.packageName)} (local path)`));
267
+ const targetDir = path.join(process.cwd(), "node_modules", local.packageName);
268
+ const installedInfo = fs.existsSync(targetDir) ? inspectInstalledPackage(targetDir) : local;
269
+ const manifest = (installedInfo?.manifest || local.manifest);
270
+ const tools = manifest.tools || [];
271
+ console.log();
272
+ console.log(boxen(`${pc.bold("Plugin Details:")}\n` +
273
+ ` ${pc.dim("Name:")} ${manifest.name || local.packageName}\n` +
274
+ ` ${pc.dim("Version:")} ${manifest.version || "1.0.0"}\n` +
275
+ ` ${pc.dim("Source:")} ${absDir} (local path)\n` +
276
+ ` ${pc.dim("Description:")} ${manifest.description || "No description provided"}` +
277
+ (tools.length > 0 ? `\n\n${pc.bold(`Exposed Tools (${tools.length}):`)}` + tools.map((t) => `\n • ${pc.cyan(t.name)}: ${pc.dim(t.description || "")}`).join("") : ""), { padding: 1, borderStyle: "round", borderColor: "green" }));
278
+ console.log();
279
+ console.log(pc.green("✔ Plugin is ready to use! Restart Weavetab MCP server to activate changes.\n"));
280
+ }
281
+ // ---------------------------------------------------------------------------
225
282
  // 2. LIST INSTALLED PLUGINS
226
283
  // ---------------------------------------------------------------------------
227
284
  export async function runPluginList() {
@@ -387,7 +444,7 @@ export async function runPluginRemove(pkgName, args = []) {
387
444
  console.error(pc.dim(result.stderr));
388
445
  return;
389
446
  }
390
- spinner.succeed(pc.green(`Successfully removed ${pc.bold(pkgName)}`));
447
+ spinner.succeed(pc.green(`removed ${pc.bold(pkgName)}`));
391
448
  console.log(pc.dim("The plugin and its tools have been uninstalled.\n"));
392
449
  }
393
450
  // ---------------------------------------------------------------------------
@@ -419,7 +476,12 @@ export async function runPluginVerify(pkgName) {
419
476
  // 7. MAIN CLI ROUTER FOR PLUGINS (User / Operations Mode)
420
477
  // ---------------------------------------------------------------------------
421
478
  export async function runPluginCli(args) {
422
- const subcmd = args[0]?.toLowerCase();
479
+ if (args.includes("--refresh")) {
480
+ invalidateRegistryCache();
481
+ console.log(pc.green("[OK] Plugin registry cache refreshed.\n"));
482
+ }
483
+ const cleanArgs = args.filter((a) => a !== "--refresh");
484
+ const subcmd = cleanArgs[0]?.toLowerCase();
423
485
  if (!subcmd || subcmd === "--help" || subcmd === "-h" || subcmd === "help") {
424
486
  console.log();
425
487
  console.log(boxen(pc.bold(pc.cyan(" WEAVETAB PLUGIN MANAGER ")), { padding: 0, borderStyle: "round", borderColor: "cyan" }));
@@ -428,7 +490,7 @@ export async function runPluginCli(args) {
428
490
  console.log(` weavetab plugin <command> [options]`);
429
491
  console.log();
430
492
  console.log(pc.bold("COMMANDS"));
431
- console.log(` ${pc.cyan("add <pkg>")} Install a plugin (resolves official @weavetab/ & community)`);
493
+ console.log(` ${pc.cyan("add <pkg|path>")} Install a plugin by npm name or local path (e.g. @fy2ne/wt-youtube, ./my-plugin)`);
432
494
  console.log(` ${pc.cyan("browse")} Interactive terminal explorer to browse & 1-click install`);
433
495
  console.log(` ${pc.cyan("search [query]")} Search plugins across official registry & npm`);
434
496
  console.log(` ${pc.cyan("list")} List all installed plugins and their registered tools`);
@@ -436,10 +498,11 @@ export async function runPluginCli(args) {
436
498
  console.log(` ${pc.cyan("verify <pkg>")} Check trust tier and registry verification status`);
437
499
  console.log();
438
500
  console.log(pc.bold("DEVELOPER SDK (@weavetab/sdk)"));
439
- console.log(` ${pc.dim("Scaffold a plugin:")} ${pc.cyan("npx -y @weavetab/sdk init <name>")}`);
501
+ console.log(` ${pc.dim("Scaffold a plugin:")} ${pc.cyan("npx @weavetab/sdk init <name>")}`);
440
502
  console.log(` ${pc.dim("Build & validate:")} ${pc.cyan("npx @weavetab/sdk build")}`);
441
503
  console.log();
442
- console.log(pc.bold("OPTIONS (for 'add')"));
504
+ console.log(pc.bold("OPTIONS (for 'add' / general)"));
505
+ console.log(` ${pc.cyan("--refresh")} Invalidate and refresh the verified plugin cache`);
443
506
  console.log(` ${pc.cyan("--official, -O")} Prefer official @weavetab/plugin-<name>`);
444
507
  console.log(` ${pc.cyan("--community, -C")} Prefer community package`);
445
508
  console.log(` ${pc.cyan("--yes, -y")} Bypass interactive warning prompt for unverified plugins`);
@@ -448,13 +511,13 @@ export async function runPluginCli(args) {
448
511
  }
449
512
  if (subcmd === "init" || subcmd === "create" || subcmd === "new" || subcmd === "build" || subcmd === "manifest") {
450
513
  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>"));
514
+ console.log(pc.dim("To scaffold a new plugin: ") + pc.cyan("npx @weavetab/sdk init <name>"));
452
515
  console.log(pc.dim("To build/validate a plugin: ") + pc.cyan("npx @weavetab/sdk build"));
453
516
  console.log(pc.dim("SDK documentation: ") + pc.cyan("https://github.com/weavetab/sdk\n"));
454
517
  return;
455
518
  }
456
519
  if (subcmd === "add" || subcmd === "install" || subcmd === "i") {
457
- await runPluginAdd(args[1], args.slice(2));
520
+ await runPluginAdd(cleanArgs[1], cleanArgs.slice(2));
458
521
  return;
459
522
  }
460
523
  if (subcmd === "browse" || subcmd === "explore") {
@@ -462,7 +525,7 @@ export async function runPluginCli(args) {
462
525
  return;
463
526
  }
464
527
  if (subcmd === "search" || subcmd === "find") {
465
- await runPluginSearch(args[1]);
528
+ await runPluginSearch(cleanArgs[1]);
466
529
  return;
467
530
  }
468
531
  if (subcmd === "list" || subcmd === "ls") {
@@ -470,11 +533,11 @@ export async function runPluginCli(args) {
470
533
  return;
471
534
  }
472
535
  if (subcmd === "remove" || subcmd === "rm" || subcmd === "uninstall") {
473
- await runPluginRemove(args[1], args.slice(2));
536
+ await runPluginRemove(cleanArgs[1], cleanArgs.slice(2));
474
537
  return;
475
538
  }
476
539
  if (subcmd === "verify" || subcmd === "check") {
477
- await runPluginVerify(args[1]);
540
+ await runPluginVerify(cleanArgs[1]);
478
541
  return;
479
542
  }
480
543
  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>;