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.
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/auto-webmcp.cjs.js +186 -24
- package/dist/auto-webmcp.cjs.js.map +2 -2
- package/dist/auto-webmcp.esm.js +186 -24
- package/dist/auto-webmcp.esm.js.map +2 -2
- package/dist/auto-webmcp.iife.js +1 -1
- package/dist/auto-webmcp.iife.js.map +3 -3
- package/dist/discovery.d.ts.map +1 -1
- package/dist/interceptor.d.ts +10 -0
- package/dist/interceptor.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/auto-webmcp.esm.js
CHANGED
|
@@ -490,14 +490,15 @@ function collectShadowControls(root, visited = /* @__PURE__ */ new Set()) {
|
|
|
490
490
|
const results = [];
|
|
491
491
|
for (const el of Array.from(root.querySelectorAll("*"))) {
|
|
492
492
|
if (el.shadowRoot) {
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
)
|
|
498
|
-
),
|
|
499
|
-
...collectShadowControls(el.shadowRoot, visited)
|
|
493
|
+
const found = Array.from(
|
|
494
|
+
el.shadowRoot.querySelectorAll(
|
|
495
|
+
"input, textarea, select"
|
|
496
|
+
)
|
|
500
497
|
);
|
|
498
|
+
if (found.length > 0) {
|
|
499
|
+
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}"]`));
|
|
500
|
+
}
|
|
501
|
+
results.push(...found, ...collectShadowControls(el.shadowRoot, visited));
|
|
501
502
|
}
|
|
502
503
|
}
|
|
503
504
|
return results;
|
|
@@ -581,9 +582,23 @@ function buildSchema(form) {
|
|
|
581
582
|
if (!name) {
|
|
582
583
|
fieldElements.set(fieldKey, control);
|
|
583
584
|
}
|
|
584
|
-
|
|
585
|
-
|
|
585
|
+
let isRequired = control.required;
|
|
586
|
+
if (!isRequired) {
|
|
587
|
+
let hostNode = control;
|
|
588
|
+
while (true) {
|
|
589
|
+
const root = hostNode.getRootNode();
|
|
590
|
+
if (!(root instanceof ShadowRoot))
|
|
591
|
+
break;
|
|
592
|
+
const host = root.host;
|
|
593
|
+
if (host.hasAttribute("required") || host.getAttribute("aria-required") === "true") {
|
|
594
|
+
isRequired = true;
|
|
595
|
+
break;
|
|
596
|
+
}
|
|
597
|
+
hostNode = host;
|
|
598
|
+
}
|
|
586
599
|
}
|
|
600
|
+
if (isRequired)
|
|
601
|
+
required.push(fieldKey);
|
|
587
602
|
}
|
|
588
603
|
const ariaControls = collectAriaControls(form);
|
|
589
604
|
const processedAriaRadioGroups = /* @__PURE__ */ new Set();
|
|
@@ -626,11 +641,40 @@ function resolveNativeControlFallbackKey(control) {
|
|
|
626
641
|
if ((control instanceof HTMLInputElement || control instanceof HTMLTextAreaElement) && control.placeholder?.trim()) {
|
|
627
642
|
return sanitizeName(control.placeholder.trim());
|
|
628
643
|
}
|
|
644
|
+
const hostKey = resolveShadowHostKey(control);
|
|
645
|
+
if (hostKey)
|
|
646
|
+
return hostKey;
|
|
629
647
|
if (control instanceof HTMLInputElement && control.type !== "text") {
|
|
630
648
|
return control.type;
|
|
631
649
|
}
|
|
632
650
|
return null;
|
|
633
651
|
}
|
|
652
|
+
function resolveShadowHostKey(el) {
|
|
653
|
+
let node = el;
|
|
654
|
+
while (true) {
|
|
655
|
+
const root = node.getRootNode();
|
|
656
|
+
if (!(root instanceof ShadowRoot))
|
|
657
|
+
break;
|
|
658
|
+
const host = root.host;
|
|
659
|
+
const fieldName = host.getAttribute("field-name");
|
|
660
|
+
if (fieldName) {
|
|
661
|
+
console.log("[auto-webmcp] shadow host key: field-name=", fieldName);
|
|
662
|
+
return sanitizeName(fieldName);
|
|
663
|
+
}
|
|
664
|
+
const hostLabel = host.getAttribute("label") || host.getAttribute("aria-label");
|
|
665
|
+
if (hostLabel) {
|
|
666
|
+
console.log("[auto-webmcp] shadow host key: label=", hostLabel);
|
|
667
|
+
return sanitizeName(hostLabel);
|
|
668
|
+
}
|
|
669
|
+
const hostName = host.getAttribute("name");
|
|
670
|
+
if (hostName) {
|
|
671
|
+
console.log("[auto-webmcp] shadow host key: name=", hostName);
|
|
672
|
+
return sanitizeName(hostName);
|
|
673
|
+
}
|
|
674
|
+
node = host;
|
|
675
|
+
}
|
|
676
|
+
return null;
|
|
677
|
+
}
|
|
634
678
|
function resolveAriaElementKey(el) {
|
|
635
679
|
if (el.dataset["webmcpName"])
|
|
636
680
|
return sanitizeName(el.dataset["webmcpName"]);
|
|
@@ -792,12 +836,40 @@ function getAssociatedLabelText(control) {
|
|
|
792
836
|
return text;
|
|
793
837
|
}
|
|
794
838
|
}
|
|
839
|
+
const ownRoot = control.getRootNode();
|
|
840
|
+
if (ownRoot instanceof ShadowRoot) {
|
|
841
|
+
if (control.id) {
|
|
842
|
+
const shadowLabel = ownRoot.querySelector(`label[for="${CSS.escape(control.id)}"]`);
|
|
843
|
+
if (shadowLabel) {
|
|
844
|
+
const text = labelTextWithoutNested(shadowLabel);
|
|
845
|
+
if (text)
|
|
846
|
+
return text;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
const anyLabel = ownRoot.querySelector("label");
|
|
850
|
+
if (anyLabel) {
|
|
851
|
+
const text = labelTextWithoutNested(anyLabel);
|
|
852
|
+
if (text)
|
|
853
|
+
return text;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
795
856
|
const parent = control.closest("label");
|
|
796
857
|
if (parent) {
|
|
797
858
|
const text = labelTextWithoutNested(parent);
|
|
798
859
|
if (text)
|
|
799
860
|
return text;
|
|
800
861
|
}
|
|
862
|
+
let node = control;
|
|
863
|
+
while (true) {
|
|
864
|
+
const root = node.getRootNode();
|
|
865
|
+
if (!(root instanceof ShadowRoot))
|
|
866
|
+
break;
|
|
867
|
+
const host = root.host;
|
|
868
|
+
const hostLabel = host.getAttribute("label") || host.getAttribute("aria-label");
|
|
869
|
+
if (hostLabel)
|
|
870
|
+
return hostLabel;
|
|
871
|
+
node = host;
|
|
872
|
+
}
|
|
801
873
|
return "";
|
|
802
874
|
}
|
|
803
875
|
function labelTextWithoutNested(label) {
|
|
@@ -1497,6 +1569,51 @@ function getMissingRequired(metadata, params) {
|
|
|
1497
1569
|
return [];
|
|
1498
1570
|
return metadata.inputSchema.required.filter((fieldKey) => !(fieldKey in params));
|
|
1499
1571
|
}
|
|
1572
|
+
async function fillComboboxButton(el, value) {
|
|
1573
|
+
const text = String(value ?? "").trim();
|
|
1574
|
+
console.log("[auto-webmcp] fillComboboxButton: clicking button, value=", JSON.stringify(text));
|
|
1575
|
+
el.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
|
|
1576
|
+
const listbox = await new Promise((resolve) => {
|
|
1577
|
+
const deadline = Date.now() + 1e3;
|
|
1578
|
+
const poll = () => {
|
|
1579
|
+
const candidate = document.querySelector('[role="listbox"]') ?? document.querySelector('[role="option"]')?.closest('[role="listbox"]') ?? null;
|
|
1580
|
+
if (candidate) {
|
|
1581
|
+
resolve(candidate);
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
if (Date.now() >= deadline) {
|
|
1585
|
+
resolve(null);
|
|
1586
|
+
return;
|
|
1587
|
+
}
|
|
1588
|
+
setTimeout(poll, 50);
|
|
1589
|
+
};
|
|
1590
|
+
poll();
|
|
1591
|
+
});
|
|
1592
|
+
if (!listbox) {
|
|
1593
|
+
console.warn("[auto-webmcp] fillComboboxButton: listbox did not appear after 1s");
|
|
1594
|
+
return;
|
|
1595
|
+
}
|
|
1596
|
+
const options = Array.from(listbox.querySelectorAll('[role="option"]'));
|
|
1597
|
+
console.log("[auto-webmcp] fillComboboxButton: listbox has", options.length, "options");
|
|
1598
|
+
const lowerValue = text.toLowerCase();
|
|
1599
|
+
const match = options.find((opt) => {
|
|
1600
|
+
const dataValue = (opt.getAttribute("data-value") ?? "").toLowerCase();
|
|
1601
|
+
const ariaLabel = (opt.getAttribute("aria-label") ?? "").toLowerCase();
|
|
1602
|
+
const optText = (opt.textContent ?? "").trim().toLowerCase();
|
|
1603
|
+
return dataValue === lowerValue || ariaLabel === lowerValue || optText === lowerValue;
|
|
1604
|
+
});
|
|
1605
|
+
if (match) {
|
|
1606
|
+
console.log("[auto-webmcp] fillComboboxButton: clicking option", match.textContent?.trim());
|
|
1607
|
+
match.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
|
|
1608
|
+
} else {
|
|
1609
|
+
console.warn(
|
|
1610
|
+
"[auto-webmcp] fillComboboxButton: no option matched",
|
|
1611
|
+
JSON.stringify(text),
|
|
1612
|
+
"available:",
|
|
1613
|
+
options.map((o) => o.textContent?.trim())
|
|
1614
|
+
);
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1500
1617
|
|
|
1501
1618
|
// src/discovery.ts
|
|
1502
1619
|
function emit(type, form, toolName) {
|
|
@@ -1564,6 +1681,17 @@ var registeredForms = /* @__PURE__ */ new WeakSet();
|
|
|
1564
1681
|
var registeredFormCount = 0;
|
|
1565
1682
|
var reAnalysisTimers = /* @__PURE__ */ new Map();
|
|
1566
1683
|
var RE_ANALYSIS_DEBOUNCE_MS = 300;
|
|
1684
|
+
var orphanRescanTimer = null;
|
|
1685
|
+
var ORPHAN_RESCAN_DEBOUNCE_MS = 500;
|
|
1686
|
+
var registeredOrphanToolNames = /* @__PURE__ */ new Set();
|
|
1687
|
+
function scheduleOrphanRescan(config) {
|
|
1688
|
+
if (orphanRescanTimer)
|
|
1689
|
+
clearTimeout(orphanRescanTimer);
|
|
1690
|
+
orphanRescanTimer = setTimeout(() => {
|
|
1691
|
+
orphanRescanTimer = null;
|
|
1692
|
+
void scanOrphanInputs(config);
|
|
1693
|
+
}, ORPHAN_RESCAN_DEBOUNCE_MS);
|
|
1694
|
+
}
|
|
1567
1695
|
function isInterestingNode(node) {
|
|
1568
1696
|
const tag = node.tagName.toLowerCase();
|
|
1569
1697
|
if (tag === "input" || tag === "textarea" || tag === "select")
|
|
@@ -1610,6 +1738,9 @@ function startObserver(config) {
|
|
|
1610
1738
|
for (const form of Array.from(node.querySelectorAll("form"))) {
|
|
1611
1739
|
void registerForm(form, config);
|
|
1612
1740
|
}
|
|
1741
|
+
if (isInterestingNode(node) && !node.closest("form")) {
|
|
1742
|
+
scheduleOrphanRescan(config);
|
|
1743
|
+
}
|
|
1613
1744
|
}
|
|
1614
1745
|
for (const node of mutation.removedNodes) {
|
|
1615
1746
|
if (!(node instanceof Element))
|
|
@@ -1657,10 +1788,10 @@ async function scanOrphanInputs(config) {
|
|
|
1657
1788
|
return;
|
|
1658
1789
|
const SUBMIT_BTN_SELECTOR = '[type="submit"]:not([disabled]), button[data-variant="primary"]:not([disabled])';
|
|
1659
1790
|
const SUBMIT_BTN_GROUPING_SELECTOR = '[type="submit"], button[data-variant="primary"]';
|
|
1660
|
-
const SUBMIT_TEXT_RE = /subscribe|submit|sign[\s-]?up|send|join|go|search|post|tweet|publish/i;
|
|
1791
|
+
const SUBMIT_TEXT_RE = /subscribe|submit|sign[\s-]?up|send|join|go|search|post|tweet|publish|save/i;
|
|
1661
1792
|
const orphanInputs = Array.from(
|
|
1662
1793
|
document.querySelectorAll(
|
|
1663
|
-
'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)'
|
|
1794
|
+
'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"])'
|
|
1664
1795
|
)
|
|
1665
1796
|
).filter((el) => {
|
|
1666
1797
|
if (el instanceof HTMLInputElement && ORPHAN_EXCLUDED_TYPES.has(el.type.toLowerCase())) {
|
|
@@ -1791,7 +1922,11 @@ async function scanOrphanInputs(config) {
|
|
|
1791
1922
|
for (const { key, el } of inputPairs) {
|
|
1792
1923
|
if (params[key] !== void 0) {
|
|
1793
1924
|
console.log(`[auto-webmcp] orphan execute: filling key="${key}" value=`, params[key], "element=", el);
|
|
1794
|
-
|
|
1925
|
+
if (el.getAttribute("role") === "combobox" && el.tagName.toLowerCase() === "button") {
|
|
1926
|
+
await fillComboboxButton(el, params[key]);
|
|
1927
|
+
} else {
|
|
1928
|
+
fillElement(el, params[key]);
|
|
1929
|
+
}
|
|
1795
1930
|
console.log(`[auto-webmcp] orphan execute: after fill, element value=`, el.value);
|
|
1796
1931
|
} else {
|
|
1797
1932
|
console.log(`[auto-webmcp] orphan execute: key="${key}" not in params, skipping`);
|
|
@@ -1803,22 +1938,43 @@ async function scanOrphanInputs(config) {
|
|
|
1803
1938
|
console.log(`[auto-webmcp] orphan execute: autoSubmit=false, returning without clicking submit`);
|
|
1804
1939
|
return { content: [{ type: "text", text: "Fields filled. Ready to submit." }] };
|
|
1805
1940
|
}
|
|
1806
|
-
console.log(`[auto-webmcp] orphan execute:
|
|
1941
|
+
console.log(`[auto-webmcp] orphan execute: resolving submit button (up to 2s)...`);
|
|
1807
1942
|
let btn = null;
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
const
|
|
1811
|
-
|
|
1943
|
+
if (submitBtn && document.contains(submitBtn)) {
|
|
1944
|
+
const isEnabled = !submitBtn.disabled && submitBtn.getAttribute("aria-disabled") !== "true";
|
|
1945
|
+
const r = submitBtn.getBoundingClientRect();
|
|
1946
|
+
if (isEnabled && r.width > 0 && r.height > 0) {
|
|
1947
|
+
btn = submitBtn;
|
|
1948
|
+
console.log(`[auto-webmcp] orphan execute: using captured submit button "${btn.textContent?.trim()}"`);
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
if (!btn) {
|
|
1952
|
+
const deadline = Date.now() + 2e3;
|
|
1953
|
+
while (Date.now() < deadline) {
|
|
1954
|
+
const candidates = Array.from(
|
|
1955
|
+
container.querySelectorAll(SUBMIT_BTN_SELECTOR)
|
|
1956
|
+
).filter((b) => {
|
|
1957
|
+
const r = b.getBoundingClientRect();
|
|
1958
|
+
return r.width > 0 && r.height > 0;
|
|
1959
|
+
});
|
|
1960
|
+
const last = candidates[candidates.length - 1] ?? null;
|
|
1961
|
+
if (last) {
|
|
1962
|
+
btn = last;
|
|
1963
|
+
break;
|
|
1964
|
+
}
|
|
1965
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
if (!btn) {
|
|
1969
|
+
const textBtns = Array.from(
|
|
1970
|
+
(container !== document.body ? container : document).querySelectorAll('button, [role="button"]')
|
|
1812
1971
|
).filter((b) => {
|
|
1813
1972
|
const r = b.getBoundingClientRect();
|
|
1814
|
-
return r.width > 0 && r.height > 0;
|
|
1973
|
+
return r.width > 0 && r.height > 0 && !b.disabled && b.getAttribute("aria-disabled") !== "true" && SUBMIT_TEXT_RE.test(b.textContent ?? "");
|
|
1815
1974
|
});
|
|
1816
|
-
|
|
1817
|
-
if (
|
|
1818
|
-
|
|
1819
|
-
break;
|
|
1820
|
-
}
|
|
1821
|
-
await new Promise((r) => setTimeout(r, 100));
|
|
1975
|
+
btn = textBtns[textBtns.length - 1] ?? null;
|
|
1976
|
+
if (btn)
|
|
1977
|
+
console.log(`[auto-webmcp] orphan execute: using text-matched fallback button "${btn.textContent?.trim()}"`);
|
|
1822
1978
|
}
|
|
1823
1979
|
if (!btn) {
|
|
1824
1980
|
console.warn(`[auto-webmcp] orphan execute: submit button still disabled after 2s`);
|
|
@@ -1829,6 +1985,10 @@ async function scanOrphanInputs(config) {
|
|
|
1829
1985
|
return { content: [{ type: "text", text: "Fields filled and form submitted." }] };
|
|
1830
1986
|
};
|
|
1831
1987
|
try {
|
|
1988
|
+
if (registeredOrphanToolNames.has(metadata.name)) {
|
|
1989
|
+
console.log(`[auto-webmcp] orphan: "${metadata.name}" already registered, skipping`);
|
|
1990
|
+
continue;
|
|
1991
|
+
}
|
|
1832
1992
|
const toolDef = {
|
|
1833
1993
|
name: metadata.name,
|
|
1834
1994
|
description: metadata.description,
|
|
@@ -1839,6 +1999,7 @@ async function scanOrphanInputs(config) {
|
|
|
1839
1999
|
toolDef.annotations = metadata.annotations;
|
|
1840
2000
|
}
|
|
1841
2001
|
await navigator.modelContext.registerTool(toolDef);
|
|
2002
|
+
registeredOrphanToolNames.add(metadata.name);
|
|
1842
2003
|
const pendingBtns = window["__pendingSubmitBtns"] ??= {};
|
|
1843
2004
|
pendingBtns[metadata.name] = submitBtn;
|
|
1844
2005
|
if (config.debug) {
|
|
@@ -1866,6 +2027,7 @@ async function startDiscovery(config) {
|
|
|
1866
2027
|
);
|
|
1867
2028
|
}
|
|
1868
2029
|
registeredFormCount = 0;
|
|
2030
|
+
registeredOrphanToolNames.clear();
|
|
1869
2031
|
startObserver(config);
|
|
1870
2032
|
listenForRouteChanges(config);
|
|
1871
2033
|
await scanForms(config);
|