@enricai/barnacle 1.12.35 → 1.12.36
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/lib/llm/schemas.d.ts +2 -0
- package/dist/lib/llm/schemas.d.ts.map +1 -1
- package/dist/lib/llm/schemas.js +8 -0
- package/dist/lib/llm/schemas.js.map +1 -1
- package/dist/plugins/config-plugin.d.ts +1 -0
- package/dist/plugins/config-plugin.d.ts.map +1 -1
- package/dist/plugins/config-plugin.js +3 -0
- package/dist/plugins/config-plugin.js.map +1 -1
- package/dist/scraper/errors.d.ts +4 -1
- package/dist/scraper/errors.d.ts.map +1 -1
- package/dist/scraper/errors.js +3 -0
- package/dist/scraper/errors.js.map +1 -1
- package/dist/scraper/flow-runner.d.ts +50 -38
- package/dist/scraper/flow-runner.d.ts.map +1 -1
- package/dist/scraper/flow-runner.js +125 -107
- package/dist/scraper/flow-runner.js.map +1 -1
- package/dist/scraper/spa-readiness.d.ts +28 -0
- package/dist/scraper/spa-readiness.d.ts.map +1 -0
- package/dist/scraper/spa-readiness.js +57 -0
- package/dist/scraper/spa-readiness.js.map +1 -0
- package/dist/scripts/recon-browser.d.ts +5 -0
- package/dist/scripts/recon-browser.d.ts.map +1 -1
- package/dist/scripts/recon-browser.js +64 -69
- package/dist/scripts/recon-browser.js.map +1 -1
- package/dist/scripts/recon-generate.d.ts +1 -0
- package/dist/scripts/recon-generate.d.ts.map +1 -1
- package/dist/scripts/recon-generate.js +11 -1
- package/dist/scripts/recon-generate.js.map +1 -1
- package/package.json +5 -1
|
@@ -88,7 +88,7 @@ exports.probeStepBeforeAttempts = probeStepBeforeAttempts;
|
|
|
88
88
|
exports.extractLinkFromMessage = extractLinkFromMessage;
|
|
89
89
|
exports.extractCodeFromMessage = extractCodeFromMessage;
|
|
90
90
|
exports.executeStepWithHealing = executeStepWithHealing;
|
|
91
|
-
exports.
|
|
91
|
+
exports.executeNavigateStep = executeNavigateStep;
|
|
92
92
|
exports.runHealingFlow = runHealingFlow;
|
|
93
93
|
const node_crypto_1 = require("node:crypto");
|
|
94
94
|
const node_fs_1 = require("node:fs");
|
|
@@ -118,6 +118,7 @@ const errors_2 = require("../scraper/errors");
|
|
|
118
118
|
const frame_target_1 = require("../scraper/frame-target");
|
|
119
119
|
const phantom_click_1 = require("../scraper/phantom-click");
|
|
120
120
|
const session_teardown_1 = require("../scraper/session-teardown");
|
|
121
|
+
const spa_readiness_1 = require("../scraper/spa-readiness");
|
|
121
122
|
const stagehand_guard_1 = require("../scraper/stagehand-guard");
|
|
122
123
|
const submit_control_1 = require("../scraper/submit-control");
|
|
123
124
|
const watchdog_1 = require("../scraper/watchdog");
|
|
@@ -4268,18 +4269,25 @@ async function waitForTransitionBody(params) {
|
|
|
4268
4269
|
return false;
|
|
4269
4270
|
}
|
|
4270
4271
|
/**
|
|
4271
|
-
* Site-agnostic captcha-solve hand-off: given an already-solved token,
|
|
4272
|
-
*
|
|
4273
|
-
*
|
|
4272
|
+
* Site-agnostic captcha-solve hand-off: given an already-solved token,
|
|
4273
|
+
* discover the widget's registered `data-callback` and invoke it with the
|
|
4274
|
+
* token so the site assembles its own submit (its own hidden fields, its own
|
|
4275
|
+
* submit path); only when no callback is discoverable does this fall back to
|
|
4276
|
+
* setting the hidden response field directly and dispatching the
|
|
4277
|
+
* framework-visible `change` event so the field's own value listener
|
|
4278
|
+
* observes it.
|
|
4274
4279
|
*
|
|
4275
|
-
* Widgets of this shape
|
|
4276
|
-
*
|
|
4277
|
-
*
|
|
4278
|
-
*
|
|
4279
|
-
*
|
|
4280
|
-
*
|
|
4281
|
-
*
|
|
4282
|
-
*
|
|
4280
|
+
* Widgets of this shape anchor a `[data-sitekey]` element whose
|
|
4281
|
+
* `data-callback` attribute names a function on `window`; that callback (not
|
|
4282
|
+
* a bare field assignment) is what some sites depend on to append companion
|
|
4283
|
+
* hidden fields before submitting. Invoking it — or, absent one, assigning
|
|
4284
|
+
* the response field's value — can itself navigate the frame synchronously,
|
|
4285
|
+
* mid-evaluate, on widgets whose callback (or value-set side effect) submits
|
|
4286
|
+
* the enclosing form as soon as a token lands. No eval this function issues
|
|
4287
|
+
* after the precheck is depended on for a return value: only the precheck (a
|
|
4288
|
+
* pure DOM query, which cannot navigate) is awaited for its result; the
|
|
4289
|
+
* callback-invoke and the value-set/change-dispatch evals are fired and
|
|
4290
|
+
* their rejections discarded, exactly like a navigating `form.submit()`
|
|
4283
4291
|
* would be.
|
|
4284
4292
|
*
|
|
4285
4293
|
* Deliberately narrow: no polling, no verification, no wait budget, and no
|
|
@@ -4299,18 +4307,35 @@ async function injectCaptchaTokenAndSubmit(target, token, responseField = "h-cap
|
|
|
4299
4307
|
const precheckExpr = `(() => {
|
|
4300
4308
|
const responseField = ${JSON.stringify(responseField)};
|
|
4301
4309
|
const field = document.querySelector('[name="' + responseField + '"]');
|
|
4302
|
-
|
|
4310
|
+
const sitekeyEl = document.querySelector("[data-sitekey]");
|
|
4311
|
+
const callbackName = sitekeyEl ? sitekeyEl.getAttribute("data-callback") : null;
|
|
4312
|
+
const callbackExists = Boolean(callbackName) && typeof window[callbackName] === "function";
|
|
4313
|
+
const callback = callbackExists ? callbackName : null;
|
|
4314
|
+
if (field) return { fieldExists: true, hasForm: Boolean(field.closest("form")), callback };
|
|
4303
4315
|
const forms = Array.from(document.querySelectorAll("form"));
|
|
4304
4316
|
const form = forms.find((candidate) => candidate.querySelector("[data-sitekey]")) ?? forms[0];
|
|
4305
|
-
return { fieldExists: false, hasForm: Boolean(form) };
|
|
4317
|
+
return { fieldExists: false, hasForm: Boolean(form), callback };
|
|
4306
4318
|
})()`;
|
|
4307
4319
|
// Purely a DOM query — reads but never mutates the page, so it cannot
|
|
4308
4320
|
// itself trigger navigation. This is the only evaluate call in this
|
|
4309
4321
|
// function whose return value is depended on.
|
|
4310
|
-
const { fieldExists, hasForm } = await target.evaluate(precheckExpr);
|
|
4322
|
+
const { fieldExists, hasForm, callback } = await target.evaluate(precheckExpr);
|
|
4311
4323
|
const injected = fieldExists || hasForm;
|
|
4312
4324
|
if (!injected)
|
|
4313
4325
|
return { injected, hasForm };
|
|
4326
|
+
if (callback) {
|
|
4327
|
+
const invokeCallbackExpr = `(() => {
|
|
4328
|
+
const token = ${JSON.stringify(token)};
|
|
4329
|
+
window[${JSON.stringify(callback)}](token);
|
|
4330
|
+
})()`;
|
|
4331
|
+
// Invoking the widget's own callback can navigate the frame synchronously
|
|
4332
|
+
// from within this call (the callback is free to build its own fields and
|
|
4333
|
+
// submit), tearing down the execution context before a return value
|
|
4334
|
+
// marshals — that rejection is the expected outcome, not a real failure,
|
|
4335
|
+
// so it's discarded here rather than depended on.
|
|
4336
|
+
await target.evaluate(invokeCallbackExpr).catch(() => undefined);
|
|
4337
|
+
return { injected, hasForm };
|
|
4338
|
+
}
|
|
4314
4339
|
const setValueExpr = `(() => {
|
|
4315
4340
|
const responseField = ${JSON.stringify(responseField)};
|
|
4316
4341
|
const token = ${JSON.stringify(token)};
|
|
@@ -4357,14 +4382,18 @@ async function injectCaptchaTokenAndSubmit(target, token, responseField = "h-cap
|
|
|
4357
4382
|
* used only when the caller's own transition poll observed no advance after
|
|
4358
4383
|
* the inject, so the widget's callback (if any) evidently didn't submit for
|
|
4359
4384
|
* us. Kept separate from the inject primitive so the caller can gate its use
|
|
4360
|
-
* on an observed transition rather than firing it unconditionally.
|
|
4385
|
+
* on an observed transition rather than firing it unconditionally. Prefers
|
|
4386
|
+
* `form.requestSubmit()` so any submit-event listener (including one that
|
|
4387
|
+
* calls `preventDefault()` and drives its own submit logic) and native form
|
|
4388
|
+
* validation still run, matching real-browser submit semantics; falls back
|
|
4389
|
+
* to the bare `form.submit()` only when `requestSubmit` isn't available.
|
|
4361
4390
|
*/
|
|
4362
4391
|
async function submitCaptchaGatedForm(target, responseField = "h-captcha-response") {
|
|
4363
4392
|
const submitExpr = `(() => {
|
|
4364
4393
|
const responseField = ${JSON.stringify(responseField)};
|
|
4365
4394
|
const field = document.querySelector('[name="' + responseField + '"]');
|
|
4366
4395
|
const form = field ? field.closest("form") : null;
|
|
4367
|
-
if (form) form.submit();
|
|
4396
|
+
if (form) (form.requestSubmit ? form.requestSubmit() : form.submit());
|
|
4368
4397
|
})()`;
|
|
4369
4398
|
// form.submit() navigates the frame synchronously, tearing down the execution
|
|
4370
4399
|
// context before Runtime.evaluate can marshal a return value for this call —
|
|
@@ -9331,57 +9360,32 @@ async function executeStepWithHealing(params) {
|
|
|
9331
9360
|
}
|
|
9332
9361
|
throw new errors_2.StepVerificationError(`${formatStepPrefix(stepIndex, totalSteps)} (${step.slice(0, 60)}) failed verification after ${MAX_STEP_ATTEMPTS} attempts${dumpPath ? `; see ${dumpPath}` : ""}`, phantomClickAfterAttempt1 ? "phantom-click-exhausted" : "cascade-exhausted");
|
|
9333
9362
|
}
|
|
9334
|
-
/**
|
|
9335
|
-
|
|
9336
|
-
|
|
9337
|
-
|
|
9338
|
-
|
|
9339
|
-
*
|
|
9340
|
-
*
|
|
9341
|
-
*
|
|
9342
|
-
|
|
9343
|
-
|
|
9344
|
-
|
|
9345
|
-
|
|
9346
|
-
|
|
9347
|
-
|
|
9348
|
-
|
|
9349
|
-
|
|
9350
|
-
* same pattern as `waitForChildFrameReady`'s `isReady()` probe. Without this,
|
|
9351
|
-
* a single wedged CDP round-trip inside `readBodyLength` would pend forever
|
|
9352
|
-
* and the `while (Date.now() < deadline)` loop below would never be
|
|
9353
|
-
* re-entered, defeating the very deadline it exists to enforce.
|
|
9354
|
-
* `page.waitForTimeout(pollMs)` is left unguarded: it is a plain delay (no
|
|
9355
|
-
* DOM/network read whose response can be lost), so it does not carry the
|
|
9356
|
-
* "wedged read" failure mode this fix targets.
|
|
9357
|
-
*/
|
|
9358
|
-
async function waitForSpaReady(page, logger, opts = {}) {
|
|
9359
|
-
const timeoutMs = opts.timeoutMs ?? SPA_READINESS_TIMEOUT_MS;
|
|
9360
|
-
const pollMs = opts.pollMs ?? SPA_READINESS_POLL_MS;
|
|
9361
|
-
const minBodyLength = opts.minBodyLength ?? SPA_MIN_BODY_LENGTH;
|
|
9362
|
-
const bodyLengthExpr = "document.body ? document.body.outerHTML.length : 0";
|
|
9363
|
-
const readBodyLength = async () => {
|
|
9364
|
-
const raw = await (0, watchdog_1.withWatchdog)(() => page.evaluate(bodyLengthExpr), {
|
|
9365
|
-
timeoutMs: pollMs,
|
|
9366
|
-
label: "flow-runner: spa readiness body-length probe",
|
|
9367
|
-
}).catch(() => 0);
|
|
9368
|
-
return typeof raw === "number" ? raw : 0;
|
|
9369
|
-
};
|
|
9370
|
-
let bodyLength = await readBodyLength();
|
|
9371
|
-
if (bodyLength >= minBodyLength) {
|
|
9372
|
-
return;
|
|
9363
|
+
/**
|
|
9364
|
+
* Navigates `page` directly to `url` and waits for the SPA to hydrate,
|
|
9365
|
+
* bypassing the self-heal cascade entirely — a multi-page capture flow's
|
|
9366
|
+
* page transitions are a known URL, not an element to locate and click, so
|
|
9367
|
+
* there is nothing for the cascade's DOM-healing attempts to add. Matches
|
|
9368
|
+
* {@link executeStepWithHealing}'s `"completed"|"skipped"` outcome contract
|
|
9369
|
+
* so `runHealingFlow`'s step loop (stuck-detection, `submitStep` gating,
|
|
9370
|
+
* trajectory) needs zero changes to also handle navigate steps.
|
|
9371
|
+
*/
|
|
9372
|
+
async function executeNavigateStep(params) {
|
|
9373
|
+
const { page, url, optional, logger: log, waitUntil, timeoutMs } = params;
|
|
9374
|
+
try {
|
|
9375
|
+
await page.goto(url, {
|
|
9376
|
+
waitUntil: waitUntil ?? "domcontentloaded",
|
|
9377
|
+
timeoutMs: timeoutMs ?? exports.GOTO_TIMEOUT_MS,
|
|
9378
|
+
});
|
|
9373
9379
|
}
|
|
9374
|
-
|
|
9375
|
-
|
|
9376
|
-
|
|
9377
|
-
|
|
9378
|
-
bodyLength = await readBodyLength();
|
|
9379
|
-
if (bodyLength >= minBodyLength) {
|
|
9380
|
-
logger.info(`spa readiness: body grew to ${bodyLength} chars — SPA rendered`);
|
|
9381
|
-
return;
|
|
9380
|
+
catch (err) {
|
|
9381
|
+
if (optional) {
|
|
9382
|
+
log.warn(`navigate step: goto failed for optional step, skipping: ${(0, errors_1.toErrorMessage)(err)}`);
|
|
9383
|
+
return "skipped";
|
|
9382
9384
|
}
|
|
9385
|
+
throw new errors_2.StepVerificationError(`navigate step failed: goto to ${url} threw: ${(0, errors_1.toErrorMessage)(err)}`, "navigate-failed");
|
|
9383
9386
|
}
|
|
9384
|
-
|
|
9387
|
+
await (0, spa_readiness_1.waitForSpaReady)(page, log);
|
|
9388
|
+
return "completed";
|
|
9385
9389
|
}
|
|
9386
9390
|
/**
|
|
9387
9391
|
* Plugin-facing wrapper that drives a recon flow's steps through the SAME
|
|
@@ -9455,48 +9459,62 @@ async function runHealingFlow(deps) {
|
|
|
9455
9459
|
throw new errors_2.SessionTimeoutError(`${formatStepPrefix(i, () => steps.length)} session appears closed/dead (page.url() threw: ${(0, errors_1.toErrorMessage)(err)}) — aborting after ${i} of ${steps.length} steps completed`);
|
|
9456
9460
|
}
|
|
9457
9461
|
lastStepIndex = i;
|
|
9458
|
-
//
|
|
9459
|
-
//
|
|
9460
|
-
//
|
|
9461
|
-
|
|
9462
|
-
|
|
9463
|
-
|
|
9464
|
-
|
|
9465
|
-
|
|
9466
|
-
|
|
9467
|
-
|
|
9468
|
-
|
|
9469
|
-
|
|
9470
|
-
|
|
9471
|
-
|
|
9472
|
-
|
|
9473
|
-
|
|
9474
|
-
|
|
9475
|
-
|
|
9476
|
-
|
|
9477
|
-
|
|
9478
|
-
|
|
9479
|
-
|
|
9480
|
-
|
|
9481
|
-
|
|
9482
|
-
|
|
9483
|
-
|
|
9484
|
-
|
|
9485
|
-
|
|
9486
|
-
|
|
9487
|
-
|
|
9488
|
-
|
|
9489
|
-
|
|
9490
|
-
|
|
9491
|
-
|
|
9492
|
-
|
|
9493
|
-
|
|
9494
|
-
|
|
9495
|
-
|
|
9496
|
-
|
|
9497
|
-
|
|
9498
|
-
|
|
9499
|
-
|
|
9462
|
+
// A navigateTo step is a direct page.goto, not an act/observe against
|
|
9463
|
+
// the current DOM, so it has no target frame to resolve and skips
|
|
9464
|
+
// straight to executeNavigateStep instead of the self-heal cascade.
|
|
9465
|
+
let stepPromise;
|
|
9466
|
+
if (s.navigateTo !== undefined) {
|
|
9467
|
+
stepPromise = executeNavigateStep({
|
|
9468
|
+
page,
|
|
9469
|
+
url: s.navigateTo,
|
|
9470
|
+
optional: s.optional,
|
|
9471
|
+
logger,
|
|
9472
|
+
});
|
|
9473
|
+
}
|
|
9474
|
+
else {
|
|
9475
|
+
// Resolved fresh per step (not cached across the run) so a cross-origin
|
|
9476
|
+
// iframe that attaches mid-flow (e.g. after an "Apply" click reveals a
|
|
9477
|
+
// wizard embedded later in the DOM) is picked up as soon as it's
|
|
9478
|
+
// reachable, paralleling the recon CLI's per-step resolution. `resolveFrameTarget`
|
|
9479
|
+
// falls back to the main-frame target when `frameSelector` is null/unresolvable,
|
|
9480
|
+
// so this is a no-op for every flow that doesn't declare one.
|
|
9481
|
+
const frameTarget = await (0, frame_target_1.resolveFrameTarget)(page, deps.frameSelector);
|
|
9482
|
+
await (0, frame_target_1.waitForChildFrameReady)(frameTarget);
|
|
9483
|
+
stepPromise = executeStepWithHealing({
|
|
9484
|
+
stagehand,
|
|
9485
|
+
page,
|
|
9486
|
+
step: s.instruction,
|
|
9487
|
+
optional: s.optional,
|
|
9488
|
+
upload: s.upload,
|
|
9489
|
+
submitStep: s.submitStep,
|
|
9490
|
+
captchaGated: s.captchaGated === true,
|
|
9491
|
+
emailStep: s.emailStep === true,
|
|
9492
|
+
emailStepConfig: s.emailStepConfig,
|
|
9493
|
+
allocatedInbox: deps.allocatedInbox ?? null,
|
|
9494
|
+
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
9495
|
+
stepIndex: i,
|
|
9496
|
+
totalSteps: () => steps.length,
|
|
9497
|
+
phase: "flow",
|
|
9498
|
+
signalCounter,
|
|
9499
|
+
recentCaptures,
|
|
9500
|
+
recentCaptureMeta,
|
|
9501
|
+
anthropic,
|
|
9502
|
+
rephraseModel,
|
|
9503
|
+
logger,
|
|
9504
|
+
uploadFixture,
|
|
9505
|
+
frameTarget,
|
|
9506
|
+
isFinalStep: i === steps.length - 1,
|
|
9507
|
+
submitEndpointPattern: deps.submitEndpointPattern ?? null,
|
|
9508
|
+
submittedStateSelectors: deps.submittedStateSelectors ?? [],
|
|
9509
|
+
requireSubmitEndpointMatch: deps.requireSubmitEndpointMatch ?? false,
|
|
9510
|
+
advanceTransitionBodyPattern: deps.advanceTransitionBodyPattern ?? null,
|
|
9511
|
+
successUrlFragments: deps.successUrlFragments ?? [],
|
|
9512
|
+
successPageTitleHints: deps.successPageTitleHints ?? [],
|
|
9513
|
+
ownBackendHostnames: deps.ownBackendHostnames ?? [],
|
|
9514
|
+
knownErrorClassPrefixes: deps.knownErrorClassPrefixes ?? [],
|
|
9515
|
+
wizardExitButtonLabels: deps.wizardExitButtonLabels ?? [],
|
|
9516
|
+
});
|
|
9517
|
+
}
|
|
9500
9518
|
const outcome = deps.deathSignal
|
|
9501
9519
|
? await (0, session_teardown_1.raceAgainstTeardown)(stepPromise, deps.deathSignal)
|
|
9502
9520
|
: await stepPromise;
|