@geometra/mcp 1.20.0 → 1.21.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 +102 -7
- package/dist/session.d.ts +20 -4
- package/dist/session.js +43 -0
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -761,6 +761,7 @@ Pass \`valuesById\` with field ids from \`geometra_form_schema\` for the most st
|
|
|
761
761
|
const wait = await sendFillFields(session, directFields);
|
|
762
762
|
const ackResult = parseProxyFillAckResult(wait.result);
|
|
763
763
|
if (ackResult && ackResult.invalidCount === 0) {
|
|
764
|
+
recordWorkflowFill(session, undefined, undefined, valuesById, valuesByLabel, 0, directFields.length);
|
|
764
765
|
return ok(JSON.stringify({
|
|
765
766
|
...connection,
|
|
766
767
|
completed: true,
|
|
@@ -777,6 +778,7 @@ Pass \`valuesById\` with field ids from \`geometra_form_schema\` for the most st
|
|
|
777
778
|
const afterDirect = sessionA11y(session);
|
|
778
779
|
const directSignals = afterDirect ? collectSessionSignals(afterDirect) : undefined;
|
|
779
780
|
if (directSignals && directSignals.invalidFields.length === 0) {
|
|
781
|
+
recordWorkflowFill(session, undefined, undefined, valuesById, valuesByLabel, 0, directFields.length);
|
|
780
782
|
return ok(JSON.stringify({
|
|
781
783
|
...connection,
|
|
782
784
|
completed: true,
|
|
@@ -872,8 +874,12 @@ Pass \`valuesById\` with field ids from \`geometra_form_schema\` for the most st
|
|
|
872
874
|
fieldCount: planned.fields.length,
|
|
873
875
|
successCount: planned.fields.length,
|
|
874
876
|
errorCount: 0,
|
|
877
|
+
minConfidence: planned.planned.length > 0
|
|
878
|
+
? Number(Math.min(...planned.planned.map(p => p.confidence)).toFixed(2))
|
|
879
|
+
: undefined,
|
|
875
880
|
...(signals ? { final: sessionSignalsPayload(signals, detail) } : {}),
|
|
876
881
|
};
|
|
882
|
+
recordWorkflowFill(session, schema.formId, schema.name, valuesById, valuesByLabel, invalidRemaining, planned.fields.length);
|
|
877
883
|
if (failOnInvalid && invalidRemaining > 0) {
|
|
878
884
|
return err(JSON.stringify(payload, null, detail === 'verbose' ? 2 : undefined));
|
|
879
885
|
}
|
|
@@ -885,15 +891,18 @@ Pass \`valuesById\` with field ids from \`geometra_form_schema\` for the most st
|
|
|
885
891
|
const startIndex = resumeFromIndex ?? 0;
|
|
886
892
|
for (let index = startIndex; index < planned.fields.length; index++) {
|
|
887
893
|
const field = planned.fields[index];
|
|
894
|
+
const plan = planned.planned[index];
|
|
895
|
+
const confidence = plan?.confidence;
|
|
896
|
+
const matchMethod = plan?.matchMethod;
|
|
888
897
|
try {
|
|
889
898
|
const result = await executeFillField(session, field, detail);
|
|
890
899
|
steps.push(detail === 'verbose'
|
|
891
|
-
? { index, kind: field.kind, ok: true, summary: result.summary }
|
|
892
|
-
: { index, kind: field.kind, ok: true, ...result.compact });
|
|
900
|
+
? { index, kind: field.kind, ok: true, ...(confidence !== undefined ? { confidence, matchMethod } : {}), summary: result.summary }
|
|
901
|
+
: { index, kind: field.kind, ok: true, ...(confidence !== undefined ? { confidence, matchMethod } : {}), ...result.compact });
|
|
893
902
|
}
|
|
894
903
|
catch (e) {
|
|
895
904
|
const message = e instanceof Error ? e.message : String(e);
|
|
896
|
-
steps.push({ index, kind: field.kind, ok: false, error: message });
|
|
905
|
+
steps.push({ index, kind: field.kind, ok: false, ...(confidence !== undefined ? { confidence, matchMethod } : {}), error: message });
|
|
897
906
|
if (stopOnError) {
|
|
898
907
|
stoppedAt = index;
|
|
899
908
|
break;
|
|
@@ -914,11 +923,15 @@ Pass \`valuesById\` with field ids from \`geometra_form_schema\` for the most st
|
|
|
914
923
|
fieldCount: planned.fields.length,
|
|
915
924
|
successCount,
|
|
916
925
|
errorCount,
|
|
926
|
+
minConfidence: planned.planned.length > 0
|
|
927
|
+
? Number(Math.min(...planned.planned.map(p => p.confidence)).toFixed(2))
|
|
928
|
+
: undefined,
|
|
917
929
|
...(startIndex > 0 ? { resumedFromIndex: startIndex } : {}),
|
|
918
930
|
...(includeSteps ? { steps } : {}),
|
|
919
931
|
...(stoppedAt !== undefined ? { stoppedAt, resumeFromIndex: stoppedAt + 1 } : {}),
|
|
920
932
|
...(signals ? { final: sessionSignalsPayload(signals, detail) } : {}),
|
|
921
933
|
};
|
|
934
|
+
recordWorkflowFill(session, schema.formId, schema.name, valuesById, valuesByLabel, invalidRemaining, planned.fields.length);
|
|
922
935
|
if (failOnInvalid && invalidRemaining > 0) {
|
|
923
936
|
return err(JSON.stringify(payload, null, detail === 'verbose' ? 2 : undefined));
|
|
924
937
|
}
|
|
@@ -1713,6 +1726,43 @@ For a token-efficient semantic view, use geometra_snapshot (default compact). Fo
|
|
|
1713
1726
|
return err('Not connected. Call geometra_connect first.');
|
|
1714
1727
|
return ok(JSON.stringify(session.layout, null, 2));
|
|
1715
1728
|
});
|
|
1729
|
+
// ── workflow state ───────────────────────────────────────────
|
|
1730
|
+
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.
|
|
1731
|
+
|
|
1732
|
+
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.`, {
|
|
1733
|
+
clear: z.boolean().optional().default(false).describe('Reset the workflow state'),
|
|
1734
|
+
}, async ({ clear }) => {
|
|
1735
|
+
const session = getSession();
|
|
1736
|
+
if (!session)
|
|
1737
|
+
return err('Not connected. Call geometra_connect first.');
|
|
1738
|
+
if (clear) {
|
|
1739
|
+
session.workflowState = undefined;
|
|
1740
|
+
return ok(JSON.stringify({ cleared: true }));
|
|
1741
|
+
}
|
|
1742
|
+
if (!session.workflowState || session.workflowState.pages.length === 0) {
|
|
1743
|
+
return ok(JSON.stringify({
|
|
1744
|
+
pageCount: 0,
|
|
1745
|
+
message: 'No workflow state recorded yet. Fill a form with geometra_fill_form to start tracking.',
|
|
1746
|
+
}));
|
|
1747
|
+
}
|
|
1748
|
+
const state = session.workflowState;
|
|
1749
|
+
const totalFields = state.pages.reduce((sum, p) => sum + p.fieldCount, 0);
|
|
1750
|
+
const totalInvalid = state.pages.reduce((sum, p) => sum + p.invalidCount, 0);
|
|
1751
|
+
return ok(JSON.stringify({
|
|
1752
|
+
pageCount: state.pages.length,
|
|
1753
|
+
totalFieldsFilled: totalFields,
|
|
1754
|
+
totalInvalidRemaining: totalInvalid,
|
|
1755
|
+
elapsedMs: Date.now() - state.startedAt,
|
|
1756
|
+
pages: state.pages.map(p => ({
|
|
1757
|
+
pageUrl: p.pageUrl,
|
|
1758
|
+
...(p.formId ? { formId: p.formId } : {}),
|
|
1759
|
+
...(p.formName ? { formName: p.formName } : {}),
|
|
1760
|
+
fieldCount: p.fieldCount,
|
|
1761
|
+
invalidCount: p.invalidCount,
|
|
1762
|
+
filledValues: p.filledValues,
|
|
1763
|
+
})),
|
|
1764
|
+
}));
|
|
1765
|
+
});
|
|
1716
1766
|
// ── disconnect ───────────────────────────────────────────────
|
|
1717
1767
|
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
1768
|
closeBrowser: z.boolean().optional().default(false).describe('Fully close the spawned proxy/browser instead of keeping it warm for reuse'),
|
|
@@ -2031,6 +2081,12 @@ function collectSessionSignals(root) {
|
|
|
2031
2081
|
};
|
|
2032
2082
|
const seenAlerts = new Set();
|
|
2033
2083
|
const seenInvalidIds = new Set();
|
|
2084
|
+
const captchaPattern = /recaptcha|g-recaptcha|hcaptcha|h-captcha|turnstile|cf-turnstile|captcha/i;
|
|
2085
|
+
const captchaTypes = {
|
|
2086
|
+
recaptcha: 'recaptcha', 'g-recaptcha': 'recaptcha',
|
|
2087
|
+
hcaptcha: 'hcaptcha', 'h-captcha': 'hcaptcha',
|
|
2088
|
+
turnstile: 'turnstile', 'cf-turnstile': 'turnstile',
|
|
2089
|
+
};
|
|
2034
2090
|
function walk(node) {
|
|
2035
2091
|
if (!signals.focus && node.state?.focused) {
|
|
2036
2092
|
signals.focus = {
|
|
@@ -2063,6 +2119,14 @@ function collectSessionSignals(root) {
|
|
|
2063
2119
|
});
|
|
2064
2120
|
}
|
|
2065
2121
|
}
|
|
2122
|
+
if (!signals.captchaDetected) {
|
|
2123
|
+
const text = [node.name, node.value].filter(Boolean).join(' ');
|
|
2124
|
+
if (captchaPattern.test(text)) {
|
|
2125
|
+
signals.captchaDetected = true;
|
|
2126
|
+
const match = text.toLowerCase().match(/recaptcha|g-recaptcha|hcaptcha|h-captcha|turnstile|cf-turnstile/);
|
|
2127
|
+
signals.captchaType = match ? (captchaTypes[match[0]] ?? 'unknown') : 'unknown';
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2066
2130
|
for (const child of node.children)
|
|
2067
2131
|
walk(child);
|
|
2068
2132
|
}
|
|
@@ -2124,6 +2188,7 @@ function sessionSignalsPayload(signals, detail = 'minimal') {
|
|
|
2124
2188
|
? { scroll: { x: signals.scrollX ?? 0, y: signals.scrollY ?? 0 } }
|
|
2125
2189
|
: {}),
|
|
2126
2190
|
...(signals.focus ? { focus: signals.focus } : {}),
|
|
2191
|
+
...(signals.captchaDetected ? { captchaDetected: true, captchaType: signals.captchaType ?? 'unknown' } : {}),
|
|
2127
2192
|
dialogCount: signals.dialogCount,
|
|
2128
2193
|
busyCount: signals.busyCount,
|
|
2129
2194
|
alertCount: signals.alerts.length,
|
|
@@ -2442,7 +2507,7 @@ function planFormFill(schema, opts) {
|
|
|
2442
2507
|
else
|
|
2443
2508
|
fieldsByLabel.set(key, [field]);
|
|
2444
2509
|
}
|
|
2445
|
-
const
|
|
2510
|
+
const allPlanned = [];
|
|
2446
2511
|
const seenFieldIds = new Set();
|
|
2447
2512
|
for (const [fieldId, value] of Object.entries(opts.valuesById ?? {})) {
|
|
2448
2513
|
const field = fieldById.get(fieldId);
|
|
@@ -2451,7 +2516,8 @@ function planFormFill(schema, opts) {
|
|
|
2451
2516
|
const next = plannedFillInputsForField(field, value);
|
|
2452
2517
|
if ('error' in next)
|
|
2453
2518
|
return { ok: false, error: next.error };
|
|
2454
|
-
|
|
2519
|
+
for (const n of next)
|
|
2520
|
+
allPlanned.push({ field: n, confidence: 1.0, matchMethod: 'id' });
|
|
2455
2521
|
seenFieldIds.add(field.id);
|
|
2456
2522
|
}
|
|
2457
2523
|
for (const [label, value] of Object.entries(opts.valuesByLabel ?? {})) {
|
|
@@ -2468,10 +2534,14 @@ function planFormFill(schema, opts) {
|
|
|
2468
2534
|
const next = plannedFillInputsForField(field, value);
|
|
2469
2535
|
if ('error' in next)
|
|
2470
2536
|
return { ok: false, error: next.error };
|
|
2471
|
-
|
|
2537
|
+
const isExact = field.label === label;
|
|
2538
|
+
const confidence = isExact ? 0.95 : 0.8;
|
|
2539
|
+
const matchMethod = isExact ? 'label-exact' : 'label-normalized';
|
|
2540
|
+
for (const n of next)
|
|
2541
|
+
allPlanned.push({ field: n, confidence, matchMethod });
|
|
2472
2542
|
seenFieldIds.add(field.id);
|
|
2473
2543
|
}
|
|
2474
|
-
return { ok: true, fields: planned };
|
|
2544
|
+
return { ok: true, fields: allPlanned.map(p => p.field), planned: allPlanned };
|
|
2475
2545
|
}
|
|
2476
2546
|
function isResolvedFillFieldInput(field) {
|
|
2477
2547
|
if (field.kind === 'toggle')
|
|
@@ -3073,6 +3143,31 @@ function ok(text, screenshot) {
|
|
|
3073
3143
|
}
|
|
3074
3144
|
return { content };
|
|
3075
3145
|
}
|
|
3146
|
+
function recordWorkflowFill(session, formId, formName, valuesById, valuesByLabel, invalidCount, fieldCount) {
|
|
3147
|
+
if (!session.workflowState) {
|
|
3148
|
+
session.workflowState = { pages: [], startedAt: Date.now() };
|
|
3149
|
+
}
|
|
3150
|
+
const filledValues = {};
|
|
3151
|
+
for (const [k, v] of Object.entries(valuesById ?? {})) {
|
|
3152
|
+
if (typeof v === 'string' || typeof v === 'boolean')
|
|
3153
|
+
filledValues[k] = v;
|
|
3154
|
+
}
|
|
3155
|
+
for (const [k, v] of Object.entries(valuesByLabel ?? {})) {
|
|
3156
|
+
if (typeof v === 'string' || typeof v === 'boolean')
|
|
3157
|
+
filledValues[k] = v;
|
|
3158
|
+
}
|
|
3159
|
+
const a11y = sessionA11y(session);
|
|
3160
|
+
const pageUrl = a11y?.meta?.pageUrl ?? session.url;
|
|
3161
|
+
session.workflowState.pages.push({
|
|
3162
|
+
pageUrl,
|
|
3163
|
+
formId,
|
|
3164
|
+
formName,
|
|
3165
|
+
filledValues,
|
|
3166
|
+
filledAt: Date.now(),
|
|
3167
|
+
fieldCount,
|
|
3168
|
+
invalidCount,
|
|
3169
|
+
});
|
|
3170
|
+
}
|
|
3076
3171
|
async function captureScreenshotBase64(session) {
|
|
3077
3172
|
try {
|
|
3078
3173
|
const wait = await sendScreenshot(session);
|
package/dist/session.d.ts
CHANGED
|
@@ -121,6 +121,11 @@ export interface PageDialogModel extends PageSectionSummaryBase {
|
|
|
121
121
|
export interface PageListModel extends PageSectionSummaryBase {
|
|
122
122
|
itemCount: number;
|
|
123
123
|
}
|
|
124
|
+
export interface CaptchaDetection {
|
|
125
|
+
detected: boolean;
|
|
126
|
+
type?: 'recaptcha' | 'hcaptcha' | 'turnstile' | 'cloudflare-challenge' | 'unknown';
|
|
127
|
+
hint?: string;
|
|
128
|
+
}
|
|
124
129
|
export interface PageModel {
|
|
125
130
|
viewport: {
|
|
126
131
|
width: number;
|
|
@@ -134,6 +139,7 @@ export interface PageModel {
|
|
|
134
139
|
listCount: number;
|
|
135
140
|
focusableCount: number;
|
|
136
141
|
};
|
|
142
|
+
captcha?: CaptchaDetection;
|
|
137
143
|
primaryActions: PagePrimaryAction[];
|
|
138
144
|
landmarks: PageLandmark[];
|
|
139
145
|
forms: PageFormModel[];
|
|
@@ -343,6 +349,19 @@ export interface UiDelta {
|
|
|
343
349
|
viewport?: UiViewportChange;
|
|
344
350
|
focus?: UiFocusChange;
|
|
345
351
|
}
|
|
352
|
+
export interface WorkflowPageEntry {
|
|
353
|
+
pageUrl: string;
|
|
354
|
+
formId?: string;
|
|
355
|
+
formName?: string;
|
|
356
|
+
filledValues: Record<string, string | boolean>;
|
|
357
|
+
filledAt: number;
|
|
358
|
+
fieldCount: number;
|
|
359
|
+
invalidCount: number;
|
|
360
|
+
}
|
|
361
|
+
export interface WorkflowState {
|
|
362
|
+
pages: WorkflowPageEntry[];
|
|
363
|
+
startedAt: number;
|
|
364
|
+
}
|
|
346
365
|
export interface Session {
|
|
347
366
|
ws: WebSocket;
|
|
348
367
|
layout: Record<string, unknown> | null;
|
|
@@ -360,6 +379,7 @@ export interface Session {
|
|
|
360
379
|
revision: number;
|
|
361
380
|
forms: FormSchemaModel[];
|
|
362
381
|
}>;
|
|
382
|
+
workflowState?: WorkflowState;
|
|
363
383
|
}
|
|
364
384
|
export interface SessionConnectTrace {
|
|
365
385
|
mode: 'direct-ws' | 'fresh-proxy' | 'reused-proxy';
|
|
@@ -563,10 +583,6 @@ export declare function buildCompactUiIndex(root: A11yNode, options?: {
|
|
|
563
583
|
};
|
|
564
584
|
export declare function summarizeCompactIndex(nodes: CompactUiNode[], maxLines?: number): string;
|
|
565
585
|
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
586
|
export declare function buildPageModel(root: A11yNode, options?: {
|
|
571
587
|
maxPrimaryActions?: number;
|
|
572
588
|
maxSectionsPerKind?: number;
|
package/dist/session.js
CHANGED
|
@@ -1845,6 +1845,47 @@ function inferPageArchetypes(model) {
|
|
|
1845
1845
|
* Build a summary-first, stable-ID webpage model from the accessibility tree.
|
|
1846
1846
|
* Use {@link expandPageSection} to fetch details for a specific section on demand.
|
|
1847
1847
|
*/
|
|
1848
|
+
const CAPTCHA_PATTERNS = [
|
|
1849
|
+
{ pattern: /recaptcha|g-recaptcha/i, type: 'recaptcha', hint: 'Google reCAPTCHA detected' },
|
|
1850
|
+
{ pattern: /hcaptcha|h-captcha/i, type: 'hcaptcha', hint: 'hCaptcha detected' },
|
|
1851
|
+
{ pattern: /turnstile|cf-turnstile/i, type: 'turnstile', hint: 'Cloudflare Turnstile detected' },
|
|
1852
|
+
{ pattern: /cloudflare.*challenge|challenge-platform|just a moment/i, type: 'cloudflare-challenge', hint: 'Cloudflare challenge page detected' },
|
|
1853
|
+
{ pattern: /captcha/i, type: 'unknown', hint: 'CAPTCHA element detected' },
|
|
1854
|
+
];
|
|
1855
|
+
function detectCaptcha(root) {
|
|
1856
|
+
let found;
|
|
1857
|
+
function walk(node) {
|
|
1858
|
+
if (found)
|
|
1859
|
+
return;
|
|
1860
|
+
const text = [node.name, node.value, node.role].filter(Boolean).join(' ');
|
|
1861
|
+
for (const { pattern, type, hint } of CAPTCHA_PATTERNS) {
|
|
1862
|
+
if (pattern.test(text)) {
|
|
1863
|
+
found = { detected: true, type, hint };
|
|
1864
|
+
return;
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
// Check iframe placeholders (common for reCAPTCHA/hCaptcha/Turnstile)
|
|
1868
|
+
if (node.meta && typeof node.meta.frameUrl === 'string') {
|
|
1869
|
+
const frameUrl = node.meta.frameUrl;
|
|
1870
|
+
for (const { pattern, type, hint } of CAPTCHA_PATTERNS) {
|
|
1871
|
+
if (pattern.test(frameUrl)) {
|
|
1872
|
+
found = { detected: true, type, hint };
|
|
1873
|
+
return;
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
for (const child of node.children)
|
|
1878
|
+
walk(child);
|
|
1879
|
+
}
|
|
1880
|
+
walk(root);
|
|
1881
|
+
// Also check the page URL for Cloudflare challenge pages
|
|
1882
|
+
if (!found && root.meta?.pageUrl) {
|
|
1883
|
+
if (/challenge|cdn-cgi.*challenge/i.test(root.meta.pageUrl)) {
|
|
1884
|
+
found = { detected: true, type: 'cloudflare-challenge', hint: 'Cloudflare challenge page URL detected' };
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
return found ?? { detected: false };
|
|
1888
|
+
}
|
|
1848
1889
|
export function buildPageModel(root, options) {
|
|
1849
1890
|
const maxPrimaryActions = options?.maxPrimaryActions ?? 6;
|
|
1850
1891
|
const maxSectionsPerKind = options?.maxSectionsPerKind ?? 8;
|
|
@@ -1928,8 +1969,10 @@ export function buildPageModel(root, options) {
|
|
|
1928
1969
|
dialogs: sortByBounds(dialogs).slice(0, maxSectionsPerKind),
|
|
1929
1970
|
lists: sortByBounds(lists).slice(0, maxSectionsPerKind),
|
|
1930
1971
|
};
|
|
1972
|
+
const captcha = detectCaptcha(root);
|
|
1931
1973
|
return {
|
|
1932
1974
|
...baseModel,
|
|
1975
|
+
...(captcha.detected ? { captcha } : {}),
|
|
1933
1976
|
archetypes: inferPageArchetypes(baseModel),
|
|
1934
1977
|
};
|
|
1935
1978
|
}
|
package/package.json
CHANGED