site-agent-pro 1.0.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.
Files changed (81) hide show
  1. package/README.md +689 -0
  2. package/dist/auth/credentialStore.js +62 -0
  3. package/dist/auth/inbox.js +193 -0
  4. package/dist/auth/profile.js +379 -0
  5. package/dist/auth/runner.js +1124 -0
  6. package/dist/backend/dashboardData.js +194 -0
  7. package/dist/backend/runArtifacts.js +48 -0
  8. package/dist/backend/runRepository.js +93 -0
  9. package/dist/bin.js +2 -0
  10. package/dist/cli/backfillSiteChecks.js +143 -0
  11. package/dist/cli/run.js +309 -0
  12. package/dist/cli/trade.js +69 -0
  13. package/dist/config.js +199 -0
  14. package/dist/core/agentProfiles.js +55 -0
  15. package/dist/core/aggregateReport.js +382 -0
  16. package/dist/core/audit.js +30 -0
  17. package/dist/core/customTaskSuite.js +148 -0
  18. package/dist/core/evaluator.js +217 -0
  19. package/dist/core/executor.js +788 -0
  20. package/dist/core/fallbackReport.js +335 -0
  21. package/dist/core/formHeuristics.js +411 -0
  22. package/dist/core/gameplaySummary.js +164 -0
  23. package/dist/core/interaction.js +202 -0
  24. package/dist/core/pageState.js +201 -0
  25. package/dist/core/planner.js +1669 -0
  26. package/dist/core/processSubmissionBatch.js +204 -0
  27. package/dist/core/runAuditJob.js +170 -0
  28. package/dist/core/runner.js +2352 -0
  29. package/dist/core/siteBrief.js +107 -0
  30. package/dist/core/siteChecks.js +1526 -0
  31. package/dist/core/taskDirectives.js +279 -0
  32. package/dist/core/taskHeuristics.js +263 -0
  33. package/dist/dashboard/client.js +1256 -0
  34. package/dist/dashboard/contracts.js +95 -0
  35. package/dist/dashboard/narrative.js +277 -0
  36. package/dist/dashboard/server.js +458 -0
  37. package/dist/dashboard/theme.js +888 -0
  38. package/dist/index.js +84 -0
  39. package/dist/llm/client.js +188 -0
  40. package/dist/paystack/account.js +123 -0
  41. package/dist/paystack/client.js +100 -0
  42. package/dist/paystack/index.js +13 -0
  43. package/dist/paystack/test-paystack.js +83 -0
  44. package/dist/paystack/transfer.js +138 -0
  45. package/dist/paystack/types.js +74 -0
  46. package/dist/paystack/webhook.js +121 -0
  47. package/dist/prompts/browserAgent.js +124 -0
  48. package/dist/prompts/reviewer.js +71 -0
  49. package/dist/reporting/clickReplay.js +290 -0
  50. package/dist/reporting/html.js +930 -0
  51. package/dist/reporting/markdown.js +238 -0
  52. package/dist/reporting/template.js +1141 -0
  53. package/dist/schemas/types.js +361 -0
  54. package/dist/submissions/customTasks.js +196 -0
  55. package/dist/submissions/html.js +770 -0
  56. package/dist/submissions/model.js +56 -0
  57. package/dist/submissions/publicUrl.js +76 -0
  58. package/dist/submissions/service.js +74 -0
  59. package/dist/submissions/store.js +37 -0
  60. package/dist/submissions/types.js +65 -0
  61. package/dist/trade/engine.js +241 -0
  62. package/dist/trade/evm/erc20.js +44 -0
  63. package/dist/trade/extractor.js +148 -0
  64. package/dist/trade/policy.js +35 -0
  65. package/dist/trade/session.js +31 -0
  66. package/dist/trade/types.js +107 -0
  67. package/dist/trade/validator.js +148 -0
  68. package/dist/utils/files.js +59 -0
  69. package/dist/utils/log.js +24 -0
  70. package/dist/utils/playwrightCompat.js +14 -0
  71. package/dist/utils/time.js +3 -0
  72. package/dist/wallet/provider.js +345 -0
  73. package/dist/wallet/relay.js +129 -0
  74. package/dist/wallet/wallet.js +178 -0
  75. package/docs/01-installation.md +134 -0
  76. package/docs/02-running-your-first-audit.md +136 -0
  77. package/docs/03-configuration.md +233 -0
  78. package/docs/04-how-the-agent-thinks.md +41 -0
  79. package/docs/05-extending-personas-and-tasks.md +42 -0
  80. package/docs/06-hardening-for-production.md +92 -0
  81. package/package.json +60 -0
