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,335 @@
1
+ import { deriveGameplaySummary } from "./gameplaySummary.js";
2
+ const INTERSTITIAL_PATTERNS = [
3
+ /just a moment/i,
4
+ /verification successful/i,
5
+ /checking your browser/i,
6
+ /cloudflare/i,
7
+ /security check/i,
8
+ /access denied/i,
9
+ /captcha/i,
10
+ /human verification/i
11
+ ];
12
+ const TIME_LIMIT_PATTERNS = [
13
+ /remaining session time/i,
14
+ /execution budget/i,
15
+ /time limit/i,
16
+ /ran out of time/i,
17
+ /too short for another meaningful interaction/i
18
+ ];
19
+ function clampScore(value) {
20
+ return Math.min(10, Math.max(1, Math.round(value)));
21
+ }
22
+ function normalizeText(value) {
23
+ return value.replace(/\s+/g, " ").trim();
24
+ }
25
+ function uniqueItems(items, limit) {
26
+ return [...new Set(items.map((item) => normalizeText(item)).filter(Boolean))].slice(0, limit);
27
+ }
28
+ function formatTaskStatus(status) {
29
+ switch (status) {
30
+ case "success":
31
+ return "succeeded";
32
+ case "partial_success":
33
+ return "partially succeeded";
34
+ case "failed":
35
+ default:
36
+ return "failed";
37
+ }
38
+ }
39
+ function buildTaskOutcomeSummary(taskResults) {
40
+ if (taskResults.length === 0) {
41
+ return "";
42
+ }
43
+ const successCount = taskResults.filter((task) => task.status === "success").length;
44
+ const partialCount = taskResults.filter((task) => task.status === "partial_success").length;
45
+ const failedCount = taskResults.filter((task) => task.status === "failed").length;
46
+ const perTask = taskResults
47
+ .slice(0, 5)
48
+ .map((task) => `${task.name} ${formatTaskStatus(task.status)}`)
49
+ .join("; ");
50
+ return ensureSentence(`Accepted task outcomes: ${successCount} succeeded, ${partialCount} partially succeeded, and ${failedCount} failed.${perTask ? ` Per task: ${perTask}.` : ""}`);
51
+ }
52
+ function ensureSentence(value) {
53
+ const trimmed = normalizeText(value);
54
+ if (!trimmed) {
55
+ return "";
56
+ }
57
+ return /[.!?]$/.test(trimmed) ? trimmed : `${trimmed}.`;
58
+ }
59
+ function readSiteLabel(baseUrl) {
60
+ try {
61
+ return new URL(baseUrl).hostname.replace(/^www\./, "") || baseUrl;
62
+ }
63
+ catch {
64
+ return baseUrl;
65
+ }
66
+ }
67
+ function stripMilliseconds(value) {
68
+ return value.replace(/ after \d+ms/gi, "");
69
+ }
70
+ function isInternalToolingText(value) {
71
+ return /could not find clickable element for 'https?:\/\//i.test(value) ||
72
+ /model evaluator/i.test(value) ||
73
+ /planner request did not finish/i.test(value) ||
74
+ /storage state/i.test(value);
75
+ }
76
+ function taskTextBlob(task) {
77
+ return [
78
+ task.name,
79
+ task.reason,
80
+ task.finalTitle,
81
+ task.finalUrl,
82
+ ...task.history.flatMap((entry) => [
83
+ entry.title,
84
+ entry.url,
85
+ entry.decision.target,
86
+ entry.result.note,
87
+ entry.result.visibleTextSnippet ?? "",
88
+ entry.result.destinationTitle ?? "",
89
+ entry.result.destinationUrl ?? ""
90
+ ])
91
+ ]
92
+ .filter(Boolean)
93
+ .join(" ");
94
+ }
95
+ function isInterstitialLimited(task) {
96
+ return INTERSTITIAL_PATTERNS.some((pattern) => pattern.test(taskTextBlob(task)));
97
+ }
98
+ function isTimeLimited(task) {
99
+ return TIME_LIMIT_PATTERNS.some((pattern) => pattern.test(taskTextBlob(task)));
100
+ }
101
+ function observationFromEntry(entry) {
102
+ const note = normalizeText(entry.result.note);
103
+ const target = normalizeText(entry.decision.target);
104
+ const targetLabel = target || "that path";
105
+ if (!note || isInternalToolingText(note)) {
106
+ return null;
107
+ }
108
+ const successMatch = note.match(/Clicked '([^']+)' and reached '([^']+)'(?: after (\d+)ms)?/i);
109
+ if (successMatch) {
110
+ const clickedLabel = successMatch[1] ?? targetLabel;
111
+ const destination = successMatch[2] ?? entry.result.destinationTitle ?? entry.result.destinationUrl ?? "the next page";
112
+ const elapsed = successMatch[3] ? ` in ${successMatch[3]}ms` : "";
113
+ return {
114
+ tone: "positive",
115
+ priority: 70,
116
+ text: `Clicking "${clickedLabel}" opened "${destination}"${elapsed}.`
117
+ };
118
+ }
119
+ const noChangeMatch = note.match(/Clicked '([^']+)' but the page showed no clear visible change(?: after (\d+)ms)?/i);
120
+ if (noChangeMatch) {
121
+ const clickedLabel = noChangeMatch[1] ?? targetLabel;
122
+ const elapsed = noChangeMatch[2] ? ` after ${noChangeMatch[2]}ms` : "";
123
+ return {
124
+ tone: "negative",
125
+ priority: 100,
126
+ text: `Clicking "${clickedLabel}" did not produce a clear visible change${elapsed}.`,
127
+ fix: `Make the "${clickedLabel}" control lead to a clear destination or give obvious feedback that the click worked.`
128
+ };
129
+ }
130
+ if (/security or verification interstitial/i.test(note)) {
131
+ return {
132
+ tone: "limitation",
133
+ priority: 95,
134
+ text: target
135
+ ? `A security or verification interstitial blocked the path after clicking "${target}".`
136
+ : "A security or verification interstitial blocked a visible navigation path.",
137
+ fix: "Reduce how often verification walls interrupt normal first-visit navigation, or provide a trusted QA lane."
138
+ };
139
+ }
140
+ if (/Stopped after repeated unchanged page states with no meaningful progress/i.test(note)) {
141
+ return {
142
+ tone: "negative",
143
+ priority: 90,
144
+ text: "After several attempts, the page stayed visually unchanged and the path stalled.",
145
+ fix: "Make high-value navigation paths produce clearer state changes and stronger feedback when the interface is waiting or blocked."
146
+ };
147
+ }
148
+ if (/Waited \d+ms with no clear visible page change/i.test(note)) {
149
+ return {
150
+ tone: "negative",
151
+ priority: 45,
152
+ text: "Waiting did not reveal any clearer next step or visible page change."
153
+ };
154
+ }
155
+ if (/Tried to go back, but the visible page did not clearly change/i.test(note)) {
156
+ return {
157
+ tone: "negative",
158
+ priority: 55,
159
+ text: "Back navigation did not return to a clearly different visible state.",
160
+ fix: "Make browser back behavior and in-product recovery paths more predictable."
161
+ };
162
+ }
163
+ if (/Could not find clickable element for '([^']+)'/i.test(note)) {
164
+ const match = note.match(/Could not find clickable element for '([^']+)'/i);
165
+ const clickedLabel = normalizeText(match?.[1] ?? target);
166
+ if (!clickedLabel || /^https?:\/\//i.test(clickedLabel)) {
167
+ return null;
168
+ }
169
+ return {
170
+ tone: "negative",
171
+ priority: 75,
172
+ text: `The interface did not expose "${clickedLabel}" as a clear visible click target when I tried to use it.`,
173
+ fix: `Make "${clickedLabel}" visually obvious and reliably clickable as a normal first-visit control.`
174
+ };
175
+ }
176
+ if (/Action failed:/i.test(note)) {
177
+ const fix = target ? `Stabilize the "${target}" interaction so it completes reliably.` : undefined;
178
+ return {
179
+ tone: "negative",
180
+ priority: 70,
181
+ text: ensureSentence(stripMilliseconds(note.replace(/^Action failed:\s*/i, ""))),
182
+ ...(fix ? { fix } : {})
183
+ };
184
+ }
185
+ if (entry.result.success && entry.result.stateChanged) {
186
+ const destination = entry.result.destinationTitle ?? entry.result.destinationUrl ?? "the next visible state";
187
+ return {
188
+ tone: "positive",
189
+ priority: 40,
190
+ text: target
191
+ ? `Using "${target}" led to "${destination}".`
192
+ : `One of the visible paths did lead to "${destination}".`
193
+ };
194
+ }
195
+ return null;
196
+ }
197
+ function collectTaskObservations(task) {
198
+ const observations = task.history
199
+ .map((entry) => observationFromEntry(entry))
200
+ .filter((value) => Boolean(value));
201
+ if (observations.length > 0) {
202
+ return observations;
203
+ }
204
+ if (!isInternalToolingText(task.reason)) {
205
+ return [
206
+ {
207
+ tone: task.status === "success" ? "positive" : "negative",
208
+ priority: task.status === "success" ? 30 : 60,
209
+ text: ensureSentence(stripMilliseconds(task.reason))
210
+ }
211
+ ];
212
+ }
213
+ return [];
214
+ }
215
+ function extractTaskEvidence(task) {
216
+ const observationEvidence = collectTaskObservations(task)
217
+ .sort((left, right) => right.priority - left.priority)
218
+ .map((item) => item.text);
219
+ const rawHistoryEvidence = task.history
220
+ .map((entry) => {
221
+ const target = normalizeText(entry.decision.target);
222
+ const prefix = target ? `${entry.decision.action} "${target}"` : entry.decision.action;
223
+ const visibleSnippet = entry.result.visibleTextSnippet ? ` Visible text: ${entry.result.visibleTextSnippet.slice(0, 160)}.` : "";
224
+ return `Step ${entry.step}: ${prefix} -> ${stripMilliseconds(entry.result.note)}${visibleSnippet}`;
225
+ });
226
+ return uniqueItems([
227
+ ...(isInternalToolingText(task.reason) ? [] : [task.reason]),
228
+ ...observationEvidence,
229
+ ...rawHistoryEvidence
230
+ ], 5);
231
+ }
232
+ export function buildFallbackReport(args) {
233
+ const gameplaySummary = deriveGameplaySummary({
234
+ suite: args.suite,
235
+ taskResults: args.taskResults
236
+ });
237
+ const totalTasks = Math.max(1, args.taskResults.length);
238
+ const successCount = args.taskResults.filter((task) => task.status === "success").length;
239
+ const partialCount = args.taskResults.filter((task) => task.status === "partial_success").length;
240
+ const failedCount = args.taskResults.filter((task) => task.status === "failed").length;
241
+ const interstitialLimitedTasks = args.taskResults.filter((task) => isInterstitialLimited(task));
242
+ const timeLimitedTasks = args.taskResults.filter((task) => isTimeLimited(task));
243
+ const assessableTaskCount = Math.max(1, totalTasks - interstitialLimitedTasks.length);
244
+ const totalSteps = args.taskResults.reduce((sum, task) => sum + task.history.length, 0);
245
+ const failedSteps = args.taskResults.reduce((sum, task) => sum + task.history.filter((entry) => !entry.result.success || entry.decision.friction === "high").length, 0);
246
+ const actionableFailures = args.taskResults.filter((task) => task.status === "failed" &&
247
+ !isInterstitialLimited(task) &&
248
+ !isTimeLimited(task) &&
249
+ collectTaskObservations(task).some((item) => item.tone === "negative")).length;
250
+ const weightedCompletion = (successCount + partialCount * 0.6) / assessableTaskCount;
251
+ const frictionRatio = totalSteps > 0 ? failedSteps / totalSteps : actionableFailures / assessableTaskCount;
252
+ const violationCount = args.accessibility.violations.length;
253
+ const severeViolationCount = args.accessibility.violations.filter((item) => item.impact === "serious" || item.impact === "critical").length;
254
+ const clarity = clampScore(4 + weightedCompletion * 4 - actionableFailures * 0.6 - Math.min(1, interstitialLimitedTasks.length * 0.2));
255
+ const navigation = clampScore(4 + weightedCompletion * 4 - frictionRatio * 3 - actionableFailures * 0.6);
256
+ const trust = clampScore(4 + successCount * 0.5 - actionableFailures * 0.6);
257
+ const friction = clampScore(8 - frictionRatio * 4 - actionableFailures * 0.4 - Math.min(1, timeLimitedTasks.length * 0.3));
258
+ const conversionReadiness = clampScore(4 + weightedCompletion * 4 - actionableFailures * 0.6);
259
+ const accessibilityBasics = clampScore(8 - severeViolationCount * 2 - Math.min(3, violationCount * 0.4));
260
+ const overallScore = clampScore((clarity + navigation + trust + friction + conversionReadiness + accessibilityBasics) / 6);
261
+ const allObservations = args.taskResults
262
+ .flatMap((task) => collectTaskObservations(task))
263
+ .sort((left, right) => right.priority - left.priority);
264
+ const positiveObservations = uniqueItems(allObservations.filter((item) => item.tone === "positive").map((item) => item.text), 5);
265
+ const negativeObservations = uniqueItems(allObservations.filter((item) => item.tone === "negative").map((item) => item.text), 6);
266
+ const limitationObservations = uniqueItems([
267
+ ...allObservations.filter((item) => item.tone === "limitation").map((item) => item.text),
268
+ ...(interstitialLimitedTasks.length > 0
269
+ ? [`${interstitialLimitedTasks.length} visible path${interstitialLimitedTasks.length === 1 ? "" : "s"} were blocked by a verification or security wall before the destination could be validated.`]
270
+ : []),
271
+ ...(timeLimitedTasks.length > 0
272
+ ? [`${timeLimitedTasks.length} task${timeLimitedTasks.length === 1 ? "" : "s"} ended before there was enough time to validate more visible destinations.`]
273
+ : [])
274
+ ], 4);
275
+ const accessibilityObservations = args.accessibility.violations
276
+ .slice(0, 3)
277
+ .map((violation) => `${violation.help} affected ${violation.nodes} part${violation.nodes === 1 ? "" : "s"} of the page.`);
278
+ const strengths = uniqueItems([
279
+ ...positiveObservations,
280
+ ...(violationCount === 0 ? ["The saved axe scan did not flag any automated accessibility violations on this run."] : [])
281
+ ], 5);
282
+ const weaknesses = uniqueItems([
283
+ ...negativeObservations,
284
+ ...limitationObservations,
285
+ ...accessibilityObservations,
286
+ ...(args.accessibility.error ? [args.accessibility.error] : [])
287
+ ], 6);
288
+ const topFixes = uniqueItems([
289
+ ...allObservations
290
+ .filter((item) => item.tone !== "positive" && item.fix)
291
+ .map((item) => item.fix),
292
+ ...args.accessibility.violations
293
+ .slice(0, 2)
294
+ .map((violation) => `Resolve the "${violation.id}" accessibility issue so the page stops failing on ${violation.help.toLowerCase()}.`),
295
+ ...(timeLimitedTasks.length > 0
296
+ ? ["Narrow very broad navigation journeys into clearer, higher-signal paths so a first visit can validate the important destinations faster."]
297
+ : []),
298
+ "Make the primary navigation and key calls to action produce obvious, trustworthy next states on first click."
299
+ ], 5);
300
+ const siteLabel = readSiteLabel(args.baseUrl);
301
+ const summaryParts = [
302
+ `I checked ${siteLabel}${args.mobile ? " on a mobile-sized screen" : ""}.`,
303
+ buildTaskOutcomeSummary(args.taskResults),
304
+ gameplaySummary ? gameplaySummary.summary : "",
305
+ negativeObservations[0] ?? "",
306
+ positiveObservations[0] ?? "",
307
+ limitationObservations[0] ? `Coverage was still limited because ${limitationObservations[0].replace(/[.!?]$/g, "").toLowerCase()}.` : "",
308
+ violationCount > 0
309
+ ? `The saved accessibility pass also flagged ${violationCount} issue${violationCount === 1 ? "" : "s"}, including ${severeViolationCount} higher-impact item${severeViolationCount === 1 ? "" : "s"}.`
310
+ : "The saved accessibility pass did not flag any automated accessibility issues."
311
+ ];
312
+ return {
313
+ overall_score: overallScore,
314
+ summary: uniqueItems(summaryParts.map((part) => ensureSentence(part)), 5).join(" "),
315
+ scores: {
316
+ clarity,
317
+ navigation,
318
+ trust,
319
+ friction,
320
+ conversion_readiness: conversionReadiness,
321
+ accessibility_basics: accessibilityBasics
322
+ },
323
+ strengths: strengths.length > 0 ? strengths : ["I still gathered enough concrete interaction evidence to describe what a first visit felt like."],
324
+ weaknesses: weaknesses.length > 0 ? weaknesses : [`The run on ${siteLabel} stayed too inconclusive to support a stronger claim.`],
325
+ task_results: args.taskResults.map((task) => ({
326
+ name: task.name,
327
+ status: task.status,
328
+ reason: collectTaskObservations(task).map((item) => item.text).find(Boolean) ??
329
+ (isInternalToolingText(task.reason) ? "The path stayed inconclusive on this run." : task.reason),
330
+ evidence: extractTaskEvidence(task)
331
+ })),
332
+ top_fixes: topFixes,
333
+ ...(gameplaySummary ? { gameplay_summary: gameplaySummary } : {})
334
+ };
335
+ }