slapify 0.0.21 → 0.0.22
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 +18 -4
- package/dist/ai/interpreter.js +1 -1
- package/dist/chat/telegram.d.ts +9 -0
- package/dist/chat/telegram.js +1 -1
- package/dist/cli.js +1 -1
- package/dist/config/loader.js +1 -1
- package/dist/task/runner.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,8 +12,9 @@ https://github.com/user-attachments/assets/52564f16-7664-4ac3-9e06-e04c17dc4bbf
|
|
|
12
12
|
|
|
13
13
|
| Mode | What it does |
|
|
14
14
|
| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
15
|
-
| **`task`**
|
|
16
|
-
| **`
|
|
15
|
+
| **`task`** | Autonomous AI agent — give it any goal, it figures out how to achieve it. Browses, logs in, schedules itself, audits performance, remembers state across runs. |
|
|
16
|
+
| **`server`** | Persistent server mode — stays online and waits for instructions via Telegram. Start tasks remotely from your phone. |
|
|
17
|
+
| **`run`** | Execute `.flow` test files — plain-English E2E tests with screenshots and HTML reports |
|
|
17
18
|
|
|
18
19
|
---
|
|
19
20
|
|
|
@@ -80,6 +81,18 @@ npx slapify task "..." --max-iterations N # cap agent loop iterations (default
|
|
|
80
81
|
npx slapify task "..." --schema <json> --output <file> # structured JSON output (see below)
|
|
81
82
|
```
|
|
82
83
|
|
|
84
|
+
### Server Mode (`slapify server`)
|
|
85
|
+
|
|
86
|
+
Run Slapify as a persistent background process that you control remotely via Telegram.
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
# Start the server (waits for Telegram commands)
|
|
90
|
+
npx slapify server
|
|
91
|
+
|
|
92
|
+
# With visible browser and debug logs
|
|
93
|
+
npx slapify server --headed --debug
|
|
94
|
+
```
|
|
95
|
+
|
|
83
96
|
**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.
|
|
84
97
|
|
|
85
98
|
```bash
|
|
@@ -537,8 +550,9 @@ When the Slapify agent hits an obstacle, it will seamlessly fall back to using y
|
|
|
537
550
|
### Telegram Integration
|
|
538
551
|
Add a `telegram` block to your config. The agent will:
|
|
539
552
|
1. Message you when long-running background tasks start and finish.
|
|
540
|
-
2. Ping you via Telegram when it gets stuck or needs a 2FA code (instead of waiting at the CLI).
|
|
541
|
-
3. **Listen for instructions:** You can text the bot mid-run ("Hey, don't click that button
|
|
553
|
+
2. Ping you via Telegram when it gets stuck or needs a 2FA code (instead of waiting at the CLI). You can reply directly in the chat!
|
|
554
|
+
3. **Listen for instructions:** You can text the bot mid-run ("Hey, don't click that button") and it will process your instruction live.
|
|
555
|
+
4. **Remote Activation:** Use the `/task` command (e.g. `/task Buy more coffee`) to start a new mission from your phone anytime while the server is running.
|
|
542
556
|
|
|
543
557
|
---
|
|
544
558
|
|
package/dist/ai/interpreter.js
CHANGED
|
@@ -1 +1 @@
|
|
|
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 s}from"@ai-sdk/mistral";import{createGroq as i}from"@ai-sdk/groq";export function getModel(e){switch(e.provider){case"anthropic":return n({apiKey:e.api_key,baseURL:e.base_url})(e.model);case"openai":{const n={apiKey:e.api_key,fetch:async(e,n)=>{if(process.env.SLAPIFY_DEBUG_AI){if(console.log("\n[DEBUG] === OUTGOING AI REQUEST ==="),console.log("[DEBUG] URL:",e),n.body)try{const e=JSON.parse(n.body);console.log("[DEBUG] BODY:",JSON.stringify(e,null,2))}catch{console.log("[DEBUG] BODY:",n.body)}console.log("[DEBUG] ===================================\n")}const t=await fetch(e,n);if(process.env.SLAPIFY_DEBUG_AI){const e=t.clone();console.log("\n[DEBUG] === INCOMING AI RESPONSE ==="),console.log("[DEBUG] STATUS:",e.status);try{const n=await e.text();console.log("[DEBUG] PAYLOAD:",n)}catch(e){console.log("[DEBUG] Could not read response text")}console.log("[DEBUG] ====================================\n")}return t}};if(e.base_url){let t=e.base_url;t.endsWith("/chat/completions")&&(t=t.replace(/\/chat\/completions\/?$/,"")),n.baseURL=t,n.compatibility="compatible"}let o=e.model;o.startsWith("openai/")&&(o=o.substring(7));return t(n)(o)}case"google":return o({apiKey:e.api_key})(e.model);case"mistral":return s({apiKey:e.api_key})(e.model);case"groq":return i({apiKey:e.api_key})(e.model);case"ollama":return t({apiKey:"ollama",baseURL:e.base_url||"http://localhost:11434/v1",fetch:async(e,n)=>{if(process.env.SLAPIFY_DEBUG_AI){if(console.log("\n[DEBUG] === OUTGOING OLLAMA REQUEST ==="),console.log("[DEBUG] URL:",e),n.body)try{const e=JSON.parse(n.body);console.log("[DEBUG] BODY:",JSON.stringify(e,null,2))}catch{console.log("[DEBUG] BODY:",n.body)}console.log("[DEBUG] =====================================\n")}const t=await fetch(e,n);if(process.env.SLAPIFY_DEBUG_AI){const e=t.clone();console.log("\n[DEBUG] === INCOMING OLLAMA RESPONSE ==="),console.log("[DEBUG] STATUS:",e.status);try{const n=await e.text();console.log("[DEBUG] PAYLOAD:",n)}catch(e){console.log("[DEBUG] Could not read response text")}console.log("[DEBUG] =====================================\n")}return t}})(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 s=getModel(this.config),i=`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:s,system:i,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": [] }`,s=await e({model:t,system:o,prompt:"Analyze this page for interruptions to auto-handle.",maxTokens:500});try{const e=s.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 }`,s=await e({model:t,system:o,prompt:"Find the login form elements on this page.",maxTokens:300});try{const e=s.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),s=`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}`,i=await e({model:o,system:s,prompt:`Verify this condition: "${n}"`,maxTokens:300});try{const e=i.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 s=JSON.parse(n[0]);return s.found&&s.ref?{ref:s.ref,description:s.description||"captcha"}:null}catch{return null}}}
|
|
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 s}from"@ai-sdk/mistral";import{createGroq as i}from"@ai-sdk/groq";export function getModel(e){switch(e.provider){case"anthropic":return n({apiKey:e.api_key,baseURL:e.base_url?e.base_url.endsWith("/v1")?e.base_url:`${e.base_url.replace(/\/$/,"")}/v1`:void 0,fetch:async(e,n)=>{if(process.env.SLAPIFY_DEBUG_AI&&(console.log("\n[DEBUG] === OUTGOING ANTHROPIC REQUEST ==="),console.log("[DEBUG] URL:",e),n?.body))try{const e=JSON.parse(n.body);console.log("[DEBUG] BODY:",JSON.stringify(e,null,2))}catch{console.log("[DEBUG] BODY:",n.body)}const t=await fetch(e,n),o=t.clone();if(t.headers.get("content-type")?.includes("application/json"))try{let e=await t.text();if(process.env.SLAPIFY_DEBUG_AI&&(console.log("\n[DEBUG] === INCOMING ANTHROPIC RESPONSE ==="),console.log("[DEBUG] STATUS:",t.status),console.log("[DEBUG] PAYLOAD:",e.substring(0,1e3)+(e.length>1e3?"...":""))),e.includes('"thoughtSignature"')||e.includes('"signature"')||e.includes('"thinking"')){const n=JSON.parse(e);n.content&&Array.isArray(n.content)&&(n.content=n.content.map(e=>{if(void 0!==e.thoughtSignature&&delete e.thoughtSignature,void 0!==e.signature&&delete e.signature,"thinking"===e.type){e.type="text";const n=e.thinking||"";e.text=`<thought>\n${n}\n</thought>`,delete e.thinking}return e})),e=JSON.stringify(n)}const n=new Headers(t.headers);return n.delete("content-length"),n.delete("content-encoding"),new Response(e,{status:t.status,statusText:t.statusText,headers:n})}catch(e){return process.env.SLAPIFY_DEBUG_AI&&console.log("[DEBUG] Could not read or scrub response text:",e),o}return t}})(e.model);case"openai":{const n={apiKey:e.api_key,fetch:async(e,n)=>{if(process.env.SLAPIFY_DEBUG_AI){if(console.log("\n[DEBUG] === OUTGOING AI REQUEST ==="),console.log("[DEBUG] URL:",e),n.body)try{const e=JSON.parse(n.body);console.log("[DEBUG] BODY:",JSON.stringify(e,null,2))}catch{console.log("[DEBUG] BODY:",n.body)}console.log("[DEBUG] ===================================\n")}const t=await fetch(e,n);if(process.env.SLAPIFY_DEBUG_AI){const e=t.clone();console.log("\n[DEBUG] === INCOMING AI RESPONSE ==="),console.log("[DEBUG] STATUS:",e.status);try{const n=await e.text();console.log("[DEBUG] PAYLOAD:",n)}catch(e){console.log("[DEBUG] Could not read response text")}console.log("[DEBUG] ====================================\n")}return t}};if(e.base_url){let t=e.base_url;t.endsWith("/chat/completions")&&(t=t.replace(/\/chat\/completions\/?$/,"")),n.baseURL=t,n.compatibility="compatible"}let o=e.model;o.startsWith("openai/")&&(o=o.substring(7));return t(n)(o)}case"google":return o({apiKey:e.api_key})(e.model);case"mistral":return s({apiKey:e.api_key})(e.model);case"groq":return i({apiKey:e.api_key})(e.model);case"ollama":return t({apiKey:"ollama",baseURL:e.base_url||"http://localhost:11434/v1",fetch:async(e,n)=>{if(process.env.SLAPIFY_DEBUG_AI){if(console.log("\n[DEBUG] === OUTGOING OLLAMA REQUEST ==="),console.log("[DEBUG] URL:",e),n.body)try{const e=JSON.parse(n.body);console.log("[DEBUG] BODY:",JSON.stringify(e,null,2))}catch{console.log("[DEBUG] BODY:",n.body)}console.log("[DEBUG] =====================================\n")}const t=await fetch(e,n);if(process.env.SLAPIFY_DEBUG_AI){const e=t.clone();console.log("\n[DEBUG] === INCOMING OLLAMA RESPONSE ==="),console.log("[DEBUG] STATUS:",e.status);try{const n=await e.text();console.log("[DEBUG] PAYLOAD:",n)}catch(e){console.log("[DEBUG] Could not read response text")}console.log("[DEBUG] =====================================\n")}return t}})(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 s=getModel(this.config),i=`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:s,system:i,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": [] }`,s=await e({model:t,system:o,prompt:"Analyze this page for interruptions to auto-handle.",maxTokens:500});try{const e=s.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 }`,s=await e({model:t,system:o,prompt:"Find the login form elements on this page.",maxTokens:300});try{const e=s.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),s=`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}`,i=await e({model:o,system:s,prompt:`Verify this condition: "${n}"`,maxTokens:300});try{const e=i.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 s=JSON.parse(n[0]);return s.found&&s.ref?{ref:s.ref,description:s.description||"captcha"}:null}catch{return null}}}
|
package/dist/chat/telegram.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export declare class TelegramService {
|
|
|
11
11
|
private isPolling;
|
|
12
12
|
private messageQueue;
|
|
13
13
|
private activeChatId?;
|
|
14
|
+
private pendingAskResolver?;
|
|
14
15
|
constructor(config: TelegramConfig);
|
|
15
16
|
/**
|
|
16
17
|
* Start long-polling in the background.
|
|
@@ -21,6 +22,14 @@ export declare class TelegramService {
|
|
|
21
22
|
* Drain any live messages received from the user.
|
|
22
23
|
*/
|
|
23
24
|
popMessages(): string[];
|
|
25
|
+
/**
|
|
26
|
+
* Wait for a single reply from the user (used for ask_user prompts).
|
|
27
|
+
*/
|
|
28
|
+
waitForReply(): Promise<string>;
|
|
29
|
+
/**
|
|
30
|
+
* Cancel an active wait (e.g., if CLI input won the race).
|
|
31
|
+
*/
|
|
32
|
+
cancelWaitForReply(): void;
|
|
24
33
|
/**
|
|
25
34
|
* Send a message to the active chat (wherever the last command came from,
|
|
26
35
|
* or the specifically configured allowed_chat_id).
|
package/dist/chat/telegram.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export class TelegramService{botToken;allowedChatId;lastUpdateId=0;isPolling=!1;messageQueue=[];activeChatId;constructor(
|
|
1
|
+
export class TelegramService{botToken;allowedChatId;lastUpdateId=0;isPolling=!1;messageQueue=[];activeChatId;pendingAskResolver;constructor(e){this.botToken=e.bot_token,this.allowedChatId=e.allowed_chat_id}start(){this.isPolling||(this.isPolling=!0,this.pollLoop().catch(e=>{console.error("[Telegram] Fatal polling error:",e),this.isPolling=!1}))}stop(){this.isPolling=!1}popMessages(){if(0===this.messageQueue.length)return[];const e=[...this.messageQueue];return this.messageQueue=[],e}waitForReply(){return new Promise(e=>{this.pendingAskResolver=e})}cancelWaitForReply(){this.pendingAskResolver=void 0}async sendMessage(e){const t=this.allowedChatId||this.activeChatId;if(t)try{await fetch(`https://api.telegram.org/bot${this.botToken}/sendMessage`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({chat_id:t,text:e,parse_mode:"Markdown"})})}catch(e){console.error("[Telegram] Failed to send message:",e)}}async pollLoop(){for(;this.isPolling;)try{const e=await fetch(`https://api.telegram.org/bot${this.botToken}/getUpdates?offset=${this.lastUpdateId+1}&timeout=30`,{signal:AbortSignal.timeout?.(35e3)||void 0});if(!e.ok){if(401===e.status)return console.error("\n⚠️ [Telegram] Invalid bot token. Disabling Telegram integration."),void(this.isPolling=!1);await new Promise(e=>setTimeout(e,2e3));continue}const t=await e.json();if(t.ok&&Array.isArray(t.result))for(const e of t.result){this.lastUpdateId=Math.max(this.lastUpdateId,e.update_id);const t=e.message;if(!t||!t.text)continue;const s=t.chat.id;if((!this.allowedChatId||String(s)===String(this.allowedChatId))&&(this.activeChatId=s,t.text))if(this.pendingAskResolver)console.log(`\n💬 [Telegram] Received reply: "${t.text}"`),this.pendingAskResolver(t.text.trim()),this.pendingAskResolver=void 0;else{const e=t.text.trim();console.log(`\n💬 [Telegram] Received live instruction: "${e}"`),e.toLowerCase().startsWith("/task ")?this.messageQueue.push(e.slice(6)):this.messageQueue.push(e)}}}catch(e){"AbortError"!==e.name&&"TimeoutError"!==e.name&&await new Promise(e=>setTimeout(e,2e3))}}}
|
package/dist/cli.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{Command as e}from"commander";import o from"chalk";import s from"ora";import t from"path";import n from"fs";import l from"dotenv";import{loadConfig as r,loadCredentials as a,getConfigDir as i}from"./config/loader.js";import{parseFlowFile as c,findFlowFiles as d,validateFlowFile as g,getFlowSummary as p}from"./parser/flow.js";import{TestRunner as f}from"./runner/index.js";import{ReportGenerator as u}from"./report/generator.js";import{BrowserAgent as m}from"./browser/agent.js";import{generateText as y}from"ai";import{createAnthropic as w}from"@ai-sdk/anthropic";import{createOpenAI as h}from"@ai-sdk/openai";import{createGoogleGenerativeAI as $}from"@ai-sdk/google";import{createMistral as b}from"@ai-sdk/mistral";import{createGroq as S}from"@ai-sdk/groq";import x from"yaml";function k(e){switch(e.provider){case"anthropic":return w({apiKey:e.api_key})(e.model);case"openai":return h({apiKey:e.api_key})(e.model);case"google":return $({apiKey:e.api_key})(e.model);case"mistral":return b({apiKey:e.api_key})(e.model);case"groq":return S({apiKey:e.api_key})(e.model);case"ollama":return h({apiKey:"ollama",baseURL:e.base_url||"http://localhost:11434/v1"})(e.model);default:throw new Error(`Unsupported provider: ${e.provider}`)}}l.config();const v=new e;function A(e){if("string"==typeof e)try{e=JSON.parse(e)}catch{return{}}if(!e||"object"!=typeof e||Array.isArray(e))return{};const o={};for(const[s,t]of Object.entries(e))o[String(s)]="string"==typeof t?t:JSON.stringify(t);return o}function I(e,s){const l=t.resolve(e);if(!n.existsSync(l))return console.log(o.yellow(` Skip (not found): ${l}`)),!1;const r=n.readFileSync(l,"utf-8");let a;try{a=x.parse(r)}catch(e){return console.log(o.red(` Invalid YAML: ${l}`)),console.log(o.gray(` ${e.message}`)),!1}if(!a||!a.profiles||"object"!=typeof a.profiles)return console.log(o.yellow(` No profiles in: ${l}`)),!1;let i=!1;for(const[e,o]of Object.entries(a.profiles)){if("inject"!==o.type)continue;const s="string"==typeof o.localStorage||o.localStorage&&(Array.isArray(o.localStorage)||"object"!=typeof o.localStorage),t="string"==typeof o.sessionStorage||o.sessionStorage&&(Array.isArray(o.sessionStorage)||"object"!=typeof o.sessionStorage);(s||t)&&(a.profiles[e]={...o,...s&&{localStorage:A(o.localStorage)},...t&&{sessionStorage:A(o.sessionStorage)}},i=!0)}if(!i)return console.log(o.gray(` No changes needed: ${l}`)),!1;if(s)return console.log(o.cyan(` Would fix: ${l}`)),!0;const c=l+".backup";return n.copyFileSync(l,c),n.writeFileSync(l,x.stringify(a,{indent:2,lineWidth:0})),console.log(o.green(` Fixed: ${l}`)),console.log(o.gray(` Backup: ${c}`)),!0}v.name("slapify").description("AI-powered test automation using natural language flow files").version("0.0.21","-v, -V, --version"),v.command("init").description("Initialize Slapify in the current directory").option("-y, --yes","Skip prompts and use defaults").action(async e=>{const t=await import("readline");if(n.existsSync(".slapify"))return console.log(o.yellow("Slapify is already initialized in this directory.")),void console.log(o.gray("Delete .slapify folder to reinitialize."));console.log(o.blue.bold("\n🖐️ Welcome to Slapify!\n")),console.log(o.gray("AI-powered E2E testing that slaps - by slaps.dev\n"));const{findSystemBrowsers:l,initConfig:r}=await import("./config/loader.js");let a,i,c,d="anthropic";const g={anthropic:{name:"Anthropic (Claude)",envVar:"ANTHROPIC_API_KEY",defaultModel:"claude-sonnet-4-6",models:[{id:"claude-sonnet-4-6",name:"Sonnet 4.6 (claude-sonnet-4-6) - highly capable & recommended",recommended:!0},{id:"claude-sonnet-4-5",name:"Sonnet 4.5 - fast & reliable"},{id:"claude-opus-4-6",name:"Opus 4.6 - extended reasoning & advanced tasks"},{id:"claude-3-7-sonnet-latest",name:"Sonnet 3.7 - legacy hybrid reasoning"},{id:"custom",name:"Enter custom model ID"}]},openai:{name:"OpenAI",envVar:"OPENAI_API_KEY",defaultModel:"gpt-5.2",models:[{id:"gpt-5.2",name:"GPT-5.2 - latest flagship model",recommended:!0},{id:"o3-mini",name:"o3-mini - fast reasoning & coding"},{id:"gpt-5.3-codex",name:"GPT-5.3 Codex - advanced coding"},{id:"custom",name:"Enter custom model ID"}]},google:{name:"Google (Gemini)",envVar:"GOOGLE_API_KEY",defaultModel:"gemini-3-flash",models:[{id:"gemini-3-flash",name:"Gemini 3 Flash - balanced & fast",recommended:!0},{id:"gemini-3.1-pro",name:"Gemini 3.1 Pro - highly capable"},{id:"gemini-2.5-flash",name:"Gemini 2.5 Flash - older stable"},{id:"custom",name:"Enter custom model ID"}]},mistral:{name:"Mistral",envVar:"MISTRAL_API_KEY",askModel:!0,defaultModel:"mistral-small-latest"},groq:{name:"Groq (Fast inference)",envVar:"GROQ_API_KEY",askModel:!0,defaultModel:"llama-3.3-70b-versatile"},ollama:{name:"Ollama (Local)",envVar:"",askModel:!0,defaultModel:"llama3"}};if(e.yes)console.log(o.gray("Using default settings...\n"));else{const e=t.createInterface({input:process.stdin,output:process.stdout}),n=o=>new Promise(s=>e.question(o,s));console.log(o.cyan("1. Choose your LLM provider:\n")),console.log(" 1) Anthropic (Claude) "+o.green("- recommended")),console.log(" 2) OpenAI (GPT-4)"),console.log(" 3) Google (Gemini)"),console.log(" 4) Mistral"),console.log(" 5) Groq "+o.gray("- fast & free tier")),console.log(" 6) Ollama "+o.gray("- local, no API key")),console.log("");d={1:"anthropic",2:"openai",3:"google",4:"mistral",5:"groq",6:"ollama"}[await n(o.white(" Select [1]: "))]||"anthropic";const r=g[d];if(console.log(o.green(` ✓ Using ${r.name}\n`)),r.models&&r.models.length>0){console.log(o.cyan(" Choose model:\n")),r.models.forEach((e,s)=>{const t=e.recommended?o.green(" ← recommended"):"";console.log(` ${s+1}) ${e.name}${t}`)}),console.log("");const e=await n(o.white(" Select [1]: ")),s=parseInt(e)-1||0,t=r.models[s];if("custom"===t?.id){a=(await n(o.white(" Enter model ID: "))).trim()||r.defaultModel}else a=t?.id||r.defaultModel;console.log(o.green(` ✓ Using model: ${a}\n`))}else if(r.askModel){console.log(o.gray(` Enter model ID (default: ${r.defaultModel})`)),"ollama"===d?console.log(o.gray(" Common models: llama3, mistral, codellama, phi3")):"groq"===d?console.log(o.gray(" Common models: llama-3.3-70b-versatile, mixtral-8x7b-32768")):"mistral"===d&&console.log(o.gray(" Common models: mistral-small-latest, mistral-large-latest")),console.log("");a=(await n(o.white(` Model [${r.defaultModel}]: `))).trim()||r.defaultModel,console.log(o.green(` ✓ Using model: ${a}\n`)),"ollama"===d&&(console.log(o.gray(" Make sure Ollama is running: ollama serve")),console.log(""))}if("ollama"!==d){console.log(o.cyan("2. API Key verification:\n"));const t=r.envVar;let l=process.env[t];if(l)console.log(o.gray(` Found ${t} in environment`));else{console.log(o.yellow(` ${t} not found in environment`)),console.log(o.gray(" You can set it now or add it to your shell config later.\n"));const e=await n(o.white(" Enter API key (or press Enter to skip): "));e.trim()&&(l=e.trim())}let i=!1;for(;!i;){if(!l){console.log(o.yellow(`\n Remember to set ${t} before running tests.`));break}if("n"===(await n(o.white(" Verify API key with test call? (Y/n): "))).toLowerCase()){console.log(o.gray(" Skipping verification\n"));break}{e.pause();const c=s(" Verifying API key...").start();try{const e=k({provider:d,model:a||r.defaultModel||"test",api_key:l});(await y({model:e,prompt:"Reply with only the word 'pong'",maxTokens:10})).text.toLowerCase().includes("pong")?c.succeed(o.green("API key verified! ✓")):c.succeed(o.green("API key works! (got response)")),i=!0}catch(e){c.fail(o.red("API key verification failed")),console.log(o.red(` Error: ${e.message}\n`))}if(e.resume(),!i){if("n"===(await n(o.white(" Try a different API key? (Y/n): "))).toLowerCase()){console.log(o.yellow(` Remember to set ${t} correctly before running tests.`));break}const e=await n(o.white(" Enter API key: "));if(!e.trim()){console.log(o.yellow(" No key entered, skipping.\n"));break}l=e.trim()}}}console.log("")}console.log(o.cyan(("ollama"===d?"2":"3")+". Browser setup:\n"));const p=l();if(p.length>0){console.log(" Found browsers on your system:"),p.forEach((e,s)=>{console.log(o.gray(` ${s+1}) ${e.name}`))}),console.log(o.gray(` ${p.length+1}) Download Chromium (~170MB)`)),console.log(o.gray(` ${p.length+2}) Enter custom path`)),console.log("");const e=await n(o.white(" Select [1]: ")),s=parseInt(e)||1;if(s<=p.length)i=p[s-1].path,c=!0,console.log(o.green(` ✓ Using ${p[s-1].name}\n`));else if(s===p.length+2){const e=await n(o.white(" Enter browser path: "));e.trim()&&(i=e.trim(),c=!0,console.log(o.green(" ✓ Using custom browser\n")))}else c=!1,console.log(o.green(" ✓ Will download Chromium on first run\n"))}else{console.log(" No browsers found. Options:"),console.log(o.gray(" 1) Download Chromium automatically (~170MB)")),console.log(o.gray(" 2) Enter custom browser path")),console.log("");if("2"===await n(o.white(" Select [1]: "))){const e=await n(o.white(" Enter browser path: "));e.trim()&&(i=e.trim(),c=!0,console.log(o.green(" ✓ Using custom browser\n")))}else c=!1,console.log(o.green(" ✓ Will download Chromium on first run\n"))}e.close()}const p=s("Creating configuration...").start();try{r(process.cwd(),{provider:d,model:a,browserPath:i,useSystemBrowser:c}),p.succeed("Slapify initialized!"),console.log(""),console.log(o.green("Created:")),console.log(" 📁 .slapify/config.yaml - Configuration"),console.log(" 🔐 .slapify/credentials.yaml - Credentials (gitignored)"),console.log(" 📝 tests/example.flow - Sample test"),console.log("");const e=g[d];console.log(o.yellow("Next steps:")),console.log(""),"ollama"===d?(console.log(o.white(" 1. Make sure Ollama is running:")),console.log(o.cyan(" ollama serve")),console.log(o.cyan(" ollama pull llama3"))):(console.log(o.white(" 1. Set your API key:")),console.log(o.cyan(` export ${e.envVar}=your-key-here`))),console.log(""),console.log(o.white(" 2. Try an autonomous task:")),console.log(o.cyan(' npx slapify task "Monitor Bitcoin price"')),console.log(""),console.log(o.white(" 3. Run the example test flow:")),console.log(o.cyan(" npx slapify run tests/example.flow")),console.log(""),console.log(o.white(" 4. Create your own persistent tests:")),console.log(o.cyan(" npx slapify create my-first-test")),console.log(o.cyan(' npx slapify generate "test login for myapp.com"')),console.log(""),console.log(o.gray(" Config can be modified anytime in .slapify/config.yaml")),console.log("")}catch(e){p.fail(e.message),process.exit(1)}}),v.command("install").description("Install browser dependencies").action(()=>{const e=s("Checking agent-browser...").start();if(m.isInstalled())e.succeed("agent-browser is already installed");else{e.text="Installing agent-browser...";try{m.install(),e.succeed("Browser dependencies installed!")}catch(o){e.fail(`Installation failed: ${o.message}`),process.exit(1)}}}),v.command("run [files...]").description("Run flow test files").option("--headed","Run browser in headed mode (visible)").option("--report [format]","Generate report folder (html, markdown, json)").option("--output <dir>","Output directory for reports","./test-reports").option("--credentials <profile>","Default credentials profile to use").option("-p, --parallel","Run tests in parallel").option("-w, --workers <n>","Number of parallel workers (default: 4)","4").option("--performance","Run performance audit (scores, real-user metrics, framework & re-render analysis) and include in report").action(async(e,s)=>{try{const l=i();l||(console.log(o.red('No .slapify directory found. Run "slapify init" first.')),process.exit(1));const g=r(l),m=a(l);s.headed&&(g.browser={...g.browser,headless:!1});const y=void 0!==s.report;g.report={...g.report,format:"string"==typeof s.report?s.report:"html",output_dir:s.output,screenshots:y};let w=[];if(0===e.length){const e=t.join(process.cwd(),"tests");n.existsSync(e)&&(w=await d(e))}else for(const o of e)if(n.statSync(o).isDirectory()){const e=await d(o);w.push(...e)}else w.push(o);0===w.length&&(console.log(o.yellow("No .flow files found to run.")),process.exit(0));const h=new u(g.report),$=[],b=s.parallel&&w.length>1,S=parseInt(s.workers)||4;if(b){console.log(o.blue.bold(`\n━━━ Running ${w.length} tests in parallel (${S} workers) ━━━\n`));const e=[...w],s=new Map,n=new Map;for(const e of w){const s=t.basename(e,".flow");n.set(s,o.gray("⏳ pending"))}const l=()=>{process.stdout.write("["+w.length+"A");for(const e of w){const o=t.basename(e,".flow"),s=n.get(o)||"";process.stdout.write("[2K"),console.log(` ${o}: ${s}`)}};for(const e of w){const o=t.basename(e,".flow");console.log(` ${o}: ${n.get(o)}`)}const r=async e=>{const s=c(e),t=s.name;n.set(t,o.cyan("▶ running...")),l();try{const e=new f(g,m),l=await e.runFlow(s);$.push(l),"passed"===l.status?n.set(t,o.green(`✓ passed (${l.passedSteps}/${l.totalSteps} steps, ${(l.duration/1e3).toFixed(1)}s)`)):n.set(t,o.red(`✗ failed (${l.failedSteps} failed, ${l.passedSteps} passed)`))}catch(e){n.set(t,o.red(`✗ error: ${e.message}`))}l()};for(;e.length>0||s.size>0;){for(;e.length>0&&s.size<S;){const o=e.shift(),t=r(o).then(()=>{s.delete(o)});s.set(o,t)}s.size>0&&await Promise.race(s.values())}console.log("")}else for(const e of w){const t=c(e),n=p(t);console.log(""),console.log(o.blue.bold(`━━━ ${t.name} ━━━`)),console.log(o.gray(` ${n.totalSteps} steps (${n.requiredSteps} required, ${n.optionalSteps} optional)`)),console.log("");try{const e=new f(g,m),n=await e.runFlow(t,e=>{const s=e.step,t="passed"===e.status?o.green("✓"):"failed"===e.status?o.red("✗"):o.yellow("⊘"),n=s.optional?o.gray(" [optional]"):"",l=e.retried?o.yellow(" [retried]"):"",r=o.gray(`(${(e.duration/1e3).toFixed(1)}s)`);if(console.log(` ${t} ${s.text}${n}${l} ${r}`),"failed"===e.status&&e.error&&console.log(o.red(` └─ ${e.error}`)),e.assumptions&&e.assumptions.length>0)for(const s of e.assumptions)console.log(o.gray(` └─ 💡 ${s}`))},!!s.performance);if(n.perfAudit){const e=n.perfAudit,s=[];e.vitals.fcp&&s.push(`FCP ${e.vitals.fcp}ms`),e.vitals.lcp&&s.push(`LCP ${e.vitals.lcp}ms`),null!=e.vitals.cls&&s.push(`CLS ${e.vitals.cls}`);const t=e.scores??e.lighthouse;t&&s.push(`Perf ${t.performance}/100`),console.log(o.cyan(` ⚡ Perf: ${s.join(" · ")}`))}$.push(n),console.log(""),"passed"===n.status?console.log(o.green.bold(" ✓ PASSED")+o.gray(` (${n.passedSteps}/${n.totalSteps} steps in ${(n.duration/1e3).toFixed(1)}s)`)):console.log(o.red.bold(" ✗ FAILED")+o.gray(` (${n.failedSteps} failed, ${n.passedSteps} passed)`)),n.autoHandled.length>0&&console.log(o.gray(` ℹ Auto-handled: ${n.autoHandled.join(", ")}`))}catch(e){console.log(o.red(` ✗ ERROR: ${e.message}`))}}console.log(""),console.log(o.blue.bold("━━━ Summary ━━━"));const x=$.filter(e=>"passed"===e.status).length,k=$.filter(e=>"failed"===e.status).length,v=$.reduce((e,o)=>e+o.totalSteps,0),A=$.reduce((e,o)=>e+o.passedSteps,0);if(console.log(o.gray(` ${$.length} test file(s), ${v} total steps`)),0===k?console.log(o.green.bold(` ✓ All ${x} test(s) passed! (${A}/${v} steps)`)):console.log(o.red.bold(` ✗ ${k}/${$.length} test(s) failed`)),y&&$.length>0){let e;e=1===$.length?h.saveAsFolder($[0]):h.saveSuiteAsFolder($),console.log(o.cyan(`\n 📄 Report: ${e}`))}console.log(""),k>0&&process.exit(1)}catch(e){console.error(o.red(`Error: ${e.message}`)),process.exit(1)}}),v.command("create <name>").description("Create a new flow file").option("-d, --dir <directory>","Directory to create flow in","tests").action(async(e,s)=>{const l=await import("readline"),r=s.dir;n.existsSync(r)||n.mkdirSync(r,{recursive:!0});const a=e.endsWith(".flow")?e:`${e}.flow`,i=t.join(r,a);n.existsSync(i)&&(console.log(o.red(`File already exists: ${i}`)),process.exit(1)),console.log(o.blue(`\nCreating: ${i}`)),console.log(o.gray("Enter your test steps (one per line). Empty line to finish.\n"));const c=l.createInterface({input:process.stdin,output:process.stdout}),d=[`# ${e}`,""];let g=1;const p=()=>new Promise(e=>{c.question(o.cyan(`${g}. `),o=>{e(o)})});for(;;){const e=await p();if(""===e)break;d.push(e),g++}c.close(),d.length<=2?console.log(o.yellow("\nNo steps entered. File not created.")):(n.writeFileSync(i,d.join("\n")+"\n"),console.log(o.green(`\n✓ Created: ${i}`)),console.log(o.gray(` ${g-1} steps`)),console.log(o.gray(`\nRun with: slapify run ${i}`)))}),v.command("generate <prompt>").alias("gen").description("Generate a verified .flow file by running the goal as a task and recording what worked").option("-d, --dir <directory>","Directory to save flow","tests").option("--headed","Show browser window while running").action(async(e,s)=>{const t=i();t||(console.log(o.red('No .slapify directory found. Run "slapify init" first.')),process.exit(1));r(t);console.log(o.blue("\n🤖 Flow Generator\n")),console.log(o.gray(" Running the goal in the browser to discover the real path...\n"));const{runTask:n}=await import("./task/runner.js");let l;await n({goal:e,headed:s.headed,saveFlow:!0,flowOutputDir:s.dir,onEvent:e=>{"status_update"===e.type&&process.stdout.write(o.gray(` → ${e.message}\n`)),"message"===e.type&&console.log(o.white(`\n${e.text}`)),"flow_saved"===e.type&&(l=e.path),"done"===e.type&&console.log(o.green("\n✅ Done")),"error"===e.type&&console.log(o.red(`\n✗ ${e.error}`))}}),l?(console.log(o.green(`\n✓ Flow saved: ${l}`)),console.log(o.gray(` Run with: slapify run ${l}`))):console.log(o.yellow("\n⚠ No flow was saved. The agent may not have completed the goal."))}),v.command("fix <file>").description("Analyze a failing test and suggest/apply fixes").option("--auto","Automatically apply suggested fixes without confirmation").option("--headed","Run browser in headed mode for debugging").action(async(e,t)=>{const l=i();l||(console.log(o.red('No .slapify directory found. Run "slapify init" first.')),process.exit(1)),n.existsSync(e)||(console.log(o.red(`File not found: ${e}`)),process.exit(1));const d=r(l),g=a(l);t.headed&&(d.browser={...d.browser,headless:!1}),d.report={...d.report,screenshots:!0};const p=await import("readline");let u=s("Running test to identify failures...").start();try{const l=c(e),r=new f(d,g),a=[];if("passed"===(await r.runFlow(l,e=>{"failed"===e.status&&e.error&&a.push({step:e.step.text,error:e.error,line:e.step.line,screenshot:e.screenshot})})).status)return void u.succeed("Test passed! No fixes needed.");if(u.info(`Found ${a.length} failing step(s)`),0===a.length)return void console.log(o.yellow("No specific step failures to fix."));const i=n.readFileSync(e,"utf-8"),m=i.split("\n");u=s("Analyzing failures and generating fixes...").start();const w=a.map(e=>`Line ${e.line}: "${e.step}"\n Error: ${e.error}`).join("\n\n"),h=await y({model:k(Array.isArray(d.llm)?d.llm[0]:d.llm),system:`You are a test automation expert. Analyze failing test steps and suggest fixes.\n\nOriginal flow file:\n\`\`\`\n${i}\n\`\`\`\n\nFailing steps:\n${w}\n\nBased on the errors, suggest fixes for the flow file. Common issues and fixes:\n1. Element not found → Try more descriptive text, add wait, or make step optional\n2. Timeout → Add explicit wait or increase timeout\n3. Navigation error → Add wait after navigation, or split into smaller steps\n4. Element obscured → Add step to close popup/modal first\n5. Stale element → Add wait for page to stabilize\n\nRespond with JSON:\n{\n "analysis": "Brief explanation of what's wrong",\n "fixes": [\n {\n "line": 5,\n "original": "Click the submit button",\n "fixed": "Click the Submit button",\n "reason": "Button text is capitalized"\n }\n ],\n "additions": [\n {\n "afterLine": 4,\n "step": "[Optional] Wait for page to load",\n "reason": "Page might still be loading"\n }\n ]\n}`,prompt:"Analyze the failures and suggest specific fixes.",maxTokens:1500});u.succeed("Analysis complete");const $=h.text.match(/\{[\s\S]*\}/);if(!$)return void console.log(o.red("Could not parse AI response"));const b=JSON.parse($[0]);if(console.log(o.blue("\n━━━ Analysis ━━━\n")),console.log(o.white(b.analysis)),b.fixes?.length>0||b.additions?.length>0){if(console.log(o.blue("\n━━━ Suggested Fixes ━━━\n")),b.fixes?.length>0)for(const e of b.fixes)console.log(o.yellow(`Line ${e.line}:`)),console.log(o.red(` - ${e.original}`)),console.log(o.green(` + ${e.fixed}`)),console.log(o.gray(` Reason: ${e.reason}\n`));if(b.additions?.length>0){console.log(o.yellow("New steps to add:"));for(const e of b.additions)console.log(o.green(` + After line ${e.afterLine}: ${e.step}`)),console.log(o.gray(` Reason: ${e.reason}\n`))}let s=t.auto;if(!s){const e=p.createInterface({input:process.stdin,output:process.stdout}),t=await new Promise(s=>{e.question(o.cyan("Apply these fixes? (y/N): "),o=>{e.close(),s(o.trim().toLowerCase())})});s="y"===t||"yes"===t}if(s){let s=[...m];new Map;if(b.fixes)for(const e of b.fixes){const o=e.line-1;o>=0&&o<s.length&&(s[o]=e.fixed)}if(b.additions){const e=[...b.additions].sort((e,o)=>o.afterLine-e.afterLine);for(const o of e){const e=o.afterLine;s.splice(e,0,o.step)}}const t=s.join("\n"),l=e+".backup";n.writeFileSync(l,i),n.writeFileSync(e,t),console.log(o.green(`\n✓ Fixes applied to ${e}`)),console.log(o.gray(` Backup saved to ${l}`)),console.log(o.gray(`\nRun again with: slapify run ${e}`))}else console.log(o.yellow("No changes made."))}else console.log(o.yellow("\nNo automatic fixes suggested.")),console.log(o.gray("The failures may require manual investigation."))}catch(e){u.fail("Error"),console.error(o.red(`Error: ${e.message}`)),process.exit(1)}}),v.command("validate [files...]").description("Validate flow files for syntax issues").action(async e=>{let s=[];if(0===e.length){const e=t.join(process.cwd(),"tests");n.existsSync(e)&&(s=await d(e))}else s=e;if(0===s.length)return void console.log(o.yellow("No .flow files found."));let l=!1;for(const e of s)try{const s=c(e),t=g(s),n=p(s);if(t.length>0){l=!0,console.log(o.yellow(`⚠️ ${e}`));for(const e of t)console.log(o.yellow(` ${e}`))}else console.log(o.green(`✅ ${e}`)),console.log(o.gray(` ${n.totalSteps} steps (${n.requiredSteps} required, ${n.optionalSteps} optional)`))}catch(s){console.log(o.red(`❌ ${e}`)),console.log(o.red(` ${s.message}`)),l=!0}l&&process.exit(1)}),v.command("list").description("List all flow files").action(async()=>{const e=t.join(process.cwd(),"tests");if(!n.existsSync(e))return void console.log(o.yellow("No tests directory found."));const s=await d(e);if(0!==s.length){console.log(o.blue(`\nFound ${s.length} flow file(s):\n`));for(const e of s){const s=c(e),n=p(s),l=t.relative(process.cwd(),e);console.log(` ${o.white(l)}`),console.log(o.gray(` ${n.totalSteps} steps (${n.requiredSteps} required, ${n.optionalSteps} optional)`))}console.log("")}else console.log(o.yellow("No .flow files found."))}),v.command("credentials").description("List configured credential profiles").action(()=>{const e=i();e||(console.log(o.red('No .slapify directory found. Run "slapify init" first.')),process.exit(1));const s=a(e),t=Object.keys(s.profiles);if(0===t.length)return console.log(o.yellow("No credential profiles configured.")),void console.log(o.gray("Edit .slapify/credentials.yaml to add profiles."));console.log(o.blue("\nConfigured credential profiles:\n"));for(const e of t){const t=s.profiles[e];console.log(` ${o.white(e)} (${t.type})`),t.username&&console.log(o.gray(` username: ${t.username}`)),t.email&&console.log(o.gray(` email: ${t.email}`)),t.totp_secret&&console.log(o.gray(" 2FA: TOTP configured")),t.fixed_otp&&console.log(o.gray(" 2FA: Fixed OTP configured"))}console.log("")}),v.command("fix-credentials [files...]").description("Fix credential YAML files where localStorage/sessionStorage were saved as JSON strings").option("--dry-run","Only print what would be fixed").action((e,s)=>{const n=[];if(e&&e.length>0)n.push(...e.map(e=>t.resolve(e)));else{const e=process.cwd();n.push(t.join(e,"temp_credentials.yaml"));const o=i();o&&n.push(t.join(o,"credentials.yaml"))}console.log(o.blue("\n🔧 Fix credential YAML files\n")),s.dryRun&&console.log(o.gray(" (dry run – no files will be modified)\n"));let l=0;for(const e of n)I(e,!!s.dryRun)&&l++;0===l&&n.length>0&&console.log(o.gray("\n No files needed fixing.")),console.log("")}),v.command("interactive [url]").alias("i").description("Run steps interactively").option("--headed","Run browser in headed mode").action(async(e,s)=>{console.log(o.blue("\n🧪 Slapify Interactive Mode")),console.log(o.gray('Type test steps and press Enter to execute. Type "exit" to quit.\n'));const t=i();t||(console.log(o.red('No .slapify directory found. Run "slapify init" first.')),process.exit(1));const n=r(t),l=a(t);s.headed&&(n.browser={...n.browser,headless:!1});new f(n,l);e&&console.log(o.gray(`Navigating to ${e}...`));const c=(await import("readline")).createInterface({input:process.stdin,output:process.stdout}),d=()=>{c.question(o.cyan("> "),async e=>{const s=e.trim();"exit"!==s.toLowerCase()&&"quit"!==s.toLowerCase()||(console.log(o.gray("\nGoodbye!")),c.close(),process.exit(0)),s?(console.log(o.gray(`Executing: ${s}`)),console.log(o.green("✓ Done")),d()):d()})};d()}),v.command("task [goal]").description('Run an autonomous AI agent task in plain English.\n The agent decides everything: what to do, when to schedule, when to sleep.\n Examples:\n slapify task "Go to linkedin.com and like the latest 3 posts"\n slapify task "Monitor my Gmail for new emails every 30 min and log subjects"\n slapify task "Order breakfast from Swiggy every day at 8am"').option("--headed","Show the browser window").option("--debug","Show all tool calls and internal steps").option("--report","Generate an HTML report after the task completes").option("--save-flow","Save agent steps as a reusable .flow file when done").option("--session <id>","Resume an existing task session").option("--list-sessions","List all task sessions").option("--logs <id>","Show logs for a task session").option("--max-iterations <n>","Safety cap on agent loop iterations (default 400)",parseInt).option("--schema <json-or-file>","JSON Schema (inline JSON string or path to a .json file) the agent should use to structure its output").option("--output <file>","File path to write structured JSON output to (used together with --schema)").action(async(e,s)=>{if(s.listSessions){const{listSessions:e}=await import("./task/index.js"),s=e();if(0===s.length)return void console.log(o.gray("\nNo task sessions found.\n"));console.log(o.blue(`\n📋 Task Sessions (${s.length})\n`));for(const e of s){const s="completed"===e.status?o.green:"failed"===e.status?o.red:"scheduled"===e.status?o.blue:o.yellow;console.log(` ${s("●")} ${o.bold(e.id)}\n Goal: ${e.goal.slice(0,70)}${e.goal.length>70?"…":""}\n Status: ${s(e.status)} Iterations: ${e.iteration}\n Updated: ${new Date(e.updatedAt).toLocaleString()}\n`)}return}if(s.logs){const{loadSession:e}=await import("./task/index.js"),{loadEvents:t}=await import("./task/session.js"),n=e(s.logs);n||(console.log(o.red(`Session '${s.logs}' not found.`)),process.exit(1)),console.log(o.blue(`\n📜 Logs: ${n.id}\n`)),console.log(o.gray(`Goal: ${n.goal}\n`));const l=t(s.logs);for(const e of l){const s=o.gray(new Date(e.ts).toLocaleTimeString());"llm_response"===e.type?e.text&&console.log(`${s} 🤔 ${o.cyan(e.text.slice(0,120))}`):"tool_call"===e.type?console.log(`${s} 🔧 ${o.yellow(e.toolName)} → ${o.gray(JSON.stringify(e.result).slice(0,80))}`):"tool_error"===e.type?console.log(`${s} ❌ ${o.red(e.toolName)} → ${o.red(e.error.slice(0,80))}`):"memory_update"===e.type?console.log(`${s} 🧠 ${o.magenta("remember")} ${e.key} = ${e.value.slice(0,60)}`):"scheduled"===e.type?console.log(`${s} ⏰ ${o.blue("schedule")} ${e.cron} — ${e.task}`):"sleeping_until"===e.type?console.log(`${s} 😴 ${o.blue("sleep")} until ${e.until}`):"session_end"===e.type&&console.log(`${s} ✅ ${o.green("done")} ${e.summary.slice(0,120)}`)}return void console.log("")}let l=e||s.session?e:void 0;if(l||s.session||(console.log(o.red('\nPlease provide a goal. Example:\n slapify task "Go to example.com and check the title"\n')),process.exit(1)),!l&&s.session){const{loadSession:e}=await import("./task/index.js"),t=e(s.session);t||(console.log(o.red(`Session '${s.session}' not found.`)),process.exit(1)),l=t.goal}i()||(console.log(o.red('\nNo .slapify directory found. Run "slapify init" first.\n')),process.exit(1));let r=null;const a=async e=>{if(s.report)try{const{loadEvents:s,saveTaskReport:t}=await import("./task/index.js"),n=t(e,s(e.id));console.log(o.cyan(`\n 📊 Report: ${n}`))}catch(e){console.log(o.yellow(` ⚠ Could not generate report: ${e?.message}`))}},c=!!s.debug,d=()=>process.stdout.write("[2K\r");console.log(o.blue("\n🤖 Slapify Task Agent\n")),console.log(o.white(` Goal: ${l}`)),s.session&&console.log(o.gray(` Resuming session: ${s.session}`)),console.log(o.gray([s.report?" --report: HTML report on exit":"",c?" --debug: verbose output":""," Ctrl+C to stop"].filter(Boolean).join(" · ")+"\n")),console.log(o.gray("─".repeat(60))+"\n");let g=null,p=!1;const f=()=>{if(c||p||g)return;const e=["⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏"];let s=0;g=setInterval(()=>{process.stdout.write(o.gray(`\r ${e[s++%e.length]} working...`))},80)};c||f();const{runTask:u}=await import("./task/index.js");let m=!1;const y=async()=>{if(!m){if(m=!0,clearInterval(g),g=null,p=!0,process.stdout.write("[2K\r"),console.log(o.yellow("\n ⚡ Interrupted"+(s.report?" — generating report...":""))),r){r.status="failed",r.finalSummary="Task interrupted by user (Ctrl+C).";const{saveSessionMeta:e}=await import("./task/session.js");e(r),await a(r)}console.log(o.gray(" Goodbye.\n")),process.exit(0)}};process.once("SIGINT",y);try{const e=()=>{g&&(clearInterval(g),g=null),process.stdout.write("[2K\r")};let i;if(s.schema)try{i=JSON.parse(s.schema)}catch{try{const e=n.readFileSync(t.resolve(s.schema),"utf8");i=JSON.parse(e)}catch{console.log(o.red("Could not parse --schema: expected inline JSON or a valid .json file path.")),process.exit(1)}}const m=await u({goal:l,sessionId:s.session,headed:s.headed,saveFlow:s.saveFlow,maxIterations:s.maxIterations,schema:i,outputFile:s.output,onHumanInput:async(s,t)=>{p=!0,e();const n=(await import("readline")).createInterface({input:process.stdin,output:process.stdout,terminal:!0}),l=await new Promise(e=>{n.question(` ${o.cyan("›")} `,o=>{n.close(),e(o.trim())})});return console.log(o.yellow("─".repeat(60))+"\n"),p=!1,f(),l},onEvent:s=>{const t="status_update"===s.type||"human_input_needed"===s.type||"credentials_saved"===s.type||"done"===s.type||"error"===s.type||"tool_error"===s.type;t&&e(),(e=>{switch(e.type){case"thinking":c&&process.stdout.write(o.gray(" ⟳ thinking...\r"));break;case"message":c&&(d(),console.log(o.gray(` 💬 ${e.text}`)));break;case"tool_start":if(c){d();const s=JSON.stringify(e.args);console.log(o.dim(` › ${o.cyan(e.toolName)} `)+o.gray(s.slice(0,100)+(s.length>100?"…":"")))}break;case"tool_done":c&&console.log(o.dim(` ✓ ${e.result.slice(0,120)}`));break;case"tool_error":d(),console.log(o.red(` ✗ ${e.toolName}: ${e.error.slice(0,120)}`));break;case"status_update":d(),console.log(o.white(` ${e.message}`));break;case"human_input_needed":console.log("\n"+o.yellow("─".repeat(60))),console.log(o.yellow.bold(" 🙋 Agent needs your input")),console.log(o.white(`\n ${e.question}`)),e.hint&&console.log(o.gray(` ${e.hint}`));break;case"credentials_saved":d(),console.log(o.green(` 💾 Credentials saved: '${e.profileName}' (${e.credType}) → .slapify/credentials.yaml`));break;case"scheduled":c&&(d(),console.log(o.dim(` ⏰ scheduled: ${e.cron} — ${e.task}`)));break;case"sleeping":c&&(d(),console.log(o.dim(` 😴 sleeping until ${new Date(e.until).toLocaleString()}`)));break;case"done":d(),console.log("\n"+o.green("─".repeat(60))),console.log(o.green.bold(" ✅ Task complete!")),console.log(o.white(`\n ${e.summary}`)),console.log(o.green("─".repeat(60)));break;case"error":d(),console.log(o.red(`\n ✗ Error: ${e.error}`))}})(s),t&&"done"!==s.type&&"error"!==s.type&&"human_input_needed"!==s.type&&f()},onSessionUpdate:e=>{r=e}});if(e(),process.removeListener("SIGINT",y),console.log(o.gray(`\n Session: ${m.id}`)),m.savedFlowPath&&console.log(o.cyan(` Flow saved: ${m.savedFlowPath}`)),s.output&&null!=m.structuredOutput&&console.log(o.cyan(` Output: ${t.resolve(s.output)}`)),Object.keys(m.memory).length>0){console.log(o.gray(` Memory (${Object.keys(m.memory).length} items):`));for(const[e,s]of Object.entries(m.memory))console.log(o.gray(` • ${e}: ${s.slice(0,80)}`))}await a(m),console.log("")}catch(e){process.removeListener("SIGINT",y),clearInterval(g),g=null,process.stdout.write("[2K\r"),console.error(o.red(`\n Task failed: ${e?.message||e}`)),r&&await a(r),process.exit(1)}}),v.parse();
|
|
2
|
+
import{Command as e}from"commander";import o from"chalk";import s from"ora";import t from"path";import n from"fs";import l from"dotenv";import{loadConfig as r,loadCredentials as a,getConfigDir as i}from"./config/loader.js";import{parseFlowFile as c,findFlowFiles as d,validateFlowFile as g,getFlowSummary as p}from"./parser/flow.js";import{TestRunner as u}from"./runner/index.js";import{ReportGenerator as f}from"./report/generator.js";import{BrowserAgent as m}from"./browser/agent.js";import{generateText as y}from"ai";import{createAnthropic as w}from"@ai-sdk/anthropic";import{createOpenAI as h}from"@ai-sdk/openai";import{createGoogleGenerativeAI as $}from"@ai-sdk/google";import{createMistral as b}from"@ai-sdk/mistral";import{createGroq as S}from"@ai-sdk/groq";import k from"yaml";function v(e){switch(e.provider){case"anthropic":return w({apiKey:e.api_key})(e.model);case"openai":return h({apiKey:e.api_key})(e.model);case"google":return $({apiKey:e.api_key})(e.model);case"mistral":return b({apiKey:e.api_key})(e.model);case"groq":return S({apiKey:e.api_key})(e.model);case"ollama":return h({apiKey:"ollama",baseURL:e.base_url||"http://localhost:11434/v1"})(e.model);default:throw new Error(`Unsupported provider: ${e.provider}`)}}l.config();const x=new e;function A(e){if("string"==typeof e)try{e=JSON.parse(e)}catch{return{}}if(!e||"object"!=typeof e||Array.isArray(e))return{};const o={};for(const[s,t]of Object.entries(e))o[String(s)]="string"==typeof t?t:JSON.stringify(t);return o}function I(e,s){const l=t.resolve(e);if(!n.existsSync(l))return console.log(o.yellow(` Skip (not found): ${l}`)),!1;const r=n.readFileSync(l,"utf-8");let a;try{a=k.parse(r)}catch(e){return console.log(o.red(` Invalid YAML: ${l}`)),console.log(o.gray(` ${e.message}`)),!1}if(!a||!a.profiles||"object"!=typeof a.profiles)return console.log(o.yellow(` No profiles in: ${l}`)),!1;let i=!1;for(const[e,o]of Object.entries(a.profiles)){if("inject"!==o.type)continue;const s="string"==typeof o.localStorage||o.localStorage&&(Array.isArray(o.localStorage)||"object"!=typeof o.localStorage),t="string"==typeof o.sessionStorage||o.sessionStorage&&(Array.isArray(o.sessionStorage)||"object"!=typeof o.sessionStorage);(s||t)&&(a.profiles[e]={...o,...s&&{localStorage:A(o.localStorage)},...t&&{sessionStorage:A(o.sessionStorage)}},i=!0)}if(!i)return console.log(o.gray(` No changes needed: ${l}`)),!1;if(s)return console.log(o.cyan(` Would fix: ${l}`)),!0;const c=l+".backup";return n.copyFileSync(l,c),n.writeFileSync(l,k.stringify(a,{indent:2,lineWidth:0})),console.log(o.green(` Fixed: ${l}`)),console.log(o.gray(` Backup: ${c}`)),!0}x.name("slapify").description("AI-powered browser automation that slaps — run autonomous agents, audit performance, and write E2E tests in plain English").version("0.0.22","-v, -V, --version"),x.command("init").description("Initialize Slapify in the current directory").option("-y, --yes","Skip prompts and use defaults").action(async e=>{const t=await import("readline");if(n.existsSync(".slapify"))return console.log(o.yellow("Slapify is already initialized in this directory.")),void console.log(o.gray("Delete .slapify folder to reinitialize."));console.log(o.bold.magenta("\n🖐️ slapify\n")+o.gray("AI-powered browser automation that slaps - by slaps.dev\n"));const{findSystemBrowsers:l,initConfig:r}=await import("./config/loader.js");let a,i,c,d="anthropic";const g={anthropic:{name:"Anthropic (Claude)",envVar:"ANTHROPIC_API_KEY",defaultModel:"claude-sonnet-4-6",models:[{id:"claude-sonnet-4-6",name:"Sonnet 4.6 (claude-sonnet-4-6) - highly capable & recommended",recommended:!0},{id:"claude-sonnet-4-5",name:"Sonnet 4.5 - fast & reliable"},{id:"claude-opus-4-6",name:"Opus 4.6 - extended reasoning & advanced tasks"},{id:"claude-3-7-sonnet-latest",name:"Sonnet 3.7 - legacy hybrid reasoning"},{id:"custom",name:"Enter custom model ID"}]},openai:{name:"OpenAI",envVar:"OPENAI_API_KEY",defaultModel:"gpt-5.2",models:[{id:"gpt-5.2",name:"GPT-5.2 - latest flagship model",recommended:!0},{id:"o3-mini",name:"o3-mini - fast reasoning & coding"},{id:"gpt-5.3-codex",name:"GPT-5.3 Codex - advanced coding"},{id:"custom",name:"Enter custom model ID"}]},google:{name:"Google (Gemini)",envVar:"GOOGLE_API_KEY",defaultModel:"gemini-3-flash",models:[{id:"gemini-3-flash",name:"Gemini 3 Flash - balanced & fast",recommended:!0},{id:"gemini-3.1-pro",name:"Gemini 3.1 Pro - highly capable"},{id:"gemini-2.5-flash",name:"Gemini 2.5 Flash - older stable"},{id:"custom",name:"Enter custom model ID"}]},mistral:{name:"Mistral",envVar:"MISTRAL_API_KEY",askModel:!0,defaultModel:"mistral-small-latest"},groq:{name:"Groq (Fast inference)",envVar:"GROQ_API_KEY",askModel:!0,defaultModel:"llama-3.3-70b-versatile"},ollama:{name:"Ollama (Local)",envVar:"",askModel:!0,defaultModel:"llama3"}};if(e.yes)console.log(o.gray("Using default settings...\n"));else{const e=t.createInterface({input:process.stdin,output:process.stdout}),n=o=>new Promise(s=>e.question(o,s));console.log(o.cyan("1. Choose your LLM provider:\n")),console.log(" 1) Anthropic (Claude) "+o.green("- recommended")),console.log(" 2) OpenAI (GPT-4)"),console.log(" 3) Google (Gemini)"),console.log(" 4) Mistral"),console.log(" 5) Groq "+o.gray("- fast & free tier")),console.log(" 6) Ollama "+o.gray("- local, no API key")),console.log("");d={1:"anthropic",2:"openai",3:"google",4:"mistral",5:"groq",6:"ollama"}[await n(o.white(" Select [1]: "))]||"anthropic";const r=g[d];if(console.log(o.green(` ✓ Using ${r.name}\n`)),r.models&&r.models.length>0){console.log(o.cyan(" Choose model:\n")),r.models.forEach((e,s)=>{const t=e.recommended?o.green(" ← recommended"):"";console.log(` ${s+1}) ${e.name}${t}`)}),console.log("");const e=await n(o.white(" Select [1]: ")),s=parseInt(e)-1||0,t=r.models[s];if("custom"===t?.id){a=(await n(o.white(" Enter model ID: "))).trim()||r.defaultModel}else a=t?.id||r.defaultModel;console.log(o.green(` ✓ Using model: ${a}\n`))}else if(r.askModel){console.log(o.gray(` Enter model ID (default: ${r.defaultModel})`)),"ollama"===d?console.log(o.gray(" Common models: llama3, mistral, codellama, phi3")):"groq"===d?console.log(o.gray(" Common models: llama-3.3-70b-versatile, mixtral-8x7b-32768")):"mistral"===d&&console.log(o.gray(" Common models: mistral-small-latest, mistral-large-latest")),console.log("");a=(await n(o.white(` Model [${r.defaultModel}]: `))).trim()||r.defaultModel,console.log(o.green(` ✓ Using model: ${a}\n`)),"ollama"===d&&(console.log(o.gray(" Make sure Ollama is running: ollama serve")),console.log(""))}if("ollama"!==d){console.log(o.cyan("2. API Key verification:\n"));const t=r.envVar;let l=process.env[t];if(l)console.log(o.gray(` Found ${t} in environment`));else{console.log(o.yellow(` ${t} not found in environment`)),console.log(o.gray(" You can set it now or add it to your shell config later.\n"));const e=await n(o.white(" Enter API key (or press Enter to skip): "));e.trim()&&(l=e.trim())}let i=!1;for(;!i;){if(!l){console.log(o.yellow(`\n Remember to set ${t} before running tests.`));break}if("n"===(await n(o.white(" Verify API key with test call? (Y/n): "))).toLowerCase()){console.log(o.gray(" Skipping verification\n"));break}{e.pause();const c=s(" Verifying API key...").start();try{const e=v({provider:d,model:a||r.defaultModel||"test",api_key:l});(await y({model:e,prompt:"Reply with only the word 'pong'",maxTokens:10})).text.toLowerCase().includes("pong")?c.succeed(o.green("API key verified! ✓")):c.succeed(o.green("API key works! (got response)")),i=!0}catch(e){c.fail(o.red("API key verification failed")),console.log(o.red(` Error: ${e.message}\n`))}if(e.resume(),!i){if("n"===(await n(o.white(" Try a different API key? (Y/n): "))).toLowerCase()){console.log(o.yellow(` Remember to set ${t} correctly before running tests.`));break}const e=await n(o.white(" Enter API key: "));if(!e.trim()){console.log(o.yellow(" No key entered, skipping.\n"));break}l=e.trim()}}}console.log("")}console.log(o.cyan(("ollama"===d?"2":"3")+". Browser setup:\n"));const p=l();if(p.length>0){console.log(" Found browsers on your system:"),p.forEach((e,s)=>{console.log(o.gray(` ${s+1}) ${e.name}`))}),console.log(o.gray(` ${p.length+1}) Download Chromium (~170MB)`)),console.log(o.gray(` ${p.length+2}) Enter custom path`)),console.log("");const e=await n(o.white(" Select [1]: ")),s=parseInt(e)||1;if(s<=p.length)i=p[s-1].path,c=!0,console.log(o.green(` ✓ Using ${p[s-1].name}\n`));else if(s===p.length+2){const e=await n(o.white(" Enter browser path: "));e.trim()&&(i=e.trim(),c=!0,console.log(o.green(" ✓ Using custom browser\n")))}else c=!1,console.log(o.green(" ✓ Will download Chromium on first run\n"))}else{console.log(" No browsers found. Options:"),console.log(o.gray(" 1) Download Chromium automatically (~170MB)")),console.log(o.gray(" 2) Enter custom browser path")),console.log("");if("2"===await n(o.white(" Select [1]: "))){const e=await n(o.white(" Enter browser path: "));e.trim()&&(i=e.trim(),c=!0,console.log(o.green(" ✓ Using custom browser\n")))}else c=!1,console.log(o.green(" ✓ Will download Chromium on first run\n"))}e.close()}const p=s("Creating configuration...").start();try{r(process.cwd(),{provider:d,model:a,browserPath:i,useSystemBrowser:c}),p.succeed("Slapify initialized!"),console.log(""),console.log(o.green("Created:")),console.log(" 📁 .slapify/config.yaml - Configuration"),console.log(" 🔐 .slapify/credentials.yaml - Credentials (gitignored)"),console.log(" 📝 tests/example.flow - Sample test"),console.log("");const e=g[d];console.log(o.yellow("Next steps:")),console.log(""),"ollama"===d?(console.log(o.white(" 1. Make sure Ollama is running:")),console.log(o.cyan(" ollama serve")),console.log(o.cyan(" ollama pull llama3"))):(console.log(o.white(" 1. Set your API key:")),console.log(o.cyan(` export ${e.envVar}=your-key-here`))),console.log(""),console.log(o.white(" 2. Try an autonomous task:")),console.log(o.cyan(' npx slapify task "Monitor Bitcoin price"')),console.log(""),console.log(o.white(" 3. Run the example test flow:")),console.log(o.cyan(" npx slapify run tests/example.flow")),console.log(""),console.log(o.white(" 4. Create your own persistent tests:")),console.log(o.cyan(" npx slapify create my-first-test")),console.log(o.cyan(' npx slapify generate "test login for myapp.com"')),console.log(""),console.log(o.gray(" Config can be modified anytime in .slapify/config.yaml")),console.log("")}catch(e){p.fail(e.message),process.exit(1)}}),x.command("install").description("Install browser dependencies").action(()=>{const e=s("Checking agent-browser...").start();if(m.isInstalled())e.succeed("agent-browser is already installed");else{e.text="Installing agent-browser...";try{m.install(),e.succeed("Browser dependencies installed!")}catch(o){e.fail(`Installation failed: ${o.message}`),process.exit(1)}}}),x.command("run [files...]").description("Run flow test files").option("--headed","Run browser in headed mode (visible)").option("--report [format]","Generate report folder (html, markdown, json)").option("--output <dir>","Output directory for reports","./test-reports").option("--credentials <profile>","Default credentials profile to use").option("-p, --parallel","Run tests in parallel").option("-w, --workers <n>","Number of parallel workers (default: 4)","4").option("--performance","Run performance audit (scores, real-user metrics, framework & re-render analysis) and include in report").action(async(e,s)=>{try{const l=i();l||(console.log(o.red('No .slapify directory found. Run "slapify init" first.')),process.exit(1));const g=r(l),m=a(l);s.headed&&(g.browser={...g.browser,headless:!1});const y=void 0!==s.report;g.report={...g.report,format:"string"==typeof s.report?s.report:"html",output_dir:s.output,screenshots:y};let w=[];if(0===e.length){const e=t.join(process.cwd(),"tests");n.existsSync(e)&&(w=await d(e))}else for(const o of e)if(n.statSync(o).isDirectory()){const e=await d(o);w.push(...e)}else w.push(o);0===w.length&&(console.log(o.yellow("No .flow files found to run.")),process.exit(0));const h=new f(g.report),$=[],b=s.parallel&&w.length>1,S=parseInt(s.workers)||4;if(b){console.log(o.blue.bold(`\n━━━ Running ${w.length} tests in parallel (${S} workers) ━━━\n`));const e=[...w],s=new Map,n=new Map;for(const e of w){const s=t.basename(e,".flow");n.set(s,o.gray("⏳ pending"))}const l=()=>{process.stdout.write("["+w.length+"A");for(const e of w){const o=t.basename(e,".flow"),s=n.get(o)||"";process.stdout.write("[2K"),console.log(` ${o}: ${s}`)}};for(const e of w){const o=t.basename(e,".flow");console.log(` ${o}: ${n.get(o)}`)}const r=async e=>{const s=c(e),t=s.name;n.set(t,o.cyan("▶ running...")),l();try{const e=new u(g,m),l=await e.runFlow(s);$.push(l),"passed"===l.status?n.set(t,o.green(`✓ passed (${l.passedSteps}/${l.totalSteps} steps, ${(l.duration/1e3).toFixed(1)}s)`)):n.set(t,o.red(`✗ failed (${l.failedSteps} failed, ${l.passedSteps} passed)`))}catch(e){n.set(t,o.red(`✗ error: ${e.message}`))}l()};for(;e.length>0||s.size>0;){for(;e.length>0&&s.size<S;){const o=e.shift(),t=r(o).then(()=>{s.delete(o)});s.set(o,t)}s.size>0&&await Promise.race(s.values())}console.log("")}else for(const e of w){const t=c(e),n=p(t);console.log(""),console.log(o.blue.bold(`━━━ ${t.name} ━━━`)),console.log(o.gray(` ${n.totalSteps} steps (${n.requiredSteps} required, ${n.optionalSteps} optional)`)),console.log("");try{const e=new u(g,m),n=await e.runFlow(t,e=>{const s=e.step,t="passed"===e.status?o.green("✓"):"failed"===e.status?o.red("✗"):o.yellow("⊘"),n=s.optional?o.gray(" [optional]"):"",l=e.retried?o.yellow(" [retried]"):"",r=o.gray(`(${(e.duration/1e3).toFixed(1)}s)`);if(console.log(` ${t} ${s.text}${n}${l} ${r}`),"failed"===e.status&&e.error&&console.log(o.red(` └─ ${e.error}`)),e.assumptions&&e.assumptions.length>0)for(const s of e.assumptions)console.log(o.gray(` └─ 💡 ${s}`))},!!s.performance);if(n.perfAudit){const e=n.perfAudit,s=[];e.vitals.fcp&&s.push(`FCP ${e.vitals.fcp}ms`),e.vitals.lcp&&s.push(`LCP ${e.vitals.lcp}ms`),null!=e.vitals.cls&&s.push(`CLS ${e.vitals.cls}`);const t=e.scores??e.lighthouse;t&&s.push(`Perf ${t.performance}/100`),console.log(o.cyan(` ⚡ Perf: ${s.join(" · ")}`))}$.push(n),console.log(""),"passed"===n.status?console.log(o.green.bold(" ✓ PASSED")+o.gray(` (${n.passedSteps}/${n.totalSteps} steps in ${(n.duration/1e3).toFixed(1)}s)`)):console.log(o.red.bold(" ✗ FAILED")+o.gray(` (${n.failedSteps} failed, ${n.passedSteps} passed)`)),n.autoHandled.length>0&&console.log(o.gray(` ℹ Auto-handled: ${n.autoHandled.join(", ")}`))}catch(e){console.log(o.red(` ✗ ERROR: ${e.message}`))}}console.log(""),console.log(o.blue.bold("━━━ Summary ━━━"));const k=$.filter(e=>"passed"===e.status).length,v=$.filter(e=>"failed"===e.status).length,x=$.reduce((e,o)=>e+o.totalSteps,0),A=$.reduce((e,o)=>e+o.passedSteps,0);if(console.log(o.gray(` ${$.length} test file(s), ${x} total steps`)),0===v?console.log(o.green.bold(` ✓ All ${k} test(s) passed! (${A}/${x} steps)`)):console.log(o.red.bold(` ✗ ${v}/${$.length} test(s) failed`)),y&&$.length>0){let e;e=1===$.length?h.saveAsFolder($[0]):h.saveSuiteAsFolder($),console.log(o.cyan(`\n 📄 Report: ${e}`))}console.log(""),v>0&&process.exit(1)}catch(e){console.error(o.red(`Error: ${e.message}`)),process.exit(1)}}),x.command("create <name>").description("Create a new flow file").option("-d, --dir <directory>","Directory to create flow in","tests").action(async(e,s)=>{const l=await import("readline"),r=s.dir;n.existsSync(r)||n.mkdirSync(r,{recursive:!0});const a=e.endsWith(".flow")?e:`${e}.flow`,i=t.join(r,a);n.existsSync(i)&&(console.log(o.red(`File already exists: ${i}`)),process.exit(1)),console.log(o.blue(`\nCreating: ${i}`)),console.log(o.gray("Enter your test steps (one per line). Empty line to finish.\n"));const c=l.createInterface({input:process.stdin,output:process.stdout}),d=[`# ${e}`,""];let g=1;const p=()=>new Promise(e=>{c.question(o.cyan(`${g}. `),o=>{e(o)})});for(;;){const e=await p();if(""===e)break;d.push(e),g++}c.close(),d.length<=2?console.log(o.yellow("\nNo steps entered. File not created.")):(n.writeFileSync(i,d.join("\n")+"\n"),console.log(o.green(`\n✓ Created: ${i}`)),console.log(o.gray(` ${g-1} steps`)),console.log(o.gray(`\nRun with: slapify run ${i}`)))}),x.command("generate <prompt>").alias("gen").description("Generate a verified .flow file by running the goal as a task and recording what worked").option("-d, --dir <directory>","Directory to save flow","tests").option("--headed","Show browser window while running").action(async(e,s)=>{const t=i();t||(console.log(o.red('No .slapify directory found. Run "slapify init" first.')),process.exit(1));r(t);console.log(o.blue("\n🤖 Flow Generator\n")),console.log(o.gray(" Running the goal in the browser to discover the real path...\n"));const{runTask:n}=await import("./task/runner.js");let l;await n({goal:e,headed:s.headed,saveFlow:!0,flowOutputDir:s.dir,onEvent:e=>{"status_update"===e.type&&process.stdout.write(o.gray(` → ${e.message}\n`)),"message"===e.type&&console.log(o.white(`\n${e.text}`)),"flow_saved"===e.type&&(l=e.path),"done"===e.type&&console.log(o.green("\n✅ Done")),"error"===e.type&&console.log(o.red(`\n✗ ${e.error}`))}}),l?(console.log(o.green(`\n✓ Flow saved: ${l}`)),console.log(o.gray(` Run with: slapify run ${l}`))):console.log(o.yellow("\n⚠ No flow was saved. The agent may not have completed the goal."))}),x.command("fix <file>").description("Analyze a failing test and suggest/apply fixes").option("--auto","Automatically apply suggested fixes without confirmation").option("--headed","Run browser in headed mode for debugging").action(async(e,t)=>{const l=i();l||(console.log(o.red('No .slapify directory found. Run "slapify init" first.')),process.exit(1)),n.existsSync(e)||(console.log(o.red(`File not found: ${e}`)),process.exit(1));const d=r(l),g=a(l);t.headed&&(d.browser={...d.browser,headless:!1}),d.report={...d.report,screenshots:!0};const p=await import("readline");let f=s("Running test to identify failures...").start();try{const l=c(e),r=new u(d,g),a=[];if("passed"===(await r.runFlow(l,e=>{"failed"===e.status&&e.error&&a.push({step:e.step.text,error:e.error,line:e.step.line,screenshot:e.screenshot})})).status)return void f.succeed("Test passed! No fixes needed.");if(f.info(`Found ${a.length} failing step(s)`),0===a.length)return void console.log(o.yellow("No specific step failures to fix."));const i=n.readFileSync(e,"utf-8"),m=i.split("\n");f=s("Analyzing failures and generating fixes...").start();const w=a.map(e=>`Line ${e.line}: "${e.step}"\n Error: ${e.error}`).join("\n\n"),h=await y({model:v(Array.isArray(d.llm)?d.llm[0]:d.llm),system:`You are a test automation expert. Analyze failing test steps and suggest fixes.\n\nOriginal flow file:\n\`\`\`\n${i}\n\`\`\`\n\nFailing steps:\n${w}\n\nBased on the errors, suggest fixes for the flow file. Common issues and fixes:\n1. Element not found → Try more descriptive text, add wait, or make step optional\n2. Timeout → Add explicit wait or increase timeout\n3. Navigation error → Add wait after navigation, or split into smaller steps\n4. Element obscured → Add step to close popup/modal first\n5. Stale element → Add wait for page to stabilize\n\nRespond with JSON:\n{\n "analysis": "Brief explanation of what's wrong",\n "fixes": [\n {\n "line": 5,\n "original": "Click the submit button",\n "fixed": "Click the Submit button",\n "reason": "Button text is capitalized"\n }\n ],\n "additions": [\n {\n "afterLine": 4,\n "step": "[Optional] Wait for page to load",\n "reason": "Page might still be loading"\n }\n ]\n}`,prompt:"Analyze the failures and suggest specific fixes.",maxTokens:1500});f.succeed("Analysis complete");const $=h.text.match(/\{[\s\S]*\}/);if(!$)return void console.log(o.red("Could not parse AI response"));const b=JSON.parse($[0]);if(console.log(o.blue("\n━━━ Analysis ━━━\n")),console.log(o.white(b.analysis)),b.fixes?.length>0||b.additions?.length>0){if(console.log(o.blue("\n━━━ Suggested Fixes ━━━\n")),b.fixes?.length>0)for(const e of b.fixes)console.log(o.yellow(`Line ${e.line}:`)),console.log(o.red(` - ${e.original}`)),console.log(o.green(` + ${e.fixed}`)),console.log(o.gray(` Reason: ${e.reason}\n`));if(b.additions?.length>0){console.log(o.yellow("New steps to add:"));for(const e of b.additions)console.log(o.green(` + After line ${e.afterLine}: ${e.step}`)),console.log(o.gray(` Reason: ${e.reason}\n`))}let s=t.auto;if(!s){const e=p.createInterface({input:process.stdin,output:process.stdout}),t=await new Promise(s=>{e.question(o.cyan("Apply these fixes? (y/N): "),o=>{e.close(),s(o.trim().toLowerCase())})});s="y"===t||"yes"===t}if(s){let s=[...m];new Map;if(b.fixes)for(const e of b.fixes){const o=e.line-1;o>=0&&o<s.length&&(s[o]=e.fixed)}if(b.additions){const e=[...b.additions].sort((e,o)=>o.afterLine-e.afterLine);for(const o of e){const e=o.afterLine;s.splice(e,0,o.step)}}const t=s.join("\n"),l=e+".backup";n.writeFileSync(l,i),n.writeFileSync(e,t),console.log(o.green(`\n✓ Fixes applied to ${e}`)),console.log(o.gray(` Backup saved to ${l}`)),console.log(o.gray(`\nRun again with: slapify run ${e}`))}else console.log(o.yellow("No changes made."))}else console.log(o.yellow("\nNo automatic fixes suggested.")),console.log(o.gray("The failures may require manual investigation."))}catch(e){f.fail("Error"),console.error(o.red(`Error: ${e.message}`)),process.exit(1)}}),x.command("validate [files...]").description("Validate flow files for syntax issues").action(async e=>{let s=[];if(0===e.length){const e=t.join(process.cwd(),"tests");n.existsSync(e)&&(s=await d(e))}else s=e;if(0===s.length)return void console.log(o.yellow("No .flow files found."));let l=!1;for(const e of s)try{const s=c(e),t=g(s),n=p(s);if(t.length>0){l=!0,console.log(o.yellow(`⚠️ ${e}`));for(const e of t)console.log(o.yellow(` ${e}`))}else console.log(o.green(`✅ ${e}`)),console.log(o.gray(` ${n.totalSteps} steps (${n.requiredSteps} required, ${n.optionalSteps} optional)`))}catch(s){console.log(o.red(`❌ ${e}`)),console.log(o.red(` ${s.message}`)),l=!0}l&&process.exit(1)}),x.command("list").description("List all flow files").action(async()=>{const e=t.join(process.cwd(),"tests");if(!n.existsSync(e))return void console.log(o.yellow("No tests directory found."));const s=await d(e);if(0!==s.length){console.log(o.blue(`\nFound ${s.length} flow file(s):\n`));for(const e of s){const s=c(e),n=p(s),l=t.relative(process.cwd(),e);console.log(` ${o.white(l)}`),console.log(o.gray(` ${n.totalSteps} steps (${n.requiredSteps} required, ${n.optionalSteps} optional)`))}console.log("")}else console.log(o.yellow("No .flow files found."))}),x.command("credentials").description("List configured credential profiles").action(()=>{const e=i();e||(console.log(o.red('No .slapify directory found. Run "slapify init" first.')),process.exit(1));const s=a(e),t=Object.keys(s.profiles);if(0===t.length)return console.log(o.yellow("No credential profiles configured.")),void console.log(o.gray("Edit .slapify/credentials.yaml to add profiles."));console.log(o.blue("\nConfigured credential profiles:\n"));for(const e of t){const t=s.profiles[e];console.log(` ${o.white(e)} (${t.type})`),t.username&&console.log(o.gray(` username: ${t.username}`)),t.email&&console.log(o.gray(` email: ${t.email}`)),t.totp_secret&&console.log(o.gray(" 2FA: TOTP configured")),t.fixed_otp&&console.log(o.gray(" 2FA: Fixed OTP configured"))}console.log("")}),x.command("fix-credentials [files...]").description("Fix credential YAML files where localStorage/sessionStorage were saved as JSON strings").option("--dry-run","Only print what would be fixed").action((e,s)=>{const n=[];if(e&&e.length>0)n.push(...e.map(e=>t.resolve(e)));else{const e=process.cwd();n.push(t.join(e,"temp_credentials.yaml"));const o=i();o&&n.push(t.join(o,"credentials.yaml"))}console.log(o.blue("\n🔧 Fix credential YAML files\n")),s.dryRun&&console.log(o.gray(" (dry run – no files will be modified)\n"));let l=0;for(const e of n)I(e,!!s.dryRun)&&l++;0===l&&n.length>0&&console.log(o.gray("\n No files needed fixing.")),console.log("")}),x.command("interactive [url]").alias("i").description("Run steps interactively").option("--headed","Run browser in headed mode").action(async(e,s)=>{console.log(o.blue("\n🧪 Slapify Interactive Mode")),console.log(o.gray('Type test steps and press Enter to execute. Type "exit" to quit.\n'));const t=i();t||(console.log(o.red('No .slapify directory found. Run "slapify init" first.')),process.exit(1));const n=r(t),l=a(t);s.headed&&(n.browser={...n.browser,headless:!1});new u(n,l);e&&console.log(o.gray(`Navigating to ${e}...`));const c=(await import("readline")).createInterface({input:process.stdin,output:process.stdout}),d=()=>{c.question(o.cyan("> "),async e=>{const s=e.trim();"exit"!==s.toLowerCase()&&"quit"!==s.toLowerCase()||(console.log(o.gray("\nGoodbye!")),c.close(),process.exit(0)),s?(console.log(o.gray(`Executing: ${s}`)),console.log(o.green("✓ Done")),d()):d()})};d()}),x.command("server").description("Run Slapify in server mode. The agent stays online and waits for instructions via Telegram.\n Use this for long-running bots that you control remotely.").option("--headed","Show the browser window").option("--debug","Show all tool calls and internal steps").option("--report","Generate an HTML report after the server stops").action(async e=>{x._slapifyServerMode=!0,await x.commands.find(e=>"task"===e._name).parseAsync(["You are running in SERVER MODE. You are ALREADY connected to Telegram and this chat. Wait for the user to send you a new task or instruction. When you receive a message, treat it as a new goal. Do NOT ask for Telegram credentials or tokens; they are already active. If no command is pending, you should simply wait or use the status_update() tool to say you are ready.",...e.headed?["--headed"]:[],...e.debug?["--debug"]:[],...e.report?["--report"]:[]],{from:"user"})}),x.command("task [goal]").description('Run an autonomous AI agent task in plain English.\n The agent decides everything: what to do, when to schedule, when to sleep.\n Examples:\n slapify task "Go to linkedin.com and like the latest 3 posts"\n slapify task "Monitor my Gmail for new emails every 30 min and log subjects"\n slapify task "Order breakfast from Swiggy every day at 8am"').option("--headed","Show the browser window").option("--debug","Show all tool calls and internal steps").option("--report","Generate an HTML report after the task completes").option("--save-flow","Save agent steps as a reusable .flow file when done").option("--session <id>","Resume an existing task session").option("--list-sessions","List all task sessions").option("--logs <id>","Show logs for a task session").option("--max-iterations <n>","Safety cap on agent loop iterations (default 400)",parseInt).option("--schema <json-or-file>","JSON Schema (inline JSON string or path to a .json file) the agent should use to structure its output").option("--output <file>","File path to write structured JSON output to (used together with --schema)").action(async(e,s)=>{if(s.listSessions){const{listSessions:e}=await import("./task/index.js"),s=e();if(0===s.length)return void console.log(o.gray("\nNo task sessions found.\n"));console.log(o.blue(`\n📋 Task Sessions (${s.length})\n`));for(const e of s){const s="completed"===e.status?o.green:"failed"===e.status?o.red:"scheduled"===e.status?o.blue:o.yellow;console.log(` ${s("●")} ${o.bold(e.id)}\n Goal: ${e.goal.slice(0,70)}${e.goal.length>70?"…":""}\n Status: ${s(e.status)} Iterations: ${e.iteration}\n Updated: ${new Date(e.updatedAt).toLocaleString()}\n`)}return}if(s.logs){const{loadSession:e}=await import("./task/index.js"),{loadEvents:t}=await import("./task/session.js"),n=e(s.logs);n||(console.log(o.red(`Session '${s.logs}' not found.`)),process.exit(1)),console.log(o.blue(`\n📜 Logs: ${n.id}\n`)),console.log(o.gray(`Goal: ${n.goal}\n`));const l=t(s.logs);for(const e of l){const s=o.gray(new Date(e.ts).toLocaleTimeString());"llm_response"===e.type?e.text&&console.log(`${s} 🤔 ${o.cyan(e.text.slice(0,120))}`):"tool_call"===e.type?console.log(`${s} 🔧 ${o.yellow(e.toolName)} → ${o.gray(JSON.stringify(e.result).slice(0,80))}`):"tool_error"===e.type?console.log(`${s} ❌ ${o.red(e.toolName)} → ${o.red(e.error.slice(0,80))}`):"memory_update"===e.type?console.log(`${s} 🧠 ${o.magenta("remember")} ${e.key} = ${e.value.slice(0,60)}`):"scheduled"===e.type?console.log(`${s} ⏰ ${o.blue("schedule")} ${e.cron} — ${e.task}`):"sleeping_until"===e.type?console.log(`${s} 😴 ${o.blue("sleep")} until ${e.until}`):"session_end"===e.type&&console.log(`${s} ✅ ${o.green("done")} ${e.summary.slice(0,120)}`)}return void console.log("")}let l=e||s.session?e:void 0;if(l||s.session||(console.log(o.red('\nPlease provide a goal. Example:\n slapify task "Go to example.com and check the title"\n')),process.exit(1)),!l&&s.session){const{loadSession:e}=await import("./task/index.js"),t=e(s.session);t||(console.log(o.red(`Session '${s.session}' not found.`)),process.exit(1)),l=t.goal}i()||(console.log(o.red('\nNo .slapify directory found. Run "slapify init" first.\n')),process.exit(1));let r=null;const a=async e=>{if(s.report)try{const{loadEvents:s,saveTaskReport:t}=await import("./task/index.js"),n=t(e,s(e.id));console.log(o.cyan(`\n 📊 Report: ${n}`))}catch(e){console.log(o.yellow(` ⚠ Could not generate report: ${e?.message}`))}},c=!!s.debug,d=()=>process.stdout.write("[2K\r");console.log(o.blue("\n🤖 Slapify Task Agent\n")),console.log(o.white(` Goal: ${l}`)),s.session&&console.log(o.gray(` Resuming session: ${s.session}`)),console.log(o.gray([s.report?" --report: HTML report on exit":"",c?" --debug: verbose output":""," Ctrl+C to stop"].filter(Boolean).join(" · ")+"\n")),console.log(o.gray("─".repeat(60))+"\n");let g=null,p=!1;const u=()=>{if(c||p||g)return;const e=["⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏"];let s=0;g=setInterval(()=>{process.stdout.write(o.gray(`\r ${e[s++%e.length]} working...`))},80)};c||u();const{runTask:f}=await import("./task/index.js");let m=!1;const y=async()=>{if(!m){if(m=!0,clearInterval(g),g=null,p=!0,process.stdout.write("[2K\r"),console.log(o.yellow("\n ⚡ Interrupted"+(s.report?" — generating report...":""))),r){r.status="failed",r.finalSummary="Task interrupted by user (Ctrl+C).";const{saveSessionMeta:e}=await import("./task/session.js");e(r),await a(r)}console.log(o.gray(" Goodbye.\n")),process.exit(0)}};process.once("SIGINT",y);try{const e=()=>{g&&(clearInterval(g),g=null),process.stdout.write("[2K\r")};let i;if(s.schema)try{i=JSON.parse(s.schema)}catch{try{const e=n.readFileSync(t.resolve(s.schema),"utf8");i=JSON.parse(e)}catch{console.log(o.red("Could not parse --schema: expected inline JSON or a valid .json file path.")),process.exit(1)}}const m=await f({goal:l,sessionId:s.session,headed:s.headed,saveFlow:s.saveFlow,maxIterations:s.maxIterations,schema:i,outputFile:s.output,onHumanInput:async(s,t)=>{p=!0,e();const n=(await import("readline")).createInterface({input:process.stdin,output:process.stdout,terminal:!0}),l=new Promise(e=>{n.question(` ${o.cyan("›")} `,o=>{n.close(),e(o.trim())})});let a,i=!1;const c=r?._telegramInstance;if(c){const e=c.waitForReply().then(e=>(i=!0,e));a=await Promise.race([l,e]),i?(n.write("\n"),n.close()):c.cancelWaitForReply()}else a=await l;return console.log(o.yellow("─".repeat(60))+"\n"),p=!1,u(),a},onEvent:s=>{const t="status_update"===s.type||"human_input_needed"===s.type||"credentials_saved"===s.type||"done"===s.type||"error"===s.type||"tool_error"===s.type;t&&e(),(e=>{switch(e.type){case"thinking":c&&process.stdout.write(o.gray(" ⟳ thinking...\r"));break;case"message":c&&(d(),console.log(o.gray(` 💬 ${e.text}`)));break;case"tool_start":if(c){d();const s=JSON.stringify(e.args);console.log(o.dim(` › ${o.cyan(e.toolName)} `)+o.gray(s.slice(0,100)+(s.length>100?"…":"")))}break;case"tool_done":c&&console.log(o.dim(` ✓ ${e.result.slice(0,120)}`));break;case"tool_error":d(),console.log(o.red(` ✗ ${e.toolName}: ${e.error.slice(0,120)}`));break;case"status_update":d(),console.log(o.white(` ${e.message}`));break;case"human_input_needed":console.log("\n"+o.yellow("─".repeat(60))),console.log(o.yellow.bold(" 🙋 Agent needs your input")),console.log(o.white(`\n ${e.question}`)),e.hint&&console.log(o.gray(` ${e.hint}`));break;case"credentials_saved":d(),console.log(o.green(` 💾 Credentials saved: '${e.profileName}' (${e.credType}) → .slapify/credentials.yaml`));break;case"scheduled":c&&(d(),console.log(o.dim(` ⏰ scheduled: ${e.cron} — ${e.task}`)));break;case"sleeping":c&&(d(),console.log(o.dim(` 😴 sleeping until ${new Date(e.until).toLocaleString()}`)));break;case"done":d(),console.log("\n"+o.green("─".repeat(60))),console.log(o.green.bold(" ✅ Task complete!")),console.log(o.white(`\n ${e.summary}`)),console.log(o.green("─".repeat(60)));break;case"error":d(),console.log(o.red(`\n ✗ Error: ${e.error}`))}})(s),t&&"done"!==s.type&&"error"!==s.type&&"human_input_needed"!==s.type&&u()},onSessionUpdate:e=>{r=e}});if(e(),process.removeListener("SIGINT",y),console.log(o.gray(`\n Session: ${m.id}`)),m.savedFlowPath&&console.log(o.cyan(` Flow saved: ${m.savedFlowPath}`)),s.output&&null!=m.structuredOutput&&console.log(o.cyan(` Output: ${t.resolve(s.output)}`)),Object.keys(m.memory).length>0){console.log(o.gray(` Memory (${Object.keys(m.memory).length} items):`));for(const[e,s]of Object.entries(m.memory))console.log(o.gray(` • ${e}: ${s.slice(0,80)}`))}await a(m),console.log("")}catch(e){process.removeListener("SIGINT",y),clearInterval(g),g=null,process.stdout.write("[2K\r"),console.error(o.red(`\n Task failed: ${e?.message||e}`)),process.env.SLAPIFY_DEBUG_AI&&(console.error(o.dim(`\n Stack Trace:\n${e?.stack||""}`)),e?.cause&&console.error(o.yellow(`\n Error Cause:\n${JSON.stringify(e.cause,null,2)}`))),r&&await a(r),process.exit(1)}}),x.parse();
|
package/dist/config/loader.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"fs";import n from"path";import o from"yaml";const t=".slapify",r="config.yaml",i="credentials.yaml";function a(o=process.cwd()){let r=o;for(;r!==n.dirname(r);){const o=n.join(r,t);if(e.existsSync(o))return o;r=n.dirname(r)}return null}function s(e){if("string"==typeof e)return e.replace(/\$\{(\w+)\}/g,(e,n)=>process.env[n]||"");if(Array.isArray(e))return e.map(s);if(e&&"object"==typeof e){const n={};for(const[o,t]of Object.entries(e))n[o]=s(t);return n}return e}export function loadConfig(t){const i=t||a();if(!i)throw new Error('No .slapify directory found. Run "slapify init" to create one.');const l=n.join(i,r);if(!e.existsSync(l))throw new Error(`Config file not found: ${l}`);const c=e.readFileSync(l,"utf-8"),p=s(o.parse(c));return{...p,llm:Array.isArray(p.llm)?p.llm:[p.llm],browser:{headless:!0,timeout:3e4,viewport:{width:1280,height:720},...p.browser},report:{format:"markdown",screenshots:!0,output_dir:"./test-reports",...p.report}}}export function loadConfigDir(){return a()}export function loadCredentials(t){const r=t||a();if(!r)return{profiles:{}};const l=n.join(r,i);if(!e.existsSync(l))return{profiles:{}};const c=e.readFileSync(l,"utf-8");return s(o.parse(c))}const l={anthropic:"claude-sonnet-4-6",openai:"gpt-5.2",google:"gemini-3-flash",mistral:"mistral-small-latest",groq:"llama-3.3-70b-versatile",ollama:"llama3"},c={anthropic:"ANTHROPIC_API_KEY",openai:"OPENAI_API_KEY",google:"GOOGLE_API_KEY",mistral:"MISTRAL_API_KEY",groq:"GROQ_API_KEY",ollama:""};export function initConfig(o=process.cwd(),a={}){const s=n.join(o,t);if(e.existsSync(s))throw new Error(".slapify directory already exists");e.mkdirSync(s,{recursive:!0});const p=a.provider||"anthropic",m=a.model||l[p],u=c[p];let d="browser:\n headless: true\n timeout: 30000\n viewport:\n width: 1280\n height: 720";a.browserPath?d+=`\n # Using system browser (saves ~170MB download)\n executablePath: "${a.browserPath}"`:!1===a.useSystemBrowser&&(d+="\n # Will download Chromium on first run (~170MB)");let
|
|
1
|
+
import e from"fs";import n from"path";import o from"yaml";const t=".slapify",r="config.yaml",i="credentials.yaml";function a(o=process.cwd()){let r=o;for(;r!==n.dirname(r);){const o=n.join(r,t);if(e.existsSync(o))return o;r=n.dirname(r)}return null}function s(e){if("string"==typeof e)return e.replace(/\$\{(\w+)\}/g,(e,n)=>process.env[n]||"");if(Array.isArray(e))return e.map(s);if(e&&"object"==typeof e){const n={};for(const[o,t]of Object.entries(e))n[o]=s(t);return n}return e}export function loadConfig(t){const i=t||a();if(!i)throw new Error('No .slapify directory found. Run "slapify init" to create one.');const l=n.join(i,r);if(!e.existsSync(l))throw new Error(`Config file not found: ${l}`);const c=e.readFileSync(l,"utf-8"),p=s(o.parse(c));return{...p,llm:Array.isArray(p.llm)?p.llm:[p.llm],browser:{headless:!0,timeout:3e4,viewport:{width:1280,height:720},...p.browser},report:{format:"markdown",screenshots:!0,output_dir:"./test-reports",...p.report}}}export function loadConfigDir(){return a()}export function loadCredentials(t){const r=t||a();if(!r)return{profiles:{}};const l=n.join(r,i);if(!e.existsSync(l))return{profiles:{}};const c=e.readFileSync(l,"utf-8");return s(o.parse(c))}const l={anthropic:"claude-sonnet-4-6",openai:"gpt-5.2",google:"gemini-3-flash",mistral:"mistral-small-latest",groq:"llama-3.3-70b-versatile",ollama:"llama3"},c={anthropic:"ANTHROPIC_API_KEY",openai:"OPENAI_API_KEY",google:"GOOGLE_API_KEY",mistral:"MISTRAL_API_KEY",groq:"GROQ_API_KEY",ollama:""};export function initConfig(o=process.cwd(),a={}){const s=n.join(o,t);if(e.existsSync(s))throw new Error(".slapify directory already exists");e.mkdirSync(s,{recursive:!0});const p=a.provider||"anthropic",m=a.model||l[p],u=c[p];let d="browser:\n headless: true\n timeout: 30000\n viewport:\n width: 1280\n height: 720";a.browserPath?d+=`\n # Using system browser (saves ~170MB download)\n executablePath: "${a.browserPath}"`:!1===a.useSystemBrowser&&(d+="\n # Will download Chromium on first run (~170MB)");let f=`llm:\n provider: ${p}\n model: ${m}`;f+="ollama"===p?"\n # Ollama runs locally - no API key needed\n base_url: http://localhost:11434/v1":`\n api_key: \${${u}}`;const h=`# Slapify Configuration\n# Docs: https://slaps.dev/slapify\n\n# LLM Settings (required)\n# You can change the provider and model anytime\n${f}\n\n# Browser Automation Settings\n${d}\n # Anti-Bot Stealth & Persistence\n profile: ".slapify/browser-profile"\n userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"\n args:\n - "--disable-blink-features=AutomationControlled"\n - "--no-sandbox"\n# To use Browserbase cloud browsers instead of local (stealth, scalable, no install):\n# browser:\n# browserbase:\n# api_key: \${BROWSERBASE_API_KEY}\n# project_id: \${BROWSERBASE_PROJECT_ID} # optional\n\n# Custom User Plugins\n# Add your own markdown rules to the agent's system prompt\n# plugins:\n# rules:\n# - "rules/my-persona.md"\n# - "rules/company-guidelines.md"\n# tools:\n# - "tools/custom-utils.js"\n\n# Live Telegram Chat Integration\n# Talk to your agent while it runs and let it ask you questions\n# telegram:\n# bot_token: \${TELEGRAM_BOT_TOKEN}\n# allowed_chat_id: \${TELEGRAM_CHAT_ID} # optional: restrict who can talk to it\n\n# Report Settings\nreport:\n format: html\n screenshots: true\n output_dir: ./test-reports\n`;e.writeFileSync(n.join(s,r),h);e.writeFileSync(n.join(s,i),'# Slapify Credentials\n# WARNING: Do not commit this file to version control!\n# This file is gitignored by default\n\nprofiles:\n # Default login credentials (for form-based login)\n default:\n type: login-form\n username: ${TEST_USERNAME}\n password: ${TEST_PASSWORD}\n\n # Example: Admin account with 2FA\n # admin:\n # type: login-form\n # username: admin@example.com\n # password: ${ADMIN_PASSWORD}\n # totp_secret: JBSWY3DPEHPK3PXP\n\n # Example: Inject auth token via localStorage\n # token-auth:\n # type: inject\n # localStorage:\n # auth_token: ${AUTH_TOKEN}\n # user_id: "12345"\n\n # Example: Inject session via sessionStorage\n # session-auth:\n # type: inject\n # sessionStorage:\n # session_token: ${SESSION_TOKEN}\n\n # Example: Inject auth cookies\n # cookie-auth:\n # type: inject\n # cookies:\n # - name: auth_token\n # value: ${AUTH_TOKEN}\n # domain: .example.com\n # - name: refresh_token\n # value: ${REFRESH_TOKEN}\n # domain: .example.com\n\n # Example: Combined - cookies + localStorage\n # full-auth:\n # type: inject\n # cookies:\n # - name: session_id\n # value: ${SESSION_ID}\n # domain: .example.com\n # localStorage:\n # user_preferences: \'{"theme":"dark","lang":"en"}\'\n # feature_flags: \'{"beta":true}\'\n');const g=n.join(o,"tests");e.existsSync(g)||e.mkdirSync(g,{recursive:!0});e.writeFileSync(n.join(g,"example.flow"),'# Example Test - Getting Started with Slapify\n# Run with: slapify run tests/example.flow\n\n# Navigate to a website\nGo to https://example.com\n\n# Verify the page loaded correctly\nVerify page title contains "Example"\n\n# Handle potential popups (won\'t fail if not present)\n[Optional] Close any cookie consent popup\n\n# Interact with the page\nClick on "More information" link\n\n# Verify navigation worked\nVerify URL contains "iana.org"\n');e.writeFileSync(n.join(s,".gitignore"),"credentials.yaml\n")}export function checkBrowserPath(n){return e.existsSync(n)}export function findSystemBrowsers(){const n=[],o=[{name:"Google Chrome",path:"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"},{name:"Chrome (Linux)",path:"/usr/bin/google-chrome"},{name:"Chrome (Linux Alt)",path:"/usr/bin/google-chrome-stable"},{name:"Chromium",path:"/usr/bin/chromium"},{name:"Chromium (Linux)",path:"/usr/bin/chromium-browser"},{name:"Chrome (Windows)",path:"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"},{name:"Chrome (Windows x86)",path:"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe"},{name:"Edge",path:"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"},{name:"Edge (Windows)",path:"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe"},{name:"Brave",path:"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"}];for(const t of o)e.existsSync(t.path)&&n.push(t);return n}export function getConfigDir(){return a()}
|
package/dist/task/runner.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"fs";import t from"path";import{generateText as s}from"ai";import o from"node-cron";import{createSession as r}from"wreq-js";import{BrowserAgent as i}from"../browser/agent.js";import{loadConfig as n,loadCredentials as a,loadConfigDir as l}from"../config/loader.js";import{getModel as c}from"../ai/interpreter.js";import{taskTools as u}from"./tools.js";import{buildSystemPrompt as h}from"./prompt.js";import{createSession as d,loadSession as p,saveSessionMeta as m,appendEvent as f,updateSessionStatus as g}from"./session.js";import{TelegramService as y}from"../chat/telegram.js";const w=400;class k{wreqSession=null;browser;session;credentials;emit;onHumanInput;credentialsFilePath;isScheduledRun;schema;outputFile;customTools;constructor(e,t,s,o,r,i,n=!1,a,l,c){this.browser=e,this.session=t,this.credentials=s,this.emit=o,this.onHumanInput=r,this.credentialsFilePath=i,this.isScheduledRun=n,this.schema=a,this.outputFile=l,this.customTools=c||{}}async getWreqSession(){return this.wreqSession||(this.wreqSession=await r({browser:"chrome_131",os:"macos"})),this.wreqSession}async closeWreqSession(){if(this.wreqSession){try{await this.wreqSession.close()}catch{}this.wreqSession=null}}async execute(s,r){switch(s){case"navigate":{const e=r.url;await this.browser.navigate(e);try{await this.browser.evaluate("(function(){document.querySelectorAll('a[target=\"_blank\"]').forEach(function(a){a.setAttribute('target','_self');});if(!window.__slapifyPatched){window.__slapifyPatched=true;var _orig=window.open;window.open=function(url,name,features){if(url&&String(url).startsWith('http')){window.location.href=url;return window;}return _orig.apply(this,arguments);};}})()")}catch{}return{ok:!0,url:e}}case"get_page_state":{const e=await this.browser.getState();return{url:e.url,title:e.title,snapshot:e.snapshot,refsCount:Object.keys(e.refs).length}}case"click":{const e=r.ref;try{await this.browser.evaluate("(function(){document.querySelectorAll('a[target=\"_blank\"]').forEach(function(a){a.setAttribute('target','_self');});if(!window.__slapifyPatched){window.__slapifyPatched=true;var _orig=window.open;window.open=function(url,name,features){if(url&&String(url).startsWith('http')){window.location.href=url;return window;}return _orig.apply(this,arguments);};}})()")}catch{}return await this.browser.click(e),{ok:!0,clicked:e}}case"type":{const e=r.ref,t=r.text;return r.append?await this.browser.type(e,t):await this.browser.fill(e,t),{ok:!0}}case"press":{const e=r.key;return await this.browser.press(e),{ok:!0}}case"scroll":{const e=r.direction,t=r.amount||300;return await this.browser.scroll(e,t),{ok:!0}}case"wait":{const e=r.seconds;return await this.browser.wait(1e3*e),{ok:!0,waited:`${e}s`}}case"screenshot":return{ok:!0,path:await this.browser.screenshot(),note:"Screenshot captured. Check get_page_state() for interactive elements."};case"reload":return await this.browser.reload(),{ok:!0};case"go_back":return await this.browser.goBack(),{ok:!0};case"list_credential_profiles":return{profiles:Object.entries(this.credentials).map(([e,t])=>({name:e,type:t.type,hasUsername:!(!t.username&&!t.email),hasCookies:!!(t.cookies&&t.cookies.length>0),hasLocalStorage:!!(t.localStorage&&Object.keys(t.localStorage).length>0)}))};case"inject_credentials":{const e=r.profile_name,t=this.credentials[e];return t?"inject"!==t.type?{ok:!1,error:`Profile '${e}' is type '${t.type}', use fill_login_form for login-form profiles`}:(await this.injectProfile(t),await this.browser.wait(300),await this.browser.reload(),{ok:!0,injected:e}):{ok:!1,error:`Profile '${e}' not found`}}case"fill_login_form":{const e=r.profile_name,t=this.credentials[e];return t?"login-form"!==t.type?{ok:!1,error:`Profile '${e}' is type '${t.type}'. Use inject_credentials for inject profiles.`}:{ok:!0,username:t.username||t.email||t.phone||"",password:t.password||"",hint:"Use get_page_state() to find the username/password fields, then type into them and submit the form."}:{ok:!1,error:`Profile '${e}' not found`}}case"solve_captcha":{const e=await this.browser.getState(),t=(e.snapshot,[]),s=[],o=Object.entries(e.refs).filter(([,e])=>{const t=(e.name||e.text||"").toLowerCase();return t.includes("not a robot")||t.includes("i'm not a robot")||t.includes("checkbox")||"checkbox"===e.role});for(const[e]of o)try{await this.browser.click(e),await this.browser.wait(2e3),t.push(`Clicked checkbox ref ${e}`)}catch{s.push(`Failed to click ref ${e}`)}const r=Object.entries(e.refs).filter(([,e])=>{const t=(e.name||e.text||"").toLowerCase();return t.includes("audio")||t.includes("sound")});for(const[e]of r.slice(0,1))try{await this.browser.click(e),await this.browser.wait(1500),t.push(`Clicked audio challenge ref ${e}`)}catch{s.push(`Failed to click audio ref ${e}`)}const i=await this.browser.getState(),n=i.snapshot?.toLowerCase().includes("captcha")||i.snapshot?.toLowerCase().includes("not a robot")||i.url?.includes("sorry");return{attempted:t.length>0,solved:t,failed:s,captchaStillPresent:n,currentUrl:i.url,hint:n?"CAPTCHA still present. Try fetch_url() on a different source for the same data.":"CAPTCHA appears resolved. Call get_page_state() to continue."}}case"fetch_url":{const e=r.url,t=r.headers||{},s=await this.getWreqSession(),o=await s.fetch(e,{headers:{Accept:"application/json, text/html, */*","Accept-Language":"en-US,en;q=0.9",...t}}),i=o.headers.get("content-type")||"",n=await o.text();let a=n;if(i.includes("application/json"))try{a=JSON.parse(n)}catch{a=n}const l="string"==typeof a?a:JSON.stringify(a);return{ok:o.ok,status:o.status,body:l.slice(0,8e3)+(l.length>8e3?"…[truncated]":"")}}case"remember":{const e=r.key,t=r.value;return this.session.memory[e]=t,m(this.session),f(this.session.id,{type:"memory_update",key:e,value:t,ts:(new Date).toISOString()}),{ok:!0,stored:e}}case"recall":{const e=r.key,t=this.session.memory[e];return void 0!==t?{ok:!0,key:e,value:t}:{ok:!1,key:e,error:"Key not found in memory"}}case"list_memories":return{keys:Object.keys(this.session.memory),count:Object.keys(this.session.memory).length};case"status_update":{const e=r.message;return this.emit({type:"status_update",message:e}),{ok:!0}}case"ask_user":{const s=r.question,o=r.hint;this.emit({type:"human_input_needed",question:s,hint:o});const i=await this.onHumanInput(s,o);f(this.session.id,{type:"tool_call",toolName:"ask_user",args:{question:s,hint:o},result:{answer:"[redacted from logs]"},ts:(new Date).toISOString()});if(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z]{2,}\b/i.test(i)&&i.includes(" ")){const s=i.split(/\s+(?:and\s+)?(?:password\s+is\s+|pass(?:word)?[:\s]+)?/i),o=s[0]?.trim(),r=s[1]?.trim();this.emit({type:"human_input_needed",question:"💾 Save these credentials for future sessions?",hint:"Profile name to save as (or press Enter to skip)"});const n=await this.onHumanInput("💾 Save these credentials for future sessions?","Enter a profile name (e.g. 'linkedin', 'gmail') or leave blank to skip");if(n&&n.trim()){const s=n.trim().toLowerCase().replace(/\s+/g,"-"),i={type:"login-form",...o&&{username:o},...r&&{password:r}};try{const o=(await import("yaml")).default;let r={profiles:{}};if(e.existsSync(this.credentialsFilePath))try{const t=o.parse(e.readFileSync(this.credentialsFilePath,"utf-8"));t?.profiles&&(r=t)}catch{}r.profiles[s]=i,e.mkdirSync(t.dirname(this.credentialsFilePath),{recursive:!0}),e.writeFileSync(this.credentialsFilePath,o.stringify(r,{indent:2,lineWidth:0})),this.credentials[s]=i,this.emit({type:"credentials_saved",profileName:s,credType:"login-form"})}catch{}}}return{answer:i}}case"save_credentials":{const s=r.profile_name,o=r.type,i=r.capture_from_browser,n={type:o};if("login-form"===o&&(r.username&&(n.username=r.username),r.password&&(n.password=r.password)),"inject"===o&&i)try{const e=await this.browser.getCookies(),t=await this.browser.getLocalStorage(),s=await this.browser.getSessionStorage();e.length>0&&(n.cookies=e.map(e=>({name:e.name,value:e.value})));const o=e=>{if(!e||"object"!=typeof e||Array.isArray(e))return{};const t={};for(const[s,o]of Object.entries(e))t[String(s)]="string"==typeof o?o:JSON.stringify(o);return t},r=o(t),i=o(s);Object.keys(r).length>0&&(n.localStorage=r),Object.keys(i).length>0&&(n.sessionStorage=i)}catch(e){return{ok:!1,error:`Failed to capture browser state: ${e.message}`}}try{const r=(await import("yaml")).default;let i={profiles:{}};if(e.existsSync(this.credentialsFilePath))try{const t=r.parse(e.readFileSync(this.credentialsFilePath,"utf-8"));t?.profiles&&(i=t)}catch{}return i.profiles[s]=n,e.mkdirSync(t.dirname(this.credentialsFilePath),{recursive:!0}),e.writeFileSync(this.credentialsFilePath,r.stringify(i,{indent:2,lineWidth:0})),this.credentials[s]=n,this.emit({type:"credentials_saved",profileName:s,credType:o}),{ok:!0,message:`Saved profile '${s}' (${o}) to credentials.yaml`,cookieCount:n.cookies?.length??0,localStorageKeys:Object.keys(n.localStorage??{}).length}}catch(e){return{ok:!1,error:`Failed to save credentials: ${e.message}`}}}case"perf_audit":{const e=r.url,t=!1!==r.lighthouse,s=!1!==r.react_scan;this.emit({type:"status_update",message:`⚡ Auditing ${e}...`});try{const{runPerfAudit:o}=await import("../perf/audit.js"),r=await o(e,this.browser,{lighthouse:t,reactScan:s,settleMs:2e3,navigate:!0});this.session.perfAudits||(this.session.perfAudits=[]),this.session.perfAudits.push(r),this.session.perfAudit=r,m(this.session);const i=r.scores??r.lighthouse,n=r.network,a={url:r.url,vitals:r.vitals,scores:i,react:r.react,network:n?{totalRequests:n.totalRequests,totalKB:Math.round((n.totalBytes||0)/1024),jsKB:Math.round((n.jsBytes||0)/1024),apiCalls:n.apiCalls.length,slowApiCalls:n.slowApiCalls.length,failedApiCalls:n.failedApiCalls.length,longTasks:n.longTasks.length,totalBlockingMs:n.totalBlockingMs,memoryMB:n.memoryMB,slowApis:n.slowApiCalls.slice(0,5).map(e=>({url:e.url.length>80?"…"+e.url.slice(-80):e.url,method:e.method,status:e.status,durationMs:e.duration})),heaviestResources:n.heaviestResources.slice(0,5).map(e=>({url:e.url.split("/").slice(-2).join("/"),type:e.type,sizeKB:Math.round(e.size/1024)}))}:null},l=[`Audit complete for ${e}`];if(r.vitals.fcp&&l.push(`FCP: ${r.vitals.fcp}ms`),r.vitals.lcp&&l.push(`LCP: ${r.vitals.lcp}ms`),null!=r.vitals.cls&&l.push(`CLS: ${r.vitals.cls}`),i&&l.push(`Scores — Perf ${i.performance}/100 · A11y ${i.accessibility}/100 · SEO ${i.seo}/100`),r.react?.detected){const e=r.react.version?.startsWith("(")?r.react.version.slice(1,-1):r.react.version,t=r.react.interactionTests??[],s=t.filter(e=>e.flagged).length;l.push(`Framework: ${e||"React"} · Re-render issues: ${r.react.issues.length}${t.length?` · Interaction tests: ${t.length} (${s} flagged)`:""}`)}return n&&l.push(`Network: ${n.totalRequests} requests · ${Math.round((n.totalBytes||0)/1024)}KB total · JS ${Math.round((n.jsBytes||0)/1024)}KB · ${n.apiCalls.length} API calls${n.slowApiCalls.length?` (${n.slowApiCalls.length} slow)`:""}${n.failedApiCalls.length?` (${n.failedApiCalls.length} failed)`:""} · ${n.longTasks.length} long tasks (${n.totalBlockingMs}ms)`),this.emit({type:"status_update",message:l.join(" · ")}),a}catch(e){return{ok:!1,error:`Performance audit failed: ${e.message}`}}}case"schedule":{const e=r.cron,t=r.task_description;return this.isScheduledRun?{ok:!1,error:"You are already running as a scheduled sub-task. Do NOT call schedule() again — the parent cron is still active. Use status_update() to report findings, then finish. The next check will happen automatically."}:o.validate(e)?(this.session.scheduledJobs.push({id:`job-${Date.now()}`,cron:e,taskDescription:t,createdAt:(new Date).toISOString()}),m(this.session),f(this.session.id,{type:"scheduled",cron:e,task:t,ts:(new Date).toISOString()}),this.emit({type:"scheduled",cron:e,task:t}),{ok:!0,message:`Task scheduled: '${t}' with cron '${e}'. The process will stay alive and re-run at each interval.`}):{ok:!1,error:`Invalid cron expression: ${e}`}}case"sleep_until":{const e=r.until,t=r.reason||"",s=function(e){const t=new Date(e);if(!isNaN(t.getTime())){const e=t.getTime()-Date.now();return Math.max(0,e)}const s=e.toLowerCase().trim(),o=[[/^(\d+(?:\.\d+)?)\s*s(?:ec(?:onds?)?)?$/,1e3],[/^(\d+(?:\.\d+)?)\s*m(?:in(?:utes?)?)?$/,6e4],[/^(\d+(?:\.\d+)?)\s*h(?:ours?)?$/,36e5],[/^(\d+(?:\.\d+)?)\s*d(?:ays?)?$/,864e5]];for(const[e,t]of o){const o=s.match(e);if(o)return Math.round(parseFloat(o[1])*t)}if(s.includes("tomorrow")){const e=new Date,t=new Date(e);t.setDate(t.getDate()+1);const o=s.match(/(\d{1,2})(?::(\d{2}))?\s*(am|pm)?/);if(o){let e=parseInt(o[1]);const s=parseInt(o[2]||"0");"pm"===o[3]&&e<12&&(e+=12),"am"===o[3]&&12===e&&(e=0),t.setHours(e,s,0,0)}return Math.max(0,t.getTime()-Date.now())}return 6e4}(e),o=new Date(Date.now()+s).toISOString();return f(this.session.id,{type:"sleeping_until",until:o,ts:(new Date).toISOString()}),this.emit({type:"sleeping",until:o}),g(this.session,"sleeping"),await new Promise(e=>setTimeout(e,s)),g(this.session,"running"),{ok:!0,sleptUntil:o,reason:t}}case"write_output":{if(!this.outputFile&&!this.schema)return{ok:!1,error:"No schema or output file configured. Pass --schema and --output when starting the task."};const s=r.data,o=r.mode||"append";return this.session.structuredOutput=function(s,o,r,i){let n;if("overwrite"===o||null==r)n=s;else if(Array.isArray(r))n=Array.isArray(s)?[...r,...s]:[...r,s];else if("object"==typeof r){const e={...r};for(const[t,o]of Object.entries(s))Array.isArray(e[t])&&Array.isArray(o)?e[t]=[...e[t],...o]:e[t]=o;n=e}else n=s;if(i)try{const s=t.dirname(t.resolve(i));e.existsSync(s)||e.mkdirSync(s,{recursive:!0}),e.writeFileSync(i,JSON.stringify(n,null,2)+"\n","utf8")}catch{}return n}(s,o,this.session.structuredOutput,this.outputFile),m(this.session),this.outputFile&&this.emit({type:"output_written",path:this.outputFile,data:s}),{ok:!0,written:s}}case"done":return{ok:!0};default:if(this.customTools&&this.customTools[s]&&"function"==typeof this.customTools[s].execute)try{return await this.customTools[s].execute(r,{toolCallId:"custom"})}catch(e){return{ok:!1,error:`Custom tool '${s}' failed: ${e.message}`}}return{ok:!1,error:`Unknown tool: ${s}`}}}async injectProfile(e){if(e.cookies)for(const t of e.cookies)try{await this.browser.setCookie(t.name,t.value)}catch{}if(e.localStorage)for(const[t,s]of Object.entries(e.localStorage))try{await this.browser.setLocalStorage(t,s)}catch{}if(e.sessionStorage)for(const[t,s]of Object.entries(e.sessionStorage))try{await this.browser.setSessionStorage(t,s)}catch{}}}function S(e,t){if("get_page_state"===e){const e=t;return{url:e.url,title:e.title,snapshot:"[snapshot omitted from history — call get_page_state() again if needed]",refsCount:e.refsCount}}const s="string"==typeof t?t:JSON.stringify(t);return s.length>3e3?s.slice(0,3e3)+`…[truncated in history, was ${s.length} chars]`:t}async function $(e,t,o){const r=e.slice(0,e.length-20),i=e.slice(e.length-20);if(0===r.length)return e;try{const{text:n}=await s({model:t,messages:[{role:"user",content:"Summarize the following agent conversation history into a compact but detailed summary. Include: what was accomplished, current state, important findings stored in memory, any failures and what was tried. This summary will replace the history to save context.\n\n"+JSON.stringify(r,null,2)}]});return f(o,{type:"context_compacted",fromMessages:e.length,toMessages:1+i.length,ts:(new Date).toISOString()}),[{role:"user",content:`[Context Summary — earlier conversation compacted to save tokens]\n${n}`},{role:"assistant",content:"Understood. I will continue from this summary."},...i]}catch{return i}}const _=new Set(["get_page_state","screenshot","wait","scroll","recall","list_memories","list_credential_profiles","go_back","reload","solve_captcha","status_update","ask_user","save_credentials"]);class b{recentActions=[];WINDOW=20;THRESHOLD=5;record(e,t){if(_.has(e))return;const s=`${e}:${JSON.stringify(t)}`;this.recentActions.push(s),this.recentActions.length>this.WINDOW&&this.recentActions.shift()}isLooping(){if(this.recentActions.length<this.WINDOW)return!1;const e=new Map;for(const t of this.recentActions)e.set(t,(e.get(t)||0)+1);return[...e.values()].some(e=>e>=this.THRESHOLD)}}export async function runTask(e){const{goal:r,sessionId:_,headed:C,executablePath:j,saveFlow:O,flowOutputDir:A,schema:I,outputFile:x,maxIterations:N=w,onEvent:T,onSessionUpdate:D,isScheduledRun:P=!1,inheritedMemory:F}=e,M=e=>T?.(e),q=n(),R=l()||process.cwd(),L=Array.isArray(q.llm)?q.llm:[q.llm];let E=0;const J=()=>c(L[E]);let U,W=J(),H={};try{H=a().profiles||{}}catch{}q.telegram?.bot_token&&(U=new y(q.telegram),U.start(),U.sendMessage(`🚀 *Slapify Task Started*\n\nGoal: _${r}_`).catch(()=>{}));const B=new i({headless:!0!==C&&(!1===C||(q.browser?.headless??!0)),timeout:q.browser?.timeout,viewport:q.browser?.viewport,executablePath:j||q.browser?.executablePath});let K,z;if(_){const e=p(_);if(!e)throw new Error(`Session '${_}' not found.`);K=e,K.status="running",m(K);const{rebuildMessages:t}=await import("./session.js");z=t((await import("./session.js")).loadEvents(_)),M({type:"message",text:`Resuming session ${_} (iteration ${K.iteration})`})}else K=d(r),F&&Object.keys(F).length>0&&(Object.assign(K.memory,F),m(K)),z=[{role:"user",content:r}],f(K.id,{type:"session_start",goal:r,ts:(new Date).toISOString()}),M({type:"message",text:`Session ${K.id} started`});D?.(K);let G=t.join(process.cwd(),".slapify","credentials.yaml");try{const{getConfigDir:e}=await import("../config/loader.js"),s=e();s&&(G=t.join(s,"credentials.yaml"))}catch{}const Y=e.onHumanInput??(async(e,t)=>{const s=(await import("readline")).createInterface({input:process.stdin,output:process.stdout});return new Promise(o=>{const r=t?`\n ${e}\n (${t})\n > `:`\n ${e}\n > `;s.question(r,e=>{s.close(),o(e.trim())})})}),Z={};if(q.plugins?.tools&&R){const e=await import("url");for(const s of q.plugins.tools)try{const o=t.resolve(R,s),r=e.pathToFileURL(o).href,i=await import(r);for(const[e,t]of Object.entries(i))t&&"object"==typeof t&&"parameters"in t&&"execute"in t&&(Z[e]=t)}catch(e){console.error(`\n⚠️ [Slapify] Warning: Failed to load user tool file "${s}": ${e.message}\n`)}}const V={...u,...Z},Q=new k(B,K,H,M,Y,G,P,I,x,Z),X=new b;if(Object.keys(K.memory).length>0){const e=Object.entries(K.memory).map(([e,t])=>`- ${e}: ${t}`).join("\n");let t;if(P){const s=K.memory.thread_url||K.memory.conversation_url;t=`[SCHEDULED CHECK-IN — you are a recurring monitoring run]\nThis is NOT the first run. A parent cron job spawned you. Do NOT call schedule() again.\nYour job: check for new activity, respond if needed, call done() when finished.\n\nContext from parent session:\n${e}`+(s?`\nIMPORTANT: Navigate directly to ${s} — do NOT start a new login flow if you are already on LinkedIn.`:"")}else t=`[Memory from previous session]\n${e}`;z.unshift({role:"user",content:t})}else P&&z.unshift({role:"user",content:"[SCHEDULED CHECK-IN] You are a recurring monitoring run. Do NOT call schedule() again. Check for new activity, respond if needed, then call done()."});let ee=!1,te="";try{for(;!ee&&K.iteration<N;){K.iteration++,m(K),D?.(K),f(K.id,{type:"iteration_start",iteration:K.iteration,ts:(new Date).toISOString()});const e=JSON.stringify(z).length;if(e>2e5&&(M({type:"message",text:`Compacting context (${Math.round(e/1e3)}k chars)...`}),z=await $(z,W,K.id)),U){const e=U.popMessages();if(e.length>0){const t=e.join("\n\n");M({type:"message",text:`Injecting live instruction from Telegram: "${t.slice(0,50)}..."`}),z.push({role:"user",content:`[LIVE INSTRUCTION FROM USER VIA TELEGRAM]\nThe user just sent the following message while you were running. You MUST read this and adjust your current plan accordingly:\n\n${t}`})}}M({type:"thinking"});const t=h(r,I,q.plugins,R);let o;for(;;)try{o=await s({model:W,system:t,messages:z,tools:V});break}catch(e){const t=e?.message||String(e);if(t.includes("Too Many Requests")||t.includes("429")||t.includes("402")||t.includes("Payment Required")||t.includes("401")||t.includes("403")||t.includes("quota")||t.includes("billing")){const e=L[E].label||`${L[E].provider}/${L[E].model}`;if(E+1<L.length){E++,W=J();const s=L[E].label||`${L[E].provider}/${L[E].model}`;M({type:"message",text:`⚠️ ${e} failed (${t.slice(0,60)}). Switching to ${s}...`});continue}E=0,W=J(),M({type:"message",text:"⚠️ All models failed. Sleeping 60s before retrying from the start of the chain..."}),await new Promise(e=>setTimeout(e,6e4));continue}throw e}const i=(o.toolCalls||[]).map(e=>({toolCallId:e.toolCallId,toolName:e.toolName,args:e.args}));f(K.id,{type:"llm_response",text:o.text||"",toolCalls:i,ts:(new Date).toISOString()}),o.text&&M({type:"message",text:o.text});const n=[];o.text&&n.push({type:"text",text:o.text});for(const e of o.toolCalls||[])n.push({type:"tool-call",toolCallId:e.toolCallId,toolName:e.toolName,args:e.args});if(n.length>0&&z.push({role:"assistant",content:n}),!o.toolCalls||0===o.toolCalls.length){if("stop"===o.finishReason){te=o.text||"Task complete.",ee=!0;break}continue}const a=[];for(const e of o.toolCalls){const t=e.toolName,s=e.args;if("done"===t){if(te=s.summary||"Task complete.",ee=!0,s.save_flow||O){const e=await v(K,r,A);K.savedFlowPath=e,M({type:"flow_saved",path:e})}a.push({type:"tool-result",toolCallId:e.toolCallId,toolName:t,result:JSON.stringify({ok:!0})}),f(K.id,{type:"tool_call",toolName:t,args:s,result:{ok:!0},ts:(new Date).toISOString()});break}if(X.record(t,s),X.isLooping()){M({type:"message",text:"Loop detected — changing approach..."}),a.push({type:"tool-result",toolCallId:e.toolCallId,toolName:t,result:JSON.stringify({ok:!1,error:"Loop detected: you have been repeating the same actions. Change your approach or call done()."})});continue}let o;M({type:"tool_start",toolName:t,args:s});try{o=await Q.execute(t,s),f(K.id,{type:"tool_call",toolName:t,args:s,result:o,ts:(new Date).toISOString()});if(M({type:"tool_done",toolName:t,result:("string"==typeof o?o:JSON.stringify(o)).slice(0,200)}),"done"===t&&(ee=!0,te=JSON.stringify(o,null,2),U&&await U.sendMessage(`✅ *Task Completed*\n\n\`\`\`json\n${te}\n\`\`\``)),"ask_user"===t&&U){const e=o;await U.sendMessage(`❓ *Agent asks*:\n${e.question}`)}if("status_update"===t&&U){const e=o;await U.sendMessage(`ℹ️ *Status update*:\n${e.message}`)}const r=S(t,o),i="string"==typeof r?r:JSON.stringify(r);a.push({type:"tool-result",toolCallId:e.toolCallId,toolName:t,result:i})}catch(o){const r=o?.message||String(o);f(K.id,{type:"tool_error",toolName:t,args:s,error:r,ts:(new Date).toISOString()}),M({type:"tool_error",toolName:t,error:r}),a.push({type:"tool-result",toolCallId:e.toolCallId,toolName:t,result:JSON.stringify({ok:!1,error:r})})}}a.length>0&&z.push({role:"tool",content:a})}K.iteration>=N&&!ee?(te=`Task hit the maximum iteration limit (${N}) without completing.`,g(K,"failed")):ee&&(K.scheduledJobs.length>0?await async function(e,t,s,r,i){g(e,"scheduled");for(const s of e.scheduledJobs)i({type:"message",text:`Registering cron: ${s.cron} — ${s.taskDescription}`}),o.schedule(s.cron,async()=>{const o=(new Date).toISOString();s.lastRun=o,m(e),i({type:"message",text:`[cron ${s.cron}] Running: ${s.taskDescription}`});const r={...e.memory},n=r.thread_url||r.conversation_url,a=n?`${s.taskDescription}\n\n[Use thread_url from memory: ${n}]`:s.taskDescription;try{await runTask({...t,goal:a,sessionId:void 0,isScheduledRun:!0,inheritedMemory:r})}catch(e){i({type:"error",error:`Cron job failed: ${e?.message}`})}});i({type:"message",text:`${e.scheduledJobs.length} cron job(s) active. Process will stay alive. Press Ctrl+C to stop.`}),await new Promise(()=>{})}(K,e,0,0,M):g(K,"completed")),K.finalSummary=te,m(K),f(K.id,{type:"session_end",summary:te,status:K.status,ts:(new Date).toISOString()}),M({type:"done",summary:te})}catch(e){const t=e?.message||String(e);throw g(K,"failed"),K.finalSummary=`Error: ${t}`,m(K),M({type:"error",error:t}),U&&!t.includes("User aborted")&&U.sendMessage(`❌ *Task Failed*\n\nError: ${t}`).catch(()=>{}),e}finally{U&&U.stop();try{B.close()}catch{}try{await Q.closeWreqSession()}catch{}}return K}async function v(s,o,r){const i=[`# Generated from task: ${o}`,`# Session: ${s.id}`,`# Generated: ${(new Date).toISOString()}`,""],{loadEvents:n}=await import("./session.js"),a=n(s.id);for(const e of a)if("tool_call"===e.type){const t=C(e.toolName,e.args);t&&i.push(t)}const l=o.toLowerCase().replace(/[^a-z0-9]+/g,"-").slice(0,40).replace(/-$/,""),c=r?t.resolve(process.cwd(),r):process.cwd();e.existsSync(c)||e.mkdirSync(c,{recursive:!0});const u=t.join(c,`${l}.flow`);return e.writeFileSync(u,i.join("\n")+"\n"),u}function C(e,t){switch(e){case"navigate":return`Go to ${t.url}`;case"click":return`Click ${t.description||t.ref}`;case"type":return`Type "${t.text}" into ${t.ref}`;case"press":return`Press ${t.key}`;case"wait":return`Wait ${t.seconds} seconds`;case"scroll":return`Scroll ${t.direction}`;case"reload":return"Reload page";case"go_back":return"Go back";case"inject_credentials":return`@inject ${t.profile_name}`;case"schedule":return`# Scheduled: ${t.cron} — ${t.task_description}`;case"fetch_url":return`# Fetched: ${t.url}`;case"done":return`# Done: ${t.summary}`;default:return null}}
|
|
1
|
+
import e from"fs";import t from"path";import s from"chalk";import{generateText as o}from"ai";import r from"node-cron";import{createSession as i}from"wreq-js";import{BrowserAgent as n}from"../browser/agent.js";import{loadConfig as a,loadCredentials as l,loadConfigDir as c}from"../config/loader.js";import{getModel as u}from"../ai/interpreter.js";import{taskTools as h}from"./tools.js";import{buildSystemPrompt as d}from"./prompt.js";import{createSession as p,loadSession as m,saveSessionMeta as f,appendEvent as g,updateSessionStatus as w}from"./session.js";import{TelegramService as y}from"../chat/telegram.js";const k=400;class S{wreqSession=null;browser;session;credentials;emit;onHumanInput;credentialsFilePath;isScheduledRun;schema;outputFile;customTools;constructor(e,t,s,o,r,i,n=!1,a,l,c){this.browser=e,this.session=t,this.credentials=s,this.emit=o,this.onHumanInput=r,this.credentialsFilePath=i,this.isScheduledRun=n,this.schema=a,this.outputFile=l,this.customTools=c||{}}async getWreqSession(){return this.wreqSession||(this.wreqSession=await i({browser:"chrome_131",os:"macos"})),this.wreqSession}async closeWreqSession(){if(this.wreqSession){try{await this.wreqSession.close()}catch{}this.wreqSession=null}}async execute(s,o){switch(s){case"navigate":{const e=o.url;await this.browser.navigate(e);try{await this.browser.evaluate("(function(){document.querySelectorAll('a[target=\"_blank\"]').forEach(function(a){a.setAttribute('target','_self');});if(!window.__slapifyPatched){window.__slapifyPatched=true;var _orig=window.open;window.open=function(url,name,features){if(url&&String(url).startsWith('http')){window.location.href=url;return window;}return _orig.apply(this,arguments);};}})()")}catch{}return{ok:!0,url:e}}case"get_page_state":{const e=await this.browser.getState();return{url:e.url,title:e.title,snapshot:e.snapshot,refsCount:Object.keys(e.refs).length}}case"click":{const e=o.ref;try{await this.browser.evaluate("(function(){document.querySelectorAll('a[target=\"_blank\"]').forEach(function(a){a.setAttribute('target','_self');});if(!window.__slapifyPatched){window.__slapifyPatched=true;var _orig=window.open;window.open=function(url,name,features){if(url&&String(url).startsWith('http')){window.location.href=url;return window;}return _orig.apply(this,arguments);};}})()")}catch{}return await this.browser.click(e),{ok:!0,clicked:e}}case"type":{const e=o.ref,t=o.text;return o.append?await this.browser.type(e,t):await this.browser.fill(e,t),{ok:!0}}case"press":{const e=o.key;return await this.browser.press(e),{ok:!0}}case"scroll":{const e=o.direction,t=o.amount||300;return await this.browser.scroll(e,t),{ok:!0}}case"wait":{const e=o.seconds;return await this.browser.wait(1e3*e),{ok:!0,waited:`${e}s`}}case"screenshot":return{ok:!0,path:await this.browser.screenshot(),note:"Screenshot captured. Check get_page_state() for interactive elements."};case"reload":return await this.browser.reload(),{ok:!0};case"go_back":return await this.browser.goBack(),{ok:!0};case"list_credential_profiles":return{profiles:Object.entries(this.credentials).map(([e,t])=>({name:e,type:t.type,hasUsername:!(!t.username&&!t.email),hasCookies:!!(t.cookies&&t.cookies.length>0),hasLocalStorage:!!(t.localStorage&&Object.keys(t.localStorage).length>0)}))};case"inject_credentials":{const e=o.profile_name,t=this.credentials[e];return t?"inject"!==t.type?{ok:!1,error:`Profile '${e}' is type '${t.type}', use fill_login_form for login-form profiles`}:(await this.injectProfile(t),await this.browser.wait(300),await this.browser.reload(),{ok:!0,injected:e}):{ok:!1,error:`Profile '${e}' not found`}}case"fill_login_form":{const e=o.profile_name,t=this.credentials[e];return t?"login-form"!==t.type?{ok:!1,error:`Profile '${e}' is type '${t.type}'. Use inject_credentials for inject profiles.`}:{ok:!0,username:t.username||t.email||t.phone||"",password:t.password||"",hint:"Use get_page_state() to find the username/password fields, then type into them and submit the form."}:{ok:!1,error:`Profile '${e}' not found`}}case"solve_captcha":{const e=await this.browser.getState(),t=(e.snapshot,[]),s=[],o=Object.entries(e.refs).filter(([,e])=>{const t=(e.name||e.text||"").toLowerCase();return t.includes("not a robot")||t.includes("i'm not a robot")||t.includes("checkbox")||"checkbox"===e.role});for(const[e]of o)try{await this.browser.click(e),await this.browser.wait(2e3),t.push(`Clicked checkbox ref ${e}`)}catch{s.push(`Failed to click ref ${e}`)}const r=Object.entries(e.refs).filter(([,e])=>{const t=(e.name||e.text||"").toLowerCase();return t.includes("audio")||t.includes("sound")});for(const[e]of r.slice(0,1))try{await this.browser.click(e),await this.browser.wait(1500),t.push(`Clicked audio challenge ref ${e}`)}catch{s.push(`Failed to click audio ref ${e}`)}const i=await this.browser.getState(),n=i.snapshot?.toLowerCase().includes("captcha")||i.snapshot?.toLowerCase().includes("not a robot")||i.url?.includes("sorry");return{attempted:t.length>0,solved:t,failed:s,captchaStillPresent:n,currentUrl:i.url,hint:n?"CAPTCHA still present. Try fetch_url() on a different source for the same data.":"CAPTCHA appears resolved. Call get_page_state() to continue."}}case"fetch_url":{const e=o.url,t=o.headers||{},s=await this.getWreqSession(),r=await s.fetch(e,{headers:{Accept:"application/json, text/html, */*","Accept-Language":"en-US,en;q=0.9",...t}}),i=r.headers.get("content-type")||"",n=await r.text();let a=n;if(i.includes("application/json"))try{a=JSON.parse(n)}catch{a=n}const l="string"==typeof a?a:JSON.stringify(a);return{ok:r.ok,status:r.status,body:l.slice(0,8e3)+(l.length>8e3?"…[truncated]":"")}}case"remember":{const e=o.key,t=o.value;return this.session.memory[e]=t,f(this.session),g(this.session.id,{type:"memory_update",key:e,value:t,ts:(new Date).toISOString()}),{ok:!0,stored:e}}case"recall":{const e=o.key,t=this.session.memory[e];return void 0!==t?{ok:!0,key:e,value:t}:{ok:!1,key:e,error:"Key not found in memory"}}case"list_memories":return{keys:Object.keys(this.session.memory),count:Object.keys(this.session.memory).length};case"status_update":{const e=o.message;return this.emit({type:"status_update",message:e}),{ok:!0}}case"ask_user":{const s=o.question,r=o.hint;this.emit({type:"human_input_needed",question:s,hint:r});const i=await this.onHumanInput(s,r);g(this.session.id,{type:"tool_call",toolName:"ask_user",args:{question:s,hint:r},result:{answer:"[redacted from logs]"},ts:(new Date).toISOString()});if(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z]{2,}\b/i.test(i)&&i.includes(" ")){const s=i.split(/\s+(?:and\s+)?(?:password\s+is\s+|pass(?:word)?[:\s]+)?/i),o=s[0]?.trim(),r=s[1]?.trim();this.emit({type:"human_input_needed",question:"💾 Save these credentials for future sessions?",hint:"Profile name to save as (or press Enter to skip)"});const n=await this.onHumanInput("💾 Save these credentials for future sessions?","Enter a profile name (e.g. 'linkedin', 'gmail') or leave blank to skip");if(n&&n.trim()){const s=n.trim().toLowerCase().replace(/\s+/g,"-"),i={type:"login-form",...o&&{username:o},...r&&{password:r}};try{const o=(await import("yaml")).default;let r={profiles:{}};if(e.existsSync(this.credentialsFilePath))try{const t=o.parse(e.readFileSync(this.credentialsFilePath,"utf-8"));t?.profiles&&(r=t)}catch{}r.profiles[s]=i,e.mkdirSync(t.dirname(this.credentialsFilePath),{recursive:!0}),e.writeFileSync(this.credentialsFilePath,o.stringify(r,{indent:2,lineWidth:0})),this.credentials[s]=i,this.emit({type:"credentials_saved",profileName:s,credType:"login-form"})}catch{}}}return{answer:i}}case"save_credentials":{const s=o.profile_name,r=o.type,i=o.capture_from_browser,n={type:r};if("login-form"===r&&(o.username&&(n.username=o.username),o.password&&(n.password=o.password)),"inject"===r&&i)try{const e=await this.browser.getCookies(),t=await this.browser.getLocalStorage(),s=await this.browser.getSessionStorage();e.length>0&&(n.cookies=e.map(e=>({name:e.name,value:e.value})));const o=e=>{if(!e||"object"!=typeof e||Array.isArray(e))return{};const t={};for(const[s,o]of Object.entries(e))t[String(s)]="string"==typeof o?o:JSON.stringify(o);return t},r=o(t),i=o(s);Object.keys(r).length>0&&(n.localStorage=r),Object.keys(i).length>0&&(n.sessionStorage=i)}catch(e){return{ok:!1,error:`Failed to capture browser state: ${e.message}`}}try{const o=(await import("yaml")).default;let i={profiles:{}};if(e.existsSync(this.credentialsFilePath))try{const t=o.parse(e.readFileSync(this.credentialsFilePath,"utf-8"));t?.profiles&&(i=t)}catch{}return i.profiles[s]=n,e.mkdirSync(t.dirname(this.credentialsFilePath),{recursive:!0}),e.writeFileSync(this.credentialsFilePath,o.stringify(i,{indent:2,lineWidth:0})),this.credentials[s]=n,this.emit({type:"credentials_saved",profileName:s,credType:r}),{ok:!0,message:`Saved profile '${s}' (${r}) to credentials.yaml`,cookieCount:n.cookies?.length??0,localStorageKeys:Object.keys(n.localStorage??{}).length}}catch(e){return{ok:!1,error:`Failed to save credentials: ${e.message}`}}}case"perf_audit":{const e=o.url,t=!1!==o.lighthouse,s=!1!==o.react_scan;this.emit({type:"status_update",message:`⚡ Auditing ${e}...`});try{const{runPerfAudit:o}=await import("../perf/audit.js"),r=await o(e,this.browser,{lighthouse:t,reactScan:s,settleMs:2e3,navigate:!0});this.session.perfAudits||(this.session.perfAudits=[]),this.session.perfAudits.push(r),this.session.perfAudit=r,f(this.session);const i=r.scores??r.lighthouse,n=r.network,a={url:r.url,vitals:r.vitals,scores:i,react:r.react,network:n?{totalRequests:n.totalRequests,totalKB:Math.round((n.totalBytes||0)/1024),jsKB:Math.round((n.jsBytes||0)/1024),apiCalls:n.apiCalls.length,slowApiCalls:n.slowApiCalls.length,failedApiCalls:n.failedApiCalls.length,longTasks:n.longTasks.length,totalBlockingMs:n.totalBlockingMs,memoryMB:n.memoryMB,slowApis:n.slowApiCalls.slice(0,5).map(e=>({url:e.url.length>80?"…"+e.url.slice(-80):e.url,method:e.method,status:e.status,durationMs:e.duration})),heaviestResources:n.heaviestResources.slice(0,5).map(e=>({url:e.url.split("/").slice(-2).join("/"),type:e.type,sizeKB:Math.round(e.size/1024)}))}:null},l=[`Audit complete for ${e}`];if(r.vitals.fcp&&l.push(`FCP: ${r.vitals.fcp}ms`),r.vitals.lcp&&l.push(`LCP: ${r.vitals.lcp}ms`),null!=r.vitals.cls&&l.push(`CLS: ${r.vitals.cls}`),i&&l.push(`Scores — Perf ${i.performance}/100 · A11y ${i.accessibility}/100 · SEO ${i.seo}/100`),r.react?.detected){const e=r.react.version?.startsWith("(")?r.react.version.slice(1,-1):r.react.version,t=r.react.interactionTests??[],s=t.filter(e=>e.flagged).length;l.push(`Framework: ${e||"React"} · Re-render issues: ${r.react.issues.length}${t.length?` · Interaction tests: ${t.length} (${s} flagged)`:""}`)}return n&&l.push(`Network: ${n.totalRequests} requests · ${Math.round((n.totalBytes||0)/1024)}KB total · JS ${Math.round((n.jsBytes||0)/1024)}KB · ${n.apiCalls.length} API calls${n.slowApiCalls.length?` (${n.slowApiCalls.length} slow)`:""}${n.failedApiCalls.length?` (${n.failedApiCalls.length} failed)`:""} · ${n.longTasks.length} long tasks (${n.totalBlockingMs}ms)`),this.emit({type:"status_update",message:l.join(" · ")}),a}catch(e){return{ok:!1,error:`Performance audit failed: ${e.message}`}}}case"schedule":{const e=o.cron,t=o.task_description;return this.isScheduledRun?{ok:!1,error:"You are already running as a scheduled sub-task. Do NOT call schedule() again — the parent cron is still active. Use status_update() to report findings, then finish. The next check will happen automatically."}:r.validate(e)?(this.session.scheduledJobs.push({id:`job-${Date.now()}`,cron:e,taskDescription:t,createdAt:(new Date).toISOString()}),f(this.session),g(this.session.id,{type:"scheduled",cron:e,task:t,ts:(new Date).toISOString()}),this.emit({type:"scheduled",cron:e,task:t}),{ok:!0,message:`Task scheduled: '${t}' with cron '${e}'. The process will stay alive and re-run at each interval.`}):{ok:!1,error:`Invalid cron expression: ${e}`}}case"sleep_until":{const e=o.until,t=o.reason||"",s=function(e){const t=new Date(e);if(!isNaN(t.getTime())){const e=t.getTime()-Date.now();return Math.max(0,e)}const s=e.toLowerCase().trim(),o=[[/^(\d+(?:\.\d+)?)\s*s(?:ec(?:onds?)?)?$/,1e3],[/^(\d+(?:\.\d+)?)\s*m(?:in(?:utes?)?)?$/,6e4],[/^(\d+(?:\.\d+)?)\s*h(?:ours?)?$/,36e5],[/^(\d+(?:\.\d+)?)\s*d(?:ays?)?$/,864e5]];for(const[e,t]of o){const o=s.match(e);if(o)return Math.round(parseFloat(o[1])*t)}if(s.includes("tomorrow")){const e=new Date,t=new Date(e);t.setDate(t.getDate()+1);const o=s.match(/(\d{1,2})(?::(\d{2}))?\s*(am|pm)?/);if(o){let e=parseInt(o[1]);const s=parseInt(o[2]||"0");"pm"===o[3]&&e<12&&(e+=12),"am"===o[3]&&12===e&&(e=0),t.setHours(e,s,0,0)}return Math.max(0,t.getTime()-Date.now())}return 6e4}(e),r=new Date(Date.now()+s).toISOString();return g(this.session.id,{type:"sleeping_until",until:r,ts:(new Date).toISOString()}),this.emit({type:"sleeping",until:r}),w(this.session,"sleeping"),await new Promise(e=>setTimeout(e,s)),w(this.session,"running"),{ok:!0,sleptUntil:r,reason:t}}case"write_output":{if(!this.outputFile&&!this.schema)return{ok:!1,error:"No schema or output file configured. Pass --schema and --output when starting the task."};const s=o.data,r=o.mode||"append";return this.session.structuredOutput=function(s,o,r,i){let n;if("overwrite"===o||null==r)n=s;else if(Array.isArray(r))n=Array.isArray(s)?[...r,...s]:[...r,s];else if("object"==typeof r){const e={...r};for(const[t,o]of Object.entries(s))Array.isArray(e[t])&&Array.isArray(o)?e[t]=[...e[t],...o]:e[t]=o;n=e}else n=s;if(i)try{const s=t.dirname(t.resolve(i));e.existsSync(s)||e.mkdirSync(s,{recursive:!0}),e.writeFileSync(i,JSON.stringify(n,null,2)+"\n","utf8")}catch{}return n}(s,r,this.session.structuredOutput,this.outputFile),f(this.session),this.outputFile&&this.emit({type:"output_written",path:this.outputFile,data:s}),{ok:!0,written:s}}case"done":return{ok:!0};default:if(this.customTools&&this.customTools[s]&&"function"==typeof this.customTools[s].execute)try{return await this.customTools[s].execute(o,{toolCallId:"custom"})}catch(e){return{ok:!1,error:`Custom tool '${s}' failed: ${e.message}`}}return{ok:!1,error:`Unknown tool: ${s}`}}}async injectProfile(e){if(e.cookies)for(const t of e.cookies)try{await this.browser.setCookie(t.name,t.value)}catch{}if(e.localStorage)for(const[t,s]of Object.entries(e.localStorage))try{await this.browser.setLocalStorage(t,s)}catch{}if(e.sessionStorage)for(const[t,s]of Object.entries(e.sessionStorage))try{await this.browser.setSessionStorage(t,s)}catch{}}}function _(e,t){if("get_page_state"===e){const e=t;return{url:e.url,title:e.title,snapshot:"[snapshot omitted from history — call get_page_state() again if needed]",refsCount:e.refsCount}}const s="string"==typeof t?t:JSON.stringify(t);return s.length>3e3?s.slice(0,3e3)+`…[truncated in history, was ${s.length} chars]`:t}async function $(e,t,s){const r=e.slice(0,e.length-20),i=e.slice(e.length-20);if(0===r.length)return e;try{const{text:n}=await o({model:t,messages:[{role:"user",content:"Summarize the following agent conversation history into a compact but detailed summary. Include: what was accomplished, current state, important findings stored in memory, any failures and what was tried. This summary will replace the history to save context.\n\n"+JSON.stringify(r,null,2)}]});return g(s,{type:"context_compacted",fromMessages:e.length,toMessages:1+i.length,ts:(new Date).toISOString()}),[{role:"user",content:`[Context Summary — earlier conversation compacted to save tokens]\n${n}`},{role:"assistant",content:"Understood. I will continue from this summary."},...i]}catch{return i}}const b=new Set(["get_page_state","screenshot","wait","scroll","recall","list_memories","list_credential_profiles","go_back","reload","solve_captcha","status_update","ask_user","save_credentials"]);class v{recentActions=[];WINDOW=20;THRESHOLD=5;record(e,t){if(b.has(e))return;const s=`${e}:${JSON.stringify(t)}`;this.recentActions.push(s),this.recentActions.length>this.WINDOW&&this.recentActions.shift()}isLooping(){if(this.recentActions.length<this.WINDOW)return!1;const e=new Map;for(const t of this.recentActions)e.set(t,(e.get(t)||0)+1);return[...e.values()].some(e=>e>=this.THRESHOLD)}}export async function runTask(e){const{goal:i,sessionId:b,headed:j,executablePath:O,saveFlow:A,flowOutputDir:I,schema:x,outputFile:N,maxIterations:T=k,onEvent:D,onSessionUpdate:P,isScheduledRun:F=!1,inheritedMemory:M}=e,q=e=>D?.(e),R=a(),L=c()||process.cwd(),E=Array.isArray(R.llm)?R.llm:[R.llm];let J=0;const U=()=>u(E[J]);let W,B=U(),H={};try{H=l().profiles||{}}catch{}R.telegram?.bot_token&&(W=new y(R.telegram),W.start(),W.sendMessage(`🚀 *Slapify Task Started*\n\nGoal: _${i}_`).catch(()=>{}));const K=new n({headless:!0!==j&&(!1===j||(R.browser?.headless??!0)),timeout:R.browser?.timeout,viewport:R.browser?.viewport,executablePath:O||R.browser?.executablePath,profile:R.browser?.profile,userAgent:R.browser?.userAgent,args:R.browser?.args});let G,z;if(b){const e=m(b);if(!e)throw new Error(`Session '${b}' not found.`);G=e,G.status="running",f(G);const{rebuildMessages:t}=await import("./session.js");z=t((await import("./session.js")).loadEvents(b)),q({type:"message",text:`Resuming session ${b} (iteration ${G.iteration})`})}else G=p(i),M&&Object.keys(M).length>0&&(Object.assign(G.memory,M),f(G)),z=[{role:"user",content:i}],g(G.id,{type:"session_start",goal:i,ts:(new Date).toISOString()}),q({type:"message",text:`Session ${G.id} started`});W&&(G._telegramInstance=W),P?.(G);let Y=t.join(process.cwd(),".slapify","credentials.yaml");try{const{getConfigDir:e}=await import("../config/loader.js"),s=e();s&&(Y=t.join(s,"credentials.yaml"))}catch{}const Z=e.onHumanInput??(async(e,t)=>{const s=(await import("readline")).createInterface({input:process.stdin,output:process.stdout}),o=new Promise(o=>{const r=t?`\n ${e}\n (${t})\n > `:`\n ${e}\n > `;s.question(r,e=>{s.close(),o(e.trim())})});if(W){let e=!1;const t=W.waitForReply().then(t=>(e=!0,t)),r=await Promise.race([o,t]);return e?(s.write("\n"),s.close()):W.cancelWaitForReply(),r}return o}),V={};if(R.plugins?.tools&&L){const e=await import("url");for(const s of R.plugins.tools)try{const o=t.resolve(L,s),r=e.pathToFileURL(o).href,i=await import(r);for(const[e,t]of Object.entries(i))t&&"object"==typeof t&&"parameters"in t&&"execute"in t&&(V[e]=t)}catch(e){console.error(`\n⚠️ [Slapify] Warning: Failed to load user tool file "${s}": ${e.message}\n`)}}const Q={...h,...V},X=new S(K,G,H,q,Z,Y,F,x,N,V),ee=new v;if(Object.keys(G.memory).length>0){const e=Object.entries(G.memory).map(([e,t])=>`- ${e}: ${t}`).join("\n");let t;if(F){const s=G.memory.thread_url||G.memory.conversation_url;t=`[SCHEDULED CHECK-IN — you are a recurring monitoring run]\nThis is NOT the first run. A parent cron job spawned you. Do NOT call schedule() again.\nYour job: check for new activity, respond if needed, call done() when finished.\n\nContext from parent session:\n${e}`+(s?`\nIMPORTANT: Navigate directly to ${s} — do NOT start a new login flow if you are already on LinkedIn.`:"")}else t=`[Memory from previous session]\n${e}`;z.unshift({role:"user",content:t})}else F&&z.unshift({role:"user",content:"[SCHEDULED CHECK-IN] You are a recurring monitoring run. Do NOT call schedule() again. Check for new activity, respond if needed, then call done()."});let te=!1,se="";try{for(;!te&&G.iteration<T;){G.iteration++,f(G),P?.(G),g(G.id,{type:"iteration_start",iteration:G.iteration,ts:(new Date).toISOString()});const e=JSON.stringify(z).length;if(e>2e5&&(q({type:"message",text:`Compacting context (${Math.round(e/1e3)}k chars)...`}),z=await $(z,B,G.id)),W){const e=W.popMessages();if(e.length>0){const t=e.join("\n\n");q({type:"message",text:`Injecting live instruction from Telegram: "${t.slice(0,50)}..."`}),z.push({role:"user",content:`[LIVE INSTRUCTION FROM USER VIA TELEGRAM]\nThe user just sent the following message while you were running. You MUST read this and adjust your current plan accordingly:\n\n${t}`})}}q({type:"thinking"});const t=d(i,x,R.plugins,L);let r;for(;;)try{r=await o({model:B,system:t,messages:z,tools:Q});break}catch(e){const t=e?.message||String(e),s=t.toLowerCase();if(s.includes("too many requests")||s.includes("429")||s.includes("402")||s.includes("payment required")||s.includes("401")||s.includes("403")||s.includes("quota")||s.includes("exhausted")||s.includes("overloaded")||s.includes("rate limit")||s.includes("billing")){const e=E[J].label||`${E[J].provider}/${E[J].model}`;if(J+1<E.length){J++,B=U();const s=E[J].label||`${E[J].provider}/${E[J].model}`;q({type:"message",text:`⚠️ ${e} failed (${t.slice(0,60)}). Switching to ${s}...`});continue}J=0,B=U(),q({type:"message",text:"⚠️ All models failed. Sleeping 60s before retrying from the start of the chain..."}),await new Promise(e=>setTimeout(e,6e4));continue}throw e}const n=(r.toolCalls||[]).map(e=>({toolCallId:e.toolCallId,toolName:e.toolName,args:e.args}));g(G.id,{type:"llm_response",text:r.text||"",toolCalls:n,ts:(new Date).toISOString()}),r.text&&q({type:"message",text:r.text});const a=[];r.text&&a.push({type:"text",text:r.text});for(const e of r.toolCalls||[])a.push({type:"tool-call",toolCallId:e.toolCallId,toolName:e.toolName,args:e.args});if(a.length>0&&z.push({role:"assistant",content:a}),!r.toolCalls||0===r.toolCalls.length){if("stop"===r.finishReason){se=r.text||"Task complete.",te=!0;break}continue}const l=[];for(const e of r.toolCalls){const t=e.toolName,o=e.args;if("done"===t){if(se=o.summary||"Task complete.",te=!0,o.save_flow||A){const e=await C(G,i,I);G.savedFlowPath=e,q({type:"flow_saved",path:e})}l.push({type:"tool-result",toolCallId:e.toolCallId,toolName:t,result:JSON.stringify({ok:!0})}),g(G.id,{type:"tool_call",toolName:t,args:o,result:{ok:!0},ts:(new Date).toISOString()});break}if(ee.record(t,o),ee.isLooping()){q({type:"message",text:"Loop detected — changing approach..."}),l.push({type:"tool-result",toolCallId:e.toolCallId,toolName:t,result:JSON.stringify({ok:!1,error:"Loop detected: you have been repeating the same actions. Change your approach or call done()."})});continue}let r;q({type:"tool_start",toolName:t,args:o}),process.env.SLAPIFY_DEBUG_AI||process.stdout.write(s.dim(` 🔧 ${t}...\r`)),"ask_user"===t&&W&&await W.sendMessage(`❓ *Agent asks*:\n${o.question}`),"status_update"===t&&W&&await W.sendMessage(`ℹ️ *Status update*:\n${o.message}`);try{r=await X.execute(t,o),g(G.id,{type:"tool_call",toolName:t,args:o,result:r,ts:(new Date).toISOString()});q({type:"tool_done",toolName:t,result:("string"==typeof r?r:JSON.stringify(r)).slice(0,200)}),"done"===t&&(te=!0,se=JSON.stringify(r,null,2),W&&await W.sendMessage(`✅ *Task Completed*\n\n\`\`\`json\n${se}\n\`\`\``));const s=_(t,r),i="string"==typeof s?s:JSON.stringify(s);l.push({type:"tool-result",toolCallId:e.toolCallId,toolName:t,result:i})}catch(s){const r=s?.message||String(s);g(G.id,{type:"tool_error",toolName:t,args:o,error:r,ts:(new Date).toISOString()}),q({type:"tool_error",toolName:t,error:r}),l.push({type:"tool-result",toolCallId:e.toolCallId,toolName:t,result:JSON.stringify({ok:!1,error:r})})}}l.length>0&&z.push({role:"tool",content:l})}G.iteration>=T&&!te?(se=`Task hit the maximum iteration limit (${T}) without completing.`,w(G,"failed")):te&&(G.scheduledJobs.length>0?await async function(e,t,s,o,i){w(e,"scheduled");for(const s of e.scheduledJobs)i({type:"message",text:`Registering cron: ${s.cron} — ${s.taskDescription}`}),r.schedule(s.cron,async()=>{const o=(new Date).toISOString();s.lastRun=o,f(e),i({type:"message",text:`[cron ${s.cron}] Running: ${s.taskDescription}`});const r={...e.memory},n=r.thread_url||r.conversation_url,a=n?`${s.taskDescription}\n\n[Use thread_url from memory: ${n}]`:s.taskDescription;try{await runTask({...t,goal:a,sessionId:void 0,isScheduledRun:!0,inheritedMemory:r})}catch(e){i({type:"error",error:`Cron job failed: ${e?.message}`})}});i({type:"message",text:`${e.scheduledJobs.length} cron job(s) active. Process will stay alive. Press Ctrl+C to stop.`}),await new Promise(()=>{})}(G,e,0,0,q):w(G,"completed")),G.finalSummary=se,f(G),g(G.id,{type:"session_end",summary:se,status:G.status,ts:(new Date).toISOString()}),q({type:"done",summary:se})}catch(e){const t=e?.message||String(e);throw w(G,"failed"),G.finalSummary=`Error: ${t}`,f(G),q({type:"error",error:t}),W&&!t.includes("User aborted")&&W.sendMessage(`❌ *Task Failed*\n\nError: ${t}`).catch(()=>{}),e}finally{W&&W.stop();try{K.close()}catch{}try{await X.closeWreqSession()}catch{}}return G}async function C(s,o,r){const i=[`# Generated from task: ${o}`,`# Session: ${s.id}`,`# Generated: ${(new Date).toISOString()}`,""],{loadEvents:n}=await import("./session.js"),a=n(s.id);for(const e of a)if("tool_call"===e.type){const t=j(e.toolName,e.args);t&&i.push(t)}const l=o.toLowerCase().replace(/[^a-z0-9]+/g,"-").slice(0,40).replace(/-$/,""),c=r?t.resolve(process.cwd(),r):process.cwd();e.existsSync(c)||e.mkdirSync(c,{recursive:!0});const u=t.join(c,`${l}.flow`);return e.writeFileSync(u,i.join("\n")+"\n"),u}function j(e,t){switch(e){case"navigate":return`Go to ${t.url}`;case"click":return`Click ${t.description||t.ref}`;case"type":return`Type "${t.text}" into ${t.ref}`;case"press":return`Press ${t.key}`;case"wait":return`Wait ${t.seconds} seconds`;case"scroll":return`Scroll ${t.direction}`;case"reload":return"Reload page";case"go_back":return"Go back";case"inject_credentials":return`@inject ${t.profile_name}`;case"schedule":return`# Scheduled: ${t.cron} — ${t.task_description}`;case"fetch_url":return`# Fetched: ${t.url}`;case"done":return`# Done: ${t.summary}`;default:return null}}
|
package/package.json
CHANGED