auto-webmcp 0.2.0 → 0.2.2

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/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  **Automatically make any HTML form WebMCP-ready — zero explicit coding required.**
4
4
 
5
+ [Read the article on dev.to](https://dev.to/prasannagyde/every-web-form-should-be-callable-by-ai-agents-and-yours-can-be-today-228)  ·  [Live demo](https://autowebmcp.dev/demo)  ·  [Platform guides](https://autowebmcp.dev/platforms)
6
+
5
7
  Drop in one script tag (or one `import`) and every `<form>` on your page is
6
8
  instantly registered as a structured tool that in-browser AI agents can
7
9
  discover and use via Chrome's
@@ -7,6 +7,8 @@ export interface ToolMetadata {
7
7
  name: string;
8
8
  description: string;
9
9
  inputSchema: JsonSchema;
10
+ /** Key → DOM element for fields not addressable by name (id-keyed or ARIA-role controls). */
11
+ fieldElements?: Map<string, Element>;
10
12
  }
11
13
  /** Reset form index counter (useful in tests) */
12
14
  export declare function resetFormIndex(): void;
@@ -1 +1 @@
1
- {"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,UAAU,EAA8E,MAAM,aAAa,CAAC;AACrH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,UAAU,CAAC;CACzB;AAKD,iDAAiD;AACjD,wBAAgB,cAAc,IAAI,IAAI,CAErC;AAED,gDAAgD;AAChD,wBAAgB,WAAW,CAAC,IAAI,EAAE,eAAe,EAAE,QAAQ,CAAC,EAAE,YAAY,GAAG,YAAY,CAMxF"}
1
+ {"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,UAAU,EAA8H,MAAM,aAAa,CAAC;AACrK,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,UAAU,CAAC;IACxB,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,CAMxF"}
@@ -40,12 +40,25 @@ async function registerFormTool(form, metadata, execute) {
40
40
  if (existing) {
41
41
  await unregisterFormTool(form);
42
42
  }
43
- await navigator.modelContext.registerTool({
44
- name: metadata.name,
45
- description: metadata.description,
46
- inputSchema: metadata.inputSchema,
47
- execute
48
- });
43
+ try {
44
+ await navigator.modelContext.registerTool({
45
+ name: metadata.name,
46
+ description: metadata.description,
47
+ inputSchema: metadata.inputSchema,
48
+ execute
49
+ });
50
+ } catch {
51
+ try {
52
+ await navigator.modelContext.unregisterTool(metadata.name);
53
+ await navigator.modelContext.registerTool({
54
+ name: metadata.name,
55
+ description: metadata.description,
56
+ inputSchema: metadata.inputSchema,
57
+ execute
58
+ });
59
+ } catch {
60
+ }
61
+ }
49
62
  registeredTools.set(form, metadata.name);
50
63
  }
51
64
  async function unregisterFormTool(form) {
@@ -97,6 +110,16 @@ function resolveConfig(userConfig) {
97
110
  }
98
111
 
99
112
  // src/schema.ts
113
+ var ARIA_ROLES_TO_SCAN = [
114
+ "textbox",
115
+ "combobox",
116
+ "checkbox",
117
+ "radio",
118
+ "switch",
119
+ "spinbutton",
120
+ "searchbox",
121
+ "slider"
122
+ ];
100
123
  function inputTypeToSchema(input) {
101
124
  if (input instanceof HTMLInputElement) {
102
125
  return mapInputElement(input);
@@ -192,6 +215,49 @@ function collectRadioOneOf(form, name) {
192
215
  return { const: r.value, title: title || r.value };
193
216
  });
194
217
  }
218
+ function ariaRoleToSchema(el, role) {
219
+ switch (role) {
220
+ case "checkbox":
221
+ case "switch":
222
+ return { type: "boolean" };
223
+ case "spinbutton":
224
+ case "slider": {
225
+ const prop = { type: "number" };
226
+ const min = el.getAttribute("aria-valuemin");
227
+ const max = el.getAttribute("aria-valuemax");
228
+ if (min !== null)
229
+ prop.minimum = parseFloat(min);
230
+ if (max !== null)
231
+ prop.maximum = parseFloat(max);
232
+ return prop;
233
+ }
234
+ case "combobox": {
235
+ const ownedId = el.getAttribute("aria-owns") ?? el.getAttribute("aria-controls");
236
+ if (ownedId) {
237
+ const listbox = document.getElementById(ownedId);
238
+ if (listbox) {
239
+ const options = Array.from(listbox.querySelectorAll('[role="option"]')).filter(
240
+ (o) => o.getAttribute("aria-disabled") !== "true"
241
+ );
242
+ if (options.length > 0) {
243
+ const enumValues = options.map((o) => (o.getAttribute("data-value") ?? o.textContent ?? "").trim()).filter(Boolean);
244
+ const oneOf = options.map((o) => ({
245
+ const: (o.getAttribute("data-value") ?? o.textContent ?? "").trim(),
246
+ title: (o.textContent ?? "").trim()
247
+ }));
248
+ return { type: "string", enum: enumValues, oneOf };
249
+ }
250
+ }
251
+ }
252
+ return { type: "string" };
253
+ }
254
+ case "textbox":
255
+ case "searchbox":
256
+ case "radio":
257
+ default:
258
+ return { type: "string" };
259
+ }
260
+ }
195
261
  function getRadioLabelText(radio) {
196
262
  const parent = radio.closest("label");
197
263
  if (parent) {
@@ -217,8 +283,8 @@ var formIndex = 0;
217
283
  function analyzeForm(form, override) {
218
284
  const name = override?.name ?? inferToolName(form);
219
285
  const description = override?.description ?? inferToolDescription(form);
220
- const inputSchema = buildSchema(form);
221
- return { name, description, inputSchema };
286
+ const { schema: inputSchema, fieldElements } = buildSchema(form);
287
+ return { name, description, inputSchema, fieldElements };
222
288
  }
223
289
  function inferToolName(form) {
224
290
  const nativeName = form.getAttribute("toolname");
@@ -318,6 +384,7 @@ function inferToolDescription(form) {
318
384
  function buildSchema(form) {
319
385
  const properties = {};
320
386
  const required = [];
387
+ const fieldElements = /* @__PURE__ */ new Map();
321
388
  const processedRadioGroups = /* @__PURE__ */ new Set();
322
389
  const controls = Array.from(
323
390
  form.querySelectorAll(
@@ -326,12 +393,13 @@ function buildSchema(form) {
326
393
  );
327
394
  for (const control of controls) {
328
395
  const name = control.name;
329
- if (!name)
396
+ const fieldKey = name || resolveNativeControlFallbackKey(control);
397
+ if (!fieldKey)
330
398
  continue;
331
399
  if (control instanceof HTMLInputElement && control.type === "radio") {
332
- if (processedRadioGroups.has(name))
400
+ if (processedRadioGroups.has(fieldKey))
333
401
  continue;
334
- processedRadioGroups.add(name);
402
+ processedRadioGroups.add(fieldKey);
335
403
  }
336
404
  const schemaProp = inputTypeToSchema(control);
337
405
  if (!schemaProp)
@@ -341,17 +409,123 @@ function buildSchema(form) {
341
409
  if (desc)
342
410
  schemaProp.description = desc;
343
411
  if (control instanceof HTMLInputElement && control.type === "radio") {
344
- schemaProp.enum = collectRadioEnum(form, name);
345
- const radioOneOf = collectRadioOneOf(form, name);
412
+ schemaProp.enum = collectRadioEnum(form, fieldKey);
413
+ const radioOneOf = collectRadioOneOf(form, fieldKey);
346
414
  if (radioOneOf.length > 0)
347
415
  schemaProp.oneOf = radioOneOf;
348
416
  }
349
- properties[name] = schemaProp;
417
+ properties[fieldKey] = schemaProp;
418
+ if (!name) {
419
+ fieldElements.set(fieldKey, control);
420
+ }
350
421
  if (control.required) {
351
- required.push(name);
422
+ required.push(fieldKey);
423
+ }
424
+ }
425
+ const ariaControls = collectAriaControls(form);
426
+ const processedAriaRadioGroups = /* @__PURE__ */ new Set();
427
+ for (const { el, role, key } of ariaControls) {
428
+ if (properties[key])
429
+ continue;
430
+ if (role === "radio") {
431
+ if (processedAriaRadioGroups.has(key))
432
+ continue;
433
+ processedAriaRadioGroups.add(key);
434
+ }
435
+ const schemaProp = ariaRoleToSchema(el, role);
436
+ schemaProp.title = inferAriaFieldTitle(el);
437
+ const desc = inferAriaFieldDescription(el);
438
+ if (desc)
439
+ schemaProp.description = desc;
440
+ properties[key] = schemaProp;
441
+ fieldElements.set(key, el);
442
+ if (el.getAttribute("aria-required") === "true") {
443
+ required.push(key);
352
444
  }
353
445
  }
354
- return { type: "object", properties, required };
446
+ return { schema: { type: "object", properties, required }, fieldElements };
447
+ }
448
+ function resolveNativeControlFallbackKey(control) {
449
+ const el = control;
450
+ if (el.dataset["webmcpName"])
451
+ return sanitizeName(el.dataset["webmcpName"]);
452
+ if (control.id)
453
+ return sanitizeName(control.id);
454
+ const label = control.getAttribute("aria-label");
455
+ if (label)
456
+ return sanitizeName(label);
457
+ return null;
458
+ }
459
+ function collectAriaControls(form) {
460
+ const selector = ARIA_ROLES_TO_SCAN.map((r) => `[role="${r}"]`).join(", ");
461
+ const results = [];
462
+ for (const el of Array.from(form.querySelectorAll(selector))) {
463
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement)
464
+ continue;
465
+ if (el.getAttribute("aria-hidden") === "true" || el.hidden)
466
+ continue;
467
+ const role = el.getAttribute("role");
468
+ const key = resolveAriaFieldKey(el);
469
+ if (!key)
470
+ continue;
471
+ results.push({ el, role, key });
472
+ }
473
+ return results;
474
+ }
475
+ function resolveAriaFieldKey(el) {
476
+ const htmlEl = el;
477
+ if (htmlEl.dataset?.["webmcpName"])
478
+ return sanitizeName(htmlEl.dataset["webmcpName"]);
479
+ if (el.id)
480
+ return sanitizeName(el.id);
481
+ const label = el.getAttribute("aria-label");
482
+ if (label)
483
+ return sanitizeName(label);
484
+ const labelledById = el.getAttribute("aria-labelledby");
485
+ if (labelledById) {
486
+ const text = document.getElementById(labelledById)?.textContent?.trim();
487
+ if (text)
488
+ return sanitizeName(text);
489
+ }
490
+ return null;
491
+ }
492
+ function inferAriaFieldTitle(el) {
493
+ const htmlEl = el;
494
+ if (htmlEl.dataset?.["webmcpTitle"])
495
+ return htmlEl.dataset["webmcpTitle"];
496
+ const label = el.getAttribute("aria-label");
497
+ if (label)
498
+ return label.trim();
499
+ const labelledById = el.getAttribute("aria-labelledby");
500
+ if (labelledById) {
501
+ const text = document.getElementById(labelledById)?.textContent?.trim();
502
+ if (text)
503
+ return text;
504
+ }
505
+ if (el.id)
506
+ return humanizeName(el.id);
507
+ return "";
508
+ }
509
+ function inferAriaFieldDescription(el) {
510
+ const nativeParamDesc = el.getAttribute("toolparamdescription");
511
+ if (nativeParamDesc)
512
+ return nativeParamDesc.trim();
513
+ const htmlEl = el;
514
+ if (htmlEl.dataset?.["webmcpDescription"])
515
+ return htmlEl.dataset["webmcpDescription"];
516
+ const ariaDesc = el.getAttribute("aria-description");
517
+ if (ariaDesc)
518
+ return ariaDesc;
519
+ const describedById = el.getAttribute("aria-describedby");
520
+ if (describedById) {
521
+ const text = document.getElementById(describedById)?.textContent?.trim();
522
+ if (text)
523
+ return text;
524
+ }
525
+ const placeholder = el.getAttribute("placeholder") ?? el.dataset?.["placeholder"];
526
+ if (placeholder)
527
+ return placeholder.trim();
528
+ return "";
355
529
  }
356
530
  function inferFieldTitle(control) {
357
531
  if ("dataset" in control && control.dataset["webmcpTitle"]) {
@@ -420,7 +594,15 @@ init_registry();
420
594
 
421
595
  // src/interceptor.ts
422
596
  var pendingExecutions = /* @__PURE__ */ new WeakMap();
423
- function buildExecuteHandler(form, config, toolName) {
597
+ var lastParams = /* @__PURE__ */ new WeakMap();
598
+ var formFieldElements = /* @__PURE__ */ new WeakMap();
599
+ var _inputValueSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
600
+ var _textareaValueSetter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, "value")?.set;
601
+ var _checkedSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "checked")?.set;
602
+ function buildExecuteHandler(form, config, toolName, metadata) {
603
+ if (metadata?.fieldElements) {
604
+ formFieldElements.set(form, metadata.fieldElements);
605
+ }
424
606
  attachSubmitInterceptor(form, toolName);
425
607
  return async (params) => {
426
608
  fillFormFields(form, params);
@@ -443,7 +625,7 @@ function attachSubmitInterceptor(form, toolName) {
443
625
  return;
444
626
  const { resolve } = pending;
445
627
  pendingExecutions.delete(form);
446
- const formData = serializeFormData(form);
628
+ const formData = serializeFormData(form, lastParams.get(form), formFieldElements.get(form));
447
629
  const text = JSON.stringify(formData);
448
630
  const result = { content: [{ type: "text", text }] };
449
631
  if (e.agentInvoked && typeof e.respondWith === "function") {
@@ -456,52 +638,98 @@ function attachSubmitInterceptor(form, toolName) {
456
638
  window.dispatchEvent(new CustomEvent("toolcancel", { detail: { toolName } }));
457
639
  });
458
640
  }
641
+ function setReactValue(el, v) {
642
+ const setter = el instanceof HTMLTextAreaElement ? _textareaValueSetter : _inputValueSetter;
643
+ if (setter) {
644
+ setter.call(el, v);
645
+ } else {
646
+ el.value = v;
647
+ }
648
+ el.dispatchEvent(new Event("input", { bubbles: true }));
649
+ el.dispatchEvent(new Event("change", { bubbles: true }));
650
+ }
651
+ function setReactChecked(el, checked) {
652
+ if (_checkedSetter) {
653
+ _checkedSetter.call(el, checked);
654
+ } else {
655
+ el.checked = checked;
656
+ }
657
+ el.dispatchEvent(new Event("change", { bubbles: true }));
658
+ }
659
+ function findNativeField(form, key) {
660
+ const esc = CSS.escape(key);
661
+ return form.querySelector(`[name="${esc}"]`) ?? form.querySelector(
662
+ `input#${esc}, textarea#${esc}, select#${esc}`
663
+ );
664
+ }
459
665
  function fillFormFields(form, params) {
460
- for (const [name, value] of Object.entries(params)) {
461
- const escapedName = CSS.escape(name);
462
- const input = form.querySelector(
463
- `[name="${escapedName}"]`
464
- );
465
- if (!input)
666
+ lastParams.set(form, params);
667
+ const fieldEls = formFieldElements.get(form);
668
+ for (const [key, value] of Object.entries(params)) {
669
+ const input = findNativeField(form, key);
670
+ if (input) {
671
+ if (input instanceof HTMLInputElement) {
672
+ fillInput(input, form, key, value);
673
+ } else if (input instanceof HTMLTextAreaElement) {
674
+ setReactValue(input, String(value ?? ""));
675
+ } else if (input instanceof HTMLSelectElement) {
676
+ input.value = String(value ?? "");
677
+ input.dispatchEvent(new Event("change", { bubbles: true }));
678
+ }
466
679
  continue;
467
- if (input instanceof HTMLInputElement) {
468
- fillInput(input, form, name, value);
469
- } else if (input instanceof HTMLTextAreaElement) {
470
- input.value = String(value ?? "");
471
- input.dispatchEvent(new Event("input", { bubbles: true }));
472
- input.dispatchEvent(new Event("change", { bubbles: true }));
473
- } else if (input instanceof HTMLSelectElement) {
474
- input.value = String(value ?? "");
475
- input.dispatchEvent(new Event("change", { bubbles: true }));
680
+ }
681
+ const ariaEl = fieldEls?.get(key);
682
+ if (ariaEl) {
683
+ fillAriaField(ariaEl, value);
476
684
  }
477
685
  }
478
686
  }
479
- function fillInput(input, form, name, value) {
687
+ function fillInput(input, form, key, value) {
480
688
  const type = input.type.toLowerCase();
481
689
  if (type === "checkbox") {
482
- input.checked = Boolean(value);
483
- input.dispatchEvent(new Event("change", { bubbles: true }));
690
+ setReactChecked(input, Boolean(value));
484
691
  return;
485
692
  }
486
693
  if (type === "radio") {
487
- const escapedName = CSS.escape(name);
694
+ const esc = CSS.escape(key);
488
695
  const radios = form.querySelectorAll(
489
- `input[type="radio"][name="${escapedName}"]`
696
+ `input[type="radio"][name="${esc}"]`
490
697
  );
491
698
  for (const radio of radios) {
492
699
  if (radio.value === String(value)) {
493
- radio.checked = true;
700
+ if (_checkedSetter) {
701
+ _checkedSetter.call(radio, true);
702
+ } else {
703
+ radio.checked = true;
704
+ }
494
705
  radio.dispatchEvent(new Event("change", { bubbles: true }));
495
706
  break;
496
707
  }
497
708
  }
498
709
  return;
499
710
  }
500
- input.value = String(value ?? "");
501
- input.dispatchEvent(new Event("input", { bubbles: true }));
502
- input.dispatchEvent(new Event("change", { bubbles: true }));
711
+ setReactValue(input, String(value ?? ""));
503
712
  }
504
- function serializeFormData(form) {
713
+ function fillAriaField(el, value) {
714
+ const role = el.getAttribute("role");
715
+ if (role === "checkbox" || role === "switch") {
716
+ el.setAttribute("aria-checked", String(Boolean(value)));
717
+ el.dispatchEvent(new MouseEvent("click", { bubbles: true }));
718
+ return;
719
+ }
720
+ if (role === "radio") {
721
+ el.setAttribute("aria-checked", "true");
722
+ el.dispatchEvent(new MouseEvent("click", { bubbles: true }));
723
+ return;
724
+ }
725
+ const htmlEl = el;
726
+ if (htmlEl.isContentEditable) {
727
+ htmlEl.textContent = String(value ?? "");
728
+ }
729
+ el.dispatchEvent(new Event("input", { bubbles: true }));
730
+ el.dispatchEvent(new Event("change", { bubbles: true }));
731
+ }
732
+ function serializeFormData(form, params, fieldEls) {
505
733
  const result = {};
506
734
  const data = new FormData(form);
507
735
  for (const [key, val] of data.entries()) {
@@ -516,6 +744,27 @@ function serializeFormData(form) {
516
744
  result[key] = val;
517
745
  }
518
746
  }
747
+ if (params) {
748
+ for (const key of Object.keys(params)) {
749
+ if (key in result)
750
+ continue;
751
+ const el = findNativeField(form, key) ?? fieldEls?.get(key) ?? null;
752
+ if (!el)
753
+ continue;
754
+ if (el instanceof HTMLInputElement && el.type === "checkbox") {
755
+ result[key] = el.checked;
756
+ } else if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) {
757
+ result[key] = el.value;
758
+ } else {
759
+ const role = el.getAttribute("role");
760
+ if (role === "checkbox" || role === "switch") {
761
+ result[key] = el.getAttribute("aria-checked") === "true";
762
+ } else {
763
+ result[key] = el.textContent?.trim() ?? "";
764
+ }
765
+ }
766
+ }
767
+ }
519
768
  return result;
520
769
  }
521
770
 
@@ -627,8 +876,9 @@ async function registerForm(form, config) {
627
876
  if (config.debug) {
628
877
  warnToolQuality(metadata.name, metadata.description);
629
878
  }
630
- const execute = buildExecuteHandler(form, config, metadata.name);
879
+ const execute = buildExecuteHandler(form, config, metadata.name, metadata);
631
880
  await registerFormTool(form, metadata, execute);
881
+ registeredForms.add(form);
632
882
  if (config.debug) {
633
883
  console.debug(`[auto-webmcp] Registered: ${metadata.name}`, metadata);
634
884
  }
@@ -640,12 +890,43 @@ async function unregisterForm(form, config) {
640
890
  if (!name)
641
891
  return;
642
892
  await unregisterFormTool(form);
893
+ registeredForms.delete(form);
643
894
  if (config.debug) {
644
895
  console.debug(`[auto-webmcp] Unregistered: ${name}`);
645
896
  }
646
897
  emit("form:unregistered", form, name);
647
898
  }
648
899
  var observer = null;
900
+ var registeredForms = /* @__PURE__ */ new WeakSet();
901
+ var reAnalysisTimers = /* @__PURE__ */ new Map();
902
+ var RE_ANALYSIS_DEBOUNCE_MS = 300;
903
+ function isInterestingNode(node) {
904
+ const tag = node.tagName.toLowerCase();
905
+ if (tag === "input" || tag === "textarea" || tag === "select")
906
+ return true;
907
+ const role = node.getAttribute("role");
908
+ if (role && ARIA_ROLES_TO_SCAN.includes(role))
909
+ return true;
910
+ if (node.querySelector("input, textarea, select"))
911
+ return true;
912
+ for (const r of ARIA_ROLES_TO_SCAN) {
913
+ if (node.querySelector(`[role="${r}"]`))
914
+ return true;
915
+ }
916
+ return false;
917
+ }
918
+ function scheduleReAnalysis(form, config) {
919
+ const existing = reAnalysisTimers.get(form);
920
+ if (existing)
921
+ clearTimeout(existing);
922
+ reAnalysisTimers.set(
923
+ form,
924
+ setTimeout(() => {
925
+ reAnalysisTimers.delete(form);
926
+ void registerForm(form, config);
927
+ }, RE_ANALYSIS_DEBOUNCE_MS)
928
+ );
929
+ }
649
930
  function startObserver(config) {
650
931
  if (observer)
651
932
  return;
@@ -654,8 +935,15 @@ function startObserver(config) {
654
935
  for (const node of mutation.addedNodes) {
655
936
  if (!(node instanceof Element))
656
937
  continue;
657
- const forms = node instanceof HTMLFormElement ? [node] : Array.from(node.querySelectorAll("form"));
658
- for (const form of forms) {
938
+ if (node instanceof HTMLFormElement) {
939
+ void registerForm(node, config);
940
+ continue;
941
+ }
942
+ const parentForm = node.closest("form");
943
+ if (parentForm instanceof HTMLFormElement && registeredForms.has(parentForm) && isInterestingNode(node)) {
944
+ scheduleReAnalysis(parentForm, config);
945
+ }
946
+ for (const form of Array.from(node.querySelectorAll("form"))) {
659
947
  void registerForm(form, config);
660
948
  }
661
949
  }
@@ -689,7 +977,7 @@ function listenForRouteChanges(config) {
689
977
  }
690
978
  async function scanForms(config) {
691
979
  const forms = Array.from(document.querySelectorAll("form"));
692
- await Promise.all(forms.map((form) => registerForm(form, config)));
980
+ await Promise.allSettled(forms.map((form) => registerForm(form, config)));
693
981
  }
694
982
  function warnToolQuality(name, description) {
695
983
  if (/^form_\d+$|^submit$|^form$/.test(name)) {
@@ -708,9 +996,9 @@ async function startDiscovery(config) {
708
996
  (resolve) => document.addEventListener("DOMContentLoaded", () => resolve(), { once: true })
709
997
  );
710
998
  }
711
- await scanForms(config);
712
999
  startObserver(config);
713
1000
  listenForRouteChanges(config);
1001
+ await scanForms(config);
714
1002
  }
715
1003
  function stopDiscovery() {
716
1004
  observer?.disconnect();