@geometra/mcp 1.43.0 → 1.45.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 +57 -1
- package/dist/session.js +48 -7
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -3546,6 +3546,62 @@ function suggestRecovery(field, error) {
|
|
|
3546
3546
|
}
|
|
3547
3547
|
return undefined;
|
|
3548
3548
|
}
|
|
3549
|
+
// Normalize a value for verifyFills comparison so caller-friendly inputs
|
|
3550
|
+
// match site-formatted readbacks. The legacy comparison was strict lowercase
|
|
3551
|
+
// only — which broke every form that auto-formats phone numbers, dates, or
|
|
3552
|
+
// currency fields. Greenhouse turns "+1-929-608-1737" into "(929) 608-1737";
|
|
3553
|
+
// Workday turns "$160000" into "$160,000.00"; Lever turns "2026-01-01" into
|
|
3554
|
+
// "01/01/2026". The lowercase comparator flagged all of these as mismatches
|
|
3555
|
+
// even though the field state was correct, forcing every caller to either
|
|
3556
|
+
// disable verifyFills or wrap it in defensive try/catch.
|
|
3557
|
+
//
|
|
3558
|
+
// The fix: detect phone-like and number-like values and compare on the
|
|
3559
|
+
// canonical digit sequence. Plain text and short strings still go through
|
|
3560
|
+
// the strict lowercase comparator so unrelated content can't accidentally
|
|
3561
|
+
// match (e.g. "1234 Main St" vs "12-34 Main").
|
|
3562
|
+
function looksLikePhoneNumber(value) {
|
|
3563
|
+
// Heuristic: at least 7 digits, and after stripping whitespace + the
|
|
3564
|
+
// common phone separator characters (+ - . ( ) space ext) only digits
|
|
3565
|
+
// and a single optional leading + remain. Catches international and
|
|
3566
|
+
// domestic formats without false-matching addresses or IDs.
|
|
3567
|
+
const stripped = value.replace(/[\s().\-+x]|ext\.?/gi, '');
|
|
3568
|
+
if (stripped.length < 7)
|
|
3569
|
+
return false;
|
|
3570
|
+
if (!/^\d+$/.test(stripped))
|
|
3571
|
+
return false;
|
|
3572
|
+
return /\d.*[\s().\-+]|^\+\d/.test(value) || /^\d{7,}$/.test(value);
|
|
3573
|
+
}
|
|
3574
|
+
function looksLikeFormattedNumber(value) {
|
|
3575
|
+
// Catches "$160,000.00" / "1,000,000" / "1.5e6" style readbacks where the
|
|
3576
|
+
// site adds thousands separators or currency prefixes. Requires at least
|
|
3577
|
+
// one comma OR currency prefix to avoid matching plain text.
|
|
3578
|
+
if (!/[$€£¥,]/.test(value))
|
|
3579
|
+
return false;
|
|
3580
|
+
return /\d/.test(value);
|
|
3581
|
+
}
|
|
3582
|
+
function digitSignature(value) {
|
|
3583
|
+
return value.replace(/\D+/g, '');
|
|
3584
|
+
}
|
|
3585
|
+
function valuesEquivalent(expected, actual) {
|
|
3586
|
+
if (expected.toLowerCase() === actual.toLowerCase())
|
|
3587
|
+
return true;
|
|
3588
|
+
// Both look like phones → compare digit signatures only
|
|
3589
|
+
if (looksLikePhoneNumber(expected) && looksLikePhoneNumber(actual)) {
|
|
3590
|
+
return digitSignature(expected) === digitSignature(actual);
|
|
3591
|
+
}
|
|
3592
|
+
// Both look like formatted numbers → compare digit signatures only
|
|
3593
|
+
if (looksLikeFormattedNumber(expected) || looksLikeFormattedNumber(actual)) {
|
|
3594
|
+
const eDigits = digitSignature(expected);
|
|
3595
|
+
const aDigits = digitSignature(actual);
|
|
3596
|
+
if (eDigits && aDigits && eDigits === aDigits)
|
|
3597
|
+
return true;
|
|
3598
|
+
}
|
|
3599
|
+
// Whitespace normalization for everything else (handles "Charlie Greenman"
|
|
3600
|
+
// vs "Charlie Greenman" auto-collapsed by ATS forms)
|
|
3601
|
+
const eNorm = expected.replace(/\s+/g, ' ').trim().toLowerCase();
|
|
3602
|
+
const aNorm = actual.replace(/\s+/g, ' ').trim().toLowerCase();
|
|
3603
|
+
return eNorm === aNorm;
|
|
3604
|
+
}
|
|
3549
3605
|
function verifyFormFills(session, planned) {
|
|
3550
3606
|
const a11y = sessionA11y(session);
|
|
3551
3607
|
if (!a11y)
|
|
@@ -3566,7 +3622,7 @@ function verifyFormFills(session, planned) {
|
|
|
3566
3622
|
if (!actual || !expected) {
|
|
3567
3623
|
mismatches.push({ fieldLabel: label, expected, actual, ...(p.field.fieldId ? { fieldId: p.field.fieldId } : {}) });
|
|
3568
3624
|
}
|
|
3569
|
-
else if (
|
|
3625
|
+
else if (!valuesEquivalent(expected, actual)) {
|
|
3570
3626
|
mismatches.push({ fieldLabel: label, expected, actual, ...(p.field.fieldId ? { fieldId: p.field.fieldId } : {}) });
|
|
3571
3627
|
}
|
|
3572
3628
|
else {
|
package/dist/session.js
CHANGED
|
@@ -645,19 +645,46 @@ export function connect(url, opts) {
|
|
|
645
645
|
let resolved = false;
|
|
646
646
|
let lastMessageAt = Date.now();
|
|
647
647
|
let heartbeatInterval = null;
|
|
648
|
+
let pendingPongBy = null;
|
|
649
|
+
// Heartbeat: send a real WS-level ping every 15s and only tear the socket
|
|
650
|
+
// down if the peer fails to respond to two consecutive pings (i.e. ~45s of
|
|
651
|
+
// true unresponsiveness). Previous versions used a dumb idle timer that
|
|
652
|
+
// closed the socket after 30s of no inbound frames — which killed sessions
|
|
653
|
+
// during normal form-submission flows where the DOM is legitimately idle
|
|
654
|
+
// for 20-30+ seconds while the backend processes (Greenhouse submit →
|
|
655
|
+
// security-code dialog is the canonical repro). A real ping/pong cycle
|
|
656
|
+
// distinguishes a silent-but-healthy session from a dead one.
|
|
648
657
|
function startHeartbeat() {
|
|
649
658
|
if (heartbeatInterval)
|
|
650
659
|
return;
|
|
651
660
|
heartbeatInterval = setInterval(() => {
|
|
652
|
-
|
|
661
|
+
// If we're waiting on a pong and it's overdue, the peer is dead.
|
|
662
|
+
if (pendingPongBy !== null && Date.now() > pendingPongBy) {
|
|
653
663
|
try {
|
|
654
664
|
ws.close();
|
|
655
665
|
}
|
|
656
666
|
catch { /* ignore */ }
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
// Only send a new ping if we haven't heard anything for a while,
|
|
670
|
+
// to avoid spamming a chatty session.
|
|
671
|
+
if (Date.now() - lastMessageAt > 10_000) {
|
|
672
|
+
try {
|
|
673
|
+
ws.ping();
|
|
674
|
+
// Allow 30s for the pong before declaring the peer dead.
|
|
675
|
+
pendingPongBy = Date.now() + 30_000;
|
|
676
|
+
}
|
|
677
|
+
catch {
|
|
678
|
+
/* if ping throws, the socket is already gone — let 'close' handle */
|
|
679
|
+
}
|
|
657
680
|
}
|
|
658
|
-
},
|
|
681
|
+
}, 15_000);
|
|
659
682
|
heartbeatInterval.unref();
|
|
660
683
|
}
|
|
684
|
+
ws.on('pong', () => {
|
|
685
|
+
lastMessageAt = Date.now();
|
|
686
|
+
pendingPongBy = null;
|
|
687
|
+
});
|
|
661
688
|
const timeout = setTimeout(() => {
|
|
662
689
|
if (!resolved) {
|
|
663
690
|
resolved = true;
|
|
@@ -1782,12 +1809,26 @@ function compactSchemaContext(context, label) {
|
|
|
1782
1809
|
return Object.keys(out).length > 0 ? out : undefined;
|
|
1783
1810
|
}
|
|
1784
1811
|
function compactSchemaValue(value, inlineLimit = 80) {
|
|
1785
|
-
|
|
1786
|
-
|
|
1812
|
+
// Measure the length of the FULL whitespace-normalized value first, before
|
|
1813
|
+
// any inline truncation. The previous implementation called sanitizeInlineName
|
|
1814
|
+
// with max=120 and then read normalized.length, which capped reported length
|
|
1815
|
+
// at 120 even when the actual filled content was 1000+ characters. That made
|
|
1816
|
+
// form-required snapshots look like long-textarea fills had only landed
|
|
1817
|
+
// ~120 chars, when in reality the field was correctly filled — agents then
|
|
1818
|
+
// re-typed the same content thinking they had a partial fill, doubling the
|
|
1819
|
+
// value or hitting the textarea length cap.
|
|
1820
|
+
if (!value)
|
|
1821
|
+
return {};
|
|
1822
|
+
const fullNormalized = normalizeUiText(value);
|
|
1823
|
+
if (!fullNormalized)
|
|
1787
1824
|
return {};
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1825
|
+
const fullLength = fullNormalized.length;
|
|
1826
|
+
const inlineNormalized = sanitizeInlineName(value, Math.max(120, inlineLimit + 32));
|
|
1827
|
+
if (!inlineNormalized)
|
|
1828
|
+
return { valueLength: fullLength };
|
|
1829
|
+
return fullLength <= inlineLimit
|
|
1830
|
+
? { value: inlineNormalized }
|
|
1831
|
+
: { valueLength: fullLength };
|
|
1791
1832
|
}
|
|
1792
1833
|
function schemaOptionLabel(node) {
|
|
1793
1834
|
return sanitizeFieldName(node.name, 80) ?? sanitizeInlineName(node.name, 80);
|
package/package.json
CHANGED