slapify 0.0.20 → 0.0.21
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 +67 -5
- package/dist/ai/interpreter.js +1 -1
- package/dist/browser/agent.d.ts +3 -0
- package/dist/browser/agent.js +1 -1
- package/dist/browser/browserbase.d.ts +26 -0
- package/dist/browser/browserbase.js +1 -0
- package/dist/chat/telegram.d.ts +31 -0
- package/dist/chat/telegram.js +1 -0
- package/dist/cli.js +1 -1
- package/dist/config/loader.d.ts +5 -0
- package/dist/config/loader.js +1 -1
- package/dist/runner/index.js +1 -1
- package/dist/task/prompt.d.ts +11 -0
- package/dist/task/prompt.js +1 -0
- package/dist/task/rules/auth.md +28 -0
- package/dist/task/rules/base.md +27 -0
- package/dist/task/rules/batching.md +14 -0
- package/dist/task/rules/captcha.md +24 -0
- package/dist/task/rules/data_lookup.md +11 -0
- package/dist/task/rules/monitoring.md +43 -0
- package/dist/task/rules/perf_audit.md +18 -0
- package/dist/task/rules/rules/auth.md +28 -0
- package/dist/task/rules/rules/base.md +27 -0
- package/dist/task/rules/rules/batching.md +14 -0
- package/dist/task/rules/rules/captcha.md +24 -0
- package/dist/task/rules/rules/data_lookup.md +11 -0
- package/dist/task/rules/rules/monitoring.md +43 -0
- package/dist/task/rules/rules/perf_audit.md +18 -0
- package/dist/task/runner.js +1 -1
- package/dist/types.d.ts +35 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
**AI-powered browser automation that slaps** — run autonomous agents, audit performance, and write E2E tests in plain English.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
https://github.com/user-attachments/assets/52564f16-7664-4ac3-9e06-e04c17dc4bbf
|
|
7
|
+
|
|
6
8
|
|
|
7
9
|
---
|
|
8
10
|
|
|
@@ -102,10 +104,11 @@ npx slapify task "Every day at 9am, collect top tech headlines and add to report
|
|
|
102
104
|
| **Login automatically** | Detects login forms, uses saved credential profiles, asks you to save new ones |
|
|
103
105
|
| **Persistent memory** | Stores key facts between runs (thread URLs, last-seen items, etc.) |
|
|
104
106
|
| **Schedule itself** | Creates its own cron jobs for recurring subtasks |
|
|
105
|
-
| **Ask for input** | Pauses and prompts you when it needs
|
|
107
|
+
| **Ask for input** | Pauses and prompts you via CLI or **Live Telegram Chat** when it needs info (e.g. OTP, conf) |
|
|
106
108
|
| **Performance audit** | Scores, web vitals, network analysis, framework detection, re-render testing |
|
|
107
109
|
| **Structured output** | Writes JSON conforming to a schema to a file (append/update per run) |
|
|
108
110
|
| **HTML report** | Full session report with tool timeline, summaries, and perf data |
|
|
111
|
+
| **Custom Plugins** | Load your own markdown rules or `.ts` tools directly into the agent's brain |
|
|
109
112
|
|
|
110
113
|
### Programmatic (JS/TS)
|
|
111
114
|
|
|
@@ -453,6 +456,12 @@ In **task mode**, credentials are fully automatic:
|
|
|
453
456
|
- If no profile matches, it asks you to enter credentials and offers to save them
|
|
454
457
|
- Saved sessions are reused on future runs — no re-login needed
|
|
455
458
|
|
|
459
|
+
### Persistent Browser Profiles (Anti-Bot)
|
|
460
|
+
|
|
461
|
+
Instead of relying solely on `credentials.yaml`, you can configure the browser to use a persistent Chromium profile on disk. This saves everything natively: cookies, `localStorage`, `sessionStorage`, and cached 2FA tokens. It also helps evade bot detection since the browser builds up a real history.
|
|
462
|
+
|
|
463
|
+
See the `browser.profile` setting in the Configuration section below.
|
|
464
|
+
|
|
456
465
|
---
|
|
457
466
|
|
|
458
467
|
## Configuration
|
|
@@ -460,10 +469,15 @@ In **task mode**, credentials are fully automatic:
|
|
|
460
469
|
### `.slapify/config.yaml`
|
|
461
470
|
|
|
462
471
|
```yaml
|
|
472
|
+
# You can define an array of models! If the primary API drops/rate-limits,
|
|
473
|
+
# Slapify instantly falls back to the next model in the list.
|
|
463
474
|
llm:
|
|
464
|
-
provider: anthropic
|
|
465
|
-
|
|
466
|
-
|
|
475
|
+
- provider: anthropic
|
|
476
|
+
model: claude-3-7-sonnet-20250219
|
|
477
|
+
api_key: ${ANTHROPIC_API_KEY}
|
|
478
|
+
- provider: openai
|
|
479
|
+
model: o3-mini
|
|
480
|
+
api_key: ${OPENAI_API_KEY}
|
|
467
481
|
|
|
468
482
|
browser:
|
|
469
483
|
headless: true
|
|
@@ -471,13 +485,61 @@ browser:
|
|
|
471
485
|
viewport:
|
|
472
486
|
width: 1280
|
|
473
487
|
height: 720
|
|
488
|
+
# Anti-Bot Stealth & Persistence
|
|
489
|
+
profile: ".slapify/browser-profile"
|
|
490
|
+
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"
|
|
491
|
+
args:
|
|
492
|
+
- "--disable-blink-features=AutomationControlled"
|
|
493
|
+
- "--no-sandbox"
|
|
474
494
|
|
|
475
495
|
report:
|
|
476
496
|
format: html
|
|
477
497
|
screenshots: true
|
|
478
498
|
output_dir: ./test-reports
|
|
499
|
+
|
|
500
|
+
# Extend the agent with your own rules and Vercel AI SDK tools
|
|
501
|
+
plugins:
|
|
502
|
+
rules:
|
|
503
|
+
- "rules/my-persona.md"
|
|
504
|
+
tools:
|
|
505
|
+
- "tools/query_database.ts"
|
|
506
|
+
|
|
507
|
+
# Get live status updates and send instructions to the agent mid-run via Telegram
|
|
508
|
+
telegram:
|
|
509
|
+
bot_token: ${TELEGRAM_BOT_TOKEN}
|
|
510
|
+
allowed_chat_id: 123456789
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
---
|
|
514
|
+
|
|
515
|
+
## Extensibility & Live Chat
|
|
516
|
+
|
|
517
|
+
### Custom User Tools
|
|
518
|
+
You can give Slapify access to your local system or database by exporting Vercel AI SDK `tool()` definitions from a `.ts` or `.js` file, and adding the path to `plugins.tools` in your config.
|
|
519
|
+
|
|
520
|
+
```typescript
|
|
521
|
+
// .slapify/tools/database.ts
|
|
522
|
+
import { tool } from "ai";
|
|
523
|
+
import { z } from "zod";
|
|
524
|
+
|
|
525
|
+
export const queryProdDB = tool({
|
|
526
|
+
description: "Execute a secure read-only SQL query.",
|
|
527
|
+
parameters: z.object({ sql: z.string() }),
|
|
528
|
+
execute: async ({ sql }) => {
|
|
529
|
+
// Custom logic here...
|
|
530
|
+
return { rows: [{ id: 1 }] };
|
|
531
|
+
}
|
|
532
|
+
});
|
|
479
533
|
```
|
|
480
534
|
|
|
535
|
+
When the Slapify agent hits an obstacle, it will seamlessly fall back to using your custom `queryProdDB` right alongside its native browser tools!
|
|
536
|
+
|
|
537
|
+
### Telegram Integration
|
|
538
|
+
Add a `telegram` block to your config. The agent will:
|
|
539
|
+
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, go try the other flow instead") and it will process your instruction live!
|
|
542
|
+
|
|
481
543
|
---
|
|
482
544
|
|
|
483
545
|
## LLM Providers
|
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
|
|
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}}}
|
package/dist/browser/agent.d.ts
CHANGED
|
@@ -27,6 +27,9 @@ export declare class BrowserAgent {
|
|
|
27
27
|
*/
|
|
28
28
|
static install(): void;
|
|
29
29
|
private hasInitialized;
|
|
30
|
+
/** CDP URL for Browserbase remote sessions — set once, reused for the lifetime of the task. */
|
|
31
|
+
private browserbaseCdpUrl;
|
|
32
|
+
private browserbaseSessionId;
|
|
30
33
|
/**
|
|
31
34
|
* Navigate to a URL
|
|
32
35
|
*/
|
package/dist/browser/agent.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{execSync as e}from"child_process";const
|
|
1
|
+
import{execSync as e}from"child_process";import{createBrowserbaseSession as t}from"./browserbase.js";const s=["Execution context was destroyed","Target closed","Navigation interrupted","Protocol error","Session closed","Page crashed","Frame was detached","Cannot find context"];export class BrowserAgent{config;isOpen=!1;constructor(e={}){this.config={headless:!0,timeout:3e4,viewport:{width:1280,height:720},...e}}isTransientError(e){return s.some(t=>e.toLowerCase().includes(t.toLowerCase()))}sleep(t){e("sleep "+t/1e3)}exec(t,s=[],i=2){const r=["agent-browser",t,...s].join(" "),n={...process.env};this.config.executablePath&&(n.AGENT_BROWSER_EXECUTABLE_PATH=this.config.executablePath),this.config.profile&&(n.AGENT_BROWSER_PROFILE=this.config.profile),this.config.userAgent&&(n.AGENT_BROWSER_USER_AGENT=this.config.userAgent),this.config.args&&this.config.args.length>0&&(n.AGENT_BROWSER_ARGS=this.config.args.join(","));let o=null;for(let t=0;t<=i;t++)try{return e(r,{encoding:"utf-8",timeout:this.config.timeout,stdio:["pipe","pipe","pipe"],env:n}).trim()}catch(s){const n=s.stderr?.toString()||"",a=s.stdout?.toString()||"";let c=s.message||n||"";const h=`${c} ${a}`;if(h.includes("Browser not launched")&&t<i){try{e("agent-browser close",{stdio:"ignore"})}catch{}this.sleep(1e3);continue}const l=h.includes("daemon already running")||h.includes("--executable-path ignored");if(a){const e=a.trim();if(l&&e)return e;if(!this.isTransientError(e))return e}if(l)return"";if(this.isTransientError(c)&&t<i){this.sleep(500*(t+1));continue}o=new Error(`Browser command failed: ${r}\n${c}`)}throw o||new Error(`Browser command failed: ${r}`)}static isInstalled(){try{return e("agent-browser --version",{stdio:"pipe"}),!0}catch{return!1}}static install(){console.log("Installing agent-browser..."),e("npm install -g agent-browser",{stdio:"inherit"}),console.log("Installing browser..."),e("agent-browser install",{stdio:"inherit"})}hasInitialized=!1;browserbaseCdpUrl=null;browserbaseSessionId=null;async navigate(e){if(this.config.browserbase&&!this.hasInitialized){const{api_key:e,project_id:s}=this.config.browserbase;try{const i=await t(e,s);this.browserbaseCdpUrl=i.cdpUrl,this.browserbaseSessionId=i.id,this.exec("connect",[this.browserbaseCdpUrl]),this.hasInitialized=!0}catch(e){throw new Error(`Browserbase session error: ${e.message}`)}}const s=[];if(!this.hasInitialized&&(!1===this.config.headless||this.config.executablePath)){try{this.exec("close",[]),this.sleep(1e3)}catch{}this.hasInitialized=!0}!1===this.config.headless&&s.push("--headed"),this.config.executablePath&&s.push("--executable-path",`"${this.config.executablePath}"`);const i=()=>this.exec("open",[e,...s]);i(),this.isOpen=!0;for(let e=0;e<2;e++){this.sleep(1500);try{const e=this.exec("get",["url"]).trim();if(e&&"about:blank"!==e&&"about:blank/"!==e)break;try{this.exec("close",[]),this.sleep(1e3)}catch{}i(),this.sleep(1500)}catch{break}}this.config.viewport&&this.exec("set",["viewport",String(this.config.viewport.width),String(this.config.viewport.height)])}async snapshot(e=!0){const t=e?["-i"]:[];return this.exec("snapshot",t)}async snapshotJson(){const e=this.exec("snapshot",["-i","--json"]);try{const t=JSON.parse(e);return{snapshot:t.data?.snapshot||e,refs:t.data?.refs||{}}}catch{return{snapshot:e,refs:{}}}}async click(e){this.exec("click",[e])}async fill(e,t){this.exec("fill",[e,`"${t}"`])}async type(e,t){this.exec("type",[e,`"${t}"`])}async press(e){this.exec("press",[e])}async hover(e){this.exec("hover",[e])}async select(e,t){this.exec("select",[e,t])}async scroll(e,t){const s=t?[e,String(t)]:[e];this.exec("scroll",s)}async wait(e){"number"==typeof e?this.exec("wait",[String(e)]):e.startsWith("text=")?this.exec("wait",["--text",`"${e.substring(5)}"`]):e.startsWith("url=")?this.exec("wait",["--url",`"${e.substring(4)}"`]):this.exec("wait",[e])}async screenshot(e,t=!1){const s=[];return e&&s.push(e),t&&s.push("--full"),this.exec("screenshot",s)}async getText(e){return this.exec("get",["text",e])}async getUrl(){return this.exec("get",["url"])}async getTitle(){return this.exec("get",["title"])}async isVisible(e){return this.exec("is",["visible",e]).toLowerCase().includes("true")}async evaluate(e){return this.exec("eval",[`"${e.replace(/"/g,'\\"')}"`])}async setCookie(e,t){const s=`'${t.replace(/'/g,"'\\''")}'`;this.exec("cookies",["set",e,s])}async getCookies(){const e=this.exec("cookies",["get"]),t=[];if(!e||""===e.trim())return t;const s=e.includes("\n")?e.split("\n"):e.split("; ");for(const e of s){const s=e.trim();if(!s)continue;const i=s.indexOf("=");i>0&&t.push({name:s.substring(0,i),value:s.substring(i+1)})}return t}async getLocalStorage(){const e=this.exec("eval",["'(function(){var r={};for(var i=0;i<localStorage.length;i++){var k=localStorage.key(i);r[k]=localStorage.getItem(k);}return JSON.stringify(r);})()'"]);try{let t=JSON.parse(e);return"string"==typeof t&&(t=JSON.parse(t)),t||{}}catch{return{}}}async getSessionStorage(){const e=this.exec("eval",["'(function(){var r={};for(var i=0;i<sessionStorage.length;i++){var k=sessionStorage.key(i);r[k]=sessionStorage.getItem(k);}return JSON.stringify(r);})()'"]);try{let t=JSON.parse(e);return"string"==typeof t&&(t=JSON.parse(t)),t||{}}catch{return{}}}async setLocalStorage(e,t){const s=`'${t.replace(/'/g,"'\\''")}'`;this.exec("storage",["local","set",e,s])}async setSessionStorage(e,t){const s=`'${t.replace(/'/g,"'\\''")}'`;this.exec("storage",["session","set",e,s])}async goBack(){this.exec("back",[])}async goForward(){this.exec("forward",[])}async reload(){this.exec("reload",[])}async close(){if(this.isOpen){try{this.exec("close",[])}catch{}this.isOpen=!1}}async getState(){let e="",t="",s={snapshot:"",refs:{}};try{e=await this.getUrl()}catch{e="unknown"}try{t=await this.getTitle()}catch{t=""}try{s=await this.snapshotJson()}catch{try{s={snapshot:await this.snapshot(),refs:{}}}catch{s={snapshot:"Unable to get page snapshot",refs:{}}}}return{url:e,title:t,snapshot:s.snapshot,refs:s.refs}}async waitForStable(e=2e3){const t=Date.now();let s="";for(;Date.now()-t<e;){try{const e=await this.getUrl();if(e===s&&"about:blank"!==e)return;s=e}catch{}this.sleep(200)}}}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browserbase cloud browser integration.
|
|
3
|
+
*
|
|
4
|
+
* Browserbase provides managed, scalable headless browser sessions with
|
|
5
|
+
* built-in stealth mode, session recording, and bot-detection evasion.
|
|
6
|
+
*
|
|
7
|
+
* How it works:
|
|
8
|
+
* 1. We call the Browserbase REST API to create a session.
|
|
9
|
+
* 2. They return a CDP (Chrome DevTools Protocol) WebSocket URL.
|
|
10
|
+
* 3. We pass that URL to `agent-browser connect <cdp-url>` which attaches
|
|
11
|
+
* the local agent-browser daemon to the remote session.
|
|
12
|
+
* 4. All subsequent agent-browser commands run against the remote browser —
|
|
13
|
+
* the rest of Slapify's task runner is completely unaware of the difference.
|
|
14
|
+
*
|
|
15
|
+
* Docs: https://docs.browserbase.com/reference/api/create-a-session
|
|
16
|
+
*/
|
|
17
|
+
export interface BrowserbaseSession {
|
|
18
|
+
id: string;
|
|
19
|
+
cdpUrl: string;
|
|
20
|
+
debuggerFullscreenUrl: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Create a new Browserbase session and return its CDP URL.
|
|
24
|
+
*/
|
|
25
|
+
export declare function createBrowserbaseSession(apiKey: string, projectId?: string): Promise<BrowserbaseSession>;
|
|
26
|
+
//# sourceMappingURL=browserbase.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export async function createBrowserbaseSession(e,s){const o={};s&&(o.projectId=s);const t=await fetch("https://www.browserbase.com/v1/sessions",{method:"POST",headers:{"Content-Type":"application/json","X-BB-API-Key":e},body:JSON.stringify(o)});if(!t.ok){const e=await t.text().catch(()=>"");throw new Error(`Browserbase session creation failed (${t.status}): ${e}`)}const r=await t.json(),n=r.connectUrl||`wss://connect.browserbase.com?apiKey=${e}&sessionId=${r.id}`;return{id:r.id,cdpUrl:n,debuggerFullscreenUrl:r.debuggerFullscreenUrl||""}}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { TelegramConfig } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* A ultra-lightweight Telegram bot client for long-polling.
|
|
4
|
+
* We use `fetch` instead of pulling in a heavy dependency like `node-telegram-bot-api`
|
|
5
|
+
* because we only need basic text receive/send capabilities.
|
|
6
|
+
*/
|
|
7
|
+
export declare class TelegramService {
|
|
8
|
+
private botToken;
|
|
9
|
+
private allowedChatId?;
|
|
10
|
+
private lastUpdateId;
|
|
11
|
+
private isPolling;
|
|
12
|
+
private messageQueue;
|
|
13
|
+
private activeChatId?;
|
|
14
|
+
constructor(config: TelegramConfig);
|
|
15
|
+
/**
|
|
16
|
+
* Start long-polling in the background.
|
|
17
|
+
*/
|
|
18
|
+
start(): void;
|
|
19
|
+
stop(): void;
|
|
20
|
+
/**
|
|
21
|
+
* Drain any live messages received from the user.
|
|
22
|
+
*/
|
|
23
|
+
popMessages(): string[];
|
|
24
|
+
/**
|
|
25
|
+
* Send a message to the active chat (wherever the last command came from,
|
|
26
|
+
* or the specifically configured allowed_chat_id).
|
|
27
|
+
*/
|
|
28
|
+
sendMessage(text: string): Promise<void>;
|
|
29
|
+
private pollLoop;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=telegram.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export class TelegramService{botToken;allowedChatId;lastUpdateId=0;isPolling=!1;messageQueue=[];activeChatId;constructor(t){this.botToken=t.bot_token,this.allowedChatId=t.allowed_chat_id}start(){this.isPolling||(this.isPolling=!0,this.pollLoop().catch(t=>{console.error("[Telegram] Fatal polling error:",t),this.isPolling=!1}))}stop(){this.isPolling=!1}popMessages(){if(0===this.messageQueue.length)return[];const t=[...this.messageQueue];return this.messageQueue=[],t}async sendMessage(t){const e=this.allowedChatId||this.activeChatId;if(e)try{await fetch(`https://api.telegram.org/bot${this.botToken}/sendMessage`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({chat_id:e,text:t,parse_mode:"Markdown"})})}catch(t){console.error("[Telegram] Failed to send message:",t)}}async pollLoop(){for(;this.isPolling;)try{const t=await fetch(`https://api.telegram.org/bot${this.botToken}/getUpdates?offset=${this.lastUpdateId+1}&timeout=30`,{signal:AbortSignal.timeout?.(35e3)||void 0});if(!t.ok){if(401===t.status)return console.error("\n⚠️ [Telegram] Invalid bot token. Disabling Telegram integration."),void(this.isPolling=!1);await new Promise(t=>setTimeout(t,2e3));continue}const e=await t.json();if(e.ok&&Array.isArray(e.result))for(const t of e.result){this.lastUpdateId=Math.max(this.lastUpdateId,t.update_id);const e=t.message;if(!e||!e.text)continue;const s=e.chat.id;this.allowedChatId&&String(s)!==String(this.allowedChatId)||(this.activeChatId=s,console.log(`\n💬 [Telegram] Received live instruction: "${e.text}"`),this.messageQueue.push(e.text.trim()))}}catch(t){"AbortError"!==t.name&&"TimeoutError"!==t.name&&await new Promise(t=>setTimeout(t,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.19","-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(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 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();
|
package/dist/config/loader.d.ts
CHANGED
|
@@ -3,6 +3,11 @@ import { SlapifyConfig, CredentialsConfig } from "../types.js";
|
|
|
3
3
|
* Load and parse the main config file
|
|
4
4
|
*/
|
|
5
5
|
export declare function loadConfig(configDir?: string): SlapifyConfig;
|
|
6
|
+
/**
|
|
7
|
+
* Return the resolved .slapify config directory path, or null if not found.
|
|
8
|
+
* Exported so other modules (e.g. prompt builder) can discover user plugin files.
|
|
9
|
+
*/
|
|
10
|
+
export declare function loadConfigDir(): string | null;
|
|
6
11
|
/**
|
|
7
12
|
* Load and parse the credentials file
|
|
8
13
|
*/
|
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,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 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
|
|
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 h=`llm:\n provider: ${p}\n model: ${m}`;h+="ollama"===p?"\n # Ollama runs locally - no API key needed\n base_url: http://localhost:11434/v1":`\n api_key: \${${u}}`;const f=`# Slapify Configuration\n# Docs: https://slaps.dev/slapify\n\n# LLM Settings (required)\n# You can change the provider and model anytime\n${h}\n\n# Browser Automation Settings\nbrowser:\n headless: true\n timeout: 30000 # max time per action (ms)\n viewport:\n width: 1280\n height: 720\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..."\n # args:\n # - "--disable-blink-features=AutomationControlled"\n # - "--no-sandbox"\n${d}\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),f);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/runner/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{BrowserAgent as e}from"../browser/agent.js";import{AIInterpreter as t}from"../ai/interpreter.js";import*as s from"fs";import*as a from"path";import*as i from"yaml";export class TestRunner{config;credentials;browser;ai;autoHandled=[];allAssumptions=[];constructor(s,a){this.config=s,this.credentials=a,this.browser=new e(s.browser),this.ai=new t(s.llm)}async runFlow(e,t,s){const a=new Date,i=[];let o;try{for(const s of e.steps){let e=await this.executeStep(s);const a=s.text.match(/@debug_wait/i);if("failed"===e.status&&!s.optional&&!a){await this.browser.wait(1e3);const t=await this.executeStep(s);t.retried=!0,t.duration+=e.duration,e=t}if(i.push(e),t&&t(e),"failed"===e.status&&!s.optional)break}if(s)try{const e=await this.browser.getUrl(),{collectCoreWebVitals:t,collectReactScanResults:s}=await import("../perf/audit.js"),[a,i]=await Promise.all([t(this.browser),s(this.browser)]);o={url:e,auditedAt:(new Date).toISOString(),vitals:a,react:i,scores:null}}catch{}}finally{await this.browser.close()}if(s&&o)try{const{runLighthouseAudit:e}=await import("../perf/audit.js"),t=this.config.report?.output_dir||"./test-reports",s=await e(o.url,t);s&&(o.scores=s.scores,o.lighthouse=s.scores,s.reportPath&&(o.lighthouseReportPath=s.reportPath))}catch{}const r=new Date,n=i.filter(e=>"passed"===e.status).length,c=i.filter(e=>"failed"===e.status).length,l=i.filter(e=>"skipped"===e.status).length;return{flowFile:e.path||e.name,status:0===c||i.every(t=>"failed"!==t.status||e.steps[i.indexOf(t)]?.optional)?"passed":"failed",steps:i,totalSteps:e.steps.length,passedSteps:n,failedSteps:c,skippedSteps:l,duration:r.getTime()-a.getTime(),startTime:a,endTime:r,autoHandled:this.autoHandled,assumptions:this.allAssumptions,...o?{perfAudit:o}:{}}}async executeStep(e){const t=Date.now(),s=[],a=[];try{const i=await this.browser.getState();await this.handleInterruptions(s);const o=[/login\s+with\s+([\w-]+)\s+credentials/i,/@inject[:\s]+([\w-]+)/i,/inject\s+([\w-]+)\s+credentials/i,/use\s+([\w-]+)\s+credentials/i],r=[/^(log\s?in|sign\s?in|authenticate|login)(\s+to\s+\S+)?(\s+using\s+credentials?)?$/i,/^(log\s?in|sign\s?in)\s+with\s+credentials?$/i];let n=null;for(const t of o){const s=e.text.match(t);if(s){n=s[1];break}}if(!n){r.some(t=>t.test(e.text.trim()))&&(n=this.pickBestCredentialProfile(i.url))}const c=e.text.match(/@debug_wait(?:[:\s]+(.+))?/i);if(c){const a=c[1]?.trim()||"captured";return await this.handleDebugWait(a,s),{step:e,status:"passed",duration:Date.now()-t,actions:s}}if(n){const e=this.credentials.profiles[n];if(!e)throw new Error(`Credential profile not found: ${n}`);await this.handleLogin(e,s)}else{const o=await this.ai.interpretStep(e,i,this.credentials.profiles);if(o.needsCredentials&&!n){const a=o.credentialProfile,r=this.credentials?.profiles||{};if(n=a&&r[a]?a:this.pickBestCredentialProfile(i.url),n){await this.handleLogin(r[n],s);const a=this.config.report?.screenshots?await this.browser.screenshot(`step-${e.line}.png`).catch(()=>{}):void 0;return{step:e,status:"passed",duration:Date.now()-t,actions:s,assumptions:[`Auto-selected credential profile: ${n}`],screenshot:a}}}if(o.skipReason){if(e.optional||e.conditional)return{step:e,status:"skipped",duration:Date.now()-t,actions:[{type:"info",description:o.skipReason,timestamp:Date.now()}]};throw new Error(o.skipReason)}o.assumptions.length>0&&(a.push(...o.assumptions),this.allAssumptions.push(...o.assumptions));for(const e of o.actions)await this.executeCommand(e,s)}let l;if(await this.handleCaptcha(s),await this.handleInterruptions(s),this.config.report?.screenshots){const t=`step-${e.line}.png`;await this.browser.screenshot(t),l=t}return{step:e,status:"passed",duration:Date.now()-t,actions:s,assumptions:a.length>0?a:void 0,screenshot:l}}catch(a){let i;try{if(this.config.report?.screenshots){const t=`step-${e.line}-failed.png`;await this.browser.screenshot(t),i=t}}catch{}return e.optional?{step:e,status:"skipped",duration:Date.now()-t,actions:s,error:a.message,screenshot:i}:{step:e,status:"failed",duration:Date.now()-t,actions:s,error:a.message,screenshot:i}}}async executeCommand(e,t){switch(t.push({type:this.getActionType(e.command),description:e.description,selector:e.args[0],value:e.args[1],timestamp:Date.now()}),e.command){case"navigate":await this.browser.navigate(e.args[0]),await this.browser.waitForStable();break;case"click":await this.browser.click(e.args[0]),await this.browser.wait(300);break;case"fill":await this.browser.fill(e.args[0],e.args[1]);break;case"type":await this.browser.type(e.args[0],e.args[1]);break;case"press":await this.browser.press(e.args[0]);break;case"hover":await this.browser.hover(e.args[0]);break;case"select":await this.browser.select(e.args[0],e.args[1]);break;case"scroll":await this.browser.scroll(e.args[0],parseInt(e.args[1]));break;case"wait":await this.browser.wait(parseInt(e.args[0]));break;case"waitForText":await this.browser.wait(`text=${e.args[0]}`);break;case"getText":await this.browser.getText(e.args[0]);break;case"screenshot":await this.browser.screenshot(e.args[0]);break;case"goBack":await this.browser.goBack();break;case"reload":await this.browser.reload();break;default:throw new Error(`Unknown command: ${e.command}`)}await this.browser.wait(100)}pickBestCredentialProfile(e){const t=this.credentials?.profiles;if(!t||0===Object.keys(t).length)return null;try{const s=new URL(e).hostname.replace(/^www\./,""),a=s.split(".")[0];for(const e of Object.keys(t))if(e.toLowerCase()===a.toLowerCase()||e.toLowerCase()===s.toLowerCase())return e}catch{}return t.default?"default":Object.keys(t)[0]}async handleCaptcha(e){try{const t=await this.browser.getState(),s=(t.snapshot||"").toLowerCase(),a=(t.title||"").toLowerCase(),i=(t.url||"").toLowerCase();if(![s.includes("recaptcha"),s.includes("hcaptcha"),s.includes("turnstile"),s.includes("i'm not a robot"),s.includes("i am not a robot"),s.includes("verify you are human"),s.includes("verify you're human"),a.includes("just a moment"),i.includes("challenge")].some(Boolean))return;const o=await this.ai.findCaptchaAction(t);if(!o)return;await this.browser.click(o.ref),await this.browser.wait(2e3);const r=`Auto-solved captcha: ${o.description}`;this.autoHandled.push(r),e.push({type:"auto-handle",description:r,selector:o.ref,timestamp:Date.now()})}catch{}}async handleInterruptions(e){try{const t=await this.browser.getState(),s=await this.ai.checkAutoHandle(t);for(const t of s)try{await this.browser.click(t.ref);const s=`Auto-handled: ${t.description}`;this.autoHandled.push(s),e.push({type:"auto-handle",description:s,selector:t.ref,timestamp:Date.now()}),await this.browser.wait(500)}catch{}}catch{}}async handleLogin(e,t){if("inject"===e.type){let s=0,a=0;if(e.cookies)for(const i of e.cookies)try{await this.browser.setCookie(i.name,i.value),t.push({type:"fill",description:`Set cookie: ${i.name}`,timestamp:Date.now()}),s++}catch(e){t.push({type:"info",description:`⚠ Failed to set cookie: ${i.name} (${e.message?.split("\n")[0]||"unknown error"})`,timestamp:Date.now()}),a++}if(e.localStorage)for(const[i,o]of Object.entries(e.localStorage))try{await this.browser.setLocalStorage(i,o),t.push({type:"fill",description:`Set localStorage: ${i}`,timestamp:Date.now()}),s++}catch(e){t.push({type:"info",description:`⚠ Failed to set localStorage: ${i} (${e.message?.split("\n")[0]||"unknown error"})`,timestamp:Date.now()}),a++}if(e.sessionStorage)for(const[i,o]of Object.entries(e.sessionStorage))try{await this.browser.setSessionStorage(i,o),t.push({type:"fill",description:`Set sessionStorage: ${i}`,timestamp:Date.now()}),s++}catch(e){t.push({type:"info",description:`⚠ Failed to set sessionStorage: ${i} (${e.message?.split("\n")[0]||"unknown error"})`,timestamp:Date.now()}),a++}return a>0&&console.log(` ⚠ ${a} item(s) failed to inject (continuing with ${s} successful)`),await this.browser.wait(300),void await this.browser.reload()}if("login-form"===e.type&&e.username&&e.password){const s=await this.browser.getState(),a=await this.ai.findLoginForm(s);if(!a)throw new Error("Could not find login form on page");if(await this.browser.fill(a.usernameRef,e.username),t.push({type:"fill",description:"Filled username field",selector:a.usernameRef,timestamp:Date.now()}),await this.browser.fill(a.passwordRef,e.password),t.push({type:"fill",description:"Filled password field",selector:a.passwordRef,timestamp:Date.now()}),await this.browser.click(a.submitRef),t.push({type:"click",description:"Clicked login button",selector:a.submitRef,timestamp:Date.now()}),await this.browser.wait(2e3),e.totp_secret){this.generateTOTP(e.totp_secret);t.push({type:"fill",description:"Entered TOTP code",timestamp:Date.now()})}e.fixed_otp&&t.push({type:"fill",description:"Entered fixed OTP",timestamp:Date.now()})}}generateTOTP(e){Math.floor(Date.now()/1e3/30);return"000000"}async handleDebugWait(e,t){console.log("\n"+"=".repeat(60)),console.log("🔴 DEBUG WAIT - Browser paused for manual interaction"),console.log("=".repeat(60)),console.log("\nYou can now interact with the browser manually."),console.log("(e.g., log in, complete 2FA, accept cookies, etc.)\n"),console.log("Press ENTER when done to capture cookies & localStorage...\n"),t.push({type:"info",description:"Paused for manual interaction (@debug_wait)",timestamp:Date.now()}),await new Promise(e=>{const t=()=>{process.stdin.removeListener("data",t),process.stdin.pause(),e()};process.stdin.resume(),process.stdin.once("data",t)}),console.log("\n📸 Capturing browser state...\n");const o=await this.browser.getCookies();let r=await this.browser.getLocalStorage(),n=await this.browser.getSessionStorage();const c=e=>{if("string"==typeof e)try{e=JSON.parse(e)}catch{return{}}if(!e||"object"!=typeof e||Array.isArray(e))return{};const t={};for(const[s,a]of Object.entries(e))t[String(s)]="string"==typeof a?a:JSON.stringify(a);return t};r=c(r),n=c(n);const l={type:"inject"};o.length>0&&(l.cookies=o.map(e=>({name:e.name,value:e.value}))),Object.keys(r).length>0&&(l.localStorage=r),Object.keys(n).length>0&&(l.sessionStorage=n);const p=a.join(process.cwd(),"temp_credentials.yaml");let w={profiles:{}};if(s.existsSync(p))try{const e=i.parse(s.readFileSync(p,"utf-8"));e&&e.profiles&&"object"==typeof e.profiles&&(w={profiles:e.profiles})}catch{w={profiles:{}}}w.profiles[e]=l;const h=`# Captured credentials from @debug_wait\n# Generated: ${(new Date).toISOString()}\n#\n# To use: copy the profile you need to .slapify/credentials.yaml\n# Then use: @inject ${e}\n\n${i.stringify(w,{indent:2,lineWidth:0})}`;s.writeFileSync(p,h),console.log("✅ Captured:"),console.log(` - ${o.length} cookie(s)`),console.log(` - ${Object.keys(r).length} localStorage item(s)`),console.log(` - ${Object.keys(n).length} sessionStorage item(s)`),console.log(`\n📁 Saved to: ${p}`),console.log(` Profile name: "${e}"`),console.log("\n"+"=".repeat(60)+"\n"),t.push({type:"info",description:`Captured ${o.length} cookies, ${Object.keys(r).length} localStorage, ${Object.keys(n).length} sessionStorage to temp_credentials.yaml`,timestamp:Date.now()})}getActionType(e){switch(e){case"navigate":return"navigate";case"click":case"hover":case"press":return"click";case"fill":case"type":case"select":return"fill";case"wait":case"waitForText":return"wait";default:return"info"}}}
|
|
1
|
+
import{BrowserAgent as e}from"../browser/agent.js";import{AIInterpreter as t}from"../ai/interpreter.js";import*as s from"fs";import*as a from"path";import*as i from"yaml";export class TestRunner{config;credentials;browser;ai;autoHandled=[];allAssumptions=[];constructor(s,a){this.config=s,this.credentials=a,this.browser=new e(s.browser),this.ai=new t(Array.isArray(s.llm)?s.llm[0]:s.llm)}async runFlow(e,t,s){const a=new Date,i=[];let o;try{for(const s of e.steps){let e=await this.executeStep(s);const a=s.text.match(/@debug_wait/i);if("failed"===e.status&&!s.optional&&!a){await this.browser.wait(1e3);const t=await this.executeStep(s);t.retried=!0,t.duration+=e.duration,e=t}if(i.push(e),t&&t(e),"failed"===e.status&&!s.optional)break}if(s)try{const e=await this.browser.getUrl(),{collectCoreWebVitals:t,collectReactScanResults:s}=await import("../perf/audit.js"),[a,i]=await Promise.all([t(this.browser),s(this.browser)]);o={url:e,auditedAt:(new Date).toISOString(),vitals:a,react:i,scores:null}}catch{}}finally{await this.browser.close()}if(s&&o)try{const{runLighthouseAudit:e}=await import("../perf/audit.js"),t=this.config.report?.output_dir||"./test-reports",s=await e(o.url,t);s&&(o.scores=s.scores,o.lighthouse=s.scores,s.reportPath&&(o.lighthouseReportPath=s.reportPath))}catch{}const r=new Date,n=i.filter(e=>"passed"===e.status).length,c=i.filter(e=>"failed"===e.status).length,l=i.filter(e=>"skipped"===e.status).length;return{flowFile:e.path||e.name,status:0===c||i.every(t=>"failed"!==t.status||e.steps[i.indexOf(t)]?.optional)?"passed":"failed",steps:i,totalSteps:e.steps.length,passedSteps:n,failedSteps:c,skippedSteps:l,duration:r.getTime()-a.getTime(),startTime:a,endTime:r,autoHandled:this.autoHandled,assumptions:this.allAssumptions,...o?{perfAudit:o}:{}}}async executeStep(e){const t=Date.now(),s=[],a=[];try{const i=await this.browser.getState();await this.handleInterruptions(s);const o=[/login\s+with\s+([\w-]+)\s+credentials/i,/@inject[:\s]+([\w-]+)/i,/inject\s+([\w-]+)\s+credentials/i,/use\s+([\w-]+)\s+credentials/i],r=[/^(log\s?in|sign\s?in|authenticate|login)(\s+to\s+\S+)?(\s+using\s+credentials?)?$/i,/^(log\s?in|sign\s?in)\s+with\s+credentials?$/i];let n=null;for(const t of o){const s=e.text.match(t);if(s){n=s[1];break}}if(!n){r.some(t=>t.test(e.text.trim()))&&(n=this.pickBestCredentialProfile(i.url))}const c=e.text.match(/@debug_wait(?:[:\s]+(.+))?/i);if(c){const a=c[1]?.trim()||"captured";return await this.handleDebugWait(a,s),{step:e,status:"passed",duration:Date.now()-t,actions:s}}if(n){const e=this.credentials.profiles[n];if(!e)throw new Error(`Credential profile not found: ${n}`);await this.handleLogin(e,s)}else{const o=await this.ai.interpretStep(e,i,this.credentials.profiles);if(o.needsCredentials&&!n){const a=o.credentialProfile,r=this.credentials?.profiles||{};if(n=a&&r[a]?a:this.pickBestCredentialProfile(i.url),n){await this.handleLogin(r[n],s);const a=this.config.report?.screenshots?await this.browser.screenshot(`step-${e.line}.png`).catch(()=>{}):void 0;return{step:e,status:"passed",duration:Date.now()-t,actions:s,assumptions:[`Auto-selected credential profile: ${n}`],screenshot:a}}}if(o.skipReason){if(e.optional||e.conditional)return{step:e,status:"skipped",duration:Date.now()-t,actions:[{type:"info",description:o.skipReason,timestamp:Date.now()}]};throw new Error(o.skipReason)}o.assumptions.length>0&&(a.push(...o.assumptions),this.allAssumptions.push(...o.assumptions));for(const e of o.actions)await this.executeCommand(e,s)}let l;if(await this.handleCaptcha(s),await this.handleInterruptions(s),this.config.report?.screenshots){const t=`step-${e.line}.png`;await this.browser.screenshot(t),l=t}return{step:e,status:"passed",duration:Date.now()-t,actions:s,assumptions:a.length>0?a:void 0,screenshot:l}}catch(a){let i;try{if(this.config.report?.screenshots){const t=`step-${e.line}-failed.png`;await this.browser.screenshot(t),i=t}}catch{}return e.optional?{step:e,status:"skipped",duration:Date.now()-t,actions:s,error:a.message,screenshot:i}:{step:e,status:"failed",duration:Date.now()-t,actions:s,error:a.message,screenshot:i}}}async executeCommand(e,t){switch(t.push({type:this.getActionType(e.command),description:e.description,selector:e.args[0],value:e.args[1],timestamp:Date.now()}),e.command){case"navigate":await this.browser.navigate(e.args[0]),await this.browser.waitForStable();break;case"click":await this.browser.click(e.args[0]),await this.browser.wait(300);break;case"fill":await this.browser.fill(e.args[0],e.args[1]);break;case"type":await this.browser.type(e.args[0],e.args[1]);break;case"press":await this.browser.press(e.args[0]);break;case"hover":await this.browser.hover(e.args[0]);break;case"select":await this.browser.select(e.args[0],e.args[1]);break;case"scroll":await this.browser.scroll(e.args[0],parseInt(e.args[1]));break;case"wait":await this.browser.wait(parseInt(e.args[0]));break;case"waitForText":await this.browser.wait(`text=${e.args[0]}`);break;case"getText":await this.browser.getText(e.args[0]);break;case"screenshot":await this.browser.screenshot(e.args[0]);break;case"goBack":await this.browser.goBack();break;case"reload":await this.browser.reload();break;default:throw new Error(`Unknown command: ${e.command}`)}await this.browser.wait(100)}pickBestCredentialProfile(e){const t=this.credentials?.profiles;if(!t||0===Object.keys(t).length)return null;try{const s=new URL(e).hostname.replace(/^www\./,""),a=s.split(".")[0];for(const e of Object.keys(t))if(e.toLowerCase()===a.toLowerCase()||e.toLowerCase()===s.toLowerCase())return e}catch{}return t.default?"default":Object.keys(t)[0]}async handleCaptcha(e){try{const t=await this.browser.getState(),s=(t.snapshot||"").toLowerCase(),a=(t.title||"").toLowerCase(),i=(t.url||"").toLowerCase();if(![s.includes("recaptcha"),s.includes("hcaptcha"),s.includes("turnstile"),s.includes("i'm not a robot"),s.includes("i am not a robot"),s.includes("verify you are human"),s.includes("verify you're human"),a.includes("just a moment"),i.includes("challenge")].some(Boolean))return;const o=await this.ai.findCaptchaAction(t);if(!o)return;await this.browser.click(o.ref),await this.browser.wait(2e3);const r=`Auto-solved captcha: ${o.description}`;this.autoHandled.push(r),e.push({type:"auto-handle",description:r,selector:o.ref,timestamp:Date.now()})}catch{}}async handleInterruptions(e){try{const t=await this.browser.getState(),s=await this.ai.checkAutoHandle(t);for(const t of s)try{await this.browser.click(t.ref);const s=`Auto-handled: ${t.description}`;this.autoHandled.push(s),e.push({type:"auto-handle",description:s,selector:t.ref,timestamp:Date.now()}),await this.browser.wait(500)}catch{}}catch{}}async handleLogin(e,t){if("inject"===e.type){let s=0,a=0;if(e.cookies)for(const i of e.cookies)try{await this.browser.setCookie(i.name,i.value),t.push({type:"fill",description:`Set cookie: ${i.name}`,timestamp:Date.now()}),s++}catch(e){t.push({type:"info",description:`⚠ Failed to set cookie: ${i.name} (${e.message?.split("\n")[0]||"unknown error"})`,timestamp:Date.now()}),a++}if(e.localStorage)for(const[i,o]of Object.entries(e.localStorage))try{await this.browser.setLocalStorage(i,o),t.push({type:"fill",description:`Set localStorage: ${i}`,timestamp:Date.now()}),s++}catch(e){t.push({type:"info",description:`⚠ Failed to set localStorage: ${i} (${e.message?.split("\n")[0]||"unknown error"})`,timestamp:Date.now()}),a++}if(e.sessionStorage)for(const[i,o]of Object.entries(e.sessionStorage))try{await this.browser.setSessionStorage(i,o),t.push({type:"fill",description:`Set sessionStorage: ${i}`,timestamp:Date.now()}),s++}catch(e){t.push({type:"info",description:`⚠ Failed to set sessionStorage: ${i} (${e.message?.split("\n")[0]||"unknown error"})`,timestamp:Date.now()}),a++}return a>0&&console.log(` ⚠ ${a} item(s) failed to inject (continuing with ${s} successful)`),await this.browser.wait(300),void await this.browser.reload()}if("login-form"===e.type&&e.username&&e.password){const s=await this.browser.getState(),a=await this.ai.findLoginForm(s);if(!a)throw new Error("Could not find login form on page");if(await this.browser.fill(a.usernameRef,e.username),t.push({type:"fill",description:"Filled username field",selector:a.usernameRef,timestamp:Date.now()}),await this.browser.fill(a.passwordRef,e.password),t.push({type:"fill",description:"Filled password field",selector:a.passwordRef,timestamp:Date.now()}),await this.browser.click(a.submitRef),t.push({type:"click",description:"Clicked login button",selector:a.submitRef,timestamp:Date.now()}),await this.browser.wait(2e3),e.totp_secret){this.generateTOTP(e.totp_secret);t.push({type:"fill",description:"Entered TOTP code",timestamp:Date.now()})}e.fixed_otp&&t.push({type:"fill",description:"Entered fixed OTP",timestamp:Date.now()})}}generateTOTP(e){Math.floor(Date.now()/1e3/30);return"000000"}async handleDebugWait(e,t){console.log("\n"+"=".repeat(60)),console.log("🔴 DEBUG WAIT - Browser paused for manual interaction"),console.log("=".repeat(60)),console.log("\nYou can now interact with the browser manually."),console.log("(e.g., log in, complete 2FA, accept cookies, etc.)\n"),console.log("Press ENTER when done to capture cookies & localStorage...\n"),t.push({type:"info",description:"Paused for manual interaction (@debug_wait)",timestamp:Date.now()}),await new Promise(e=>{const t=()=>{process.stdin.removeListener("data",t),process.stdin.pause(),e()};process.stdin.resume(),process.stdin.once("data",t)}),console.log("\n📸 Capturing browser state...\n");const o=await this.browser.getCookies();let r=await this.browser.getLocalStorage(),n=await this.browser.getSessionStorage();const c=e=>{if("string"==typeof e)try{e=JSON.parse(e)}catch{return{}}if(!e||"object"!=typeof e||Array.isArray(e))return{};const t={};for(const[s,a]of Object.entries(e))t[String(s)]="string"==typeof a?a:JSON.stringify(a);return t};r=c(r),n=c(n);const l={type:"inject"};o.length>0&&(l.cookies=o.map(e=>({name:e.name,value:e.value}))),Object.keys(r).length>0&&(l.localStorage=r),Object.keys(n).length>0&&(l.sessionStorage=n);const p=a.join(process.cwd(),"temp_credentials.yaml");let w={profiles:{}};if(s.existsSync(p))try{const e=i.parse(s.readFileSync(p,"utf-8"));e&&e.profiles&&"object"==typeof e.profiles&&(w={profiles:e.profiles})}catch{w={profiles:{}}}w.profiles[e]=l;const h=`# Captured credentials from @debug_wait\n# Generated: ${(new Date).toISOString()}\n#\n# To use: copy the profile you need to .slapify/credentials.yaml\n# Then use: @inject ${e}\n\n${i.stringify(w,{indent:2,lineWidth:0})}`;s.writeFileSync(p,h),console.log("✅ Captured:"),console.log(` - ${o.length} cookie(s)`),console.log(` - ${Object.keys(r).length} localStorage item(s)`),console.log(` - ${Object.keys(n).length} sessionStorage item(s)`),console.log(`\n📁 Saved to: ${p}`),console.log(` Profile name: "${e}"`),console.log("\n"+"=".repeat(60)+"\n"),t.push({type:"info",description:`Captured ${o.length} cookies, ${Object.keys(r).length} localStorage, ${Object.keys(n).length} sessionStorage to temp_credentials.yaml`,timestamp:Date.now()})}getActionType(e){switch(e){case"navigate":return"navigate";case"click":case"hover":case"press":return"click";case"fill":case"type":case"select":return"fill";case"wait":case"waitForText":return"wait";default:return"info"}}}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { PluginsConfig } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Build a system prompt for the task agent by combining the base rules
|
|
4
|
+
* with relevant fragments detected from the goal string.
|
|
5
|
+
*
|
|
6
|
+
* This keeps the initial context window lean for simple tasks (e.g. data lookup
|
|
7
|
+
* only needs ~800 tokens vs the old 4,500-token monolith) while preserving full
|
|
8
|
+
* functionality for complex tasks by injecting the appropriate rule blocks.
|
|
9
|
+
*/
|
|
10
|
+
export declare function buildSystemPrompt(goal: string, schema?: object, plugins?: PluginsConfig, configDir?: string): string;
|
|
11
|
+
//# sourceMappingURL=prompt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{readFileSync as e}from"fs";import{join as t,dirname as o}from"path";import{fileURLToPath as r}from"url";const a=o(r(import.meta.url)),n=t(a,"rules"),s=new Map;function i(o){return s.has(o)||s.set(o,e(t(n,o),"utf-8")),s.get(o)}export function buildSystemPrompt(t,o,r,a){const n=t.toLowerCase(),s=[i("base.md")];/monitor|keep checking|keep watching|wait for reply|keep me updated|feel free to engage|notify when|let me know when|ongoing|until .*(happens|stops|replies|approved|resolves)|every \d+ (minute|hour|day)|daily at|check hourly|schedule|cron|recurring|spaces? (them )?out/.test(n)&&s.push(i("monitoring.md")),/login|log in|sign in|account|credential|twitter|linkedin|instagram|facebook|gmail|github|password|authenticate|session/.test(n)&&s.push(i("auth.md")),/captcha|cloudflare|bot|blocked|interact|navigate|browser|click|fill|form|post|tweet|reply|comment|search/.test(n)&&s.push(i("captcha.md")),/interact|search|browse|navigate|fill|click|post|tweet|reply|comment|form|login|log in|sign in/.test(n)&&s.push(i("batching.md")),/price|news|weather|rate|stock|fact|data|api|fetch|lookup|find out|what is|how much|current|latest/.test(n)&&s.push(i("data_lookup.md")),/perf|audit|lighthouse|speed|score|lcp|fcp|cls|ttfb|core web vitals|page health|slow|fast/.test(n)&&s.push(i("perf_audit.md"));let c=s.join("\n\n");if(o&&(c+="\n\n## Structured Output Schema\nThe user expects output conforming to this JSON schema:\n```json\n"+JSON.stringify(o,null,2)+"\n```\nUse the **write_output** tool to write conforming data whenever you have results to record (after each scheduled run, after collecting data, or before calling done). For array schemas, each write_output call appends new entries. For object schemas, each call updates the object. Always call write_output before done() when a schema is provided."),r?.rules&&a)for(const t of r.rules)try{const o=require("path").resolve(a,t);c+=`\n\n## Custom User Rule: ${t}\n${e(o,"utf-8")}`}catch(e){console.error(`[Slapify] Warning: Failed to load user plugin rule "${t}": ${e.message}`)}return c}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
## Authentication Rules
|
|
2
|
+
|
|
3
|
+
### Logging in
|
|
4
|
+
1. Check memory for a saved thread_url or page_url — navigate directly there first.
|
|
5
|
+
2. Call get_page_state() — if the URL is the target site (not a login page), you are already logged in. Proceed.
|
|
6
|
+
3. Only if you see a login form: call list_credential_profiles() and use the best matching profile.
|
|
7
|
+
This avoids unnecessary re-login on every scheduled check-in.
|
|
8
|
+
|
|
9
|
+
### MANDATORY after every successful login:
|
|
10
|
+
1. Call save_credentials with capture_from_browser: true immediately after you verify login worked.
|
|
11
|
+
Use a sensible profile_name (e.g. "linkedin", "gmail", "twitter").
|
|
12
|
+
This saves the session cookies so they can be reused next time without logging in again.
|
|
13
|
+
2. Then call status_update("✅ Logged in as [username]. Session saved as '[profile_name]' for future use.")
|
|
14
|
+
|
|
15
|
+
Do not ask the user whether to save — just save automatically.
|
|
16
|
+
If the site was already logged in via injected credentials, skip this step.
|
|
17
|
+
|
|
18
|
+
### Login form shortcut
|
|
19
|
+
Once you have refs from a snapshot, fill the whole form in ONE response:
|
|
20
|
+
type(emailRef, email) + type(passwordRef, password) + click(submitRef)
|
|
21
|
+
Then in the NEXT response: wait(3) + get_page_state() to verify.
|
|
22
|
+
|
|
23
|
+
### Asking the user for credentials
|
|
24
|
+
Use ask_user() only when you genuinely need information not available elsewhere:
|
|
25
|
+
- A one-time password (OTP) or 2FA code
|
|
26
|
+
- A missing password or PIN not in the credential store
|
|
27
|
+
- Confirmation before a destructive or irreversible action
|
|
28
|
+
Keep questions concise. Use the hint field (e.g. "Check your authenticator app").
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
You are Slapify Task Agent — a fully autonomous web agent. You decide the best approach for any task yourself.
|
|
2
|
+
|
|
3
|
+
## Tools
|
|
4
|
+
- **fetch_url(url)** — Direct HTTP GET, no JS, no CAPTCHA. Use first for data lookups.
|
|
5
|
+
- **navigate(url)** + **get_page_state()** — Browser navigation. Always call get_page_state() after navigate().
|
|
6
|
+
- **click(ref)**, **type(ref, text)**, **press(key)**, **scroll(direction)**, **wait(seconds)** — Browser interaction.
|
|
7
|
+
- **list_credential_profiles()**, **inject_credentials(profile)**, **fill_login_form(profile)** — Authentication.
|
|
8
|
+
- **remember(key, value)**, **recall(key)**, **list_memories()** — Persistent memory across runs.
|
|
9
|
+
- **schedule(cron, task)**, **sleep_until(datetime)** — Time-based scheduling.
|
|
10
|
+
- **solve_captcha(type?)** — Attempt automatic CAPTCHA solving.
|
|
11
|
+
- **perf_audit(url)** — Full performance audit (scores, metrics, re-renders, network).
|
|
12
|
+
- **status_update(message)** — Post a visible message to the user.
|
|
13
|
+
- **ask_user(question, hint?)** — Pause and ask the user for input.
|
|
14
|
+
- **done(summary)** — Signal task completion with a full summary.
|
|
15
|
+
|
|
16
|
+
## Core rules
|
|
17
|
+
- Plan before acting: decide if this is a data lookup, interactive, or authenticated task.
|
|
18
|
+
- get_page_state() snapshot contains ALL visible text. Read it carefully before giving up.
|
|
19
|
+
- Always call get_page_state() after every navigate().
|
|
20
|
+
- Do NOT use screenshot() for data extraction.
|
|
21
|
+
- Never repeat the same failing action more than twice — change strategy.
|
|
22
|
+
- Never guess URLs; prefer navigating by clicking available options.
|
|
23
|
+
- Never ask the user for help — figure it out.
|
|
24
|
+
- Use remember() the moment you find important data, before calling done().
|
|
25
|
+
- Call done() with a complete, specific summary including exact data found.
|
|
26
|
+
- Never give up without trying at least 4-5 different approaches.
|
|
27
|
+
- Use status_update() for meaningful events only, not every small step.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
## Batching Tool Calls — Reduce Round Trips
|
|
2
|
+
|
|
3
|
+
You can return **multiple tool calls in a single response** when they don't depend on each other's output.
|
|
4
|
+
This is faster — all calls in one response execute in parallel before the next LLM turn.
|
|
5
|
+
|
|
6
|
+
### Good batching examples:
|
|
7
|
+
- After a snapshot you already have refs → batch: type(emailRef, ...) + type(passwordRef, ...) + click(submitRef)
|
|
8
|
+
- Memory + notification → batch: remember(key, val) + status_update(msg)
|
|
9
|
+
- Click then wait → batch: click(ref) + wait(2) (wait does not need click result)
|
|
10
|
+
- Multiple remembers → batch: remember(k1, v1) + remember(k2, v2) + remember(k3, v3)
|
|
11
|
+
|
|
12
|
+
### Do NOT batch when the second call needs the first call's output:
|
|
13
|
+
- navigate + click → you need get_page_state() in between to learn the ref
|
|
14
|
+
- get_page_state + click → click ref comes FROM get_page_state result
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
## Handling Obstacles
|
|
2
|
+
|
|
3
|
+
### CAPTCHA in browser:
|
|
4
|
+
→ First try fetch_url() on the same URL or a different source — direct HTTP never triggers CAPTCHA.
|
|
5
|
+
→ If you must solve it in the browser: call get_page_state() to inspect the CAPTCHA.
|
|
6
|
+
Look for an iframe or checkbox element.
|
|
7
|
+
For reCAPTCHA v2, find and click the "I'm not a robot" checkbox ref.
|
|
8
|
+
For image CAPTCHAs, look for the audio challenge link.
|
|
9
|
+
→ If one site gives CAPTCHA, try a completely different site with the same data.
|
|
10
|
+
|
|
11
|
+
### "Just a moment..." / Cloudflare:
|
|
12
|
+
→ Bot protection. Switch to fetch_url() on that URL, or find a different site entirely.
|
|
13
|
+
|
|
14
|
+
### Empty page snapshot / "no interactive elements":
|
|
15
|
+
→ JS-rendered page. Call wait(3) then get_page_state() again.
|
|
16
|
+
→ Still empty? Try fetch_url() on the same URL — the raw HTML often has the data even when browser rendering fails.
|
|
17
|
+
|
|
18
|
+
### API returns error / bad format:
|
|
19
|
+
→ Try a different endpoint. Think what other public data sources exist for this topic.
|
|
20
|
+
|
|
21
|
+
### Stuck after multiple attempts:
|
|
22
|
+
→ Change strategy completely. If browser isn't working, use fetch_url(). If one site fails, try another.
|
|
23
|
+
→ Never repeat the same failing action more than twice.
|
|
24
|
+
→ Make sure to not guess URLs unless 100% sure — prefer navigating by clicking on available options.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
## Data Lookup Strategy
|
|
2
|
+
|
|
3
|
+
For tasks involving prices, news, weather, facts, rates, or any structured data:
|
|
4
|
+
|
|
5
|
+
1. Think: does a free public HTTP API exist for this? Most financial/data topics have open APIs.
|
|
6
|
+
2. Try fetch_url() on a likely API endpoint FIRST — it returns data in <1s with no CAPTCHA or JS issues.
|
|
7
|
+
3. If you find useful JSON, parse it and call done() immediately.
|
|
8
|
+
4. If no API works, navigate to a site and read get_page_state() — it contains all visible text.
|
|
9
|
+
|
|
10
|
+
fetch_url() uses Chrome TLS fingerprint impersonation (wreq-js) to bypass Cloudflare and bot detection.
|
|
11
|
+
It is always faster and more reliable than browser navigation for pure data retrieval.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
## Monitoring / Ongoing Tasks — CRITICAL RULES
|
|
2
|
+
|
|
3
|
+
Keywords that indicate this type of task: "monitor", "keep checking", "wait for reply", "keep me updated",
|
|
4
|
+
"feel free to engage", "notify when", "let me know when", "keep watching", "ongoing", "until X happens",
|
|
5
|
+
"schedule", "every day", "check hourly", "daily at", "recurring".
|
|
6
|
+
|
|
7
|
+
These tasks NEVER call done() on their own. The user stops them with Ctrl+C.
|
|
8
|
+
|
|
9
|
+
### FIRST RUN — initial session:
|
|
10
|
+
1. Perform the first action (send message, check price, post content, etc.)
|
|
11
|
+
2. IMMEDIATELY call remember() to store key context:
|
|
12
|
+
- remember("thread_url", "<exact URL of the conversation/page>")
|
|
13
|
+
- remember("last_message_sent", "<text of message you sent>")
|
|
14
|
+
- remember("monitoring_target", "<name of person/thing being monitored>")
|
|
15
|
+
3. Call status_update() to confirm what was done
|
|
16
|
+
4. Call schedule() with a sensible cron interval (e.g. every 5 min for messages, every hour for prices)
|
|
17
|
+
5. Do NOT call done() — the process stays alive, re-running at each cron interval
|
|
18
|
+
|
|
19
|
+
### SCHEDULED CHECK-IN — sub-run spawned by cron:
|
|
20
|
+
1. Check memory for thread_url — navigate DIRECTLY there (do NOT start from homepage)
|
|
21
|
+
2. Check if already logged in by reading get_page_state(). If the URL is the target page, you ARE logged in.
|
|
22
|
+
3. Only log in if the page is a login form. Use list_credential_profiles() to find the right profile.
|
|
23
|
+
4. After navigating to the thread, read get_page_state() to find the latest state
|
|
24
|
+
5. Compare with memory — look for NEW messages, changes, or triggers
|
|
25
|
+
6. If there is a new event: respond/act naturally, then update remember() with the latest state
|
|
26
|
+
7. If nothing new: call status_update("No change yet. Checking again later.")
|
|
27
|
+
8. Call done() — the cron will re-run automatically. Do NOT call schedule() again.
|
|
28
|
+
|
|
29
|
+
### Example — "send a message and monitor for reply":
|
|
30
|
+
FIRST RUN:
|
|
31
|
+
→ Send message
|
|
32
|
+
→ remember("thread_url", "https://www.linkedin.com/messaging/thread/...")
|
|
33
|
+
→ remember("last_message_sent", "Hello! 👋 How are you doing?")
|
|
34
|
+
→ status_update("✉️ Message sent. Monitoring for reply every 5 minutes.")
|
|
35
|
+
→ schedule("*/5 * * * *", "Check for reply and respond if received")
|
|
36
|
+
[do NOT call done()]
|
|
37
|
+
|
|
38
|
+
SCHEDULED CHECK-IN:
|
|
39
|
+
→ recall("thread_url") → navigate directly to that URL
|
|
40
|
+
→ get_page_state() → find latest message
|
|
41
|
+
→ if target replied: respond, remember("last_message_sent", ...)
|
|
42
|
+
→ status_update("✅ Target replied: '...' — responded with '...'" OR "No new reply yet.")
|
|
43
|
+
→ done() [cron handles the next run]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
## Performance Audit Rules
|
|
2
|
+
|
|
3
|
+
perf_audit(url) automatically navigates to the page — do NOT call navigate() before it.
|
|
4
|
+
|
|
5
|
+
It collects:
|
|
6
|
+
- Scores (Performance/Accessibility/SEO/Best Practices 0-100)
|
|
7
|
+
- Real-user metrics: FCP, LCP, CLS, TTFB
|
|
8
|
+
- Framework detection and re-render analysis with simulated interactions
|
|
9
|
+
- Network analysis: totalRequests, totalKB, jsKB, apiCalls, slowApiCalls (>500ms), failedApiCalls,
|
|
10
|
+
longTasks, totalBlockingMs, memoryMB, slowApis list, heaviestResources list
|
|
11
|
+
|
|
12
|
+
You can call perf_audit() multiple times with different URLs to compare pages — the report shows a side-by-side comparison table automatically.
|
|
13
|
+
|
|
14
|
+
If the user asks to audit multiple pages on a domain, also audit the root/home page unless they explicitly say to skip it.
|
|
15
|
+
|
|
16
|
+
When summarising results, use neutral section labels:
|
|
17
|
+
- Use: "Performance Scores", "Real User Metrics", "Lab Metrics", "Framework & Re-renders", "Interaction Tests", "Network & Runtime"
|
|
18
|
+
- Never write: "Lighthouse", "React Scan", or any specific vendor tool name
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
## Authentication Rules
|
|
2
|
+
|
|
3
|
+
### Logging in
|
|
4
|
+
1. Check memory for a saved thread_url or page_url — navigate directly there first.
|
|
5
|
+
2. Call get_page_state() — if the URL is the target site (not a login page), you are already logged in. Proceed.
|
|
6
|
+
3. Only if you see a login form: call list_credential_profiles() and use the best matching profile.
|
|
7
|
+
This avoids unnecessary re-login on every scheduled check-in.
|
|
8
|
+
|
|
9
|
+
### MANDATORY after every successful login:
|
|
10
|
+
1. Call save_credentials with capture_from_browser: true immediately after you verify login worked.
|
|
11
|
+
Use a sensible profile_name (e.g. "linkedin", "gmail", "twitter").
|
|
12
|
+
This saves the session cookies so they can be reused next time without logging in again.
|
|
13
|
+
2. Then call status_update("✅ Logged in as [username]. Session saved as '[profile_name]' for future use.")
|
|
14
|
+
|
|
15
|
+
Do not ask the user whether to save — just save automatically.
|
|
16
|
+
If the site was already logged in via injected credentials, skip this step.
|
|
17
|
+
|
|
18
|
+
### Login form shortcut
|
|
19
|
+
Once you have refs from a snapshot, fill the whole form in ONE response:
|
|
20
|
+
type(emailRef, email) + type(passwordRef, password) + click(submitRef)
|
|
21
|
+
Then in the NEXT response: wait(3) + get_page_state() to verify.
|
|
22
|
+
|
|
23
|
+
### Asking the user for credentials
|
|
24
|
+
Use ask_user() only when you genuinely need information not available elsewhere:
|
|
25
|
+
- A one-time password (OTP) or 2FA code
|
|
26
|
+
- A missing password or PIN not in the credential store
|
|
27
|
+
- Confirmation before a destructive or irreversible action
|
|
28
|
+
Keep questions concise. Use the hint field (e.g. "Check your authenticator app").
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
You are Slapify Task Agent — a fully autonomous web agent. You decide the best approach for any task yourself.
|
|
2
|
+
|
|
3
|
+
## Tools
|
|
4
|
+
- **fetch_url(url)** — Direct HTTP GET, no JS, no CAPTCHA. Use first for data lookups.
|
|
5
|
+
- **navigate(url)** + **get_page_state()** — Browser navigation. Always call get_page_state() after navigate().
|
|
6
|
+
- **click(ref)**, **type(ref, text)**, **press(key)**, **scroll(direction)**, **wait(seconds)** — Browser interaction.
|
|
7
|
+
- **list_credential_profiles()**, **inject_credentials(profile)**, **fill_login_form(profile)** — Authentication.
|
|
8
|
+
- **remember(key, value)**, **recall(key)**, **list_memories()** — Persistent memory across runs.
|
|
9
|
+
- **schedule(cron, task)**, **sleep_until(datetime)** — Time-based scheduling.
|
|
10
|
+
- **solve_captcha(type?)** — Attempt automatic CAPTCHA solving.
|
|
11
|
+
- **perf_audit(url)** — Full performance audit (scores, metrics, re-renders, network).
|
|
12
|
+
- **status_update(message)** — Post a visible message to the user.
|
|
13
|
+
- **ask_user(question, hint?)** — Pause and ask the user for input.
|
|
14
|
+
- **done(summary)** — Signal task completion with a full summary.
|
|
15
|
+
|
|
16
|
+
## Core rules
|
|
17
|
+
- Plan before acting: decide if this is a data lookup, interactive, or authenticated task.
|
|
18
|
+
- get_page_state() snapshot contains ALL visible text. Read it carefully before giving up.
|
|
19
|
+
- Always call get_page_state() after every navigate().
|
|
20
|
+
- Do NOT use screenshot() for data extraction.
|
|
21
|
+
- Never repeat the same failing action more than twice — change strategy.
|
|
22
|
+
- Never guess URLs; prefer navigating by clicking available options.
|
|
23
|
+
- Never ask the user for help — figure it out.
|
|
24
|
+
- Use remember() the moment you find important data, before calling done().
|
|
25
|
+
- Call done() with a complete, specific summary including exact data found.
|
|
26
|
+
- Never give up without trying at least 4-5 different approaches.
|
|
27
|
+
- Use status_update() for meaningful events only, not every small step.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
## Batching Tool Calls — Reduce Round Trips
|
|
2
|
+
|
|
3
|
+
You can return **multiple tool calls in a single response** when they don't depend on each other's output.
|
|
4
|
+
This is faster — all calls in one response execute in parallel before the next LLM turn.
|
|
5
|
+
|
|
6
|
+
### Good batching examples:
|
|
7
|
+
- After a snapshot you already have refs → batch: type(emailRef, ...) + type(passwordRef, ...) + click(submitRef)
|
|
8
|
+
- Memory + notification → batch: remember(key, val) + status_update(msg)
|
|
9
|
+
- Click then wait → batch: click(ref) + wait(2) (wait does not need click result)
|
|
10
|
+
- Multiple remembers → batch: remember(k1, v1) + remember(k2, v2) + remember(k3, v3)
|
|
11
|
+
|
|
12
|
+
### Do NOT batch when the second call needs the first call's output:
|
|
13
|
+
- navigate + click → you need get_page_state() in between to learn the ref
|
|
14
|
+
- get_page_state + click → click ref comes FROM get_page_state result
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
## Handling Obstacles
|
|
2
|
+
|
|
3
|
+
### CAPTCHA in browser:
|
|
4
|
+
→ First try fetch_url() on the same URL or a different source — direct HTTP never triggers CAPTCHA.
|
|
5
|
+
→ If you must solve it in the browser: call get_page_state() to inspect the CAPTCHA.
|
|
6
|
+
Look for an iframe or checkbox element.
|
|
7
|
+
For reCAPTCHA v2, find and click the "I'm not a robot" checkbox ref.
|
|
8
|
+
For image CAPTCHAs, look for the audio challenge link.
|
|
9
|
+
→ If one site gives CAPTCHA, try a completely different site with the same data.
|
|
10
|
+
|
|
11
|
+
### "Just a moment..." / Cloudflare:
|
|
12
|
+
→ Bot protection. Switch to fetch_url() on that URL, or find a different site entirely.
|
|
13
|
+
|
|
14
|
+
### Empty page snapshot / "no interactive elements":
|
|
15
|
+
→ JS-rendered page. Call wait(3) then get_page_state() again.
|
|
16
|
+
→ Still empty? Try fetch_url() on the same URL — the raw HTML often has the data even when browser rendering fails.
|
|
17
|
+
|
|
18
|
+
### API returns error / bad format:
|
|
19
|
+
→ Try a different endpoint. Think what other public data sources exist for this topic.
|
|
20
|
+
|
|
21
|
+
### Stuck after multiple attempts:
|
|
22
|
+
→ Change strategy completely. If browser isn't working, use fetch_url(). If one site fails, try another.
|
|
23
|
+
→ Never repeat the same failing action more than twice.
|
|
24
|
+
→ Make sure to not guess URLs unless 100% sure — prefer navigating by clicking on available options.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
## Data Lookup Strategy
|
|
2
|
+
|
|
3
|
+
For tasks involving prices, news, weather, facts, rates, or any structured data:
|
|
4
|
+
|
|
5
|
+
1. Think: does a free public HTTP API exist for this? Most financial/data topics have open APIs.
|
|
6
|
+
2. Try fetch_url() on a likely API endpoint FIRST — it returns data in <1s with no CAPTCHA or JS issues.
|
|
7
|
+
3. If you find useful JSON, parse it and call done() immediately.
|
|
8
|
+
4. If no API works, navigate to a site and read get_page_state() — it contains all visible text.
|
|
9
|
+
|
|
10
|
+
fetch_url() uses Chrome TLS fingerprint impersonation (wreq-js) to bypass Cloudflare and bot detection.
|
|
11
|
+
It is always faster and more reliable than browser navigation for pure data retrieval.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
## Monitoring / Ongoing Tasks — CRITICAL RULES
|
|
2
|
+
|
|
3
|
+
Keywords that indicate this type of task: "monitor", "keep checking", "wait for reply", "keep me updated",
|
|
4
|
+
"feel free to engage", "notify when", "let me know when", "keep watching", "ongoing", "until X happens",
|
|
5
|
+
"schedule", "every day", "check hourly", "daily at", "recurring".
|
|
6
|
+
|
|
7
|
+
These tasks NEVER call done() on their own. The user stops them with Ctrl+C.
|
|
8
|
+
|
|
9
|
+
### FIRST RUN — initial session:
|
|
10
|
+
1. Perform the first action (send message, check price, post content, etc.)
|
|
11
|
+
2. IMMEDIATELY call remember() to store key context:
|
|
12
|
+
- remember("thread_url", "<exact URL of the conversation/page>")
|
|
13
|
+
- remember("last_message_sent", "<text of message you sent>")
|
|
14
|
+
- remember("monitoring_target", "<name of person/thing being monitored>")
|
|
15
|
+
3. Call status_update() to confirm what was done
|
|
16
|
+
4. Call schedule() with a sensible cron interval (e.g. every 5 min for messages, every hour for prices)
|
|
17
|
+
5. Do NOT call done() — the process stays alive, re-running at each cron interval
|
|
18
|
+
|
|
19
|
+
### SCHEDULED CHECK-IN — sub-run spawned by cron:
|
|
20
|
+
1. Check memory for thread_url — navigate DIRECTLY there (do NOT start from homepage)
|
|
21
|
+
2. Check if already logged in by reading get_page_state(). If the URL is the target page, you ARE logged in.
|
|
22
|
+
3. Only log in if the page is a login form. Use list_credential_profiles() to find the right profile.
|
|
23
|
+
4. After navigating to the thread, read get_page_state() to find the latest state
|
|
24
|
+
5. Compare with memory — look for NEW messages, changes, or triggers
|
|
25
|
+
6. If there is a new event: respond/act naturally, then update remember() with the latest state
|
|
26
|
+
7. If nothing new: call status_update("No change yet. Checking again later.")
|
|
27
|
+
8. Call done() — the cron will re-run automatically. Do NOT call schedule() again.
|
|
28
|
+
|
|
29
|
+
### Example — "send a message and monitor for reply":
|
|
30
|
+
FIRST RUN:
|
|
31
|
+
→ Send message
|
|
32
|
+
→ remember("thread_url", "https://www.linkedin.com/messaging/thread/...")
|
|
33
|
+
→ remember("last_message_sent", "Hello! 👋 How are you doing?")
|
|
34
|
+
→ status_update("✉️ Message sent. Monitoring for reply every 5 minutes.")
|
|
35
|
+
→ schedule("*/5 * * * *", "Check for reply and respond if received")
|
|
36
|
+
[do NOT call done()]
|
|
37
|
+
|
|
38
|
+
SCHEDULED CHECK-IN:
|
|
39
|
+
→ recall("thread_url") → navigate directly to that URL
|
|
40
|
+
→ get_page_state() → find latest message
|
|
41
|
+
→ if target replied: respond, remember("last_message_sent", ...)
|
|
42
|
+
→ status_update("✅ Target replied: '...' — responded with '...'" OR "No new reply yet.")
|
|
43
|
+
→ done() [cron handles the next run]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
## Performance Audit Rules
|
|
2
|
+
|
|
3
|
+
perf_audit(url) automatically navigates to the page — do NOT call navigate() before it.
|
|
4
|
+
|
|
5
|
+
It collects:
|
|
6
|
+
- Scores (Performance/Accessibility/SEO/Best Practices 0-100)
|
|
7
|
+
- Real-user metrics: FCP, LCP, CLS, TTFB
|
|
8
|
+
- Framework detection and re-render analysis with simulated interactions
|
|
9
|
+
- Network analysis: totalRequests, totalKB, jsKB, apiCalls, slowApiCalls (>500ms), failedApiCalls,
|
|
10
|
+
longTasks, totalBlockingMs, memoryMB, slowApis list, heaviestResources list
|
|
11
|
+
|
|
12
|
+
You can call perf_audit() multiple times with different URLs to compare pages — the report shows a side-by-side comparison table automatically.
|
|
13
|
+
|
|
14
|
+
If the user asks to audit multiple pages on a domain, also audit the root/home page unless they explicitly say to skip it.
|
|
15
|
+
|
|
16
|
+
When summarising results, use neutral section labels:
|
|
17
|
+
- Use: "Performance Scores", "Real User Metrics", "Lab Metrics", "Framework & Re-renders", "Interaction Tests", "Network & Runtime"
|
|
18
|
+
- Never write: "Lighthouse", "React Scan", or any specific vendor tool name
|
package/dist/task/runner.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"fs";import t from"path";import{generateText as s}from"ai";import n from"node-cron";import{createSession as r}from"wreq-js";import{BrowserAgent as o}from"../browser/agent.js";import{loadConfig as a,loadCredentials as i}from"../config/loader.js";import{getModel as l}from"../ai/interpreter.js";import{taskTools as c}from"./tools.js";import{createSession as u,loadSession as h,saveSessionMeta as d,appendEvent as p,updateSessionStatus as m}from"./session.js";const g=400,f='You are Slapify Task Agent — a fully autonomous web agent. You decide the best approach for any task yourself.\n\n## Tools\n- **fetch_url(url)** — Direct HTTP GET, bypasses browser. No CAPTCHA. Instant. Use for APIs and data.\n- **navigate(url)** + **get_page_state()** — Browser navigation. get_page_state() returns all visible text and interactive refs.\n- **click(ref)**, **type(ref, text)**, **press(key)**, **scroll(direction)**, **wait(seconds)** — Browser interaction.\n- **list_credential_profiles()**, **inject_credentials(profile)**, **fill_login_form(profile)** — Authentication.\n- **remember(key, value)**, **recall(key)**, **list_memories()** — Persistent memory.\n- **schedule(cron, task)**, **sleep_until(datetime)** — Time-based control.\n- **perf_audit(url)** — Full performance audit for a URL. Automatically navigates to the page, then collects: scores (Performance/Accessibility/SEO/Best Practices 0-100), real-user metrics (FCP, LCP, CLS, TTFB), framework detection, re-render analysis with simulated interactions, and network analysis (resource sizes, API calls, long tasks).\n You can call perf_audit multiple times with different URLs to compare pages — the report will show a side-by-side comparison table automatically.\n Do NOT call navigate() before perf_audit — it handles navigation itself.\n If the user asks to audit multiple pages on a domain (e.g. "check pricing and about on vercel.com"), also audit the root/home page (e.g. https://vercel.com/) unless they explicitly say to skip it.\n When summarising results, use neutral section labels. Never write "Lighthouse", "React Scan", or any vendor tool name. Use: "Performance Scores", "Real User Metrics", "Lab Metrics", "Framework & Re-renders", "Interaction Tests", "Network & Runtime".\n The result includes a "network" field with: totalRequests, totalKB, jsKB, apiCalls count, slowApiCalls (>500ms), failedApiCalls, longTasks count, totalBlockingMs, memoryMB, slowApis list, and heaviestResources list. Include these in your summary.\n- **done(summary)** — Signal task complete with full results.\n\n## How to approach any task\n\n**Step 1 — Plan before acting.** Decide: is this a data lookup, an interactive task, or an authenticated task?\n\n**Data lookup** (prices, news, weather, facts, rates):\n Think: does a free public HTTP API exist for this? Most financial/data topics have open APIs.\n Try fetch_url() on a likely API endpoint first — it returns data in <1s with no CAPTCHA or JS rendering issues.\n If you find useful JSON, parse it and call done() immediately.\n If no API works, navigate to a site and read get_page_state() — it contains all visible text.\n\n**Interactive task** (filling forms, clicking buttons, posting content):\n Use the browser. Navigate → get_page_state() → interact using ref IDs from the snapshot.\n\n**Authenticated task** (anything requiring login):\n 1. Check memory for a saved thread_url or page_url — navigate directly there first.\n 2. Call get_page_state() — if the URL is the target site (not a login page), you are already logged in. Proceed.\n 3. Only if you see a login form: call list_credential_profiles() and use the best matching profile.\n This avoids unnecessary re-login on every scheduled check-in.\n\n**Monitoring / ongoing task** — CRITICAL RULE:\n Keywords: "monitor", "keep checking", "wait for reply", "keep me updated", "feel free to engage",\n "notify when", "let me know when", "keep watching", "ongoing", "until X happens"\n\n These tasks NEVER call done() on their own. The user stops them with Ctrl+C.\n Correct flow (FIRST RUN — initial session):\n 1. Perform the first action (send message, check price, etc.)\n 2. IMMEDIATELY call remember() to store key context:\n - remember("thread_url", "<exact URL of the conversation/page>")\n - remember("last_message_sent", "<text of message you sent>")\n - remember("monitoring_target", "<name of person/thing being monitored>")\n 3. Call status_update() to confirm what was done\n 4. Call schedule() with a sensible cron interval (e.g. every 5 min for messages, every hour for prices)\n 5. Do NOT call done() — the process stays alive, re-running at each cron interval\n\n Correct flow (SCHEDULED CHECK-IN — sub-run spawned by cron):\n 1. Check memory for thread_url — navigate DIRECTLY there (do NOT start from homepage)\n 2. Check if you are already logged in by reading get_page_state(). If the URL is the target page, you ARE logged in.\n 3. Only log in if the page is a login form. Use list_credential_profiles() to find the right profile.\n 4. After navigating to the thread, read get_page_state() to find the latest messages\n 5. Compare with last_message_sent in memory — look for NEW messages from the other person\n 6. If there is a new message: respond naturally, then update remember("last_message_sent", ...) with your reply\n 7. If no new message: call status_update("No new reply from <person> yet. Checking again later.")\n 8. Call done() — the cron will re-run automatically. Do NOT call schedule() again.\n\n Example — "send a message and monitor for reply":\n FIRST RUN:\n → Send message\n → remember("thread_url", "https://www.linkedin.com/messaging/thread/...")\n → remember("last_message_sent", "Hello Payal! 👋 How are you doing?")\n → status_update("✉️ Message sent. Monitoring for reply every 5 minutes.")\n → schedule("*/5 * * * *", "Check LinkedIn messages from Payal Sahu and respond if she replied")\n [do NOT call done()]\n\n SCHEDULED CHECK-IN:\n → recall("thread_url") → navigate directly to that URL\n → get_page_state() → find latest message in the thread\n → if Target replied: type and send a response, remember("last_message_sent", ...)\n → status_update("✅ Target replied: \'...\' — responded with \'...\'" OR "No new reply yet.")\n → done() [cron handles the next run]\n\n**Recurring task** ("every day", "check hourly", "daily at 9am"):\n Execute once, then call schedule() with the cron expression you choose. Don\'t call done().\n\n## Handling obstacles — figure it out\n\n**CAPTCHA in browser:**\n → First try fetch_url() on the same URL or a different source — direct HTTP never triggers CAPTCHA.\n → If you must solve it in the browser: call get_page_state() to inspect the CAPTCHA. Look for an iframe or checkbox element. For reCAPTCHA v2, find and click the "I\'m not a robot" checkbox ref. For image CAPTCHAs, look for the audio challenge link.\n → If one site gives CAPTCHA, try a completely different site with the same data.\n\n**"Just a moment..." / Cloudflare:**\n → Bot protection. Switch to fetch_url() on that URL, or find a different site entirely.\n\n**Empty page snapshot / "no interactive elements":**\n → JS-rendered page. Call wait(3) then get_page_state() again.\n → Still empty? Try fetch_url() on the same URL — the raw HTML often has the data even when browser rendering fails.\n\n**API returns error / bad format:**\n → Try a different endpoint. Think what other public data sources exist for this topic.\n\n**Stuck after multiple attempts:**\n → Change strategy completely. If browser isn\'t working, use fetch_url(). If one site fails, try another.\n → Never repeat the same failing action more than twice.\n -> Make sure to not guess URLs unless you are 100% sure about it, prefer navigating by clicking on available options.\n\n## Batching tool calls — reduce round trips\n\nYou can return **multiple tool calls in a single response** when they don\'t depend on each other\'s output.\nThis is faster — all calls in one response execute in parallel before the next LLM turn.\n\n**Good batching examples:**\n- After a snapshot you already have refs → batch: type(emailRef, ...) + type(passwordRef, ...) + click(submitRef)\n- Memory + notification → batch: remember(key, val) + status_update(msg)\n- Click then wait → batch: click(ref) + wait(2) (wait does not need click result)\n- Multiple remembers → batch: remember(k1, v1) + remember(k2, v2) + remember(k3, v3)\n\n**Do NOT batch when the second call needs the first call\'s output:**\n- navigate + click → you need get_page_state() in between to learn the ref\n- get_page_state + click → click ref comes FROM get_page_state result\n\n**Login form shortcut** — once you have refs from a snapshot, fill the whole form in ONE response:\n type(emailRef, email) + type(passwordRef, password) + click(submitRef)\n Then in the NEXT response: wait(3) + get_page_state() to verify.\n\n## Reading data\n\n- get_page_state() snapshot contains ALL visible text: prices, numbers, paragraphs, labels. Read it carefully before giving up.\n- Do NOT use screenshot() for data extraction — you cannot see images.\n- Always call get_page_state() after every navigate().\n\n## Human in the loop\n\nUse **ask_user(question, hint?)** when you genuinely need information not available elsewhere:\n- A one-time password (OTP) or 2FA code\n- A missing password or PIN that isn\'t in the credential store\n- Clarification about what the user wants when the goal is ambiguous\n- Confirmation before taking a destructive or irreversible action\n\nKeep questions concise. Use the hint field to tell them where to find the answer (e.g. "Check your authenticator app").\n\n**MANDATORY after every successful login:**\n1. Call save_credentials with capture_from_browser: true immediately after you verify the login worked.\n Use a sensible profile_name (e.g. "linkedin", "gmail", "twitter").\n This saves the session cookies so they can be reused next time without logging in again.\n2. Then call status_update("✅ Logged in as [username]. Session saved as \'[profile_name]\' for future use.")\n\nDo not ask the user whether to save — just save automatically. If the site was already logged in via injected credentials, skip this step.\n\n## Keeping the user informed\n\nUse **status_update(message)** to post visible updates whenever something meaningful happens:\n- When starting a scheduled check: "⏰ Running scheduled gold price check..."\n- When you find data: "📊 Found gold price: $4,986/oz"\n- When retrying or switching approach: "🔄 Switching to Yahoo Finance..."\n- When sleeping: "😴 Waiting 30 minutes before next check. Last price: $4,986/oz"\n- For recurring tasks: post a status_update at the start and end of each run\n\nDo NOT use status_update for every small step — only for things the user would actually want to see.\n\n## Completion rules\n- Use remember() the moment you find important data, before calling done().\n- Call done() with a complete, specific summary including exact data found.\n- Never give up without trying at least 4-5 different approaches.\n- Never ask the user for help — figure it out.\n- **NEVER call done() if the task involves monitoring, waiting for replies, or ongoing engagement.**\n Those tasks end only when the user presses Ctrl+C. Use schedule() instead.\n';class y{browser;session;credentials;emit;onHumanInput;credentialsFilePath;isScheduledRun;schema;outputFile;wreqSession=null;constructor(e,t,s,n,r,o,a=!1,i,l){this.browser=e,this.session=t,this.credentials=s,this.emit=n,this.onHumanInput=r,this.credentialsFilePath=o,this.isScheduledRun=a,this.schema=i,this.outputFile=l}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=[],n=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 n)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 o=await this.browser.getState(),a=o.snapshot?.toLowerCase().includes("captcha")||o.snapshot?.toLowerCase().includes("not a robot")||o.url?.includes("sorry");return{attempted:t.length>0,solved:t,failed:s,captchaStillPresent:a,currentUrl:o.url,hint:a?"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(),n=await s.fetch(e,{headers:{Accept:"application/json, text/html, */*","Accept-Language":"en-US,en;q=0.9",...t}}),o=n.headers.get("content-type")||"",a=await n.text();let i=a;if(o.includes("application/json"))try{i=JSON.parse(a)}catch{i=a}const l="string"==typeof i?i:JSON.stringify(i);return{ok:n.ok,status:n.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,d(this.session),p(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,n=r.hint;this.emit({type:"human_input_needed",question:s,hint:n});const o=await this.onHumanInput(s,n);p(this.session.id,{type:"tool_call",toolName:"ask_user",args:{question:s,hint:n},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(o)&&o.includes(" ")){const s=o.split(/\s+(?:and\s+)?(?:password\s+is\s+|pass(?:word)?[:\s]+)?/i),n=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 a=await this.onHumanInput("💾 Save these credentials for future sessions?","Enter a profile name (e.g. 'linkedin', 'gmail') or leave blank to skip");if(a&&a.trim()){const s=a.trim().toLowerCase().replace(/\s+/g,"-"),o={type:"login-form",...n&&{username:n},...r&&{password:r}};try{const n=(await import("yaml")).default;let r={profiles:{}};if(e.existsSync(this.credentialsFilePath))try{const t=n.parse(e.readFileSync(this.credentialsFilePath,"utf-8"));t?.profiles&&(r=t)}catch{}r.profiles[s]=o,e.mkdirSync(t.dirname(this.credentialsFilePath),{recursive:!0}),e.writeFileSync(this.credentialsFilePath,n.stringify(r,{indent:2,lineWidth:0})),this.credentials[s]=o,this.emit({type:"credentials_saved",profileName:s,credType:"login-form"})}catch{}}}return{answer:o}}case"save_credentials":{const s=r.profile_name,n=r.type,o=r.capture_from_browser,a={type:n};if("login-form"===n&&(r.username&&(a.username=r.username),r.password&&(a.password=r.password)),"inject"===n&&o)try{const e=await this.browser.getCookies(),t=await this.browser.getLocalStorage(),s=await this.browser.getSessionStorage();e.length>0&&(a.cookies=e.map(e=>({name:e.name,value:e.value})));const n=e=>{if(!e||"object"!=typeof e||Array.isArray(e))return{};const t={};for(const[s,n]of Object.entries(e))t[String(s)]="string"==typeof n?n:JSON.stringify(n);return t},r=n(t),o=n(s);Object.keys(r).length>0&&(a.localStorage=r),Object.keys(o).length>0&&(a.sessionStorage=o)}catch(e){return{ok:!1,error:`Failed to capture browser state: ${e.message}`}}try{const r=(await import("yaml")).default;let o={profiles:{}};if(e.existsSync(this.credentialsFilePath))try{const t=r.parse(e.readFileSync(this.credentialsFilePath,"utf-8"));t?.profiles&&(o=t)}catch{}return o.profiles[s]=a,e.mkdirSync(t.dirname(this.credentialsFilePath),{recursive:!0}),e.writeFileSync(this.credentialsFilePath,r.stringify(o,{indent:2,lineWidth:0})),this.credentials[s]=a,this.emit({type:"credentials_saved",profileName:s,credType:n}),{ok:!0,message:`Saved profile '${s}' (${n}) to credentials.yaml`,cookieCount:a.cookies?.length??0,localStorageKeys:Object.keys(a.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:n}=await import("../perf/audit.js"),r=await n(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,d(this.session);const o=r.scores??r.lighthouse,a=r.network,i={url:r.url,vitals:r.vitals,scores:o,react:r.react,network:a?{totalRequests:a.totalRequests,totalKB:Math.round((a.totalBytes||0)/1024),jsKB:Math.round((a.jsBytes||0)/1024),apiCalls:a.apiCalls.length,slowApiCalls:a.slowApiCalls.length,failedApiCalls:a.failedApiCalls.length,longTasks:a.longTasks.length,totalBlockingMs:a.totalBlockingMs,memoryMB:a.memoryMB,slowApis:a.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:a.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}`),o&&l.push(`Scores — Perf ${o.performance}/100 · A11y ${o.accessibility}/100 · SEO ${o.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 a&&l.push(`Network: ${a.totalRequests} requests · ${Math.round((a.totalBytes||0)/1024)}KB total · JS ${Math.round((a.jsBytes||0)/1024)}KB · ${a.apiCalls.length} API calls${a.slowApiCalls.length?` (${a.slowApiCalls.length} slow)`:""}${a.failedApiCalls.length?` (${a.failedApiCalls.length} failed)`:""} · ${a.longTasks.length} long tasks (${a.totalBlockingMs}ms)`),this.emit({type:"status_update",message:l.join(" · ")}),i}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."}:n.validate(e)?(this.session.scheduledJobs.push({id:`job-${Date.now()}`,cron:e,taskDescription:t,createdAt:(new Date).toISOString()}),d(this.session),p(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(),n=[[/^(\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 n){const n=s.match(e);if(n)return Math.round(parseFloat(n[1])*t)}if(s.includes("tomorrow")){const e=new Date,t=new Date(e);t.setDate(t.getDate()+1);const n=s.match(/(\d{1,2})(?::(\d{2}))?\s*(am|pm)?/);if(n){let e=parseInt(n[1]);const s=parseInt(n[2]||"0");"pm"===n[3]&&e<12&&(e+=12),"am"===n[3]&&12===e&&(e=0),t.setHours(e,s,0,0)}return Math.max(0,t.getTime()-Date.now())}return 6e4}(e),n=new Date(Date.now()+s).toISOString();return p(this.session.id,{type:"sleeping_until",until:n,ts:(new Date).toISOString()}),this.emit({type:"sleeping",until:n}),m(this.session,"sleeping"),await new Promise(e=>setTimeout(e,s)),m(this.session,"running"),{ok:!0,sleptUntil:n,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,n=r.mode||"append";return this.session.structuredOutput=function(s,n,r,o){let a;if("overwrite"===n||null==r)a=s;else if(Array.isArray(r))a=Array.isArray(s)?[...r,...s]:[...r,s];else if("object"==typeof r){const e={...r};for(const[t,n]of Object.entries(s))Array.isArray(e[t])&&Array.isArray(n)?e[t]=[...e[t],...n]:e[t]=n;a=e}else a=s;if(o)try{const s=t.dirname(t.resolve(o));e.existsSync(s)||e.mkdirSync(s,{recursive:!0}),e.writeFileSync(o,JSON.stringify(a,null,2)+"\n","utf8")}catch{}return a}(s,n,this.session.structuredOutput,this.outputFile),d(this.session),this.outputFile&&this.emit({type:"output_written",path:this.outputFile,data:s}),{ok:!0,written:s}}case"done":return{ok:!0};default: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{}}}async function w(e,t,n){const r=e.slice(0,e.length-20),o=e.slice(e.length-20);if(0===r.length)return e;try{const{text:a}=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 p(n,{type:"context_compacted",fromMessages:e.length,toMessages:1+o.length,ts:(new Date).toISOString()}),[{role:"user",content:`[Session history summary]\n${a}`},...o]}catch{return o}}const k=new Set(["get_page_state","screenshot","wait","scroll","recall","list_memories","list_credential_profiles","go_back","reload","fetch_url","solve_captcha","status_update","ask_user","save_credentials"]);class _{recentActions=[];WINDOW=20;THRESHOLD=5;record(e,t){if(k.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:k,headed:v,executablePath:S,saveFlow:C,flowOutputDir:A,schema:$,outputFile:T,maxIterations:I=g,onEvent:O,onSessionUpdate:N,isScheduledRun:P=!1,inheritedMemory:x}=e,j=e=>O?.(e),D=a(),R=l(D.llm);let L={};try{L=i().profiles||{}}catch{}const F=new o({headless:!0!==v&&(!1===v||(D.browser?.headless??!0)),timeout:D.browser?.timeout,viewport:D.browser?.viewport,executablePath:S||D.browser?.executablePath});let E,M;if(k){const e=h(k);if(!e)throw new Error(`Session '${k}' not found.`);E=e,E.status="running",d(E);const{rebuildMessages:t}=await import("./session.js");M=t((await import("./session.js")).loadEvents(k)),j({type:"message",text:`Resuming session ${k} (iteration ${E.iteration})`})}else E=u(r),x&&Object.keys(x).length>0&&(Object.assign(E.memory,x),d(E)),M=[{role:"user",content:r}],p(E.id,{type:"session_start",goal:r,ts:(new Date).toISOString()}),j({type:"message",text:`Session ${E.id} started`});N?.(E);let U=t.join(process.cwd(),".slapify","credentials.yaml");try{const{getConfigDir:e}=await import("../config/loader.js"),s=e();s&&(U=t.join(s,"credentials.yaml"))}catch{}const H=e.onHumanInput??(async(e,t)=>{const s=(await import("readline")).createInterface({input:process.stdin,output:process.stdout});return new Promise(n=>{const r=t?`\n ${e}\n (${t})\n > `:`\n ${e}\n > `;s.question(r,e=>{s.close(),n(e.trim())})})}),q=new y(F,E,L,j,H,U,P,$,T),B=new _;if(Object.keys(E.memory).length>0){const e=Object.entries(E.memory).map(([e,t])=>`- ${e}: ${t}`).join("\n");let t;if(P){const s=E.memory.thread_url||E.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}`;M.unshift({role:"user",content:t})}else P&&M.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 W=!1,J="";try{for(;!W&&E.iteration<I;){E.iteration++,d(E),N?.(E),p(E.id,{type:"iteration_start",iteration:E.iteration,ts:(new Date).toISOString()}),M.length>60&&(j({type:"message",text:"Compacting context..."}),M=await w(M,R,E.id)),j({type:"thinking"});const e=$?f+`\n\n## Structured Output Schema\nThe user expects output that conforms to this JSON schema:\n\`\`\`json\n${JSON.stringify($,null,2)}\n\`\`\`\nUse the **write_output** tool to write conforming data whenever you have results to record (after each scheduled run, after collecting data, or before calling done). For array schemas, each write_output call appends new entries. For object schemas, each call updates the object. Always call write_output before done() when a schema is provided.`:f,t=await s({model:R,system:e,messages:M,tools:c}),n=(t.toolCalls||[]).map(e=>({toolCallId:e.toolCallId,toolName:e.toolName,args:e.args}));p(E.id,{type:"llm_response",text:t.text||"",toolCalls:n,ts:(new Date).toISOString()}),t.text&&j({type:"message",text:t.text});const o=[];t.text&&o.push({type:"text",text:t.text});for(const e of t.toolCalls||[])o.push({type:"tool-call",toolCallId:e.toolCallId,toolName:e.toolName,args:e.args});if(o.length>0&&M.push({role:"assistant",content:o}),!t.toolCalls||0===t.toolCalls.length){if("stop"===t.finishReason){J=t.text||"Task complete.",W=!0;break}continue}const a=[];for(const e of t.toolCalls){const t=e.toolName,s=e.args;if("done"===t){if(J=s.summary||"Task complete.",W=!0,s.save_flow||C){const e=await b(E,r,A);E.savedFlowPath=e,j({type:"flow_saved",path:e})}a.push({type:"tool-result",toolCallId:e.toolCallId,toolName:t,result:JSON.stringify({ok:!0})}),p(E.id,{type:"tool_call",toolName:t,args:s,result:{ok:!0},ts:(new Date).toISOString()});break}if(B.record(t,s),B.isLooping()){j({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 n;j({type:"tool_start",toolName:t,args:s});try{n=await q.execute(t,s),p(E.id,{type:"tool_call",toolName:t,args:s,result:n,ts:(new Date).toISOString()});const r="string"==typeof n?n:JSON.stringify(n);j({type:"tool_done",toolName:t,result:r.slice(0,200)}),a.push({type:"tool-result",toolCallId:e.toolCallId,toolName:t,result:r})}catch(n){const r=n?.message||String(n);p(E.id,{type:"tool_error",toolName:t,args:s,error:r,ts:(new Date).toISOString()}),j({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&&M.push({role:"tool",content:a})}E.iteration>=I&&!W?(J=`Task hit the maximum iteration limit (${I}) without completing.`,m(E,"failed")):W&&(E.scheduledJobs.length>0?await async function(e,t,s,r,o){m(e,"scheduled");for(const s of e.scheduledJobs)o({type:"message",text:`Registering cron: ${s.cron} — ${s.taskDescription}`}),n.schedule(s.cron,async()=>{const n=(new Date).toISOString();s.lastRun=n,d(e),o({type:"message",text:`[cron ${s.cron}] Running: ${s.taskDescription}`});const r={...e.memory},a=r.thread_url||r.conversation_url,i=a?`${s.taskDescription}\n\n[Use thread_url from memory: ${a}]`:s.taskDescription;try{await runTask({...t,goal:i,sessionId:void 0,isScheduledRun:!0,inheritedMemory:r})}catch(e){o({type:"error",error:`Cron job failed: ${e?.message}`})}});o({type:"message",text:`${e.scheduledJobs.length} cron job(s) active. Process will stay alive. Press Ctrl+C to stop.`}),await new Promise(()=>{})}(E,e,0,0,j):m(E,"completed")),E.finalSummary=J,d(E),p(E.id,{type:"session_end",summary:J,status:E.status,ts:(new Date).toISOString()}),j({type:"done",summary:J})}catch(e){const t=e?.message||String(e);throw m(E,"failed"),E.finalSummary=`Error: ${t}`,d(E),j({type:"error",error:t}),e}finally{try{F.close()}catch{}try{await q.closeWreqSession()}catch{}}return E}async function b(s,n,r){const o=[`# Generated from task: ${n}`,`# Session: ${s.id}`,`# Generated: ${(new Date).toISOString()}`,""],{loadEvents:a}=await import("./session.js"),i=a(s.id);for(const e of i)if("tool_call"===e.type){const t=v(e.toolName,e.args);t&&o.push(t)}const l=n.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,o.join("\n")+"\n"),u}function v(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{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}}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,13 +1,37 @@
|
|
|
1
1
|
export interface SlapifyConfig {
|
|
2
|
-
|
|
2
|
+
/** Single model or an ordered fallback chain (tried in order on failure). */
|
|
3
|
+
llm: LLMConfig | LLMConfig[];
|
|
3
4
|
browser?: BrowserConfig;
|
|
4
5
|
report?: ReportConfig;
|
|
6
|
+
plugins?: PluginsConfig;
|
|
7
|
+
telegram?: TelegramConfig;
|
|
5
8
|
}
|
|
6
9
|
export interface LLMConfig {
|
|
7
10
|
provider: "anthropic" | "openai" | "google" | "mistral" | "groq" | "ollama";
|
|
8
11
|
model: string;
|
|
9
12
|
api_key?: string;
|
|
10
13
|
base_url?: string;
|
|
14
|
+
/** Human-readable label shown in fallback logs, e.g. 'Claude (primary)' */
|
|
15
|
+
label?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface PluginsConfig {
|
|
18
|
+
/**
|
|
19
|
+
* Paths to additional markdown rule files to always append to the system prompt.
|
|
20
|
+
* Supports glob patterns relative to the .slapify directory.
|
|
21
|
+
* Example: ["rules/my-persona.md", "rules/company-style.md"]
|
|
22
|
+
*/
|
|
23
|
+
rules?: string[];
|
|
24
|
+
/**
|
|
25
|
+
* Paths to .js/.ts files exporting tools to merge with the agent's built-in tools.
|
|
26
|
+
* Each file must export an object containing Vercel AI SDK `tool()` definitions.
|
|
27
|
+
* Example: ["tools/custom-utils.js"]
|
|
28
|
+
*/
|
|
29
|
+
tools?: string[];
|
|
30
|
+
}
|
|
31
|
+
export interface TelegramConfig {
|
|
32
|
+
bot_token: string;
|
|
33
|
+
/** Optional: only allow messages from this chat/user ID */
|
|
34
|
+
allowed_chat_id?: string | number;
|
|
11
35
|
}
|
|
12
36
|
export interface BrowserConfig {
|
|
13
37
|
headless?: boolean;
|
|
@@ -17,6 +41,16 @@ export interface BrowserConfig {
|
|
|
17
41
|
height: number;
|
|
18
42
|
};
|
|
19
43
|
executablePath?: string;
|
|
44
|
+
/** Path to a persistent Chromium user-data-dir/profile (enables session saving) */
|
|
45
|
+
profile?: string;
|
|
46
|
+
/** Custom User-Agent string to bypass basic checks */
|
|
47
|
+
userAgent?: string;
|
|
48
|
+
/** Array of Chromium launch args (e.g. ['--disable-blink-features=AutomationControlled']) */
|
|
49
|
+
args?: string[];
|
|
50
|
+
browserbase?: {
|
|
51
|
+
api_key: string;
|
|
52
|
+
project_id?: string;
|
|
53
|
+
};
|
|
20
54
|
}
|
|
21
55
|
export interface ReportConfig {
|
|
22
56
|
format?: "markdown" | "html" | "json";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slapify",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.21",
|
|
4
4
|
"description": "AI-powered browser automation — autonomous task agent, performance auditing, and E2E test flows in plain English",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -11,10 +11,11 @@
|
|
|
11
11
|
"files": [
|
|
12
12
|
"dist/**/*.js",
|
|
13
13
|
"dist/**/*.d.ts",
|
|
14
|
+
"dist/**/*.md",
|
|
14
15
|
"bin"
|
|
15
16
|
],
|
|
16
17
|
"scripts": {
|
|
17
|
-
"build": "tsc",
|
|
18
|
+
"build": "tsc && cp -r src/task/rules dist/task/rules",
|
|
18
19
|
"dev": "tsc --watch",
|
|
19
20
|
"start": "node dist/index.js",
|
|
20
21
|
"lint": "tsc --noEmit",
|
|
@@ -51,7 +52,7 @@
|
|
|
51
52
|
"@ai-sdk/mistral": "^1.0.0",
|
|
52
53
|
"@ai-sdk/openai": "^0.0.30",
|
|
53
54
|
"@types/node-cron": "^3.0.11",
|
|
54
|
-
"agent-browser": "^0.9.
|
|
55
|
+
"agent-browser": "^0.9.4",
|
|
55
56
|
"ai": "^3.0.0",
|
|
56
57
|
"chalk": "^5.3.0",
|
|
57
58
|
"commander": "^12.0.0",
|