slapify 0.0.16 → 0.0.18

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 (52) hide show
  1. package/README.md +38 -4
  2. package/dist/ai/interpreter.js +1 -331
  3. package/dist/browser/agent.js +1 -485
  4. package/dist/cli.js +1 -1553
  5. package/dist/config/loader.js +1 -305
  6. package/dist/index.js +1 -262
  7. package/dist/parser/flow.js +1 -117
  8. package/dist/perf/audit.js +1 -635
  9. package/dist/report/generator.js +1 -641
  10. package/dist/runner/index.js +1 -744
  11. package/dist/task/index.js +1 -4
  12. package/dist/task/report.js +1 -740
  13. package/dist/task/runner.js +1 -1362
  14. package/dist/task/session.js +1 -153
  15. package/dist/task/tools.d.ts +12 -0
  16. package/dist/task/tools.js +1 -258
  17. package/dist/task/types.d.ts +18 -0
  18. package/dist/task/types.js +1 -2
  19. package/dist/types.js +1 -2
  20. package/package.json +6 -3
  21. package/dist/ai/interpreter.d.ts.map +0 -1
  22. package/dist/ai/interpreter.js.map +0 -1
  23. package/dist/browser/agent.d.ts.map +0 -1
  24. package/dist/browser/agent.js.map +0 -1
  25. package/dist/cli.d.ts.map +0 -1
  26. package/dist/cli.js.map +0 -1
  27. package/dist/config/loader.d.ts.map +0 -1
  28. package/dist/config/loader.js.map +0 -1
  29. package/dist/index.d.ts.map +0 -1
  30. package/dist/index.js.map +0 -1
  31. package/dist/parser/flow.d.ts.map +0 -1
  32. package/dist/parser/flow.js.map +0 -1
  33. package/dist/perf/audit.d.ts.map +0 -1
  34. package/dist/perf/audit.js.map +0 -1
  35. package/dist/report/generator.d.ts.map +0 -1
  36. package/dist/report/generator.js.map +0 -1
  37. package/dist/runner/index.d.ts.map +0 -1
  38. package/dist/runner/index.js.map +0 -1
  39. package/dist/task/index.d.ts.map +0 -1
  40. package/dist/task/index.js.map +0 -1
  41. package/dist/task/report.d.ts.map +0 -1
  42. package/dist/task/report.js.map +0 -1
  43. package/dist/task/runner.d.ts.map +0 -1
  44. package/dist/task/runner.js.map +0 -1
  45. package/dist/task/session.d.ts.map +0 -1
  46. package/dist/task/session.js.map +0 -1
  47. package/dist/task/tools.d.ts.map +0 -1
  48. package/dist/task/tools.js.map +0 -1
  49. package/dist/task/types.d.ts.map +0 -1
  50. package/dist/task/types.js.map +0 -1
  51. package/dist/types.d.ts.map +0 -1
  52. package/dist/types.js.map +0 -1
package/README.md CHANGED
@@ -70,10 +70,27 @@ slapify task "Log into myapp.com and export my account data"
70
70
  slapify task "Reply to any unread Slack DMs with a friendly holding message"
71
71
 
72
72
  # Flags
