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.
- package/README.md +689 -0
- package/dist/auth/credentialStore.js +62 -0
- package/dist/auth/inbox.js +193 -0
- package/dist/auth/profile.js +379 -0
- package/dist/auth/runner.js +1124 -0
- package/dist/backend/dashboardData.js +194 -0
- package/dist/backend/runArtifacts.js +48 -0
- package/dist/backend/runRepository.js +93 -0
- package/dist/bin.js +2 -0
- package/dist/cli/backfillSiteChecks.js +143 -0
- package/dist/cli/run.js +309 -0
- package/dist/cli/trade.js +69 -0
- package/dist/config.js +199 -0
- package/dist/core/agentProfiles.js +55 -0
- package/dist/core/aggregateReport.js +382 -0
- package/dist/core/audit.js +30 -0
- package/dist/core/customTaskSuite.js +148 -0
- package/dist/core/evaluator.js +217 -0
- package/dist/core/executor.js +788 -0
- package/dist/core/fallbackReport.js +335 -0
- package/dist/core/formHeuristics.js +411 -0
- package/dist/core/gameplaySummary.js +164 -0
- package/dist/core/interaction.js +202 -0
- package/dist/core/pageState.js +201 -0
- package/dist/core/planner.js +1669 -0
- package/dist/core/processSubmissionBatch.js +204 -0
- package/dist/core/runAuditJob.js +170 -0
- package/dist/core/runner.js +2352 -0
- package/dist/core/siteBrief.js +107 -0
- package/dist/core/siteChecks.js +1526 -0
- package/dist/core/taskDirectives.js +279 -0
- package/dist/core/taskHeuristics.js +263 -0
- package/dist/dashboard/client.js +1256 -0
- package/dist/dashboard/contracts.js +95 -0
- package/dist/dashboard/narrative.js +277 -0
- package/dist/dashboard/server.js +458 -0
- package/dist/dashboard/theme.js +888 -0
- package/dist/index.js +84 -0
- package/dist/llm/client.js +188 -0
- package/dist/paystack/account.js +123 -0
- package/dist/paystack/client.js +100 -0
- package/dist/paystack/index.js +13 -0
- package/dist/paystack/test-paystack.js +83 -0
- package/dist/paystack/transfer.js +138 -0
- package/dist/paystack/types.js +74 -0
- package/dist/paystack/webhook.js +121 -0
- package/dist/prompts/browserAgent.js +124 -0
- package/dist/prompts/reviewer.js +71 -0
- package/dist/reporting/clickReplay.js +290 -0
- package/dist/reporting/html.js +930 -0
- package/dist/reporting/markdown.js +238 -0
- package/dist/reporting/template.js +1141 -0
- package/dist/schemas/types.js +361 -0
- package/dist/submissions/customTasks.js +196 -0
- package/dist/submissions/html.js +770 -0
- package/dist/submissions/model.js +56 -0
- package/dist/submissions/publicUrl.js +76 -0
- package/dist/submissions/service.js +74 -0
- package/dist/submissions/store.js +37 -0
- package/dist/submissions/types.js +65 -0
- package/dist/trade/engine.js +241 -0
- package/dist/trade/evm/erc20.js +44 -0
- package/dist/trade/extractor.js +148 -0
- package/dist/trade/policy.js +35 -0
- package/dist/trade/session.js +31 -0
- package/dist/trade/types.js +107 -0
- package/dist/trade/validator.js +148 -0
- package/dist/utils/files.js +59 -0
- package/dist/utils/log.js +24 -0
- package/dist/utils/playwrightCompat.js +14 -0
- package/dist/utils/time.js +3 -0
- package/dist/wallet/provider.js +345 -0
- package/dist/wallet/relay.js +129 -0
- package/dist/wallet/wallet.js +178 -0
- package/docs/01-installation.md +134 -0
- package/docs/02-running-your-first-audit.md +136 -0
- package/docs/03-configuration.md +233 -0
- package/docs/04-how-the-agent-thinks.md +41 -0
- package/docs/05-extending-personas-and-tasks.md +42 -0
- package/docs/06-hardening-for-production.md +92 -0
- package/package.json +60 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
export const INTERACTION_VISIBLE_TIMEOUT_MS = 5000;
|
|
2
|
+
const HUMAN_TYPING_DELAY_MS = 30;
|
|
3
|
+
function normalizeInteractionText(value) {
|
|
4
|
+
return value.replace(/\s+/g, " ").trim();
|
|
5
|
+
}
|
|
6
|
+
function normalizeInteractionKey(value) {
|
|
7
|
+
return normalizeInteractionText(value).toLowerCase();
|
|
8
|
+
}
|
|
9
|
+
function escapeRegExp(value) {
|
|
10
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
11
|
+
}
|
|
12
|
+
function escapeAttributeValue(value) {
|
|
13
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
14
|
+
}
|
|
15
|
+
function buildPattern(value, mode) {
|
|
16
|
+
const normalized = normalizeInteractionText(value);
|
|
17
|
+
if (!normalized) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
const flexible = escapeRegExp(normalized).replace(/\s+/g, "\\s+");
|
|
21
|
+
if (mode === "exact") {
|
|
22
|
+
return new RegExp(`^\\s*${flexible}\\s*(?:[:*]\\s*)?$`, "i");
|
|
23
|
+
}
|
|
24
|
+
return new RegExp(flexible, "i");
|
|
25
|
+
}
|
|
26
|
+
function buildUniquePatterns(values, mode) {
|
|
27
|
+
const seen = new Set();
|
|
28
|
+
const patterns = [];
|
|
29
|
+
for (const value of values) {
|
|
30
|
+
const pattern = buildPattern(value, mode);
|
|
31
|
+
if (!pattern) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const key = `${pattern.source}/${pattern.flags}`;
|
|
35
|
+
if (seen.has(key)) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
seen.add(key);
|
|
39
|
+
patterns.push(pattern);
|
|
40
|
+
}
|
|
41
|
+
return patterns;
|
|
42
|
+
}
|
|
43
|
+
function resolveFieldRoles(field) {
|
|
44
|
+
if (field.tag === "textarea") {
|
|
45
|
+
return ["textbox"];
|
|
46
|
+
}
|
|
47
|
+
if (field.tag === "select") {
|
|
48
|
+
return ["combobox", "listbox"];
|
|
49
|
+
}
|
|
50
|
+
switch (field.inputType) {
|
|
51
|
+
case "checkbox":
|
|
52
|
+
return ["checkbox"];
|
|
53
|
+
case "radio":
|
|
54
|
+
return ["radio"];
|
|
55
|
+
case "number":
|
|
56
|
+
return ["spinbutton"];
|
|
57
|
+
case "password":
|
|
58
|
+
return [];
|
|
59
|
+
default:
|
|
60
|
+
return ["textbox"];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async function firstVisibleCandidate(candidates) {
|
|
64
|
+
for (const candidate of candidates) {
|
|
65
|
+
const locator = candidate.locator.first();
|
|
66
|
+
try {
|
|
67
|
+
if (await locator.isVisible({ timeout: 400 })) {
|
|
68
|
+
return {
|
|
69
|
+
locator,
|
|
70
|
+
strategy: candidate.strategy
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// continue
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
export function buildLooseAccessiblePattern(value) {
|
|
81
|
+
return buildPattern(value, "loose");
|
|
82
|
+
}
|
|
83
|
+
export async function waitForVisible(locator, timeout = INTERACTION_VISIBLE_TIMEOUT_MS) {
|
|
84
|
+
const resolved = locator.first();
|
|
85
|
+
await resolved.waitFor({ state: "visible", timeout });
|
|
86
|
+
return resolved;
|
|
87
|
+
}
|
|
88
|
+
export async function prepareLocatorForInteraction(locator) {
|
|
89
|
+
const resolved = await waitForVisible(locator);
|
|
90
|
+
await resolved.scrollIntoViewIfNeeded().catch(() => undefined);
|
|
91
|
+
return resolved;
|
|
92
|
+
}
|
|
93
|
+
export async function resolvePreferredFieldLocator(args) {
|
|
94
|
+
const exactNamePatterns = buildUniquePatterns([args.field.label], "exact");
|
|
95
|
+
const looseNamePatterns = buildUniquePatterns([...(args.preferredNames ?? []), args.field.label], "loose");
|
|
96
|
+
const exactPlaceholderPatterns = buildUniquePatterns([args.field.placeholder], "exact");
|
|
97
|
+
const loosePlaceholderPatterns = buildUniquePatterns([args.field.placeholder, ...(args.preferredNames ?? [])], "loose");
|
|
98
|
+
const roles = resolveFieldRoles(args.field);
|
|
99
|
+
const candidates = [];
|
|
100
|
+
for (const pattern of exactNamePatterns) {
|
|
101
|
+
for (const role of roles) {
|
|
102
|
+
candidates.push({
|
|
103
|
+
locator: args.page.getByRole(role, { name: pattern }),
|
|
104
|
+
strategy: `getByRole(${role})`
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
candidates.push({
|
|
108
|
+
locator: args.page.getByLabel(pattern),
|
|
109
|
+
strategy: "getByLabel"
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
for (const pattern of exactPlaceholderPatterns) {
|
|
113
|
+
candidates.push({
|
|
114
|
+
locator: args.page.getByPlaceholder(pattern),
|
|
115
|
+
strategy: "getByPlaceholder"
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
for (const pattern of looseNamePatterns) {
|
|
119
|
+
for (const role of roles) {
|
|
120
|
+
candidates.push({
|
|
121
|
+
locator: args.page.getByRole(role, { name: pattern }),
|
|
122
|
+
strategy: `getByRole(${role})`
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
candidates.push({
|
|
126
|
+
locator: args.page.getByLabel(pattern),
|
|
127
|
+
strategy: "getByLabel"
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
for (const pattern of loosePlaceholderPatterns) {
|
|
131
|
+
candidates.push({
|
|
132
|
+
locator: args.page.getByPlaceholder(pattern),
|
|
133
|
+
strategy: "getByPlaceholder"
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
if (normalizeInteractionText(args.field.name)) {
|
|
137
|
+
candidates.push({
|
|
138
|
+
locator: args.page.locator(`[name="${escapeAttributeValue(args.field.name)}"]`),
|
|
139
|
+
strategy: "name attribute"
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
if (normalizeInteractionText(args.field.id)) {
|
|
143
|
+
candidates.push({
|
|
144
|
+
locator: args.page.locator(`[id="${escapeAttributeValue(args.field.id)}"]`),
|
|
145
|
+
strategy: "id attribute"
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
candidates.push({
|
|
149
|
+
locator: args.fallbackLocator.first(),
|
|
150
|
+
strategy: "marker fallback"
|
|
151
|
+
});
|
|
152
|
+
return (await firstVisibleCandidate(candidates)) ?? {
|
|
153
|
+
locator: args.fallbackLocator.first(),
|
|
154
|
+
strategy: "marker fallback"
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
async function readLocatorValue(locator) {
|
|
158
|
+
return locator
|
|
159
|
+
.inputValue()
|
|
160
|
+
.catch(async () => {
|
|
161
|
+
return locator
|
|
162
|
+
.evaluate((element) => {
|
|
163
|
+
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
|
|
164
|
+
return element.value;
|
|
165
|
+
}
|
|
166
|
+
return "";
|
|
167
|
+
})
|
|
168
|
+
.catch(() => "");
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
async function clearFieldValue(locator) {
|
|
172
|
+
await locator.click({ timeout: 2000 }).catch(async () => {
|
|
173
|
+
await locator.focus().catch(() => undefined);
|
|
174
|
+
});
|
|
175
|
+
await locator.selectText().catch(() => undefined);
|
|
176
|
+
const modifier = process.platform === "darwin" ? "Meta" : "Control";
|
|
177
|
+
await locator.press(`${modifier}+A`).catch(() => undefined);
|
|
178
|
+
await locator.press("Delete").catch(() => undefined);
|
|
179
|
+
await locator.press("Backspace").catch(() => undefined);
|
|
180
|
+
const remainingValue = normalizeInteractionKey(await readLocatorValue(locator));
|
|
181
|
+
if (!remainingValue) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
// Fall back to a DOM-level clear only when keyboard selection did not take.
|
|
185
|
+
await locator
|
|
186
|
+
.evaluate((element) => {
|
|
187
|
+
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
|
|
188
|
+
element.value = "";
|
|
189
|
+
element.dispatchEvent(new Event("input", { bubbles: true }));
|
|
190
|
+
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
191
|
+
}
|
|
192
|
+
})
|
|
193
|
+
.catch(() => undefined);
|
|
194
|
+
}
|
|
195
|
+
export async function typeLikeHuman(locator, value) {
|
|
196
|
+
const prepared = await prepareLocatorForInteraction(locator);
|
|
197
|
+
await clearFieldValue(prepared);
|
|
198
|
+
if (!value) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
await prepared.pressSequentially(value, { delay: HUMAN_TYPING_DELAY_MS });
|
|
202
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { PageStateSchema } from "../schemas/types.js";
|
|
2
|
+
export async function capturePageState(page) {
|
|
3
|
+
const title = await page.title().catch(() => "");
|
|
4
|
+
const url = page.url();
|
|
5
|
+
const rawVisibleText = await page.locator("body").innerText().catch(() => "");
|
|
6
|
+
const visibleText = rawVisibleText.replace(/\s+/g, " ").slice(0, 4200);
|
|
7
|
+
const visibleLines = rawVisibleText
|
|
8
|
+
.split(/\r?\n/)
|
|
9
|
+
.map((line) => line.replace(/\s+/g, " ").trim())
|
|
10
|
+
.filter(Boolean)
|
|
11
|
+
.slice(0, 140);
|
|
12
|
+
const snapshot = await page.evaluate(() => {
|
|
13
|
+
let nextAgentId = 1;
|
|
14
|
+
const assignAgentId = (element) => {
|
|
15
|
+
const existing = (element.getAttribute("data-site-agent-id") || "").trim();
|
|
16
|
+
if (existing) {
|
|
17
|
+
const existingNumber = Number(existing);
|
|
18
|
+
if (Number.isFinite(existingNumber) && existingNumber >= nextAgentId) {
|
|
19
|
+
nextAgentId = existingNumber + 1;
|
|
20
|
+
}
|
|
21
|
+
return existing;
|
|
22
|
+
}
|
|
23
|
+
const agentId = String(nextAgentId++);
|
|
24
|
+
element.setAttribute("data-site-agent-id", agentId);
|
|
25
|
+
return agentId;
|
|
26
|
+
};
|
|
27
|
+
const nodes = Array.from(document.querySelectorAll("a, button, input, textarea, select, summary, [role='button'], [role='link'], [role='tab'], [role='menuitem'], [role='textbox']"));
|
|
28
|
+
const interactive = nodes
|
|
29
|
+
.map((element) => {
|
|
30
|
+
const rect = element.getBoundingClientRect();
|
|
31
|
+
const style = window.getComputedStyle(element);
|
|
32
|
+
const visible = rect.width > 0 &&
|
|
33
|
+
rect.height > 0 &&
|
|
34
|
+
style.visibility !== "hidden" &&
|
|
35
|
+
style.display !== "none";
|
|
36
|
+
if (!visible) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const buttonValueText = element instanceof HTMLInputElement && ["submit", "button", "reset"].includes((element.type || "").toLowerCase())
|
|
40
|
+
? element.value || ""
|
|
41
|
+
: "";
|
|
42
|
+
const text = (element.innerText ||
|
|
43
|
+
element.getAttribute("aria-label") ||
|
|
44
|
+
buttonValueText ||
|
|
45
|
+
element.getAttribute("placeholder") ||
|
|
46
|
+
element.getAttribute("title") ||
|
|
47
|
+
"")
|
|
48
|
+
.trim()
|
|
49
|
+
.replace(/\s+/g, " ")
|
|
50
|
+
.slice(0, 120);
|
|
51
|
+
const role = element.getAttribute("role") || element.tagName.toLowerCase();
|
|
52
|
+
const tag = element.tagName.toLowerCase();
|
|
53
|
+
const type = element.getAttribute("type") || undefined;
|
|
54
|
+
const href = element instanceof HTMLAnchorElement ? element.href : undefined;
|
|
55
|
+
const disabled = element.hasAttribute("disabled") || element.getAttribute("aria-disabled") === "true";
|
|
56
|
+
if (!text) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const agentId = assignAgentId(element);
|
|
60
|
+
return {
|
|
61
|
+
agentId,
|
|
62
|
+
role,
|
|
63
|
+
tag,
|
|
64
|
+
type,
|
|
65
|
+
text,
|
|
66
|
+
href,
|
|
67
|
+
disabled
|
|
68
|
+
};
|
|
69
|
+
})
|
|
70
|
+
.filter(Boolean)
|
|
71
|
+
.slice(0, 100);
|
|
72
|
+
const formFields = Array.from(document.querySelectorAll("input, textarea, select"))
|
|
73
|
+
.map((element) => {
|
|
74
|
+
const rect = element.getBoundingClientRect();
|
|
75
|
+
const style = window.getComputedStyle(element);
|
|
76
|
+
const inputType = element instanceof HTMLInputElement ? (element.type || "text").replace(/\s+/g, " ").trim().toLowerCase() : "";
|
|
77
|
+
if (rect.width <= 0 ||
|
|
78
|
+
rect.height <= 0 ||
|
|
79
|
+
style.visibility === "hidden" ||
|
|
80
|
+
style.display === "none" ||
|
|
81
|
+
element.disabled ||
|
|
82
|
+
inputType === "hidden" ||
|
|
83
|
+
inputType === "submit" ||
|
|
84
|
+
inputType === "button" ||
|
|
85
|
+
inputType === "image") {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
const labelParts = [];
|
|
89
|
+
if ("labels" in element && element.labels) {
|
|
90
|
+
for (const label of Array.from(element.labels)) {
|
|
91
|
+
labelParts.push((label.innerText || label.textContent || "").replace(/\s+/g, " ").trim());
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const ariaLabelledBy = element.getAttribute("aria-labelledby");
|
|
95
|
+
if (ariaLabelledBy) {
|
|
96
|
+
for (const id of ariaLabelledBy.split(/\s+/)) {
|
|
97
|
+
const labelElement = document.getElementById(id);
|
|
98
|
+
if (labelElement) {
|
|
99
|
+
labelParts.push((labelElement.textContent || "").replace(/\s+/g, " ").trim());
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const closestLabel = element.closest("label");
|
|
104
|
+
if (closestLabel) {
|
|
105
|
+
labelParts.push((closestLabel.innerText || closestLabel.textContent || "").replace(/\s+/g, " ").trim());
|
|
106
|
+
}
|
|
107
|
+
const ariaLabel = (element.getAttribute("aria-label") || "").replace(/\s+/g, " ").trim();
|
|
108
|
+
if (ariaLabel) {
|
|
109
|
+
labelParts.push(ariaLabel);
|
|
110
|
+
}
|
|
111
|
+
// If still no labels, look for a preceding text-heavy sibling or parent text
|
|
112
|
+
if (labelParts.filter(Boolean).length === 0) {
|
|
113
|
+
let prev = element.previousElementSibling;
|
|
114
|
+
while (prev) {
|
|
115
|
+
const text = (prev.textContent || "").replace(/\s+/g, " ").trim();
|
|
116
|
+
if (text.length > 1 && text.length < 60) {
|
|
117
|
+
labelParts.push(text);
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
prev = prev.previousElementSibling;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const options = element instanceof HTMLSelectElement
|
|
124
|
+
? Array.from(element.options)
|
|
125
|
+
.map((option) => (option.textContent || "").replace(/\s+/g, " ").trim())
|
|
126
|
+
.filter(Boolean)
|
|
127
|
+
.slice(0, 50)
|
|
128
|
+
: [];
|
|
129
|
+
const agentId = assignAgentId(element);
|
|
130
|
+
return {
|
|
131
|
+
agentId,
|
|
132
|
+
label: labelParts.filter(Boolean).join(" ").replace(/\s+/g, " ").trim(),
|
|
133
|
+
placeholder: (element.getAttribute("placeholder") || "").replace(/\s+/g, " ").trim(),
|
|
134
|
+
name: (element.getAttribute("name") || "").replace(/\s+/g, " ").trim(),
|
|
135
|
+
id: (element.id || "").replace(/\s+/g, " ").trim(),
|
|
136
|
+
tag: element.tagName.toLowerCase(),
|
|
137
|
+
inputType: inputType || element.tagName.toLowerCase(),
|
|
138
|
+
autocomplete: (element.getAttribute("autocomplete") || "").replace(/\s+/g, " ").trim().toLowerCase(),
|
|
139
|
+
inputMode: (element.getAttribute("inputmode") || "").replace(/\s+/g, " ").trim().toLowerCase(),
|
|
140
|
+
value: element instanceof HTMLSelectElement
|
|
141
|
+
? (element.selectedOptions[0]?.textContent || "").replace(/\s+/g, " ").trim()
|
|
142
|
+
: (element.value || "").replace(/\s+/g, " ").trim(),
|
|
143
|
+
required: element.required || element.getAttribute("aria-required") === "true",
|
|
144
|
+
checked: element instanceof HTMLInputElement ? element.checked : undefined,
|
|
145
|
+
maxLength: element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement
|
|
146
|
+
? element.maxLength > 0
|
|
147
|
+
? element.maxLength
|
|
148
|
+
: null
|
|
149
|
+
: null,
|
|
150
|
+
options
|
|
151
|
+
};
|
|
152
|
+
})
|
|
153
|
+
.filter(Boolean)
|
|
154
|
+
.slice(0, 50);
|
|
155
|
+
const numberedElements = [];
|
|
156
|
+
const seenAgentIds = new Set();
|
|
157
|
+
for (const item of interactive) {
|
|
158
|
+
if (!item) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (seenAgentIds.has(item.agentId)) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
seenAgentIds.add(item.agentId);
|
|
165
|
+
numberedElements.push(`[${item.agentId}] ${item.role || item.tag}: "${item.text}"`);
|
|
166
|
+
}
|
|
167
|
+
for (const field of formFields) {
|
|
168
|
+
if (!field) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (seenAgentIds.has(field.agentId)) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
seenAgentIds.add(field.agentId);
|
|
175
|
+
const label = field.label || field.placeholder || field.name || field.id || field.inputType || field.tag;
|
|
176
|
+
numberedElements.push(`[${field.agentId}] field: "${label}"`);
|
|
177
|
+
}
|
|
178
|
+
const headings = Array.from(document.querySelectorAll("h1, h2, h3"))
|
|
179
|
+
.map((heading) => heading.textContent?.trim() || "")
|
|
180
|
+
.filter(Boolean)
|
|
181
|
+
.slice(0, 16);
|
|
182
|
+
const formsPresent = document.querySelectorAll("form").length > 0;
|
|
183
|
+
const modalHints = Array.from(document.querySelectorAll("dialog, [role='dialog'], .modal, [aria-modal='true']"))
|
|
184
|
+
.map((element) => element.textContent?.trim()?.slice(0, 140) || "")
|
|
185
|
+
.filter(Boolean)
|
|
186
|
+
.slice(0, 4);
|
|
187
|
+
return { interactive, formFields, numberedElements, headings, formsPresent, modalHints };
|
|
188
|
+
});
|
|
189
|
+
return PageStateSchema.parse({
|
|
190
|
+
title,
|
|
191
|
+
url,
|
|
192
|
+
visibleText,
|
|
193
|
+
visibleLines,
|
|
194
|
+
formFields: snapshot.formFields,
|
|
195
|
+
interactive: snapshot.interactive,
|
|
196
|
+
numberedElements: snapshot.numberedElements,
|
|
197
|
+
headings: snapshot.headings,
|
|
198
|
+
formsPresent: snapshot.formsPresent,
|
|
199
|
+
modalHints: snapshot.modalHints
|
|
200
|
+
});
|
|
201
|
+
}
|