@@ -0,0 +1,788 @@
1
+ import { findMatchingSelectOption, fitValueToField, scoreFormFieldTargetMatch, shouldCheckField, } from "./formHeuristics.js";
2
+ import { buildLooseAccessiblePattern, prepareLocatorForInteraction, resolvePreferredFieldLocator, typeLikeHuman, } from "./interaction.js";
3
+ const INTERSTITIAL_PATTERNS = [
4
+ /just a moment/i,
5
+ /verification successful/i,
6
+ /checking your browser/i,
7
+ /cloudflare/i,
8
+ /security check/i,
9
+ /access denied/i,
10
+ /captcha/i,
11
+ /human verification/i,
12
+ ];
13
+ function cleanErrorMessage(error) {
14
+ const message = error instanceof Error ? error.message : String(error);
15
+ const withoutAnsi = message.replace(/\u001b\[[0-9;]*m/g, "");
16
+ return withoutAnsi.replace(/\s+/g, " ").trim() || "Unknown error";
17
+ }
18
+ function normalizeText(value) {
19
+ return value.replace(/\s+/g, " ").trim();
20
+ }
21
+ function normalizeKey(value) {
22
+ return normalizeText(value).toLowerCase();
23
+ }
24
+ function escapeAttributeValue(value) {
25
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
26
+ }
27
+ function resolveDecisionTargetLabel(decision) {
28
+ return decision.target.trim() || decision.target_id.trim();
29
+ }
30
+ function truncateLabel(value, maxLength) {
31
+ if (value.length <= maxLength) {
32
+ return value;
33
+ }
34
+ return `${value.slice(0, Math.max(1, maxLength - 1)).trimEnd()}...`;
35
+ }
36
+ function isInterstitialState(state) {
37
+ const textBlob = `${state.title} ${state.textSnippet} ${state.url}`;
38
+ return INTERSTITIAL_PATTERNS.some((pattern) => pattern.test(textBlob));
39
+ }
40
+ async function readVisibleState(page) {
41
+ const title = await page.title().catch(() => "");
42
+ const url = page.url();
43
+ const textSnippet = normalizeText(await page
44
+ .locator("body")
45
+ .innerText()
46
+ .catch(() => "")).slice(0, 800);
47
+ const modalCount = await page
48
+ .evaluate(() => {
49
+ return document.querySelectorAll("dialog, [role='dialog'], .modal, [aria-modal='true']").length;
50
+ })
51
+ .catch(() => 0);
52
+ return { url, title, textSnippet, modalCount };
53
+ }
54
+ async function captureClickNeighborhood(locator) {
55
+ try {
56
+ return await locator.evaluate((element) => {
57
+ const container = element.closest("section, article, nav, main, aside, dialog, [role='dialog'], [role='tabpanel'], .modal, .card, .panel, .accordion") || element.parentElement;
58
+ if (!container) {
59
+ return "";
60
+ }
61
+ return (container.innerHTML || "")
62
+ .replace(/\s+/g, " ")
63
+ .trim()
64
+ .slice(0, 600);
65
+ });
66
+ }
67
+ catch {
68
+ return "";
69
+ }
70
+ }
71
+ function describeStateChange(before, after) {
72
+ const stateChanged = before.url !== after.url ||
73
+ normalizeText(before.title) !== normalizeText(after.title) ||
74
+ before.textSnippet !== after.textSnippet ||
75
+ before.modalCount !== after.modalCount;
76
+ return {
77
+ stateChanged,
78
+ destinationLabel: normalizeText(after.title) || after.url,
79
+ };
80
+ }
81
+ async function firstVisible(locators) {
82
+ for (const item of locators) {
83
+ try {
84
+ if (await item.locator.first().isVisible({ timeout: 1200 })) {
85
+ return { locator: item.locator.first(), name: item.name };
86
+ }
87
+ }
88
+ catch {
89
+ // continue
90
+ }
91
+ }
92
+ return null;
93
+ }
94
+ async function findClickTarget(page, target) {
95
+ const escapedTarget = escapeAttributeValue(target);
96
+ const targetPattern = buildLooseAccessiblePattern(target);
97
+ if (!targetPattern) {
98
+ return null;
99
+ }
100
+ return firstVisible([
101
+ {
102
+ locator: page.getByRole("button", { name: targetPattern }),
103
+ name: "getByRole(button)",
104
+ },
105
+ {
106
+ locator: page.getByRole("link", { name: targetPattern }),
107
+ name: "getByRole(link)",
108
+ },
109
+ {
110
+ locator: page.getByRole("tab", { name: targetPattern }),
111
+ name: "getByRole(tab)",
112
+ },
113
+ {
114
+ locator: page.getByRole("menuitem", { name: targetPattern }),
115
+ name: "getByRole(menuitem)",
116
+ },
117
+ {
118
+ locator: page.locator(`input[type="submit"][value*="${escapedTarget}" i], input[type="button"][value*="${escapedTarget}" i], input[type="reset"][value*="${escapedTarget}" i]`),
119
+ name: "input[value contains]",
120
+ },
121
+ { locator: page.getByText(targetPattern), name: "getByText" },
122
+ {
123
+ locator: page.locator(`[aria-label*="${escapedTarget}" i]`),
124
+ name: "aria-label contains",
125
+ },
126
+ ]);
127
+ }
128
+ async function findClickTargetById(page, targetId) {
129
+ const normalizedTargetId = targetId.trim();
130
+ if (!normalizedTargetId) {
131
+ return null;
132
+ }
133
+ const locator = page
134
+ .locator(`[data-site-agent-id="${escapeAttributeValue(normalizedTargetId)}"]`)
135
+ .first();
136
+ try {
137
+ if (await locator.isVisible({ timeout: 1200 })) {
138
+ return { locator, name: "target_id" };
139
+ }
140
+ }
141
+ catch {
142
+ // continue
143
+ }
144
+ return null;
145
+ }
146
+ async function buildClickIndicator(locator, target) {
147
+ await locator.scrollIntoViewIfNeeded().catch(() => undefined);
148
+ const box = await locator.boundingBox().catch(() => null);
149
+ if (!box || box.width <= 0 || box.height <= 0) {
150
+ return undefined;
151
+ }
152
+ return {
153
+ x: box.x,
154
+ y: box.y,
155
+ width: box.width,
156
+ height: box.height,
157
+ targetLabel: truncateLabel(normalizeText(target) || "Click target", 72),
158
+ };
159
+ }
160
+ export async function prepareClickDecision(page, decision) {
161
+ const targetLabel = resolveDecisionTargetLabel(decision);
162
+ const targetId = decision.target_id.trim();
163
+ if (targetId) {
164
+ const match = await findClickTargetById(page, targetId);
165
+ if (!match) {
166
+ return {
167
+ note: `Could not find numbered clickable element for target_id '${targetId}'`,
168
+ };
169
+ }
170
+ const clickIndicator = await buildClickIndicator(match.locator, targetLabel || targetId);
171
+ return {
172
+ preparedClick: {
173
+ locator: match.locator,
174
+ matchedBy: match.name,
175
+ ...(clickIndicator ? { clickIndicator } : {}),
176
+ },
177
+ };
178
+ }
179
+ if (!targetLabel) {
180
+ return { note: "Decision required a target but did not provide one" };
181
+ }
182
+ const match = await findClickTarget(page, targetLabel);
183
+ if (!match) {
184
+ return { note: `Could not find clickable element for '${targetLabel}'` };
185
+ }
186
+ const clickIndicator = await buildClickIndicator(match.locator, targetLabel);
187
+ return {
188
+ preparedClick: {
189
+ locator: match.locator,
190
+ matchedBy: match.name,
191
+ ...(clickIndicator ? { clickIndicator } : {}),
192
+ },
193
+ };
194
+ }
195
+ async function collectVisibleFillableFields(page) {
196
+ const fields = await page.evaluate(() => {
197
+ return Array.from(document.querySelectorAll("input, textarea, select"))
198
+ .map((element, index) => {
199
+ const rect = element.getBoundingClientRect();
200
+ const style = window.getComputedStyle(element);
201
+ const inputType = element instanceof HTMLInputElement
202
+ ? (element.type || "text").replace(/\s+/g, " ").trim().toLowerCase()
203
+ : "";
204
+ if (rect.width <= 0 ||
205
+ rect.height <= 0 ||
206
+ style.visibility === "hidden" ||
207
+ style.display === "none" ||
208
+ element.disabled ||
209
+ inputType === "hidden" ||
210
+ inputType === "submit" ||
211
+ inputType === "button" ||
212
+ inputType === "image") {
213
+ return null;
214
+ }
215
+ const marker = String(index + 1);
216
+ element.setAttribute("data-site-agent-fill-field", marker);
217
+ const agentId = (element.getAttribute("data-site-agent-id") || "").trim() || marker;
218
+ element.setAttribute("data-site-agent-id", agentId);
219
+ const labelParts = [];
220
+ if ("labels" in element && element.labels) {
221
+ for (const label of Array.from(element.labels)) {
222
+ labelParts.push((label.innerText || label.textContent || "")
223
+ .replace(/\s+/g, " ")
224
+ .trim());
225
+ }
226
+ }
227
+ const ariaLabelledBy = element.getAttribute("aria-labelledby");
228
+ if (ariaLabelledBy) {
229
+ for (const id of ariaLabelledBy.split(/\s+/)) {
230
+ const labelElement = document.getElementById(id);
231
+ if (labelElement) {
232
+ labelParts.push((labelElement.textContent || "").replace(/\s+/g, " ").trim());
233
+ }
234
+ }
235
+ }
236
+ const closestLabel = element.closest("label");
237
+ if (closestLabel) {
238
+ labelParts.push((closestLabel.innerText || closestLabel.textContent || "")
239
+ .replace(/\s+/g, " ")
240
+ .trim());
241
+ }
242
+ labelParts.push((element.getAttribute("aria-label") || "")
243
+ .replace(/\s+/g, " ")
244
+ .trim());
245
+ const options = element instanceof HTMLSelectElement
246
+ ? Array.from(element.options)
247
+ .map((option) => (option.textContent || "").replace(/\s+/g, " ").trim())
248
+ .filter(Boolean)
249
+ .slice(0, 60)
250
+ : [];
251
+ return {
252
+ agentId,
253
+ marker,
254
+ label: labelParts
255
+ .filter(Boolean)
256
+ .join(" ")
257
+ .replace(/\s+/g, " ")
258
+ .trim(),
259
+ placeholder: (element.getAttribute("placeholder") || "")
260
+ .replace(/\s+/g, " ")
261
+ .trim(),
262
+ name: (element.getAttribute("name") || "")
263
+ .replace(/\s+/g, " ")
264
+ .trim(),
265
+ id: (element.id || "").replace(/\s+/g, " ").trim(),
266
+ tag: element.tagName.toLowerCase(),
267
+ inputType: inputType || element.tagName.toLowerCase(),
268
+ autocomplete: (element.getAttribute("autocomplete") || "")
269
+ .replace(/\s+/g, " ")
270
+ .trim()
271
+ .toLowerCase(),
272
+ inputMode: (element.getAttribute("inputmode") || "")
273
+ .replace(/\s+/g, " ")
274
+ .trim()
275
+ .toLowerCase(),
276
+ checked: element instanceof HTMLInputElement ? element.checked : undefined,
277
+ maxLength: element instanceof HTMLInputElement ||
278
+ element instanceof HTMLTextAreaElement
279
+ ? element.maxLength > 0
280
+ ? element.maxLength
281
+ : null
282
+ : null,
283
+ options,
284
+ };
285
+ })
286
+ .filter(Boolean);
287
+ });
288
+ return fields.filter((field) => Boolean(field));
289
+ }
290
+ function scoreFieldMatch(field, target) {
291
+ return scoreFormFieldTargetMatch(field, target);
292
+ }
293
+ function findBestFillableField(fields, target) {
294
+ const ranked = fields
295
+ .map((field) => ({
296
+ field,
297
+ score: scoreFieldMatch(field, target),
298
+ }))
299
+ .filter((candidate) => candidate.score > 0)
300
+ .sort((left, right) => right.score - left.score);
301
+ return ranked[0]?.field ?? null;
302
+ }
303
+ async function resolveFillableFieldLocator(page, field) {
304
+ const fallbackLocator = page
305
+ .locator(`[data-site-agent-fill-field="${escapeAttributeValue(field.marker)}"]`)
306
+ .first();
307
+ return resolvePreferredFieldLocator({
308
+ page,
309
+ field,
310
+ fallbackLocator,
311
+ preferredNames: [
312
+ field.label,
313
+ field.placeholder,
314
+ field.name,
315
+ field.id,
316
+ ].filter(Boolean),
317
+ });
318
+ }
319
+ async function readCheckedState(locator, inputType) {
320
+ return locator.isChecked().catch(async () => {
321
+ return locator
322
+ .evaluate((element, expectedType) => {
323
+ if (element instanceof HTMLInputElement) {
324
+ return element.checked;
325
+ }
326
+ const nested = element.querySelector(`input[type="${expectedType}"], input[type="checkbox"], input[type="radio"]`);
327
+ return nested instanceof HTMLInputElement ? nested.checked : false;
328
+ }, inputType)
329
+ .catch(() => false);
330
+ });
331
+ }
332
+ async function setCheckedStateWithDomFallback(locator, inputType) {
333
+ await locator.evaluate((element, expectedType) => {
334
+ const nested = element.querySelector(`input[type="${expectedType}"], input[type="checkbox"], input[type="radio"]`);
335
+ const target = element instanceof HTMLInputElement
336
+ ? element
337
+ : nested instanceof HTMLInputElement
338
+ ? nested
339
+ : null;
340
+ if (!target) {
341
+ if (element instanceof HTMLElement) {
342
+ element.click();
343
+ }
344
+ return;
345
+ }
346
+ const prototype = window.HTMLInputElement.prototype;
347
+ const descriptor = Object.getOwnPropertyDescriptor(prototype, "checked");
348
+ if (descriptor?.set) {
349
+ descriptor.set.call(target, true);
350
+ }
351
+ else {
352
+ target.checked = true;
353
+ }
354
+ target.dispatchEvent(new Event("input", { bubbles: true }));
355
+ target.dispatchEvent(new Event("change", { bubbles: true }));
356
+ }, inputType);
357
+ }
358
+ async function readFieldRuntimeState(locator, field) {
359
+ if (field.inputType === "checkbox" || field.inputType === "radio") {
360
+ const checked = await readCheckedState(locator, field.inputType);
361
+ return {
362
+ value: checked ? "checked" : "",
363
+ checked,
364
+ };
365
+ }
366
+ if (field.tag === "select") {
367
+ const value = await locator
368
+ .evaluate((element) => {
369
+ if (!(element instanceof HTMLSelectElement)) {
370
+ return "";
371
+ }
372
+ return (element.selectedOptions[0]?.textContent ||
373
+ element.value ||
374
+ "")
375
+ .replace(/\s+/g, " ")
376
+ .trim();
377
+ })
378
+ .catch(() => "");
379
+ return {
380
+ value: normalizeText(value),
381
+ checked: null,
382
+ };
383
+ }
384
+ const value = await locator.inputValue().catch(async () => {
385
+ return locator
386
+ .evaluate((element) => {
387
+ if (element instanceof HTMLInputElement ||
388
+ element instanceof HTMLTextAreaElement) {
389
+ return element.value;
390
+ }
391
+ return "";
392
+ })
393
+ .catch(() => "");
394
+ });
395
+ return {
396
+ value: normalizeText(value),
397
+ checked: null,
398
+ };
399
+ }
400
+ function didFieldStateChange(before, after) {
401
+ return (normalizeKey(before.value) !== normalizeKey(after.value) ||
402
+ before.checked !== after.checked);
403
+ }
404
+ function doesFieldStateMatchExpected(args) {
405
+ if (args.operation.mode === "check") {
406
+ return args.after.checked === true;
407
+ }
408
+ return (normalizeKey(args.after.value) ===
409
+ normalizeKey(args.operation.expectedValue));
410
+ }
411
+ async function fillFieldValue(args) {
412
+ if (shouldCheckField(args.value) &&
413
+ (args.field.inputType === "checkbox" || args.field.inputType === "radio")) {
414
+ const preparedLocator = await prepareLocatorForInteraction(args.locator).catch(() => args.locator.first());
415
+ const labelLocator = args.locator
416
+ .locator("xpath=ancestor::label[1]")
417
+ .first();
418
+ const preparedLabelLocator = await prepareLocatorForInteraction(labelLocator).catch(() => null);
419
+ if (args.field.checked ||
420
+ (await readCheckedState(preparedLocator, args.field.inputType).catch(() => false))) {
421
+ return {
422
+ expectedValue: "checked",
423
+ mode: "check",
424
+ };
425
+ }
426
+ if (preparedLabelLocator) {
427
+ await preparedLabelLocator.click({ force: true }).catch(() => undefined);
428
+ }
429
+ let checkedAfterAttempt = await readCheckedState(preparedLocator, args.field.inputType).catch(() => false);
430
+ if (!checkedAfterAttempt) {
431
+ await preparedLocator.check({ force: true }).catch(async () => {
432
+ await preparedLocator.click({ force: true }).catch(() => undefined);
433
+ });
434
+ checkedAfterAttempt = await readCheckedState(preparedLocator, args.field.inputType).catch(() => false);
435
+ }
436
+ if (!checkedAfterAttempt && preparedLabelLocator) {
437
+ await preparedLabelLocator.click({ force: true }).catch(() => undefined);
438
+ checkedAfterAttempt = await readCheckedState(preparedLocator, args.field.inputType).catch(() => false);
439
+ }
440
+ if (!checkedAfterAttempt) {
441
+ await setCheckedStateWithDomFallback(preparedLocator, args.field.inputType).catch(() => undefined);
442
+ }
443
+ return {
444
+ expectedValue: "checked",
445
+ mode: "check",
446
+ };
447
+ }
448
+ if (args.field.tag === "select") {
449
+ const preparedLocator = await prepareLocatorForInteraction(args.locator);
450
+ const desired = normalizeText(args.value);
451
+ const matchedOption = findMatchingSelectOption(args.field.options, [
452
+ desired,
453
+ ]);
454
+ try {
455
+ await preparedLocator.selectOption({ label: desired });
456
+ return {
457
+ expectedValue: desired,
458
+ mode: "select",
459
+ };
460
+ }
461
+ catch {
462
+ if (matchedOption) {
463
+ await preparedLocator.selectOption({ label: matchedOption });
464
+ return {
465
+ expectedValue: matchedOption,
466
+ mode: "select",
467
+ };
468
+ }
469
+ }
470
+ }
471
+ const fittedValue = fitValueToField(args.field, args.value);
472
+ // Native date/time inputs require .fill() with ISO format — typeLikeHuman types
473
+ // individual characters which the browser's native picker interprets incorrectly
474
+ // (e.g. "1998-04-17" typed char-by-char becomes "12/09/80417").
475
+ const isNativeDateInput = [
476
+ "date",
477
+ "time",
478
+ "datetime-local",
479
+ "month",
480
+ "week",
481
+ ].includes(args.field.inputType);
482
+ if (isNativeDateInput) {
483
+ const preparedLocator = await prepareLocatorForInteraction(args.locator);
484
+ await preparedLocator.fill(fittedValue);
485
+ return {
486
+ expectedValue: fittedValue,
487
+ mode: "fill",
488
+ };
489
+ }
490
+ await typeLikeHuman(args.locator, fittedValue);
491
+ return {
492
+ expectedValue: fittedValue,
493
+ mode: "fill",
494
+ };
495
+ }
496
+ async function triggerLocatorClick(locator, options = {}) {
497
+ const preparedLocator = await prepareLocatorForInteraction(locator);
498
+ try {
499
+ await preparedLocator.click({
500
+ timeout: 4000,
501
+ ...(options.singleAttempt ? { noWaitAfter: true } : {}),
502
+ });
503
+ return options.singleAttempt
504
+ ? "playwright-click-single-attempt"
505
+ : "playwright-click";
506
+ }
507
+ catch (error) {
508
+ if (options.singleAttempt) {
509
+ throw error;
510
+ }
511
+ try {
512
+ await preparedLocator.click({ force: true, timeout: 4000 });
513
+ return "playwright-force-click";
514
+ }
515
+ catch {
516
+ await preparedLocator.evaluate((element) => {
517
+ if (!(element instanceof HTMLElement)) {
518
+ return;
519
+ }
520
+ element.scrollIntoView({ block: "center", inline: "center" });
521
+ if (element instanceof HTMLButtonElement &&
522
+ element.type === "submit" &&
523
+ element.form) {
524
+ element.form.requestSubmit(element);
525
+ return;
526
+ }
527
+ if (element instanceof HTMLInputElement &&
528
+ element.type === "submit" &&
529
+ element.form) {
530
+ element.form.requestSubmit(element);
531
+ return;
532
+ }
533
+ element.click();
534
+ });
535
+ return "dom-click";
536
+ }
537
+ }
538
+ }
539
+ async function waitForPostActionState(page) {
540
+ await page.waitForLoadState("domcontentloaded").catch(() => undefined);
541
+ await page.waitForTimeout(250);
542
+ return readVisibleState(page);
543
+ }
544
+ function waitForClickDialog(page) {
545
+ return new Promise((resolve) => {
546
+ let settled = false;
547
+ const timeoutId = setTimeout(() => {
548
+ if (settled) {
549
+ return;
550
+ }
551
+ settled = true;
552
+ page.off("dialog", onDialog);
553
+ resolve(null);
554
+ }, 1500);
555
+ const onDialog = (dialog) => {
556
+ if (settled) {
557
+ return;
558
+ }
559
+ settled = true;
560
+ clearTimeout(timeoutId);
561
+ const result = {
562
+ type: dialog.type(),
563
+ message: dialog.message(),
564
+ defaultValue: dialog.defaultValue(),
565
+ action: "accepted"
566
+ };
567
+ dialog.accept().then(() => resolve(result), () => resolve({ ...result, action: "dismissed" }));
568
+ };
569
+ page.once("dialog", onDialog);
570
+ });
571
+ }
572
+ export async function executeDecision(page, decision, preparedClick, options = {}) {
573
+ try {
574
+ if (decision.action === "scroll") {
575
+ await page.mouse.wheel(0, 850);
576
+ const after = await readVisibleState(page);
577
+ return {
578
+ success: true,
579
+ note: "Scrolled down page",
580
+ destinationUrl: after.url,
581
+ destinationTitle: after.title,
582
+ visibleTextSnippet: after.textSnippet,
583
+ };
584
+ }
585
+ if (decision.action === "wait") {
586
+ const before = await readVisibleState(page);
587
+ const startedAt = Date.now();
588
+ const waitContext = `${decision.thought} ${decision.expectation} ${decision.target}`.toLowerCase();
589
+ const waitMs = /\b(?:transaction|tx|sending|pending|broadcast|confirm|confirmation|settle|receipt)\b/.test(waitContext) ? 5000 : 1500;
590
+ await page.waitForTimeout(waitMs);
591
+ const after = await readVisibleState(page);
592
+ const elapsedMs = Date.now() - startedAt;
593
+ const { stateChanged, destinationLabel } = describeStateChange(before, after);
594
+ return {
595
+ success: true,
596
+ note: stateChanged
597
+ ? `Waited ${elapsedMs}ms and the visible page changed to '${destinationLabel}'`
598
+ : `Waited ${elapsedMs}ms with no clear visible page change`,
599
+ elapsedMs,
600
+ destinationUrl: after.url,
601
+ destinationTitle: after.title,
602
+ stateChanged,
603
+ visibleTextSnippet: after.textSnippet,
604
+ };
605
+ }
606
+ if (decision.action === "back") {
607
+ const before = await readVisibleState(page);
608
+ const startedAt = Date.now();
609
+ await page
610
+ .goBack({ waitUntil: "domcontentloaded" })
611
+ .catch(() => undefined);
612
+ const after = await readVisibleState(page);
613
+ const elapsedMs = Date.now() - startedAt;
614
+ const { stateChanged, destinationLabel } = describeStateChange(before, after);
615
+ return {
616
+ success: stateChanged,
617
+ note: stateChanged
618
+ ? `Went back and reached '${destinationLabel}' after ${elapsedMs}ms`
619
+ : `Tried to go back, but the visible page did not clearly change after ${elapsedMs}ms`,
620
+ elapsedMs,
621
+ destinationUrl: after.url,
622
+ destinationTitle: after.title,
623
+ stateChanged,
624
+ visibleTextSnippet: after.textSnippet,
625
+ };
626
+ }
627
+ if (decision.action === "extract") {
628
+ const after = await readVisibleState(page);
629
+ return {
630
+ success: true,
631
+ note: `Recorded page state at '${normalizeText(after.title) || after.url}' without interaction`,
632
+ destinationUrl: after.url,
633
+ destinationTitle: after.title,
634
+ visibleTextSnippet: after.textSnippet,
635
+ };
636
+ }
637
+ if (decision.action === "stop") {
638
+ return {
639
+ success: true,
640
+ stop: true,
641
+ note: "Planner decided to stop due to friction or completion",
642
+ };
643
+ }
644
+ const target = resolveDecisionTargetLabel(decision);
645
+ const targetId = decision.target_id.trim();
646
+ if (!target && !targetId) {
647
+ return {
648
+ success: false,
649
+ note: "Decision required a target but did not provide one",
650
+ };
651
+ }
652
+ if (decision.action === "click") {
653
+ const preparedClickResolution = preparedClick
654
+ ? { preparedClick }
655
+ : await prepareClickDecision(page, decision);
656
+ if (!preparedClickResolution.preparedClick) {
657
+ return {
658
+ success: false,
659
+ note: preparedClickResolution.note ??
660
+ `Could not find clickable element for '${target || targetId}'`,
661
+ };
662
+ }
663
+ const before = await readVisibleState(page);
664
+ const neighborhoodBefore = await captureClickNeighborhood(preparedClickResolution.preparedClick.locator);
665
+ const startedAt = Date.now();
666
+ const dialogResultPromise = waitForClickDialog(page);
667
+ const clickStrategy = await triggerLocatorClick(preparedClickResolution.preparedClick.locator, options.singleClickAttempt ? { singleAttempt: true } : {});
668
+ const dialogResult = await dialogResultPromise;
669
+ let after = await waitForPostActionState(page);
670
+ let elapsedMs = Date.now() - startedAt;
671
+ let { stateChanged, destinationLabel } = describeStateChange(before, after);
672
+ if (!stateChanged) {
673
+ await page.waitForTimeout(1200);
674
+ after = await readVisibleState(page);
675
+ elapsedMs = Date.now() - startedAt;
676
+ ({ stateChanged, destinationLabel } = describeStateChange(before, after));
677
+ }
678
+ if (!stateChanged) {
679
+ const neighborhoodAfter = await captureClickNeighborhood(preparedClickResolution.preparedClick.locator);
680
+ if (neighborhoodBefore &&
681
+ neighborhoodAfter &&
682
+ neighborhoodBefore !== neighborhoodAfter) {
683
+ stateChanged = true;
684
+ destinationLabel = normalizeText(after.title) || after.url;
685
+ }
686
+ }
687
+ const blockedByInterstitial = isInterstitialState(after);
688
+ const dialogNote = dialogResult
689
+ ? ` Accepted ${dialogResult.type} dialog${dialogResult.message ? `: ${truncateLabel(dialogResult.message, 140)}` : ""}.`
690
+ : "";
691
+ if (blockedByInterstitial) {
692
+ return {
693
+ success: false,
694
+ note: `Clicked '${target || targetId}' and hit a security or verification interstitial after ${elapsedMs}ms.${dialogNote}`,
695
+ matchedBy: `${preparedClickResolution.preparedClick.matchedBy}:${clickStrategy}`,
696
+ elapsedMs,
697
+ destinationUrl: after.url,
698
+ destinationTitle: after.title,
699
+ stateChanged,
700
+ visibleTextSnippet: after.textSnippet,
701
+ ...(preparedClickResolution.preparedClick.clickIndicator
702
+ ? {
703
+ clickIndicator: preparedClickResolution.preparedClick.clickIndicator,
704
+ }
705
+ : {}),
706
+ };
707
+ }
708
+ return {
709
+ success: stateChanged || Boolean(dialogResult),
710
+ note: stateChanged
711
+ ? `Clicked '${target || targetId}' and reached '${destinationLabel}' after ${elapsedMs}ms.${dialogNote}`
712
+ : dialogResult
713
+ ? `Clicked '${target || targetId}' and accepted a ${dialogResult.type} dialog after ${elapsedMs}ms${dialogResult.message ? `: ${truncateLabel(dialogResult.message, 140)}` : ""}`
714
+ : `Clicked '${target || targetId}' but the page showed no clear visible change after ${elapsedMs}ms`,
715
+ matchedBy: `${preparedClickResolution.preparedClick.matchedBy}:${clickStrategy}`,
716
+ elapsedMs,
717
+ destinationUrl: after.url,
718
+ destinationTitle: after.title,
719
+ stateChanged: stateChanged || Boolean(dialogResult),
720
+ visibleTextSnippet: after.textSnippet,
721
+ ...(preparedClickResolution.preparedClick.clickIndicator
722
+ ? {
723
+ clickIndicator: preparedClickResolution.preparedClick.clickIndicator,
724
+ }
725
+ : {}),
726
+ };
727
+ }
728
+ if (decision.action === "type") {
729
+ const fields = await collectVisibleFillableFields(page);
730
+ const matchedField = targetId
731
+ ? (fields.find((field) => field.agentId === targetId) ?? null)
732
+ : findBestFillableField(fields, target);
733
+ if (!matchedField) {
734
+ return {
735
+ success: false,
736
+ note: `Could not find input for '${target || targetId}'`,
737
+ };
738
+ }
739
+ const resolvedField = await resolveFillableFieldLocator(page, matchedField);
740
+ const locator = resolvedField.locator;
741
+ const beforeFieldState = await readFieldRuntimeState(locator, matchedField).catch(() => ({
742
+ value: "",
743
+ checked: matchedField.checked ?? null,
744
+ }));
745
+ const operation = await fillFieldValue({
746
+ locator,
747
+ field: matchedField,
748
+ value: decision.text || "",
749
+ });
750
+ const afterFieldState = await readFieldRuntimeState(locator, matchedField).catch(() => beforeFieldState);
751
+ const stateChanged = didFieldStateChange(beforeFieldState, afterFieldState);
752
+ const valueApplied = doesFieldStateMatchExpected({
753
+ after: afterFieldState,
754
+ operation,
755
+ });
756
+ const after = await readVisibleState(page);
757
+ if (!valueApplied) {
758
+ return {
759
+ success: false,
760
+ note: `Tried to fill '${target || targetId}', but the field did not keep the requested value`,
761
+ matchedBy: `target_id:${matchedField.tag}/${matchedField.inputType}:${resolvedField.strategy}`,
762
+ destinationUrl: after.url,
763
+ destinationTitle: after.title,
764
+ stateChanged,
765
+ visibleTextSnippet: after.textSnippet,
766
+ };
767
+ }
768
+ return {
769
+ success: true,
770
+ note: stateChanged
771
+ ? `Filled '${target || targetId}'`
772
+ : `Field '${target || targetId}' already held the requested value`,
773
+ matchedBy: `target_id:${matchedField.tag}/${matchedField.inputType}:${resolvedField.strategy}`,
774
+ destinationUrl: after.url,
775
+ destinationTitle: after.title,
776
+ stateChanged,
777
+ visibleTextSnippet: after.textSnippet,
778
+ };
779
+ }
780
+ return { success: false, note: `Unsupported action '${decision.action}'` };
781
+ }
782
+ catch (error) {
783
+ return {
784
+ success: false,
785
+ note: `Action failed: ${cleanErrorMessage(error)}`,
786
+ };
787
+ }
788
+ }