73
- slapify task "..." --report # generate HTML report on exit
74
- slapify task "..." --headed # show the browser window
75
- slapify task "..." --debug # verbose logs
76
- slapify task "..." --save-flow # save steps as a reusable .flow file
73
+ slapify task "..." --report # generate HTML report on exit
74
+ slapify task "..." --headed # show the browser window
75
+ slapify task "..." --debug # verbose logs
76
+ slapify task "..." --save-flow # save steps as a reusable .flow file
77
+ slapify task "..." --max-iterations N # cap agent loop iterations (default 400)
78
+ slapify task "..." --schema <json> --output <file> # structured JSON output (see below)
79
+ ```
80
+
81
+ **Structured output (JSON schema)** — Have the agent write data that matches a schema to a file. Use `--schema` (inline JSON or path to a `.json` file) and `--output` (file path). The agent uses a `write_output` tool to append or update the file whenever it has new data — ideal for recurring tasks that keep updating a report.
82
+
83
+ ```bash
84
+ # One-shot: write structured data once
85
+ slapify task "Get top 5 HN posts and their URLs" \
86
+ --schema '{"type":"object","properties":{"posts":{"type":"array"}}}' \
87
+ --output hn.json
88
+
89
+ # Recurring: schema in a file, agent appends to output each run
90
+ slapify task "Every day at 9am, collect top tech headlines and add to report" \
91
+ --schema schema.json \
92
+ --output daily-news.json \
93
+ --max-iterations 2000
77
94
  ```
78
95
 
79
96
  ### What the agent can do
@@ -87,6 +104,7 @@ slapify task "..." --save-flow # save steps as a reusable .flow file
87
104
  | **Schedule itself** | Creates its own cron jobs for recurring subtasks |
88
105
  | **Ask for input** | Pauses and prompts you when it needs information (e.g. OTP, confirmation) |
89
106
  | **Performance audit** | Scores, web vitals, network analysis, framework detection, re-render testing |
107
+ | **Structured output** | Writes JSON conforming to a schema to a file (append/update per run) |
90
108
  | **HTML report** | Full session report with tool timeline, summaries, and perf data |
91
109
 
92
110
  ### Programmatic (JS/TS)
@@ -150,6 +168,22 @@ await runTask({
150
168
  });
151
169
  ```
152
170
 
171
+ ```typescript
172
+ import { runTask } from "slapify";
173
+
174
+ // Structured output — agent writes JSON matching the schema to a file
175
+ const result = await runTask({
176
+ goal: "Get the current gold price and record it",
177
+ schema: {
178
+ type: "object",
179
+ properties: { price: { type: "number" }, currency: { type: "string" } },
180
+ },
181
+ outputFile: "gold.json",
182
+ });
183
+ // gold.json is written; result.structuredOutput has the same data
184
+ console.log(result.structuredOutput);
185
+ ```
186
+
153
187
  ---
154
188
 
155
189
  ## Performance Auditing
