@geometra/mcp 1.20.0 → 1.22.0
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/server.js +356 -10
- package/dist/session.d.ts +30 -4
- package/dist/session.js +68 -0
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -617,6 +617,73 @@ Equivalent to \`geometra_wait_for\` with \`present: false\` and \`text\` set to
|
|
|
617
617
|
return err(waited.error);
|
|
618
618
|
return ok(waitConditionSuccessLine(waited.value));
|
|
619
619
|
});
|
|
620
|
+
server.tool('geometra_wait_for_navigation', `Wait until the page URL changes and the new page's DOM stabilizes. Use this after clicking "Next", "Submit", or "Continue" on multi-page forms.
|
|
621
|
+
|
|
622
|
+
Captures the current URL, then polls until the URL changes and a stable UI tree is available. Returns the new page URL and a page model summary.`, {
|
|
623
|
+
timeoutMs: z
|
|
624
|
+
.number()
|
|
625
|
+
.int()
|
|
626
|
+
.min(500)
|
|
627
|
+
.max(60_000)
|
|
628
|
+
.optional()
|
|
629
|
+
.default(10_000)
|
|
630
|
+
.describe('Max time to wait for navigation + DOM stabilization (default 10s)'),
|
|
631
|
+
expectedUrl: z.string().optional().describe('Optional URL substring to match — keeps waiting if the URL changes to something else (e.g. intermediate redirects)'),
|
|
632
|
+
}, async ({ timeoutMs, expectedUrl }) => {
|
|
633
|
+
const session = getSession();
|
|
634
|
+
if (!session)
|
|
635
|
+
return err('Not connected. Call geometra_connect first.');
|
|
636
|
+
const beforeA11y = sessionA11y(session);
|
|
637
|
+
const beforeUrl = beforeA11y?.meta?.pageUrl ?? session.url;
|
|
638
|
+
const startedAt = performance.now();
|
|
639
|
+
let navigated = false;
|
|
640
|
+
while (performance.now() - startedAt < timeoutMs) {
|
|
641
|
+
await waitForUiCondition(session, () => {
|
|
642
|
+
const a = sessionA11y(session);
|
|
643
|
+
if (!a?.meta?.pageUrl)
|
|
644
|
+
return false;
|
|
645
|
+
const currentUrl = a.meta.pageUrl;
|
|
646
|
+
if (currentUrl === beforeUrl)
|
|
647
|
+
return false;
|
|
648
|
+
if (expectedUrl && !currentUrl.includes(expectedUrl))
|
|
649
|
+
return false;
|
|
650
|
+
return true;
|
|
651
|
+
}, Math.max(500, timeoutMs - (performance.now() - startedAt)));
|
|
652
|
+
const afterA11y = sessionA11y(session);
|
|
653
|
+
const afterUrl = afterA11y?.meta?.pageUrl;
|
|
654
|
+
if (afterUrl && afterUrl !== beforeUrl) {
|
|
655
|
+
if (!expectedUrl || afterUrl.includes(expectedUrl)) {
|
|
656
|
+
navigated = true;
|
|
657
|
+
break;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
const elapsedMs = Number((performance.now() - startedAt).toFixed(1));
|
|
662
|
+
const afterA11y = await sessionA11yWhenReady(session);
|
|
663
|
+
const afterUrl = afterA11y?.meta?.pageUrl;
|
|
664
|
+
if (!navigated) {
|
|
665
|
+
return err(JSON.stringify({
|
|
666
|
+
navigated: false,
|
|
667
|
+
beforeUrl,
|
|
668
|
+
currentUrl: afterUrl,
|
|
669
|
+
elapsedMs,
|
|
670
|
+
message: `URL did not change within ${timeoutMs}ms`,
|
|
671
|
+
}));
|
|
672
|
+
}
|
|
673
|
+
const model = afterA11y ? buildPageModel(afterA11y) : undefined;
|
|
674
|
+
return ok(JSON.stringify({
|
|
675
|
+
navigated: true,
|
|
676
|
+
beforeUrl,
|
|
677
|
+
afterUrl,
|
|
678
|
+
elapsedMs,
|
|
679
|
+
...(model ? {
|
|
680
|
+
summary: model.summary,
|
|
681
|
+
archetypes: model.archetypes,
|
|
682
|
+
formCount: model.forms.length,
|
|
683
|
+
...(model.captcha ? { captcha: model.captcha } : {}),
|
|
684
|
+
} : {}),
|
|
685
|
+
}));
|
|
686
|
+
});
|
|
620
687
|
server.tool('geometra_fill_fields', `Fill several labeled form fields in one MCP call. This is the preferred high-level primitive for long forms.
|
|
621
688
|
|
|
622
689
|
Use \`kind: "text"\` for textboxes / textareas, \`"choice"\` for selects / comboboxes / radio-style questions addressed by field label + answer, \`"toggle"\` for individually labeled checkboxes or radios, and \`"file"\` for labeled uploads. When \`fieldId\` from \`geometra_form_schema\` is present, MCP can resolve the current label server-side so you do not need to duplicate \`fieldLabel\` / \`label\` for text, choice, and toggle fields.`, {
|
|
@@ -676,7 +743,8 @@ Use \`kind: "text"\` for textboxes / textareas, \`"choice"\` for selects / combo
|
|
|
676
743
|
}
|
|
677
744
|
catch (e) {
|
|
678
745
|
const message = e instanceof Error ? e.message : String(e);
|
|
679
|
-
|
|
746
|
+
const suggestion = isResolvedFillFieldInput(field) ? suggestRecovery(field, message) : undefined;
|
|
747
|
+
steps.push({ index, kind: field.kind, ok: false, error: message, ...(suggestion ? { suggestion } : {}) });
|
|
680
748
|
if (stopOnError) {
|
|
681
749
|
stoppedAt = index;
|
|
682
750
|
break;
|
|
@@ -732,8 +800,18 @@ Pass \`valuesById\` with field ids from \`geometra_form_schema\` for the most st
|
|
|
732
800
|
.min(0)
|
|
733
801
|
.optional()
|
|
734
802
|
.describe('Resume a partial fill from this field index (from a previous stoppedAt + 1). Skips already-filled fields.'),
|
|
803
|
+
verifyFills: z
|
|
804
|
+
.boolean()
|
|
805
|
+
.optional()
|
|
806
|
+
.default(false)
|
|
807
|
+
.describe('After filling, read back each field value and flag mismatches (e.g. autocomplete rejected input, format transformed). Adds a verification array to the response.'),
|
|
808
|
+
skipPreFilled: z
|
|
809
|
+
.boolean()
|
|
810
|
+
.optional()
|
|
811
|
+
.default(false)
|
|
812
|
+
.describe('Skip fields that already contain a matching value. Avoids overwriting good data from resume parsing or previous fills.'),
|
|
735
813
|
detail: detailInput(),
|
|
736
|
-
}, async ({ url, pageUrl, port, headless, width, height, slowMo, formId, valuesById, valuesByLabel, stopOnError, failOnInvalid, includeSteps, resumeFromIndex, detail }) => {
|
|
814
|
+
}, async ({ url, pageUrl, port, headless, width, height, slowMo, formId, valuesById, valuesByLabel, stopOnError, failOnInvalid, includeSteps, resumeFromIndex, verifyFills, skipPreFilled, detail }) => {
|
|
737
815
|
const directFields = !includeSteps && !formId && Object.keys(valuesById ?? {}).length === 0
|
|
738
816
|
? directLabelBatchFields(valuesByLabel)
|
|
739
817
|
: null;
|
|
@@ -761,6 +839,7 @@ Pass \`valuesById\` with field ids from \`geometra_form_schema\` for the most st
|
|
|
761
839
|
const wait = await sendFillFields(session, directFields);
|
|
762
840
|
const ackResult = parseProxyFillAckResult(wait.result);
|
|
763
841
|
if (ackResult && ackResult.invalidCount === 0) {
|
|
842
|
+
recordWorkflowFill(session, undefined, undefined, valuesById, valuesByLabel, 0, directFields.length);
|
|
764
843
|
return ok(JSON.stringify({
|
|
765
844
|
...connection,
|
|
766
845
|
completed: true,
|
|
@@ -777,6 +856,7 @@ Pass \`valuesById\` with field ids from \`geometra_form_schema\` for the most st
|
|
|
777
856
|
const afterDirect = sessionA11y(session);
|
|
778
857
|
const directSignals = afterDirect ? collectSessionSignals(afterDirect) : undefined;
|
|
779
858
|
if (directSignals && directSignals.invalidFields.length === 0) {
|
|
859
|
+
recordWorkflowFill(session, undefined, undefined, valuesById, valuesByLabel, 0, directFields.length);
|
|
780
860
|
return ok(JSON.stringify({
|
|
781
861
|
...connection,
|
|
782
862
|
completed: true,
|
|
@@ -816,6 +896,34 @@ Pass \`valuesById\` with field ids from \`geometra_form_schema\` for the most st
|
|
|
816
896
|
const planned = planFormFill(schema, { valuesById, valuesByLabel });
|
|
817
897
|
if (!planned.ok)
|
|
818
898
|
return err(planned.error);
|
|
899
|
+
let skippedCount = 0;
|
|
900
|
+
if (skipPreFilled) {
|
|
901
|
+
const schemaFieldById = new Map(schema.fields.map(f => [f.id, f]));
|
|
902
|
+
const indicesToRemove = new Set();
|
|
903
|
+
for (let i = 0; i < planned.planned.length; i++) {
|
|
904
|
+
const p = planned.planned[i];
|
|
905
|
+
const fieldId = p.field.fieldId;
|
|
906
|
+
if (!fieldId)
|
|
907
|
+
continue;
|
|
908
|
+
const schemaField = schemaFieldById.get(fieldId);
|
|
909
|
+
if (!schemaField?.value)
|
|
910
|
+
continue;
|
|
911
|
+
const currentVal = schemaField.value.toLowerCase().trim();
|
|
912
|
+
let intendedVal;
|
|
913
|
+
if (p.field.kind === 'text')
|
|
914
|
+
intendedVal = p.field.value;
|
|
915
|
+
else if (p.field.kind === 'choice')
|
|
916
|
+
intendedVal = p.field.value;
|
|
917
|
+
if (intendedVal && currentVal === intendedVal.toLowerCase().trim()) {
|
|
918
|
+
indicesToRemove.add(i);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
if (indicesToRemove.size > 0) {
|
|
922
|
+
skippedCount = indicesToRemove.size;
|
|
923
|
+
planned.planned = planned.planned.filter((_, i) => !indicesToRemove.has(i));
|
|
924
|
+
planned.fields = planned.planned.map(p => p.field);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
819
927
|
if (!includeSteps) {
|
|
820
928
|
let usedBatch = false;
|
|
821
929
|
let batchAckResult;
|
|
@@ -872,8 +980,12 @@ Pass \`valuesById\` with field ids from \`geometra_form_schema\` for the most st
|
|
|
872
980
|
fieldCount: planned.fields.length,
|
|
873
981
|
successCount: planned.fields.length,
|
|
874
982
|
errorCount: 0,
|
|
983
|
+
minConfidence: planned.planned.length > 0
|
|
984
|
+
? Number(Math.min(...planned.planned.map(p => p.confidence)).toFixed(2))
|
|
985
|
+
: undefined,
|
|
875
986
|
...(signals ? { final: sessionSignalsPayload(signals, detail) } : {}),
|
|
876
987
|
};
|
|
988
|
+
recordWorkflowFill(session, schema.formId, schema.name, valuesById, valuesByLabel, invalidRemaining, planned.fields.length);
|
|
877
989
|
if (failOnInvalid && invalidRemaining > 0) {
|
|
878
990
|
return err(JSON.stringify(payload, null, detail === 'verbose' ? 2 : undefined));
|
|
879
991
|
}
|
|
@@ -885,15 +997,19 @@ Pass \`valuesById\` with field ids from \`geometra_form_schema\` for the most st
|
|
|
885
997
|
const startIndex = resumeFromIndex ?? 0;
|
|
886
998
|
for (let index = startIndex; index < planned.fields.length; index++) {
|
|
887
999
|
const field = planned.fields[index];
|
|
1000
|
+
const plan = planned.planned[index];
|
|
1001
|
+
const confidence = plan?.confidence;
|
|
1002
|
+
const matchMethod = plan?.matchMethod;
|
|
888
1003
|
try {
|
|
889
1004
|
const result = await executeFillField(session, field, detail);
|
|
890
1005
|
steps.push(detail === 'verbose'
|
|
891
|
-
? { index, kind: field.kind, ok: true, summary: result.summary }
|
|
892
|
-
: { index, kind: field.kind, ok: true, ...result.compact });
|
|
1006
|
+
? { index, kind: field.kind, ok: true, ...(confidence !== undefined ? { confidence, matchMethod } : {}), summary: result.summary }
|
|
1007
|
+
: { index, kind: field.kind, ok: true, ...(confidence !== undefined ? { confidence, matchMethod } : {}), ...result.compact });
|
|
893
1008
|
}
|
|
894
1009
|
catch (e) {
|
|
895
1010
|
const message = e instanceof Error ? e.message : String(e);
|
|
896
|
-
|
|
1011
|
+
const suggestion = suggestRecovery(field, message);
|
|
1012
|
+
steps.push({ index, kind: field.kind, ok: false, ...(confidence !== undefined ? { confidence, matchMethod } : {}), error: message, ...(suggestion ? { suggestion } : {}) });
|
|
897
1013
|
if (stopOnError) {
|
|
898
1014
|
stoppedAt = index;
|
|
899
1015
|
break;
|
|
@@ -905,6 +1021,7 @@ Pass \`valuesById\` with field ids from \`geometra_form_schema\` for the most st
|
|
|
905
1021
|
const invalidRemaining = signals?.invalidFields.length ?? 0;
|
|
906
1022
|
const successCount = steps.filter(step => step.ok === true).length;
|
|
907
1023
|
const errorCount = steps.length - successCount;
|
|
1024
|
+
const verification = verifyFills ? verifyFormFills(session, planned.planned) : undefined;
|
|
908
1025
|
const payload = {
|
|
909
1026
|
...connection,
|
|
910
1027
|
completed: stoppedAt === undefined && (startIndex + steps.length) === planned.fields.length,
|
|
@@ -914,11 +1031,17 @@ Pass \`valuesById\` with field ids from \`geometra_form_schema\` for the most st
|
|
|
914
1031
|
fieldCount: planned.fields.length,
|
|
915
1032
|
successCount,
|
|
916
1033
|
errorCount,
|
|
1034
|
+
...(skippedCount > 0 ? { skippedPreFilled: skippedCount } : {}),
|
|
1035
|
+
minConfidence: planned.planned.length > 0
|
|
1036
|
+
? Number(Math.min(...planned.planned.map(p => p.confidence)).toFixed(2))
|
|
1037
|
+
: undefined,
|
|
917
1038
|
...(startIndex > 0 ? { resumedFromIndex: startIndex } : {}),
|
|
918
1039
|
...(includeSteps ? { steps } : {}),
|
|
919
1040
|
...(stoppedAt !== undefined ? { stoppedAt, resumeFromIndex: stoppedAt + 1 } : {}),
|
|
1041
|
+
...(verification ? { verification } : {}),
|
|
920
1042
|
...(signals ? { final: sessionSignalsPayload(signals, detail) } : {}),
|
|
921
1043
|
};
|
|
1044
|
+
recordWorkflowFill(session, schema.formId, schema.name, valuesById, valuesByLabel, invalidRemaining, planned.fields.length);
|
|
922
1045
|
if (failOnInvalid && invalidRemaining > 0) {
|
|
923
1046
|
return err(JSON.stringify(payload, null, detail === 'verbose' ? 2 : undefined));
|
|
924
1047
|
}
|
|
@@ -1713,6 +1836,43 @@ For a token-efficient semantic view, use geometra_snapshot (default compact). Fo
|
|
|
1713
1836
|
return err('Not connected. Call geometra_connect first.');
|
|
1714
1837
|
return ok(JSON.stringify(session.layout, null, 2));
|
|
1715
1838
|
});
|
|
1839
|
+
// ── workflow state ───────────────────────────────────────────
|
|
1840
|
+
server.tool('geometra_workflow_state', `Get the accumulated workflow state across page navigations. Shows which pages/forms have been filled, what values were submitted, and the fill status per page.
|
|
1841
|
+
|
|
1842
|
+
Use this after navigating to a new page in a multi-step flow (e.g. job applications) to understand what has been completed so far. Pass \`clear: true\` to reset the workflow state.`, {
|
|
1843
|
+
clear: z.boolean().optional().default(false).describe('Reset the workflow state'),
|
|
1844
|
+
}, async ({ clear }) => {
|
|
1845
|
+
const session = getSession();
|
|
1846
|
+
if (!session)
|
|
1847
|
+
return err('Not connected. Call geometra_connect first.');
|
|
1848
|
+
if (clear) {
|
|
1849
|
+
session.workflowState = undefined;
|
|
1850
|
+
return ok(JSON.stringify({ cleared: true }));
|
|
1851
|
+
}
|
|
1852
|
+
if (!session.workflowState || session.workflowState.pages.length === 0) {
|
|
1853
|
+
return ok(JSON.stringify({
|
|
1854
|
+
pageCount: 0,
|
|
1855
|
+
message: 'No workflow state recorded yet. Fill a form with geometra_fill_form to start tracking.',
|
|
1856
|
+
}));
|
|
1857
|
+
}
|
|
1858
|
+
const state = session.workflowState;
|
|
1859
|
+
const totalFields = state.pages.reduce((sum, p) => sum + p.fieldCount, 0);
|
|
1860
|
+
const totalInvalid = state.pages.reduce((sum, p) => sum + p.invalidCount, 0);
|
|
1861
|
+
return ok(JSON.stringify({
|
|
1862
|
+
pageCount: state.pages.length,
|
|
1863
|
+
totalFieldsFilled: totalFields,
|
|
1864
|
+
totalInvalidRemaining: totalInvalid,
|
|
1865
|
+
elapsedMs: Date.now() - state.startedAt,
|
|
1866
|
+
pages: state.pages.map(p => ({
|
|
1867
|
+
pageUrl: p.pageUrl,
|
|
1868
|
+
...(p.formId ? { formId: p.formId } : {}),
|
|
1869
|
+
...(p.formName ? { formName: p.formName } : {}),
|
|
1870
|
+
fieldCount: p.fieldCount,
|
|
1871
|
+
invalidCount: p.invalidCount,
|
|
1872
|
+
filledValues: p.filledValues,
|
|
1873
|
+
})),
|
|
1874
|
+
}));
|
|
1875
|
+
});
|
|
1716
1876
|
// ── disconnect ───────────────────────────────────────────────
|
|
1717
1877
|
server.tool('geometra_disconnect', `Disconnect from the Geometra server. Proxy-backed sessions keep compatible browsers alive by default so the next geometra_connect can reuse them quickly; pass closeBrowser=true to fully tear down the warm proxy/browser pool.`, {
|
|
1718
1878
|
closeBrowser: z.boolean().optional().default(false).describe('Fully close the spawned proxy/browser instead of keeping it warm for reuse'),
|
|
@@ -1815,6 +1975,7 @@ function packedFormSchemas(forms) {
|
|
|
1815
1975
|
...(field.checked !== undefined ? { c: field.checked ? 1 : 0 } : {}),
|
|
1816
1976
|
...(field.values && field.values.length > 0 ? { vs: field.values } : {}),
|
|
1817
1977
|
...(field.aliases ? { al: field.aliases } : {}),
|
|
1978
|
+
...(field.format ? { fmt: field.format } : {}),
|
|
1818
1979
|
...(field.context ? { x: field.context } : {}),
|
|
1819
1980
|
})),
|
|
1820
1981
|
...(form.sections ? { s: form.sections.map(s => ({ n: s.name, fi: s.fieldIds })) } : {}),
|
|
@@ -2031,6 +2192,12 @@ function collectSessionSignals(root) {
|
|
|
2031
2192
|
};
|
|
2032
2193
|
const seenAlerts = new Set();
|
|
2033
2194
|
const seenInvalidIds = new Set();
|
|
2195
|
+
const captchaPattern = /recaptcha|g-recaptcha|hcaptcha|h-captcha|turnstile|cf-turnstile|captcha/i;
|
|
2196
|
+
const captchaTypes = {
|
|
2197
|
+
recaptcha: 'recaptcha', 'g-recaptcha': 'recaptcha',
|
|
2198
|
+
hcaptcha: 'hcaptcha', 'h-captcha': 'hcaptcha',
|
|
2199
|
+
turnstile: 'turnstile', 'cf-turnstile': 'turnstile',
|
|
2200
|
+
};
|
|
2034
2201
|
function walk(node) {
|
|
2035
2202
|
if (!signals.focus && node.state?.focused) {
|
|
2036
2203
|
signals.focus = {
|
|
@@ -2063,6 +2230,14 @@ function collectSessionSignals(root) {
|
|
|
2063
2230
|
});
|
|
2064
2231
|
}
|
|
2065
2232
|
}
|
|
2233
|
+
if (!signals.captchaDetected) {
|
|
2234
|
+
const text = [node.name, node.value].filter(Boolean).join(' ');
|
|
2235
|
+
if (captchaPattern.test(text)) {
|
|
2236
|
+
signals.captchaDetected = true;
|
|
2237
|
+
const match = text.toLowerCase().match(/recaptcha|g-recaptcha|hcaptcha|h-captcha|turnstile|cf-turnstile/);
|
|
2238
|
+
signals.captchaType = match ? (captchaTypes[match[0]] ?? 'unknown') : 'unknown';
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2066
2241
|
for (const child of node.children)
|
|
2067
2242
|
walk(child);
|
|
2068
2243
|
}
|
|
@@ -2124,6 +2299,7 @@ function sessionSignalsPayload(signals, detail = 'minimal') {
|
|
|
2124
2299
|
? { scroll: { x: signals.scrollX ?? 0, y: signals.scrollY ?? 0 } }
|
|
2125
2300
|
: {}),
|
|
2126
2301
|
...(signals.focus ? { focus: signals.focus } : {}),
|
|
2302
|
+
...(signals.captchaDetected ? { captchaDetected: true, captchaType: signals.captchaType ?? 'unknown' } : {}),
|
|
2127
2303
|
dialogCount: signals.dialogCount,
|
|
2128
2304
|
busyCount: signals.busyCount,
|
|
2129
2305
|
alertCount: signals.alerts.length,
|
|
@@ -2393,11 +2569,86 @@ function coerceChoiceValue(field, value) {
|
|
|
2393
2569
|
const option = field.options?.find(option => normalizeLookupKey(option) === desired);
|
|
2394
2570
|
return option ?? (value ? 'Yes' : 'No');
|
|
2395
2571
|
}
|
|
2572
|
+
/**
|
|
2573
|
+
* Normalize a text value based on field format hints (placeholder, inputType, pattern).
|
|
2574
|
+
* Handles common date and phone format conversions.
|
|
2575
|
+
*/
|
|
2576
|
+
function normalizeFieldValue(value, format) {
|
|
2577
|
+
if (!format)
|
|
2578
|
+
return value;
|
|
2579
|
+
// Date normalization: detect ISO dates and convert to placeholder format
|
|
2580
|
+
if (format.inputType === 'date')
|
|
2581
|
+
return value; // native date inputs handle ISO fine
|
|
2582
|
+
const isDateLike = /^\d{4}-\d{2}-\d{2}$/.test(value) || /^\d{1,2}[/\-.]\d{1,2}[/\-.]\d{2,4}$/.test(value);
|
|
2583
|
+
if (isDateLike && format.placeholder) {
|
|
2584
|
+
const parsed = parseDateLoose(value);
|
|
2585
|
+
if (parsed) {
|
|
2586
|
+
const ph = format.placeholder.toLowerCase();
|
|
2587
|
+
if (ph.includes('mm/dd/yyyy') || ph.includes('mm/dd/yy')) {
|
|
2588
|
+
return `${pad2(parsed.month)}/${pad2(parsed.day)}/${parsed.year}`;
|
|
2589
|
+
}
|
|
2590
|
+
if (ph.includes('dd/mm/yyyy') || ph.includes('dd/mm/yy')) {
|
|
2591
|
+
return `${pad2(parsed.day)}/${pad2(parsed.month)}/${parsed.year}`;
|
|
2592
|
+
}
|
|
2593
|
+
if (ph.includes('yyyy-mm-dd')) {
|
|
2594
|
+
return `${parsed.year}-${pad2(parsed.month)}-${pad2(parsed.day)}`;
|
|
2595
|
+
}
|
|
2596
|
+
if (ph.includes('mm-dd-yyyy')) {
|
|
2597
|
+
return `${pad2(parsed.month)}-${pad2(parsed.day)}-${parsed.year}`;
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
// Phone normalization
|
|
2602
|
+
if (format.inputType === 'tel' || format.autocomplete?.includes('tel')) {
|
|
2603
|
+
const digits = value.replace(/\D/g, '');
|
|
2604
|
+
if (digits.length === 10 && format.placeholder) {
|
|
2605
|
+
const ph = format.placeholder;
|
|
2606
|
+
if (ph.includes('(') && ph.includes(')')) {
|
|
2607
|
+
return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`;
|
|
2608
|
+
}
|
|
2609
|
+
if (ph.includes('-') && !ph.includes('(')) {
|
|
2610
|
+
return `${digits.slice(0, 3)}-${digits.slice(3, 6)}-${digits.slice(6)}`;
|
|
2611
|
+
}
|
|
2612
|
+
if (ph.includes('.')) {
|
|
2613
|
+
return `${digits.slice(0, 3)}.${digits.slice(3, 6)}.${digits.slice(6)}`;
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
if (digits.length === 11 && digits.startsWith('1') && format.placeholder) {
|
|
2617
|
+
const core = digits.slice(1);
|
|
2618
|
+
const ph = format.placeholder;
|
|
2619
|
+
if (ph.startsWith('+1') || ph.startsWith('1')) {
|
|
2620
|
+
return `+1 (${core.slice(0, 3)}) ${core.slice(3, 6)}-${core.slice(6)}`;
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
return value;
|
|
2625
|
+
}
|
|
2626
|
+
function parseDateLoose(value) {
|
|
2627
|
+
// YYYY-MM-DD
|
|
2628
|
+
const iso = value.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
2629
|
+
if (iso)
|
|
2630
|
+
return { year: +iso[1], month: +iso[2], day: +iso[3] };
|
|
2631
|
+
// MM/DD/YYYY or MM-DD-YYYY or MM.DD.YYYY
|
|
2632
|
+
const mdy = value.match(/^(\d{1,2})[/\-.](\d{1,2})[/\-.](\d{4})$/);
|
|
2633
|
+
if (mdy)
|
|
2634
|
+
return { year: +mdy[3], month: +mdy[1], day: +mdy[2] };
|
|
2635
|
+
// MM/DD/YY
|
|
2636
|
+
const mdy2 = value.match(/^(\d{1,2})[/\-.](\d{1,2})[/\-.](\d{2})$/);
|
|
2637
|
+
if (mdy2) {
|
|
2638
|
+
const yr = +mdy2[3];
|
|
2639
|
+
return { year: yr < 50 ? 2000 + yr : 1900 + yr, month: +mdy2[1], day: +mdy2[2] };
|
|
2640
|
+
}
|
|
2641
|
+
return null;
|
|
2642
|
+
}
|
|
2643
|
+
function pad2(n) {
|
|
2644
|
+
return n < 10 ? `0${n}` : String(n);
|
|
2645
|
+
}
|
|
2396
2646
|
function plannedFillInputsForField(field, value) {
|
|
2397
2647
|
if (field.kind === 'text') {
|
|
2398
2648
|
if (typeof value !== 'string')
|
|
2399
2649
|
return { error: `Field "${field.label}" expects a string value` };
|
|
2400
|
-
|
|
2650
|
+
const normalized = normalizeFieldValue(value, field.format);
|
|
2651
|
+
return [{ kind: 'text', fieldId: field.id, fieldLabel: field.label, value: normalized }];
|
|
2401
2652
|
}
|
|
2402
2653
|
if (field.kind === 'choice') {
|
|
2403
2654
|
const coerced = coerceChoiceValue(field, value);
|
|
@@ -2442,7 +2693,7 @@ function planFormFill(schema, opts) {
|
|
|
2442
2693
|
else
|
|
2443
2694
|
fieldsByLabel.set(key, [field]);
|
|
2444
2695
|
}
|
|
2445
|
-
const
|
|
2696
|
+
const allPlanned = [];
|
|
2446
2697
|
const seenFieldIds = new Set();
|
|
2447
2698
|
for (const [fieldId, value] of Object.entries(opts.valuesById ?? {})) {
|
|
2448
2699
|
const field = fieldById.get(fieldId);
|
|
@@ -2451,7 +2702,8 @@ function planFormFill(schema, opts) {
|
|
|
2451
2702
|
const next = plannedFillInputsForField(field, value);
|
|
2452
2703
|
if ('error' in next)
|
|
2453
2704
|
return { ok: false, error: next.error };
|
|
2454
|
-
|
|
2705
|
+
for (const n of next)
|
|
2706
|
+
allPlanned.push({ field: n, confidence: 1.0, matchMethod: 'id' });
|
|
2455
2707
|
seenFieldIds.add(field.id);
|
|
2456
2708
|
}
|
|
2457
2709
|
for (const [label, value] of Object.entries(opts.valuesByLabel ?? {})) {
|
|
@@ -2468,10 +2720,14 @@ function planFormFill(schema, opts) {
|
|
|
2468
2720
|
const next = plannedFillInputsForField(field, value);
|
|
2469
2721
|
if ('error' in next)
|
|
2470
2722
|
return { ok: false, error: next.error };
|
|
2471
|
-
|
|
2723
|
+
const isExact = field.label === label;
|
|
2724
|
+
const confidence = isExact ? 0.95 : 0.8;
|
|
2725
|
+
const matchMethod = isExact ? 'label-exact' : 'label-normalized';
|
|
2726
|
+
for (const n of next)
|
|
2727
|
+
allPlanned.push({ field: n, confidence, matchMethod });
|
|
2472
2728
|
seenFieldIds.add(field.id);
|
|
2473
2729
|
}
|
|
2474
|
-
return { ok: true, fields: planned };
|
|
2730
|
+
return { ok: true, fields: allPlanned.map(p => p.field), planned: allPlanned };
|
|
2475
2731
|
}
|
|
2476
2732
|
function isResolvedFillFieldInput(field) {
|
|
2477
2733
|
if (field.kind === 'toggle')
|
|
@@ -2990,6 +3246,71 @@ async function executeBatchAction(session, action, detail, includeSteps) {
|
|
|
2990
3246
|
}
|
|
2991
3247
|
}
|
|
2992
3248
|
}
|
|
3249
|
+
function suggestRecovery(field, error) {
|
|
3250
|
+
const lowerError = error.toLowerCase();
|
|
3251
|
+
if (field.kind === 'choice') {
|
|
3252
|
+
if (lowerError.includes('no option') || lowerError.includes('not found') || lowerError.includes('visible')) {
|
|
3253
|
+
return `Try geometra_pick_listbox_option with fieldLabel="${field.fieldLabel}" and label="${field.value}" for custom dropdowns.`;
|
|
3254
|
+
}
|
|
3255
|
+
if (lowerError.includes('timeout')) {
|
|
3256
|
+
return `Dropdown may load options asynchronously. Retry with a higher timeoutMs, or use geometra_pick_listbox_option with a query parameter.`;
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
if (field.kind === 'text') {
|
|
3260
|
+
if (lowerError.includes('format') || lowerError.includes('pattern') || lowerError.includes('invalid')) {
|
|
3261
|
+
return `Field may require a specific format (e.g. MM/DD/YYYY for dates, (555) 123-4567 for phone). Check the field's placeholder or aria-describedby for format hints.`;
|
|
3262
|
+
}
|
|
3263
|
+
if (lowerError.includes('not found') || lowerError.includes('no match')) {
|
|
3264
|
+
return `Field label "${field.fieldLabel}" may have changed after page update. Refresh with geometra_form_schema and retry.`;
|
|
3265
|
+
}
|
|
3266
|
+
if (lowerError.includes('timeout')) {
|
|
3267
|
+
return `Field may be disabled or obscured. Check geometra_snapshot for the field's current state.`;
|
|
3268
|
+
}
|
|
3269
|
+
}
|
|
3270
|
+
if (field.kind === 'toggle') {
|
|
3271
|
+
if (lowerError.includes('not found') || lowerError.includes('no match')) {
|
|
3272
|
+
return `Checkbox/radio "${field.label}" not found. The label may be dynamic — try geometra_set_checked with exact=false.`;
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
if (field.kind === 'file') {
|
|
3276
|
+
if (lowerError.includes('no file') || lowerError.includes('not found')) {
|
|
3277
|
+
return `File input not found by label. Try geometra_upload_files with strategy="hidden" or provide click coordinates.`;
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
if (lowerError.includes('timeout')) {
|
|
3281
|
+
return `Action timed out. The page may still be loading. Try geometra_wait_for with a loading indicator, then retry.`;
|
|
3282
|
+
}
|
|
3283
|
+
return undefined;
|
|
3284
|
+
}
|
|
3285
|
+
function verifyFormFills(session, planned) {
|
|
3286
|
+
const a11y = sessionA11y(session);
|
|
3287
|
+
if (!a11y)
|
|
3288
|
+
return { verified: 0, mismatches: [] };
|
|
3289
|
+
const mismatches = [];
|
|
3290
|
+
let verified = 0;
|
|
3291
|
+
for (const p of planned) {
|
|
3292
|
+
if (p.field.kind === 'toggle' || p.field.kind === 'file')
|
|
3293
|
+
continue;
|
|
3294
|
+
const label = p.field.fieldLabel;
|
|
3295
|
+
const expected = p.field.kind === 'text' ? p.field.value : p.field.value;
|
|
3296
|
+
const matches = [
|
|
3297
|
+
...findNodes(a11y, { name: label, role: 'textbox' }),
|
|
3298
|
+
...findNodes(a11y, { name: label, role: 'combobox' }),
|
|
3299
|
+
];
|
|
3300
|
+
const match = matches[0];
|
|
3301
|
+
const actual = match?.value?.trim();
|
|
3302
|
+
if (!actual || !expected) {
|
|
3303
|
+
mismatches.push({ fieldLabel: label, expected, actual, ...(p.field.fieldId ? { fieldId: p.field.fieldId } : {}) });
|
|
3304
|
+
}
|
|
3305
|
+
else if (actual.toLowerCase() !== expected.toLowerCase()) {
|
|
3306
|
+
mismatches.push({ fieldLabel: label, expected, actual, ...(p.field.fieldId ? { fieldId: p.field.fieldId } : {}) });
|
|
3307
|
+
}
|
|
3308
|
+
else {
|
|
3309
|
+
verified++;
|
|
3310
|
+
}
|
|
3311
|
+
}
|
|
3312
|
+
return { verified, mismatches };
|
|
3313
|
+
}
|
|
2993
3314
|
async function executeFillField(session, field, detail) {
|
|
2994
3315
|
switch (field.kind) {
|
|
2995
3316
|
case 'text': {
|
|
@@ -3073,6 +3394,31 @@ function ok(text, screenshot) {
|
|
|
3073
3394
|
}
|
|
3074
3395
|
return { content };
|
|
3075
3396
|
}
|
|
3397
|
+
function recordWorkflowFill(session, formId, formName, valuesById, valuesByLabel, invalidCount, fieldCount) {
|
|
3398
|
+
if (!session.workflowState) {
|
|
3399
|
+
session.workflowState = { pages: [], startedAt: Date.now() };
|
|
3400
|
+
}
|
|
3401
|
+
const filledValues = {};
|
|
3402
|
+
for (const [k, v] of Object.entries(valuesById ?? {})) {
|
|
3403
|
+
if (typeof v === 'string' || typeof v === 'boolean')
|
|
3404
|
+
filledValues[k] = v;
|
|
3405
|
+
}
|
|
3406
|
+
for (const [k, v] of Object.entries(valuesByLabel ?? {})) {
|
|
3407
|
+
if (typeof v === 'string' || typeof v === 'boolean')
|
|
3408
|
+
filledValues[k] = v;
|
|
3409
|
+
}
|
|
3410
|
+
const a11y = sessionA11y(session);
|
|
3411
|
+
const pageUrl = a11y?.meta?.pageUrl ?? session.url;
|
|
3412
|
+
session.workflowState.pages.push({
|
|
3413
|
+
pageUrl,
|
|
3414
|
+
formId,
|
|
3415
|
+
formName,
|
|
3416
|
+
filledValues,
|
|
3417
|
+
filledAt: Date.now(),
|
|
3418
|
+
fieldCount,
|
|
3419
|
+
invalidCount,
|
|
3420
|
+
});
|
|
3421
|
+
}
|
|
3076
3422
|
async function captureScreenshotBase64(session) {
|
|
3077
3423
|
try {
|
|
3078
3424
|
const wait = await sendScreenshot(session);
|
package/dist/session.d.ts
CHANGED
|
@@ -29,6 +29,10 @@ export interface A11yNode {
|
|
|
29
29
|
scrollX?: number;
|
|
30
30
|
scrollY?: number;
|
|
31
31
|
controlTag?: string;
|
|
32
|
+
placeholder?: string;
|
|
33
|
+
inputPattern?: string;
|
|
34
|
+
inputType?: string;
|
|
35
|
+
autocomplete?: string;
|
|
32
36
|
};
|
|
33
37
|
bounds: {
|
|
34
38
|
x: number;
|
|
@@ -121,6 +125,11 @@ export interface PageDialogModel extends PageSectionSummaryBase {
|
|
|
121
125
|
export interface PageListModel extends PageSectionSummaryBase {
|
|
122
126
|
itemCount: number;
|
|
123
127
|
}
|
|
128
|
+
export interface CaptchaDetection {
|
|
129
|
+
detected: boolean;
|
|
130
|
+
type?: 'recaptcha' | 'hcaptcha' | 'turnstile' | 'cloudflare-challenge' | 'unknown';
|
|
131
|
+
hint?: string;
|
|
132
|
+
}
|
|
124
133
|
export interface PageModel {
|
|
125
134
|
viewport: {
|
|
126
135
|
width: number;
|
|
@@ -134,6 +143,7 @@ export interface PageModel {
|
|
|
134
143
|
listCount: number;
|
|
135
144
|
focusableCount: number;
|
|
136
145
|
};
|
|
146
|
+
captcha?: CaptchaDetection;
|
|
137
147
|
primaryActions: PagePrimaryAction[];
|
|
138
148
|
landmarks: PageLandmark[];
|
|
139
149
|
forms: PageFormModel[];
|
|
@@ -264,6 +274,12 @@ export interface FormSchemaField {
|
|
|
264
274
|
optionCount?: number;
|
|
265
275
|
options?: string[];
|
|
266
276
|
aliases?: Record<string, string[]>;
|
|
277
|
+
format?: {
|
|
278
|
+
placeholder?: string;
|
|
279
|
+
pattern?: string;
|
|
280
|
+
inputType?: string;
|
|
281
|
+
autocomplete?: string;
|
|
282
|
+
};
|
|
267
283
|
context?: NodeContextModel;
|
|
268
284
|
}
|
|
269
285
|
export interface FormSchemaSection {
|
|
@@ -343,6 +359,19 @@ export interface UiDelta {
|
|
|
343
359
|
viewport?: UiViewportChange;
|
|
344
360
|
focus?: UiFocusChange;
|
|
345
361
|
}
|
|
362
|
+
export interface WorkflowPageEntry {
|
|
363
|
+
pageUrl: string;
|
|
364
|
+
formId?: string;
|
|
365
|
+
formName?: string;
|
|
366
|
+
filledValues: Record<string, string | boolean>;
|
|
367
|
+
filledAt: number;
|
|
368
|
+
fieldCount: number;
|
|
369
|
+
invalidCount: number;
|
|
370
|
+
}
|
|
371
|
+
export interface WorkflowState {
|
|
372
|
+
pages: WorkflowPageEntry[];
|
|
373
|
+
startedAt: number;
|
|
374
|
+
}
|
|
346
375
|
export interface Session {
|
|
347
376
|
ws: WebSocket;
|
|
348
377
|
layout: Record<string, unknown> | null;
|
|
@@ -360,6 +389,7 @@ export interface Session {
|
|
|
360
389
|
revision: number;
|
|
361
390
|
forms: FormSchemaModel[];
|
|
362
391
|
}>;
|
|
392
|
+
workflowState?: WorkflowState;
|
|
363
393
|
}
|
|
364
394
|
export interface SessionConnectTrace {
|
|
365
395
|
mode: 'direct-ws' | 'fresh-proxy' | 'reused-proxy';
|
|
@@ -563,10 +593,6 @@ export declare function buildCompactUiIndex(root: A11yNode, options?: {
|
|
|
563
593
|
};
|
|
564
594
|
export declare function summarizeCompactIndex(nodes: CompactUiNode[], maxLines?: number): string;
|
|
565
595
|
export declare function nodeContextForNode(root: A11yNode, node: A11yNode): NodeContextModel | undefined;
|
|
566
|
-
/**
|
|
567
|
-
* Build a summary-first, stable-ID webpage model from the accessibility tree.
|
|
568
|
-
* Use {@link expandPageSection} to fetch details for a specific section on demand.
|
|
569
|
-
*/
|
|
570
596
|
export declare function buildPageModel(root: A11yNode, options?: {
|
|
571
597
|
maxPrimaryActions?: number;
|
|
572
598
|
maxSectionsPerKind?: number;
|
package/dist/session.js
CHANGED
|
@@ -1597,6 +1597,21 @@ function computeOptionAliases(options) {
|
|
|
1597
1597
|
}
|
|
1598
1598
|
return Object.keys(result).length > 0 ? result : undefined;
|
|
1599
1599
|
}
|
|
1600
|
+
function buildFieldFormat(node) {
|
|
1601
|
+
const m = node.meta;
|
|
1602
|
+
if (!m)
|
|
1603
|
+
return undefined;
|
|
1604
|
+
const format = {};
|
|
1605
|
+
if (m.placeholder)
|
|
1606
|
+
format.placeholder = m.placeholder;
|
|
1607
|
+
if (m.inputPattern)
|
|
1608
|
+
format.pattern = m.inputPattern;
|
|
1609
|
+
if (m.inputType)
|
|
1610
|
+
format.inputType = m.inputType;
|
|
1611
|
+
if (m.autocomplete)
|
|
1612
|
+
format.autocomplete = m.autocomplete;
|
|
1613
|
+
return Object.keys(format).length > 0 ? format : undefined;
|
|
1614
|
+
}
|
|
1600
1615
|
function simpleSchemaField(root, node) {
|
|
1601
1616
|
const context = nodeContextForNode(root, node);
|
|
1602
1617
|
const label = fieldLabel(node) ?? sanitizeInlineName(node.name, 80) ?? context?.prompt;
|
|
@@ -1607,6 +1622,7 @@ function simpleSchemaField(root, node) {
|
|
|
1607
1622
|
? 'select'
|
|
1608
1623
|
: 'listbox'
|
|
1609
1624
|
: undefined;
|
|
1625
|
+
const format = buildFieldFormat(node);
|
|
1610
1626
|
return {
|
|
1611
1627
|
id: formFieldIdForPath(node.path),
|
|
1612
1628
|
kind: node.role === 'combobox' ? 'choice' : 'text',
|
|
@@ -1615,6 +1631,7 @@ function simpleSchemaField(root, node) {
|
|
|
1615
1631
|
...(node.state?.required ? { required: true } : {}),
|
|
1616
1632
|
...(node.state?.invalid ? { invalid: true } : {}),
|
|
1617
1633
|
...compactSchemaValue(node.value, 72),
|
|
1634
|
+
...(format ? { format } : {}),
|
|
1618
1635
|
...(compactSchemaContext(context, label) ? { context: compactSchemaContext(context, label) } : {}),
|
|
1619
1636
|
};
|
|
1620
1637
|
}
|
|
@@ -1845,6 +1862,47 @@ function inferPageArchetypes(model) {
|
|
|
1845
1862
|
* Build a summary-first, stable-ID webpage model from the accessibility tree.
|
|
1846
1863
|
* Use {@link expandPageSection} to fetch details for a specific section on demand.
|
|
1847
1864
|
*/
|
|
1865
|
+
const CAPTCHA_PATTERNS = [
|
|
1866
|
+
{ pattern: /recaptcha|g-recaptcha/i, type: 'recaptcha', hint: 'Google reCAPTCHA detected' },
|
|
1867
|
+
{ pattern: /hcaptcha|h-captcha/i, type: 'hcaptcha', hint: 'hCaptcha detected' },
|
|
1868
|
+
{ pattern: /turnstile|cf-turnstile/i, type: 'turnstile', hint: 'Cloudflare Turnstile detected' },
|
|
1869
|
+
{ pattern: /cloudflare.*challenge|challenge-platform|just a moment/i, type: 'cloudflare-challenge', hint: 'Cloudflare challenge page detected' },
|
|
1870
|
+
{ pattern: /captcha/i, type: 'unknown', hint: 'CAPTCHA element detected' },
|
|
1871
|
+
];
|
|
1872
|
+
function detectCaptcha(root) {
|
|
1873
|
+
let found;
|
|
1874
|
+
function walk(node) {
|
|
1875
|
+
if (found)
|
|
1876
|
+
return;
|
|
1877
|
+
const text = [node.name, node.value, node.role].filter(Boolean).join(' ');
|
|
1878
|
+
for (const { pattern, type, hint } of CAPTCHA_PATTERNS) {
|
|
1879
|
+
if (pattern.test(text)) {
|
|
1880
|
+
found = { detected: true, type, hint };
|
|
1881
|
+
return;
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
// Check iframe placeholders (common for reCAPTCHA/hCaptcha/Turnstile)
|
|
1885
|
+
if (node.meta && typeof node.meta.frameUrl === 'string') {
|
|
1886
|
+
const frameUrl = node.meta.frameUrl;
|
|
1887
|
+
for (const { pattern, type, hint } of CAPTCHA_PATTERNS) {
|
|
1888
|
+
if (pattern.test(frameUrl)) {
|
|
1889
|
+
found = { detected: true, type, hint };
|
|
1890
|
+
return;
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
for (const child of node.children)
|
|
1895
|
+
walk(child);
|
|
1896
|
+
}
|
|
1897
|
+
walk(root);
|
|
1898
|
+
// Also check the page URL for Cloudflare challenge pages
|
|
1899
|
+
if (!found && root.meta?.pageUrl) {
|
|
1900
|
+
if (/challenge|cdn-cgi.*challenge/i.test(root.meta.pageUrl)) {
|
|
1901
|
+
found = { detected: true, type: 'cloudflare-challenge', hint: 'Cloudflare challenge page URL detected' };
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
return found ?? { detected: false };
|
|
1905
|
+
}
|
|
1848
1906
|
export function buildPageModel(root, options) {
|
|
1849
1907
|
const maxPrimaryActions = options?.maxPrimaryActions ?? 6;
|
|
1850
1908
|
const maxSectionsPerKind = options?.maxSectionsPerKind ?? 8;
|
|
@@ -1928,8 +1986,10 @@ export function buildPageModel(root, options) {
|
|
|
1928
1986
|
dialogs: sortByBounds(dialogs).slice(0, maxSectionsPerKind),
|
|
1929
1987
|
lists: sortByBounds(lists).slice(0, maxSectionsPerKind),
|
|
1930
1988
|
};
|
|
1989
|
+
const captcha = detectCaptcha(root);
|
|
1931
1990
|
return {
|
|
1932
1991
|
...baseModel,
|
|
1992
|
+
...(captcha.detected ? { captcha } : {}),
|
|
1933
1993
|
archetypes: inferPageArchetypes(baseModel),
|
|
1934
1994
|
};
|
|
1935
1995
|
}
|
|
@@ -2430,6 +2490,14 @@ function walkNode(element, layout, path) {
|
|
|
2430
2490
|
meta.scrollY = semantic.scrollY;
|
|
2431
2491
|
if (typeof semantic?.tag === 'string' && semantic.tag.trim().length > 0)
|
|
2432
2492
|
meta.controlTag = semantic.tag;
|
|
2493
|
+
if (typeof semantic?.placeholder === 'string')
|
|
2494
|
+
meta.placeholder = semantic.placeholder;
|
|
2495
|
+
if (typeof semantic?.inputPattern === 'string')
|
|
2496
|
+
meta.inputPattern = semantic.inputPattern;
|
|
2497
|
+
if (typeof semantic?.inputType === 'string')
|
|
2498
|
+
meta.inputType = semantic.inputType;
|
|
2499
|
+
if (typeof semantic?.autocomplete === 'string')
|
|
2500
|
+
meta.autocomplete = semantic.autocomplete;
|
|
2433
2501
|
const children = [];
|
|
2434
2502
|
const elementChildren = element.children;
|
|
2435
2503
|
const layoutChildren = layout.children;
|
package/package.json
CHANGED