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 +2 -0
- package/dist/analyzer.d.ts +2 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/auto-webmcp.cjs.js +336 -48
- package/dist/auto-webmcp.cjs.js.map +2 -2
- package/dist/auto-webmcp.esm.js +336 -48
- package/dist/auto-webmcp.esm.js.map +2 -2
- package/dist/auto-webmcp.iife.js +2 -2
- package/dist/auto-webmcp.iife.js.map +3 -3
- package/dist/discovery.d.ts.map +1 -1
- package/dist/interceptor.d.ts +2 -1
- package/dist/interceptor.d.ts.map +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/schema.d.ts +4 -0
- package/dist/schema.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/auto-webmcp.esm.js
CHANGED
|
@@ -28,12 +28,25 @@ async function registerFormTool(form, metadata, execute) {
|
|
|
28
28
|
if (existing) {
|
|
29
29
|
await unregisterFormTool(form);
|
|
30
30
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
try {
|
|
32
|
+
await navigator.modelContext.registerTool({
|
|
33
|
+
name: metadata.name,
|
|
34
|
+
description: metadata.description,
|
|
35
|
+
inputSchema: metadata.inputSchema,
|
|
36
|
+
execute
|
|
37
|
+
});
|
|
38
|
+
} catch {
|
|
39
|
+
try {
|
|
40
|
+
await navigator.modelContext.unregisterTool(metadata.name);
|
|
41
|
+
await navigator.modelContext.registerTool({
|
|
42
|
+
name: metadata.name,
|
|
43
|
+
description: metadata.description,
|
|
44
|
+
inputSchema: metadata.inputSchema,
|
|
45
|
+
execute
|
|
46
|
+
});
|
|
47
|
+
} catch {
|
|
48
|
+
}
|
|
49
|
+
}
|
|
37
50
|
registeredTools.set(form, metadata.name);
|
|
38
51
|
}
|
|
39
52
|
async function unregisterFormTool(form) {
|
|
@@ -78,6 +91,16 @@ function resolveConfig(userConfig) {
|
|
|
78
91
|
}
|
|
79
92
|
|
|
80
93
|
// src/schema.ts
|
|
94
|
+
var ARIA_ROLES_TO_SCAN = [
|
|
95
|
+
"textbox",
|
|
96
|
+
"combobox",
|
|
97
|
+
"checkbox",
|
|
98
|
+
"radio",
|
|
99
|
+
"switch",
|
|
100
|
+
"spinbutton",
|
|
101
|
+
"searchbox",
|
|
102
|
+
"slider"
|
|
103
|
+
];
|
|
81
104
|
function inputTypeToSchema(input) {
|
|
82
105
|
if (input instanceof HTMLInputElement) {
|
|
83
106
|
return mapInputElement(input);
|
|
@@ -173,6 +196,49 @@ function collectRadioOneOf(form, name) {
|
|
|
173
196
|
return { const: r.value, title: title || r.value };
|
|
174
197
|
});
|
|
175
198
|
}
|
|
199
|
+
function ariaRoleToSchema(el, role) {
|
|
200
|
+
switch (role) {
|
|
201
|
+
case "checkbox":
|
|
202
|
+
case "switch":
|
|
203
|
+
return { type: "boolean" };
|
|
204
|
+
case "spinbutton":
|
|
205
|
+
case "slider": {
|
|
206
|
+
const prop = { type: "number" };
|
|
207
|
+
const min = el.getAttribute("aria-valuemin");
|
|
208
|
+
const max = el.getAttribute("aria-valuemax");
|
|
209
|
+
if (min !== null)
|
|
210
|
+
prop.minimum = parseFloat(min);
|
|
211
|
+
if (max !== null)
|
|
212
|
+
prop.maximum = parseFloat(max);
|
|
213
|
+
return prop;
|
|
214
|
+
}
|
|
215
|
+
case "combobox": {
|
|
216
|
+
const ownedId = el.getAttribute("aria-owns") ?? el.getAttribute("aria-controls");
|
|
217
|
+
if (ownedId) {
|
|
218
|
+
const listbox = document.getElementById(ownedId);
|
|
219
|
+
if (listbox) {
|
|
220
|
+
const options = Array.from(listbox.querySelectorAll('[role="option"]')).filter(
|
|
221
|
+
(o) => o.getAttribute("aria-disabled") !== "true"
|
|
222
|
+
);
|
|
223
|
+
if (options.length > 0) {
|
|
224
|
+
const enumValues = options.map((o) => (o.getAttribute("data-value") ?? o.textContent ?? "").trim()).filter(Boolean);
|
|
225
|
+
const oneOf = options.map((o) => ({
|
|
226
|
+
const: (o.getAttribute("data-value") ?? o.textContent ?? "").trim(),
|
|
227
|
+
title: (o.textContent ?? "").trim()
|
|
228
|
+
}));
|
|
229
|
+
return { type: "string", enum: enumValues, oneOf };
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return { type: "string" };
|
|
234
|
+
}
|
|
235
|
+
case "textbox":
|
|
236
|
+
case "searchbox":
|
|
237
|
+
case "radio":
|
|
238
|
+
default:
|
|
239
|
+
return { type: "string" };
|
|
240
|
+
}
|
|
241
|
+
}
|
|
176
242
|
function getRadioLabelText(radio) {
|
|
177
243
|
const parent = radio.closest("label");
|
|
178
244
|
if (parent) {
|
|
@@ -198,8 +264,8 @@ var formIndex = 0;
|
|
|
198
264
|
function analyzeForm(form, override) {
|
|
199
265
|
const name = override?.name ?? inferToolName(form);
|
|
200
266
|
const description = override?.description ?? inferToolDescription(form);
|
|
201
|
-
const inputSchema = buildSchema(form);
|
|
202
|
-
return { name, description, inputSchema };
|
|
267
|
+
const { schema: inputSchema, fieldElements } = buildSchema(form);
|
|
268
|
+
return { name, description, inputSchema, fieldElements };
|
|
203
269
|
}
|
|
204
270
|
function inferToolName(form) {
|
|
205
271
|
const nativeName = form.getAttribute("toolname");
|
|
@@ -299,6 +365,7 @@ function inferToolDescription(form) {
|
|
|
299
365
|
function buildSchema(form) {
|
|
300
366
|
const properties = {};
|
|
301
367
|
const required = [];
|
|
368
|
+
const fieldElements = /* @__PURE__ */ new Map();
|
|
302
369
|
const processedRadioGroups = /* @__PURE__ */ new Set();
|
|
303
370
|
const controls = Array.from(
|
|
304
371
|
form.querySelectorAll(
|
|
@@ -307,12 +374,13 @@ function buildSchema(form) {
|
|
|
307
374
|
);
|
|
308
375
|
for (const control of controls) {
|
|
309
376
|
const name = control.name;
|
|
310
|
-
|
|
377
|
+
const fieldKey = name || resolveNativeControlFallbackKey(control);
|
|
378
|
+
if (!fieldKey)
|
|
311
379
|
continue;
|
|
312
380
|
if (control instanceof HTMLInputElement && control.type === "radio") {
|
|
313
|
-
if (processedRadioGroups.has(
|
|
381
|
+
if (processedRadioGroups.has(fieldKey))
|
|
314
382
|
continue;
|
|
315
|
-
processedRadioGroups.add(
|
|
383
|
+
processedRadioGroups.add(fieldKey);
|
|
316
384
|
}
|
|
317
385
|
const schemaProp = inputTypeToSchema(control);
|
|
318
386
|
if (!schemaProp)
|
|
@@ -322,17 +390,123 @@ function buildSchema(form) {
|
|
|
322
390
|
if (desc)
|
|
323
391
|
schemaProp.description = desc;
|
|
324
392
|
if (control instanceof HTMLInputElement && control.type === "radio") {
|
|
325
|
-
schemaProp.enum = collectRadioEnum(form,
|
|
326
|
-
const radioOneOf = collectRadioOneOf(form,
|
|
393
|
+
schemaProp.enum = collectRadioEnum(form, fieldKey);
|
|
394
|
+
const radioOneOf = collectRadioOneOf(form, fieldKey);
|
|
327
395
|
if (radioOneOf.length > 0)
|
|
328
396
|
schemaProp.oneOf = radioOneOf;
|
|
329
397
|
}
|
|
330
|
-
properties[
|
|
398
|
+
properties[fieldKey] = schemaProp;
|
|
399
|
+
if (!name) {
|
|
400
|
+
fieldElements.set(fieldKey, control);
|
|
401
|
+
}
|
|
331
402
|
if (control.required) {
|
|
332
|
-
required.push(
|
|
403
|
+
required.push(fieldKey);
|
|
333
404
|
}
|
|
334
405
|
}
|
|
335
|
-
|
|
406
|
+
const ariaControls = collectAriaControls(form);
|
|
407
|
+
const processedAriaRadioGroups = /* @__PURE__ */ new Set();
|
|
408
|
+
for (const { el, role, key } of ariaControls) {
|
|
409
|
+
if (properties[key])
|
|
410
|
+
continue;
|
|
411
|
+
if (role === "radio") {
|
|
412
|
+
if (processedAriaRadioGroups.has(key))
|
|
413
|
+
continue;
|
|
414
|
+
processedAriaRadioGroups.add(key);
|
|
415
|
+
}
|
|
416
|
+
const schemaProp = ariaRoleToSchema(el, role);
|
|
417
|
+
schemaProp.title = inferAriaFieldTitle(el);
|
|
418
|
+
const desc = inferAriaFieldDescription(el);
|
|
419
|
+
if (desc)
|
|
420
|
+
schemaProp.description = desc;
|
|
421
|
+
properties[key] = schemaProp;
|
|
422
|
+
fieldElements.set(key, el);
|
|
423
|
+
if (el.getAttribute("aria-required") === "true") {
|
|
424
|
+
required.push(key);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return { schema: { type: "object", properties, required }, fieldElements };
|
|
428
|
+
}
|
|
429
|
+
function resolveNativeControlFallbackKey(control) {
|
|
430
|
+
const el = control;
|
|
431
|
+
if (el.dataset["webmcpName"])
|
|
432
|
+
return sanitizeName(el.dataset["webmcpName"]);
|
|
433
|
+
if (control.id)
|
|
434
|
+
return sanitizeName(control.id);
|
|
435
|
+
const label = control.getAttribute("aria-label");
|
|
436
|
+
if (label)
|
|
437
|
+
return sanitizeName(label);
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
function collectAriaControls(form) {
|
|
441
|
+
const selector = ARIA_ROLES_TO_SCAN.map((r) => `[role="${r}"]`).join(", ");
|
|
442
|
+
const results = [];
|
|
443
|
+
for (const el of Array.from(form.querySelectorAll(selector))) {
|
|
444
|
+
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement)
|
|
445
|
+
continue;
|
|
446
|
+
if (el.getAttribute("aria-hidden") === "true" || el.hidden)
|
|
447
|
+
continue;
|
|
448
|
+
const role = el.getAttribute("role");
|
|
449
|
+
const key = resolveAriaFieldKey(el);
|
|
450
|
+
if (!key)
|
|
451
|
+
continue;
|
|
452
|
+
results.push({ el, role, key });
|
|
453
|
+
}
|
|
454
|
+
return results;
|
|
455
|
+
}
|
|
456
|
+
function resolveAriaFieldKey(el) {
|
|
457
|
+
const htmlEl = el;
|
|
458
|
+
if (htmlEl.dataset?.["webmcpName"])
|
|
459
|
+
return sanitizeName(htmlEl.dataset["webmcpName"]);
|
|
460
|
+
if (el.id)
|
|
461
|
+
return sanitizeName(el.id);
|
|
462
|
+
const label = el.getAttribute("aria-label");
|
|
463
|
+
if (label)
|
|
464
|
+
return sanitizeName(label);
|
|
465
|
+
const labelledById = el.getAttribute("aria-labelledby");
|
|
466
|
+
if (labelledById) {
|
|
467
|
+
const text = document.getElementById(labelledById)?.textContent?.trim();
|
|
468
|
+
if (text)
|
|
469
|
+
return sanitizeName(text);
|
|
470
|
+
}
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
function inferAriaFieldTitle(el) {
|
|
474
|
+
const htmlEl = el;
|
|
475
|
+
if (htmlEl.dataset?.["webmcpTitle"])
|
|
476
|
+
return htmlEl.dataset["webmcpTitle"];
|
|
477
|
+
const label = el.getAttribute("aria-label");
|
|
478
|
+
if (label)
|
|
479
|
+
return label.trim();
|
|
480
|
+
const labelledById = el.getAttribute("aria-labelledby");
|
|
481
|
+
if (labelledById) {
|
|
482
|
+
const text = document.getElementById(labelledById)?.textContent?.trim();
|
|
483
|
+
if (text)
|
|
484
|
+
return text;
|
|
485
|
+
}
|
|
486
|
+
if (el.id)
|
|
487
|
+
return humanizeName(el.id);
|
|
488
|
+
return "";
|
|
489
|
+
}
|
|
490
|
+
function inferAriaFieldDescription(el) {
|
|
491
|
+
const nativeParamDesc = el.getAttribute("toolparamdescription");
|
|
492
|
+
if (nativeParamDesc)
|
|
493
|
+
return nativeParamDesc.trim();
|
|
494
|
+
const htmlEl = el;
|
|
495
|
+
if (htmlEl.dataset?.["webmcpDescription"])
|
|
496
|
+
return htmlEl.dataset["webmcpDescription"];
|
|
497
|
+
const ariaDesc = el.getAttribute("aria-description");
|
|
498
|
+
if (ariaDesc)
|
|
499
|
+
return ariaDesc;
|
|
500
|
+
const describedById = el.getAttribute("aria-describedby");
|
|
501
|
+
if (describedById) {
|
|
502
|
+
const text = document.getElementById(describedById)?.textContent?.trim();
|
|
503
|
+
if (text)
|
|
504
|
+
return text;
|
|
505
|
+
}
|
|
506
|
+
const placeholder = el.getAttribute("placeholder") ?? el.dataset?.["placeholder"];
|
|
507
|
+
if (placeholder)
|
|
508
|
+
return placeholder.trim();
|
|
509
|
+
return "";
|
|
336
510
|
}
|
|
337
511
|
function inferFieldTitle(control) {
|
|
338
512
|
if ("dataset" in control && control.dataset["webmcpTitle"]) {
|
|
@@ -401,7 +575,15 @@ init_registry();
|
|
|
401
575
|
|
|
402
576
|
// src/interceptor.ts
|
|
403
577
|
var pendingExecutions = /* @__PURE__ */ new WeakMap();
|
|
404
|
-
|
|
578
|
+
var lastParams = /* @__PURE__ */ new WeakMap();
|
|
579
|
+
var formFieldElements = /* @__PURE__ */ new WeakMap();
|
|
580
|
+
var _inputValueSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
|
|
581
|
+
var _textareaValueSetter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, "value")?.set;
|
|
582
|
+
var _checkedSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "checked")?.set;
|
|
583
|
+
function buildExecuteHandler(form, config, toolName, metadata) {
|
|
584
|
+
if (metadata?.fieldElements) {
|
|
585
|
+
formFieldElements.set(form, metadata.fieldElements);
|
|
586
|
+
}
|
|
405
587
|
attachSubmitInterceptor(form, toolName);
|
|
406
588
|
return async (params) => {
|
|
407
589
|
fillFormFields(form, params);
|
|
@@ -424,7 +606,7 @@ function attachSubmitInterceptor(form, toolName) {
|
|
|
424
606
|
return;
|
|
425
607
|
const { resolve } = pending;
|
|
426
608
|
pendingExecutions.delete(form);
|
|
427
|
-
const formData = serializeFormData(form);
|
|
609
|
+
const formData = serializeFormData(form, lastParams.get(form), formFieldElements.get(form));
|
|
428
610
|
const text = JSON.stringify(formData);
|
|
429
611
|
const result = { content: [{ type: "text", text }] };
|
|
430
612
|
if (e.agentInvoked && typeof e.respondWith === "function") {
|
|
@@ -437,52 +619,98 @@ function attachSubmitInterceptor(form, toolName) {
|
|
|
437
619
|
window.dispatchEvent(new CustomEvent("toolcancel", { detail: { toolName } }));
|
|
438
620
|
});
|
|
439
621
|
}
|
|
622
|
+
function setReactValue(el, v) {
|
|
623
|
+
const setter = el instanceof HTMLTextAreaElement ? _textareaValueSetter : _inputValueSetter;
|
|
624
|
+
if (setter) {
|
|
625
|
+
setter.call(el, v);
|
|
626
|
+
} else {
|
|
627
|
+
el.value = v;
|
|
628
|
+
}
|
|
629
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
630
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
631
|
+
}
|
|
632
|
+
function setReactChecked(el, checked) {
|
|
633
|
+
if (_checkedSetter) {
|
|
634
|
+
_checkedSetter.call(el, checked);
|
|
635
|
+
} else {
|
|
636
|
+
el.checked = checked;
|
|
637
|
+
}
|
|
638
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
639
|
+
}
|
|
640
|
+
function findNativeField(form, key) {
|
|
641
|
+
const esc = CSS.escape(key);
|
|
642
|
+
return form.querySelector(`[name="${esc}"]`) ?? form.querySelector(
|
|
643
|
+
`input#${esc}, textarea#${esc}, select#${esc}`
|
|
644
|
+
);
|
|
645
|
+
}
|
|
440
646
|
function fillFormFields(form, params) {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
)
|
|
446
|
-
|
|
647
|
+
lastParams.set(form, params);
|
|
648
|
+
const fieldEls = formFieldElements.get(form);
|
|
649
|
+
for (const [key, value] of Object.entries(params)) {
|
|
650
|
+
const input = findNativeField(form, key);
|
|
651
|
+
if (input) {
|
|
652
|
+
if (input instanceof HTMLInputElement) {
|
|
653
|
+
fillInput(input, form, key, value);
|
|
654
|
+
} else if (input instanceof HTMLTextAreaElement) {
|
|
655
|
+
setReactValue(input, String(value ?? ""));
|
|
656
|
+
} else if (input instanceof HTMLSelectElement) {
|
|
657
|
+
input.value = String(value ?? "");
|
|
658
|
+
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
659
|
+
}
|
|
447
660
|
continue;
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
453
|
-
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
454
|
-
} else if (input instanceof HTMLSelectElement) {
|
|
455
|
-
input.value = String(value ?? "");
|
|
456
|
-
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
661
|
+
}
|
|
662
|
+
const ariaEl = fieldEls?.get(key);
|
|
663
|
+
if (ariaEl) {
|
|
664
|
+
fillAriaField(ariaEl, value);
|
|
457
665
|
}
|
|
458
666
|
}
|
|
459
667
|
}
|
|
460
|
-
function fillInput(input, form,
|
|
668
|
+
function fillInput(input, form, key, value) {
|
|
461
669
|
const type = input.type.toLowerCase();
|
|
462
670
|
if (type === "checkbox") {
|
|
463
|
-
input
|
|
464
|
-
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
671
|
+
setReactChecked(input, Boolean(value));
|
|
465
672
|
return;
|
|
466
673
|
}
|
|
467
674
|
if (type === "radio") {
|
|
468
|
-
const
|
|
675
|
+
const esc = CSS.escape(key);
|
|
469
676
|
const radios = form.querySelectorAll(
|
|
470
|
-
`input[type="radio"][name="${
|
|
677
|
+
`input[type="radio"][name="${esc}"]`
|
|
471
678
|
);
|
|
472
679
|
for (const radio of radios) {
|
|
473
680
|
if (radio.value === String(value)) {
|
|
474
|
-
|
|
681
|
+
if (_checkedSetter) {
|
|
682
|
+
_checkedSetter.call(radio, true);
|
|
683
|
+
} else {
|
|
684
|
+
radio.checked = true;
|
|
685
|
+
}
|
|
475
686
|
radio.dispatchEvent(new Event("change", { bubbles: true }));
|
|
476
687
|
break;
|
|
477
688
|
}
|
|
478
689
|
}
|
|
479
690
|
return;
|
|
480
691
|
}
|
|
481
|
-
input
|
|
482
|
-
|
|
483
|
-
|
|
692
|
+
setReactValue(input, String(value ?? ""));
|
|
693
|
+
}
|
|
694
|
+
function fillAriaField(el, value) {
|
|
695
|
+
const role = el.getAttribute("role");
|
|
696
|
+
if (role === "checkbox" || role === "switch") {
|
|
697
|
+
el.setAttribute("aria-checked", String(Boolean(value)));
|
|
698
|
+
el.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
if (role === "radio") {
|
|
702
|
+
el.setAttribute("aria-checked", "true");
|
|
703
|
+
el.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
const htmlEl = el;
|
|
707
|
+
if (htmlEl.isContentEditable) {
|
|
708
|
+
htmlEl.textContent = String(value ?? "");
|
|
709
|
+
}
|
|
710
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
711
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
484
712
|
}
|
|
485
|
-
function serializeFormData(form) {
|
|
713
|
+
function serializeFormData(form, params, fieldEls) {
|
|
486
714
|
const result = {};
|
|
487
715
|
const data = new FormData(form);
|
|
488
716
|
for (const [key, val] of data.entries()) {
|
|
@@ -497,6 +725,27 @@ function serializeFormData(form) {
|
|
|
497
725
|
result[key] = val;
|
|
498
726
|
}
|
|
499
727
|
}
|
|
728
|
+
if (params) {
|
|
729
|
+
for (const key of Object.keys(params)) {
|
|
730
|
+
if (key in result)
|
|
731
|
+
continue;
|
|
732
|
+
const el = findNativeField(form, key) ?? fieldEls?.get(key) ?? null;
|
|
733
|
+
if (!el)
|
|
734
|
+
continue;
|
|
735
|
+
if (el instanceof HTMLInputElement && el.type === "checkbox") {
|
|
736
|
+
result[key] = el.checked;
|
|
737
|
+
} else if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) {
|
|
738
|
+
result[key] = el.value;
|
|
739
|
+
} else {
|
|
740
|
+
const role = el.getAttribute("role");
|
|
741
|
+
if (role === "checkbox" || role === "switch") {
|
|
742
|
+
result[key] = el.getAttribute("aria-checked") === "true";
|
|
743
|
+
} else {
|
|
744
|
+
result[key] = el.textContent?.trim() ?? "";
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
500
749
|
return result;
|
|
501
750
|
}
|
|
502
751
|
|
|
@@ -608,8 +857,9 @@ async function registerForm(form, config) {
|
|
|
608
857
|
if (config.debug) {
|
|
609
858
|
warnToolQuality(metadata.name, metadata.description);
|
|
610
859
|
}
|
|
611
|
-
const execute = buildExecuteHandler(form, config, metadata.name);
|
|
860
|
+
const execute = buildExecuteHandler(form, config, metadata.name, metadata);
|
|
612
861
|
await registerFormTool(form, metadata, execute);
|
|
862
|
+
registeredForms.add(form);
|
|
613
863
|
if (config.debug) {
|
|
614
864
|
console.debug(`[auto-webmcp] Registered: ${metadata.name}`, metadata);
|
|
615
865
|
}
|
|
@@ -621,12 +871,43 @@ async function unregisterForm(form, config) {
|
|
|
621
871
|
if (!name)
|
|
622
872
|
return;
|
|
623
873
|
await unregisterFormTool(form);
|
|
874
|
+
registeredForms.delete(form);
|
|
624
875
|
if (config.debug) {
|
|
625
876
|
console.debug(`[auto-webmcp] Unregistered: ${name}`);
|
|
626
877
|
}
|
|
627
878
|
emit("form:unregistered", form, name);
|
|
628
879
|
}
|
|
629
880
|
var observer = null;
|
|
881
|
+
var registeredForms = /* @__PURE__ */ new WeakSet();
|
|
882
|
+
var reAnalysisTimers = /* @__PURE__ */ new Map();
|
|
883
|
+
var RE_ANALYSIS_DEBOUNCE_MS = 300;
|
|
884
|
+
function isInterestingNode(node) {
|
|
885
|
+
const tag = node.tagName.toLowerCase();
|
|
886
|
+
if (tag === "input" || tag === "textarea" || tag === "select")
|
|
887
|
+
return true;
|
|
888
|
+
const role = node.getAttribute("role");
|
|
889
|
+
if (role && ARIA_ROLES_TO_SCAN.includes(role))
|
|
890
|
+
return true;
|
|
891
|
+
if (node.querySelector("input, textarea, select"))
|
|
892
|
+
return true;
|
|
893
|
+
for (const r of ARIA_ROLES_TO_SCAN) {
|
|
894
|
+
if (node.querySelector(`[role="${r}"]`))
|
|
895
|
+
return true;
|
|
896
|
+
}
|
|
897
|
+
return false;
|
|
898
|
+
}
|
|
899
|
+
function scheduleReAnalysis(form, config) {
|
|
900
|
+
const existing = reAnalysisTimers.get(form);
|
|
901
|
+
if (existing)
|
|
902
|
+
clearTimeout(existing);
|
|
903
|
+
reAnalysisTimers.set(
|
|
904
|
+
form,
|
|
905
|
+
setTimeout(() => {
|
|
906
|
+
reAnalysisTimers.delete(form);
|
|
907
|
+
void registerForm(form, config);
|
|
908
|
+
}, RE_ANALYSIS_DEBOUNCE_MS)
|
|
909
|
+
);
|
|
910
|
+
}
|
|
630
911
|
function startObserver(config) {
|
|
631
912
|
if (observer)
|
|
632
913
|
return;
|
|
@@ -635,8 +916,15 @@ function startObserver(config) {
|
|
|
635
916
|
for (const node of mutation.addedNodes) {
|
|
636
917
|
if (!(node instanceof Element))
|
|
637
918
|
continue;
|
|
638
|
-
|
|
639
|
-
|
|
919
|
+
if (node instanceof HTMLFormElement) {
|
|
920
|
+
void registerForm(node, config);
|
|
921
|
+
continue;
|
|
922
|
+
}
|
|
923
|
+
const parentForm = node.closest("form");
|
|
924
|
+
if (parentForm instanceof HTMLFormElement && registeredForms.has(parentForm) && isInterestingNode(node)) {
|
|
925
|
+
scheduleReAnalysis(parentForm, config);
|
|
926
|
+
}
|
|
927
|
+
for (const form of Array.from(node.querySelectorAll("form"))) {
|
|
640
928
|
void registerForm(form, config);
|
|
641
929
|
}
|
|
642
930
|
}
|
|
@@ -670,7 +958,7 @@ function listenForRouteChanges(config) {
|
|
|
670
958
|
}
|
|
671
959
|
async function scanForms(config) {
|
|
672
960
|
const forms = Array.from(document.querySelectorAll("form"));
|
|
673
|
-
await Promise.
|
|
961
|
+
await Promise.allSettled(forms.map((form) => registerForm(form, config)));
|
|
674
962
|
}
|
|
675
963
|
function warnToolQuality(name, description) {
|
|
676
964
|
if (/^form_\d+$|^submit$|^form$/.test(name)) {
|
|
@@ -689,9 +977,9 @@ async function startDiscovery(config) {
|
|
|
689
977
|
(resolve) => document.addEventListener("DOMContentLoaded", () => resolve(), { once: true })
|
|
690
978
|
);
|
|
691
979
|
}
|
|
692
|
-
await scanForms(config);
|
|
693
980
|
startObserver(config);
|
|
694
981
|
listenForRouteChanges(config);
|
|
982
|
+
await scanForms(config);
|
|
695
983
|
}
|
|
696
984
|
function stopDiscovery() {
|
|
697
985
|
observer?.disconnect();
|