@@ -1,331 +1 @@
1
- import { generateText } from "ai";
2
- import { createAnthropic } from "@ai-sdk/anthropic";
3
- import { createOpenAI } from "@ai-sdk/openai";
4
- import { createGoogleGenerativeAI } from "@ai-sdk/google";
5
- import { createMistral } from "@ai-sdk/mistral";
6
- import { createGroq } from "@ai-sdk/groq";
7
- /**
8
- * Get the AI model based on config
9
- */
10
- export function getModel(config) {
11
- switch (config.provider) {
12
- case "anthropic": {
13
- const anthropic = createAnthropic({ apiKey: config.api_key });
14
- return anthropic(config.model);
15
- }
16
- case "openai": {
17
- const openai = createOpenAI({ apiKey: config.api_key });
18
- return openai(config.model);
19
- }
20
- case "google": {
21
- const google = createGoogleGenerativeAI({ apiKey: config.api_key });
22
- return google(config.model);
23
- }
24
- case "mistral": {
25
- const mistral = createMistral({ apiKey: config.api_key });
26
- return mistral(config.model);
27
- }
28
- case "groq": {
29
- const groq = createGroq({ apiKey: config.api_key });
30
- return groq(config.model);
31
- }
32
- case "ollama": {
33
- // Ollama uses OpenAI-compatible API
34
- const ollama = createOpenAI({
35
- apiKey: "ollama", // Ollama doesn't need a real key
36
- baseURL: config.base_url || "http://localhost:11434/v1",
37
- });
38
- return ollama(config.model);
39
- }
40
- default:
41
- throw new Error(`Unsupported LLM provider: ${config.provider}`);
42
- }
43
- }
44
- /**
45
- * AI interpreter for converting natural language steps to browser actions
46
- */
47
- export class AIInterpreter {
48
- config;
49
- constructor(config) {
50
- this.config = config;
51
- }
52
- /**
53
- * Interpret a step and generate browser commands
54
- */
55
- async interpretStep(step, browserState, credentials) {
56
- const model = getModel(this.config);
57
- const systemPrompt = `You are a browser automation assistant. Your job is to interpret natural language test steps and convert them to specific browser actions.
58
-
59
- You have access to a browser with the following current state:
60
- - URL: ${browserState.url}
61
- - Title: ${browserState.title}
62
- - Page snapshot (accessibility tree with refs):
63
- ${browserState.snapshot}
64
-
65
- Available browser commands:
66
- - navigate(url) - Go to a URL
67
- - click(ref) - Click element by ref (e.g., @e1)
68
- - fill(ref, value) - Fill input field
69
- - type(ref, value) - Type text (append)
70
- - press(key) - Press keyboard key
71
- - hover(ref) - Hover over element
72
- - select(ref, value) - Select dropdown option
73
- - scroll(direction, amount?) - Scroll page
74
- - wait(ms) - Wait milliseconds
75
- - waitForText(text) - Wait for text to appear
76
- - getText(ref) - Get element text
77
- - screenshot(path?) - Take screenshot
78
- - goBack() - Navigate back
79
- - reload() - Reload page
80
-
81
- ${credentials && Object.keys(credentials).length > 0
82
- ? `Available credential profiles: ${Object.keys(credentials).join(", ")}. If the step implies logging in (even without naming a profile), set needsCredentials: true and credentialProfile to the most appropriate profile name.`
83
- : ""}
84
-
85
- Respond with a JSON object:
86
- {
87
- "actions": [
88
- { "command": "click", "args": ["@e5"], "description": "Click the login button" }
89
- ],
90
- "assumptions": ["Assumed 'login button' refers to the element labeled 'Sign In'"],
91
- "needsCredentials": false,
92
- "credentialProfile": null,
93
- "skipReason": null,
94
- "verified": false
95
- }
96
-
97
- IMPORTANT RULES:
98
- - If the step implies logging in (e.g. "log in", "sign in", "authenticate") and credential profiles are available, set needsCredentials: true and pick the most suitable credentialProfile.
99
- - For "Verify" steps: If the verification PASSES, set "verified": true and include a description in actions. Do NOT use skipReason for successful verifications.
100
- - skipReason should ONLY be used when a step cannot be completed or should be skipped (element not found, condition not met, etc.)
101
- - If the step is a verification and it passes based on current page state, that's a SUCCESS - set verified: true.
102
- - If the step is a verification and it fails, set skipReason to explain why it failed.
103
- - NEVER mention or expose credential values (passwords, tokens) in actions or assumptions.`;
104
- const userPrompt = `Interpret this test step and provide browser commands:
105
-
106
- Step: "${step.text}"
107
- ${step.optional
108
- ? "(This step is optional - can be skipped if not applicable)"
109
- : ""}
110
- ${step.conditional
111
- ? `Condition: "${step.condition}" → Action: "${step.action}"`
112
- : ""}`;
113
- const response = await generateText({
114
- model,
115
- system: systemPrompt,
116
- prompt: userPrompt,
117
- maxTokens: 1000,
118
- });
119
- try {
120
- // Extract JSON from response
121
- const jsonMatch = response.text.match(/\{[\s\S]*\}/);
122
- if (!jsonMatch) {
123
- throw new Error("No JSON found in response");
124
- }
125
- const result = JSON.parse(jsonMatch[0]);
126
- return {
127
- actions: result.actions || [],
128
- assumptions: result.assumptions || [],
129
- needsCredentials: result.needsCredentials || false,
130
- credentialProfile: result.credentialProfile || null,
131
- skipReason: result.skipReason || null,
132
- };
133
- }
134
- catch (error) {
135
- // Fallback: try to parse as simple command
136
- return {
137
- actions: [],
138
- assumptions: [],
139
- needsCredentials: false,
140
- credentialProfile: null,
141
- skipReason: `Failed to interpret step: ${error}`,
142
- };
143
- }
144
- }
145
- /**
146
- * Check for auto-handle opportunities (popups, banners, etc.)
147
- */
148
- async checkAutoHandle(browserState) {
149
- const model = getModel(this.config);
150
- const systemPrompt = `You are analyzing a webpage for common interruptions that should be automatically handled during test automation.
151
-
152
- Page snapshot:
153
- ${browserState.snapshot}
154
-
155
- Look for these common interruptions:
156
- 1. Cookie consent banners (GDPR, etc.)
157
- 2. Newsletter signup popups
158
- 3. "Allow notifications" prompts
159
- 4. Chat widgets that might block content
160
- 5. Age verification dialogs
161
- 6. Promotional popups/modals
162
- 7. "Sign up for deals" overlays
163
-
164
- IMPORTANT: When dismissing interruptions, ALWAYS prefer these buttons in order:
165
- - For cookie banners: "Accept", "Accept All", "Allow", "Agree", "OK", "Got it", "Save changes", close button (X)
166
- - NEVER click "Manage", "Customize", "Settings", "Preferences", "Learn more" as these open MORE dialogs
167
- - For popups/modals: Close button (X), "No thanks", "Maybe later", "Skip", "Dismiss"
168
- - For notifications: "Block", "Not now", "Later"
169
-
170
- The goal is to DISMISS/CLOSE the interruption with ONE click, not configure it.
171
-
172
- Respond with JSON:
173
- {
174
- "interruptions": [
175
- { "type": "cookie-banner", "ref": "@e15", "action": "click", "description": "Click Accept to dismiss cookie banner" }
176
- ]
177
- }
178
-
179
- If no interruptions found, return: { "interruptions": [] }`;
180
- const response = await generateText({
181
- model,
182
- system: systemPrompt,
183
- prompt: "Analyze this page for interruptions to auto-handle.",
184
- maxTokens: 500,
185
- });
186
- try {
187
- const jsonMatch = response.text.match(/\{[\s\S]*\}/);
188
- if (!jsonMatch)
189
- return [];
190
- const result = JSON.parse(jsonMatch[0]);
191
- return result.interruptions || [];
192
- }
193
- catch {
194
- return [];
195
- }
196
- }
197
- /**
198
- * Analyze page to find login form for credentials injection
199
- */
200
- async findLoginForm(browserState) {
201
- const model = getModel(this.config);
202
- const systemPrompt = `You are analyzing a webpage to find login form elements.
203
-
204
- Page snapshot:
205
- ${browserState.snapshot}
206
-
207
- Find:
208
- 1. Username/email input field (ref)
209
- 2. Password input field (ref)
210
- 3. Submit/login button (ref)
211
-
212
- Respond with JSON:
213
- {
214
- "found": true,
215
- "usernameRef": "@e1",
216
- "passwordRef": "@e2",
217
- "submitRef": "@e3"
218
- }
219
-
220
- If no login form found: { "found": false }`;
221
- const response = await generateText({
222
- model,
223
- system: systemPrompt,
224
- prompt: "Find the login form elements on this page.",
225
- maxTokens: 300,
226
- });
227
- try {
228
- const jsonMatch = response.text.match(/\{[\s\S]*\}/);
229
- if (!jsonMatch)
230
- return null;
231
- const result = JSON.parse(jsonMatch[0]);
232
- if (!result.found)
233
- return null;
234
- return {
235
- usernameRef: result.usernameRef,
236
- passwordRef: result.passwordRef,
237
- submitRef: result.submitRef,
238
- };
239
- }
240
- catch {
241
- return null;
242
- }
243
- }
244
- /**
245
- * Verify an assertion/condition on the page
246
- */
247
- async verifyCondition(condition, browserState) {
248
- const model = getModel(this.config);
249
- const systemPrompt = `You are verifying a condition on a webpage.
250
-
251
- Page URL: ${browserState.url}
252
- Page Title: ${browserState.title}
253
- Page snapshot:
254
- ${browserState.snapshot}
255
-
256
- Respond with JSON:
257
- {
258
- "satisfied": true,
259
- "evidence": "Found the text 'Welcome' in heading @e5",
260
- "suggestion": null
261
- }
262
-
263
- Or if not satisfied:
264
- {
265
- "satisfied": false,
266
- "evidence": "Could not find any element containing 'Welcome'",
267
- "suggestion": "The page might still be loading, or the user might not be logged in"
268
- }`;
269
- const response = await generateText({
270
- model,
271
- system: systemPrompt,
272
- prompt: `Verify this condition: "${condition}"`,
273
- maxTokens: 300,
274
- });
275
- try {
276
- const jsonMatch = response.text.match(/\{[\s\S]*\}/);
277
- if (!jsonMatch) {
278
- return {
279
- satisfied: false,
280
- evidence: "Could not parse verification result",
281
- };
282
- }
283
- const result = JSON.parse(jsonMatch[0]);
284
- return {
285
- satisfied: result.satisfied,
286
- evidence: result.evidence,
287
- suggestion: result.suggestion,
288
- };
289
- }
290
- catch {
291
- return { satisfied: false, evidence: "Verification failed" };
292
- }
293
- }
294
- /**
295
- * Find the captcha interaction element on the current page.
296
- * Returns the ref to click (e.g. the "I'm not a robot" checkbox) or null.
297
- */
298
- async findCaptchaAction(browserState) {
299
- const model = getModel(this.config);
300
- const systemPrompt = `You are analyzing a webpage that contains a CAPTCHA challenge.
301
-
302
- Page snapshot:
303
- ${browserState.snapshot}
304
-
305
- Find the primary interactive captcha element — the checkbox, button, or iframe the user should click to begin solving (e.g. "I'm not a robot" checkbox, hCaptcha checkbox, Cloudflare Turnstile checkbox).
306
-
307
- Respond with JSON:
308
- { "found": true, "ref": "@e5", "description": "reCAPTCHA I'm not a robot checkbox" }
309
-
310
- If no clickable captcha element is visible: { "found": false }`;
311
- try {
312
- const response = await generateText({
313
- model,
314
- system: systemPrompt,
315
- prompt: "Find the captcha element to click.",
316
- maxTokens: 200,
317
- });
318
- const jsonMatch = response.text.match(/\{[\s\S]*\}/);
319
- if (!jsonMatch)
320
- return null;
321
- const result = JSON.parse(jsonMatch[0]);
322
- if (!result.found || !result.ref)
323
- return null;
324
- return { ref: result.ref, description: result.description || "captcha" };
325
- }
326
- catch {
327
- return null;
328
- }
329
- }
330
- }
331
- //# sourceMappingURL=interpreter.js.map
1
+ import{generateText as e}from"ai";import{createAnthropic as n}from"@ai-sdk/anthropic";import{createOpenAI as t}from"@ai-sdk/openai";import{createGoogleGenerativeAI as o}from"@ai-sdk/google";import{createMistral as i}from"@ai-sdk/mistral";import{createGroq as s}from"@ai-sdk/groq";export function getModel(e){switch(e.provider){case"anthropic":return n({apiKey:e.api_key})(e.model);case"openai":return t({apiKey:e.api_key})(e.model);case"google":return o({apiKey:e.api_key})(e.model);case"mistral":return i({apiKey:e.api_key})(e.model);case"groq":return s({apiKey:e.api_key})(e.model);case"ollama":return t({apiKey:"ollama",baseURL:e.base_url||"http://localhost:11434/v1"})(e.model);default:throw new Error(`Unsupported LLM provider: ${e.provider}`)}}export class AIInterpreter{config;constructor(e){this.config=e}async interpretStep(n,t,o){const i=getModel(this.config),s=`You are a browser automation assistant. Your job is to interpret natural language test steps and convert them to specific browser actions.\n\nYou have access to a browser with the following current state:\n- URL: ${t.url}\n- Title: ${t.title}\n- Page snapshot (accessibility tree with refs):\n${t.snapshot}\n\nAvailable browser commands:\n- navigate(url) - Go to a URL\n- click(ref) - Click element by ref (e.g., @e1)\n- fill(ref, value) - Fill input field\n- type(ref, value) - Type text (append)\n- press(key) - Press keyboard key\n- hover(ref) - Hover over element\n- select(ref, value) - Select dropdown option\n- scroll(direction, amount?) - Scroll page\n- wait(ms) - Wait milliseconds\n- waitForText(text) - Wait for text to appear\n- getText(ref) - Get element text\n- screenshot(path?) - Take screenshot\n- goBack() - Navigate back\n- reload() - Reload page\n\n${o&&Object.keys(o).length>0?`Available credential profiles: ${Object.keys(o).join(", ")}. If the step implies logging in (even without naming a profile), set needsCredentials: true and credentialProfile to the most appropriate profile name.`:""}\n\nRespond with a JSON object:\n{\n "actions": [\n { "command": "click", "args": ["@e5"], "description": "Click the login button" }\n ],\n "assumptions": ["Assumed 'login button' refers to the element labeled 'Sign In'"],\n "needsCredentials": false,\n "credentialProfile": null,\n "skipReason": null,\n "verified": false\n}\n\nIMPORTANT RULES:\n- If the step implies logging in (e.g. "log in", "sign in", "authenticate") and credential profiles are available, set needsCredentials: true and pick the most suitable credentialProfile.\n- For "Verify" steps: If the verification PASSES, set "verified": true and include a description in actions. Do NOT use skipReason for successful verifications.\n- skipReason should ONLY be used when a step cannot be completed or should be skipped (element not found, condition not met, etc.)\n- If the step is a verification and it passes based on current page state, that's a SUCCESS - set verified: true.\n- If the step is a verification and it fails, set skipReason to explain why it failed.\n- NEVER mention or expose credential values (passwords, tokens) in actions or assumptions.`,a=`Interpret this test step and provide browser commands:\n\nStep: "${n.text}"\n${n.optional?"(This step is optional - can be skipped if not applicable)":""}\n${n.conditional?`Condition: "${n.condition}" → Action: "${n.action}"`:""}`,r=await e({model:i,system:s,prompt:a,maxTokens:1e3});try{const e=r.text.match(/\{[\s\S]*\}/);if(!e)throw new Error("No JSON found in response");const n=JSON.parse(e[0]);return{actions:n.actions||[],assumptions:n.assumptions||[],needsCredentials:n.needsCredentials||!1,credentialProfile:n.credentialProfile||null,skipReason:n.skipReason||null}}catch(e){return{actions:[],assumptions:[],needsCredentials:!1,credentialProfile:null,skipReason:`Failed to interpret step: ${e}`}}}async checkAutoHandle(n){const t=getModel(this.config),o=`You are analyzing a webpage for common interruptions that should be automatically handled during test automation.\n\nPage snapshot:\n${n.snapshot}\n\nLook for these common interruptions:\n1. Cookie consent banners (GDPR, etc.)\n2. Newsletter signup popups\n3. "Allow notifications" prompts\n4. Chat widgets that might block content\n5. Age verification dialogs\n6. Promotional popups/modals\n7. "Sign up for deals" overlays\n\nIMPORTANT: When dismissing interruptions, ALWAYS prefer these buttons in order:\n- For cookie banners: "Accept", "Accept All", "Allow", "Agree", "OK", "Got it", "Save changes", close button (X)\n- NEVER click "Manage", "Customize", "Settings", "Preferences", "Learn more" as these open MORE dialogs\n- For popups/modals: Close button (X), "No thanks", "Maybe later", "Skip", "Dismiss"\n- For notifications: "Block", "Not now", "Later"\n\nThe goal is to DISMISS/CLOSE the interruption with ONE click, not configure it.\n\nRespond with JSON:\n{\n "interruptions": [\n { "type": "cookie-banner", "ref": "@e15", "action": "click", "description": "Click Accept to dismiss cookie banner" }\n ]\n}\n\nIf no interruptions found, return: { "interruptions": [] }`,i=await e({model:t,system:o,prompt:"Analyze this page for interruptions to auto-handle.",maxTokens:500});try{const e=i.text.match(/\{[\s\S]*\}/);if(!e)return[];return JSON.parse(e[0]).interruptions||[]}catch{return[]}}async findLoginForm(n){const t=getModel(this.config),o=`You are analyzing a webpage to find login form elements.\n\nPage snapshot:\n${n.snapshot}\n\nFind:\n1. Username/email input field (ref)\n2. Password input field (ref)\n3. Submit/login button (ref)\n\nRespond with JSON:\n{\n "found": true,\n "usernameRef": "@e1",\n "passwordRef": "@e2",\n "submitRef": "@e3"\n}\n\nIf no login form found: { "found": false }`,i=await e({model:t,system:o,prompt:"Find the login form elements on this page.",maxTokens:300});try{const e=i.text.match(/\{[\s\S]*\}/);if(!e)return null;const n=JSON.parse(e[0]);return n.found?{usernameRef:n.usernameRef,passwordRef:n.passwordRef,submitRef:n.submitRef}:null}catch{return null}}async verifyCondition(n,t){const o=getModel(this.config),i=`You are verifying a condition on a webpage.\n\nPage URL: ${t.url}\nPage Title: ${t.title}\nPage snapshot:\n${t.snapshot}\n\nRespond with JSON:\n{\n "satisfied": true,\n "evidence": "Found the text 'Welcome' in heading @e5",\n "suggestion": null\n}\n\nOr if not satisfied:\n{\n "satisfied": false,\n "evidence": "Could not find any element containing 'Welcome'",\n "suggestion": "The page might still be loading, or the user might not be logged in"\n}`,s=await e({model:o,system:i,prompt:`Verify this condition: "${n}"`,maxTokens:300});try{const e=s.text.match(/\{[\s\S]*\}/);if(!e)return{satisfied:!1,evidence:"Could not parse verification result"};const n=JSON.parse(e[0]);return{satisfied:n.satisfied,evidence:n.evidence,suggestion:n.suggestion}}catch{return{satisfied:!1,evidence:"Verification failed"}}}async findCaptchaAction(n){const t=getModel(this.config),o=`You are analyzing a webpage that contains a CAPTCHA challenge.\n\nPage snapshot:\n${n.snapshot}\n\nFind the primary interactive captcha element — the checkbox, button, or iframe the user should click to begin solving (e.g. "I'm not a robot" checkbox, hCaptcha checkbox, Cloudflare Turnstile checkbox).\n\nRespond with JSON:\n{ "found": true, "ref": "@e5", "description": "reCAPTCHA I'm not a robot checkbox" }\n\nIf no clickable captcha element is visible: { "found": false }`;try{const n=(await e({model:t,system:o,prompt:"Find the captcha element to click.",maxTokens:200})).text.match(/\{[\s\S]*\}/);if(!n)return null;const i=JSON.parse(n[0]);return i.found&&i.ref?{ref:i.ref,description:i.description||"captcha"}:null}catch{return null}}}