auto-webmcp 0.3.13 → 0.3.15

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.
@@ -1 +1 @@
1
- {"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,UAAU,EAAmJ,MAAM,aAAa,CAAC;AAC1L,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,WAAW,eAAe;IAC9B,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,UAAU,CAAC;IACxB,WAAW,CAAC,EAAE,eAAe,CAAC;IAC9B,6FAA6F;IAC7F,aAAa,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAKD,iDAAiD;AACjD,wBAAgB,cAAc,IAAI,IAAI,CAErC;AAED,gDAAgD;AAChD,wBAAgB,WAAW,CAAC,IAAI,EAAE,eAAe,EAAE,QAAQ,CAAC,EAAE,YAAY,GAAG,YAAY,CAOxF;AAiqBD;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,OAAO,EAClB,MAAM,EAAE,KAAK,CAAC,gBAAgB,GAAG,mBAAmB,GAAG,iBAAiB,GAAG,WAAW,CAAC,EACvF,SAAS,EAAE,iBAAiB,GAAG,gBAAgB,GAAG,IAAI,GACrD,YAAY,CAMd"}
1
+ {"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,UAAU,EAAmJ,MAAM,aAAa,CAAC;AAC1L,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,WAAW,eAAe;IAC9B,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,UAAU,CAAC;IACxB,WAAW,CAAC,EAAE,eAAe,CAAC;IAC9B,6FAA6F;IAC7F,aAAa,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAKD,iDAAiD;AACjD,wBAAgB,cAAc,IAAI,IAAI,CAErC;AAED,gDAAgD;AAChD,wBAAgB,WAAW,CAAC,IAAI,EAAE,eAAe,EAAE,QAAQ,CAAC,EAAE,YAAY,GAAG,YAAY,CAOxF;AA4uBD;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,OAAO,EAClB,MAAM,EAAE,KAAK,CAAC,gBAAgB,GAAG,mBAAmB,GAAG,iBAAiB,GAAG,WAAW,CAAC,EACvF,SAAS,EAAE,iBAAiB,GAAG,gBAAgB,GAAG,IAAI,GACrD,YAAY,CAMd"}
@@ -509,14 +509,15 @@ function collectShadowControls(root, visited = /* @__PURE__ */ new Set()) {
509
509
  const results = [];
510
510
  for (const el of Array.from(root.querySelectorAll("*"))) {
511
511
  if (el.shadowRoot) {
512
- results.push(
513
- ...Array.from(
514
- el.shadowRoot.querySelectorAll(
515
- "input, textarea, select"
516
- )
517
- ),
518
- ...collectShadowControls(el.shadowRoot, visited)
512
+ const found = Array.from(
513
+ el.shadowRoot.querySelectorAll(
514
+ "input, textarea, select"
515
+ )
519
516
  );
517
+ if (found.length > 0) {
518
+ console.log(`[auto-webmcp] shadow: found ${found.length} control(s) in ${el.tagName.toLowerCase()} shadow root:`, found.map((f) => `${f.tagName.toLowerCase()}[type=${f.type ?? "?"}][name="${f.name}"][id="${f.id}"]`));
519
+ }
520
+ results.push(...found, ...collectShadowControls(el.shadowRoot, visited));
520
521
  }
521
522
  }
522
523
  return results;
@@ -600,9 +601,23 @@ function buildSchema(form) {
600
601
  if (!name) {
601
602
  fieldElements.set(fieldKey, control);
602
603
  }
603
- if (control.required) {
604
- required.push(fieldKey);
604
+ let isRequired = control.required;
605
+ if (!isRequired) {
606
+ let hostNode = control;
607
+ while (true) {
608
+ const root = hostNode.getRootNode();
609
+ if (!(root instanceof ShadowRoot))
610
+ break;
611
+ const host = root.host;
612
+ if (host.hasAttribute("required") || host.getAttribute("aria-required") === "true") {
613
+ isRequired = true;
614
+ break;
615
+ }
616
+ hostNode = host;
617
+ }
605
618
  }
619
+ if (isRequired)
620
+ required.push(fieldKey);
606
621
  }
607
622
  const ariaControls = collectAriaControls(form);
608
623
  const processedAriaRadioGroups = /* @__PURE__ */ new Set();
@@ -645,11 +660,40 @@ function resolveNativeControlFallbackKey(control) {
645
660
  if ((control instanceof HTMLInputElement || control instanceof HTMLTextAreaElement) && control.placeholder?.trim()) {
646
661
  return sanitizeName(control.placeholder.trim());
647
662
  }
663
+ const hostKey = resolveShadowHostKey(control);
664
+ if (hostKey)
665
+ return hostKey;
648
666
  if (control instanceof HTMLInputElement && control.type !== "text") {
649
667
  return control.type;
650
668
  }
651
669
  return null;
652
670
  }
671
+ function resolveShadowHostKey(el) {
672
+ let node = el;
673
+ while (true) {
674
+ const root = node.getRootNode();
675
+ if (!(root instanceof ShadowRoot))
676
+ break;
677
+ const host = root.host;
678
+ const fieldName = host.getAttribute("field-name");
679
+ if (fieldName) {
680
+ console.log("[auto-webmcp] shadow host key: field-name=", fieldName);
681
+ return sanitizeName(fieldName);
682
+ }
683
+ const hostLabel = host.getAttribute("label") || host.getAttribute("aria-label");
684
+ if (hostLabel) {
685
+ console.log("[auto-webmcp] shadow host key: label=", hostLabel);
686
+ return sanitizeName(hostLabel);
687
+ }
688
+ const hostName = host.getAttribute("name");
689
+ if (hostName) {
690
+ console.log("[auto-webmcp] shadow host key: name=", hostName);
691
+ return sanitizeName(hostName);
692
+ }
693
+ node = host;
694
+ }
695
+ return null;
696
+ }
653
697
  function resolveAriaElementKey(el) {
654
698
  if (el.dataset["webmcpName"])
655
699
  return sanitizeName(el.dataset["webmcpName"]);
@@ -811,12 +855,40 @@ function getAssociatedLabelText(control) {
811
855
  return text;
812
856
  }
813
857
  }
858
+ const ownRoot = control.getRootNode();
859
+ if (ownRoot instanceof ShadowRoot) {
860
+ if (control.id) {
861
+ const shadowLabel = ownRoot.querySelector(`label[for="${CSS.escape(control.id)}"]`);
862
+ if (shadowLabel) {
863
+ const text = labelTextWithoutNested(shadowLabel);
864
+ if (text)
865
+ return text;
866
+ }
867
+ }
868
+ const anyLabel = ownRoot.querySelector("label");
869
+ if (anyLabel) {
870
+ const text = labelTextWithoutNested(anyLabel);
871
+ if (text)
872
+ return text;
873
+ }
874
+ }
814
875
  const parent = control.closest("label");
815
876
  if (parent) {
816
877
  const text = labelTextWithoutNested(parent);
817
878
  if (text)
818
879
  return text;
819
880
  }
881
+ let node = control;
882
+ while (true) {
883
+ const root = node.getRootNode();
884
+ if (!(root instanceof ShadowRoot))
885
+ break;
886
+ const host = root.host;
887
+ const hostLabel = host.getAttribute("label") || host.getAttribute("aria-label");
888
+ if (hostLabel)
889
+ return hostLabel;
890
+ node = host;
891
+ }
820
892
  return "";
821
893
  }
822
894
  function labelTextWithoutNested(label) {
@@ -1516,6 +1588,51 @@ function getMissingRequired(metadata, params) {
1516
1588
  return [];
1517
1589
  return metadata.inputSchema.required.filter((fieldKey) => !(fieldKey in params));
1518
1590
  }
1591
+ async function fillComboboxButton(el, value) {
1592
+ const text = String(value ?? "").trim();
1593
+ console.log("[auto-webmcp] fillComboboxButton: clicking button, value=", JSON.stringify(text));
1594
+ el.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
1595
+ const listbox = await new Promise((resolve) => {
1596
+ const deadline = Date.now() + 1e3;
1597
+ const poll = () => {
1598
+ const candidate = document.querySelector('[role="listbox"]') ?? document.querySelector('[role="option"]')?.closest('[role="listbox"]') ?? null;
1599
+ if (candidate) {
1600
+ resolve(candidate);
1601
+ return;
1602
+ }
1603
+ if (Date.now() >= deadline) {
1604
+ resolve(null);
1605
+ return;
1606
+ }
1607
+ setTimeout(poll, 50);
1608
+ };
1609
+ poll();
1610
+ });
1611
+ if (!listbox) {
1612
+ console.warn("[auto-webmcp] fillComboboxButton: listbox did not appear after 1s");
1613
+ return;
1614
+ }
1615
+ const options = Array.from(listbox.querySelectorAll('[role="option"]'));
1616
+ console.log("[auto-webmcp] fillComboboxButton: listbox has", options.length, "options");
1617
+ const lowerValue = text.toLowerCase();
1618
+ const match = options.find((opt) => {
1619
+ const dataValue = (opt.getAttribute("data-value") ?? "").toLowerCase();
1620
+ const ariaLabel = (opt.getAttribute("aria-label") ?? "").toLowerCase();
1621
+ const optText = (opt.textContent ?? "").trim().toLowerCase();
1622
+ return dataValue === lowerValue || ariaLabel === lowerValue || optText === lowerValue;
1623
+ });
1624
+ if (match) {
1625
+ console.log("[auto-webmcp] fillComboboxButton: clicking option", match.textContent?.trim());
1626
+ match.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
1627
+ } else {
1628
+ console.warn(
1629
+ "[auto-webmcp] fillComboboxButton: no option matched",
1630
+ JSON.stringify(text),
1631
+ "available:",
1632
+ options.map((o) => o.textContent?.trim())
1633
+ );
1634
+ }
1635
+ }
1519
1636
 
1520
1637
  // src/discovery.ts
1521
1638
  function emit(type, form, toolName) {
@@ -1583,6 +1700,17 @@ var registeredForms = /* @__PURE__ */ new WeakSet();
1583
1700
  var registeredFormCount = 0;
1584
1701
  var reAnalysisTimers = /* @__PURE__ */ new Map();
1585
1702
  var RE_ANALYSIS_DEBOUNCE_MS = 300;
1703
+ var orphanRescanTimer = null;
1704
+ var ORPHAN_RESCAN_DEBOUNCE_MS = 500;
1705
+ var registeredOrphanToolNames = /* @__PURE__ */ new Set();
1706
+ function scheduleOrphanRescan(config) {
1707
+ if (orphanRescanTimer)
1708
+ clearTimeout(orphanRescanTimer);
1709
+ orphanRescanTimer = setTimeout(() => {
1710
+ orphanRescanTimer = null;
1711
+ void scanOrphanInputs(config);
1712
+ }, ORPHAN_RESCAN_DEBOUNCE_MS);
1713
+ }
1586
1714
  function isInterestingNode(node) {
1587
1715
  const tag = node.tagName.toLowerCase();
1588
1716
  if (tag === "input" || tag === "textarea" || tag === "select")
@@ -1629,6 +1757,9 @@ function startObserver(config) {
1629
1757
  for (const form of Array.from(node.querySelectorAll("form"))) {
1630
1758
  void registerForm(form, config);
1631
1759
  }
1760
+ if (isInterestingNode(node) && !node.closest("form")) {
1761
+ scheduleOrphanRescan(config);
1762
+ }
1632
1763
  }
1633
1764
  for (const node of mutation.removedNodes) {
1634
1765
  if (!(node instanceof Element))
@@ -1676,10 +1807,10 @@ async function scanOrphanInputs(config) {
1676
1807
  return;
1677
1808
  const SUBMIT_BTN_SELECTOR = '[type="submit"]:not([disabled]), button[data-variant="primary"]:not([disabled])';
1678
1809
  const SUBMIT_BTN_GROUPING_SELECTOR = '[type="submit"], button[data-variant="primary"]';
1679
- const SUBMIT_TEXT_RE = /subscribe|submit|sign[\s-]?up|send|join|go|search|post|tweet|publish/i;
1810
+ const SUBMIT_TEXT_RE = /subscribe|submit|sign[\s-]?up|send|join|go|search|post|tweet|publish|save/i;
1680
1811
  const orphanInputs = Array.from(
1681
1812
  document.querySelectorAll(
1682
- 'input:not(form input), textarea:not(form textarea), select:not(form select), [role="textbox"]:not(form [role="textbox"]):not(input):not(textarea), [role="searchbox"]:not(form [role="searchbox"]):not(input):not(textarea), [contenteditable="true"]:not(form [contenteditable="true"]):not(input):not(textarea)'
1813
+ 'input:not(form input), textarea:not(form textarea), select:not(form select), [role="textbox"]:not(form [role="textbox"]):not(input):not(textarea), [role="searchbox"]:not(form [role="searchbox"]):not(input):not(textarea), [contenteditable="true"]:not(form [contenteditable="true"]):not(input):not(textarea), button[role="combobox"]:not(form button[role="combobox"])'
1683
1814
  )
1684
1815
  ).filter((el) => {
1685
1816
  if (el instanceof HTMLInputElement && ORPHAN_EXCLUDED_TYPES.has(el.type.toLowerCase())) {
@@ -1810,7 +1941,11 @@ async function scanOrphanInputs(config) {
1810
1941
  for (const { key, el } of inputPairs) {
1811
1942
  if (params[key] !== void 0) {
1812
1943
  console.log(`[auto-webmcp] orphan execute: filling key="${key}" value=`, params[key], "element=", el);
1813
- fillElement(el, params[key]);
1944
+ if (el.getAttribute("role") === "combobox" && el.tagName.toLowerCase() === "button") {
1945
+ await fillComboboxButton(el, params[key]);
1946
+ } else {
1947
+ fillElement(el, params[key]);
1948
+ }
1814
1949
  console.log(`[auto-webmcp] orphan execute: after fill, element value=`, el.value);
1815
1950
  } else {
1816
1951
  console.log(`[auto-webmcp] orphan execute: key="${key}" not in params, skipping`);
@@ -1822,22 +1957,43 @@ async function scanOrphanInputs(config) {
1822
1957
  console.log(`[auto-webmcp] orphan execute: autoSubmit=false, returning without clicking submit`);
1823
1958
  return { content: [{ type: "text", text: "Fields filled. Ready to submit." }] };
1824
1959
  }
1825
- console.log(`[auto-webmcp] orphan execute: polling for enabled submit button (up to 2s)...`);
1960
+ console.log(`[auto-webmcp] orphan execute: resolving submit button (up to 2s)...`);
1826
1961
  let btn = null;
1827
- const deadline = Date.now() + 2e3;
1828
- while (Date.now() < deadline) {
1829
- const candidates = Array.from(
1830
- container.querySelectorAll(SUBMIT_BTN_SELECTOR)
1962
+ if (submitBtn && document.contains(submitBtn)) {
1963
+ const isEnabled = !submitBtn.disabled && submitBtn.getAttribute("aria-disabled") !== "true";
1964
+ const r = submitBtn.getBoundingClientRect();
1965
+ if (isEnabled && r.width > 0 && r.height > 0) {
1966
+ btn = submitBtn;
1967
+ console.log(`[auto-webmcp] orphan execute: using captured submit button "${btn.textContent?.trim()}"`);
1968
+ }
1969
+ }
1970
+ if (!btn) {
1971
+ const deadline = Date.now() + 2e3;
1972
+ while (Date.now() < deadline) {
1973
+ const candidates = Array.from(
1974
+ container.querySelectorAll(SUBMIT_BTN_SELECTOR)
1975
+ ).filter((b) => {
1976
+ const r = b.getBoundingClientRect();
1977
+ return r.width > 0 && r.height > 0;
1978
+ });
1979
+ const last = candidates[candidates.length - 1] ?? null;
1980
+ if (last) {
1981
+ btn = last;
1982
+ break;
1983
+ }
1984
+ await new Promise((r) => setTimeout(r, 100));
1985
+ }
1986
+ }
1987
+ if (!btn) {
1988
+ const textBtns = Array.from(
1989
+ (container !== document.body ? container : document).querySelectorAll('button, [role="button"]')
1831
1990
  ).filter((b) => {
1832
1991
  const r = b.getBoundingClientRect();
1833
- return r.width > 0 && r.height > 0;
1992
+ return r.width > 0 && r.height > 0 && !b.disabled && b.getAttribute("aria-disabled") !== "true" && SUBMIT_TEXT_RE.test(b.textContent ?? "");
1834
1993
  });
1835
- const last = candidates[candidates.length - 1] ?? null;
1836
- if (last) {
1837
- btn = last;
1838
- break;
1839
- }
1840
- await new Promise((r) => setTimeout(r, 100));
1994
+ btn = textBtns[textBtns.length - 1] ?? null;
1995
+ if (btn)
1996
+ console.log(`[auto-webmcp] orphan execute: using text-matched fallback button "${btn.textContent?.trim()}"`);
1841
1997
  }
1842
1998
  if (!btn) {
1843
1999
  console.warn(`[auto-webmcp] orphan execute: submit button still disabled after 2s`);
@@ -1848,6 +2004,10 @@ async function scanOrphanInputs(config) {
1848
2004
  return { content: [{ type: "text", text: "Fields filled and form submitted." }] };
1849
2005
  };
1850
2006
  try {
2007
+ if (registeredOrphanToolNames.has(metadata.name)) {
2008
+ console.log(`[auto-webmcp] orphan: "${metadata.name}" already registered, skipping`);
2009
+ continue;
2010
+ }
1851
2011
  const toolDef = {
1852
2012
  name: metadata.name,
1853
2013
  description: metadata.description,
@@ -1858,6 +2018,7 @@ async function scanOrphanInputs(config) {
1858
2018
  toolDef.annotations = metadata.annotations;
1859
2019
  }
1860
2020
  await navigator.modelContext.registerTool(toolDef);
2021
+ registeredOrphanToolNames.add(metadata.name);
1861
2022
  const pendingBtns = window["__pendingSubmitBtns"] ??= {};
1862
2023
  pendingBtns[metadata.name] = submitBtn;
1863
2024
  if (config.debug) {
@@ -1885,6 +2046,7 @@ async function startDiscovery(config) {
1885
2046
  );
1886
2047
  }
1887
2048
  registeredFormCount = 0;
2049
+ registeredOrphanToolNames.clear();
1888
2050
  startObserver(config);
1889
2051
  listenForRouteChanges(config);
1890
2052
  await scanForms(config);